[feat] Python toolkits

This commit is contained in:
acite
2025-09-09 12:10:00 +08:00
parent a2bf8bfcec
commit 99a5e42d99
4 changed files with 674 additions and 0 deletions

View File

@@ -0,0 +1,318 @@
import os
import sys
import json
import re
import tkinter as tk
from tkinter import simpledialog, Toplevel, Canvas, Frame, Scrollbar
from PIL import Image, ImageTk
# --- Configuration ---
# Supported image file extensions
SUPPORTED_EXTENSIONS = ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp')
# Default thumbnail size for the GUI on first launch
DEFAULT_THUMBNAIL_SIZE = (300, 300)
# Number of columns in the GUI grid
GRID_COLUMNS = 5
def natural_sort_key(s):
return [int(text) if text.isdigit() else text.lower()
for text in re.split('([0-9]+)', s)]
class BookmarkApp:
"""
A GUI application for selecting images and creating bookmarks, with zoom functionality.
"""
def __init__(self, parent, image_dir, image_files):
"""
Initialize the bookmark creation window.
"""
self.top = Toplevel(parent)
self.top.title("Bookmark Creator | Keys: [+] Zoom In, [-] Zoom Out")
self.top.grid_rowconfigure(0, weight=1)
self.top.grid_columnconfigure(0, weight=1)
self.image_dir = image_dir
self.image_files = image_files
self.bookmarks = []
self._photo_images = [] # To prevent garbage collection
# --- Zoom Configuration ---
self.current_size = DEFAULT_THUMBNAIL_SIZE[0]
self.zoom_step = 25
self.min_zoom_size = 50
self.max_zoom_size = 500
# --- Create a scrollable frame ---
self.canvas = Canvas(self.top)
self.scrollbar = Scrollbar(self.top, orient="vertical", command=self.canvas.yview)
self.scrollable_frame = Frame(self.canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(
scrollregion=self.canvas.bbox("all")
)
)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.canvas.grid(row=0, column=0, sticky="nsew")
self.scrollbar.grid(row=0, column=1, sticky="ns")
# --- Bind Events ---
self.top.bind('<MouseWheel>', self._on_mousewheel)
self.top.bind('<Button-4>', self._on_mousewheel)
self.top.bind('<Button-5>', self._on_mousewheel)
# Bind zoom keys
self.top.bind('<KeyPress-plus>', self._zoom_in)
self.top.bind('<KeyPress-equal>', self._zoom_in) # For keyboards where + is shift+=
self.top.bind('<KeyPress-minus>', self._zoom_out)
self._repopulate_images()
def _zoom_in(self, event=None):
"""Increases the size of the thumbnails."""
new_size = self.current_size + self.zoom_step
if new_size > self.max_zoom_size:
new_size = self.max_zoom_size
if new_size != self.current_size:
self.current_size = new_size
print(f"Zoom In. New thumbnail size: {self.current_size}x{self.current_size}")
self._repopulate_images()
def _zoom_out(self, event=None):
"""Decreases the size of the thumbnails."""
new_size = self.current_size - self.zoom_step
if new_size < self.min_zoom_size:
new_size = self.min_zoom_size
if new_size != self.current_size:
self.current_size = new_size
print(f"Zoom Out. New thumbnail size: {self.current_size}x{self.current_size}")
self._repopulate_images()
def _on_mousewheel(self, event):
"""Handle mouse wheel scrolling."""
if sys.platform == "linux":
scroll_delta = -1 if event.num == 4 else 1
else:
scroll_delta = int(-1 * (event.delta / 120))
self.canvas.yview_scroll(scroll_delta, "units")
def _repopulate_images(self):
"""
Clear and redraw all images in the grid with the current size.
This is called on initial load and after every zoom action.
"""
# Clear existing widgets
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
self._photo_images.clear() # Clear the photo references
new_thumbnail_size = (self.current_size, self.current_size)
for i, filename in enumerate(self.image_files):
try:
filepath = os.path.join(self.image_dir, filename)
with Image.open(filepath) as img:
img.thumbnail(new_thumbnail_size, Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(img)
self._photo_images.append(photo)
container = Frame(self.scrollable_frame, bd=2, relief="groove")
img_label = tk.Label(container, image=photo)
img_label.pack()
text_label = tk.Label(container, text=filename)
text_label.pack()
container.bind("<Button-1>", lambda e, f=filename: self.add_bookmark(f))
img_label.bind("<Button-1>", lambda e, f=filename: self.add_bookmark(f))
text_label.bind("<Button-1>", lambda e, f=filename: self.add_bookmark(f))
row = i // GRID_COLUMNS
col = i % GRID_COLUMNS
container.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
except Exception as e:
print(f"Warning: Could not load image {filename}. Error: {e}")
def add_bookmark(self, page_filename):
"""Prompt user for a bookmark name and add it."""
bookmark_name = simpledialog.askstring(
"Add Bookmark",
f"Enter a name for the bookmark on page:\n{page_filename}",
parent=self.top
)
if bookmark_name:
self.bookmarks.append({
"name": bookmark_name,
"page": page_filename
})
print(f"Success: Bookmark '{bookmark_name}' created for page '{page_filename}'.")
def wait(self):
"""Wait for the Toplevel window to be closed."""
self.top.wait_window()
return self.bookmarks
def load_existing_summary(summary_path):
"""Load existing summary.json if it exists, else return None."""
if os.path.exists(summary_path):
try:
with open(summary_path, 'r', encoding='utf-8') as f:
return json.load(f)
except (IOError, json.JSONDecodeError) as e:
print(f"Warning: Could not read existing summary.json: {e}")
return None
def get_tags_from_user():
"""Prompt user to enter tags in the console."""
try:
tags_input = input("Enter tags (comma-separated): ").strip()
if tags_input:
return [tag.strip() for tag in tags_input.split(",") if tag.strip()]
return []
except (KeyboardInterrupt, EOFError):
print("\nOperation cancelled by user.")
sys.exit(0)
def main():
"""
Main function to execute the script.
"""
# --- 1. Get and Validate Directory from Command Line ---
if len(sys.argv) != 2:
print("Usage: python restructure_comic.py <directory_path>")
sys.exit(1)
target_dir = sys.argv[1]
if not os.path.isdir(target_dir):
print(f"Error: The provided path '{target_dir}' is not a valid directory.")
sys.exit(1)
print(f"Processing directory: {target_dir}")
# --- 2. Check for existing summary.json ---
json_filepath = os.path.join(target_dir, "summary.json")
existing_summary = load_existing_summary(json_filepath)
# --- 3. Get User Input for Metadata ---
if existing_summary:
print("Found existing summary.json. Using existing data where available.")
comic_name = existing_summary.get("comic_name", "")
author = existing_summary.get("author", "anonymous")
tags = existing_summary.get("tags", [])
existing_bookmarks = existing_summary.get("bookmarks", [])
# Only prompt for missing fields
if not comic_name:
try:
comic_name = input("Enter the comic name: ")
except (KeyboardInterrupt, EOFError):
print("\nOperation cancelled by user.")
sys.exit(0)
else:
try:
comic_name = input("Enter the comic name: ")
author = input("Enter the author name (or leave blank for 'anonymous'): ")
if not author:
author = "anonymous"
tags = get_tags_from_user()
existing_bookmarks = []
except (KeyboardInterrupt, EOFError):
print("\nOperation cancelled by user.")
sys.exit(0)
# --- 4. Scan, Sort, and Rename Image Files ---
try:
all_files = os.listdir(target_dir)
image_files = sorted(
[f for f in all_files if f.lower().endswith(SUPPORTED_EXTENSIONS)],
key=natural_sort_key
)
if not image_files:
print("Error: No supported image files found in the directory.")
sys.exit(1)
# Only rename files if we don't have an existing summary with a file list
if existing_summary and "list" in existing_summary:
new_filenames = existing_summary["list"]
print("Using existing file list from summary.json")
else:
page_count = len(image_files)
num_digits = len(str(page_count))
new_filenames = []
print("\nRenaming files...")
for i, old_filename in enumerate(image_files, start=1):
file_ext = os.path.splitext(old_filename)[1]
new_filename_base = f"{i:0{num_digits}d}"
new_filename = f"{new_filename_base}{file_ext}"
old_filepath = os.path.join(target_dir, old_filename)
new_filepath = os.path.join(target_dir, new_filename)
if old_filepath != new_filepath:
os.rename(old_filepath, new_filepath)
print(f" '{old_filename}' -> '{new_filename}'")
else:
print(f" '{old_filename}' is already correctly named. Skipping.")
new_filenames.append(new_filename)
except OSError as e:
print(f"\nAn error occurred during file operations: {e}")
sys.exit(1)
print("\nFile operations complete.")
# --- 5. Launch GUI for Bookmark Creation ---
print("Launching bookmark creator GUI...")
print("Please click on images in the new window to create bookmarks.")
print("Use '+' and '-' keys to zoom in and out. Close the window when finished.")
root = tk.Tk()
root.withdraw()
gui = BookmarkApp(root, target_dir, new_filenames)
new_bookmarks = gui.wait()
root.destroy()
print("Bookmark creation finished.")
# Combine existing bookmarks with new ones
all_bookmarks = existing_bookmarks + new_bookmarks
# --- 6. Create and Write summary.json ---
summary_data = {
"comic_name": comic_name,
"page_count": len(new_filenames),
"bookmarks": all_bookmarks,
"author": author,
"tags": tags,
"list": new_filenames
}
try:
with open(json_filepath, 'w', encoding='utf-8') as f:
json.dump(summary_data, f, indent=2, ensure_ascii=False)
print(f"\nSuccessfully created/updated '{json_filepath}'")
except IOError as e:
print(f"\nError: Could not write to '{json_filepath}'. Reason: {e}")
sys.exit(1)
print("\nOperation completed successfully!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,78 @@
import json
import os
import sys
from pathlib import Path
def process_directory(directory_path):
"""
处理指定目录扫描图片文件并更新summary.json
Args:
directory_path (str): 目录路径
"""
try:
# 转换为Path对象
path = Path(directory_path)
# 检查目录是否存在
if not path.exists() or not path.is_dir():
print(f"错误: 目录 '{directory_path}' 不存在或不是目录")
return False
# 支持的图片文件扩展名
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
# 扫描目录中的图片文件
image_files = []
for file in path.iterdir():
if file.is_file() and file.suffix.lower() in image_extensions:
image_files.append(file.name)
# 按文件名排序
image_files.sort()
print(f"找到 {len(image_files)} 个图片文件")
# 读取或创建summary.json
summary_file = path / "summary.json"
if summary_file.exists():
try:
with open(summary_file, 'r', encoding='utf-8') as f:
summary_data = json.load(f)
except json.JSONDecodeError:
print("错误: summary.json 格式不正确")
return False
else:
summary_data = {}
# 更新列表
summary_data['list'] = image_files
# 写回文件
with open(summary_file, 'w', encoding='utf-8') as f:
json.dump(summary_data, f, ensure_ascii=False, indent=2)
print(f"成功更新 {summary_file}")
return True
except Exception as e:
print(f"处理过程中发生错误: {e}")
return False
def main():
# 检查命令行参数
if len(sys.argv) != 2:
print("用法: python script.py <目录路径>")
sys.exit(1)
directory_path = sys.argv[1]
# 处理目录
if process_directory(directory_path):
print("操作完成")
else:
print("操作失败")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,72 @@
import os
import json
import sys
def process_directory(base_path):
# 检查基础路径是否存在
if not os.path.exists(base_path):
print(f"错误:路径 '{base_path}' 不存在")
return
# 遍历基础路径下的所有子目录
for item in os.listdir(base_path):
item_path = os.path.join(base_path, item)
# 只处理目录,忽略文件
if os.path.isdir(item_path):
summary_path = os.path.join(item_path, "summary.json")
# 检查summary.json文件是否存在
if os.path.exists(summary_path):
try:
# 读取JSON文件
with open(summary_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 获取comic_name和tags
comic_name = data.get('comic_name', '未知名称')
tags = data.get('tags', [])
# 输出信息
print(f"\n漫画名称: {comic_name}")
print(f"当前标签: {tags}")
# 提示用户输入新标签
user_input = input("请输入新标签(多个标签用英文逗号分隔,直接回车跳过): ").strip()
if user_input:
# 分割用户输入的标签
new_tags = [tag.strip() for tag in user_input.split(',') if tag.strip()]
if new_tags:
# 添加新标签到列表
tags.extend(new_tags)
data['tags'] = tags
# 写回文件
with open(summary_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
print(f"已添加新标签: {new_tags}")
else:
print("未输入有效标签,跳过")
else:
print("未输入标签,跳过")
except json.JSONDecodeError:
print(f"错误:{summary_path} 不是有效的JSON文件")
except Exception as e:
print(f"处理文件 {summary_path} 时出错: {e}")
else:
print(f"跳过目录 {item}未找到summary.json文件")
def main():
if len(sys.argv) != 2:
print("用法: python script.py <目录路径>")
sys.exit(1)
base_dir = sys.argv[1]
process_directory(base_dir)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,206 @@
import json
import os
import sys
import subprocess
import shutil
from pathlib import Path
def get_video_duration(video_path):
"""Get video duration in milliseconds using ffprobe"""
try:
cmd = [
'ffprobe',
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
str(video_path)
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
duration_seconds = float(result.stdout.strip())
return int(duration_seconds * 1000)
except (subprocess.CalledProcessError, FileNotFoundError, ValueError) as e:
print(f"Error getting video duration: {e}")
return 0
def create_thumbnails(video_path, gallery_path, num_thumbnails=10):
"""
Extracts thumbnails from a video and saves them to the gallery directory.
"""
try:
# Check if ffmpeg is installed
subprocess.run(['ffmpeg', '-version'], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: ffmpeg is not installed or not in your PATH. Skipping thumbnail creation.")
return
try:
# Get video duration using ffprobe
duration_cmd = [
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', str(video_path)
]
result = subprocess.run(duration_cmd, capture_output=True, text=True, check=True)
duration = float(result.stdout)
except (subprocess.CalledProcessError, ValueError) as e:
print(f"Could not get duration for '{video_path}': {e}. Skipping thumbnail creation.")
return
if duration <= 0:
print(f"Warning: Invalid video duration for '{video_path}'. Skipping thumbnail creation.")
return
interval = duration / (num_thumbnails + 1)
print(f"Generating {num_thumbnails} thumbnails for {video_path.name}...")
for i in range(num_thumbnails):
timestamp = (i + 1) * interval
output_thumbnail_path = gallery_path / f"{i}.jpg"
ffmpeg_cmd = [
'ffmpeg', '-ss', str(timestamp), '-i', str(video_path),
'-vframes', '1', '-q:v', '2', str(output_thumbnail_path), '-y'
]
try:
subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print(f" Extracted thumbnail {i}.jpg")
except subprocess.CalledProcessError as e:
print(f" Error extracting thumbnail {i}.jpg: {e}")
def update_summary(base_path, name_input=None, author_input=None):
"""
Updates the summary.json file for a given path.
name_input and author_input are optional, used for the '-a' mode.
"""
summary_path = base_path / "summary.json"
video_path = base_path / "video.mp4"
gallery_path = base_path / "gallery"
# Default template
default_summary = {
"name": name_input if name_input is not None else "null",
"duration": 0,
"gallery": [],
"comment": [],
"star": False,
"like": 0,
"author": author_input if author_input is not None else "anonymous"
}
# Load existing summary if available
if summary_path.exists():
try:
with open(summary_path, 'r', encoding='utf-8') as f:
existing_data = json.load(f)
# Update default with existing values
for key in default_summary:
if key in existing_data:
default_summary[key] = existing_data[key]
except json.JSONDecodeError:
print("Warning: Invalid JSON in summary.json, using defaults")
# Update duration from video file
if video_path.exists():
default_summary["duration"] = get_video_duration(video_path)
else:
print(f"Warning: video.mp4 not found at {video_path}")
# Update gallery from directory
if gallery_path.exists() and gallery_path.is_dir():
gallery_files = []
for file in gallery_path.iterdir():
if file.is_file():
gallery_files.append(file.name)
gallery_files.sort()
default_summary["gallery"] = gallery_files
else:
print(f"Warning: gallery directory not found at {gallery_path}")
# Write updated summary
with open(summary_path, 'w', encoding='utf-8') as f:
json.dump(default_summary, f, indent=4, ensure_ascii=False)
print(f"Summary updated successfully at {summary_path}")
def find_next_directory(base_path):
"""Find the next available integer directory name."""
existing_dirs = set()
for item in base_path.iterdir():
if item.is_dir() and item.name.isdigit():
existing_dirs.add(int(item.name))
next_num = 1
while next_num in existing_dirs:
next_num += 1
return str(next_num)
def main():
if len(sys.argv) < 2:
print("Usage: python script.py <command> [arguments]")
print("Commands:")
print(" -u <path> Update the summary.json in the specified path.")
print(" -a <video_file> <path> Add a new video project in a new directory under the specified path.")
sys.exit(1)
command = sys.argv[1]
if command == '-u':
if len(sys.argv) != 3:
print("Usage: python script.py -u <path>")
sys.exit(1)
base_path = Path(sys.argv[2])
if not base_path.is_dir():
print(f"Error: Path not found or is not a directory: {base_path}")
sys.exit(1)
update_summary(base_path)
elif command == '-a':
if len(sys.argv) != 4:
print("Usage: python script.py -a <video_file> <path>")
sys.exit(1)
video_source_path = Path(sys.argv[2])
base_path = Path(sys.argv[3])
if not video_source_path.exists() or not video_source_path.is_file():
print(f"Error: Video file not found: {video_source_path}")
sys.exit(1)
if not base_path.is_dir():
print(f"Error: Base path not found or is not a directory: {base_path}")
sys.exit(1)
# Find a new directory name (e.g., "1", "2", "3")
new_dir_name = find_next_directory(base_path)
new_project_path = base_path / new_dir_name
# Create the new project directory and the gallery subdirectory
new_project_path.mkdir(exist_ok=True)
gallery_path = new_project_path / "gallery"
gallery_path.mkdir(exist_ok=True)
print(f"New project directory created at {new_project_path}")
# Copy video file to the new directory
shutil.copy(video_source_path, new_project_path / "video.mp4")
print(f"Video copied to {new_project_path / 'video.mp4'}")
# --- 新增功能:自动生成缩略图 ---
video_dest_path = new_project_path / "video.mp4"
create_thumbnails(video_dest_path, gallery_path)
# ------------------------------------
# Get user input for name and author
video_name = input("Enter the video name: ")
video_author = input("Enter the author's name: ")
# Update the summary with user input
update_summary(new_project_path, name_input=video_name, author_input=video_author)
else:
print("Invalid command. Use -u or -a.")
print("Usage: python script.py <command> [arguments]")
sys.exit(1)
if __name__ == "__main__":
main()