from typing import List, Any, Annotated, Optional import cn2an from fastapi import Request, Depends, HTTPException, Header from app.schemas.common import IdData as _SchemaIdData from app.schemas.response import Response as _SchemaResponse from app.schemas.subscribe import SubscrbieInfo as _SchemaSubscrbieInfo from app.schemas.subscribe import SubscribeShare as _SchemaSubscribeShare from app.schemas.subscribe import SubscribeShareStatistics as _SchemaSubscribeShareStatistics from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.workflow import MediaInfo as _SchemaMediaInfo from app.schemas.workflow import Subscribe as _SchemaSubscribe from app.api.response import ResponseAPIRouter from app.api.context import ( get_background_task_registry, resolve_background_task_registry, ) from app.chain.subscribe import SubscribeChain from app.runtime.events import eventmanager from app.domain.context import MediaInfo from app.domain.metainfo import MetaInfo from app.adapters.web.security.access import verify_token, verify_apitoken from app.application.subscription.delete import ( DeleteSubscribeCommand, SubscribeDeletionActor, ) from app.application.subscription.identity import ( DeleteSubscriptionsByIdentityCommand, ) from app.application.subscription.search import ( SearchSubscriptionsCommand, SubscribeSearchActor, ) 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_api_runtime_config_snapshot, get_configured_system_config, ) from app.api.dependencies.auth import ( get_current_active_user, get_current_active_user_async, ) from app.api.dependencies.subscription import ( 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.application.scheduling import get_scheduler from app.runtime.tasks import TaskRegistry from app.schemas.event import SubscribeModifiedEventData from app.schemas.types import ( MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaSource, MediaType, EventType, SystemConfigKey, ) from app.schemas.media import normalize_media_source, resolve_media_identity router = ResponseAPIRouter() def start_subscribe_add( title: str, year: str, mtype: MediaType, media_source: MediaSource, media_id: str, season: int, username: str, ): """ 启动订阅任务 """ SubscribeChain().add( title=title, year=year, mtype=mtype, media_source=media_source, media_id=media_id, season=season, username=username, ) def build_subscribe_event_payload(subscribe: Any) -> dict: """ 从 ORM 已加载字段构造订阅事件快照,避免异步接口里属性懒加载触发隐式 IO。 """ return subscribe.to_dict() def can_access_subscribe( subscribe: Any, current_user: ApiPrincipal ) -> bool: """ 判断当前用户是否可访问订阅及其历史记录。 超级用户拥有全局订阅管理能力;普通用户只能访问 username 精确匹配自己的订阅。 空 username 表示无法归属的 legacy 订阅,只能由超级用户管理。 """ if not subscribe: return False if current_user.is_superuser: return True username = subscribe.username return bool(username) and username == current_user.name def select_accessible_subscribe( subscribes: List[Any], current_user: ApiPrincipal ) -> Any: """ 从候选订阅中选择当前用户可访问的第一条记录。 """ for subscribe in subscribes or []: if can_access_subscribe(subscribe, current_user): return subscribe return None def matches_subscribe_music_type( subscribe: Any, music_type: Optional[str], ) -> bool: """匹配订阅音乐实体,并把迁移前未标注类型的历史记录兼容为单曲。""" if not music_type: return True subscribe_music_type = getattr(subscribe, "music_type", None) return subscribe_music_type == music_type \ or (music_type == MUSIC_ENTITY_RECORDING and subscribe_music_type is None) @router.get("/", summary="查询所有订阅", response_model=List[_SchemaSubscribe]) async def read_subscribes( 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 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( query: SubscriptionQueryService = Depends(get_subscription_query_service), _: Annotated[str, Depends(verify_apitoken)] = None, ) -> Any: """ 查询所有订阅 API_TOKEN认证(?token=xxx) """ return await query.list_public() @router.post( "/", summary="新增订阅", response_model=_SchemaResponse[_SchemaIdData], ) async def create_subscribe( *, subscribe_in: _SchemaSubscribe, current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> _SchemaResponse: """ 新增订阅 """ # 类型转换 if subscribe_in.type: mtype = MediaType(subscribe_in.type) else: mtype = None # 非 TMDB 来源的标题可能自带季标记,入库前统一拆分。 if ( mtype != MediaType.MUSIC and normalize_media_source(subscribe_in.media_source) not in (None, MediaSource.TMDB) ): meta = MetaInfo(subscribe_in.name) subscribe_in.name = meta.name if subscribe_in.season is None: subscribe_in.season = meta.begin_season # 标题转换 if subscribe_in.name: title = subscribe_in.name else: title = None subscribe_dict = subscribe_in.to_public_write_payload() identity_fields = {"media_source", "media_id"}.intersection( subscribe_in.model_fields_set ) if identity_fields: media_source, media_id = resolve_media_identity( media_source=subscribe_in.media_source, media_id=subscribe_in.media_id, ) if media_source and media_id: subscribe_dict["media_source"] = media_source subscribe_dict["media_id"] = media_id elif subscribe_in.media_source is None and subscribe_in.media_id is None: # 完整空对表示订阅暂无可用身份,与只提交其中一个字段语义不同。 subscribe_dict["media_source"] = None subscribe_dict["media_id"] = None else: return _SchemaResponse( success=False, message="新增订阅时必须同时提供有效的 media_source 和 media_id", ) subscribe_dict["username"] = current_user.name sid, message = await SubscribeChain().async_add( mtype=mtype, title=title, exist_ok=True, owner_scope=not current_user.is_superuser, **subscribe_dict, ) return _SchemaResponse(success=bool(sid), message=message, data={"id": sid}) @router.put("/", summary="更新订阅", response_model=_SchemaResponse[None]) async def update_subscribe( *, subscribe_in: _SchemaSubscribe, mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 更新订阅信息 """ 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() subscribe_dict = subscribe_in.to_public_write_payload(exclude_unset=True) identity_fields = {"media_source", "media_id"}.intersection( subscribe_in.model_fields_set ) if identity_fields: media_source, media_id = resolve_media_identity( media_source=subscribe_in.media_source, media_id=subscribe_in.media_id, ) if media_source and media_id: subscribe_dict["media_source"] = media_source subscribe_dict["media_id"] = media_id elif subscribe_in.media_source is None and subscribe_in.media_id is None: # 只有两个身份键都显式为空时才清空;全部省略则保留存量身份。 subscribe_dict["media_source"] = None subscribe_dict["media_id"] = None else: return _SchemaResponse( success=False, message="更新媒体身份时必须同时提供有效的 media_source 和 media_id", ) subscribe_dict["username"] = subscribe.username if getattr(subscribe, "type", None) == MediaType.MUSIC.value: # 音乐实体与曲目总数来自识别链,编辑接口不得把专辑改成单曲而提前完成订阅。 subscribe_dict["type"] = subscribe.type subscribe_dict["music_type"] = subscribe.music_type subscribe_dict["total_tracks"] = subscribe.total_tracks \ if subscribe.music_type == MUSIC_ENTITY_ALBUM else None total_episode_updated = "total_episode" in subscribe_in.model_fields_set if ( total_episode_updated and subscribe_in.total_episode and subscribe_in.total_episode > (subscribe.total_episode or 0) ): # 扩大目标范围时,新增加的集数尚无下载事实,应同步计入缺失集数。 subscribe_dict["lack_episode"] = (subscribe.lack_episode or 0) + ( subscribe_in.total_episode - (subscribe.total_episode or 0) ) # 是否手动修改过总集数 if total_episode_updated and subscribe_in.total_episode != subscribe.total_episode: subscribe_dict["manual_total_episode"] = 1 # 更新到数据库 change = await mutation.update( subscribe_in.id, subscribe_dict, actor, existing=subscribe, ) if not change: return _SchemaResponse(success=False, message="订阅不存在") if not change.event_published: await eventmanager.async_send_event( EventType.SubscribeModified, SubscribeModifiedEventData( subscribe_id=subscribe_in.id, old_subscribe_info=change.old, subscribe_info=change.new, scene="update", ).to_dict(), ) return _SchemaResponse(success=True) @router.put("/status/{subid}", summary="更新订阅状态", response_model=_SchemaResponse[None]) async def update_subscribe_status( subid: int, state: str, mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 更新订阅状态 """ valid_states = ["R", "P", "S"] if state not in valid_states: return _SchemaResponse(success=False, message="无效的订阅状态") 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="订阅不存在") if not change.event_published: await eventmanager.async_send_event( EventType.SubscribeModified, SubscribeModifiedEventData( subscribe_id=subid, old_subscribe_info=change.old, subscribe_info=change.new, scene="status", ).to_dict(), ) return _SchemaResponse(success=True) @router.get("/media/{media_id}", summary="查询订阅", response_model=_SchemaSubscribe) async def subscribe_media_identity( media_id: str, media_source: MediaSource, season: Optional[int] = None, title: Optional[str] = None, music_type: Optional[str] = None, query: SubscriptionQueryService = Depends(get_subscription_query_service), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 根据媒体来源和原生 ID 查询订阅。 """ 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 _SchemaSubscribe() @router.get("/refresh", summary="刷新订阅", response_model=_SchemaResponse[None]) def refresh_subscribes( current_user: ApiPrincipal = Depends(get_current_active_user), ) -> Any: """ 刷新所有订阅 """ if not current_user.is_superuser: return _SchemaResponse(success=False, message="订阅不存在") get_scheduler().start("subscribe_refresh") return _SchemaResponse(success=True) @router.get("/reset/{subid}", summary="重置订阅", response_model=_SchemaResponse[None]) async def reset_subscribes( subid: int, mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 重置订阅 """ actor = SubscriptionActor( name=current_user.name, is_superuser=current_user.is_superuser, ) change = await mutation.reset(subid, actor) if change: if not change.event_published: await eventmanager.async_send_event( EventType.SubscribeModified, SubscribeModifiedEventData( subscribe_id=subid, old_subscribe_info=change.old, subscribe_info=change.new, scene="reset", ).to_dict(), ) return _SchemaResponse(success=True) return _SchemaResponse(success=False, message="订阅不存在") @router.get("/check", summary="刷新订阅 TMDB 信息", response_model=_SchemaResponse[None]) def check_subscribes( current_user: ApiPrincipal = Depends(get_current_active_user), ) -> Any: """ 刷新订阅 TMDB 信息 """ if not current_user.is_superuser: return _SchemaResponse(success=False, message="订阅不存在") get_scheduler().start("subscribe_tmdb") return _SchemaResponse(success=True) @router.get("/search", summary="搜索所有订阅", response_model=_SchemaResponse[None]) async def search_subscribes( command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 搜索所有订阅 """ await command.execute( SubscribeSearchActor( username=current_user.name, is_superuser=current_user.is_superuser, ) ) return _SchemaResponse(success=True) @router.get( "/search/{subscribe_id}", summary="搜索订阅", response_model=_SchemaResponse[None] ) async def search_subscribe( subscribe_id: int, command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 根据订阅编号搜索订阅 """ found = await command.execute( SubscribeSearchActor( username=current_user.name, is_superuser=current_user.is_superuser, ), subscribe_id=subscribe_id, ) if not found: return _SchemaResponse(success=False, message="订阅不存在") return _SchemaResponse(success=True) @router.delete("/media/{media_id}", summary="删除订阅", response_model=_SchemaResponse[None]) async def delete_subscribe_by_media_identity( media_id: str, media_source: MediaSource, season: Optional[int] = None, music_type: Optional[str] = None, command: DeleteSubscriptionsByIdentityCommand = Depends( get_delete_subscriptions_by_identity_command ), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 根据任意媒体数据源 ID 删除订阅。 """ await command.execute( media_source, media_id, season, music_type, SubscribeDeletionActor( username=current_user.name, is_superuser=current_user.is_superuser, ), ) return _SchemaResponse(success=True) @router.post( "/seerr", summary="OverSeerr/JellySeerr通知订阅", response_model=_SchemaResponse[None] ) async def seerr_subscribe( request: Request, task_registry: Annotated[TaskRegistry, Depends(get_background_task_registry)], authorization: Annotated[str | None, Header()] = None, ) -> Any: """ Jellyseerr/Overseerr网络勾子通知订阅 """ if not authorization or authorization != get_api_runtime_config_snapshot().api_token: raise HTTPException( status_code=400, detail="授权失败", ) req_json = await request.json() if not req_json: raise HTTPException( status_code=500, detail="报文内容为空", ) notification_type = req_json.get("notification_type") if notification_type not in ["MEDIA_APPROVED", "MEDIA_AUTO_APPROVED"]: return _SchemaResponse(success=False, message="不支持的通知类型") subject = req_json.get("subject") media_type = ( MediaType.MOVIE if req_json.get("media", {}).get("media_type") == "movie" else MediaType.TV ) tmdbId = req_json.get("media", {}).get("tmdbId") if not media_type or not tmdbId or not subject: return _SchemaResponse(success=False, message="请求参数不正确") user_name = req_json.get("request", {}).get("requestedBy_username") # 添加订阅 if media_type == MediaType.MOVIE: resolve_background_task_registry(task_registry).create_sync( start_subscribe_add, mtype=media_type, media_source=MediaSource.TMDB, media_id=str(tmdbId), title=subject, year="", # 电影不传季号,避免被误判为剧集(S00)并污染通知标题 season=None, username=user_name, owner="api.subscribe.seerr", ) else: seasons = [] for extra in req_json.get("extra", []): if extra.get("name") == "Requested Seasons": seasons = [ int(str(sea).strip()) for sea in extra.get("value").split(", ") if str(sea).isdigit() ] break for season in seasons: resolve_background_task_registry(task_registry).create_sync( start_subscribe_add, mtype=media_type, media_source=MediaSource.TMDB, media_id=str(tmdbId), title=subject, year="", season=season, username=user_name, owner="api.subscribe.seerr", ) return _SchemaResponse(success=True) @router.get( "/history/{mtype}", summary="查询订阅历史", response_model=List[_SchemaSubscribe] ) async def subscribe_history( mtype: str, page: Optional[int] = 1, count: Optional[int] = 30, query: SubscriptionQueryService = Depends(get_subscription_query_service), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 查询电影、电视剧或音乐订阅历史 """ return await query.list_history( mtype, page=page, count=count, username=None if current_user.is_superuser else current_user.name, ) @router.delete( "/history/{history_id}", summary="删除订阅历史", response_model=_SchemaResponse[None] ) async def delete_subscribe_history( history_id: int, mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 删除订阅历史 """ actor = SubscriptionActor( name=current_user.name, is_superuser=current_user.is_superuser, ) await mutation.delete_history(history_id, actor) return _SchemaResponse(success=True) @router.get( "/popular", summary="热门订阅(基于用户共享数据)", response_model=List[_SchemaMediaInfo], ) async def popular_subscribes( stype: str, page: Optional[int] = 1, count: Optional[int] = 30, min_sub: Optional[int] = None, genre_id: Optional[int] = None, min_rating: Optional[float] = None, max_rating: Optional[float] = None, sort_type: Optional[str] = None, _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 查询热门订阅 """ subscribes = await MoviePilotServerHelper.async_get_subscribe_statistic( stype=stype, page=page, count=count, genre_id=genre_id, min_rating=min_rating, max_rating=max_rating, sort_type=sort_type, ) if subscribes: ret_medias = [] for sub in subscribes: # 订阅人数 count = sub.get("count") if min_sub and count < min_sub: continue media = MediaInfo() media.type = MediaType(sub.get("type")) media.media_source = normalize_media_source(sub.get("media_source")) media.media_id = str(sub.get("media_id")) if sub.get("media_id") is not None else None # 处理标题 title = sub.get("name") season = sub.get("season") if season not in (None, "") and int(season) != 1: # 小写数据转大写 season_str = cn2an.an2cn(season, "low") title = f"{title} 第{season_str}季" media.title = title media.year = sub.get("year") media.season = sub.get("season") media.overview = sub.get("description") media.vote_average = sub.get("vote") media.poster_path = sub.get("poster") media.backdrop_path = sub.get("backdrop") media.popularity = count ret_medias.append(media) return [media.to_dict() for media in ret_medias] return [] @router.get( "/user/{username}", summary="用户订阅", response_model=List[_SchemaSubscribe] ) async def user_subscribes( username: str, 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 query.list_public(username) @router.get( "/files/{subscribe_id}", summary="订阅相关文件信息", response_model=_SchemaSubscrbieInfo, ) def subscribe_files( subscribe_id: int, mutation: SubscriptionMutationService = Depends(get_subscription_sync_mutation_service), current_user: ApiPrincipal = Depends(get_current_active_user), ) -> Any: """ 订阅相关文件信息 """ 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() @router.post("/share", summary="分享订阅", response_model=_SchemaResponse[None]) async def subscribe_share( sub: _SchemaSubscribeShare, mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 分享订阅 """ 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( subscribe_id=sub.subscribe_id, share_title=sub.share_title, share_comment=sub.share_comment, share_user=sub.share_user, ) return _SchemaResponse(success=state, message=errmsg) @router.delete("/share/{share_id}", summary="删除分享", response_model=_SchemaResponse[None]) async def subscribe_share_delete( share_id: int, _: _SchemaTokenPayload = Depends(verify_token) ) -> Any: """ 删除分享 """ state, errmsg = await MoviePilotServerHelper.async_share_delete(share_id=share_id) return _SchemaResponse(success=state, message=errmsg) @router.post("/fork", summary="复用订阅", response_model=_SchemaResponse[None]) async def subscribe_fork( sub: _SchemaSubscribeShare, current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 复用订阅 """ sub_dict = sub.model_dump() sub_dict.pop("id") for key in list(sub_dict.keys()): if not hasattr(_SchemaSubscribe(), key): sub_dict.pop(key) result = await create_subscribe( subscribe_in=_SchemaSubscribe(**sub_dict), current_user=current_user ) if result.success: await MoviePilotServerHelper.async_sub_fork(share_id=sub.id) return result @router.get("/follow", summary="查询已Follow的订阅分享人", response_model=List[str]) async def followed_subscribers(_: _SchemaTokenPayload = Depends(verify_token)) -> Any: """ 查询已Follow的订阅分享人 """ return get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or [] @router.post("/follow", summary="Follow订阅分享人", response_model=_SchemaResponse[None]) async def follow_subscriber( share_uid: Optional[str] = None, _: _SchemaTokenPayload = Depends(verify_token) ) -> Any: """ Follow订阅分享人 """ subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or [] if share_uid and share_uid not in subscribers: subscribers.append(share_uid) await get_configured_system_config().async_set( SystemConfigKey.FollowSubscribers, subscribers ) return _SchemaResponse(success=True) @router.delete( "/follow", summary="取消Follow订阅分享人", response_model=_SchemaResponse[None] ) async def unfollow_subscriber( share_uid: Optional[str] = None, _: _SchemaTokenPayload = Depends(verify_token) ) -> Any: """ 取消Follow订阅分享人 """ subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or [] if share_uid and share_uid in subscribers: subscribers.remove(share_uid) await get_configured_system_config().async_set( SystemConfigKey.FollowSubscribers, subscribers ) return _SchemaResponse(success=True) @router.get( "/shares", summary="查询分享的订阅", response_model=List[_SchemaSubscribeShare] ) async def subscribe_shares( name: Optional[str] = None, page: Optional[int] = 1, count: Optional[int] = 30, genre_id: Optional[int] = None, min_rating: Optional[float] = None, max_rating: Optional[float] = None, sort_type: Optional[str] = None, _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 查询分享的订阅 """ return await MoviePilotServerHelper.async_get_subscribe_shares( name=name, page=page, count=count, genre_id=genre_id, min_rating=min_rating, max_rating=max_rating, sort_type=sort_type, ) @router.get( "/share/statistics", summary="查询订阅分享统计", response_model=List[_SchemaSubscribeShareStatistics], ) async def subscribe_share_statistics( _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 查询订阅分享统计 返回每个分享人分享的媒体数量以及总的复用人次 """ return await MoviePilotServerHelper.async_get_subscribe_share_statistics() @router.get("/{subscribe_id}", summary="订阅详情", response_model=_SchemaSubscribe) async def read_subscribe( subscribe_id: int, query: SubscriptionQueryService = Depends(get_subscription_query_service), current_user: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 根据订阅编号查询订阅信息 """ if not subscribe_id: 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: ApiPrincipal = Depends(get_current_active_user_async), ) -> Any: """ 删除订阅信息 """ await command.execute( subscribe_id, SubscribeDeletionActor( username=current_user.name, is_superuser=current_user.is_superuser, ), ) return _SchemaResponse(success=True)