mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
fix: preserve all recognized resource types
This commit is contained in:
+55
-42
@@ -28,22 +28,27 @@ DIY_TITLE_RE = re.compile(r'-DIY@', re.IGNORECASE)
|
|||||||
DESCRIPTION_SPLIT_RE = re.compile(r'[\s/|]+')
|
DESCRIPTION_SPLIT_RE = re.compile(r'[\s/|]+')
|
||||||
SPACE_RE = re.compile(r'\s+')
|
SPACE_RE = re.compile(r'\s+')
|
||||||
SEASON_SUFFIX_RE = re.compile(r"SEASON$", re.IGNORECASE)
|
SEASON_SUFFIX_RE = re.compile(r"SEASON$", re.IGNORECASE)
|
||||||
REMUX_RESOURCE_RE = re.compile(r'(?<![A-Z0-9])REMUX(?![A-Z0-9])', re.IGNORECASE)
|
|
||||||
|
|
||||||
|
SOURCE_RE = (
|
||||||
def normalize_resource_type(resource_type: Optional[str], title: Optional[str]) -> Optional[str]:
|
r"^BLURAY$|^HDTV$|^UHDTV$|^HDDVD$|^WEBRIP$|^DVDRIP$|^BDRIP$|"
|
||||||
"""
|
r"^BLU$|^WEB$|^BD$|^HDRip$|^REMUX$|^UHD$"
|
||||||
根据原始标题补全连续出现的复合资源类型。
|
)
|
||||||
|
SOURCE_PATTERN = re.compile(r"(%s)" % SOURCE_RE, re.IGNORECASE)
|
||||||
:param resource_type: 解析器已识别的资源类型
|
SOURCE_NAMES = {
|
||||||
:param title: 原始标题
|
"BLURAY": "BluRay",
|
||||||
:return: 补全后的资源类型
|
"HDTV": "HDTV",
|
||||||
"""
|
"UHDTV": "UHDTV",
|
||||||
if resource_type == "UHD BluRay" \
|
"HDDVD": "HDDVD",
|
||||||
and title \
|
"WEBRIP": "WEBRip",
|
||||||
and REMUX_RESOURCE_RE.search(title):
|
"DVDRIP": "DVDRip",
|
||||||
return f"{resource_type} REMUX"
|
"BDRIP": "BDRIP",
|
||||||
return resource_type
|
"BLU": "BLU",
|
||||||
|
"WEB": "WEB",
|
||||||
|
"BD": "BD",
|
||||||
|
"HDRIP": "HDRip",
|
||||||
|
"REMUX": "REMUX",
|
||||||
|
"UHD": "UHD",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MetaVideo(MetaBase):
|
class MetaVideo(MetaBase):
|
||||||
@@ -57,14 +62,14 @@ class MetaVideo(MetaBase):
|
|||||||
_last_token_type = ""
|
_last_token_type = ""
|
||||||
_continue_flag = True
|
_continue_flag = True
|
||||||
_unknown_name_str = ""
|
_unknown_name_str = ""
|
||||||
_source = ""
|
_sources = []
|
||||||
_effect = []
|
_effect = []
|
||||||
# 正则式区
|
# 正则式区
|
||||||
_season_re = r"S(\d{3})|^S(\d{1,3})$|S(\d{1,3})E"
|
_season_re = r"S(\d{3})|^S(\d{1,3})$|S(\d{1,3})E"
|
||||||
_episode_re = r"EP?(\d{2,4})$|^EP?(\d{1,4})$|^S\d{1,2}EP?(\d{1,4})$|S\d{2}EP?(\d{2,4})"
|
_episode_re = r"EP?(\d{2,4})$|^EP?(\d{1,4})$|^S\d{1,2}EP?(\d{1,4})$|S\d{2}EP?(\d{2,4})"
|
||||||
_part_re = r"(^PART[0-9ABI]{0,2}$|^CD[0-9]{0,2}$|^DVD[0-9]{0,2}$|^DISK[0-9]{0,2}$|^DISC[0-9]{0,2}$)"
|
_part_re = r"(^PART[0-9ABI]{0,2}$|^CD[0-9]{0,2}$|^DVD[0-9]{0,2}$|^DISK[0-9]{0,2}$|^DISC[0-9]{0,2}$)"
|
||||||
_roman_numerals = r"^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"
|
_roman_numerals = r"^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"
|
||||||
_source_re = r"^BLURAY$|^HDTV$|^UHDTV$|^HDDVD$|^WEBRIP$|^DVDRIP$|^BDRIP$|^BLU$|^WEB$|^BD$|^HDRip$|^REMUX$|^UHD$"
|
_source_re = SOURCE_RE
|
||||||
_effect_re = r"^SDR$|^HDR\d*$|^HDRVIVID$|^DOLBY$|^DOVI$|^DV$|^3D$|^REPACK$|^HLG$|^HDR10(\+|Plus)$|^HDR10P$|^VIVID$|^EDR$|^HQ$"
|
_effect_re = r"^SDR$|^HDR\d*$|^HDRVIVID$|^DOLBY$|^DOVI$|^DV$|^3D$|^REPACK$|^HLG$|^HDR10(\+|Plus)$|^HDR10P$|^VIVID$|^EDR$|^HQ$"
|
||||||
_resources_type_re = r"%s|%s" % (_source_re, _effect_re)
|
_resources_type_re = r"%s|%s" % (_source_re, _effect_re)
|
||||||
_name_no_begin_re = r"^[\[【].+?[\]】]"
|
_name_no_begin_re = r"^[\[【].+?[\]】]"
|
||||||
@@ -91,7 +96,7 @@ class MetaVideo(MetaBase):
|
|||||||
_episode_pattern = re.compile(_episode_re, re.IGNORECASE)
|
_episode_pattern = re.compile(_episode_re, re.IGNORECASE)
|
||||||
_part_pattern = re.compile(_part_re, re.IGNORECASE)
|
_part_pattern = re.compile(_part_re, re.IGNORECASE)
|
||||||
_roman_numerals_pattern = re.compile(_roman_numerals)
|
_roman_numerals_pattern = re.compile(_roman_numerals)
|
||||||
_source_pattern = re.compile(r"(%s)" % _source_re, re.IGNORECASE)
|
_source_pattern = SOURCE_PATTERN
|
||||||
_effect_pattern = re.compile(r"(%s)" % _effect_re, re.IGNORECASE)
|
_effect_pattern = re.compile(r"(%s)" % _effect_re, re.IGNORECASE)
|
||||||
_resources_type_pattern = re.compile(r"(%s)" % _resources_type_re, re.IGNORECASE)
|
_resources_type_pattern = re.compile(r"(%s)" % _resources_type_re, re.IGNORECASE)
|
||||||
_name_no_chinese_pattern = re.compile(_name_no_chinese_re, re.IGNORECASE)
|
_name_no_chinese_pattern = re.compile(_name_no_chinese_re, re.IGNORECASE)
|
||||||
@@ -114,7 +119,7 @@ class MetaVideo(MetaBase):
|
|||||||
if not title:
|
if not title:
|
||||||
return
|
return
|
||||||
original_title = title
|
original_title = title
|
||||||
self._source = ""
|
self._sources = []
|
||||||
self._effect = []
|
self._effect = []
|
||||||
self._index = 0
|
self._index = 0
|
||||||
# 判断是否纯数字命名
|
# 判断是否纯数字命名
|
||||||
@@ -200,8 +205,8 @@ class MetaVideo(MetaBase):
|
|||||||
if self._effect:
|
if self._effect:
|
||||||
self._effect.reverse()
|
self._effect.reverse()
|
||||||
self.resource_effect = " ".join(self._effect)
|
self.resource_effect = " ".join(self._effect)
|
||||||
if self._source:
|
if self._sources:
|
||||||
self.resource_type = normalize_resource_type(self._source.strip(), original_title)
|
self.resource_type = " ".join(self._sources)
|
||||||
# 提取原盘DIY
|
# 提取原盘DIY
|
||||||
if self.resource_type and "BluRay" in self.resource_type:
|
if self.resource_type and "BluRay" in self.resource_type:
|
||||||
if (self.subtitle and DIY_RE.search(self.subtitle)) \
|
if (self.subtitle and DIY_RE.search(self.subtitle)) \
|
||||||
@@ -590,6 +595,29 @@ class MetaVideo(MetaBase):
|
|||||||
elif token.upper() == "EPISODE":
|
elif token.upper() == "EPISODE":
|
||||||
self._last_token_type = "EPISODE"
|
self._last_token_type = "EPISODE"
|
||||||
|
|
||||||
|
def __append_resource_source(self, source: str) -> None:
|
||||||
|
"""
|
||||||
|
按出现顺序追加资源类型并忽略重复项。
|
||||||
|
|
||||||
|
:param source: 原始资源类型标记
|
||||||
|
"""
|
||||||
|
source_name = SOURCE_NAMES.get(source.upper(), source)
|
||||||
|
if source_name.casefold() not in {
|
||||||
|
item.casefold() for item in self._sources
|
||||||
|
}:
|
||||||
|
self._sources.append(source_name)
|
||||||
|
|
||||||
|
def __replace_last_resource_source(self, source: str, replacement: str) -> None:
|
||||||
|
"""
|
||||||
|
将拆分的资源类型前缀替换为完整规范名称。
|
||||||
|
|
||||||
|
:param source: 待替换的末尾资源类型
|
||||||
|
:param replacement: 完整资源类型
|
||||||
|
"""
|
||||||
|
if self._sources and self._sources[-1].casefold() == source.casefold():
|
||||||
|
self._sources.pop()
|
||||||
|
self.__append_resource_source(replacement)
|
||||||
|
|
||||||
def __init_resource_type(self, token):
|
def __init_resource_type(self, token):
|
||||||
"""
|
"""
|
||||||
识别资源类型
|
识别资源类型
|
||||||
@@ -599,32 +627,17 @@ class MetaVideo(MetaBase):
|
|||||||
if token.upper() == "DL" \
|
if token.upper() == "DL" \
|
||||||
and self._last_token_type == "source" \
|
and self._last_token_type == "source" \
|
||||||
and self._last_token == "WEB":
|
and self._last_token == "WEB":
|
||||||
self._source = "WEB-DL"
|
self.__replace_last_resource_source("WEB", "WEB-DL")
|
||||||
self._continue_flag = False
|
self._continue_flag = False
|
||||||
return
|
return
|
||||||
elif token.upper() == "RAY" \
|
elif token.upper() == "RAY" \
|
||||||
and self._last_token_type == "source" \
|
and self._last_token_type == "source" \
|
||||||
and self._last_token == "BLU":
|
and self._last_token == "BLU":
|
||||||
# UHD BluRay组合
|
self.__replace_last_resource_source("BLU", "BluRay")
|
||||||
if self._source == "UHD":
|
|
||||||
self._source = "UHD BluRay"
|
|
||||||
else:
|
|
||||||
self._source = "BluRay"
|
|
||||||
self._continue_flag = False
|
self._continue_flag = False
|
||||||
return
|
return
|
||||||
elif token.upper() == "WEBDL":
|
elif token.upper() == "WEBDL":
|
||||||
self._source = "WEB-DL"
|
self.__append_resource_source("WEB-DL")
|
||||||
self._continue_flag = False
|
|
||||||
return
|
|
||||||
# UHD REMUX组合
|
|
||||||
if token.upper() == "REMUX" \
|
|
||||||
and self._source == "BluRay":
|
|
||||||
self._source = "BluRay REMUX"
|
|
||||||
self._continue_flag = False
|
|
||||||
return
|
|
||||||
elif token.upper() == "BLURAY" \
|
|
||||||
and self._source == "UHD":
|
|
||||||
self._source = "UHD BluRay"
|
|
||||||
self._continue_flag = False
|
self._continue_flag = False
|
||||||
return
|
return
|
||||||
source_res = self._source_pattern.search(token)
|
source_res = self._source_pattern.search(token)
|
||||||
@@ -632,9 +645,9 @@ class MetaVideo(MetaBase):
|
|||||||
self._last_token_type = "source"
|
self._last_token_type = "source"
|
||||||
self._continue_flag = False
|
self._continue_flag = False
|
||||||
self._stop_name_flag = True
|
self._stop_name_flag = True
|
||||||
if not self._source:
|
source = source_res.group(1)
|
||||||
self._source = source_res.group(1)
|
self.__append_resource_source(source)
|
||||||
self._last_token = self._source.upper()
|
self._last_token = source.upper()
|
||||||
return
|
return
|
||||||
effect_res = self._effect_pattern.search(token)
|
effect_res = self._effect_pattern.search(token)
|
||||||
if effect_res:
|
if effect_res:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from app.core.meta.infopath import (
|
|||||||
clear_parsed_title_for_parent_merge,
|
clear_parsed_title_for_parent_merge,
|
||||||
should_use_parent_title_for_file_stem,
|
should_use_parent_title_for_file_stem,
|
||||||
)
|
)
|
||||||
from app.core.meta.metavideo import normalize_resource_type
|
|
||||||
from app.core.meta.words import WordsMatcher
|
from app.core.meta.words import WordsMatcher
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas.types import MediaType
|
from app.schemas.types import MediaType
|
||||||
@@ -380,10 +379,7 @@ def _meta_from_rust(parsed: dict) -> Optional[MetaBase]:
|
|||||||
"begin_episode": parsed.get("begin_episode"),
|
"begin_episode": parsed.get("begin_episode"),
|
||||||
"end_episode": parsed.get("end_episode"),
|
"end_episode": parsed.get("end_episode"),
|
||||||
"part": parsed.get("part"),
|
"part": parsed.get("part"),
|
||||||
"resource_type": normalize_resource_type(
|
"resource_type": parsed.get("resource_type"),
|
||||||
parsed.get("resource_type"),
|
|
||||||
parsed.get("org_string") or parsed.get("title"),
|
|
||||||
),
|
|
||||||
"resource_effect": parsed.get("resource_effect"),
|
"resource_effect": parsed.get("resource_effect"),
|
||||||
"resource_pix": parsed.get("resource_pix"),
|
"resource_pix": parsed.get("resource_pix"),
|
||||||
"resource_team": parsed.get("resource_team"),
|
"resource_team": parsed.get("resource_team"),
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
moviepilot-rust~=0.2.5
|
moviepilot-rust~=0.2.6
|
||||||
pydantic>=2.13.4,<3.0.0
|
pydantic>=2.13.4,<3.0.0
|
||||||
pydantic-settings>=2.14.2,<3.0.0
|
pydantic-settings>=2.14.2,<3.0.0
|
||||||
SQLAlchemy~=2.0.50
|
SQLAlchemy~=2.0.50
|
||||||
|
|||||||
+1
-1
@@ -1246,7 +1246,7 @@ meta_cases = [{
|
|||||||
"part": "",
|
"part": "",
|
||||||
"season": "",
|
"season": "",
|
||||||
"episode": "",
|
"episode": "",
|
||||||
"restype": "UHD",
|
"restype": "UHD BluRay",
|
||||||
"pix": "2160p",
|
"pix": "2160p",
|
||||||
"video_codec": "HEVC",
|
"video_codec": "HEVC",
|
||||||
"audio_codec": "DTS-HD MA 5.1"
|
"audio_codec": "DTS-HD MA 5.1"
|
||||||
|
|||||||
+37
-14
@@ -3,6 +3,8 @@ from pathlib import Path
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from app.core.metainfo import MetaInfo, MetaInfoPath, find_metainfo
|
from app.core.metainfo import MetaInfo, MetaInfoPath, find_metainfo
|
||||||
from app.core.meta import MetaBase, MetaMusic
|
from app.core.meta import MetaBase, MetaMusic
|
||||||
from app.core.meta.metaanime import MetaAnime
|
from app.core.meta.metaanime import MetaAnime
|
||||||
@@ -173,25 +175,46 @@ def test_python_metainfo_fallback_preserves_xxx_movie_title():
|
|||||||
assert meta.audio_encode == "DDP 5.1"
|
assert meta.audio_encode == "DDP 5.1"
|
||||||
|
|
||||||
|
|
||||||
def test_metainfo_preserves_uhd_bluray_remux_resource_type():
|
RESOURCE_TYPE_CASES = [
|
||||||
"""默认解析入口应保留 UHD、BluRay 与 REMUX 三段资源类型。"""
|
(
|
||||||
meta = MetaInfo(
|
|
||||||
"They.Will.Kill.You.2026.2160p.UHD.BluRay.Remux."
|
"They.Will.Kill.You.2026.2160p.UHD.BluRay.Remux."
|
||||||
"HEVC.DV.TrueHD.7.1.Atmos.mkv"
|
"HEVC.DV.TrueHD.7.1.Atmos.mkv",
|
||||||
)
|
"UHD BluRay REMUX",
|
||||||
|
),
|
||||||
assert meta.resource_type == "UHD BluRay REMUX"
|
(
|
||||||
|
"Movie.2026.2160p.UHD.Blu-ray.Remux.BDRip.HEVC.mkv",
|
||||||
|
"UHD BluRay REMUX BDRIP",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Movie.2026.2160p.UHD.BluRay.UHD.Remux.Remux.HEVC.mkv",
|
||||||
|
"UHD BluRay REMUX",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Movie.2026.1080p.WEB-DL.WEBRip.Remux.H264.mkv",
|
||||||
|
"WEB-DL WEBRip REMUX",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_python_metainfo_preserves_uhd_bluray_remux_resource_type():
|
@pytest.mark.parametrize(("title", "expected"), RESOURCE_TYPE_CASES)
|
||||||
"""Python 兜底解析应保留 UHD、BluRay 与 REMUX 三段资源类型。"""
|
def test_metainfo_preserves_all_resource_types(title, expected):
|
||||||
|
"""默认解析入口应按顺序保留并去重所有受支持的资源类型。"""
|
||||||
|
meta = MetaInfo(title)
|
||||||
|
|
||||||
|
assert meta.resource_type == expected
|
||||||
|
assert meta.edition.startswith(expected)
|
||||||
|
assert expected in meta.resource_term
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("title", "expected"), RESOURCE_TYPE_CASES)
|
||||||
|
def test_python_metainfo_preserves_all_resource_types(title, expected):
|
||||||
|
"""Python 兜底解析应按顺序保留并去重所有受支持的资源类型。"""
|
||||||
with patch("app.core.metainfo.rust_accel.parse_metainfo", return_value=None):
|
with patch("app.core.metainfo.rust_accel.parse_metainfo", return_value=None):
|
||||||
meta = MetaInfo(
|
meta = MetaInfo(title)
|
||||||
"They.Will.Kill.You.2026.2160p.UHD.BluRay.Remux."
|
|
||||||
"HEVC.DV.TrueHD.7.1.Atmos.mkv"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert meta.resource_type == "UHD BluRay REMUX"
|
assert meta.resource_type == expected
|
||||||
|
assert meta.edition.startswith(expected)
|
||||||
|
assert expected in meta.resource_term
|
||||||
|
|
||||||
|
|
||||||
def test_metainfo_routes_audio_filename_to_music():
|
def test_metainfo_routes_audio_filename_to_music():
|
||||||
|
|||||||
Reference in New Issue
Block a user