Skip to content

Instantly share code, notes, and snippets.

@roflsunriz
Last active March 22, 2025 14:09
Show Gist options
  • Save roflsunriz/4c71999062b61bb42d98712a60eaa1ae to your computer and use it in GitHub Desktop.
Save roflsunriz/4c71999062b61bb42d98712a60eaa1ae to your computer and use it in GitHub Desktop.
filter_matome : 「フィルタまとめ」パッケージ更新ツール
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