Merge pull request #5349 from cddjr/fix_bluray

This commit is contained in:
jxxghp
2026-01-13 07:03:16 +08:00
committed by GitHub
9 changed files with 374 additions and 205 deletions

View File

@@ -315,21 +315,6 @@ class MediaChain(ChainBase):
)
return None
@staticmethod
def is_bluray_folder(fileitem: schemas.FileItem) -> bool:
"""
判断是否为原盘目录
"""
if not fileitem or fileitem.type != "dir":
return False
# 蓝光原盘目录必备的文件或文件夹
required_files = ['BDMV', 'CERTIFICATE']
# 检查目录下是否存在所需文件或文件夹
for item in StorageChain().list_files(fileitem):
if item.name in required_files:
return True
return False
@eventmanager.register(EventType.MetadataScrape)
def scrape_metadata_event(self, event: Event):
"""
@@ -370,7 +355,7 @@ class MediaChain(ChainBase):
else:
if file_list:
# 如果是BDMV原盘目录只对根目录进行刮削不处理子目录
if self.is_bluray_folder(fileitem):
if storagechain.is_bluray_folder(fileitem):
logger.info(f"检测到BDMV原盘目录只对根目录进行刮削{fileitem.path}")
self.scrape_metadata(fileitem=fileitem,
mediainfo=mediainfo,
@@ -563,10 +548,23 @@ class MediaChain(ChainBase):
logger.info("电影NFO刮削已关闭跳过")
else:
# 电影目录
if recursive:
# 处理文件
if self.is_bluray_folder(fileitem):
# 原盘目录
files = __list_files(_fileitem=fileitem)
is_bluray_folder = storagechain.contains_bluray_subdirectories(files)
if recursive and not is_bluray_folder:
# 处理非原盘目录内的文件
for file in files:
if file.type == "dir":
# 电影不处理子目录
continue
self.scrape_metadata(fileitem=file,
mediainfo=mediainfo,
init_folder=False,
parent=fileitem,
overwrite=overwrite)
# 生成目录内图片文件
if init_folder:
if is_bluray_folder:
# 检查电影NFO开关
if scraping_switchs.get('movie_nfo', True):
nfo_path = filepath / (filepath.name + ".nfo")
if overwrite or not storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
@@ -581,20 +579,6 @@ class MediaChain(ChainBase):
logger.info(f"已存在nfo文件{nfo_path}")
else:
logger.info("电影NFO刮削已关闭跳过")
else:
# 处理目录内的文件
files = __list_files(_fileitem=fileitem)
for file in files:
if file.type == "dir":
# 电影不处理子目录
continue
self.scrape_metadata(fileitem=file,
mediainfo=mediainfo,
init_folder=False,
parent=fileitem,
overwrite=overwrite)
# 生成目录内图片文件
if init_folder:
# 图片
image_dict = self.metadata_img(mediainfo=mediainfo)
if image_dict:

View File

@@ -133,22 +133,29 @@ class StorageChain(ChainBase):
"""
return self.run_module("support_transtype", storage=storage)
def is_bluray_folder(self, fileitem: Optional[schemas.FileItem]) -> bool:
"""
检查是否蓝光目录
"""
if not fileitem or fileitem.type != "dir":
return False
return self.contains_bluray_subdirectories(self.list_files(fileitem))
@staticmethod
def contains_bluray_subdirectories(fileitems: Optional[List[schemas.FileItem]]) -> bool:
"""
判断是否包含蓝光必备的文件夹
"""
required_files = ("BDMV", "CERTIFICATE")
for item in fileitems or []:
if item.type == "dir" and item.name in required_files:
return True
return False
def delete_media_file(self, fileitem: schemas.FileItem, delete_self: bool = True) -> bool:
"""
删除媒体文件,以及不含媒体文件的目录
"""
def __is_bluray_dir(_fileitem: schemas.FileItem) -> bool:
"""
检查是否蓝光目录
"""
_dir_files = self.list_files(fileitem=_fileitem, recursion=False)
if _dir_files:
for _f in _dir_files:
if _f.type == "dir" and _f.name in ["BDMV", "CERTIFICATE"]:
return True
return False
media_exts = settings.RMT_MEDIAEXT + settings.DOWNLOAD_TMPEXT
fileitem_path = Path(fileitem.path) if fileitem.path else Path("")
if len(fileitem_path.parts) <= 2:
@@ -156,7 +163,7 @@ class StorageChain(ChainBase):
return False
if fileitem.type == "dir":
# 本身是目录
if __is_bluray_dir(fileitem):
if self.is_bluray_folder(fileitem):
logger.warn(f"正在删除蓝光原盘目录:【{fileitem.storage}{fileitem.path}")
if not self.delete_file(fileitem):
logger.warn(f"{fileitem.storage}{fileitem.path} 删除失败")

View File

@@ -376,7 +376,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
self._transfer_interval = 15
# 事件管理器
self.jobview = JobManager()
# 移成功的文件清单
# 移成功的文件清单
self._success_target_files: Dict[str, List[str]] = {}
# 启动整理任务
self.__init()
@@ -873,7 +873,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
state, errmsg = self.do_transfer(
fileitem=FileItem(
storage="local",
path=file_path.as_posix(),
path=file_path.as_posix() + ("/" if file_path.is_dir() else ""),
type="dir" if not file_path.is_file() else "file",
name=file_path.name,
size=file_path.stat().st_size,
@@ -908,16 +908,6 @@ class TransferChain(ChainBase, metaclass=Singleton):
"""
storagechain = StorageChain()
def __contains_bluray_sub(_fileitems: List[FileItem]) -> bool:
"""
判断是否包含蓝光子目录
"""
if _fileitems:
for sub in _fileitems:
if sub.type == "dir" and sub.name in ["BDMV", "CERTIFICATE"]:
return True
return False
def __is_bluray_sub(_path: str) -> bool:
"""
判断是否蓝光原盘目录内的子目录或文件
@@ -949,7 +939,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
# 蓝光原盘根目录
sub_items = storagechain.list_files(fileitem) or []
if __contains_bluray_sub(sub_items):
if storagechain.contains_bluray_subdirectories(sub_items):
return [(fileitem, True)]
# 需要整理的文件项列表

View File

@@ -132,6 +132,15 @@ class TransHandler:
return self.result.model_copy()
else:
new_path = target_path / fileitem.name
# 在整理目录前先尝试获取原盘大小避免整理记录出现0字节的情况
# TODO 当前只计算STREAM目录内的文件大小如果需要精确则递归完整目录
if stream_fileitem := source_oper.get_item(
Path(fileitem.path) / "BDMV" / "STREAM"
):
fileitem.size = 0
files = source_oper.list(stream_fileitem) or []
for file in files:
fileitem.size += file.size
# 整理目录
new_diritem, errmsg = self.__transfer_dir(fileitem=fileitem,
mediainfo=mediainfo,

View File

@@ -695,11 +695,13 @@ class Monitor(ConfigReloadMixin, metaclass=SingletonClass):
# 全程加锁
with lock:
is_bluray_folder = False
# 蓝光原盘文件处理
if __is_bluray_sub(event_path):
event_path = __get_bluray_dir(event_path)
if not event_path:
return
is_bluray_folder = True
# TTL缓存控重
if self._cache.get(str(event_path)):
@@ -708,13 +710,20 @@ class Monitor(ConfigReloadMixin, metaclass=SingletonClass):
self._cache[str(event_path)] = True
try:
logger.info(f"开始整理文件: {event_path}")
if is_bluray_folder:
logger.info(f"开始整理蓝光原盘: {event_path}")
else:
logger.info(f"开始整理文件: {event_path}")
# 开始整理
TransferChain().do_transfer(
fileitem=FileItem(
storage=storage,
path=event_path.as_posix(),
type="file",
path=(
event_path.as_posix()
if not is_bluray_folder
else event_path.as_posix() + "/"
),
type="file" if not is_bluray_folder else "dir",
name=event_path.name,
basename=event_path.stem,
extension=event_path.suffix[1:],

View File

@@ -479,6 +479,8 @@ class SystemUtils:
def is_bluray_dir(dir_path: Path) -> bool:
"""
判断是否为蓝光原盘目录
(该方法已弃用,改用`StorageChain().is_bluray_folder)`
"""
if not dir_path.is_dir():
return False

View File

@@ -1,161 +1,99 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# 文件列表结构 list[tuple(名称, 子文件列表 或 文件大小)]
bluray_files = [
{
"name": "FOLDER",
"children": [
{
"name": "Digimon",
"children": [
{
"name": "Digimon (2055)",
"children": [
{
"name": "BDMV",
"children": [
{
"name": "STREAM",
"children": [
{
"name": "00000.m2ts",
"size": 104857600,
},
{
"name": "00001.m2ts",
"size": 104857600,
},
(
"FOLDER",
[
(
"Digimon",
[
(
"Digimon BluRay (2055)",
[
(
"BDMV",
[
(
"STREAM",
[
("00000.m2ts", 104857600),
("00001.m2ts", 104857600),
],
},
),
],
},
{
"name": "CERTIFICATE",
"children": [],
},
),
("CERTIFICATE", None),
],
},
{
"name": "Digimon (2099)",
"children": [
{
"name": "BDMV",
"children": [
{
"name": "STREAM",
"children": [
{
"name": "00000.m2ts",
"size": 104857600,
},
{
"name": "00001.m2ts",
"size": 104857600,
},
{
"name": "00002.m2ts.!qB",
"size": 104857600,
},
),
(
"Digimon BluRay (2099)",
[
(
"BDMV",
[
(
"STREAM",
[
("00000.m2ts", 104857600),
("00001.m2ts", 104857600),
("00002.m2ts.!qB", 104857600),
],
},
),
],
},
{
"name": "CERTIFICATE",
"children": [],
},
),
("CERTIFICATE", None),
],
},
{
"name": "Digimon (2199)",
"children": [
{
"name": "Digimon.2199.mp4",
"size": 104857600,
},
],
},
),
("Digimon (2199)", [("Digimon.2199.mp4", 104857600)]),
],
},
{
"name": "Pokemon (2016)",
"children": [
{
"name": "BDMV",
"children": [
{
"name": "STREAM",
"children": [
{
"name": "00000.m2ts",
"size": 104857600,
},
{
"name": "00001.m2ts",
"size": 104857600,
},
),
(
"Pokemon BluRay (2016)",
[
(
"BDMV",
[
(
"STREAM",
[
("00000.m2ts", 104857600),
("00001.m2ts", 104857600),
],
},
)
],
},
{
"name": "CERTIFICATE",
"children": [],
},
),
("CERTIFICATE", None),
],
},
{
"name": "Pokemon (2021)",
"children": [
{
"name": "BDMV",
"children": [
{
"name": "STREAM",
"children": [
{
"name": "00000.m2ts",
"size": 104857600,
},
{
"name": "00001.m2ts",
"size": 104857600,
},
),
(
"Pokemon BluRay (2021)",
[
(
"BDMV",
[
(
"STREAM",
[
("00000.m2ts", 104857600),
("00001.m2ts", 104857600),
],
},
)
],
},
{
"name": "CERTIFICATE",
"children": [],
},
),
("CERTIFICATE", None),
],
},
{
"name": "Pokemon (2028)",
"children": [
{
"name": "Pokemon.2028.mkv",
"size": 104857600,
},
{
"name": "Pokemon.2028.hdr.mkv.!qB",
"size": 104857600,
},
),
(
"Pokemon (2028)",
[
("Pokemon.2028.mkv", 104857600),
("Pokemon.2028.hdr.mkv.!qB", 104857600),
],
},
{
"name": "Pokemon.2029.mp4",
"size": 104857600,
},
{
"name": "Pokemon (2030)",
"children": [
{
"name": "S",
"size": 104857600,
},
],
},
),
("Pokemon.2029.mp4", 104857600),
("Pokemon.2039.mp4", 104857600),
("Pokemon (2030)", [("S", 104857600)]),
],
},
)
]

View File

@@ -1,5 +1,6 @@
import unittest
from tests.test_bluray import BluRayTest
from tests.test_metainfo import MetaInfoTest
from tests.test_object import ObjectUtilsTest
@@ -12,6 +13,9 @@ if __name__ == '__main__':
suite.addTest(MetaInfoTest('test_emby_format_ids'))
suite.addTest(ObjectUtilsTest('test_check_method'))
# 测试蓝光目录识别
suite.addTest(BluRayTest())
# 运行测试
runner = unittest.TextTestRunner()
runner.run(suite)

226
tests/test_bluray.py Normal file
View File

@@ -0,0 +1,226 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from pathlib import Path
from typing import Optional
from unittest import TestCase
from unittest.mock import patch
from app import schemas
from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.chain.transfer import TransferChain
from app.core.context import MediaInfo
from app.core.event import Event
from app.core.metainfo import MetaInfoPath
from app.db.models.transferhistory import TransferHistory
from app.log import logger
from app.schemas.types import EventType
from tests.cases.files import bluray_files
class BluRayTest(TestCase):
def __init__(self, methodName="test"):
super().__init__(methodName)
self.__history = []
self.__root = schemas.FileItem(
path="/", name="", type="dir", extension="", size=0
)
self.__all = {self.__root.path: self.__root}
def __build_child(parent: schemas.FileItem, files: list[tuple[str, list | int]]):
parent.children = []
for name, children in files:
sep = "" if parent.path.endswith("/") else "/"
file_item = schemas.FileItem(
path=f"{parent.path}{sep}{name}",
name=name,
extension=Path(name).suffix[1:],
basename=Path(name).stem,
type="file" if isinstance(children, int) else "dir",
size=children if isinstance(children, int) else 0,
)
parent.children.append(file_item)
self.__all[file_item.path] = file_item
if isinstance(children, list):
__build_child(file_item, children)
__build_child(self.__root, bluray_files)
def _test_do_transfer(self):
def __test_do_transfer(path: str):
self.__history.clear()
TransferChain().do_transfer(
force=False,
background=False,
fileitem=StorageChain().get_file_item(None, Path(path)),
)
return self.__history
self.assertEqual(
[
"/FOLDER/Digimon/Digimon BluRay (2055)",
"/FOLDER/Digimon/Digimon BluRay (2099)",
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
],
__test_do_transfer("/FOLDER/Digimon"),
)
self.assertEqual(
[
"/FOLDER/Digimon/Digimon BluRay (2055)",
],
__test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)"),
)
self.assertEqual(
[
"/FOLDER/Digimon/Digimon BluRay (2055)",
],
__test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)/BDMV"),
)
self.assertEqual(
[
"/FOLDER/Digimon/Digimon BluRay (2055)",
],
__test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)/BDMV/STREAM"),
)
self.assertEqual(
[
"/FOLDER/Digimon/Digimon BluRay (2055)",
],
__test_do_transfer(
"/FOLDER/Digimon/Digimon BluRay (2055)/BDMV/STREAM/00001.m2ts"
),
)
self.assertEqual(
[
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
],
__test_do_transfer("/FOLDER/Digimon/Digimon (2199)"),
)
self.assertEqual(
[
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
],
__test_do_transfer("/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4"),
)
self.assertEqual(
[
"/FOLDER/Pokemon.2029.mp4",
],
__test_do_transfer("/FOLDER/Pokemon.2029.mp4"),
)
self.assertEqual(
[
"/FOLDER/Digimon/Digimon BluRay (2055)",
"/FOLDER/Digimon/Digimon BluRay (2099)",
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
"/FOLDER/Pokemon BluRay (2016)",
"/FOLDER/Pokemon BluRay (2021)",
"/FOLDER/Pokemon (2028)/Pokemon.2028.mkv",
"/FOLDER/Pokemon.2029.mp4",
"/FOLDER/Pokemon.2039.mp4",
],
__test_do_transfer("/FOLDER"),
)
def _test_scrape_metadata(self, mock_metadata_nfo):
def __test_scrape_metadata(path: str, excepted_nfo_count: int = 1):
"""
分别测试手动和自动刮削
"""
fileitem = StorageChain().get_file_item(None, Path(path))
meta = MetaInfoPath(Path(fileitem.path))
mediainfo = MediaInfo(tmdb_info={"id": 1, "title": "Test"})
# 测试手动刮削
logger.debug(f"测试手动刮削 {path}")
mock_metadata_nfo.call_count = 0
MediaChain().scrape_metadata(
fileitem=fileitem, meta=meta, mediainfo=mediainfo, overwrite=True
)
# 确保调用了指定次数的metadata_nfo
self.assertEqual(mock_metadata_nfo.call_count, excepted_nfo_count)
# 测试自动刮削
logger.debug(f"测试自动刮削 {path}")
mock_metadata_nfo.call_count = 0
MediaChain().scrape_metadata_event(
Event(
event_type=EventType.MetadataScrape,
event_data={
"meta": meta,
"mediainfo": mediainfo,
"fileitem": fileitem,
"file_list": [fileitem.path],
"overwrite": False,
},
)
)
# 调用了指定次数的metadata_nfo
self.assertEqual(mock_metadata_nfo.call_count, excepted_nfo_count)
# 刮削原盘目录
__test_scrape_metadata("/FOLDER/Digimon/Digimon BluRay (2099)")
# 刮削电影文件
__test_scrape_metadata("/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4")
# 刮削电影目录
__test_scrape_metadata("/FOLDER", excepted_nfo_count=2)
@patch("app.chain.ChainBase.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")
@patch("app.chain.storage.StorageChain.get_parent_item")
@patch("app.chain.storage.StorageChain.get_file_item")
def test(
self,
mock_get_file_item,
mock_get_parent_item,
mock_list_files,
mock_get_by_src,
*_,
):
def get_file_item(storage: str, path: Path):
path_posix = path.as_posix()
return self.__all.get(path_posix)
def get_parent_item(fileitem: schemas.FileItem):
return get_file_item(None, Path(fileitem.path).parent)
def list_files(fileitem: schemas.FileItem, recursion: bool = False):
if fileitem.type != "dir":
return None
if recursion:
result = []
file_path = f"{fileitem.path}/"
for path, item in self.__all.items():
if path.startswith(file_path):
result.append(item)
return result
else:
return fileitem.children
def get_by_src(src: str, storage: Optional[str] = None):
self.__history.append(src)
result = TransferHistory()
result.status = True
return result
mock_get_file_item.side_effect = get_file_item
mock_get_parent_item.side_effect = get_parent_item
mock_list_files.side_effect = list_files
mock_get_by_src.side_effect = get_by_src
self._test_do_transfer()
with patch(
"app.chain.media.MediaChain.metadata_nfo", return_value=None
) as mock:
self._test_scrape_metadata(mock_metadata_nfo=mock)