mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 04:47:03 +08:00
252 lines
9.2 KiB
Python
252 lines
9.2 KiB
Python
"""
|
|
媒体服务器条目缓存的 ORM 模型与会话内数据操作。
|
|
|
|
会话及事务由调用方管理,本模块不负责提交或回滚。
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Any, List, Optional, Self
|
|
|
|
from sqlalchemy import JSON, Index, Select, String, delete, or_, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import Mapped, Session, mapped_column
|
|
|
|
from app.db.base import Base, execute_dml, get_id_column
|
|
from app.db.models._constraints import media_identity_constraint
|
|
from app.schemas.types import MediaSource
|
|
|
|
|
|
class MediaServerItem(Base):
|
|
"""
|
|
媒体服务器媒体条目表
|
|
"""
|
|
id = get_id_column()
|
|
# 服务器类型
|
|
server: Mapped[Optional[str]] = mapped_column(String)
|
|
# 媒体库ID
|
|
library: Mapped[Optional[str]] = mapped_column(String)
|
|
# 媒体服务器分配的条目标识。
|
|
item_id: Mapped[Optional[str]] = mapped_column(String, index=True)
|
|
# 类型
|
|
item_type: Mapped[Optional[str]] = mapped_column(String)
|
|
# 标题
|
|
title: Mapped[Optional[str]] = mapped_column(String, index=True)
|
|
# 原标题
|
|
original_title: Mapped[Optional[str]] = mapped_column(String)
|
|
# 年份
|
|
year: Mapped[Optional[str]] = mapped_column(String)
|
|
# 媒体数据源与原生ID
|
|
media_source: Mapped[Optional[str]] = mapped_column(String, index=True)
|
|
media_id: Mapped[Optional[str]] = mapped_column(String, index=True)
|
|
# 路径
|
|
path: Mapped[Optional[str]] = mapped_column(String)
|
|
# 季集
|
|
seasoninfo: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
|
|
# 备注
|
|
note: Mapped[Optional[Any]] = mapped_column(JSON)
|
|
# 同步时间
|
|
lst_mod_date: Mapped[Optional[str]] = mapped_column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
|
|
__table_args__ = (
|
|
media_identity_constraint("mediaserveritem"),
|
|
Index('ux_mediaserveritem_server_item_id', 'server', 'item_id', unique=True),
|
|
Index(
|
|
'ix_mediaserveritem_media_identity_type',
|
|
'media_source', 'media_id', 'item_type',
|
|
),
|
|
)
|
|
|
|
@classmethod
|
|
def get_by_itemid(cls, db: Session, item_id: str) -> Optional[Self]:
|
|
"""
|
|
按媒体条目 ID 查询首条记录。
|
|
"""
|
|
item: Optional[Self] = db.execute(select(cls).where(cls.item_id == item_id)).scalars().first()
|
|
return item
|
|
|
|
@classmethod
|
|
def get_by_server_itemid(cls, db: Session, server: str, item_id: str) -> Optional[Self]:
|
|
"""
|
|
在指定服务器中按媒体条目 ID 查询首条记录。
|
|
"""
|
|
item: Optional[Self] = db.execute(
|
|
select(cls).where(cls.server == server, cls.item_id == item_id)
|
|
).scalars().first()
|
|
return item
|
|
|
|
@classmethod
|
|
def empty(cls, db: Session, server: Optional[str] = None) -> None:
|
|
"""
|
|
在调用方事务中暂存媒体服务器条目清空操作。
|
|
"""
|
|
statement = delete(cls)
|
|
if server is not None:
|
|
statement = statement.where(cls.server == server)
|
|
db.execute(statement, execution_options={"synchronize_session": False})
|
|
|
|
@classmethod
|
|
def delete_stale(cls, db: Session, server: str, sync_time: str) -> int:
|
|
"""
|
|
在调用方事务中删除本轮同步未更新的条目。
|
|
"""
|
|
return execute_dml(
|
|
db,
|
|
delete(cls).where(
|
|
cls.server == server,
|
|
or_(cls.lst_mod_date.is_(None), cls.lst_mod_date != sync_time),
|
|
),
|
|
execution_options={"synchronize_session": False},
|
|
)
|
|
|
|
@classmethod
|
|
def delete_excluded_servers(cls, db: Session, servers: List[str]) -> int:
|
|
"""
|
|
在调用方事务中删除不属于启用服务器的条目。
|
|
"""
|
|
statement = delete(cls)
|
|
if servers:
|
|
statement = statement.where(
|
|
or_(cls.server.is_(None), ~cls.server.in_(servers))
|
|
)
|
|
return execute_dml(
|
|
db, statement, execution_options={"synchronize_session": False}
|
|
)
|
|
|
|
@classmethod
|
|
def exist_by_media_identity(
|
|
cls, db: Session, media_source: MediaSource | str, media_id: str,
|
|
mtype: Optional[str],
|
|
server: Optional[str] = None,
|
|
) -> Optional[Self]:
|
|
"""
|
|
按规范媒体身份和类型查询媒体服务器条目。
|
|
"""
|
|
statement = select(cls).where(
|
|
cls.media_source == str(media_source),
|
|
cls.media_id == str(media_id),
|
|
cls.item_type == mtype,
|
|
)
|
|
if server:
|
|
statement = statement.where(cls.server == server)
|
|
item: Optional[Self] = db.execute(statement).scalars().first()
|
|
return item
|
|
|
|
@classmethod
|
|
def exists_by_title(
|
|
cls, db: Session, title: str, mtype: Optional[str], year: Optional[str | int],
|
|
server: Optional[str] = None,
|
|
) -> Optional[Self]:
|
|
"""
|
|
按标题及可选类型、年份和服务器查询首条记录。
|
|
"""
|
|
statement = select(cls).where(cls.title == title)
|
|
if mtype:
|
|
statement = statement.where(cls.item_type == mtype)
|
|
if year:
|
|
statement = statement.where(cls.year == str(year))
|
|
if server:
|
|
statement = statement.where(cls.server == server)
|
|
item: Optional[Self] = db.execute(statement).scalars().first()
|
|
return item
|
|
|
|
@classmethod
|
|
def _cross_source_title_statement(
|
|
cls, title: str, mtype: Optional[str], year: str | int,
|
|
media_source: MediaSource | str, server: Optional[str] = None,
|
|
) -> Select[tuple["MediaServerItem"]]:
|
|
"""
|
|
构造跨来源标题回退查询;同来源条目在精确身份未命中时必然是另一部作品。
|
|
"""
|
|
statement = select(cls).where(
|
|
cls.title == title,
|
|
cls.year == str(year),
|
|
or_(cls.media_source.is_(None), cls.media_source != str(media_source)),
|
|
)
|
|
if mtype:
|
|
statement = statement.where(cls.item_type == mtype)
|
|
if server:
|
|
statement = statement.where(cls.server == server)
|
|
return statement.order_by(cls.id)
|
|
|
|
@classmethod
|
|
def list_cross_source_by_title(
|
|
cls, db: Session, title: str, mtype: Optional[str], year: str | int,
|
|
media_source: MediaSource | str, server: Optional[str] = None,
|
|
) -> List["MediaServerItem"]:
|
|
"""
|
|
返回可作为跨来源候选的同标题同年份条目。
|
|
"""
|
|
statement = cls._cross_source_title_statement(
|
|
title, mtype, year, media_source, server
|
|
)
|
|
return list(db.execute(statement).scalars().all())
|
|
|
|
@classmethod
|
|
async def async_get_by_itemid(cls, db: AsyncSession, item_id: str) -> Optional[Self]:
|
|
"""
|
|
异步按媒体条目 ID 查询首条记录。
|
|
"""
|
|
result = await db.execute(select(cls).filter(cls.item_id == item_id))
|
|
item: Optional[Self] = result.scalars().first()
|
|
return item
|
|
|
|
@classmethod
|
|
async def async_exist_by_media_identity(
|
|
cls, db: AsyncSession, media_source: MediaSource | str, media_id: str,
|
|
mtype: Optional[str],
|
|
server: Optional[str] = None,
|
|
) -> Optional[Self]:
|
|
"""
|
|
异步按规范媒体身份和类型查询媒体服务器条目。
|
|
"""
|
|
statement = select(cls).filter(
|
|
cls.media_source == str(media_source),
|
|
cls.media_id == str(media_id),
|
|
cls.item_type == mtype,
|
|
)
|
|
if server:
|
|
statement = statement.filter(cls.server == server)
|
|
result = await db.execute(statement)
|
|
item: Optional[Self] = result.scalars().first()
|
|
return item
|
|
|
|
@classmethod
|
|
async def async_list_cross_source_by_title(
|
|
cls, db: AsyncSession, title: str, mtype: Optional[str], year: str | int,
|
|
media_source: MediaSource | str, server: Optional[str] = None,
|
|
) -> List["MediaServerItem"]:
|
|
"""
|
|
异步返回可作为跨来源候选的同标题同年份条目。
|
|
"""
|
|
statement = cls._cross_source_title_statement(
|
|
title, mtype, year, media_source, server
|
|
)
|
|
result = await db.execute(statement)
|
|
return list(result.scalars().all())
|
|
|
|
@classmethod
|
|
async def async_exists_by_title(
|
|
cls, db: AsyncSession, title: str, mtype: Optional[str], year: Optional[str | int],
|
|
server: Optional[str] = None,
|
|
) -> Optional[Self]:
|
|
"""
|
|
异步按标题及可选类型、年份和服务器查询首条记录。
|
|
"""
|
|
if not mtype and not year:
|
|
statement = select(cls).filter(cls.title == title)
|
|
elif not year:
|
|
statement = select(cls).filter(cls.title == title, cls.item_type == mtype)
|
|
elif not mtype:
|
|
statement = select(cls).filter(cls.title == title, cls.year == str(year))
|
|
else:
|
|
statement = select(cls).filter(
|
|
cls.title == title,
|
|
cls.item_type == mtype,
|
|
cls.year == str(year),
|
|
)
|
|
if server:
|
|
statement = statement.filter(cls.server == server)
|
|
result = await db.execute(statement)
|
|
item: Optional[Self] = result.scalars().first()
|
|
return item
|