refactor: 收口 V3 分层架构与插件兼容边界

This commit is contained in:
jxxghp
2026-08-18 13:22:02 +08:00
parent cca99bd421
commit 8472bcff43
274 changed files with 10730 additions and 6130 deletions
+125 -178
View File
@@ -2,8 +2,6 @@ from typing import List, Any, Annotated, Optional
import cn2an
from fastapi import Request, BackgroundTasks, Depends, HTTPException, Header
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.schemas.common import IdData as _SchemaIdData
from app.schemas.response import Response as _SchemaResponse
@@ -19,7 +17,7 @@ from app.runtime.config import settings
from app.domain.context import MediaInfo
from app.runtime.events import eventmanager
from app.domain.metainfo import MetaInfo
from app.application.security.access import verify_token, verify_apitoken
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.application.subscription.delete import (
DeleteSubscribeCommand,
SubscribeDeletionActor,
@@ -31,20 +29,25 @@ from app.application.subscription.search import (
SearchSubscriptionsCommand,
SubscribeSearchActor,
)
from app.db import get_async_db, get_db
from app.db.models.subscribe import Subscribe
from app.db.models.subscribehistory import SubscribeHistory
from app.db.models.user import User
from app.db.oper.systemconfig import SystemConfigOper
from app.api.principal import ApiPrincipal
from app.application.subscription.query import SubscriptionQueryService
from app.application.subscription.mutation import (
SubscriptionActor,
SubscriptionMutationService,
)
from app.application.configuration import get_configured_system_config
from app.api.deps import (
get_current_active_user,
get_current_active_user_async,
get_delete_subscribe_command,
get_delete_subscriptions_by_identity_command,
get_search_subscriptions_command,
get_subscription_query_service,
get_subscription_mutation_service,
get_subscription_sync_mutation_service,
)
from app.adapters.external.server import MoviePilotServerHelper
from app.scheduler import Scheduler
from app.application.scheduling import Scheduler
from app.schemas.event import SubscribeModifiedEventData
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
@@ -82,16 +85,15 @@ def start_subscribe_add(
)
def build_subscribe_event_payload(subscribe: Subscribe) -> dict:
def build_subscribe_event_payload(subscribe: Any) -> dict:
"""
从 ORM 已加载字段构造订阅事件快照,避免异步接口里属性懒加载触发隐式 IO。
"""
values = subscribe.__dict__
return {column.name: values.get(column.name) for column in subscribe.__table__.columns}
return subscribe.to_dict()
def can_access_subscribe(
subscribe: Subscribe | SubscribeHistory | None, current_user: User
subscribe: Any, current_user: ApiPrincipal
) -> bool:
"""
判断当前用户是否可访问订阅及其历史记录。
@@ -107,33 +109,9 @@ def can_access_subscribe(
return bool(username) and username == current_user.name
async def get_accessible_subscribe(
db: AsyncSession, subscribe_id: int, current_user: User
) -> Subscribe | None:
"""
按订阅 ID 读取当前用户可访问的订阅行。
"""
subscribe = await Subscribe.async_get(db, subscribe_id)
if can_access_subscribe(subscribe, current_user):
return subscribe
return None
def get_accessible_subscribe_sync(
db: Session, subscribe_id: int, current_user: User
) -> Subscribe | None:
"""
同步读取当前用户可访问的订阅行。
"""
subscribe = Subscribe.get(db, subscribe_id)
if can_access_subscribe(subscribe, current_user):
return subscribe
return None
def select_accessible_subscribe(
subscribes: List[Subscribe], current_user: User
) -> Subscribe | None:
subscribes: List[Any], current_user: ApiPrincipal
) -> Any:
"""
从候选订阅中选择当前用户可访问的第一条记录。
"""
@@ -144,7 +122,7 @@ def select_accessible_subscribe(
def matches_subscribe_music_type(
subscribe: Subscribe,
subscribe: Any,
music_type: Optional[str],
) -> bool:
"""匹配订阅音乐实体,并把迁移前未标注类型的历史记录兼容为单曲。"""
@@ -155,54 +133,30 @@ def matches_subscribe_music_type(
or (music_type == MUSIC_ENTITY_RECORDING and subscribe_music_type is None)
async def list_subscribes_by_media_identity(
db: AsyncSession,
media_source: MediaSource,
media_id: str,
season: Optional[int] = None,
music_type: Optional[str] = None,
) -> List[Subscribe]:
"""按媒体来源、原生 ID 及音乐实体查询订阅。"""
subscribes = list(await Subscribe.async_list_by_media_identity(
db,
media_source=media_source,
media_id=media_id,
music_type=music_type,
))
unique_subscribes = {
subscribe.id: subscribe
for subscribe in subscribes
if matches_subscribe_music_type(subscribe, music_type)
}
if season is not None:
return [
subscribe for subscribe in unique_subscribes.values()
if subscribe.season == season
]
return list(unique_subscribes.values())
@router.get("/", summary="查询所有订阅", response_model=List[_SchemaSubscribe])
async def read_subscribes(
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
查询所有订阅
"""
if not current_user.is_superuser:
return await Subscribe.async_list_by_username(db, current_user.name)
return await Subscribe.async_list(db)
return await query.list_public(current_user.name)
return await query.list_public()
@router.get(
"/list", summary="查询所有订阅(API_TOKEN", response_model=List[_SchemaSubscribe]
)
async def list_subscribes(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
async def list_subscribes(
query: SubscriptionQueryService = Depends(get_subscription_query_service),
_: Annotated[str, Depends(verify_apitoken)] = None,
) -> Any:
"""
查询所有订阅 API_TOKEN认证(?token=xxx
"""
return await Subscribe.async_list()
return await query.list_public()
@router.post(
@@ -213,7 +167,7 @@ async def list_subscribes(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
async def create_subscribe(
*,
subscribe_in: _SchemaSubscribe,
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> _SchemaResponse:
"""
新增订阅
@@ -274,13 +228,17 @@ async def create_subscribe(
async def update_subscribe(
*,
subscribe_in: _SchemaSubscribe,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
更新订阅信息
"""
subscribe = await get_accessible_subscribe(db, subscribe_in.id, current_user)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
subscribe = await mutation.get_accessible(subscribe_in.id, actor)
if not subscribe:
return _SchemaResponse(success=False, message="订阅不存在")
old_subscribe_dict = subscribe.to_dict()
@@ -326,16 +284,21 @@ async def update_subscribe(
if total_episode_updated and subscribe_in.total_episode != subscribe.total_episode:
subscribe_dict["manual_total_episode"] = 1
# 更新到数据库
await subscribe.async_update(db, subscribe_dict)
# 重新获取更新后的订阅数据
updated_subscribe = await Subscribe.async_get(db, subscribe_in.id)
change = await mutation.update(
subscribe_in.id,
subscribe_dict,
actor,
existing=subscribe,
)
if not change:
return _SchemaResponse(success=False, message="订阅不存在")
# 发送订阅调整事件
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subscribe_in.id,
old_subscribe_info=old_subscribe_dict,
subscribe_info=updated_subscribe.to_dict() if updated_subscribe else {},
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="update",
).to_dict(),
)
@@ -346,29 +309,29 @@ async def update_subscribe(
async def update_subscribe_status(
subid: int,
state: str,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
更新订阅状态
"""
subscribe = await get_accessible_subscribe(db, subid, current_user)
if not subscribe:
return _SchemaResponse(success=False, message="订阅不存在")
valid_states = ["R", "P", "S"]
if state not in valid_states:
return _SchemaResponse(success=False, message="无效的订阅状态")
old_subscribe_dict = subscribe.to_dict()
await subscribe.async_update(db, {"state": state})
# 重新获取更新后的订阅数据
updated_subscribe = await Subscribe.async_get(db, subid)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
change = await mutation.update_status(subid, state, actor)
if not change:
return _SchemaResponse(success=False, message="订阅不存在")
# 发送订阅调整事件
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subid,
old_subscribe_info=old_subscribe_dict,
subscribe_info=updated_subscribe.to_dict() if updated_subscribe else {},
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="status",
).to_dict(),
)
@@ -382,22 +345,22 @@ async def subscribe_media_identity(
season: Optional[int] = None,
title: Optional[str] = None,
music_type: Optional[str] = None,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
根据媒体来源和原生 ID 查询订阅。
"""
subscribes = await list_subscribes_by_media_identity(
db, media_source, media_id, season, music_type
)
subscribes = await query.list_by_media_identity(media_source, media_id, music_type)
if season is not None:
subscribes = [subscribe for subscribe in subscribes if subscribe.season == season]
result = select_accessible_subscribe(subscribes, current_user)
return result if result else Subscribe()
return result if result else _SchemaSubscribe()
@router.get("/refresh", summary="刷新订阅", response_model=_SchemaResponse[None])
def refresh_subscribes(
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> Any:
"""
刷新所有订阅
@@ -411,44 +374,24 @@ def refresh_subscribes(
@router.get("/reset/{subid}", summary="重置订阅", response_model=_SchemaResponse[None])
async def reset_subscribes(
subid: int,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
重置订阅
"""
subscribe = await get_accessible_subscribe(db, subid, current_user)
if subscribe:
# 在更新之前获取旧数据
old_subscribe_dict = subscribe.to_dict()
# 更新订阅
await subscribe.async_update(
db,
{
"note": [],
"lack_episode": subscribe.total_episode,
"current_priority": None,
"current_audio_format": None,
"current_bitrate": None,
"current_bit_depth": None,
"current_sample_rate": None,
"episode_priority": {},
# 重置代表放弃手动总集数,后续订阅检查重新按 TMDB 集数更新。
"manual_total_episode": 0,
"state": "R",
},
)
# 重新获取更新后的订阅数据
updated_subscribe = await Subscribe.async_get(db, subid)
# 发送订阅调整事件
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
change = await mutation.reset(subid, actor)
if change:
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subid,
old_subscribe_info=old_subscribe_dict,
subscribe_info=updated_subscribe.to_dict()
if updated_subscribe
else {},
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="reset",
).to_dict(),
)
@@ -458,7 +401,7 @@ async def reset_subscribes(
@router.get("/check", summary="刷新订阅 TMDB 信息", response_model=_SchemaResponse[None])
def check_subscribes(
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> Any:
"""
刷新订阅 TMDB 信息
@@ -472,7 +415,7 @@ def check_subscribes(
@router.get("/search", summary="搜索所有订阅", response_model=_SchemaResponse[None])
async def search_subscribes(
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
搜索所有订阅
@@ -492,7 +435,7 @@ async def search_subscribes(
async def search_subscribe(
subscribe_id: int,
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
根据订阅编号搜索订阅
@@ -518,7 +461,7 @@ async def delete_subscribe_by_media_identity(
command: DeleteSubscriptionsByIdentityCommand = Depends(
get_delete_subscriptions_by_identity_command
),
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
根据任意媒体数据源 ID 删除订阅。
@@ -616,28 +559,18 @@ async def subscribe_history(
mtype: str,
page: Optional[int] = 1,
count: Optional[int] = 30,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
查询电影、电视剧或音乐订阅历史
"""
if current_user.is_superuser:
histories = await SubscribeHistory.async_list_by_type(
db, mtype=mtype, page=page, count=count
)
else:
histories = await SubscribeHistory.async_list_by_type_and_username(
db, mtype=mtype, username=current_user.name, page=page, count=count
)
result = []
for history in histories:
history_item = _SchemaSubscribe.model_validate(history, from_attributes=True)
if history_item.type == MediaType.TV.value:
history_item.total_episode = 0
history_item.lack_episode = 0
result.append(history_item)
return result
return await query.list_history(
mtype,
page=page,
count=count,
username=None if current_user.is_superuser else current_user.name,
)
@router.delete(
@@ -645,15 +578,17 @@ async def subscribe_history(
)
async def delete_subscribe_history(
history_id: int,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
删除订阅历史
"""
history = await SubscribeHistory.async_get(db, history_id)
if can_access_subscribe(history, current_user):
await SubscribeHistory.async_delete(db, history_id)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
await mutation.delete_history(history_id, actor)
return _SchemaResponse(success=True)
@@ -721,15 +656,15 @@ async def popular_subscribes(
)
async def user_subscribes(
username: str,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
查询用户订阅
"""
if not current_user.is_superuser and username != current_user.name:
return []
return await Subscribe.async_list_by_username(db, username)
return await query.list_public(username)
@router.get(
@@ -739,13 +674,17 @@ async def user_subscribes(
)
def subscribe_files(
subscribe_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
mutation: SubscriptionMutationService = Depends(get_subscription_sync_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> Any:
"""
订阅相关文件信息
"""
subscribe = get_accessible_subscribe_sync(db, subscribe_id, current_user)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
subscribe = mutation.get_accessible_sync(subscribe_id, actor)
if subscribe:
return SubscribeChain().subscribe_files_info(subscribe)
return _SchemaSubscrbieInfo()
@@ -754,13 +693,17 @@ def subscribe_files(
@router.post("/share", summary="分享订阅", response_model=_SchemaResponse[None])
async def subscribe_share(
sub: _SchemaSubscribeShare,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
分享订阅
"""
subscribe = await get_accessible_subscribe(db, sub.subscribe_id, current_user)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
subscribe = await mutation.get_accessible(sub.subscribe_id, actor)
if not subscribe:
return _SchemaResponse(success=False, message="订阅不存在")
state, errmsg = await MoviePilotServerHelper.async_sub_share(
@@ -786,7 +729,7 @@ async def subscribe_share_delete(
@router.post("/fork", summary="复用订阅", response_model=_SchemaResponse[None])
async def subscribe_fork(
sub: _SchemaSubscribeShare,
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
复用订阅
@@ -809,7 +752,7 @@ async def followed_subscribers(_: _SchemaTokenPayload = Depends(verify_token)) -
"""
查询已Follow的订阅分享人
"""
return SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
return get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
@router.post("/follow", summary="Follow订阅分享人", response_model=_SchemaResponse[None])
@@ -819,10 +762,10 @@ async def follow_subscriber(
"""
Follow订阅分享人
"""
subscribers = SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
if share_uid and share_uid not in subscribers:
subscribers.append(share_uid)
await SystemConfigOper().async_set(
await get_configured_system_config().async_set(
SystemConfigKey.FollowSubscribers, subscribers
)
return _SchemaResponse(success=True)
@@ -837,10 +780,10 @@ async def unfollow_subscriber(
"""
取消Follow订阅分享人
"""
subscribers = SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
if share_uid and share_uid in subscribers:
subscribers.remove(share_uid)
await SystemConfigOper().async_set(
await get_configured_system_config().async_set(
SystemConfigKey.FollowSubscribers, subscribers
)
return _SchemaResponse(success=True)
@@ -891,23 +834,27 @@ async def subscribe_share_statistics(
@router.get("/{subscribe_id}", summary="订阅详情", response_model=_SchemaSubscribe)
async def read_subscribe(
subscribe_id: int,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
根据订阅编号查询订阅信息
"""
if not subscribe_id:
return Subscribe()
subscribe = await get_accessible_subscribe(db, subscribe_id, current_user)
return subscribe if subscribe else Subscribe()
return _SchemaSubscribe()
subscribe = await query.get_public(subscribe_id)
return (
subscribe
if subscribe and can_access_subscribe(subscribe, current_user)
else _SchemaSubscribe()
)
@router.delete("/{subscribe_id}", summary="删除订阅", response_model=_SchemaResponse[None])
async def delete_subscribe(
subscribe_id: int,
command: DeleteSubscribeCommand = Depends(get_delete_subscribe_command),
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
删除订阅信息