fix: preserve tv bluray disc folders (#5788)

This commit is contained in:
XiaoChao Fang
2026-05-19 16:42:23 +08:00
committed by GitHub
parent 407b60a14f
commit cdddd8e080
3 changed files with 181 additions and 8 deletions
+54 -4
View File
@@ -34,6 +34,49 @@ class TransHandler:
def __init__(self): def __init__(self):
pass 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 @staticmethod
def __update_result(result: TransferInfo, **kwargs): def __update_result(result: TransferInfo, **kwargs):
""" """
@@ -156,7 +199,7 @@ class TransHandler:
if fileitem.type == "dir": if fileitem.type == "dir":
# 整理整个目录,一般为蓝光原盘 # 整理整个目录,一般为蓝光原盘
if need_rename: if need_rename:
new_path = self.get_rename_path( rendered_path = self.get_rename_path(
path=target_path, path=target_path,
template_string=rename_format, template_string=rename_format,
rename_dict=self.get_naming_dict( rename_dict=self.get_naming_dict(
@@ -165,9 +208,16 @@ class TransHandler:
source_path=fileitem.path, source_path=fileitem.path,
source_item=fileitem, source_item=fileitem,
) )
new_path = DirectoryHelper.get_media_root_path( if mediainfo.type == MediaType.TV:
rename_format, rename_path=new_path 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: if not new_path:
self.__update_result( self.__update_result(
result=result, result=result,
+6 -1
View File
@@ -1,10 +1,15 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from pathlib import Path from pathlib import Path
import sys
from types import ModuleType
from typing import Optional from typing import Optional
from unittest import TestCase from unittest import TestCase
from unittest.mock import patch 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 import schemas
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.chain.storage import StorageChain from app.chain.storage import StorageChain
@@ -174,7 +179,7 @@ class BluRayTest(TestCase):
# 刮削电影目录 # 刮削电影目录
__test_scrape_metadata("/FOLDER", excepted_nfo_count=2) __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.chain.ChainBase.__init__", return_value=None) # 避免不必要的模块初始化
@patch("app.db.transferhistory_oper.TransferHistoryOper.get_by_src") @patch("app.db.transferhistory_oper.TransferHistoryOper.get_by_src")
@patch("app.chain.storage.StorageChain.list_files") @patch("app.chain.storage.StorageChain.list_files")
+121 -3
View File
@@ -1,7 +1,12 @@
from pathlib import Path 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.context import MediaInfo
from app.core.metainfo import MetaInfoPath from app.core.meta import MetaBase
from app.modules.filemanager import FileManagerModule from app.modules.filemanager import FileManagerModule
from app.schemas import FileItem, TransferDirectoryConf from app.schemas import FileItem, TransferDirectoryConf
from app.schemas.types import MediaType from app.schemas.types import MediaType
@@ -31,6 +36,23 @@ class GuardedStorage:
raise AssertionError("预览不应删除文件") 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(): def test_cloud_storage_preview_only_calculates_target_path():
fileitem = FileItem( fileitem = FileItem(
storage="alist", storage="alist",
@@ -41,7 +63,7 @@ def test_cloud_storage_preview_only_calculates_target_path():
extension="mkv", extension="mkv",
size=1024, size=1024,
) )
meta = MetaInfoPath(Path(fileitem.path)) meta = _build_meta("Test.Show.S01E01.mkv", season=1, episode=1)
mediainfo = MediaInfo( mediainfo = MediaInfo(
type=MediaType.TV, type=MediaType.TV,
title="Test Show", title="Test Show",
@@ -94,7 +116,7 @@ def test_local_storage_preview_skips_target_conflict_checks(tmp_path):
extension="mkv", extension="mkv",
size=source_file.stat().st_size, size=source_file.stat().st_size,
) )
meta = MetaInfoPath(source_file) meta = _build_meta(source_file.name, season=1, episode=2)
mediainfo = MediaInfo( mediainfo = MediaInfo(
type=MediaType.TV, type=MediaType.TV,
title="Test Show", 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.target_item.path.endswith(".mkv")
assert transferinfo.file_list == [fileitem.path] assert transferinfo.file_list == [fileitem.path]
assert transferinfo.file_list_new == [transferinfo.target_item.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)"