Files
NewComicDownloader/komga.py
T
2026-08-15 22:35:15 +08:00

502 lines
19 KiB
Python

import requests
from requests.auth import HTTPBasicAuth
import json
from typing import Dict, List, Optional, Set, Any
from tqdm import tqdm
import argparse
import os
from datetime import datetime
class KomgaChecker:
def __init__(self, base_url: str = None, username: str = None, password: str = None):
self.base_url = base_url.rstrip('/') if base_url else None
if base_url and username and password:
self.auth = HTTPBasicAuth(username, password)
self.session = requests.Session()
self.session.auth = self.auth
else:
self.session = None
def get_libraries(self) -> List[Dict]:
"""获取所有漫画库"""
if not self.session:
raise ValueError("未初始化Komga连接")
response = self.session.get(f"{self.base_url}/api/v1/libraries")
response.raise_for_status()
return response.json()
def get_series(self, library_id: str) -> List[Dict]:
"""获取指定库中的所有系列"""
if not self.session:
raise ValueError("未初始化Komga连接")
series = []
page = 0
size = 100
while True:
response = self.session.get(
f"{self.base_url}/api/v1/series",
params={"library_id": library_id, "page": page, "size": size}
)
response.raise_for_status()
data = response.json()
if not data["content"]:
break
series.extend(data["content"])
page += 1
return series
def get_books(self, series_id: str) -> List[Dict]:
"""获取指定系列中的所有书籍"""
if not self.session:
raise ValueError("未初始化Komga连接")
books = []
page = 0
size = 100
while True:
response = self.session.get(
f"{self.base_url}/api/v1/series/{series_id}/books",
params={"page": page, "size": size}
)
response.raise_for_status()
data = response.json()
if not data["content"]:
break
books.extend(data["content"])
page += 1
return books
def extract_volume_number(self, metadata: Dict) -> Optional[float]:
"""从元数据中提取卷号"""
# 尝试从volume字段获取
volume = metadata.get("number")
if volume is not None and volume != "":
try:
return float(str(volume))
except (ValueError, TypeError):
pass
# 尝试从标题中提取卷号
title = metadata.get("title", "").lower()
# 匹配 "vol. 1", "volume 1", "第1卷" 等格式
import re
patterns = [
r'vol\.?\s*(\d+(?:\.\d+)?)',
r'volume\s*(\d+(?:\.\d+)?)',
r'第\s*(\d+(?:\.\d+)?)\s*卷',
r'#\s*(\d+(?:\.\d+)?)'
]
for pattern in patterns:
match = re.search(pattern, title)
if match:
try:
return float(match.group(1))
except (ValueError, TypeError):
continue
return None
def check_missing_volumes(self, books: List[Dict]) -> Dict:
"""检查缺失的卷"""
volumes = []
for book in books:
metadata = book.get("metadata", {})
volume_num = self.extract_volume_number(metadata)
if volume_num is not None:
volumes.append({
'number': volume_num,
'title': metadata.get('title', 'Unknown'),
'id': book['id']
})
# 按卷号排序
volumes.sort(key=lambda x: x['number'])
if not volumes:
return {'has_volumes': False, 'volumes': [], 'missing': []}
# 找出缺失的卷号
volume_numbers = [v['number'] for v in volumes]
min_vol = int(min(volume_numbers))
max_vol = int(max(volume_numbers))
missing_volumes = []
for vol in range(min_vol, max_vol + 1):
if vol not in volume_numbers:
# 检查是否有接近的卷号(如1.0, 1.5等)
has_close_match = any(abs(v - vol) < 0.1 for v in volume_numbers)
if not has_close_match:
missing_volumes.append(vol)
return {
'has_volumes': True,
'volumes': volumes,
'missing': missing_volumes,
'total_found': len(volumes),
'expected_range': f"{min_vol}-{max_vol}"
}
def analyze_series(self, library_name: str, series: Dict) -> Optional[Dict]:
"""分析单个系列"""
try:
series_id = series['id']
series_name = series['metadata']['title']
print(f"正在检查系列: {series_name}")
books = self.get_books(series_id)
if not books:
return None
result = self.check_missing_volumes(books)
result.update({
'series_name': series_name,
'series_id': series_id,
'library_name': library_name,
'total_books': len(books)
})
return result
except Exception as e:
print(f"分析系列 {series.get('metadata', {}).get('title', 'Unknown')} 时出错: {e}")
return None
def run_check(self) -> List[Dict]:
"""运行完整的检查"""
if not self.session:
raise ValueError("未初始化Komga连接")
print("开始检查Komga服务器...")
try:
libraries = self.get_libraries()
print(f"找到 {len(libraries)} 个库")
except Exception as e:
print(f"获取库列表失败: {e}")
return []
all_results = []
for library in libraries:
library_id = library['id']
library_name = library['name']
print(f"\n正在处理库: {library_name}")
try:
series_list = self.get_series(library_id)
print(f"找到 {len(series_list)} 个系列")
for series in tqdm(series_list, desc=f"检查 {library_name}"):
result = self.analyze_series(library_name, series)
if result:
all_results.append(result)
except Exception as e:
print(f"处理库 {library_name} 时出错: {e}")
return all_results
def save_missing_to_json(self, results: List[Dict], filename: str = None):
"""将缺失卷的系列保存到JSON文件"""
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"komga_missing_volumes_{timestamp}.json"
# 筛选有缺失卷的系列
series_with_missing = [r for r in results if r['has_volumes'] and r['missing']]
if not series_with_missing:
print("没有找到缺失卷的系列,不生成JSON文件。")
return
# 准备保存的数据
output_data = {
'generated_at': datetime.now().isoformat(),
'komga_server': self.base_url,
'total_series_checked': len(results),
'series_with_missing_volumes': len(series_with_missing),
'missing_series': []
}
for result in series_with_missing:
series_data = {
'series_name': result['series_name'],
'series_id': result['series_id'],
'library_name': result['library_name'],
'total_books': result['total_books'],
'volumes_found': result['total_found'],
'expected_range': result['expected_range'],
'missing_volumes': result['missing'],
'existing_volumes': [
{
'number': vol['number'],
'title': vol['title'],
'book_id': vol['id']
} for vol in result['volumes']
]
}
output_data['missing_series'].append(series_data)
# 保存到文件
try:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(output_data, f, ensure_ascii=False, indent=2)
print(f"\n✅ 缺失卷信息已保存到: {filename}")
except Exception as e:
print(f"❌ 保存JSON文件失败: {e}")
return filename
def load_from_json(self, filename: str) -> Dict[str, Any]:
"""从JSON文件加载检查结果"""
try:
with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"✅ 已从 {filename} 加载检查结果")
return data
except Exception as e:
print(f"❌ 加载JSON文件失败: {e}")
return {}
def compare_with_previous(self, current_results: List[Dict], previous_filename: str):
"""与之前的检查结果进行比较"""
previous_data = self.load_from_json(previous_filename)
if not previous_data:
return
previous_missing = {s['series_name']: s['missing_volumes'] for s in previous_data.get('missing_series', [])}
current_missing = {r['series_name']: r['missing'] for r in current_results if r['has_volumes'] and r['missing']}
print("\n" + "="*80)
print("与之前检查结果比较")
print("="*80)
improved_series = []
worsened_series = []
unchanged_series = []
new_series_with_missing = []
fixed_series = []
for series_name, current_missing_vols in current_missing.items():
if series_name in previous_missing:
previous_missing_vols = previous_missing[series_name]
if set(current_missing_vols) == set(previous_missing_vols):
unchanged_series.append(series_name)
elif len(current_missing_vols) < len(previous_missing_vols):
improved_series.append({
'name': series_name,
'previous': previous_missing_vols,
'current': current_missing_vols,
'fixed': list(set(previous_missing_vols) - set(current_missing_vols))
})
else:
worsened_series.append({
'name': series_name,
'previous': previous_missing_vols,
'current': current_missing_vols,
'added': list(set(current_missing_vols) - set(previous_missing_vols))
})
else:
new_series_with_missing.append(series_name)
# 检查之前有缺失但现在完整的系列
for series_name in previous_missing:
if series_name not in current_missing:
fixed_series.append(series_name)
# 输出比较结果
if fixed_series:
print(f"\n🎉 已修复的系列 ({len(fixed_series)}):")
for series in fixed_series:
print(f" ✅ {series}")
if improved_series:
print(f"\n📈 改善的系列 ({len(improved_series)}):")
for series in improved_series:
print(f" 📚 {series['name']}")
print(f" 之前缺失: {series['previous']}")
print(f" 现在缺失: {series['current']}")
print(f" 已补全: {series['fixed']}")
if worsened_series:
print(f"\n📉 恶化的系列 ({len(worsened_series)}):")
for series in worsened_series:
print(f" 📚 {series['name']}")
print(f" 之前缺失: {series['previous']}")
print(f" 现在缺失: {series['current']}")
print(f" 新增缺失: {series['added']}")
if new_series_with_missing:
print(f"\n🆕 新发现的缺失系列 ({len(new_series_with_missing)}):")
for series in new_series_with_missing:
print(f" 📚 {series} - 缺失卷: {current_missing[series]}")
if unchanged_series:
print(f"\n➡️ 未变化的系列 ({len(unchanged_series)}):")
for i, series in enumerate(unchanged_series[:10]): # 只显示前10个
print(f" 📚 {series} - 缺失卷: {current_missing[series]}")
if len(unchanged_series) > 10:
print(f" ... 还有 {len(unchanged_series) - 10} 个系列")
# 总体统计
print(f"\n📊 总体比较:")
print(f" 已修复的系列: {len(fixed_series)}")
print(f" 改善的系列: {len(improved_series)}")
print(f" 恶化的系列: {len(worsened_series)}")
print(f" 新发现的缺失系列: {len(new_series_with_missing)}")
print(f" 未变化的系列: {len(unchanged_series)}")
def generate_report_from_json(self, filename: str):
"""从JSON文件生成报告"""
data = self.load_from_json(filename)
if not data:
return
print("\n" + "="*80)
print(f"Komga漫画缺集检查报告 (来自: {filename})")
print("="*80)
print(f"生成时间: {data.get('generated_at', '未知')}")
print(f"Komga服务器: {data.get('komga_server', '未知')}")
print(f"检查的系列总数: {data.get('total_series_checked', '未知')}")
print(f"有缺失卷的系列数: {data.get('series_with_missing_volumes', '未知')}")
missing_series = data.get('missing_series', [])
if missing_series:
print(f"\n{'='*50}")
print("有缺失卷的系列:")
print(f"{'='*50}")
for series in missing_series:
print(f"\n📚 系列: {series['series_name']}")
print(f" 库: {series['library_name']}")
print(f" 找到卷数: {series['volumes_found']}")
print(f" 预期范围: {series['expected_range']}")
print(f" ❌ 缺失卷号: {series['missing_volumes']}")
else:
print("\n🎉 没有发现缺失卷的系列!")
def generate_report(self, results: List[Dict], save_json: bool = True):
"""生成检查报告"""
print("\n" + "="*80)
print("Komga漫画缺集检查报告")
print("="*80)
series_with_missing = [r for r in results if r['has_volumes'] and r['missing']]
series_complete = [r for r in results if r['has_volumes'] and not r['missing']]
series_no_volumes = [r for r in results if not r['has_volumes']]
print(f"\n总计检查 {len(results)} 个系列")
print(f"🔍 有缺失卷的系列: {len(series_with_missing)}")
print(f"✅ 完整的系列: {len(series_complete)}")
print(f"❓ 无法识别卷号的系列: {len(series_no_volumes)}")
# 保存JSON文件
if save_json and series_with_missing:
json_filename = self.save_missing_to_json(results)
if series_with_missing:
print(f"\n{'='*50}")
print("有缺失卷的系列:")
print(f"{'='*50}")
for result in series_with_missing:
print(f"\n📚 系列: {result['series_name']}")
print(f" 库: {result['library_name']}")
print(f" 找到卷数: {result['total_found']}")
print(f" 预期范围: {result['expected_range']}")
print(f" ❌ 缺失卷号: {result['missing']}")
# 显示现有的卷号范围
volumes = result['volumes']
if len(volumes) <= 10:
existing = [v['number'] for v in volumes]
print(f" 📖 现有卷号: {existing}")
if series_no_volumes:
print(f"\n{'='*50}")
print("无法识别卷号的系列:")
print(f"{'='*50}")
for result in series_no_volumes[:10]: # 只显示前10个
print(f"📚 {result['series_name']} (库: {result['library_name']})")
if len(series_no_volumes) > 10:
print(f"... 还有 {len(series_no_volumes) - 10} 个系列")
# 返回JSON文件名用于后续比较
return json_filename if save_json and series_with_missing else None
def main():
parser = argparse.ArgumentParser(description='检查Komga服务器中的漫画缺集情况')
parser.add_argument('--url', help='Komga服务器地址 (例如: http://localhost:8080)')
parser.add_argument('--username', help='Komga用户名')
parser.add_argument('--password', help='Komga密码')
parser.add_argument('--output', '-o', help='输出JSON文件名 (可选)')
parser.add_argument('--no-json', action='store_true', help='不生成JSON文件')
parser.add_argument('--load-json', help='从JSON文件加载并显示报告')
parser.add_argument('--compare', help='与之前的JSON检查结果进行比较')
args = parser.parse_args()
# 创建检查器实例
if args.load_json:
# 仅加载模式,不需要Komga连接
checker = KomgaChecker()
checker.generate_report_from_json(args.load_json)
elif args.url and args.username and args.password:
# 正常检查模式
checker = KomgaChecker(args.url, args.username, args.password)
# 运行检查
results = checker.run_check()
# 生成报告
json_filename = checker.generate_report(results, save_json=not args.no_json)
# 如果指定了输出文件名,额外保存一份
if args.output and results:
checker.save_missing_to_json(results, args.output)
json_filename = args.output
# 如果指定了比较文件,进行比较
if args.compare and json_filename:
checker.compare_with_previous(results, args.compare)
else:
print("错误: 需要提供Komga服务器连接信息或指定--load-json参数")
parser.print_help()
if __name__ == "__main__":
#main()
# 直接在你的代码中使用
checker = KomgaChecker(
base_url="https://komga.caiwenxiu.cn",
username="caiwenxiu0806@163.com",
password="cwx@komga"
)
# 运行检查
results = checker.run_check()
# 生成报告
json_filename = checker.generate_report(results, save_json=True)
# 如果指定了输出文件名,额外保存一份
output="komga.json"
if output and results:
checker.save_missing_to_json(results, output)
json_filename = output