1+ import logging
2+
13from fastapi import APIRouter , Depends
24from pydantic import BaseModel
35
6+ from module .database import Database
47from module .downloader import DownloadClient
58from module .security .api import get_current_user
69
10+ logger = logging .getLogger (__name__ )
11+
712router = APIRouter (prefix = "/downloader" , tags = ["downloader" ])
813
914
@@ -16,6 +21,12 @@ class TorrentDeleteRequest(BaseModel):
1621 delete_files : bool = False
1722
1823
24+ class TorrentTagRequest (BaseModel ):
25+ """Request to tag a torrent with a bangumi ID."""
26+ hash : str
27+ bangumi_id : int
28+
29+
1930@router .get ("/torrents" , dependencies = [Depends (get_current_user )])
2031async def get_torrents ():
2132 async with DownloadClient () as client :
@@ -44,3 +55,91 @@ async def delete_torrents(req: TorrentDeleteRequest):
4455 async with DownloadClient () as client :
4556 await client .delete_torrent (hashes , delete_files = req .delete_files )
4657 return {"msg_en" : "Torrents deleted" , "msg_zh" : "种子已删除" }
58+
59+
60+ @router .post ("/torrents/tag" , dependencies = [Depends (get_current_user )])
61+ async def tag_torrent (req : TorrentTagRequest ):
62+ """Tag a torrent with a bangumi ID for accurate offset lookup.
63+
64+ This adds the 'ab:ID' tag to the torrent in qBittorrent, which allows
65+ the renamer to look up the correct episode/season offset.
66+ """
67+ # Verify bangumi exists
68+ with Database () as db :
69+ bangumi = db .bangumi .search_id (req .bangumi_id )
70+ if not bangumi :
71+ return {
72+ "status" : False ,
73+ "msg_en" : f"Bangumi { req .bangumi_id } not found" ,
74+ "msg_zh" : f"未找到番剧 { req .bangumi_id } " ,
75+ }
76+
77+ tag = f"ab:{ req .bangumi_id } "
78+ async with DownloadClient () as client :
79+ await client .add_tag (req .hash , tag )
80+
81+ return {
82+ "status" : True ,
83+ "msg_en" : f"Tagged torrent with { tag } " ,
84+ "msg_zh" : f"已为种子添加标签 { tag } " ,
85+ }
86+
87+
88+ @router .post ("/torrents/tag/auto" , dependencies = [Depends (get_current_user )])
89+ async def auto_tag_torrents ():
90+ """Auto-tag all untagged Bangumi torrents based on name/path matching.
91+
92+ This helps fix torrents that were added before tagging was implemented.
93+ Returns the number of torrents tagged and any that couldn't be matched.
94+ """
95+ tagged_count = 0
96+ unmatched = []
97+
98+ async with DownloadClient () as client :
99+ # Get all Bangumi torrents
100+ torrents = await client .get_torrent_info (category = "Bangumi" , status_filter = None )
101+
102+ with Database () as db :
103+ for torrent in torrents :
104+ torrent_hash = torrent ["hash" ]
105+ torrent_name = torrent ["name" ]
106+ save_path = torrent ["save_path" ]
107+ tags = torrent .get ("tags" , "" )
108+
109+ # Skip if already has ab: tag
110+ if "ab:" in tags :
111+ continue
112+
113+ # Try to match bangumi
114+ bangumi = None
115+
116+ # First try by torrent name
117+ bangumi = db .bangumi .match_torrent (torrent_name )
118+
119+ # Then try by save_path
120+ if not bangumi :
121+ bangumi = db .bangumi .match_by_save_path (save_path )
122+
123+ if bangumi and not bangumi .deleted :
124+ tag = f"ab:{ bangumi .id } "
125+ await client .add_tag (torrent_hash , tag )
126+ tagged_count += 1
127+ logger .info (
128+ f"[AutoTag] Tagged '{ torrent_name [:50 ]} ...' with { tag } "
129+ f"(matched: { bangumi .official_title } )"
130+ )
131+ else :
132+ unmatched .append ({
133+ "hash" : torrent_hash ,
134+ "name" : torrent_name ,
135+ "save_path" : save_path ,
136+ })
137+
138+ return {
139+ "status" : True ,
140+ "tagged_count" : tagged_count ,
141+ "unmatched_count" : len (unmatched ),
142+ "unmatched" : unmatched [:10 ], # Return first 10 unmatched for debugging
143+ "msg_en" : f"Tagged { tagged_count } torrents, { len (unmatched )} could not be matched" ,
144+ "msg_zh" : f"已标记 { tagged_count } 个种子,{ len (unmatched )} 个无法匹配" ,
145+ }
0 commit comments