diff --git a/app/modules/filemanager/transhandler.py b/app/modules/filemanager/transhandler.py index db00fc81..277f6a0c 100644 --- a/app/modules/filemanager/transhandler.py +++ b/app/modules/filemanager/transhandler.py @@ -34,6 +34,49 @@ class TransHandler: def __init__(self): pass + @staticmethod + def __normalize_disc_folder_name(value: Optional[str]) -> Optional[str]: + """ + 从 Disc/Disk/DVD/CD 标识中提取盘号并统一为 Disc N。 + """ + if not value: + return None + match = re.search( + r"(?:disc|disk|dvd|cd)[\s._-]*0*(\d{1,3})", + value, + re.IGNORECASE, + ) + if not match: + return None + return f"Disc {int(match.group(1))}" + + @classmethod + def __get_tv_bluray_dir_path( + cls, + rendered_path: Path, + source_item: FileItem, + meta: MetaBase, + ) -> Path: + """ + 电视剧原盘目录没有单集文件名,保留季目录并追加盘片目录。 + """ + disc_folder = cls.__normalize_disc_folder_name(getattr(meta, "part", None)) + if not disc_folder and source_item: + source_name = source_item.name or Path(source_item.path).name + disc_folder = cls.__normalize_disc_folder_name(source_name) + if not disc_folder: + match = re.search( + r"(?:^|[^A-Za-z0-9])S\d{1,3}D0*(\d{1,3})(?:[^A-Za-z0-9]|$)", + source_name, + re.IGNORECASE, + ) + if match: + disc_folder = f"Disc {int(match.group(1))}" + if not disc_folder: + disc_folder = source_name + + return rendered_path.parent / (disc_folder or "Disc") + @staticmethod def __update_result(result: TransferInfo, **kwargs): """ @@ -156,7 +199,7 @@ class TransHandler: if fileitem.type == "dir": # 整理整个目录,一般为蓝光原盘 if need_rename: - new_path = self.get_rename_path( + rendered_path = self.get_rename_path( path=target_path, template_string=rename_format, rename_dict=self.get_naming_dict( @@ -165,9 +208,16 @@ class TransHandler: source_path=fileitem.path, source_item=fileitem, ) - new_path = DirectoryHelper.get_media_root_path( - rename_format, rename_path=new_path - ) + if mediainfo.type == MediaType.TV: + new_path = self.__get_tv_bluray_dir_path( + rendered_path=rendered_path, + source_item=fileitem, + meta=in_meta, + ) + else: + new_path = DirectoryHelper.get_media_root_path( + rename_format, rename_path=rendered_path + ) if not new_path: self.__update_result( result=result, diff --git a/tests/test_bluray.py b/tests/test_bluray.py index 225c911b..bd7eb99a 100644 --- a/tests/test_bluray.py +++ b/tests/test_bluray.py @@ -1,10 +1,15 @@ #!/usr/bin/env python # -*- coding:utf-8 -*- from pathlib import Path +import sys +from types import ModuleType from typing import Optional from unittest import TestCase from unittest.mock import patch +sys.modules.setdefault("app.helper.sites", ModuleType("app.helper.sites")) +setattr(sys.modules["app.helper.sites"], "SitesHelper", object) + from app import schemas from app.chain.media import MediaChain from app.chain.storage import StorageChain @@ -174,7 +179,7 @@ class BluRayTest(TestCase): # 刮削电影目录 __test_scrape_metadata("/FOLDER", excepted_nfo_count=2) - @patch("app.chain.ChainBase.metadata_img", return_value=None) # 避免获取图片 + @patch("app.chain.media.MediaChain.metadata_img", return_value=None) # 避免获取图片 @patch("app.chain.ChainBase.__init__", return_value=None) # 避免不必要的模块初始化 @patch("app.db.transferhistory_oper.TransferHistoryOper.get_by_src") @patch("app.chain.storage.StorageChain.list_files") diff --git a/tests/test_transfer_preview.py b/tests/test_transfer_preview.py index a4a8fb6d..1027ccac 100644 --- a/tests/test_transfer_preview.py +++ b/tests/test_transfer_preview.py @@ -1,7 +1,12 @@ from pathlib import Path +import sys +from types import ModuleType + +sys.modules.setdefault("app.helper.sites", ModuleType("app.helper.sites")) +setattr(sys.modules["app.helper.sites"], "SitesHelper", object) from app.core.context import MediaInfo -from app.core.metainfo import MetaInfoPath +from app.core.meta import MetaBase from app.modules.filemanager import FileManagerModule from app.schemas import FileItem, TransferDirectoryConf from app.schemas.types import MediaType @@ -31,6 +36,23 @@ class GuardedStorage: raise AssertionError("预览不应删除文件") +def _build_meta( + title: str, + media_type: MediaType = MediaType.TV, + season: int = 1, + episode: int = 1, + part: str = None, +): + meta = MetaBase(title) + meta.type = media_type + meta.name = "Breaking Bad" if media_type == MediaType.TV else "Test Movie" + meta.year = "2008" if media_type == MediaType.TV else "2026" + meta.begin_season = season + meta.begin_episode = episode + meta.part = part + return meta + + def test_cloud_storage_preview_only_calculates_target_path(): fileitem = FileItem( storage="alist", @@ -41,7 +63,7 @@ def test_cloud_storage_preview_only_calculates_target_path(): extension="mkv", size=1024, ) - meta = MetaInfoPath(Path(fileitem.path)) + meta = _build_meta("Test.Show.S01E01.mkv", season=1, episode=1) mediainfo = MediaInfo( type=MediaType.TV, title="Test Show", @@ -94,7 +116,7 @@ def test_local_storage_preview_skips_target_conflict_checks(tmp_path): extension="mkv", size=source_file.stat().st_size, ) - meta = MetaInfoPath(source_file) + meta = _build_meta(source_file.name, season=1, episode=2) mediainfo = MediaInfo( type=MediaType.TV, title="Test Show", @@ -131,3 +153,99 @@ def test_local_storage_preview_skips_target_conflict_checks(tmp_path): assert transferinfo.target_item.path.endswith(".mkv") assert transferinfo.file_list == [fileitem.path] assert transferinfo.file_list_new == [transferinfo.target_item.path] + + +def _build_bluray_dir_preview( + source_name: str, + media_type: MediaType, + season: int = None, + part: str = None, +): + fileitem = FileItem( + storage="alist", + path=f"/downloads/{source_name}", + type="dir", + name=source_name, + basename=source_name, + ) + meta = _build_meta( + fileitem.name, + media_type=media_type, + season=season, + episode=None, + part=part, + ) + mediainfo = MediaInfo( + type=media_type, + title="Breaking Bad" if media_type == MediaType.TV else "Test Movie", + year="2008" if media_type == MediaType.TV else "2026", + tmdb_id=1396, + ) + target_directory = TransferDirectoryConf( + name="cloud-library", + transfer_type="copy", + overwrite_mode="never", + library_path="/library", + library_storage="alist", + renaming=True, + scraping=True, + notify=True, + ) + + return FileManagerModule().transfer( + fileitem=fileitem, + meta=meta, + mediainfo=mediainfo, + target_directory=target_directory, + source_oper=GuardedStorage(), + target_oper=GuardedStorage(), + preview=True, + ) + + +def test_tv_bluray_dir_preview_preserves_disk_folder_from_meta_part(): + transferinfo = _build_bluray_dir_preview( + source_name="Breaking Bad Season 2 - Disk 1", + media_type=MediaType.TV, + season=2, + part="Disk1", + ) + + assert transferinfo.success is True + assert transferinfo.target_item.path == "/library/Breaking Bad (2008)/Season 2/Disc 1" + assert transferinfo.target_diritem.path == transferinfo.target_item.path + assert transferinfo.file_list_new == [transferinfo.target_item.path] + + +def test_tv_bluray_dir_preview_preserves_disc_folder_from_source_name(): + transferinfo = _build_bluray_dir_preview( + source_name="BREAKING_BAD_S01D01", + media_type=MediaType.TV, + season=1, + ) + + assert transferinfo.success is True + assert transferinfo.target_item.path == "/library/Breaking Bad (2008)/Season 1/Disc 1" + + +def test_tv_bluray_dir_preview_falls_back_to_source_name_without_disc_number(): + source_name = "Breaking Bad Season 3 Bonus Disc" + transferinfo = _build_bluray_dir_preview( + source_name=source_name, + media_type=MediaType.TV, + season=3, + ) + + assert transferinfo.success is True + assert transferinfo.target_item.path == f"/library/Breaking Bad (2008)/Season 3/{source_name}" + + +def test_movie_bluray_dir_preview_keeps_movie_root_layout(): + transferinfo = _build_bluray_dir_preview( + source_name="Test Movie Disc 1", + media_type=MediaType.MOVIE, + part="Disc1", + ) + + assert transferinfo.success is True + assert transferinfo.target_item.path == "/library/Test Movie (2026)"