Last active
March 22, 2025 14:09
-
-
Save roflsunriz/4c71999062b61bb42d98712a60eaa1ae to your computer and use it in GitHub Desktop.
filter_matome : 「フィルタまとめ」パッケージ更新ツール
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tkinter as tk | |
from tkinter import ttk, filedialog, messagebox | |
import json | |
import os | |
import sys | |
import subprocess | |
import pkg_resources | |
import shutil | |
import py7zr | |
from typing import Dict, List | |
from datetime import datetime | |
class FilterMatomeApp: | |
def __init__(self, root: tk.Tk): | |
# 設定ファイルの初期化 | |
self.config_dir = os.path.join("misc", "config") | |
os.makedirs(self.config_dir, exist_ok=True) # フォルダがなければ作成 | |
self.config_file = os.path.join(self.config_dir, "filter_matome_config.json") | |
self.default_config = { | |
"source_dir": r"C:\NicoCache_nl", | |
"dest_dir": r"C:\NicoCache_nl other", | |
"selected_files": [], | |
"exclude_patterns": "desktop.ini, thumbs.db, .ds_store, $recycle.bin, system volume information" | |
} | |
self.current_config = self.load_config() | |
# GUIの初期化 | |
self.root = root | |
self.root.title("フィルタまとめパッケージ更新ツール") | |
# メインフレーム | |
self.main_frame = ttk.Frame(self.root, padding="10") | |
self.main_frame.grid(row=0, column=0, sticky="nsew") | |
# GUI要素の作成(ステータスバーを最初に作成) | |
self._create_status_bar() | |
self._create_source_dir_widgets() | |
self._create_dest_dir_widgets() | |
self._create_exclude_widgets() | |
self._create_file_selection_widgets() | |
self._create_progress_widgets() | |
self._create_control_buttons() | |
# ウィンドウのリサイズ設定 | |
self.root.columnconfigure(0, weight=1) | |
self.root.rowconfigure(0, weight=1) | |
self.main_frame.columnconfigure(1, weight=1) | |
def load_config(self) -> Dict: | |
"""設定ファイルを読み込む""" | |
if os.path.exists(self.config_file): | |
try: | |
with open(self.config_file, 'r', encoding='utf-8') as f: | |
loaded_config = json.load(f) | |
# デフォルト値で設定を補完 | |
for key, value in self.default_config.items(): | |
if key not in loaded_config: | |
loaded_config[key] = value | |
return loaded_config | |
except json.JSONDecodeError: | |
return self.default_config.copy() | |
return self.default_config.copy() | |
def save_config(self) -> None: | |
"""現在の設定を保存する""" | |
with open(self.config_file, 'w', encoding='utf-8') as f: | |
json.dump(self.current_config, f, indent=2, ensure_ascii=False) | |
def _create_source_dir_widgets(self): | |
"""ソースディレクトリ関連のウィジェットを作成""" | |
ttk.Label(self.main_frame, text="ソースディレクトリ:").grid(row=0, column=0, sticky="w") | |
self.source_dir_var = tk.StringVar(value=self.current_config["source_dir"]) | |
self.source_entry = ttk.Entry(self.main_frame, textvariable=self.source_dir_var, width=50) | |
self.source_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") | |
ttk.Button(self.main_frame, text="参照", command=self._browse_source_dir).grid(row=0, column=2) | |
def _create_dest_dir_widgets(self): | |
"""出力先ディレクトリ関連のウィジェットを作成""" | |
ttk.Label(self.main_frame, text="出力先ディレクトリ:").grid(row=1, column=0, sticky="w") | |
self.dest_dir_var = tk.StringVar(value=self.current_config["dest_dir"]) | |
self.dest_entry = ttk.Entry(self.main_frame, textvariable=self.dest_dir_var, width=50) | |
self.dest_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew") | |
ttk.Button(self.main_frame, text="参照", command=self._browse_dest_dir).grid(row=1, column=2) | |
def _create_exclude_widgets(self): | |
"""除外パターン関連のウィジェットを作成""" | |
exclude_frame = ttk.Frame(self.main_frame) | |
exclude_frame.grid(row=2, column=0, columnspan=3, sticky="ew", padx=5, pady=5) | |
ttk.Label(exclude_frame, text="除外ファイル名(カンマ区切り):").pack(side=tk.LEFT) | |
self.exclude_var = tk.StringVar(value=self.current_config["exclude_patterns"]) | |
self.exclude_entry = ttk.Entry(exclude_frame, textvariable=self.exclude_var, width=50) | |
self.exclude_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) | |
def _create_file_selection_widgets(self): | |
"""ファイル選択関連のウィジェットを作成""" | |
ttk.Label(self.main_frame, text="選択されたファイル/フォルダ:").grid(row=3, column=0, sticky="w") | |
self.file_listbox = tk.Listbox(self.main_frame, width=50, height=10) | |
self.file_listbox.grid(row=4, column=0, columnspan=2, padx=5, pady=5, sticky="nsew") | |
scrollbar = ttk.Scrollbar(self.main_frame, orient=tk.VERTICAL, command=self.file_listbox.yview) | |
scrollbar.grid(row=4, column=2, sticky="ns") | |
self.file_listbox.configure(yscrollcommand=scrollbar.set) | |
button_frame = ttk.Frame(self.main_frame) | |
button_frame.grid(row=5, column=0, columnspan=3, pady=5) | |
ttk.Button(button_frame, text="ファイル選択", command=self._select_files).pack(side=tk.LEFT, padx=5) | |
ttk.Button(button_frame, text="フォルダ選択", command=self._select_folders).pack(side=tk.LEFT, padx=5) | |
ttk.Button(button_frame, text="選択解除", command=self._clear_selection).pack(side=tk.LEFT, padx=5) | |
# 保存された選択ファイルを読み込む | |
self._load_saved_files() | |
def _load_saved_files(self): | |
"""保存された選択ファイルをリストボックスに読み込む""" | |
saved_files = self.current_config.get("selected_files", []) | |
if saved_files: | |
self.file_listbox.delete(0, tk.END) | |
valid_files = [] | |
for file_path in saved_files: | |
# ファイルが存在する場合のみ追加 | |
if os.path.exists(file_path): | |
valid_files.append(file_path) | |
self.file_listbox.insert(tk.END, file_path) | |
# 存在するファイルのみを設定に保存 | |
if len(valid_files) != len(saved_files): | |
self.current_config["selected_files"] = valid_files | |
self.save_config() | |
if valid_files: | |
self.status_var.set(f"合計: {len(valid_files)}個のファイルが選択されています") | |
else: | |
self.status_var.set("選択されたファイルは見つかりませんでした") | |
def _create_progress_widgets(self): | |
"""進捗バー関連のウィジェットを作成""" | |
self.progress_var = tk.DoubleVar() | |
self.progress_bar = ttk.Progressbar( | |
self.main_frame, | |
variable=self.progress_var, | |
maximum=100 | |
) | |
self.progress_bar.grid(row=6, column=0, columnspan=3, sticky="ew", padx=5, pady=5) | |
def _create_control_buttons(self): | |
"""制御ボタンを作成""" | |
button_frame = ttk.Frame(self.main_frame) | |
button_frame.grid(row=7, column=0, columnspan=3, pady=10) | |
ttk.Button(button_frame, text="設定保存", command=self._save_settings).pack(side=tk.LEFT, padx=5) | |
ttk.Button(button_frame, text="実行", command=self._execute).pack(side=tk.LEFT, padx=5) | |
ttk.Button(button_frame, text="終了", command=self.root.quit).pack(side=tk.LEFT, padx=5) | |
def _create_status_bar(self): | |
"""ステータスバーを作成""" | |
self.status_var = tk.StringVar() | |
self.status_bar = ttk.Label(self.main_frame, textvariable=self.status_var, relief=tk.SUNKEN) | |
self.status_bar.grid(row=8, column=0, columnspan=3, sticky="ew") | |
self.status_var.set("準備完了") | |
def _browse_source_dir(self): | |
"""ソースディレクトリを選択""" | |
dir_path = filedialog.askdirectory(initialdir=self.source_dir_var.get()) | |
if dir_path: | |
self.source_dir_var.set(dir_path) | |
self.current_config["source_dir"] = dir_path | |
self.save_config() | |
def _browse_dest_dir(self): | |
"""出力先ディレクトリを選択""" | |
dir_path = filedialog.askdirectory(initialdir=self.dest_dir_var.get()) | |
if dir_path: | |
self.dest_dir_var.set(dir_path) | |
self.current_config["dest_dir"] = dir_path | |
self.save_config() | |
def _is_system_file(self, file_path: str) -> bool: | |
"""システムファイルかどうかを判定""" | |
try: | |
# Windowsのシステム属性を確認 | |
import stat | |
if bool(os.stat(file_path).st_file_attributes & stat.FILE_ATTRIBUTE_SYSTEM): | |
return True | |
except: | |
pass | |
# ユーザー定義の除外パターンをチェック | |
exclude_patterns = [p.strip().lower() for p in self.exclude_var.get().split(',')] | |
file_name = os.path.basename(file_path).lower() | |
return any(pattern in file_name for pattern in exclude_patterns) | |
def _select_folders(self): | |
"""フォルダを選択""" | |
folders = filedialog.askdirectory( | |
initialdir=self.source_dir_var.get(), | |
title="コピーするフォルダを選択" | |
) | |
if folders: | |
# 既存の選択を保持 | |
existing_items = list(self.file_listbox.get(0, tk.END)) | |
selected_items = existing_items.copy() | |
# フォルダ内のすべてのファイルとフォルダを再帰的に取得 | |
for root, dirs, files in os.walk(folders): | |
# システムフォルダをスキップ | |
if self._is_system_file(root): | |
continue | |
# 通常のファイルを追加 | |
for file in files: | |
file_path = os.path.join(root, file) | |
if not self._is_system_file(file_path) and file_path not in selected_items: | |
selected_items.append(file_path) | |
self.file_listbox.insert(tk.END, file_path) | |
self.current_config["selected_files"] = selected_items | |
self.save_config() | |
self.status_var.set(f"合計: {len(selected_items)}個のファイルが選択されています") | |
def _clear_selection(self): | |
"""選択を解除""" | |
self.file_listbox.delete(0, tk.END) | |
self.current_config["selected_files"] = [] | |
self.save_config() | |
self.status_var.set("選択が解除されました") | |
def _select_files(self): | |
"""ファイルを選択""" | |
files = filedialog.askopenfilenames( | |
initialdir=self.source_dir_var.get(), | |
title="コピーするファイルを選択" | |
) | |
if files: | |
# 既存の選択を保持 | |
existing_items = list(self.file_listbox.get(0, tk.END)) | |
selected_items = existing_items.copy() | |
# システムファイルを除外して新しいファイルを追加 | |
for file in files: | |
if not self._is_system_file(file) and file not in selected_items: | |
selected_items.append(file) | |
self.file_listbox.insert(tk.END, file) | |
self.current_config["selected_files"] = selected_items | |
self.save_config() | |
self.status_var.set(f"合計: {len(selected_items)}個のファイルが選択されています") | |
def _save_settings(self): | |
"""設定を保存""" | |
self.current_config["source_dir"] = self.source_dir_var.get() | |
self.current_config["dest_dir"] = self.dest_dir_var.get() | |
self.current_config["exclude_patterns"] = self.exclude_var.get() | |
self.save_config() | |
self.status_var.set("設定を保存しました") | |
def write_log(self, message: str): | |
"""ログを記録""" | |
log_dir = os.path.join("misc", "log") | |
os.makedirs(log_dir, exist_ok=True) # フォルダがなければ作成 | |
log_file = os.path.join(log_dir, "filter_matome_log.txt") | |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
with open(log_file, "a", encoding="utf-8") as f: | |
f.write(f"{timestamp} - {message}\n") | |
def _execute(self): | |
"""実行処理""" | |
try: | |
# 選択されたファイルの確認 | |
if not self.file_listbox.get(0, tk.END): | |
messagebox.showerror("エラー", "ファイルが選択されていません") | |
return | |
# 出力先ディレクトリの作成 | |
dest_dir = os.path.join(self.dest_dir_var.get(), "test_nlFilters") | |
os.makedirs(dest_dir, exist_ok=True) | |
# 進捗バーの初期化 | |
total_files = len(self.file_listbox.get(0, tk.END)) | |
self.progress_var.set(0) | |
progress_step = 100.0 / (total_files + 1) # +1 for archiving | |
# ファイルのコピー | |
self.write_log("ファイルコピー開始") | |
for i, src_file in enumerate(self.file_listbox.get(0, tk.END)): | |
try: | |
# システムファイルをスキップ | |
if self._is_system_file(src_file): | |
continue | |
# 相対パスの計算 | |
rel_path = os.path.relpath(src_file, self.source_dir_var.get()) | |
dest_path = os.path.join(dest_dir, rel_path) | |
# 必要なディレクトリの作成 | |
os.makedirs(os.path.dirname(dest_path), exist_ok=True) | |
# ファイルのコピー | |
shutil.copy2(src_file, dest_path) | |
self.write_log(f"コピー成功: {rel_path}") | |
# 進捗の更新 | |
self.progress_var.set((i + 1) * progress_step) | |
self.status_var.set(f"コピー中... ({i + 1}/{total_files})") | |
self.root.update() | |
except Exception as e: | |
self.write_log(f"コピーエラー: {rel_path} - {str(e)}") | |
messagebox.showerror("エラー", f"ファイルのコピー中にエラーが発生しました:\n{str(e)}") | |
return | |
# 7zアーカイブの作成 | |
self.status_var.set("アーカイブ作成中...") | |
archive_path = os.path.join(self.dest_dir_var.get(), "test_nlFilters.7z") | |
try: | |
with py7zr.SevenZipFile(archive_path, 'w') as archive: | |
archive.writeall(dest_dir, "test_nlFilters") | |
self.write_log("アーカイブ作成成功") | |
except Exception as e: | |
self.write_log(f"アーカイブ作成エラー: {str(e)}") | |
messagebox.showerror("エラー", f"アーカイブの作成中にエラーが発生しました:\n{str(e)}") | |
return | |
# 完了 | |
self.progress_var.set(100) | |
self.status_var.set("処理が完了しました") | |
self.write_log("処理完了") | |
messagebox.showinfo("完了", "フィルタまとめパッケージの更新が完了しました") | |
except Exception as e: | |
self.write_log(f"実行エラー: {str(e)}") | |
messagebox.showerror("エラー", f"処理中にエラーが発生しました:\n{str(e)}") | |
def install_required_packages(): | |
"""必要なパッケージをインストール""" | |
required = {'py7zr'} | |
installed = {pkg.key for pkg in pkg_resources.working_set} | |
missing = required - installed | |
if missing: | |
python = sys.executable | |
subprocess.check_call([python, '-m', 'pip', 'install', *missing], stdout=subprocess.DEVNULL) | |
def main(): | |
"""メイン関数""" | |
# 必要なパッケージをインストール | |
install_required_packages() | |
# メインウィンドウを作成 | |
root = tk.Tk() | |
app = FilterMatomeApp(root) | |
# ウィンドウサイズを設定 | |
root.geometry("800x600") | |
# メインループを開始 | |
root.mainloop() | |
if __name__ == "__main__": | |
main() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment