Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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:
Expand All @@ -143,7 +148,7 @@ summarizer:

extra_fanarts:
# 是否下载剧照?
enabled: true
enabled: false
# 间隔的两次封面爬取请求之间应该间隔多久
scrap_interval: PT1.5S

Expand Down
109 changes: 109 additions & 0 deletions javsp/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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='步骤错误'):
Expand Down
39 changes: 38 additions & 1 deletion javsp/avid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions javsp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
47 changes: 47 additions & 0 deletions javsp/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*'
Expand Down
11 changes: 7 additions & 4 deletions javsp/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions javsp/nfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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='<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>'))
Expand Down
7 changes: 6 additions & 1 deletion javsp/web/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down