mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-03-20 03:57:30 +08:00
226 lines
8.1 KiB
Python
226 lines
8.1 KiB
Python
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 ""
|