fix: preserve all recognized resource types

This commit is contained in:
jxxghp
2026-08-09 18:41:54 +08:00
parent 36666313b7
commit 53a169d955
5 changed files with 95 additions and 63 deletions
+55 -42
View File
@@ -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:
+1 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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():