from typing import Optional, List, Dict, Any from pydantic import BaseModel, Field, ConfigDict, model_validator from app.schemas.types import MediaType class Subscribe(BaseModel): id: Optional[int] = None # 订阅名称 name: Optional[str] = None # 订阅年份 year: Optional[str] = None # 订阅类型 电影/电视剧 type: Optional[str] = None # 搜索关键字 keyword: Optional[str] = None tmdbid: Optional[int] = None doubanid: Optional[str] = None bangumiid: Optional[int] = None mediaid: Optional[str] = None # 季号 season: Optional[int] = None # 海报 poster: Optional[str] = None # 背景图 backdrop: Optional[str] = None # 评分 vote: Optional[float] = 0.0 # 描述 description: Optional[str] = None # 过滤规则 filter: Optional[str] = None # 包含 include: Optional[str] = None # 排除 exclude: Optional[str] = None # 质量 quality: Optional[str] = None # 分辨率 resolution: Optional[str] = None # 特效 effect: Optional[str] = None # 总集数 total_episode: Optional[int] = 0 # 开始集数 start_episode: Optional[int] = 0 # 缺失集数 lack_episode: Optional[int] = 0 # 已完成集数 completed_episode: Optional[int] = None # 附加信息 note: Optional[Any] = None # 状态:N-新建, R-订阅中 state: Optional[str] = None # 最后更新时间 last_update: Optional[str] = None # 订阅用户 username: Optional[str] = None # 订阅站点 sites: Optional[List[int]] = Field(default_factory=list) # 下载器 downloader: Optional[str] = None # 是否洗版 best_version: Optional[int] = 0 # 是否只洗全集整包 best_version_full: Optional[int] = 0 # 当前优先级 current_priority: Optional[int] = None # 洗版时已下载剧集的优先级状态 episode_priority: Optional[Dict[str, int]] = None # 保存路径 save_path: Optional[str] = None # 是否使用 imdbid 搜索 search_imdbid: Optional[int] = 0 # 时间 date: Optional[str] = None # 自定义识别词 custom_words: Optional[str] = None # 自定义媒体类别 media_category: Optional[str] = None # 过滤规则组 filter_groups: Optional[List[str]] = Field(default_factory=list) # 剧集组 episode_group: Optional[str] = None model_config = ConfigDict(from_attributes=True) @model_validator(mode="after") def _fill_completed_episode(self) -> "Subscribe": """ 填充 ``completed_episode`` 派生字段。电视剧订阅按 best_version 分支计算, 电影或缺少 total_episode 时保持 None。 """ if self.completed_episode is not None: # 调用方显式提供过的值不覆盖 return self total_episode = self.total_episode or 0 if self.type != MediaType.TV.value or not total_episode: return self start_episode = self.start_episode or 1 if not self.best_version: lack = self.lack_episode or 0 self.completed_episode = max(total_episode - lack, 0) return self # 洗版口径:起始集前视为逻辑完成 + [start, total] 范围内 priority==100 命中。 # ``start_episode > total_episode`` 属于异常配置,需把 "起始集前" 偏移截断到 total, # 防止 completed_episode 越过分母 total_episode。 episode_priority = self.episode_priority or {} priority_completed = sum( 1 for ep_key, priority in episode_priority.items() if str(ep_key).isdigit() and start_episode <= int(ep_key) <= total_episode and priority == 100 ) self.completed_episode = min(max(start_episode - 1, 0), total_episode) + priority_completed return self class SubscribeShare(BaseModel): # 分享ID id: Optional[int] = None # 订阅ID subscribe_id: Optional[int] = None # 分享标题 share_title: Optional[str] = None # 分享说明 share_comment: Optional[str] = None # 分享人 share_user: Optional[str] = None # 分享人唯一ID share_uid: Optional[str] = None # 订阅名称 name: Optional[str] = None # 订阅年份 year: Optional[str] = None # 订阅类型 电影/电视剧 type: Optional[str] = None # 搜索关键字 keyword: Optional[str] = None tmdbid: Optional[int] = None doubanid: Optional[str] = None bangumiid: Optional[int] = None # 季号 season: Optional[int] = None # 海报 poster: Optional[str] = None # 背景图 backdrop: Optional[str] = None # 评分 vote: Optional[float] = 0.0 # 描述 description: Optional[str] = None # 包含 include: Optional[str] = None # 排除 exclude: Optional[str] = None # 质量 quality: Optional[str] = None # 分辨率 resolution: Optional[str] = None # 特效 effect: Optional[str] = None # 总集数 total_episode: Optional[int] = 0 # 时间 date: Optional[str] = None # 自定义识别词 custom_words: Optional[str] = None # 自定义媒体类别 media_category: Optional[str] = None # 自定义剧集组 episode_group: Optional[str] = None # 复用人次 count: Optional[int] = 0 class SubscribeShareStatistics(BaseModel): # 分享人 share_user: Optional[str] = None # 分享数量 share_count: Optional[int] = 0 # 总复用人次 total_reuse_count: Optional[int] = 0 class SubscribeDownloadFileInfo(BaseModel): # 种子名称 torrent_title: Optional[str] = None # 站点名称 site_name: Optional[str] = None # 下载器 downloader: Optional[str] = None # hash hash: Optional[str] = None # 文件路径 file_path: Optional[str] = None class SubscribeLibraryFileInfo(BaseModel): # 存储 storage: Optional[str] = "local" # 文件路径 file_path: Optional[str] = None class SubscribeEpisodeInfo(BaseModel): # 标题 title: Optional[str] = None # 描述 description: Optional[str] = None # 背景图 backdrop: Optional[str] = None # 下载文件信息 download: Optional[List[SubscribeDownloadFileInfo]] = Field(default_factory=list) # 媒体库文件信息 library: Optional[List[SubscribeLibraryFileInfo]] = Field(default_factory=list) class SubscrbieInfo(BaseModel): # 订阅信息 subscribe: Optional[Subscribe] = None # 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}} episodes: Optional[Dict[int, SubscribeEpisodeInfo]] = Field(default_factory=dict)