diff --git a/config.yml b/config.yml index 17547fbff..ddc53e69b 100644 --- a/config.yml +++ b/config.yml @@ -20,7 +20,7 @@ scanner: # 格式要求:https://docs.pydantic.dev/2.0/usage/types/bytesize/ minimum_size: 232MiB skip_nfo_dir: yes - manual: yes + manual: no ################################ network: # 设置代理服务器地址,支持 http, socks5/socks5h 代理,比如'http://127.0.0.1:1080' @@ -30,7 +30,7 @@ network: proxy_free: avsox: 'https://avsox.click' javbus: 'https://www.seedmm.help' - javdb: 'https://javdb368.com' + javdb: 'https://javdb.com' javlib: 'https://www.y78k.com' # 网络问题导致抓取数据失败时的重试次数,通常3次就差不多了 retry: 3 @@ -42,7 +42,8 @@ crawler: # 要使用的爬虫列表(汇总数据时从前到后进行) # airav avsox avwiki fanza fc2 fc2fan javbus javdb javlib javmenu jav321 mgstage prestige arzon arzon_iv selection: - normal: [airav, avsox, javbus, javdb, javlib, jav321, mgstage, prestige] + #normal: [airav, avsox, javbus, javdb, javlib, jav321, mgstage, prestige] + normal: [javdb] fc2: [fc2, avsox, javdb, javmenu, fc2ppvdb] cid: [fanza] getchu: [dl_getchu] @@ -118,8 +119,12 @@ summarizer: basename_pattern: "poster" # 尽可能下载高清封面?(高清封面大小约 8-10 MiB,远大于普通封面,如果你的网络条件不佳,会降低整理速度) highres: true - # 在封面图上添加水印(标签),例如“字幕” + # 在封面图上添加水印(标签),例如"字幕" add_label: false + # 生成文件夹缩略图 (folder.jpg),用作文件夹封面图标 + create_folder_thumbnail: true + # 文件夹缩略图的大小 (宽x高) + folder_thumbnail_size: [300, 450] crop: # 要使用图像识别来裁剪的番号系列需要匹配的正则表达式 on_id_pattern: @@ -143,7 +148,7 @@ summarizer: extra_fanarts: # 是否下载剧照? - enabled: true + enabled: false # 间隔的两次封面爬取请求之间应该间隔多久 scrap_interval: PT1.5S diff --git a/javsp/__main__.py b/javsp/__main__.py index 4bf5b01e4..545d116ba 100644 --- a/javsp/__main__.py +++ b/javsp/__main__.py @@ -4,6 +4,7 @@ import json import time import logging +from sys import platform from PIL import Image from pydantic import ValidationError from pydantic_extra_types.pendulum_dt import Duration @@ -419,8 +420,116 @@ def should_use_ai_crop_match(label): fanart_cropped = add_label_to_poster(fanart_cropped, SUBTITLE_MARK_FILE, LabelPostion.BOTTOM_RIGHT) if movie.uncensored: fanart_cropped = add_label_to_poster(fanart_cropped, UNCENSORED_MARK_FILE, LabelPostion.BOTTOM_LEFT) + + # 在保存前检查目标路径是否为目录,如果是则删除 + import shutil + if os.path.exists(movie.poster_file) and os.path.isdir(movie.poster_file): + shutil.rmtree(movie.poster_file) # 删除整个目录树 + fanart_cropped.save(movie.poster_file) + # 生成文件夹缩略图 (folder.jpg) - 根据配置决定是否生成 + if Cfg().summarizer.cover.create_folder_thumbnail: + # 这个缩略图会被 Windows/macOS 等系统用作文件夹图标 + folder_dir = os.path.dirname(movie.poster_file) + # 清理路径,去除末尾可能的错误字符 + folder_dir = folder_dir.rstrip('(').rstrip() + folder_thumb_path = os.path.join(folder_dir, 'folder.jpg') + + # 调试信息 + logger.debug(f'文件夹路径: {folder_dir}') + logger.debug(f'缩略图路径: {folder_thumb_path}') + + # 从配置读取缩略图大小 + thumb_width, thumb_height = Cfg().summarizer.cover.folder_thumbnail_size + thumb_size = (thumb_width, thumb_height) + + # 创建缩略图 - 使用 resize 而不是 thumbnail 以确保正确的图片生成 + folder_thumb = fanart_cropped.copy() + # 计算缩略图尺寸,保持宽高比 + original_width, original_height = folder_thumb.size + aspect_ratio = original_width / original_height + + # 根据目标尺寸和原始宽高比计算实际尺寸 + if aspect_ratio > thumb_width / thumb_height: + # 原图更宽,以宽度为准 + new_width = thumb_width + new_height = int(thumb_width / aspect_ratio) + else: + # 原图更高,以高度为准 + new_height = thumb_height + new_width = int(thumb_height * aspect_ratio) + + # 使用 resize 创建缩略图 + folder_thumb = folder_thumb.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # 调试信息 + logger.debug(f'原始尺寸: {original_width}x{original_height}') + logger.debug(f'缩略图尺寸: {new_width}x{new_height}') + + # 检查并删除已存在的文件夹封面(无论是目录还是文件) + if os.path.exists(folder_thumb_path): + if os.path.isdir(folder_thumb_path): + logger.debug(f'删除已存在的目录: {folder_thumb_path}') + shutil.rmtree(folder_thumb_path) + else: + logger.debug(f'删除已存在的文件: {folder_thumb_path}') + os.remove(folder_thumb_path) + + # 确保父目录存在 + if not os.path.exists(folder_dir): + logger.error(f'文件夹不存在: {folder_dir}') + return + + # 保存文件夹缩略图 - 确保图片有内容再保存 + if folder_thumb and folder_thumb.size[0] > 0 and folder_thumb.size[1] > 0: + try: + # 如果是 RGBA 模式,转换为 RGB 以便保存为 JPEG + if folder_thumb.mode == 'RGBA': + # 创建白色背景 + background = Image.new('RGB', folder_thumb.size, (255, 255, 255)) + background.paste(folder_thumb, mask=folder_thumb.split()[3]) + folder_thumb = background + elif folder_thumb.mode != 'RGB': + folder_thumb = folder_thumb.convert('RGB') + + folder_thumb.save(folder_thumb_path, 'JPEG', quality=85, optimize=True) + + # 验证文件是否成功保存 + if os.path.exists(folder_thumb_path): + file_size = os.path.getsize(folder_thumb_path) + logger.debug(f'已生成文件夹封面: {folder_thumb_path} (大小: {file_size} bytes)') + if file_size == 0: + logger.error(f'文件夹封面保存失败,文件大小为0: {folder_thumb_path}') + except Exception as e: + logger.error(f'保存文件夹封面时出错: {e}') + else: + logger.error(f'缩略图生成失败或尺寸无效') + + # 为 Windows 生成 desktop.ini 文件,使文件夹显示自定义图标 + if platform == 'win32': + desktop_ini_path = os.path.join(folder_dir, 'desktop.ini') + desktop_ini_content = """[.ShellClassInfo] +IconResource=folder.jpg,0 +[ViewState] +Mode= +Vid= +FolderType=Pictures +""" + # 写入 desktop.ini 文件 + with open(desktop_ini_path, 'w', encoding='utf-8') as f: + f.write(desktop_ini_content) + + # 设置文件和文件夹属性(隐藏 desktop.ini,设置文件夹为系统文件夹) + import ctypes + FILE_ATTRIBUTE_HIDDEN = 0x02 + FILE_ATTRIBUTE_SYSTEM = 0x04 + # 隐藏 desktop.ini + ctypes.windll.kernel32.SetFileAttributesW(desktop_ini_path, FILE_ATTRIBUTE_HIDDEN) + # 设置文件夹为系统文件夹(这样 Windows 会读取 desktop.ini) + folder_attrs = ctypes.windll.kernel32.GetFileAttributesW(folder_dir) + ctypes.windll.kernel32.SetFileAttributesW(folder_dir, folder_attrs | FILE_ATTRIBUTE_SYSTEM) + def RunNormalMode(all_movies): """普通整理模式""" def check_step(result, msg='步骤错误'): diff --git a/javsp/avid.py b/javsp/avid.py index 3ed4a9297..4fb7da8ec 100644 --- a/javsp/avid.py +++ b/javsp/avid.py @@ -4,17 +4,54 @@ from pathlib import Path -__all__ = ['get_id', 'get_cid', 'guess_av_type'] +__all__ = ['get_id', 'get_cid', 'guess_av_type', 'get_special_suffix'] from javsp.config import Cfg +def get_special_suffix(filepath_str: str) -> tuple: + """从文件名中提取特殊后缀(UC/U/C)并返回属性标识 + + Returns: + tuple: (is_uncensored, has_chinese_subtitle) + """ + filepath = Path(filepath_str) + filename = filepath.stem.upper() + + # 检查无码标识 (UC 或 U) + is_uncensored = bool(re.search(r'[-_](UC|U)$', filename, re.I)) + + # 检查中文字幕标识 (C 或 UC 包含 C) + has_chinese_subtitle = bool(re.search(r'[-_](UC|C)$', filename, re.I)) + + return is_uncensored, has_chinese_subtitle + def get_id(filepath_str: str) -> str: """从给定的文件路径中提取番号(DVD ID)""" filepath = Path(filepath_str) # 通常是接收文件的路径,当然如果是普通字符串也可以 ignore_pattern = re.compile('|'.join(Cfg().scanner.ignored_id_pattern)) norm = ignore_pattern.sub('', filepath.stem).upper() + + # 处理特殊格式: 网站@番号_分集_分辨率.扩展名 (如 4k2.com@sivr00441_1_8k.mp4) + # 移除网站前缀和@符号 + if '@' in norm: + norm = norm.split('@', 1)[1] + + # 处理特殊后缀标识 (如 MIDA-234-UC, MIDA-234-U, MIDA-234-C) + # 这些后缀通常表示: UC=无码, U=无码, C=中文字幕 + # 先移除这些后缀以便正确识别番号,但保留信息供后续使用 + suffix_pattern = r'[-_](UC|U|C)$' + suffix_match = re.search(suffix_pattern, norm, re.I) + if suffix_match: + # 移除后缀,但只是为了番号识别 + norm = re.sub(suffix_pattern, '', norm, flags=re.I) + + # 将番号中的00替换为-(如 sivr00441 -> sivr-441) + # 但要保留分集和分辨率信息在后面处理 + match = re.match(r'^([A-Z]+)00(\d+)(?:_.*)?$', norm, re.I) + if match: + norm = match.group(1) + '-' + match.group(2) if 'FC2' in norm: # 根据FC2 Club的影片数据,FC2编号为5-7个数字 match = re.search(r'FC2[^A-Z\d]{0,5}(PPV[^A-Z\d]{0,5})?(\d{5,7})', norm, re.I) diff --git a/javsp/config.py b/javsp/config.py index c491e6621..40b3a2a67 100644 --- a/javsp/config.py +++ b/javsp/config.py @@ -156,6 +156,8 @@ class CoverSummarize(BaseConfig): basename_pattern: str highres: bool add_label: bool + create_folder_thumbnail: bool = True + folder_thumbnail_size: list[int] = [300, 450] crop: CoverCrop class FanartSummarize(BaseConfig): diff --git a/javsp/file.py b/javsp/file.py index e80988ce8..a6c32d3a4 100644 --- a/javsp/file.py +++ b/javsp/file.py @@ -99,6 +99,53 @@ def scan_movies(root: str) -> List[Movie]: continue # 提取分片信息(如果正则替换成功,只会剩下单个小写字符)。相关变量都要使用同样的列表生成顺序 basenames = [os.path.basename(i) for i in files] + + # 尝试智能分片识别 + file_info = [] + for filepath in files: + basename = os.path.basename(filepath).lower() + # 移除网站前缀 (如 4k2.com@) + if '@' in basename: + basename = basename.split('@', 1)[1] + # 尝试多种模式匹配分片编号 + slice_num = None + + # 模式: 支持多种分片格式 + patterns = [ + r'[-_](\d+)[-_]\d+k\.', # 匹配 sivr00441_1_8k.mp4 格式 (先匹配更精确的) + r'[-_\s]+(?:cd)?(\d+)[-_\s]*(?:_\d+k)?\.', # 匹配 -1, _1, -CD1, _1_8k 等 + r'[-_](\d+)\.mp4$', # 匹配简单的 -2.mp4 格式 + r'[-_\s]+([a-z])[-_\s]*\.', # 匹配 -a, _a 等 + ] + + for pattern in patterns: + match = re.search(pattern, basename) + if match: + slice_id = match.group(1) + # 如果是数字,直接使用;如果是字母,转换为对应的数字 + if slice_id.isdigit(): + slice_num = int(slice_id) + else: + slice_num = ord(slice_id) - ord('a') + 1 + break + + if slice_num is not None: + file_info.append((filepath, slice_num)) + logger.debug(f"识别到分片: {basename} -> 编号 {slice_num}") + + # 如果智能识别成功,使用新的逻辑 + if len(file_info) == len(files): + file_info.sort(key=lambda x: x[1]) + numbers = [info[1] for info in file_info] + + # 检查编号是否连续(允许从0或1开始) + min_num = min(numbers) + if min_num in (0, 1) and numbers == list(range(min_num, min_num + len(numbers))): + logger.debug(f"智能分片识别成功: {avid} -> {numbers}") + dic[avid] = [info[0] for info in file_info] + continue + + # 如果智能识别失败,回退到原始逻辑 prefix = os.path.commonprefix(basenames) try: pattern_expr = re_escape(prefix) + r'\s*([a-z\d])\s*' diff --git a/javsp/lib.py b/javsp/lib.py index 3b6932d76..9a209d364 100644 --- a/javsp/lib.py +++ b/javsp/lib.py @@ -56,10 +56,13 @@ def detect_special_attr(filepath: str, avid: str = None) -> str: match = _PATTERN.search(base) if match: result += 'U' - # 尝试匹配-C/-U/-UC后缀的影片 - postfix = base.split('-')[-1] - if postfix in ('U', 'C', 'UC'): - result += postfix + # 尝试匹配-C/-U/-UC或_C/_U/_UC后缀的影片 + # 支持使用 - 或 _ 作为分隔符 + parts = re.split(r'[-_]', base) + if parts: + postfix = parts[-1] + if postfix in ('U', 'C', 'UC'): + result += postfix elif avid: pattern_str = re.sub(r'[_-]', '[_-]*', avid) + r'(UC|U|C)\b' match = re.search(pattern_str, base, flags=re.I) diff --git a/javsp/nfo.py b/javsp/nfo.py index 573aa0cc3..844430a3d 100644 --- a/javsp/nfo.py +++ b/javsp/nfo.py @@ -109,6 +109,15 @@ def write_nfo(info: MovieInfo, nfo_file): else: nfo.append(E.actor(E.name(i))) + # 在保存前检查目标路径是否为目录或文件,如果存在则删除 + import os + import shutil + if os.path.exists(nfo_file): + if os.path.isdir(nfo_file): + shutil.rmtree(nfo_file) # 删除整个目录树 + else: + os.remove(nfo_file) # 删除文件 + with open(nfo_file, 'wt', encoding='utf-8') as f: f.write(tostring(nfo, encoding='unicode', pretty_print=True, doctype='')) diff --git a/javsp/web/base.py b/javsp/web/base.py index 717b5168a..2dae01abf 100644 --- a/javsp/web/base.py +++ b/javsp/web/base.py @@ -235,7 +235,12 @@ def urlretrieve(url, filename=None, reporthook=None, headers=None): def download(url, output_path, desc=None): """下载指定url的资源""" - # 支持“下载”本地资源,以供fc2fan的本地镜像所使用 + # 在下载前检查目标路径是否为目录,如果是则删除 + import os + if os.path.exists(output_path) and os.path.isdir(output_path): + shutil.rmtree(output_path) # 删除整个目录树 + + # 支持"下载"本地资源,以供fc2fan的本地镜像所使用 if not url.startswith('http'): start_time = time.time() shutil.copyfile(url, output_path)