Files
MoviePilot/app/modules/themoviedb/category.py
2026-01-26 04:10:15 +00:00

226 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import shutil
from pathlib import Path
from typing import Union
import ruamel.yaml
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
from app.schemas.category import CategoryConfig
from app.utils.singleton import WeakSingleton
HEADER_COMMENTS = """####### 配置说明 #######
# 1. 该配置文件用于配置电影和电视剧的分类策略配置后程序会按照配置的分类策略名称进行分类配置文件采用yaml格式需要严格符合语法规则
# 2. 配置文件中的一级分类名称:`movie`、`tv` 为固定名称不可修改,二级名称同时也是目录名称,会按先后顺序匹配,匹配后程序会按这个名称建立二级目录
# 3. 支持的分类条件:
# `original_language` 语种,具体含义参考下方字典
# `production_countries` 国家或地区(电影)、`origin_country` 国家或地区(电视剧),具体含义参考下方字典
# `genre_ids` 内容类型,具体含义参考下方字典
# `release_year` 发行年份格式YYYY电影实际对应`release_date`字段,电视剧实际对应`first_air_date`字段,支持范围设定,如:`YYYY-YYYY`
# themoviedb 详情API返回的其它一级字段
# 4. 配置多项条件时需要同时满足,一个条件需要匹配多个值是使用`,`分隔
# 5. !条件值表示排除该值
"""
class CategoryHelper(metaclass=WeakSingleton):
"""
二级分类
"""
def __init__(self):
self._category_path: Path = settings.CONFIG_PATH / "category.yaml"
self._categorys = {}
self._movie_categorys = {}
self._tv_categorys = {}
self.init()
def init(self):
"""
初始化
"""
try:
if not self._category_path.exists():
shutil.copy(settings.INNER_CONFIG_PATH / "category.yaml", self._category_path)
with open(self._category_path, mode='r', encoding='utf-8') as f:
try:
yaml_loader = ruamel.yaml.YAML()
self._categorys = yaml_loader.load(f)
except Exception as e:
logger.warn(f"二级分类策略配置文件格式出现严重错误!请检查:{str(e)}")
self._categorys = {}
except Exception as err:
logger.warn(f"二级分类策略配置文件加载出错:{str(err)}")
if self._categorys:
self._movie_categorys = self._categorys.get('movie')
self._tv_categorys = self._categorys.get('tv')
logger.info(f"已加载二级分类策略 category.yaml")
def load(self) -> CategoryConfig:
"""
加载配置
"""
config = CategoryConfig()
if not self._category_path.exists():
return config
try:
with open(self._category_path, 'r', encoding='utf-8') as f:
yaml_loader = ruamel.yaml.YAML()
data = yaml_loader.load(f)
if data:
config = CategoryConfig(**data)
except Exception as e:
logger.error(f"Load category config failed: {e}")
return config
def save(self, config: CategoryConfig) -> bool:
"""
保存配置
"""
data = config.model_dump(exclude_none=True)
try:
with open(self._category_path, 'w', encoding='utf-8') as f:
f.write(HEADER_COMMENTS)
yaml_dumper = ruamel.yaml.YAML()
yaml_dumper.dump(data, f)
# 保存后重新加载配置
self.init()
return True
except Exception as e:
logger.error(f"Save category config failed: {e}")
return False
@property
def is_movie_category(self) -> bool:
"""
获取电影分类标志
"""
if self._movie_categorys:
return True
return False
@property
def is_tv_category(self) -> bool:
"""
获取电视剧分类标志
"""
if self._tv_categorys:
return True
return False
@property
def movie_categorys(self) -> list:
"""
获取电影分类清单
"""
if not self._movie_categorys:
return []
return list(self._movie_categorys.keys())
@property
def tv_categorys(self) -> list:
"""
获取电视剧分类清单
"""
if not self._tv_categorys:
return []
return list(self._tv_categorys.keys())
def get_movie_category(self, tmdb_info) -> str:
"""
判断电影的分类
:param tmdb_info: 识别的TMDB中的信息
:return: 二级分类的名称
"""
return self.get_category(self._movie_categorys, tmdb_info)
def get_tv_category(self, tmdb_info) -> str:
"""
判断电视剧的分类,包括动漫
:param tmdb_info: 识别的TMDB中的信息
:return: 二级分类的名称
"""
return self.get_category(self._tv_categorys, tmdb_info)
@staticmethod
def get_category(categorys: Union[dict, CommentedMap], tmdb_info: dict) -> str:
"""
根据 TMDB信息与分类配置文件进行比较确定所属分类
:param categorys: 分类配置
:param tmdb_info: TMDB信息
:return: 分类的名称
"""
if not tmdb_info:
return ""
if not categorys:
return ""
for key, item in categorys.items():
if not item:
return key
match_flag = True
for attr, value in item.items():
if not value:
continue
if attr == "release_year":
# 发行年份
info_value = tmdb_info.get("release_date") or tmdb_info.get("first_air_date")
if info_value:
info_value = str(info_value)[:4]
else:
info_value = tmdb_info.get(attr)
if not info_value:
match_flag = False
continue
elif attr == "production_countries":
# 制片国家
info_values = [str(val.get("iso_3166_1")).upper() for val in info_value] # type: ignore
else:
if isinstance(info_value, list):
info_values = [str(val).upper() for val in info_value]
else:
info_values = [str(info_value).upper()]
values = []
invert_values = []
# 如果有 "," 进行分割
values = [str(val) for val in value.split(",") if val]
expanded_values = []
for v in values:
if "-" not in v:
expanded_values.append(v)
continue
# - 表示范围
value_begin, value_end = v.split("-", 1)
prefix = ""
if value_begin.startswith('!'):
prefix = '!'
value_begin = value_begin[1:]
if value_begin.isdigit() and value_end.isdigit():
# 数字范围
expanded_values.extend(f"{prefix}{val}" for val in range(int(value_begin), int(value_end) + 1))
else:
# 字符串范围
expanded_values.extend([f"{prefix}{value_begin}", f"{prefix}{value_end}"])
values = list(map(str.upper, expanded_values))
invert_values = [val[1:] for val in values if val.startswith('!')]
values = [val for val in values if not val.startswith('!')]
if values and not set(values).intersection(set(info_values)):
match_flag = False
if invert_values and set(invert_values).intersection(set(info_values)):
match_flag = False
if match_flag:
return key
return ""