mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-01 13:37:24 +08:00
220 lines
7.9 KiB
Python
220 lines
7.9 KiB
Python
from typing import Literal, Optional
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from app.schemas.common import JsonData
|
|
from app.schemas.media import OptionalMediaIdentityMixin, RequiredMediaIdentityMixin
|
|
from app.schemas.types import MediaSource, MusicEntityType, MusicTargetEntityType
|
|
|
|
|
|
class MusicMeta(OptionalMediaIdentityMixin, BaseModel):
|
|
"""音乐名称及音频文件解析结果。"""
|
|
|
|
type: Literal["音乐"] = "音乐"
|
|
org_string: Optional[str] = None
|
|
title: Optional[str] = None
|
|
artists: list[str] = Field(default_factory=list)
|
|
artist: Optional[str] = None
|
|
album: Optional[str] = None
|
|
album_artist: Optional[str] = None
|
|
year: Optional[int] = None
|
|
disc_number: Optional[int] = None
|
|
track_number: Optional[int] = None
|
|
total_discs: Optional[int] = None
|
|
total_tracks: Optional[int] = None
|
|
version: Optional[str] = None
|
|
audio_format: Optional[str] = None
|
|
audio_lossless: Optional[bool] = None
|
|
audio_quality: Optional[Literal["hires", "lossless", "lossy"]] = None
|
|
audio_quality_score: int = 0
|
|
audio_specs: Optional[str] = None
|
|
bit_depth: Optional[int] = None
|
|
sample_rate: Optional[int] = None
|
|
bitrate: Optional[int] = None
|
|
duration: Optional[int] = None
|
|
isrc: Optional[str] = None
|
|
media_source: Optional[MediaSource] = None
|
|
media_id: Optional[str] = None
|
|
|
|
|
|
class MusicInfo(OptionalMediaIdentityMixin, BaseModel):
|
|
"""标准化音乐元数据信息。"""
|
|
|
|
type: Literal["音乐"] = "音乐"
|
|
# 音乐实体类型:recording 单曲、album 专辑、artist 艺术家
|
|
music_type: MusicEntityType = "recording"
|
|
media_source: Optional[MediaSource] = None
|
|
media_id: Optional[str] = None
|
|
title: Optional[str] = None
|
|
artists: list[str] = Field(default_factory=list)
|
|
artist: Optional[str] = None
|
|
artist_ids: list[str] = Field(default_factory=list)
|
|
album: Optional[str] = None
|
|
album_artist: Optional[str] = None
|
|
album_id: Optional[str] = None
|
|
album_type: Optional[str] = None
|
|
year: Optional[int] = None
|
|
release_date: Optional[str] = None
|
|
disc_number: Optional[int] = None
|
|
track_number: Optional[int] = None
|
|
total_tracks: Optional[int] = None
|
|
duration: Optional[int] = None
|
|
isrc: Optional[str] = None
|
|
cover_url: Optional[str] = None
|
|
lyrics: Optional[str] = None
|
|
version: Optional[str] = None
|
|
audio_format: Optional[str] = None
|
|
audio_lossless: Optional[bool] = None
|
|
audio_quality: Optional[Literal["hires", "lossless", "lossy"]] = None
|
|
audio_quality_score: int = 0
|
|
audio_specs: Optional[str] = None
|
|
bit_depth: Optional[int] = None
|
|
sample_rate: Optional[int] = None
|
|
bitrate: Optional[int] = None
|
|
category: Optional[str] = ""
|
|
genres: list[str] = Field(default_factory=list)
|
|
names: list[str] = Field(default_factory=list)
|
|
detail_link: Optional[str] = None
|
|
listen_count: Optional[int] = None
|
|
raw_data: dict[str, JsonData] = Field(default_factory=dict)
|
|
title_year: Optional[str] = None
|
|
poster_path: Optional[str] = None
|
|
backdrop_path: Optional[str] = None
|
|
overview: Optional[str] = None
|
|
vote_average: float = 0.0
|
|
|
|
@field_validator("album_type", mode="before")
|
|
@classmethod
|
|
def _strip_album_type(cls, v: Optional[str]) -> Optional[str]:
|
|
"""去除专辑类型字段两端的空白,防止数据源返回带空格的值。"""
|
|
return v.strip() if isinstance(v, str) and v.strip() else None
|
|
|
|
def __getattr__(self, name: str) -> None:
|
|
"""影视专用字段兜底返回 None:音乐模型不存在这些字段,避免下游逐点安全访问。
|
|
|
|
与 domain 层的 MusicInfo 保持一致:通知、整理等共享影视字段的读写路径
|
|
直接访问缺失属性时按空值处理,而不是抛 AttributeError。dunder 特殊方法
|
|
除外,避免 copy/pickle 等机制对钩子的 hasattr 探测误判。
|
|
"""
|
|
if name.startswith("__") and name.endswith("__"):
|
|
raise AttributeError(name)
|
|
return None
|
|
|
|
|
|
class MusicRelease(BaseModel):
|
|
"""音乐专辑下的单个发行版本。"""
|
|
|
|
media_id: Optional[str] = None
|
|
title: Optional[str] = None
|
|
date: Optional[str] = None
|
|
year: Optional[int] = None
|
|
country: Optional[str] = None
|
|
status: Optional[str] = None
|
|
packaging: Optional[str] = None
|
|
formats: list[str] = Field(default_factory=list)
|
|
track_count: Optional[int] = None
|
|
cover_url: Optional[str] = None
|
|
|
|
|
|
class MusicAlbumInfo(OptionalMediaIdentityMixin, BaseModel):
|
|
"""标准化音乐专辑信息。"""
|
|
|
|
type: Literal["音乐"] = "音乐"
|
|
music_type: Literal["album"] = "album"
|
|
media_source: Optional[MediaSource] = None
|
|
media_id: Optional[str] = None
|
|
title: Optional[str] = None
|
|
artists: list[str] = Field(default_factory=list)
|
|
artist: Optional[str] = None
|
|
artist_ids: list[str] = Field(default_factory=list)
|
|
album: Optional[str] = None
|
|
album_type: Optional[str] = None
|
|
secondary_types: list[str] = Field(default_factory=list)
|
|
year: Optional[int] = None
|
|
release_date: Optional[str] = None
|
|
total_tracks: Optional[int] = None
|
|
duration: Optional[int] = None
|
|
cover_url: Optional[str] = None
|
|
genres: list[str] = Field(default_factory=list)
|
|
tags: list[str] = Field(default_factory=list)
|
|
category: Optional[str] = ""
|
|
rating: float = 0.0
|
|
rating_votes: Optional[int] = None
|
|
detail_link: Optional[str] = None
|
|
tracks: list[MusicInfo] = Field(default_factory=list)
|
|
releases: list[MusicRelease] = Field(default_factory=list)
|
|
raw_data: dict[str, JsonData] = Field(default_factory=dict)
|
|
title_year: Optional[str] = None
|
|
poster_path: Optional[str] = None
|
|
backdrop_path: Optional[str] = None
|
|
overview: Optional[str] = None
|
|
vote_average: float = 0.0
|
|
|
|
@field_validator("album_type", mode="before")
|
|
@classmethod
|
|
def _strip_album_type(cls, v: Optional[str]) -> Optional[str]:
|
|
"""去除专辑类型字段两端的空白,防止数据源返回带空格的值。"""
|
|
return v.strip() if isinstance(v, str) and v.strip() else None
|
|
|
|
|
|
class MusicArtistInfo(OptionalMediaIdentityMixin, BaseModel):
|
|
"""标准化音乐艺术家信息。"""
|
|
|
|
type: Literal["音乐"] = "音乐"
|
|
music_type: Literal["artist"] = "artist"
|
|
media_source: Optional[MediaSource] = None
|
|
media_id: Optional[str] = None
|
|
name: Optional[str] = None
|
|
title: Optional[str] = None
|
|
sort_name: Optional[str] = None
|
|
disambiguation: Optional[str] = None
|
|
artist_type: Optional[str] = None
|
|
gender: Optional[str] = None
|
|
country: Optional[str] = None
|
|
area: Optional[str] = None
|
|
begin_date: Optional[str] = None
|
|
end_date: Optional[str] = None
|
|
ended: bool = False
|
|
life_span: Optional[str] = None
|
|
genres: list[str] = Field(default_factory=list)
|
|
tags: list[str] = Field(default_factory=list)
|
|
aliases: list[str] = Field(default_factory=list)
|
|
relation: Optional[str] = None
|
|
image_url: Optional[str] = None
|
|
detail_link: Optional[str] = None
|
|
external_links: dict[str, str] = Field(default_factory=dict)
|
|
album_count: Optional[int] = None
|
|
raw_data: dict[str, JsonData] = Field(default_factory=dict)
|
|
poster_path: Optional[str] = None
|
|
overview: Optional[str] = None
|
|
|
|
|
|
class MusicRecognizeRequest(RequiredMediaIdentityMixin, BaseModel):
|
|
"""音乐元数据详情识别请求。"""
|
|
|
|
media_source: MediaSource
|
|
media_id: str
|
|
music_type: Optional[MusicTargetEntityType] = None
|
|
|
|
|
|
class MusicRecognitionCacheItem(BaseModel):
|
|
"""单条 MusicBrainz 识别缓存。"""
|
|
|
|
key: str
|
|
media_id: str = ""
|
|
title: str = ""
|
|
artists: list[str] = Field(default_factory=list)
|
|
album: str = ""
|
|
year: str | int = ""
|
|
music_type: str = "recording"
|
|
cover_url: str = ""
|
|
|
|
|
|
class MusicRecognitionCacheData(BaseModel):
|
|
"""MusicBrainz 识别缓存统计及明细。"""
|
|
|
|
count: int = 0
|
|
recognized: int = 0
|
|
unrecognized: int = 0
|
|
data: list[MusicRecognitionCacheItem] = Field(default_factory=list)
|