From 730bf8b261e77767f34169e4bcd94c58cabcfd09 Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 27 Mar 2026 11:52:59 -0300
Subject: [PATCH 01/13] 1
---
README.md | 32 +--
data/example-config.py | 8 +-
src/torrent_clients/qbittorrent.py | 1 +
src/trackers/MTEAM.py | 326 +++++++++++++++++++++++++++++
src/trackersetup.py | 13 +-
5 files changed, 357 insertions(+), 23 deletions(-)
create mode 100644 src/trackers/MTEAM.py
diff --git a/README.md b/README.md
index 5eeb5a4cd..3a8cdb24a 100644
--- a/README.md
+++ b/README.md
@@ -68,22 +68,22 @@ Many thanks to all who have contributed.
|ItaTorrents|ITT|LastDigitalUnderground|LDU|
|Lat-Team|LT|Locadora|LCD|
|LST|LST|Luminarr|LUME|
-|MoreThanTV|MTV|Nebulance|NBL|
-|OldToonsWorld|OTW|OnlyEncodes+|OE|
-|PassThePopcorn|PTP|PolishTorrent|PTT|
-|Portugas|PT|PrivateHD|PHD|
-|PTerClub|PTER|PTSKIT|PTS|
-|Racing4Everyone|R4E|Rastastugan|RAS|
-|ReelFLiX|RF|RetroFlix|RTF|
-|Samaritano|SAM|seedpool|SP|
-|ShareIsland|SHRI|SkipTheCommerials|STC|
-|SpeedApp|SPD|Swarmazon|SN|
-|The Leach Zone|TLZ|TheOldSchool|TOS|
-|ToTheGlory|TTG|TorrentHR|THR|
-|Torrenteros|TTR|TorrentLeech|TL|
-|TVChaosUK|TVC|ULCX|ULCX|
-|UTOPIA|UTP|YOiNKED|YOINK|
-|YUSCENE|YUS|||
+|MoreThanTV|MTV|M-Team|MTEAM|
+|Nebulance|NBL|OldToonsWorld|OTW|
+|OnlyEncodes+|OE|PassThePopcorn|PTP|
+|PolishTorrent|PTT|Portugas|PT|
+|PrivateHD|PHD|PTerClub|PTER|
+|PTSKIT|PTS|Racing4Everyone|R4E|
+|Rastastugan|RAS|ReelFLiX|RF|
+|RetroFlix|RTF|Samaritano|SAM|
+|seedpool|SP|ShareIsland|SHRI|
+|SkipTheCommerials|STC|SpeedApp|SPD|
+|Swarmazon|SN|The Leach Zone|TLZ|
+|TheOldSchool|TOS|ToTheGlory|TTG|
+|TorrentHR|THR|Torrenteros|TTR|
+|TorrentLeech|TL|TVChaosUK|TVC|
+|ULCX|ULCX|UTOPIA|UTP|
+|YOiNKED|YOINK|YUSCENE|YUS|
## **Setup:**
- **REQUIRES AT LEAST PYTHON 3.9 AND PIP3**
diff --git a/data/example-config.py b/data/example-config.py
index 0827842ac..49abfc13b 100644
--- a/data/example-config.py
+++ b/data/example-config.py
@@ -350,7 +350,7 @@
"TRACKERS": {
# Which trackers do you want to upload to?
- # Available tracker: A4K, ACM, AITHER, ANT, AR, ASC, AZ, BHD, BHDTV, BJS, BLU, BT, CBR, CZ, DC, DP, DT, EMUW, FF, FL, FNP, FRIKI, GPW, HDB, HDS, HDT, HHD, HUNO, IHD, IS, ITT, LCD, LDU, LST, LT, LUME, MTV, NBL, OE, OTW, PHD, PT, PTER, PTP, PTS, PTT, R4E, RAS, RF, RTF, SAM, SHRI, SN, SP, SPD, STC, THR, TIK, TL, TLZ, TOS, TTG, TTR, TVC, ULCX, UTP, YOINK, YUS
+ # Available tracker: A4K, ACM, AITHER, ANT, AR, ASC, AZ, BHD, BHDTV, BJS, BLU, BT, CBR, CZ, DC, DP, DT, EMUW, FF, FL, FNP, FRIKI, GPW, HDB, HDS, HDT, HHD, HUNO, IHD, IS, ITT, LCD, LDU, LST, LT, LUME, MTEAM, MTV, NBL, OE, OTW, PHD, PT, PTER, PTP, PTS, PTT, R4E, RAS, RF, RTF, SAM, SHRI, SN, SP, SPD, STC, THR, TIK, TL, TLZ, TOS, TTG, TTR, TVC, ULCX, UTP, YOINK, YUS
# Only add the trackers you want to upload to on a regular basis
"default_trackers": "",
@@ -676,6 +676,12 @@
# Send uploads to LUME modq for staff approval
"modq": False,
},
+ "MTEAM": {
+ # Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
+ "link_dir_name": "",
+ "api_key": "",
+ "anon": False,
+ },
"MTV": {
# Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
"link_dir_name": "",
diff --git a/src/torrent_clients/qbittorrent.py b/src/torrent_clients/qbittorrent.py
index b05f3d3f8..8bc40551f 100644
--- a/src/torrent_clients/qbittorrent.py
+++ b/src/torrent_clients/qbittorrent.py
@@ -1835,6 +1835,7 @@ async def match_tracker_url(tracker_urls: list[str], meta: dict[str, Any]) -> No
'lst': ["https://lst.gg"],
'lt': ["https://lat-team.com"],
'lume': ["https://luminarr.me"],
+ 'mteam': ["https://tracker.m-team.cc"],
'mtv': ["tracker.morethantv"],
'nbl': ["tracker.nebulance"],
'oe': ["https://onlyencodes.cc"],
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
new file mode 100644
index 000000000..baac5c26f
--- /dev/null
+++ b/src/trackers/MTEAM.py
@@ -0,0 +1,326 @@
+# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
+import os
+import unicodedata
+from typing import Any, Optional, cast
+
+import re
+import hmac
+import hashlib
+import base64
+import aiofiles
+import time
+import platform
+import httpx
+
+from cogs.redaction import Redaction
+from src.console import console
+from src.get_desc import DescriptionBuilder
+from src.rehostimages import RehostImagesManager
+from src.trackers.COMMON import COMMON
+
+Meta = dict[str, Any]
+Config = dict[str, Any]
+
+class MTEAM:
+ """
+ https://test2.m-team.cc/api/swagger-ui/index.html
+ https://wiki.m-team.cc/zh-tw/api
+ """
+ def __init__(self, config: Config):
+ self.config = config
+ self.common = COMMON(config)
+ self.tracker = 'MTEAM'
+ self.base_url = 'https://kp.m-team.cc'
+ self.api_base_url = f'https://api.m-team.cc/api'
+ self.torrent_url = f'{self.base_url}/detail'
+ self.banned_groups = ['']
+ self.api_key = self.config['TRACKERS'][self.tracker].get('api_key')
+ self.session = httpx.AsyncClient(
+ headers = {
+ "x-api-key": self.api_key,
+ "Content-Type": "application/json",
+ "Accept": "application/json"
+ },
+ timeout=30.0
+ )
+
+ async def mediainfo(self, meta: Meta) -> str:
+ mi_path: str = ""
+ mediainfo: str = ""
+
+ if meta.get('is_disc') == 'BDMV':
+ disc_folder = os.path.join(meta['base_dir'], 'tmp', meta['uuid'])
+ for filename in os.listdir(disc_folder):
+ if filename.endswith('_FULL.txt'):
+ mi_path = os.path.join(disc_folder, filename)
+ else:
+ mi_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO_CLEANPATH.txt"
+
+ if mi_path:
+ async with aiofiles.open(mi_path, encoding='utf-8') as f:
+ mediainfo = await f.read()
+
+ return mediainfo
+
+ async def generate_description(self, meta: Meta) -> str:
+ builder = DescriptionBuilder(self.tracker, self.config)
+ desc_parts: list[str] = []
+
+ # Custom Header
+ custom_header = await builder.get_custom_header()
+ desc_parts.append(custom_header)
+
+ # User description
+ user_description = await builder.get_user_description(meta)
+ desc_parts.append(user_description)
+
+ # Screenshots
+ all_images: list[dict[str, Any]] = []
+
+ menu_images = meta.get("menu_images")
+ menu_images_list: list[Any] = []
+ if isinstance(menu_images, list):
+ menu_images_list = cast(list[Any], menu_images)
+ all_images.extend(
+ [cast(dict[str, Any], img) for img in menu_images_list if isinstance(img, dict)]
+ )
+
+ images_key = f"{self.tracker}_images_key"
+ images_value = meta.get(images_key) if images_key in meta else meta.get("image_list")
+ images_list: list[Any] = []
+ if isinstance(images_value, list):
+ images_list = cast(list[Any], images_value)
+ all_images.extend(
+ [cast(dict[str, Any], img) for img in images_list if isinstance(img, dict)]
+ )
+
+ if all_images:
+ screenshots_block = ""
+ for image in all_images:
+ raw_url = image.get("raw_url")
+ screenshots_block += f""
+ if screenshots_block:
+ desc_parts.append(f"[center]{screenshots_block}[/center]")
+
+ # Tonemapped Header
+ tonemapped_header = await builder.get_tonemapped_header(meta)
+ desc_parts.append(tonemapped_header)
+
+ # Signature
+ desc_parts.append(f"[center][url=https://github.com/Audionut/Upload-Assistant]{meta['ua_signature']}[/url][/center]")
+
+ description = '\n\n'.join(part for part in desc_parts if part.strip())
+
+ from src.bbcode import BBCODE
+ bbcode = BBCODE()
+ description = description.strip()
+ description = bbcode.remove_extra_lines(description)
+
+ async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'w', encoding='utf-8') as description_file:
+ await description_file.write(description)
+
+ return description
+
+ def get_category_id(self, meta: Meta) -> Optional[int]:
+ movie_sd = 401 # Movie/SD
+ movie_hd = 419 # Movie/HD
+ movie_dvdiso = 420 # Movie/DVDiSo
+ movie_blu_ray = 421 # Movie/Blu-Ray
+ movie_remux = 439 # Movie/Remux
+ tv_series_sd = 403 # TV Series/SD
+ tv_series_hd = 402 # TV Series/HD
+ tv_series_bd = 438 # TV Series/BD
+ tv_series_dvdiso = 435 # TV Series/DVDiSo
+ anime = 405 # Anime
+
+ category_id = None
+
+
+
+ def get_small_description(self, meta: Meta) -> str:
+ resolution = meta.get('resolution', '')
+ audio = meta.get('audio', '')
+ video_bitrate, audio_bitrate = self.get_bitrates(meta)
+
+ return f"{resolution} @ {video_bitrate} kbps - {audio} @ {audio_bitrate} kbps"
+
+ def get_bitrates(self, meta) -> tuple[int, int]:
+ v_raw = None
+ a_raw = None
+ is_bdmv = meta.get("is_disc") == "BDMV"
+ is_dvd = meta.get("is_disc") == "DVD"
+
+ if is_bdmv:
+ discs = meta.get("discs", [])
+ if discs:
+ bdinfo = discs[0].get("bdinfo", {})
+ v_tracks = bdinfo.get("video", [])
+ a_tracks = bdinfo.get("audio", [])
+ if v_tracks: v_raw = v_tracks[0].get("bitrate")
+ if a_tracks: a_raw = a_tracks[0].get("bitrate")
+ elif is_dvd:
+ pass
+ else:
+ tracks = meta.get("mediainfo", {}).get("media", {}).get("track", [])
+ for track in tracks:
+ t_type = track.get("@type")
+ if t_type == "Video" and v_raw is None:
+ v_raw = track.get("BitRate")
+ elif t_type == "Audio" and a_raw is None:
+ a_raw = track.get("BitRate")
+
+ def clean_to_int(val, bdmv_mode):
+ if not val or isinstance(val, dict):
+ return 0
+
+ try:
+ if bdmv_mode:
+ numeric_match = re.search(r'\d+', str(val).replace('.', '').replace(',', ''))
+ return int(numeric_match.group()) if numeric_match else 0
+ else:
+ return int(val) // 1000
+ except (ValueError, TypeError, AttributeError):
+ return 0
+
+ return (clean_to_int(v_raw, is_bdmv), clean_to_int(a_raw, is_bdmv))
+
+ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]:
+ imdb_id = meta.get('imdb_info', {}).get('imdbID')
+
+ if not imdb_id:
+ print(f'[bold yellow]Cannot perform search on {self.tracker}: IMDb ID not found in metadata.[/bold yellow]')
+ return []
+
+ api_url = f"{self.api_base_url}/api/torrent/search"
+
+ payload = {
+ "mode": "normal",
+ "imdb": imdb_id,
+ }
+ dupes: list[dict[str, Any]] = []
+
+ try:
+ response = await self.session.post(
+ api_url,
+ json=payload, timeout=15
+ )
+ res_json = response.json()
+
+ if res_json.get('code') != '0':
+ print(f"[bold red]API Error: {res_json.get('message')}[/bold red]")
+ return []
+
+ torrents = res_json.get('data', {}).get('data', [])
+
+ for torrent in torrents:
+ t_id = torrent.get('id')
+ if not t_id:
+ continue
+
+ dupes.append({
+ 'name': torrent.get('name'),
+ 'size': int(torrent.get('size', 0)),
+ 'link': f"https://kp.m-team.cc/detail/{t_id}"
+ })
+
+ return dupes
+
+ except Exception as e:
+ print(f'[bold red]Error searching for IMDb ID {imdb_id} on {self.tracker}: {e}[/bold red]')
+
+ return []
+
+ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
+ data = {
+ "torrent": 0,
+ "offer": 0,
+ "name": meta["name"],
+ "smallDescr": self.get_small_description(meta),
+ "descr": await self.generate_description(meta),
+ "category": self.get_category_id(meta),
+ "source": 0,
+ "medium": 0,
+ "standard": 0,
+ "videoCodec": 0,
+ "audioCodec": 0,
+ "team": 0,
+ "processing": 0,
+ "countries": "",
+ "imdb": meta.get('imdb_info', {}).get('imdbID', ""),
+ "douban": "",
+ "dmmCode": "",
+ "cids": "",
+ "aids": "",
+ "anonymous": bool(meta.get('anonymous', False)),
+ "labels": 0,
+ "tags": "",
+ "file": "",
+ "nfo": "",
+ "mediainfo": "",
+ "mediaInfoAnalysisResult": True,
+ "labelsNew": ""
+ }
+
+ return data
+
+ async def upload(self, meta: Meta, _) -> bool:
+ data = await self.fetch_data(meta)
+ response = None
+
+ if not meta.get('debug', False):
+ try:
+ upload_url = f'{self.api_base_url}/upload'
+ await self.common.create_torrent_for_upload(meta, self.tracker, '[kp.m-team.cc] M-Team - TP')
+ torrent_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}].torrent"
+
+ async with aiofiles.open(torrent_path, 'rb') as torrent_file:
+ torrent_bytes = await torrent_file.read()
+ files = {'file': ('upload.torrent', torrent_bytes, 'application/x-bittorrent')}
+
+ response = await self.session.post(upload_url, data=data, files=files, headers=dict(self.session.headers), timeout=90)
+ response.raise_for_status()
+ response_json = response.json()
+ response_data: dict[str, Any] = cast(dict[str, Any], response_json) if isinstance(response_json, dict) else {}
+
+ if response_data.get('message') == "SUCCESS":
+ torrent_id = str(response_data['data']['id'])
+ meta['tracker_status'][self.tracker]['torrent_id'] = torrent_id
+ meta['tracker_status'][self.tracker]['status_message'] = response_data.get('message')
+
+ download_api_url = f"{self.api_base_url}/torrent/genDlToken?id={torrent_id}"
+ response = await self.session.post(download_api_url)
+ data = response.json()
+ final_download_url = data.get("data")
+ if final_download_url:
+ await self.common.download_tracker_torrent(
+ meta,
+ self.tracker,
+ headers=dict(self.session.headers),
+ downurl=final_download_url
+ )
+ return True
+ else:
+ meta['tracker_status'][self.tracker]['status_message'] = f"data error: {response_data.get('message', 'Unknown API error.')}"
+ return False
+
+ except httpx.HTTPStatusError as e:
+ meta['tracker_status'][self.tracker]['status_message'] = f'data error: HTTP {e.response.status_code} - {e.response.text}'
+ return False
+ except httpx.TimeoutException:
+ meta['tracker_status'][self.tracker]['status_message'] = f'data error: Request timed out after {self.session.timeout.write} seconds'
+ return False
+ except httpx.RequestError as e:
+ resp_text = getattr(getattr(e, 'response', None), 'text', 'No response received')
+ meta['tracker_status'][self.tracker]['status_message'] = f'data error: Unable to upload. Error: {e}.\nResponse: {resp_text}'
+ return False
+ except Exception as e:
+ resp_text = response.text if response is not None else 'No response received'
+ meta['tracker_status'][self.tracker]['status_message'] = f'data error: It may have uploaded, go check. Error: {e}.\nResponse: {resp_text}'
+ return False
+
+ else:
+ console.print("[cyan]DC Request Data:")
+ console.print(Redaction.redact_private_info(data))
+ meta['tracker_status'][self.tracker]['status_message'] = 'Debug mode enabled, not uploading'
+ await self.common.create_torrent_for_upload(meta, f"{self.tracker}" + "_DEBUG", f"{self.tracker}" + "_DEBUG", announce_url="https://fake.tracker")
+ return True # Debug mode - simulated success
diff --git a/src/trackersetup.py b/src/trackersetup.py
index ae2f13493..688ac7a88 100644
--- a/src/trackersetup.py
+++ b/src/trackersetup.py
@@ -51,6 +51,7 @@
from src.trackers.LST import LST
from src.trackers.LT import LT
from src.trackers.LUME import LUME
+from src.trackers.MTEAM import MTEAM
from src.trackers.MTV import MTV
from src.trackers.NBL import NBL
from src.trackers.OE import OE
@@ -1339,20 +1340,20 @@ async def make_trumpable_report(self, meta: Meta, tracker: str) -> bool:
tracker_class_map: dict[str, type[Any]] = {
'A4K': A4K, 'ACM': ACM, 'AITHER': AITHER, 'ANT': ANT, 'AR': AR, 'ASC': ASC, 'AZ': AZ, 'BHD': BHD, 'BHDTV': BHDTV, 'BJS': BJS, 'BLU': BLU, 'BT': BT, 'CBR': CBR,
'CZ': CZ, 'DC': DC, 'DP': DP, 'DT': DT, 'EMUW': EMUW, 'FNP': FNP, 'FF': FF, 'FL': FL, 'FRIKI': FRIKI, 'GPW': GPW, 'HDB': HDB, 'HDS': HDS, 'HDT': HDT, 'HHD': HHD, 'HUNO': HUNO, 'ITT': ITT,
- 'IHD': IHD, 'IS': IS, 'LCD': LCD, 'LDU': LDU, 'LST': LST, 'LT': LT, 'LUME': LUME, 'MTV': MTV, 'NBL': NBL, 'OE': OE, 'OTW': OTW, 'PHD': PHD, 'PT': PT, 'PTP': PTP, 'PTER': PTER, 'PTS': PTS, 'PTT': PTT,
+ 'IHD': IHD, 'IS': IS, 'LCD': LCD, 'LDU': LDU, 'LST': LST, 'LT': LT, 'LUME': LUME, 'MTEAM': MTEAM,'MTV': MTV, 'NBL': NBL, 'OE': OE, 'OTW': OTW, 'PHD': PHD, 'PT': PT, 'PTP': PTP, 'PTER': PTER, 'PTS': PTS, 'PTT': PTT,
'R4E': R4E, 'RAS': RAS, 'RF': RF, 'RTF': RTF, 'SAM': SAM, 'SHRI': SHRI, 'SN': SN, 'SP': SP, 'SPD': SPD, 'STC': STC, 'THR': THR,
'TIK': TIK, 'TL': TL, 'TLZ': TLZ, 'TOS': TOS, 'TVC': TVC, 'TTG': TTG, 'TTR': TTR, 'ULCX': ULCX, 'UTP': UTP, 'YOINK': YOINK, 'YUS': YUS
-}
+} # fmt: off
api_trackers = {
'A4K', 'ACM', 'AITHER', 'BHD', 'BLU', 'CBR', 'DP', 'DT', 'EMUW', 'FNP', 'FRIKI', 'HHD', 'HUNO', 'IHD', 'ITT', 'LCD', 'LDU', 'LST', 'LT', 'LUME',
'OE', 'OTW', 'PT', 'PTT', 'RAS', 'RF', 'R4E', 'SAM', 'SHRI', 'SP', 'STC', 'TIK', 'TLZ', 'TOS', 'TTR', 'ULCX', 'UTP', 'YOINK', 'YUS'
-}
+} # fmt: off
other_api_trackers = {
- 'ANT', 'BHDTV', 'DC', 'GPW', 'NBL', 'RTF', 'SN', 'SPD', 'TL', 'TVC'
-}
+ 'ANT', 'BHDTV', 'DC', 'GPW', 'MTEAM', 'NBL', 'RTF', 'SN', 'SPD', 'TL', 'TVC'
+} # fmt: off
http_trackers = {
'AR', 'ASC', 'AZ', 'BJS', 'BT', 'CZ', 'FF', 'FL', 'HDB', 'HDS', 'HDT', 'IS', 'MTV', 'PHD', 'PTER', 'PTS', 'TTG'
-}
+} # fmt: off
From 337c9e893d3d961132e8758dcda6d7f17712d7d4 Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 27 Mar 2026 14:55:22 -0300
Subject: [PATCH 02/13] 2
---
src/trackers/MTEAM.py | 291 ++++++++++++++++++++++++++++++++++--------
1 file changed, 237 insertions(+), 54 deletions(-)
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index baac5c26f..25c951982 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -32,14 +32,13 @@ def __init__(self, config: Config):
self.tracker = 'MTEAM'
self.base_url = 'https://kp.m-team.cc'
self.api_base_url = f'https://api.m-team.cc/api'
- self.torrent_url = f'{self.base_url}/detail'
+ self.torrent_url = f'{self.base_url}/detail/'
self.banned_groups = ['']
self.api_key = self.config['TRACKERS'][self.tracker].get('api_key')
self.session = httpx.AsyncClient(
headers = {
"x-api-key": self.api_key,
- "Content-Type": "application/json",
- "Accept": "application/json"
+ "Accept": "*/*",
},
timeout=30.0
)
@@ -62,6 +61,57 @@ async def mediainfo(self, meta: Meta) -> str:
return mediainfo
+ def bbcode_to_markdown(self, text):
+ specific_img_pattern = r'\[url=[^\]]*\]\[img(?:=[^\]]*)?\](.*?)\[/img\]\[/url\]'
+ text = re.sub(specific_img_pattern, r'', text, flags=re.IGNORECASE)
+
+ patterns = [
+ (r'\[b\](.*?)\[/b\]', r'**\1**'),
+ (r'\[i\](.*?)\[/i\]', r'*\1*'),
+ (r'\[u\](.*?)\[/u\]', r'\1'),
+ (r'\[s\](.*?)\[/s\]', r'~~\1~~'),
+ (r'\[img(?:=[^\]]*)?\](.*?)\[/img\]', r''),
+ (r'\[url=(.*?)\](.*?)\[/url\]', r'[\2](\1)'),
+ (r'\[url\](.*?)\[/url\]', r'<\1>'),
+ ]
+
+ for pattern, replacement in patterns:
+ text = re.sub(pattern, replacement, text, flags=re.IGNORECASE | re.DOTALL)
+
+ return text
+
+ def mteam_standard_desc(self, meta: Meta):
+ imdb = meta.get('imdb_info', {})
+
+ poster_url = imdb.get('cover', meta.get('poster', ''))
+ title = meta.get('title', 'N/A')
+ year = meta.get('year', 'N/A')
+ rating = imdb.get('rating', 'N/A')
+
+ writers = imdb.get('writers', [])
+ creators_str = " / ".join(writers)
+
+ cast = meta.get('tmdb_cast', [])
+ actors_str = " / ".join(cast)
+
+ plot = imdb.get('plot', meta.get('overview', ''))
+
+ desc = [
+ f"",
+ "",
+ f"**Title**: {title}",
+ f"**Year**: {year}",
+ f"**IMDb Rating**: {rating}/10",
+ f"**Creators**: {creators_str}",
+ f"**Actors**: {actors_str}",
+ "",
+ "### Introduction",
+ "",
+ f" {plot}"
+ ]
+
+ return "\n".join(desc)
+
async def generate_description(self, meta: Meta) -> str:
builder = DescriptionBuilder(self.tracker, self.config)
desc_parts: list[str] = []
@@ -70,50 +120,75 @@ async def generate_description(self, meta: Meta) -> str:
custom_header = await builder.get_custom_header()
desc_parts.append(custom_header)
+ # M-Team Standard Description
+ desc_parts.append(self.mteam_standard_desc(meta))
+
# User description
user_description = await builder.get_user_description(meta)
desc_parts.append(user_description)
- # Screenshots
- all_images: list[dict[str, Any]] = []
+ # Disc menus screenshots header
+ menu_images_value = meta.get("menu_images", [])
+ menu_images: list[dict[str, Any]] = []
+ if isinstance(menu_images_value, list):
+ menu_images_list = cast(list[Any], menu_images_value)
+ menu_images.extend(
+ [
+ cast(dict[str, Any], item)
+ for item in menu_images_list
+ if isinstance(item, dict)
+ ]
+ )
+ if menu_images:
+ desc_parts.append(await builder.menu_screenshot_header(meta))
+
+ # Disc menus screenshots
+ menu_screenshots_block = ""
+ for image in menu_images:
+ menu_raw_url = image.get("raw_url")
+ if menu_raw_url:
+ menu_screenshots_block += f""
+ if menu_screenshots_block:
+ desc_parts.append(menu_screenshots_block)
- menu_images = meta.get("menu_images")
- menu_images_list: list[Any] = []
- if isinstance(menu_images, list):
- menu_images_list = cast(list[Any], menu_images)
- all_images.extend(
- [cast(dict[str, Any], img) for img in menu_images_list if isinstance(img, dict)]
- )
+ # Tonemapped Header
+ desc_parts.append(await builder.get_tonemapped_header(meta))
- images_key = f"{self.tracker}_images_key"
- images_value = meta.get(images_key) if images_key in meta else meta.get("image_list")
- images_list: list[Any] = []
+ # Screenshot Header
+ images_value = meta.get("image_list", [])
+ images: list[dict[str, Any]] = []
if isinstance(images_value, list):
images_list = cast(list[Any], images_value)
- all_images.extend(
- [cast(dict[str, Any], img) for img in images_list if isinstance(img, dict)]
- )
-
- if all_images:
- screenshots_block = ""
- for image in all_images:
- raw_url = image.get("raw_url")
- screenshots_block += f""
- if screenshots_block:
- desc_parts.append(f"[center]{screenshots_block}[/center]")
-
- # Tonemapped Header
- tonemapped_header = await builder.get_tonemapped_header(meta)
- desc_parts.append(tonemapped_header)
+ images.extend(
+ [
+ cast(dict[str, Any], item)
+ for item in images_list
+ if isinstance(item, dict)
+ ]
+ )
+ if images:
+ desc_parts.append(await builder.screenshot_header())
+
+ # Screenshots
+ if images:
+ screenshots_block = ""
+ for image in images:
+ raw_url = image.get("raw_url")
+ if raw_url:
+ screenshots_block += f""
+ if screenshots_block:
+ desc_parts.append(screenshots_block)
# Signature
- desc_parts.append(f"[center][url=https://github.com/Audionut/Upload-Assistant]{meta['ua_signature']}[/url][/center]")
+ desc_parts.append(f"[{meta['ua_signature']}](https://github.com/Audionut/Upload-Assistant)")
description = '\n\n'.join(part for part in desc_parts if part.strip())
from src.bbcode import BBCODE
bbcode = BBCODE()
description = description.strip()
+ description = description.replace('[*] ', '• ').replace('[*]', '• ')
+ description = self.bbcode_to_markdown(description)
description = bbcode.remove_extra_lines(description)
async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'w', encoding='utf-8') as description_file:
@@ -133,9 +208,29 @@ def get_category_id(self, meta: Meta) -> Optional[int]:
tv_series_dvdiso = 435 # TV Series/DVDiSo
anime = 405 # Anime
- category_id = None
+ is_sd = meta.get('sd', False)
+ is_dvd = meta.get('is_disc') == 'DVD'
+ is_bd = meta.get('is_disc') == 'BDMV'
+ is_remux = meta.get('type', '') == "REMUX"
+ is_anime = meta.get('anime', False)
+
+ if is_anime:
+ return anime
+ if is_bd:
+ return tv_series_bd if meta['category'] == 'TV' else movie_blu_ray
+ if is_remux and meta['category'] == "MOVIE":
+ return movie_remux
+
+ if is_dvd:
+ return tv_series_dvdiso if meta['category'] == 'TV' else movie_dvdiso
+
+ if is_sd:
+ return tv_series_sd if meta['category'] == 'TV' else movie_sd
+
+ # Default to HD
+ return tv_series_hd if meta['category'] == 'TV' else movie_hd
def get_small_description(self, meta: Meta) -> str:
resolution = meta.get('resolution', '')
@@ -191,7 +286,7 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
print(f'[bold yellow]Cannot perform search on {self.tracker}: IMDb ID not found in metadata.[/bold yellow]')
return []
- api_url = f"{self.api_base_url}/api/torrent/search"
+ api_url = f"{self.api_base_url}/torrent/search"
payload = {
"mode": "normal",
@@ -230,35 +325,120 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
return []
+ def get_standard(self, meta: Meta) -> int:
+ _1080p = 1
+ _1080i = 2
+ _720p = 3
+ sd = 5
+ _4k = 6
+ _8k = 7
+
+ resolution = meta.get('resolution', '').lower()
+ if resolution == '1080p':
+ return _1080p
+ elif resolution == '1080i':
+ return _1080i
+ elif resolution == '720p':
+ return _720p
+ elif resolution == '2160p':
+ return _4k
+ elif resolution == '4320p':
+ return _8k
+ elif meta.get('sd', False):
+ return sd
+ else:
+ console.print(f"{self.tracker}: Unknown or unsupported resolution '{resolution}', defaulting to 1080p.")
+ return _1080p
+
+ def get_videocodec(self, meta: Meta) -> int:
+ x264 = 1 # H.264(x264/AVC)
+ x265 = 16 # H.265(x265/HEVC)
+ vc1 = 2 # VC-1
+ mpeg2 = 4 # MPEG-2
+ xvid = 3 # Xvid
+ av1 = 19 # AV1
+ vp8_9 = 21 # VP8/9
+
+ codec = meta.get('video_codec', '').lower()
+ if codec in ('h264', 'x264', 'avc', 'h.264', 'h.265'):
+ return x264
+ elif codec in ('h265', 'hevc', 'x265'):
+ return x265
+ elif codec in ('vc1', 'vc-1'):
+ return vc1
+ elif codec in ('mpeg2', 'mpeg-2'):
+ return mpeg2
+ elif codec == 'xvid':
+ return xvid
+ elif codec == 'av1':
+ return av1
+ elif codec in ('vp8', 'vp9'):
+ return vp8_9
+ else:
+ console.print(f"{self.tracker}: Unknown or unsupported video codec '{codec}', defaulting to x264.")
+ return x264
+
+ def get_audiocodec(self, meta: Meta) -> int:
+ aac = 6 # AAC
+ ac3 = 8 # AC3(DD)
+ dts = 3 # DTS
+ dts_hd_ma = 11 # DTS-HD MA
+ eac3 = 12 # E-AC3(DDP)
+ atmos_eac3 = 13 # E-AC3 Atoms(DDP Atoms)
+ true_hd = 9 # TrueHD
+
+ codec = meta.get('audio', '').lower()
+
+ if 'aac' in codec:
+ return aac
+ elif 'dd+' in codec:
+ return eac3
+ elif 'dd ' in codec:
+ return ac3
+ elif 'dts-hd' in codec:
+ return dts_hd_ma
+ elif 'dts' in codec:
+ return dts
+ elif 'atmos' in codec and 'dd+' in codec:
+ return atmos_eac3
+ elif 'truehd' in codec:
+ return true_hd
+ else:
+ console.print(f"{self.tracker}: Unknown or unsupported audio codec '{codec}', defaulting to AC3.")
+ return ac3
+
async def fetch_data(self, meta: Meta) -> dict[str, Any]:
+ """
+ https://test2.m-team.cc/api/swagger-ui/index.html#/種子/createOredit
+ """
data = {
- "torrent": 0,
- "offer": 0,
+ # "torrent": 0,
+ # "offer": 0,
"name": meta["name"],
"smallDescr": self.get_small_description(meta),
"descr": await self.generate_description(meta),
"category": self.get_category_id(meta),
- "source": 0,
- "medium": 0,
- "standard": 0,
- "videoCodec": 0,
- "audioCodec": 0,
- "team": 0,
- "processing": 0,
- "countries": "",
+ # "source": 0,
+ # "medium": 0,
+ "standard": self.get_standard(meta),
+ "videoCodec": self.get_videocodec(meta),
+ "audioCodec": self.get_audiocodec(meta),
+ # "team": 0,
+ # "processing": 0,
+ # "countries": "",
"imdb": meta.get('imdb_info', {}).get('imdbID', ""),
- "douban": "",
- "dmmCode": "",
- "cids": "",
- "aids": "",
+ # "douban": "",
+ # "dmmCode": "",
+ # "cids": "",
+ # "aids": "",
"anonymous": bool(meta.get('anonymous', False)),
- "labels": 0,
- "tags": "",
- "file": "",
- "nfo": "",
- "mediainfo": "",
+ # "labels": 0,
+ # "tags": "",
+ # "file": "",
+ # "nfo": "",
+ "mediainfo": await self.mediainfo(meta),
"mediaInfoAnalysisResult": True,
- "labelsNew": ""
+ # "labelsNew": ""
}
return data
@@ -269,7 +449,7 @@ async def upload(self, meta: Meta, _) -> bool:
if not meta.get('debug', False):
try:
- upload_url = f'{self.api_base_url}/upload'
+ upload_url = f'{self.api_base_url}/torrent/createOredit'
await self.common.create_torrent_for_upload(meta, self.tracker, '[kp.m-team.cc] M-Team - TP')
torrent_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}].torrent"
@@ -299,6 +479,9 @@ async def upload(self, meta: Meta, _) -> bool:
downurl=final_download_url
)
return True
+ console.print(f"{self.tracker}: Failed to get download URL from API response.")
+ meta['tracker_status'][self.tracker]['status_message'] = "Failed to get download URL from API response"
+ return False
else:
meta['tracker_status'][self.tracker]['status_message'] = f"data error: {response_data.get('message', 'Unknown API error.')}"
return False
From 076394d47207eed5206419b77ac6a4ec365437db Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 27 Mar 2026 15:17:37 -0300
Subject: [PATCH 03/13] format
---
src/trackers/MTEAM.py | 289 +++++++++++++++++++-----------------------
1 file changed, 132 insertions(+), 157 deletions(-)
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index 25c951982..10183a1d8 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -1,78 +1,73 @@
# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
import os
-import unicodedata
+import re
from typing import Any, Optional, cast
-import re
-import hmac
-import hashlib
-import base64
import aiofiles
-import time
-import platform
import httpx
from cogs.redaction import Redaction
from src.console import console
from src.get_desc import DescriptionBuilder
-from src.rehostimages import RehostImagesManager
from src.trackers.COMMON import COMMON
Meta = dict[str, Any]
Config = dict[str, Any]
+
class MTEAM:
"""
https://test2.m-team.cc/api/swagger-ui/index.html
https://wiki.m-team.cc/zh-tw/api
"""
+
def __init__(self, config: Config):
self.config = config
self.common = COMMON(config)
- self.tracker = 'MTEAM'
- self.base_url = 'https://kp.m-team.cc'
- self.api_base_url = f'https://api.m-team.cc/api'
- self.torrent_url = f'{self.base_url}/detail/'
- self.banned_groups = ['']
- self.api_key = self.config['TRACKERS'][self.tracker].get('api_key')
+ self.tracker = "MTEAM"
+ self.base_url = "https://kp.m-team.cc"
+ self.api_base_url = "https://api.m-team.cc/api"
+ self.torrent_url = f"{self.base_url}/detail/"
+ self.banned_groups = [""]
+ self.api_key = self.config["TRACKERS"][self.tracker].get("api_key")
self.session = httpx.AsyncClient(
- headers = {
+ headers={
"x-api-key": self.api_key,
"Accept": "*/*",
},
- timeout=30.0
- )
+ timeout=30.0,
+ )
async def mediainfo(self, meta: Meta) -> str:
mi_path: str = ""
mediainfo: str = ""
- if meta.get('is_disc') == 'BDMV':
- disc_folder = os.path.join(meta['base_dir'], 'tmp', meta['uuid'])
+ if meta.get("is_disc") == "BDMV":
+ disc_folder = os.path.join(meta["base_dir"], "tmp", meta["uuid"])
for filename in os.listdir(disc_folder):
- if filename.endswith('_FULL.txt'):
+ if filename.endswith("_FULL.txt"):
mi_path = os.path.join(disc_folder, filename)
else:
mi_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO_CLEANPATH.txt"
if mi_path:
- async with aiofiles.open(mi_path, encoding='utf-8') as f:
+ async with aiofiles.open(mi_path, encoding="utf-8") as f:
mediainfo = await f.read()
return mediainfo
def bbcode_to_markdown(self, text):
- specific_img_pattern = r'\[url=[^\]]*\]\[img(?:=[^\]]*)?\](.*?)\[/img\]\[/url\]'
- text = re.sub(specific_img_pattern, r'', text, flags=re.IGNORECASE)
+ specific_img_pattern = r"\[url=[^\]]*\]\[img(?:=[^\]]*)?\](.*?)\[/img\]\[/url\]"
+ text = re.sub(specific_img_pattern, r"", text, flags=re.IGNORECASE)
patterns = [
- (r'\[b\](.*?)\[/b\]', r'**\1**'),
- (r'\[i\](.*?)\[/i\]', r'*\1*'),
- (r'\[u\](.*?)\[/u\]', r'\1'),
- (r'\[s\](.*?)\[/s\]', r'~~\1~~'),
- (r'\[img(?:=[^\]]*)?\](.*?)\[/img\]', r''),
- (r'\[url=(.*?)\](.*?)\[/url\]', r'[\2](\1)'),
- (r'\[url\](.*?)\[/url\]', r'<\1>'),
+ (r"\[b\](.*?)\[/b\]", r"**\1**"),
+ (r"\[i\](.*?)\[/i\]", r"*\1*"),
+ (r"\[u\](.*?)\[/u\]", r"\1"),
+ (r"\[s\](.*?)\[/s\]", r"~~\1~~"),
+ (r"\[img(?:=[^\]]*)?\](.*?)\[/img\]", r""),
+ (r"\[url=(.*?)\](.*?)\[/url\]", r"[\2](\1)"),
+ (r"\[url\](.*?)\[/url\]", r"<\1>"),
]
for pattern, replacement in patterns:
@@ -81,20 +76,21 @@ def bbcode_to_markdown(self, text):
return text
def mteam_standard_desc(self, meta: Meta):
- imdb = meta.get('imdb_info', {})
+ imdb = meta.get("imdb_info", {})
- poster_url = imdb.get('cover', meta.get('poster', ''))
- title = meta.get('title', 'N/A')
- year = meta.get('year', 'N/A')
- rating = imdb.get('rating', 'N/A')
+ tmdb_poster = f"https://image.tmdb.org/t/p/w200{meta.get('tmdb_poster')}"
+ poster_url = tmdb_poster if tmdb_poster else imdb.get("cover", "")
+ title = meta.get("title", "N/A")
+ year = meta.get("year", "N/A")
+ rating = imdb.get("rating", "N/A")
- writers = imdb.get('writers', [])
+ writers = imdb.get("writers", [])
creators_str = " / ".join(writers)
- cast = meta.get('tmdb_cast', [])
+ cast = meta.get("tmdb_cast", [])
actors_str = " / ".join(cast)
- plot = imdb.get('plot', meta.get('overview', ''))
+ plot = imdb.get("plot", meta.get("overview", ""))
desc = [
f"",
@@ -107,7 +103,7 @@ def mteam_standard_desc(self, meta: Meta):
"",
"### Introduction",
"",
- f" {plot}"
+ f" {plot}",
]
return "\n".join(desc)
@@ -132,13 +128,7 @@ async def generate_description(self, meta: Meta) -> str:
menu_images: list[dict[str, Any]] = []
if isinstance(menu_images_value, list):
menu_images_list = cast(list[Any], menu_images_value)
- menu_images.extend(
- [
- cast(dict[str, Any], item)
- for item in menu_images_list
- if isinstance(item, dict)
- ]
- )
+ menu_images.extend([cast(dict[str, Any], item) for item in menu_images_list if isinstance(item, dict)])
if menu_images:
desc_parts.append(await builder.menu_screenshot_header(meta))
@@ -159,13 +149,7 @@ async def generate_description(self, meta: Meta) -> str:
images: list[dict[str, Any]] = []
if isinstance(images_value, list):
images_list = cast(list[Any], images_value)
- images.extend(
- [
- cast(dict[str, Any], item)
- for item in images_list
- if isinstance(item, dict)
- ]
- )
+ images.extend([cast(dict[str, Any], item) for item in images_list if isinstance(item, dict)])
if images:
desc_parts.append(await builder.screenshot_header())
@@ -182,59 +166,60 @@ async def generate_description(self, meta: Meta) -> str:
# Signature
desc_parts.append(f"[{meta['ua_signature']}](https://github.com/Audionut/Upload-Assistant)")
- description = '\n\n'.join(part for part in desc_parts if part.strip())
+ description = "\n\n".join(part for part in desc_parts if part.strip())
from src.bbcode import BBCODE
+
bbcode = BBCODE()
description = description.strip()
- description = description.replace('[*] ', '• ').replace('[*]', '• ')
+ description = description.replace("[*] ", "• ").replace("[*]", "• ")
description = self.bbcode_to_markdown(description)
description = bbcode.remove_extra_lines(description)
- async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'w', encoding='utf-8') as description_file:
+ async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", "w", encoding="utf-8") as description_file:
await description_file.write(description)
return description
def get_category_id(self, meta: Meta) -> Optional[int]:
- movie_sd = 401 # Movie/SD
- movie_hd = 419 # Movie/HD
- movie_dvdiso = 420 # Movie/DVDiSo
- movie_blu_ray = 421 # Movie/Blu-Ray
- movie_remux = 439 # Movie/Remux
- tv_series_sd = 403 # TV Series/SD
- tv_series_hd = 402 # TV Series/HD
- tv_series_bd = 438 # TV Series/BD
+ movie_sd = 401 # Movie/SD
+ movie_hd = 419 # Movie/HD
+ movie_dvdiso = 420 # Movie/DVDiSo
+ movie_blu_ray = 421 # Movie/Blu-Ray
+ movie_remux = 439 # Movie/Remux
+ tv_series_sd = 403 # TV Series/SD
+ tv_series_hd = 402 # TV Series/HD
+ tv_series_bd = 438 # TV Series/BD
tv_series_dvdiso = 435 # TV Series/DVDiSo
- anime = 405 # Anime
+ anime = 405 # Anime
- is_sd = meta.get('sd', False)
- is_dvd = meta.get('is_disc') == 'DVD'
- is_bd = meta.get('is_disc') == 'BDMV'
- is_remux = meta.get('type', '') == "REMUX"
- is_anime = meta.get('anime', False)
+ is_sd = meta.get("sd", False)
+ is_dvd = meta.get("is_disc") == "DVD"
+ is_bd = meta.get("is_disc") == "BDMV"
+ is_remux = meta.get("type", "") == "REMUX"
+ is_anime = meta.get("anime", False)
if is_anime:
return anime
if is_bd:
- return tv_series_bd if meta['category'] == 'TV' else movie_blu_ray
+ return tv_series_bd if meta["category"] == "TV" else movie_blu_ray
- if is_remux and meta['category'] == "MOVIE":
+ if is_remux and meta["category"] == "MOVIE":
return movie_remux
if is_dvd:
- return tv_series_dvdiso if meta['category'] == 'TV' else movie_dvdiso
+ return tv_series_dvdiso if meta["category"] == "TV" else movie_dvdiso
if is_sd:
- return tv_series_sd if meta['category'] == 'TV' else movie_sd
+ return tv_series_sd if meta["category"] == "TV" else movie_sd
# Default to HD
- return tv_series_hd if meta['category'] == 'TV' else movie_hd
+ return tv_series_hd if meta["category"] == "TV" else movie_hd
def get_small_description(self, meta: Meta) -> str:
- resolution = meta.get('resolution', '')
- audio = meta.get('audio', '')
+ resolution = meta.get("resolution", "")
+ audio = meta.get("audio", "")
video_bitrate, audio_bitrate = self.get_bitrates(meta)
return f"{resolution} @ {video_bitrate} kbps - {audio} @ {audio_bitrate} kbps"
@@ -251,8 +236,10 @@ def get_bitrates(self, meta) -> tuple[int, int]:
bdinfo = discs[0].get("bdinfo", {})
v_tracks = bdinfo.get("video", [])
a_tracks = bdinfo.get("audio", [])
- if v_tracks: v_raw = v_tracks[0].get("bitrate")
- if a_tracks: a_raw = a_tracks[0].get("bitrate")
+ if v_tracks:
+ v_raw = v_tracks[0].get("bitrate")
+ if a_tracks:
+ a_raw = a_tracks[0].get("bitrate")
elif is_dvd:
pass
else:
@@ -270,7 +257,7 @@ def clean_to_int(val, bdmv_mode):
try:
if bdmv_mode:
- numeric_match = re.search(r'\d+', str(val).replace('.', '').replace(',', ''))
+ numeric_match = re.search(r"\d+", str(val).replace(".", "").replace(",", ""))
return int(numeric_match.group()) if numeric_match else 0
else:
return int(val) // 1000
@@ -280,10 +267,10 @@ def clean_to_int(val, bdmv_mode):
return (clean_to_int(v_raw, is_bdmv), clean_to_int(a_raw, is_bdmv))
async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]:
- imdb_id = meta.get('imdb_info', {}).get('imdbID')
+ imdb_id = meta.get("imdb_info", {}).get("imdbID")
if not imdb_id:
- print(f'[bold yellow]Cannot perform search on {self.tracker}: IMDb ID not found in metadata.[/bold yellow]')
+ print(f"[bold yellow]Cannot perform search on {self.tracker}: IMDb ID not found in metadata.[/bold yellow]")
return []
api_url = f"{self.api_base_url}/torrent/search"
@@ -295,33 +282,26 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
dupes: list[dict[str, Any]] = []
try:
- response = await self.session.post(
- api_url,
- json=payload, timeout=15
- )
+ response = await self.session.post(api_url, json=payload, timeout=15)
res_json = response.json()
- if res_json.get('code') != '0':
+ if res_json.get("code") != "0":
print(f"[bold red]API Error: {res_json.get('message')}[/bold red]")
return []
- torrents = res_json.get('data', {}).get('data', [])
+ torrents = res_json.get("data", {}).get("data", [])
for torrent in torrents:
- t_id = torrent.get('id')
+ t_id = torrent.get("id")
if not t_id:
continue
- dupes.append({
- 'name': torrent.get('name'),
- 'size': int(torrent.get('size', 0)),
- 'link': f"https://kp.m-team.cc/detail/{t_id}"
- })
+ dupes.append({"name": torrent.get("name"), "size": int(torrent.get("size", 0)), "link": f"https://kp.m-team.cc/detail/{t_id}"})
return dupes
except Exception as e:
- print(f'[bold red]Error searching for IMDb ID {imdb_id} on {self.tracker}: {e}[/bold red]')
+ print(f"[bold red]Error searching for IMDb ID {imdb_id} on {self.tracker}: {e}[/bold red]")
return []
@@ -333,75 +313,75 @@ def get_standard(self, meta: Meta) -> int:
_4k = 6
_8k = 7
- resolution = meta.get('resolution', '').lower()
- if resolution == '1080p':
+ resolution = meta.get("resolution", "").lower()
+ if resolution == "1080p":
return _1080p
- elif resolution == '1080i':
+ elif resolution == "1080i":
return _1080i
- elif resolution == '720p':
+ elif resolution == "720p":
return _720p
- elif resolution == '2160p':
+ elif resolution == "2160p":
return _4k
- elif resolution == '4320p':
+ elif resolution == "4320p":
return _8k
- elif meta.get('sd', False):
+ elif meta.get("sd", False):
return sd
else:
console.print(f"{self.tracker}: Unknown or unsupported resolution '{resolution}', defaulting to 1080p.")
return _1080p
def get_videocodec(self, meta: Meta) -> int:
- x264 = 1 # H.264(x264/AVC)
- x265 = 16 # H.265(x265/HEVC)
- vc1 = 2 # VC-1
- mpeg2 = 4 # MPEG-2
- xvid = 3 # Xvid
- av1 = 19 # AV1
+ x264 = 1 # H.264(x264/AVC)
+ x265 = 16 # H.265(x265/HEVC)
+ vc1 = 2 # VC-1
+ mpeg2 = 4 # MPEG-2
+ xvid = 3 # Xvid
+ av1 = 19 # AV1
vp8_9 = 21 # VP8/9
- codec = meta.get('video_codec', '').lower()
- if codec in ('h264', 'x264', 'avc', 'h.264', 'h.265'):
+ codec = meta.get("video_codec", "").lower()
+ if codec in ("h264", "x264", "avc", "h.264", "h.265"):
return x264
- elif codec in ('h265', 'hevc', 'x265'):
+ elif codec in ("h265", "hevc", "x265"):
return x265
- elif codec in ('vc1', 'vc-1'):
+ elif codec in ("vc1", "vc-1"):
return vc1
- elif codec in ('mpeg2', 'mpeg-2'):
+ elif codec in ("mpeg2", "mpeg-2"):
return mpeg2
- elif codec == 'xvid':
+ elif codec == "xvid":
return xvid
- elif codec == 'av1':
+ elif codec == "av1":
return av1
- elif codec in ('vp8', 'vp9'):
+ elif codec in ("vp8", "vp9"):
return vp8_9
else:
console.print(f"{self.tracker}: Unknown or unsupported video codec '{codec}', defaulting to x264.")
return x264
def get_audiocodec(self, meta: Meta) -> int:
- aac = 6 # AAC
- ac3 = 8 # AC3(DD)
- dts = 3 # DTS
- dts_hd_ma = 11 # DTS-HD MA
- eac3 = 12 # E-AC3(DDP)
+ aac = 6 # AAC
+ ac3 = 8 # AC3(DD)
+ dts = 3 # DTS
+ dts_hd_ma = 11 # DTS-HD MA
+ eac3 = 12 # E-AC3(DDP)
atmos_eac3 = 13 # E-AC3 Atoms(DDP Atoms)
- true_hd = 9 # TrueHD
+ true_hd = 9 # TrueHD
- codec = meta.get('audio', '').lower()
+ codec = meta.get("audio", "").lower()
- if 'aac' in codec:
+ if "aac" in codec:
return aac
- elif 'dd+' in codec:
+ elif "dd+" in codec:
return eac3
- elif 'dd ' in codec:
+ elif "dd " in codec:
return ac3
- elif 'dts-hd' in codec:
+ elif "dts-hd" in codec:
return dts_hd_ma
- elif 'dts' in codec:
+ elif "dts" in codec:
return dts
- elif 'atmos' in codec and 'dd+' in codec:
+ elif "atmos" in codec and "dd+" in codec:
return atmos_eac3
- elif 'truehd' in codec:
+ elif "truehd" in codec:
return true_hd
else:
console.print(f"{self.tracker}: Unknown or unsupported audio codec '{codec}', defaulting to AC3.")
@@ -426,12 +406,12 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
# "team": 0,
# "processing": 0,
# "countries": "",
- "imdb": meta.get('imdb_info', {}).get('imdbID', ""),
+ "imdb": meta.get("imdb_info", {}).get("imdbID", ""),
# "douban": "",
# "dmmCode": "",
# "cids": "",
# "aids": "",
- "anonymous": bool(meta.get('anonymous', False)),
+ "anonymous": bool(meta.get("anonymous", False)),
# "labels": 0,
# "tags": "",
# "file": "",
@@ -439,7 +419,7 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
"mediainfo": await self.mediainfo(meta),
"mediaInfoAnalysisResult": True,
# "labelsNew": ""
- }
+ }
return data
@@ -447,63 +427,58 @@ async def upload(self, meta: Meta, _) -> bool:
data = await self.fetch_data(meta)
response = None
- if not meta.get('debug', False):
+ if not meta.get("debug", False):
try:
- upload_url = f'{self.api_base_url}/torrent/createOredit'
- await self.common.create_torrent_for_upload(meta, self.tracker, '[kp.m-team.cc] M-Team - TP')
+ upload_url = f"{self.api_base_url}/torrent/createOredit"
+ await self.common.create_torrent_for_upload(meta, self.tracker, "[kp.m-team.cc] M-Team - TP")
torrent_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}].torrent"
- async with aiofiles.open(torrent_path, 'rb') as torrent_file:
+ async with aiofiles.open(torrent_path, "rb") as torrent_file:
torrent_bytes = await torrent_file.read()
- files = {'file': ('upload.torrent', torrent_bytes, 'application/x-bittorrent')}
+ files = {"file": ("upload.torrent", torrent_bytes, "application/x-bittorrent")}
response = await self.session.post(upload_url, data=data, files=files, headers=dict(self.session.headers), timeout=90)
response.raise_for_status()
response_json = response.json()
response_data: dict[str, Any] = cast(dict[str, Any], response_json) if isinstance(response_json, dict) else {}
- if response_data.get('message') == "SUCCESS":
- torrent_id = str(response_data['data']['id'])
- meta['tracker_status'][self.tracker]['torrent_id'] = torrent_id
- meta['tracker_status'][self.tracker]['status_message'] = response_data.get('message')
+ if response_data.get("message") == "SUCCESS":
+ torrent_id = str(response_data["data"]["id"])
+ meta["tracker_status"][self.tracker]["torrent_id"] = torrent_id
+ meta["tracker_status"][self.tracker]["status_message"] = response_data.get("message")
download_api_url = f"{self.api_base_url}/torrent/genDlToken?id={torrent_id}"
response = await self.session.post(download_api_url)
data = response.json()
final_download_url = data.get("data")
if final_download_url:
- await self.common.download_tracker_torrent(
- meta,
- self.tracker,
- headers=dict(self.session.headers),
- downurl=final_download_url
- )
+ await self.common.download_tracker_torrent(meta, self.tracker, headers=dict(self.session.headers), downurl=final_download_url)
return True
console.print(f"{self.tracker}: Failed to get download URL from API response.")
- meta['tracker_status'][self.tracker]['status_message'] = "Failed to get download URL from API response"
+ meta["tracker_status"][self.tracker]["status_message"] = "Failed to get download URL from API response"
return False
else:
- meta['tracker_status'][self.tracker]['status_message'] = f"data error: {response_data.get('message', 'Unknown API error.')}"
+ meta["tracker_status"][self.tracker]["status_message"] = f"data error: {response_data.get('message', 'Unknown API error.')}"
return False
except httpx.HTTPStatusError as e:
- meta['tracker_status'][self.tracker]['status_message'] = f'data error: HTTP {e.response.status_code} - {e.response.text}'
+ meta["tracker_status"][self.tracker]["status_message"] = f"data error: HTTP {e.response.status_code} - {e.response.text}"
return False
except httpx.TimeoutException:
- meta['tracker_status'][self.tracker]['status_message'] = f'data error: Request timed out after {self.session.timeout.write} seconds'
+ meta["tracker_status"][self.tracker]["status_message"] = f"data error: Request timed out after {self.session.timeout.write} seconds"
return False
except httpx.RequestError as e:
- resp_text = getattr(getattr(e, 'response', None), 'text', 'No response received')
- meta['tracker_status'][self.tracker]['status_message'] = f'data error: Unable to upload. Error: {e}.\nResponse: {resp_text}'
+ resp_text = getattr(getattr(e, "response", None), "text", "No response received")
+ meta["tracker_status"][self.tracker]["status_message"] = f"data error: Unable to upload. Error: {e}.\nResponse: {resp_text}"
return False
except Exception as e:
- resp_text = response.text if response is not None else 'No response received'
- meta['tracker_status'][self.tracker]['status_message'] = f'data error: It may have uploaded, go check. Error: {e}.\nResponse: {resp_text}'
+ resp_text = response.text if response is not None else "No response received"
+ meta["tracker_status"][self.tracker]["status_message"] = f"data error: It may have uploaded, go check. Error: {e}.\nResponse: {resp_text}"
return False
else:
console.print("[cyan]DC Request Data:")
console.print(Redaction.redact_private_info(data))
- meta['tracker_status'][self.tracker]['status_message'] = 'Debug mode enabled, not uploading'
+ meta["tracker_status"][self.tracker]["status_message"] = "Debug mode enabled, not uploading"
await self.common.create_torrent_for_upload(meta, f"{self.tracker}" + "_DEBUG", f"{self.tracker}" + "_DEBUG", announce_url="https://fake.tracker")
return True # Debug mode - simulated success
From 9962227d14f8d18de1f710d89bbe3c5aa39cde71 Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 27 Mar 2026 15:35:37 -0300
Subject: [PATCH 04/13] fixes
---
data/example-config.py | 1 +
src/torrent_clients/qbittorrent.py | 114 ++++++++++++++---------------
src/trackers/MTEAM.py | 19 ++---
3 files changed, 68 insertions(+), 66 deletions(-)
diff --git a/data/example-config.py b/data/example-config.py
index 49abfc13b..64e023046 100644
--- a/data/example-config.py
+++ b/data/example-config.py
@@ -681,6 +681,7 @@
"link_dir_name": "",
"api_key": "",
"anon": False,
+ "base_url": "kp.m-team.cc"
},
"MTV": {
# Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
diff --git a/src/torrent_clients/qbittorrent.py b/src/torrent_clients/qbittorrent.py
index 8bc40551f..011f7bcc9 100644
--- a/src/torrent_clients/qbittorrent.py
+++ b/src/torrent_clients/qbittorrent.py
@@ -1803,63 +1803,63 @@ async def _search_single_qbit_client(self, client_config: dict[str, Any], _conte
async def match_tracker_url(tracker_urls: list[str], meta: dict[str, Any]) -> None:
tracker_url_patterns = {
- 'acm': ["https://eiga.moi"],
- 'aither': ["https://aither.cc"],
- 'ant': ["tracker.anthelion.me"],
- 'ar': ["tracker.alpharatio"],
- 'asc': ["amigos-share.club"],
- 'az': ["tracker.avistaz.to"],
- 'bhd': ["https://beyond-hd.me", "tracker.beyond-hd.me"],
- 'bjs': ["tracker.bj-share.info"],
- 'blu': ["https://blutopia.cc"],
- 'bt': ["t.brasiltracker.org"],
- 'btn': ["https://broadcasthe.net"],
- 'cbr': ["capybarabr.com"],
- 'cz': ["tracker.cinemaz.to"],
- 'dc': ["tracker.digitalcore.club", "trackerprxy.digitalcore.club"],
- 'dp': ["https://darkpeers.org"],
- 'ff': ["tracker.funfile.org"],
- 'fl': ["reactor.filelist", "reactor.thefl.org"],
- 'fnp': ["https://fearnopeer.com"],
- 'gpw': ["https://tracker.greatposterwall.com"],
- 'hdb': ["https://tracker.hdbits.org"],
- 'hds': ["hd-space.pw"],
- 'hdt': ["https://hdts-announce.ru"],
- 'hhd': ["https://homiehelpdesk.net"],
- 'huno': ["https://hawke.uno"],
- 'ihd': ["https://infinityhd.net"],
- 'is': ["https://immortalseed.me"],
- 'itt': ["https://itatorrents.xyz"],
- 'lcd': ["locadora.cc"],
- 'ldu': ["theldu.to"],
- 'lst': ["https://lst.gg"],
- 'lt': ["https://lat-team.com"],
- 'lume': ["https://luminarr.me"],
- 'mteam': ["https://tracker.m-team.cc"],
- 'mtv': ["tracker.morethantv"],
- 'nbl': ["tracker.nebulance"],
- 'oe': ["https://onlyencodes.cc"],
- 'otw': ["https://oldtoons.world"],
- 'phd': ["tracker.privatehd"],
- 'pt': ["https://portugas.org"],
- 'ptp': ["passthepopcorn.me"],
- 'pts': ["https://tracker.ptskit.com"],
- 'ras': ["https://rastastugan.org"],
- 'rf': ["https://reelflix.xyz", "https://reelflix.cc"],
- 'rtf': ["peer.retroflix"],
- 'sam': ["https://samaritano.cc"],
- 'sp': ["https://seedpool.org"],
- 'spd': ["ramjet.speedapp.io", "ramjet.speedapp.to", "ramjet.speedappio.org"],
- 'stc': ["https://skipthecommercials.xyz"],
- 'thr': ["torrenthr"],
- 'tl': ["tracker.tleechreload", "tracker.torrentleech"],
- 'tlz': ["https://tlzdigital.com/"],
- 'tos': ["https://theoldschool.cc"],
- 'ttr': ["https://torrenteros.org"],
- 'tvc': ["https://tvchaosuk.com"],
- 'ulcx': ["https://upload.cx"],
- 'yoink': ["yoinked.org"],
- 'yus': ["https://yu-scene.net"],
+ "acm": ["https://eiga.moi"],
+ "aither": ["https://aither.cc"],
+ "ant": ["tracker.anthelion.me"],
+ "ar": ["tracker.alpharatio"],
+ "asc": ["amigos-share.club"],
+ "az": ["tracker.avistaz.to"],
+ "bhd": ["https://beyond-hd.me", "tracker.beyond-hd.me"],
+ "bjs": ["tracker.bj-share.info"],
+ "blu": ["https://blutopia.cc"],
+ "bt": ["t.brasiltracker.org"],
+ "btn": ["https://broadcasthe.net"],
+ "cbr": ["capybarabr.com"],
+ "cz": ["tracker.cinemaz.to"],
+ "dc": ["tracker.digitalcore.club", "trackerprxy.digitalcore.club"],
+ "dp": ["https://darkpeers.org"],
+ "ff": ["tracker.funfile.org"],
+ "fl": ["reactor.filelist", "reactor.thefl.org"],
+ "fnp": ["https://fearnopeer.com"],
+ "gpw": ["https://tracker.greatposterwall.com"],
+ "hdb": ["https://tracker.hdbits.org"],
+ "hds": ["hd-space.pw"],
+ "hdt": ["https://hdts-announce.ru"],
+ "hhd": ["https://homiehelpdesk.net"],
+ "huno": ["https://hawke.uno"],
+ "ihd": ["https://infinityhd.net"],
+ "is": ["https://immortalseed.me"],
+ "itt": ["https://itatorrents.xyz"],
+ "lcd": ["locadora.cc"],
+ "ldu": ["theldu.to"],
+ "lst": ["https://lst.gg"],
+ "lt": ["https://lat-team.com"],
+ "lume": ["https://luminarr.me"],
+ "mteam": ["tracker.m-team.cc", "tra1.m-team.cc", "tracker.m-team.io", "tra1.m-team.io", "tra99.manfuz.co"],
+ "mtv": ["tracker.morethantv"],
+ "nbl": ["tracker.nebulance"],
+ "oe": ["https://onlyencodes.cc"],
+ "otw": ["https://oldtoons.world"],
+ "phd": ["tracker.privatehd"],
+ "pt": ["https://portugas.org"],
+ "ptp": ["passthepopcorn.me"],
+ "pts": ["https://tracker.ptskit.com"],
+ "ras": ["https://rastastugan.org"],
+ "rf": ["https://reelflix.xyz", "https://reelflix.cc"],
+ "rtf": ["peer.retroflix"],
+ "sam": ["https://samaritano.cc"],
+ "sp": ["https://seedpool.org"],
+ "spd": ["ramjet.speedapp.io", "ramjet.speedapp.to", "ramjet.speedappio.org"],
+ "stc": ["https://skipthecommercials.xyz"],
+ "thr": ["torrenthr"],
+ "tl": ["tracker.tleechreload", "tracker.torrentleech"],
+ "tlz": ["https://tlzdigital.com/"],
+ "tos": ["https://theoldschool.cc"],
+ "ttr": ["https://torrenteros.org"],
+ "tvc": ["https://tvchaosuk.com"],
+ "ulcx": ["https://upload.cx"],
+ "yoink": ["yoinked.org"],
+ "yus": ["https://yu-scene.net"],
}
found_ids: set[str] = set()
for tracker in tracker_urls:
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index 10183a1d8..8ee09ac01 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -25,7 +25,7 @@ def __init__(self, config: Config):
self.config = config
self.common = COMMON(config)
self.tracker = "MTEAM"
- self.base_url = "https://kp.m-team.cc"
+ self.base_url = f"https://{self.config['TRACKERS'][self.tracker].get('base_url', 'kp.m-team.cc')}"
self.api_base_url = "https://api.m-team.cc/api"
self.torrent_url = f"{self.base_url}/detail/"
self.banned_groups = [""]
@@ -78,8 +78,9 @@ def bbcode_to_markdown(self, text):
def mteam_standard_desc(self, meta: Meta):
imdb = meta.get("imdb_info", {})
- tmdb_poster = f"https://image.tmdb.org/t/p/w200{meta.get('tmdb_poster')}"
- poster_url = tmdb_poster if tmdb_poster else imdb.get("cover", "")
+ tmdb_poster_path = str(meta.get("tmdb_poster") or "").strip()
+ tmdb_poster = f"https://image.tmdb.org/t/p/w200{tmdb_poster_path}" if tmdb_poster_path else ""
+ poster_url = tmdb_poster or str(imdb.get("cover") or "")
title = meta.get("title", "N/A")
year = meta.get("year", "N/A")
rating = imdb.get("rating", "N/A")
@@ -340,9 +341,9 @@ def get_videocodec(self, meta: Meta) -> int:
vp8_9 = 21 # VP8/9
codec = meta.get("video_codec", "").lower()
- if codec in ("h264", "x264", "avc", "h.264", "h.265"):
+ if codec in ("h264", "x264", "avc", "h.264"):
return x264
- elif codec in ("h265", "hevc", "x265"):
+ elif codec in ("h265", "h.265", "hevc", "x265"):
return x265
elif codec in ("vc1", "vc-1"):
return vc1
@@ -369,7 +370,9 @@ def get_audiocodec(self, meta: Meta) -> int:
codec = meta.get("audio", "").lower()
- if "aac" in codec:
+ if "atmos" in codec and "dd+" in codec:
+ return atmos_eac3
+ elif "aac" in codec:
return aac
elif "dd+" in codec:
return eac3
@@ -379,8 +382,6 @@ def get_audiocodec(self, meta: Meta) -> int:
return dts_hd_ma
elif "dts" in codec:
return dts
- elif "atmos" in codec and "dd+" in codec:
- return atmos_eac3
elif "truehd" in codec:
return true_hd
else:
@@ -411,7 +412,7 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
# "dmmCode": "",
# "cids": "",
# "aids": "",
- "anonymous": bool(meta.get("anonymous", False)),
+ "anonymous": bool(meta.get("anon", False)),
# "labels": 0,
# "tags": "",
# "file": "",
From 3efc759f2c648207278c4ca59fe98d66817fe1d4 Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 27 Mar 2026 18:58:16 -0300
Subject: [PATCH 05/13] Add Douban integration and enhance metadata handling
---
src/trackers/MTEAM.py | 177 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 172 insertions(+), 5 deletions(-)
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index 8ee09ac01..46ae60429 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -5,10 +5,12 @@
import aiofiles
import httpx
+from bs4 import BeautifulSoup
from cogs.redaction import Redaction
from src.console import console
from src.get_desc import DescriptionBuilder
+from src.tmdb import TmdbManager
from src.trackers.COMMON import COMMON
Meta = dict[str, Any]
@@ -16,6 +18,7 @@
class MTEAM:
+ douban_id = ""
"""
https://test2.m-team.cc/api/swagger-ui/index.html
https://wiki.m-team.cc/zh-tw/api
@@ -24,11 +27,12 @@ class MTEAM:
def __init__(self, config: Config):
self.config = config
self.common = COMMON(config)
+ self.tmdb_manager = TmdbManager(config)
self.tracker = "MTEAM"
self.base_url = f"https://{self.config['TRACKERS'][self.tracker].get('base_url', 'kp.m-team.cc')}"
self.api_base_url = "https://api.m-team.cc/api"
self.torrent_url = f"{self.base_url}/detail/"
- self.banned_groups = [""]
+ self.banned_groups = ["FGT"]
self.api_key = self.config["TRACKERS"][self.tracker].get("api_key")
self.session = httpx.AsyncClient(
headers={
@@ -75,7 +79,76 @@ def bbcode_to_markdown(self, text):
return text
+ async def get_douban_info(self) -> None:
+ api_url = f"{self.api_base_url}/media/douban/infoV2"
+
+ params = {
+ "code": MTEAM.douban_id,
+ "refresh": False,
+ }
+
+ headers = {
+ "x-api-key": self.api_key,
+ "Accept": "*/*",
+ }
+
+ try:
+ response = await self.session.post(api_url, headers=headers, params=params, timeout=15)
+ response.raise_for_status()
+ data = response.json()
+ self.douban_info = data
+ return
+
+ except Exception as e:
+ console.print(f"Error fetching Douban info: {e}")
+ return
+
def mteam_standard_desc(self, meta: Meta):
+ db_info = getattr(self, "douban_info", None)
+
+ if db_info and db_info.get("code") == "0":
+ d = db_info.get("data", {})
+ title = d.get("title", "")
+ aka = d.get("aka", [])
+ translated_names = " / ".join([title] + aka) if title else " / ".join(aka)
+
+ countries = " / ".join(d.get("countries", []))
+ genres = " / ".join(d.get("genres", []))
+ languages = " / ".join(d.get("languages", []))
+ pubdates = " / ".join(d.get("pubdate", []))
+ durations = " / ".join(d.get("durations", []))
+
+ directors = " / ".join([person.get("name", "") for person in d.get("directors", [])])
+ actors = " / ".join([person.get("name", "") for person in d.get("actors", [])])
+
+ rating_val = d.get("score", "0")
+ rating_count = d.get("rating", {}).get("count", "0")
+ subject_id = d.get("subjectId", "")
+
+ desc = [
+ f"})",
+ "",
+ f"**◎译 名** {translated_names}",
+ f"**◎片 名** {title}",
+ f"**◎年 代** {d.get('year', 'N/A')}",
+ f"**◎产 地** {countries}",
+ f"**◎类 别** {genres}",
+ f"**◎语 言** {languages}",
+ f"**◎上映日期** {pubdates}",
+ f"**◎豆瓣评分** {rating_val}/10 from {rating_count} users",
+ f"**◎豆瓣链接** https://www.douban.com/subject/{subject_id}/",
+ f"**◎片 长** {durations}",
+ f"**◎导 演** {directors}",
+ f"**◎主 演** {actors}",
+ "",
+ "**◎简 介**",
+ "",
+ f" {d.get('intro', '')}",
+ ]
+ return "\n".join(desc)
+
+ # Fallback
+ console.print(f"{self.tracker}: Douban information is unavailable, using an alternative English version for the description.")
imdb = meta.get("imdb_info", {})
tmdb_poster_path = str(meta.get("tmdb_poster") or "").strip()
@@ -267,11 +340,74 @@ def clean_to_int(val, bdmv_mode):
return (clean_to_int(v_raw, is_bdmv), clean_to_int(a_raw, is_bdmv))
+ async def get_additional_checks(self, meta: dict[str, Any]):
+ should_continue = True
+
+ imdb_id = meta.get("imdb_info", {}).get("imdbID")
+ if not imdb_id:
+ console.print(f"{self.tracker}: [bold yellow]IMDb ID not found in metadata, skipping upload.[/bold yellow]")
+ return False
+
+ uuid: str = meta["uuid"]
+ if "upscale" in uuid.lower() and "upscale" not in meta["title"]:
+ console.print(f"{self.tracker}: Uploading upscaled files created by converting low-bitrate videos to high-bitrate versions might be prohibited.")
+ if not meta["unattended"] or (meta["unattended"] and meta.get("unattended_confirm", False)):
+ user_input = self.common.prompt_user_for_confirmation(f"{self.tracker}: Do you want to continue with the upload? (y/n): ")
+ if not user_input:
+ return False
+ else:
+ return False
+
+ if meta.get("screens", 0) < 3:
+ console.print(f"{self.tracker}: [bold yellow]At least 3 screenshots are required for video uploads. Skipping upload.[/bold yellow]")
+ return False
+
+ return should_continue
+
+ async def get_douban_id(self, meta: Meta) -> None:
+ imdb_id = meta.get("imdb_info", {}).get("imdbID")
+ if not imdb_id:
+ return
+
+ search_url = f"https://m.douban.com/search/?query={imdb_id}"
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}
+
+ try:
+ response = await self.session.get(search_url, headers=headers)
+ response.raise_for_status()
+ soup = BeautifulSoup(response.text, "html.parser")
+ result = soup.find("ul", class_="search_results_subjects")
+
+ if result:
+ link_tag = result.find("a")
+ if link_tag and "href" in link_tag.attrs:
+ link_mobile = str(link_tag["href"])
+ match = re.search(r"subject/(\d+)", link_mobile)
+ if match:
+ MTEAM.douban_id = int(match.group(1))
+ return
+
+ console.print(f"{self.tracker}: [bold yellow]No Douban ID found for IMDb ID {imdb_id}.[/bold yellow]")
+ return
+
+ except Exception:
+ console.print(f"{self.tracker}: [bold yellow]Failed to fetch Douban ID for IMDb ID {imdb_id}.[/bold yellow]")
+ return
+
async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]:
+ dupes: list[dict[str, Any]] = []
+
+ should_continue = await self.get_additional_checks(meta)
+ if not should_continue:
+ meta["skipping"] = f"{self.tracker}"
+ return dupes
+
+ await self.get_douban_id(meta)
imdb_id = meta.get("imdb_info", {}).get("imdbID")
if not imdb_id:
- print(f"[bold yellow]Cannot perform search on {self.tracker}: IMDb ID not found in metadata.[/bold yellow]")
+ console.print(f"{self.tracker}: [bold yellow]Cannot perform search on {self.tracker}: IMDb ID not found in metadata.[/bold yellow]")
+
return []
api_url = f"{self.api_base_url}/torrent/search"
@@ -280,7 +416,6 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
"mode": "normal",
"imdb": imdb_id,
}
- dupes: list[dict[str, Any]] = []
try:
response = await self.session.post(api_url, json=payload, timeout=15)
@@ -297,7 +432,20 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
if not t_id:
continue
- dupes.append({"name": torrent.get("name"), "size": int(torrent.get("size", 0)), "link": f"https://kp.m-team.cc/detail/{t_id}"})
+ dupe_entry = {
+ "name": torrent.get("name"),
+ "size": int(torrent.get("size", 0)),
+ "link": f"https://kp.m-team.cc/detail/{t_id}",
+ "file_count": torrent.get("file_count", 0),
+ "download": f"{self.api_base_url}/torrent/genDlToken?id={t_id}",
+ "id": t_id,
+ }
+ if meta.get("is_disc") == "BDMV":
+ bdinfo = await self.get_dupe_bdinfo(t_id)
+ if bdinfo:
+ dupe_entry["bd_info"] = bdinfo
+
+ dupes.append(dupe_entry)
return dupes
@@ -306,6 +454,23 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
return []
+ async def get_dupe_bdinfo(self, torrent_id: int) -> str:
+ api_url = f"{self.api_base_url}/torrent/detail?id={torrent_id}"
+
+ try:
+ response = await self.session.post(api_url, timeout=15)
+ response.raise_for_status()
+
+ response_data = response.json()
+ bdinfo = response_data.get("data", {}).get("mediainfo")
+ if not bdinfo:
+ bdinfo = response_data.get("data", {}).get("descr")
+ return bdinfo
+
+ except Exception as e:
+ console.print(f"{self.tracker}: Error fetching BDinfo: {e}")
+ return ""
+
def get_standard(self, meta: Meta) -> int:
_1080p = 1
_1080i = 2
@@ -392,6 +557,8 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
"""
https://test2.m-team.cc/api/swagger-ui/index.html#/種子/createOredit
"""
+ await self.get_douban_info()
+
data = {
# "torrent": 0,
# "offer": 0,
@@ -408,7 +575,7 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
# "processing": 0,
# "countries": "",
"imdb": meta.get("imdb_info", {}).get("imdbID", ""),
- # "douban": "",
+ "douban": MTEAM.douban_id,
# "dmmCode": "",
# "cids": "",
# "aids": "",
From 50a021756c2e8fd9b401d2a9ffedf1bd1d355558 Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 27 Mar 2026 19:33:54 -0300
Subject: [PATCH 06/13] fixes
---
src/trackers/MTEAM.py | 42 ++++++++++++++++++++++++++++++++++--------
1 file changed, 34 insertions(+), 8 deletions(-)
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index 46ae60429..52b919860 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -18,7 +18,6 @@
class MTEAM:
- douban_id = ""
"""
https://test2.m-team.cc/api/swagger-ui/index.html
https://wiki.m-team.cc/zh-tw/api
@@ -83,7 +82,7 @@ async def get_douban_info(self) -> None:
api_url = f"{self.api_base_url}/media/douban/infoV2"
params = {
- "code": MTEAM.douban_id,
+ "code": self.douban_id,
"refresh": False,
}
@@ -348,6 +347,7 @@ async def get_additional_checks(self, meta: dict[str, Any]):
console.print(f"{self.tracker}: [bold yellow]IMDb ID not found in metadata, skipping upload.[/bold yellow]")
return False
+ # Upscaled Content
uuid: str = meta["uuid"]
if "upscale" in uuid.lower() and "upscale" not in meta["title"]:
console.print(f"{self.tracker}: Uploading upscaled files created by converting low-bitrate videos to high-bitrate versions might be prohibited.")
@@ -358,10 +358,28 @@ async def get_additional_checks(self, meta: dict[str, Any]):
else:
return False
+ # Screenshots
if meta.get("screens", 0) < 3:
console.print(f"{self.tracker}: [bold yellow]At least 3 screenshots are required for video uploads. Skipping upload.[/bold yellow]")
return False
+ # LGBT Content
+ keywords: str = meta.get("keywords", "")
+ combined_genres: str = meta.get("combined_genres", "")
+ combined_text = f"{keywords}, {combined_genres}".lower()
+ combined_list = [item.strip() for item in combined_text.split(",") if item.strip()]
+ lgbt_keywords = ["lgbt", "queer", "lgbtq", "lgbtqia", "transgender", "trans", "gay", "lesbian", "bisexual", "pansexual", "non-binary", "homoerotic"]
+ if any(kw in combined_list for kw in lgbt_keywords):
+ console.print(
+ f"{self.tracker}: [bold yellow]LGBT content detected. Please ensure the cover photo does not contain depictions of genitalia per tracker rules.[/bold yellow]"
+ )
+ if not meta["unattended"] or (meta["unattended"] and meta.get("unattended_confirm", False)):
+ user_input = self.common.prompt_user_for_confirmation(f"{self.tracker}: Do you want to continue with the upload? (y/n): ")
+ if not user_input:
+ return False
+ else:
+ return False
+
return should_continue
async def get_douban_id(self, meta: Meta) -> None:
@@ -384,7 +402,7 @@ async def get_douban_id(self, meta: Meta) -> None:
link_mobile = str(link_tag["href"])
match = re.search(r"subject/(\d+)", link_mobile)
if match:
- MTEAM.douban_id = int(match.group(1))
+ self.douban_id = int(match.group(1))
return
console.print(f"{self.tracker}: [bold yellow]No Douban ID found for IMDb ID {imdb_id}.[/bold yellow]")
@@ -402,7 +420,6 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
meta["skipping"] = f"{self.tracker}"
return dupes
- await self.get_douban_id(meta)
imdb_id = meta.get("imdb_info", {}).get("imdbID")
if not imdb_id:
@@ -435,7 +452,7 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
dupe_entry = {
"name": torrent.get("name"),
"size": int(torrent.get("size", 0)),
- "link": f"https://kp.m-team.cc/detail/{t_id}",
+ "link": f"{self.base_url}/detail/{t_id}",
"file_count": torrent.get("file_count", 0),
"download": f"{self.api_base_url}/torrent/genDlToken?id={t_id}",
"id": t_id,
@@ -557,6 +574,7 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
"""
https://test2.m-team.cc/api/swagger-ui/index.html#/種子/createOredit
"""
+ await self.get_douban_id(meta)
await self.get_douban_info()
data = {
@@ -575,7 +593,7 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
# "processing": 0,
# "countries": "",
"imdb": meta.get("imdb_info", {}).get("imdbID", ""),
- "douban": MTEAM.douban_id,
+ "douban": self.douban_id,
# "dmmCode": "",
# "cids": "",
# "aids": "",
@@ -620,8 +638,16 @@ async def upload(self, meta: Meta, _) -> bool:
data = response.json()
final_download_url = data.get("data")
if final_download_url:
- await self.common.download_tracker_torrent(meta, self.tracker, headers=dict(self.session.headers), downurl=final_download_url)
- return True
+ downloaded_torrent = await self.common.download_tracker_torrent(
+ meta,
+ self.tracker,
+ headers=dict(self.session.headers),
+ downurl=final_download_url,
+ )
+ if downloaded_torrent:
+ return True
+ meta["tracker_status"][self.tracker]["status_message"] = "Upload succeeded but downloading the tracker torrent failed"
+ return False
console.print(f"{self.tracker}: Failed to get download URL from API response.")
meta["tracker_status"][self.tracker]["status_message"] = "Failed to get download URL from API response"
return False
From ba6b3f59221e9ae87b048243bd87e260c086318e Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 27 Mar 2026 20:06:38 -0300
Subject: [PATCH 07/13] improve URL handling, update get_douban_info return
type, and refactor related methods
---
src/trackers/MTEAM.py | 75 +++++++++++++++++++++++++++----------------
1 file changed, 47 insertions(+), 28 deletions(-)
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index 52b919860..a84371185 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -2,6 +2,7 @@
import os
import re
from typing import Any, Optional, cast
+from urllib.parse import urlparse, urlunparse
import aiofiles
import httpx
@@ -19,20 +20,29 @@
class MTEAM:
"""
- https://test2.m-team.cc/api/swagger-ui/index.html
- https://wiki.m-team.cc/zh-tw/api
+ API Docs: https://test2.m-team.cc/api/swagger-ui/index.html
+ API Limits: https://wiki.m-team.cc/zh-tw/api
+ Upload Rules: https://wiki.m-team.cc/zh-tw/upload-rules
"""
def __init__(self, config: Config):
self.config = config
self.common = COMMON(config)
self.tmdb_manager = TmdbManager(config)
+
self.tracker = "MTEAM"
- self.base_url = f"https://{self.config['TRACKERS'][self.tracker].get('base_url', 'kp.m-team.cc')}"
+
+ raw_url = self.config["TRACKERS"][self.tracker].get("base_url", "kp.m-team.cc").strip()
+ parsed_raw = urlparse(raw_url)
+ clean_netloc = parsed_raw.netloc if parsed_raw.netloc else parsed_raw.path
+ self.base_url = urlunparse(("https", clean_netloc, "", "", "", ""))
+
self.api_base_url = "https://api.m-team.cc/api"
self.torrent_url = f"{self.base_url}/detail/"
- self.banned_groups = ["FGT"]
self.api_key = self.config["TRACKERS"][self.tracker].get("api_key")
+
+ self.banned_groups = ["FGT"]
+
self.session = httpx.AsyncClient(
headers={
"x-api-key": self.api_key,
@@ -41,6 +51,8 @@ def __init__(self, config: Config):
timeout=30.0,
)
+ self.douban_id: int = 0
+
async def mediainfo(self, meta: Meta) -> str:
mi_path: str = ""
mediainfo: str = ""
@@ -78,7 +90,11 @@ def bbcode_to_markdown(self, text):
return text
- async def get_douban_info(self) -> None:
+ async def get_douban_info(self) -> dict[str, Any]:
+ info: dict[str, Any] = {}
+ if not self.douban_id:
+ return info
+
api_url = f"{self.api_base_url}/media/douban/infoV2"
params = {
@@ -94,16 +110,15 @@ async def get_douban_info(self) -> None:
try:
response = await self.session.post(api_url, headers=headers, params=params, timeout=15)
response.raise_for_status()
- data = response.json()
- self.douban_info = data
- return
+ info = response.json()
+ return info
except Exception as e:
console.print(f"Error fetching Douban info: {e}")
- return
+ return info
- def mteam_standard_desc(self, meta: Meta):
- db_info = getattr(self, "douban_info", None)
+ async def mteam_standard_desc(self, meta: Meta):
+ db_info = await self.get_douban_info()
if db_info and db_info.get("code") == "0":
d = db_info.get("data", {})
@@ -190,7 +205,7 @@ async def generate_description(self, meta: Meta) -> str:
desc_parts.append(custom_header)
# M-Team Standard Description
- desc_parts.append(self.mteam_standard_desc(meta))
+ desc_parts.append(await self.mteam_standard_desc(meta))
# User description
user_description = await builder.get_user_description(meta)
@@ -382,10 +397,11 @@ async def get_additional_checks(self, meta: dict[str, Any]):
return should_continue
- async def get_douban_id(self, meta: Meta) -> None:
+ async def get_douban_id(self, meta: Meta) -> int:
+ douban_id: int = 0
imdb_id = meta.get("imdb_info", {}).get("imdbID")
if not imdb_id:
- return
+ return douban_id
search_url = f"https://m.douban.com/search/?query={imdb_id}"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}
@@ -402,15 +418,16 @@ async def get_douban_id(self, meta: Meta) -> None:
link_mobile = str(link_tag["href"])
match = re.search(r"subject/(\d+)", link_mobile)
if match:
- self.douban_id = int(match.group(1))
- return
+ douban_id = int(match.group(1))
+ self.douban_id = douban_id
+ return douban_id
console.print(f"{self.tracker}: [bold yellow]No Douban ID found for IMDb ID {imdb_id}.[/bold yellow]")
- return
+ return douban_id
except Exception:
console.print(f"{self.tracker}: [bold yellow]Failed to fetch Douban ID for IMDb ID {imdb_id}.[/bold yellow]")
- return
+ return douban_id
async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]:
dupes: list[dict[str, Any]] = []
@@ -424,8 +441,7 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
if not imdb_id:
console.print(f"{self.tracker}: [bold yellow]Cannot perform search on {self.tracker}: IMDb ID not found in metadata.[/bold yellow]")
-
- return []
+ return dupes
api_url = f"{self.api_base_url}/torrent/search"
@@ -439,8 +455,8 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
res_json = response.json()
if res_json.get("code") != "0":
- print(f"[bold red]API Error: {res_json.get('message')}[/bold red]")
- return []
+ console.print(f"[bold red]API Error: {res_json.get('message')}[/bold red]")
+ return dupes
torrents = res_json.get("data", {}).get("data", [])
@@ -467,9 +483,13 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
return dupes
except Exception as e:
- print(f"[bold red]Error searching for IMDb ID {imdb_id} on {self.tracker}: {e}[/bold red]")
+ console.print(f"[bold red]Error searching for IMDb ID {imdb_id} on {self.tracker}: {e}[/bold red]")
+ if not meta["unattended"] or (meta["unattended"] and meta.get("unattended_confirm", False)):
+ pass
+ else:
+ meta["skipping"] = f"{self.tracker}"
- return []
+ return dupes
async def get_dupe_bdinfo(self, torrent_id: int) -> str:
api_url = f"{self.api_base_url}/torrent/detail?id={torrent_id}"
@@ -574,8 +594,7 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
"""
https://test2.m-team.cc/api/swagger-ui/index.html#/種子/createOredit
"""
- await self.get_douban_id(meta)
- await self.get_douban_info()
+ douban_id = await self.get_douban_id(meta)
data = {
# "torrent": 0,
@@ -593,11 +612,11 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
# "processing": 0,
# "countries": "",
"imdb": meta.get("imdb_info", {}).get("imdbID", ""),
- "douban": self.douban_id,
+ "douban": douban_id,
# "dmmCode": "",
# "cids": "",
# "aids": "",
- "anonymous": bool(meta.get("anon", False)),
+ "anonymous": bool(meta.get("anon", False) or self.config["TRACKERS"][self.tracker].get("anon", False)),
# "labels": 0,
# "tags": "",
# "file": "",
From 0f56a9b2e60c834944a1114fb7264d5f676ddb00 Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 27 Mar 2026 20:24:42 -0300
Subject: [PATCH 08/13] add arg
---
src/args.py | 1 +
src/trackers/MTEAM.py | 19 ++++++++++++++-----
2 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/src/args.py b/src/args.py
index 1731afeb3..58f81d319 100644
--- a/src/args.py
+++ b/src/args.py
@@ -101,6 +101,7 @@ def parse(self, argv: Sequence[str], meta: dict[str, Any]) -> tuple[dict[str, An
parser.add_argument('-mal', '--mal', nargs=1, required=False, help="MAL ID", type=str, dest='mal_manual')
parser.add_argument('-tvmaze', '--tvmaze', nargs=1, required=False, help="TVMAZE ID", type=str, dest='tvmaze_manual')
parser.add_argument('-tvdb', '--tvdb', nargs=1, required=False, help="TVDB ID", type=str, dest='tvdb_manual')
+ parser.add_argument("-douban", "--douban", nargs=1, required=False, help="Douban ID (Number only)", type=int, dest="douban_manual", default=0)
parser.add_argument('-g', '--tag', nargs='*', required=False, help="Group Tag", type=str)
parser.add_argument('-serv', '--service', nargs='*', required=False, help="Streaming Service", type=str)
parser.add_argument('-dist', '--distributor', nargs='*', required=False, help="Disc Distributor e.g.(Criterion, BFI, etc.)", type=str)
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index a84371185..d9d8d1b65 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -399,6 +399,13 @@ async def get_additional_checks(self, meta: dict[str, Any]):
async def get_douban_id(self, meta: Meta) -> int:
douban_id: int = 0
+ douban_manual = int(meta.get("douban_manual", 0))
+
+ if douban_manual:
+ console.print(f"{self.tracker}: Using manual Douban ID: {douban_manual}")
+ self.douban_id = douban_manual
+ return douban_manual
+
imdb_id = meta.get("imdb_info", {}).get("imdbID")
if not imdb_id:
return douban_id
@@ -407,8 +414,10 @@ async def get_douban_id(self, meta: Meta) -> int:
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}
try:
- response = await self.session.get(search_url, headers=headers)
- response.raise_for_status()
+ async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
+ response = await client.get(search_url)
+ response.raise_for_status()
+
soup = BeautifulSoup(response.text, "html.parser")
result = soup.find("ul", class_="search_results_subjects")
@@ -425,8 +434,8 @@ async def get_douban_id(self, meta: Meta) -> int:
console.print(f"{self.tracker}: [bold yellow]No Douban ID found for IMDb ID {imdb_id}.[/bold yellow]")
return douban_id
- except Exception:
- console.print(f"{self.tracker}: [bold yellow]Failed to fetch Douban ID for IMDb ID {imdb_id}.[/bold yellow]")
+ except Exception as e:
+ console.print(f"{self.tracker}: [bold yellow]Failed to fetch Douban ID for IMDb ID {imdb_id}: {e}[/bold yellow]")
return douban_id
async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]:
@@ -690,7 +699,7 @@ async def upload(self, meta: Meta, _) -> bool:
return False
else:
- console.print("[cyan]DC Request Data:")
+ console.print("[cyan]M-Team Request Data:")
console.print(Redaction.redact_private_info(data))
meta["tracker_status"][self.tracker]["status_message"] = "Debug mode enabled, not uploading"
await self.common.create_torrent_for_upload(meta, f"{self.tracker}" + "_DEBUG", f"{self.tracker}" + "_DEBUG", announce_url="https://fake.tracker")
From 307b431e30e214e7dea52928a65dfabd89ab5375 Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Sat, 28 Mar 2026 00:28:15 -0300
Subject: [PATCH 09/13] add support for request handling
---
src/trackers/MTEAM.py | 51 +++++++++++++++++++++++++++++++++++++++++++
src/trackersetup.py | 2 +-
2 files changed, 52 insertions(+), 1 deletion(-)
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index d9d8d1b65..ed43a408c 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -39,6 +39,7 @@ def __init__(self, config: Config):
self.api_base_url = "https://api.m-team.cc/api"
self.torrent_url = f"{self.base_url}/detail/"
+ self.requests_url = f"{self.api_base_url}/seek/search"
self.api_key = self.config["TRACKERS"][self.tracker].get("api_key")
self.banned_groups = ["FGT"]
@@ -53,6 +54,56 @@ def __init__(self, config: Config):
self.douban_id: int = 0
+ async def get_requests(self, meta: dict[str, Any]) -> list[dict[str, str]]:
+ requests: list[dict[str, str]] = []
+
+ category = self.get_category_id(meta)
+
+ payload = {
+ "pageNumber": 1,
+ "pageSize": 10,
+ "keyword": meta["title"],
+ "take": False,
+ }
+
+ try:
+ response = await self.session.post(self.requests_url, json=payload, timeout=15)
+ response.raise_for_status()
+ res_json = response.json()
+ console.print(res_json)
+
+ data_list = res_json.get("data", {}).get("data", [])
+
+ for item in data_list:
+ if item.get("category") != category:
+ continue
+
+ name = item.get("title", "N/A")
+ reward = item.get("rewardCurrent", "0")
+ link = f"{self.base_url}/seekDetail?id={item.get('id')}"
+
+ requests.append(
+ {
+ "Name": name,
+ "Reward": reward,
+ "Link": link,
+ }
+ )
+
+ if requests:
+ message = f"\n{self.tracker}: [bold yellow]Your upload may fulfill the following request(s), check it out:[/bold yellow]\n\n"
+ for r in requests:
+ message += f"[bold green]Name:[/bold green] {r['Name']}\n"
+ message += f"[bold green]Reward:[/bold green] {r['Reward']}\n"
+ message += f"[bold green]Link:[/bold green] {r['Link']}\n\n"
+ console.print(message)
+
+ return requests
+
+ except Exception as e:
+ console.print(f"{self.tracker}: [bold red]Error searching for requests with title {meta['title']}: {e}[/bold red]")
+ return requests
+
async def mediainfo(self, meta: Meta) -> str:
mi_path: str = ""
mediainfo: str = ""
diff --git a/src/trackersetup.py b/src/trackersetup.py
index 688ac7a88..ed2d80a8e 100644
--- a/src/trackersetup.py
+++ b/src/trackersetup.py
@@ -718,7 +718,7 @@ async def process_single_tracker(tracker_name: str) -> Union[bool, list[JsonDict
if not url:
return False
requests = await self.bhd_request_check(meta, tracker_name, url)
- elif tracker_name.upper() in ('ASC', 'BJS', 'FF', 'HDS', 'AZ', 'CZ', 'PHD'):
+ elif tracker_name.upper() in ("ASC", "BJS", "FF", "HDS", "AZ", "CZ", "PHD", "MTEAM"):
# These trackers have custom request handling
requests = cast(list[JsonDict], await tracker_instance.get_requests(meta))
return False
From 753aeafa712ed606d0d1bc198bfb7ad57554d5fd Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Sat, 28 Mar 2026 00:49:41 -0300
Subject: [PATCH 10/13] refine search
---
src/trackers/MTEAM.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index ed43a408c..0dbb8a493 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -70,7 +70,6 @@ async def get_requests(self, meta: dict[str, Any]) -> list[dict[str, str]]:
response = await self.session.post(self.requests_url, json=payload, timeout=15)
response.raise_for_status()
res_json = response.json()
- console.print(res_json)
data_list = res_json.get("data", {}).get("data", [])
@@ -450,7 +449,11 @@ async def get_additional_checks(self, meta: dict[str, Any]):
async def get_douban_id(self, meta: Meta) -> int:
douban_id: int = 0
- douban_manual = int(meta.get("douban_manual", 0))
+ try:
+ douban_manual = int(meta.get("douban_manual") or 0)
+ except (ValueError, TypeError):
+ console.print(f"{self.tracker}: [bold yellow]Invalid douban_manual value, ignoring.[/bold yellow]")
+ douban_manual = 0
if douban_manual:
console.print(f"{self.tracker}: Using manual Douban ID: {douban_manual}")
@@ -498,6 +501,8 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
return dupes
imdb_id = meta.get("imdb_info", {}).get("imdbID")
+ category = self.get_category_id(meta)
+ standard = self.get_standard(meta)
if not imdb_id:
console.print(f"{self.tracker}: [bold yellow]Cannot perform search on {self.tracker}: IMDb ID not found in metadata.[/bold yellow]")
@@ -508,6 +513,8 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]
payload = {
"mode": "normal",
"imdb": imdb_id,
+ "categories": [category],
+ "standards": [standard],
}
try:
From 4aaef6b0736d1defaecfcf00821d6b8eb9329abb Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Sat, 28 Mar 2026 01:10:34 -0300
Subject: [PATCH 11/13] fix download
---
src/trackers/MTEAM.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index 0dbb8a493..0cefa19e8 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -724,16 +724,13 @@ async def upload(self, meta: Meta, _) -> bool:
data = response.json()
final_download_url = data.get("data")
if final_download_url:
- downloaded_torrent = await self.common.download_tracker_torrent(
+ await self.common.download_tracker_torrent(
meta,
self.tracker,
headers=dict(self.session.headers),
downurl=final_download_url,
)
- if downloaded_torrent:
- return True
- meta["tracker_status"][self.tracker]["status_message"] = "Upload succeeded but downloading the tracker torrent failed"
- return False
+ return True
console.print(f"{self.tracker}: Failed to get download URL from API response.")
meta["tracker_status"][self.tracker]["status_message"] = "Failed to get download URL from API response"
return False
From 30c40c99f29ee38427e01e6d4d1d66bee84deb8d Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 8 May 2026 22:05:41 -0300
Subject: [PATCH 12/13] more chinese sites
---
README.md | 73 +++---
data/example-config.py | 48 ++++
src/args.py | 1 +
src/audio.py | 21 +-
src/get_desc.py | 2 +-
src/metadata_searching.py | 48 +++-
src/trackers/COMMON.py | 64 +++++
src/trackers/LAJIDUI.py | 296 +++++++++++++++++++++++
src/trackers/LPT.py | 229 ++++++++++++++++++
src/trackers/MTEAM.py | 124 +---------
src/trackers/NEXUSPHP.py | 489 ++++++++++++++++++++++++++++++++++++++
src/trackers/PTCAFE.py | 310 ++++++++++++++++++++++++
src/trackers/PTFANS.py | 147 ++++++++++++
src/trackers/PTGTK.py | 185 ++++++++++++++
src/trackers/RPT.py | 179 ++++++++++++++
src/trackersetup.py | 12 +-
src/trackerstatus.py | 3 +
upload.py | 28 ++-
18 files changed, 2105 insertions(+), 154 deletions(-)
create mode 100644 src/trackers/LAJIDUI.py
create mode 100644 src/trackers/LPT.py
create mode 100644 src/trackers/NEXUSPHP.py
create mode 100644 src/trackers/PTCAFE.py
create mode 100644 src/trackers/PTFANS.py
create mode 100644 src/trackers/PTGTK.py
create mode 100644 src/trackers/RPT.py
diff --git a/README.md b/README.md
index 3a8cdb24a..5a55d2604 100644
--- a/README.md
+++ b/README.md
@@ -49,41 +49,44 @@ Many thanks to all who have contributed.
|Name|Acronym|Name|Acronym|
|-|:-:|-|:-:|
-|Aither|AITHER|Alpharatio|AR|
-|Amigos-Share|ASC|Anthelion|ANT|
-|AsianCinema|ACM|Aura4K|A4K|
-|AvistaZ|AZ|Beyond-HD|BHD|
-|BitHDTV|BHDTV|Blutopia|BLU|
-|BrasilJapão-Share|BJS|BrasilTracker|BT|
-|CapybaraBR|CBR|CinemaZ|CZ|
-|Cinematik|TIK|DarkPeers|DP|
-|DigitalCore|DC|DesiTorrents|DT|
-|Emuwarez|EMUW|
-|FearNoPeer|FNP|FileList|FL|
-|Friki|FRIKI|FunFile|FF|
-|GreatPosterWall|GPW|hawke-uno|HUNO|
-|HDBits|HDB|HD-Space|HDS|
-|HD-Torrents|HDT|HomieHelpDesk|HHD|
-|ImmortalSeed|IS|InfinityHD|IHD|
-|ItaTorrents|ITT|LastDigitalUnderground|LDU|
-|Lat-Team|LT|Locadora|LCD|
-|LST|LST|Luminarr|LUME|
-|MoreThanTV|MTV|M-Team|MTEAM|
-|Nebulance|NBL|OldToonsWorld|OTW|
-|OnlyEncodes+|OE|PassThePopcorn|PTP|
-|PolishTorrent|PTT|Portugas|PT|
-|PrivateHD|PHD|PTerClub|PTER|
-|PTSKIT|PTS|Racing4Everyone|R4E|
-|Rastastugan|RAS|ReelFLiX|RF|
-|RetroFlix|RTF|Samaritano|SAM|
-|seedpool|SP|ShareIsland|SHRI|
-|SkipTheCommerials|STC|SpeedApp|SPD|
-|Swarmazon|SN|The Leach Zone|TLZ|
-|TheOldSchool|TOS|ToTheGlory|TTG|
-|TorrentHR|THR|Torrenteros|TTR|
-|TorrentLeech|TL|TVChaosUK|TVC|
-|ULCX|ULCX|UTOPIA|UTP|
-|YOiNKED|YOINK|YUSCENE|YUS|
+Aither|AITHER|Luminarr|LUME
+Alpharatio|AR|MoreThanTV|MTV
+Amigos-Share|ASC|M-Team|MTEAM
+Anthelion|ANT|Nebulance|NBL
+AsianCinema|ACM|OldToonsWorld|OTW
+Aura4K|A4K|OnlyEncodes+|OE
+AvistaZ|AZ|PassThePopcorn|PTP
+Beyond-HD|BHD|PolishTorrent|PTT
+BitHDTV|BHDTV|Portugas|PT
+Blutopia|BLU|PrivateHD|PHD
+BrasilJapão-Share|BJS|PT GTK|PTGTK
+BrasilTracker|BT|ptcafe|PTCAFE
+CapybaraBR|CBR|PTerClub|PTER
+Cinematik|TIK|PTFans|PTFANS
+CinemaZ|CZ|PTSKIT|PTS
+DarkPeers|DP|Racing4Everyone|R4E
+DesiTorrents|DT|RailgunPT|RPT
+DigitalCore|DC|Rastastugan|RAS
+Emuwarez|EMUW|ReelFLiX|RF
+FearNoPeer|FNP|RetroFlix|RTF
+FileList|FL|Samaritano|SAM
+Friki|FRIKI|seedpool|SP
+FunFile|FF|ShareIsland|SHRI
+GreatPosterWall|GPW|SkipTheCommerials|STC
+hawke-uno|HUNO|SpeedApp|SPD
+HDBits|HDB|Swarmazon|SN
+HD-Space|HDS|The Leach Zone|TLZ
+HD-Torrents|HDT|TheOldSchool|TOS
+HomieHelpDesk|HHD|Torrenteros|TTR
+ImmortalSeed|IS|TorrentHR|THR
+InfinityHD|IHD|TorrentLeech|TL
+ItaTorrents|ITT|ToTheGlory|TTG
+lajidui|LAJIDUI|TVChaosUK|TVC
+LastDigitalUnderground|LDU|ULCX|ULCX
+Lat-Team|LT|UTOPIA|UTP
+Locadora|LCD|YOiNKED|YOINK
+LongPT|LPT|YUSCENE|YUS
+LST|LST||
## **Setup:**
- **REQUIRES AT LEAST PYTHON 3.9 AND PIP3**
diff --git a/data/example-config.py b/data/example-config.py
index c25e855fb..6b603cf0b 100644
--- a/data/example-config.py
+++ b/data/example-config.py
@@ -638,6 +638,14 @@
"api_key": "",
"anon": False,
},
+ "LAJIDUI": {
+ # Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
+ "link_dir_name": "",
+ # for LAJIDUI to work you need to export cookies from https://pt.lajidui.top/ using https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/.
+ # cookies need to be in netscape format and need to be in data/cookies/LAJIDUI.txt
+ "announce_url": "https://pt.lajidui.top/announce.php?passkey=",
+ "anon": False,
+ },
"LCD": {
# Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
"link_dir_name": "",
@@ -650,6 +658,14 @@
"api_key": "",
"anon": False,
},
+ "LPT": {
+ # Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
+ "link_dir_name": "",
+ # for LPT to work you need to export cookies from https://longpt.org/ using https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/.
+ # cookies need to be in netscape format and need to be in data/cookies/LPT.txt
+ "announce_url": "https://longpt.org/announce.php?passkey=",
+ "anon": False,
+ },
"LST": {
# Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
"link_dir_name": "",
@@ -739,6 +755,14 @@
"api_key": "",
"anon": False,
},
+ "PTCAFE": {
+ # Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
+ "link_dir_name": "",
+ # for PTCAFE to work you need to export cookies from https://ptcafe.club/ using https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/.
+ # cookies need to be in netscape format and need to be in data/cookies/PTCAFE.txt
+ "announce_url": "https://tracker.ptcafe.club/announce.php?passkey=",
+ "anon": False,
+ },
"PTER": { # Does not appear to be working at all
# Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
"link_dir_name": "",
@@ -749,6 +773,22 @@
"ptgen_api": "",
"anon": True,
},
+ "PTFANS": {
+ # Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
+ "link_dir_name": "",
+ # for PTFANS to work you need to export cookies from https://ptfans.cc/ using https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/.
+ # cookies need to be in netscape format and need to be in data/cookies/PTFANS.txt
+ "announce_url": "https://ptfans.cc/announce.php?passkey=",
+ "anon": False,
+ },
+ "PTGTK": {
+ # Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
+ "link_dir_name": "",
+ # for PTGTK to work you need to export cookies from https://pt.gtkpw.xyz using https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/.
+ # cookies need to be in netscape format and need to be in data/cookies/PTGTK.txt
+ "announce_url": "https://t.myaltbox.com/announce.php?passkey=",
+ "anon": False,
+ },
"PTP": {
# Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
"link_dir_name": "",
@@ -793,6 +833,14 @@
"api_key": "",
"anon": False,
},
+ "RPT": {
+ # Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
+ "link_dir_name": "",
+ # for RPT to work you need to export cookies from https://bilibili.download using https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/.
+ # cookies need to be in netscape format and need to be in data/cookies/RPT.txt
+ "announce_url": "https://bilibili.download/announce.php?passkey=",
+ "anon": False,
+ },
"RTF": {
# Instead of using the tracker acronym for folder name when sym/hard linking, you can use a custom name
"link_dir_name": "",
diff --git a/src/args.py b/src/args.py
index 5a84f756f..e2ccba8b1 100644
--- a/src/args.py
+++ b/src/args.py
@@ -198,6 +198,7 @@ def parse(self, argv: Sequence[str], meta: dict[str, Any]) -> tuple[dict[str, An
parser.add_argument('-emby_debug', '--emby_debug', action='store_true', required=False, help="Does debugging stuff for Audionut")
parser.add_argument('-ch', '--channel', nargs=1, required=False, help="SPD only: Channel ID number or tag to upload to (preferably the ID), without '@'. Example: '-ch spd' when using a tag, or '-ch 1' when using an ID.", type=str, dest='spd_channel', default="")
parser.add_argument("-excl", "--exclusive", nargs=1, required=False, help="Set exclusive flag on all supported trackers", dest="exclusive")
+ parser.add_argument("-diy", "--diy-disc", nargs=1, required=False, help="Set DIY flag for disc uploads", dest="diy_disc")
parsed_args_ns, before_args = parser.parse_known_args(input)
parsed_args: dict[str, Any] = vars(parsed_args_ns)
# console.print(args)
diff --git a/src/audio.py b/src/audio.py
index c636d11bc..5b43b0be2 100644
--- a/src/audio.py
+++ b/src/audio.py
@@ -506,7 +506,26 @@ def bloated_check(meta: Meta, audio_languages: Union[Sequence[str], str], is_eng
if isinstance(audio_languages, str):
audio_languages = [audio_languages]
- bloat_is_allowed = ["ASC", "BJS", "BT", "CBR", "DC", "FF", "LCD", "SAM", "SHRI", "SP", "TL", "TOS"]
+ bloat_is_allowed = [
+ "ASC",
+ "BJS",
+ "BT",
+ "CBR",
+ "DC",
+ "FF",
+ "LAJIDUI",
+ "LCD",
+ "LPT",
+ "PTCAFE",
+ "PTFANS",
+ "PTGTK",
+ "RPT",
+ "SAM",
+ "SHRI",
+ "SP",
+ "TL",
+ "TOS",
+ ]
# Trackers that allow specific languages (list of allowed language codes per tracker)
tracker_allowed_bloat_languages = {
"AITHER": ["en"],
diff --git a/src/get_desc.py b/src/get_desc.py
index a15b2d3cb..d28c1c29c 100644
--- a/src/get_desc.py
+++ b/src/get_desc.py
@@ -321,7 +321,7 @@ async def get_tv_info(self, meta: dict[str, Any], resize: bool = False) -> tuple
return title, image, overview
async def get_mediainfo_section(self, meta: dict[str, Any]) -> str:
- """Returns the mediainfo/bdinfo section, using a cache file if available."""
+ """Returns the mediainfo section, using a cache file if available."""
if meta.get("is_disc") == "BDMV":
return ""
diff --git a/src/metadata_searching.py b/src/metadata_searching.py
index ac397c407..844d77206 100644
--- a/src/metadata_searching.py
+++ b/src/metadata_searching.py
@@ -4,6 +4,9 @@
from collections.abc import Awaitable
from typing import Any, Optional, Union, cast
+import httpx
+from bs4 import BeautifulSoup
+
from src.console import console
from src.imdb import imdb_manager
from src.tmdb import TmdbManager
@@ -1038,7 +1041,50 @@ async def get_tvdb_tvmaze_tmdb_episode_data(meta: dict[str, Any], tvdb_handler:
console.print("[green]TMDb episode data retrieved successfully.[/green]")
elif isinstance(tmdb_episode_data, Exception):
console.print(f"[yellow]TMDb episode data retrieval failed: {tmdb_episode_data}[/yellow]")
-
return meta
+async def get_douban_id(meta) -> int:
+ douban_id: int = 0
+ try:
+ douban_manual = int(meta.get("douban_manual") or 0)
+ except (ValueError, TypeError):
+ console.print("[bold yellow]Invalid douban_manual value, ignoring.[/bold yellow]")
+ douban_manual = 0
+
+ if douban_manual:
+ console.print(f"Using manual Douban ID: {douban_manual}")
+ return douban_manual
+
+ imdb_id = meta.get("imdb_info", {}).get("imdbID")
+ if not imdb_id:
+ return douban_id
+
+ search_url = f"https://m.douban.com/search/?query={imdb_id}"
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}
+
+ try:
+ async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
+ response = await client.get(search_url)
+ response.raise_for_status()
+
+ soup = BeautifulSoup(response.text, "html.parser")
+ result = soup.find("ul", class_="search_results_subjects")
+
+ if result:
+ link_tag = result.find("a")
+ if link_tag and "href" in link_tag.attrs:
+ link_mobile = str(link_tag["href"])
+ match = re.search(r"subject/(\d+)", link_mobile)
+ if match:
+ douban_id = int(match.group(1))
+ return douban_id
+
+ console.print(f"[bold yellow]No Douban ID found for IMDb ID {imdb_id}.[/bold yellow]")
+ return douban_id
+
+ except Exception as e:
+ console.print(f"[bold yellow]Failed to fetch Douban ID for IMDb ID {imdb_id}: {e}[/bold yellow]")
+ return douban_id
+
+
diff --git a/src/trackers/COMMON.py b/src/trackers/COMMON.py
index b85fe0563..be647e37a 100644
--- a/src/trackers/COMMON.py
+++ b/src/trackers/COMMON.py
@@ -1313,3 +1313,67 @@ async def save_html_file(self, meta: dict[str, Any], tracker: str, text: str = "
async with aiofiles.open(html_path, "w", encoding="utf-8") as f:
await f.write(text)
return html_path
+
+ def get_bitrates(self, meta: dict[str, Any]) -> tuple[int, int]:
+ """
+ Extract video and audio bitrates from meta.
+
+ :param meta: Meta data.
+ :return: Tuple of video and audio bitrates.
+ :rtype: tuple[int, int]
+ """
+ v_raw = None
+ a_raw = None
+ is_bdmv = meta.get("is_disc") == "BDMV"
+ is_dvd = meta.get("is_disc") == "DVD"
+
+ if is_bdmv:
+ discs = meta.get("discs", [])
+ if discs:
+ bdinfo = discs[0].get("bdinfo", {})
+ v_tracks = bdinfo.get("video", [])
+ a_tracks = bdinfo.get("audio", [])
+ if v_tracks:
+ v_raw = v_tracks[0].get("bitrate")
+ if a_tracks:
+ a_raw = a_tracks[0].get("bitrate")
+ elif is_dvd:
+ pass
+ else:
+ tracks = meta.get("mediainfo", {}).get("media", {}).get("track", [])
+ for track in tracks:
+ t_type = track.get("@type")
+ if t_type == "Video" and v_raw is None:
+ v_raw = track.get("BitRate")
+ elif t_type == "Audio" and a_raw is None:
+ a_raw = track.get("BitRate")
+
+ def clean_to_int(val, bdmv_mode):
+ if not val or isinstance(val, dict):
+ return 0
+
+ try:
+ if bdmv_mode:
+ numeric_match = re.search(r"\d+", str(val).replace(".", "").replace(",", ""))
+ return int(numeric_match.group()) if numeric_match else 0
+ else:
+ return int(val) // 1000
+ except (ValueError, TypeError, AttributeError):
+ return 0
+
+ return (clean_to_int(v_raw, is_bdmv), clean_to_int(a_raw, is_bdmv))
+
+ def get_small_description(self, meta: dict[str, Any]) -> str:
+ """
+ Generate a small description from meta data.
+ Mainly used for Chinese trackers.
+
+ :param meta: Meta data.
+ :return: Small description.
+ :rtype: str
+ """
+ resolution = meta.get("resolution", "")
+ audio = meta.get("audio", "")
+ video_bitrate, audio_bitrate = self.get_bitrates(meta)
+
+ return f"{resolution} @ {video_bitrate} kbps - {audio} @ {audio_bitrate} kbps"
diff --git a/src/trackers/LAJIDUI.py b/src/trackers/LAJIDUI.py
new file mode 100644
index 000000000..c9c2709b2
--- /dev/null
+++ b/src/trackers/LAJIDUI.py
@@ -0,0 +1,296 @@
+# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
+import re
+from typing import Any
+
+from src.trackers.NEXUSPHP import NEXUSPHP
+
+Meta = dict[str, Any]
+Config = dict[str, Any]
+
+
+class LAJIDUI(NEXUSPHP):
+ def __init__(self, config: Config) -> None:
+ super().__init__(config, "LAJIDUI")
+ self.banned_groups = []
+ self.base_url = "https://pt.lajidui.top"
+ self.source_flag = "[pt.lajidui.top] lajidui"
+ self.torrent_url = f"{self.base_url}/details.php?id="
+
+ def get_category(self, meta: Meta) -> int:
+ animations = 405
+ documentaries = 404
+ movies = 401
+ tv_series = 402
+ tv_shows = 403
+
+ category = str(meta.get("category", "")).upper()
+ genres = str(meta.get("genres", "")).lower()
+ keywords = str(meta.get("keywords", "")).lower()
+
+ if "documentary" in genres or "documentary" in keywords:
+ return documentaries
+ if meta.get("anime") or "animation" in genres or "animation" in keywords:
+ return animations
+
+ if category == "MOVIE":
+ return movies
+ if category == "TV":
+ game_show_keywords = [
+ "award show",
+ "competition",
+ "game show",
+ "music show",
+ "performance",
+ "reality television",
+ "reality tv",
+ "reality",
+ "stand-up",
+ "talk show",
+ "tv show",
+ "variety",
+ ]
+ if any(re.search(rf"(^|,\s*){re.escape(keyword)}(\s*,|$)", genres, re.IGNORECASE) for keyword in game_show_keywords):
+ return tv_shows
+ else:
+ return tv_series
+
+ return movies
+
+ def get_container(self, meta: Meta) -> int:
+ iso = 16
+ mkv = 10
+ mp4 = 11
+ other = 17
+
+ if meta.get("is_disc", ""):
+ return iso
+
+ container = str(meta.get("container", "")).lower()
+
+ if "mp4" in container:
+ return mp4
+ if "mkv" in container:
+ return mkv
+
+ return other
+
+ def get_region(self, meta: Meta) -> int:
+ america = [
+ 'AG', 'AI', 'AR', 'AW', 'BB', 'BL', 'BM', 'BO', 'BQ', 'BR', 'BS', 'BV', 'BZ', 'CA', 'CL',
+ 'CO', 'CR', 'CU', 'CW', 'DM', 'DO', 'EC', 'FK', 'GD', 'GF', 'GL', 'GP', 'GS', 'GT', 'GY',
+ 'HN', 'HT', 'JM', 'KN', 'KY', 'LC', 'MF', 'MQ', 'MS', 'MX', 'NI', 'PA', 'PE', 'PM', 'PR',
+ 'PY', 'SR', 'SV', 'SX', 'TC', 'TT', 'US', 'UY', 'VC', 'VE', 'VG', 'VI'
+ ] # fmt: off
+
+ europe = [
+ 'AD', 'AL', 'AT', 'AX', 'BA', 'BE', 'BG', 'BY', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI',
+ 'FO', 'FR', 'GB', 'GG', 'GI', 'GR', 'HR', 'HU', 'IE', 'IM', 'IS', 'IT', 'JE', 'LI', 'LT',
+ 'LU', 'LV', 'MC', 'MD', 'ME', 'MK', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'RU', 'SE',
+ 'SI', 'SJ', 'SK', 'SM', 'SU', 'UA', 'VA', 'XC'
+ ] # fmt: off
+
+ country = meta.get("origin_country", [])[0].upper()
+ if country in america or country in europe:
+ return 1
+ if country == "CN":
+ return 7
+ if country == "TW":
+ return 2
+ if country == "HK":
+ return 8
+ if country == "JP":
+ return 10
+ if country == "KR":
+ return 11
+ if country == "IN":
+ return 3
+
+ return 6
+
+ def get_type(self, meta: Meta) -> int:
+ blu_ray = 1
+ dvdr = 6
+ encode = 7
+ hd_dvd = 2
+ hdtv = 5
+ other = 11
+ remux = 3
+ web_dl = 10
+
+ is_disc = str(meta.get("is_disc", "")).lower()
+ mtype = str(meta.get("type", "")).lower()
+
+ if is_disc == "bdmv":
+ return blu_ray
+ if is_disc == "dvd":
+ return dvdr
+ if is_disc == "hddvd":
+ return hd_dvd
+
+ if mtype == "remux":
+ return remux
+ if mtype in ("webdl", "webrip"):
+ return web_dl
+ if mtype == "hdtv":
+ return hdtv
+ if mtype == "encode":
+ return encode
+
+ return other
+
+ def get_codec(self, meta: Meta) -> int:
+ av1 = 6
+ h264 = 1
+ h265 = 7
+ mpeg2 = 4
+ other = 5
+ vc1 = 2
+ xvid = 3
+
+ codec = str(meta.get("video_codec", "")).lower()
+
+ if "h265" in codec or "x265" in codec or "hevc" in codec:
+ return h265
+ if "h264" in codec or "x264" in codec or "avc" in codec:
+ return h264
+ if "vc1" in codec or "vc-1" in codec:
+ return vc1
+ if "mpeg2" in codec or "mpeg-2" in codec:
+ return mpeg2
+ if "av1" in codec:
+ return av1
+ if "xvid" in codec:
+ return xvid
+
+ return other
+
+ def get_resolution(self, meta: Meta) -> int:
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if resolution == "1080p":
+ return 1
+ if resolution == "1080i":
+ return 2
+ if resolution == "720p":
+ return 3
+ if meta.get("sd", False):
+ return 4
+ if resolution == "2160p":
+ return 6
+ if resolution == "4320p":
+ return 7
+
+ return 8
+
+ def get_audio_codec(self, meta: Meta) -> int:
+ audio_codec = str(meta.get("audio", "")).lower()
+
+ if "flac" in audio_codec:
+ return 1
+ if "ape" in audio_codec:
+ return 2
+ if "dts-hd" in audio_codec:
+ return 9
+ if "dts" in audio_codec:
+ return 3
+ if "mp3" in audio_codec:
+ return 4
+ if "ogg" in audio_codec:
+ return 5
+ if "aac" in audio_codec:
+ return 6
+ if "wav" in audio_codec:
+ return 8
+ if "true" in audio_codec:
+ return 10
+ if "lpcm" in audio_codec:
+ return 11
+ if "ddp" in audio_codec:
+ return 12
+ if "dd" in audio_codec:
+ return 13
+
+ return 7
+
+ def get_group_tag(self, meta: Meta) -> int:
+ group_tag = {
+ "-ade": 7,
+ "-agsvweb": 15,
+ "-beast": 18,
+ "-beitai": 21,
+ "-bmdru": 20,
+ "-catedu": 17,
+ "-chd": 2,
+ "-cmct": 8,
+ "-frds": 9,
+ "-godramas": 22,
+ "-hdhome": 14,
+ "-hdsky": 1,
+ "-hhweb": 6,
+ "-lhd": 19,
+ "-other": 5,
+ "-ourbits": 12,
+ "-pter": 16,
+ "-qhstudIo": 13,
+ "-tjupt": 10,
+ "-ubits": 11,
+ "-wiki": 4,
+ "-原创": 3,
+ }
+
+ group = str(meta.get("tag", "")).lower()
+ return group_tag.get(group, 5)
+
+ def get_checkboxes(self, meta: Meta) -> list[str]:
+ cantonese_audio = 11
+ chinese_and_english_audio = 15
+ chinese_and_english_subtitle = 16
+ chinese_audio = 5
+ chinese_subtitle = 6
+ diy = 4
+ english_audio = 14
+ hdr = 7
+ multi_track = 17
+ reposting_prohibited = 1
+ single_episode = 12
+
+ audio_tracks = meta.get("audio_languages", [])
+ subtitle_tracks = meta.get("subtitle_languages", [])
+ mhdr = meta.get("hdr", "")
+
+ checkboxes = []
+
+ if meta.get("exclusive", False):
+ checkboxes.append(str(reposting_prohibited))
+
+ if "Chinese" in audio_tracks or "Mandarin" in audio_tracks:
+ checkboxes.append(str(chinese_audio))
+
+ if "Chinese" in audio_tracks and "English" in audio_tracks:
+ checkboxes.append(str(chinese_and_english_audio))
+
+ if "English" in audio_tracks:
+ checkboxes.append(str(english_audio))
+
+ if "Chinese" in subtitle_tracks and "English" in subtitle_tracks:
+ checkboxes.append(str(chinese_and_english_subtitle))
+
+ if len(meta.get("audio_languages", [])) > 1:
+ checkboxes.append(str(multi_track))
+
+ if "Cantonese" in audio_tracks:
+ checkboxes.append(str(cantonese_audio))
+
+ if "Chinese" in subtitle_tracks or "Mandarin" in subtitle_tracks:
+ checkboxes.append(str(chinese_subtitle))
+
+ if "HDR" in mhdr.upper():
+ checkboxes.append(str(hdr))
+
+ if meta.get("diy_disc", False):
+ checkboxes.append(str(diy))
+
+ if meta["category"] == "TV" and not meta.get("tv_pack", False):
+ checkboxes.append(str(single_episode))
+
+ return checkboxes
diff --git a/src/trackers/LPT.py b/src/trackers/LPT.py
new file mode 100644
index 000000000..60571a466
--- /dev/null
+++ b/src/trackers/LPT.py
@@ -0,0 +1,229 @@
+# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
+import re
+from typing import Any
+
+from src.trackers.NEXUSPHP import NEXUSPHP
+
+Meta = dict[str, Any]
+Config = dict[str, Any]
+
+
+class LPT(NEXUSPHP):
+ def __init__(self, config: Config) -> None:
+ super().__init__(config, "LPT")
+ self.banned_groups = []
+ self.base_url = "https://longpt.org"
+ self.source_flag = "[longpt.org] LongPT"
+ self.torrent_url = f"{self.base_url}/details.php?id="
+
+ def get_category(self, meta: Meta) -> int:
+ animations = 405
+ documentaries = 404
+ movies = 401
+ tv_series = 402
+ tv_shows = 403
+
+ category = str(meta.get("category", "")).upper()
+ genres = str(meta.get("genres", "")).lower()
+ keywords = str(meta.get("keywords", "")).lower()
+
+ if "documentary" in genres or "documentary" in keywords:
+ return documentaries
+ if meta.get("anime") or "animation" in genres or "animation" in keywords:
+ return animations
+
+ if category == "MOVIE":
+ return movies
+ if category == "TV":
+ game_show_keywords = [
+ "award show",
+ "competition",
+ "game show",
+ "music show",
+ "performance",
+ "reality television",
+ "reality tv",
+ "reality",
+ "stand-up",
+ "talk show",
+ "tv show",
+ "variety",
+ ]
+ if any(re.search(rf"(^|,\s*){re.escape(keyword)}(\s*,|$)", genres, re.IGNORECASE) for keyword in game_show_keywords):
+ return tv_shows
+ else:
+ return tv_series
+
+ return movies
+
+ def get_type(self, meta: Meta) -> int:
+ blu_ray = 1
+ dvd = 6
+ encode = 7
+ remux = 3
+ tv = 5
+ uhd_master_disc = 2
+ uhd_remux = 11
+ web_dl = 4
+
+ is_disc = str(meta.get("is_disc", "")).lower()
+ mtype = str(meta.get("type", "")).lower()
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if is_disc == "bdmv":
+ if resolution == "2160p":
+ return uhd_master_disc
+ else:
+ return blu_ray
+
+ if "dvd" in is_disc:
+ return dvd
+
+ if mtype == "remux":
+ if resolution == "2160p":
+ return uhd_remux
+ return remux
+
+ if mtype in ("webdl", "webrip"):
+ return web_dl
+
+ if "tv" in mtype:
+ return tv
+
+ if mtype == "encode":
+ return encode
+
+ return encode
+
+ def get_codec(self, meta: Meta) -> int:
+ av1 = 5
+ h264 = 1
+ h265 = 2
+ mpeg2 = 4
+ other = 6
+ vc1 = 3
+
+ codec = str(meta.get("video_codec", "")).lower()
+
+ if "h265" in codec or "x265" in codec or "hevc" in codec:
+ return h265
+ if "h264" in codec or "x264" in codec or "avc" in codec:
+ return h264
+ if "vc1" in codec or "vc-1" in codec:
+ return vc1
+ if "mpeg2" in codec or "mpeg-2" in codec:
+ return mpeg2
+ if "av1" in codec:
+ return av1
+
+ return other
+
+ def get_resolution(self, meta: Meta) -> int:
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if "4320" in resolution:
+ return 6
+ if "2160" in resolution:
+ return 5
+ if "1440" in resolution:
+ return 1
+ if "1080" in resolution:
+ return 2
+ if "720" in resolution:
+ return 3
+ if meta.get("sd", False):
+ return 4
+
+ return 7
+
+ def get_audio_codec(self, meta: Meta) -> int:
+ audio_codec = str(meta.get("audio", "")).lower()
+
+ if "flac" in audio_codec:
+ return 1
+ if "dts-hd" in audio_codec:
+ return 3
+ if "dts:x" in audio_codec:
+ return 12
+ if "dts" in audio_codec:
+ return 13
+ if "lpcm" in audio_codec:
+ return 14
+ if "dd" in audio_codec:
+ return 15
+ if "alac" in audio_codec:
+ return 16
+ if "wav" in audio_codec:
+ return 17
+ if "av3a" in audio_codec:
+ return 18
+ if "true" in audio_codec:
+ return 19
+ if "ape" in audio_codec:
+ return 2
+ if "mp3" in audio_codec:
+ return 4
+ if "ogg" in audio_codec:
+ return 5
+ if "aac" in audio_codec:
+ return 6
+ if "m4a" in audio_codec:
+ return 8
+ if "atmos" in audio_codec:
+ return 9
+ if "ddp" in audio_codec:
+ return 10
+
+ return 11
+
+ def get_group_tag(self, meta: Meta) -> int:
+ group_tag = {
+ "-cmct": 7,
+ "-hhweb": 8,
+ "-longa": 1,
+ "-longpt": 3,
+ "-longweb": 2,
+ "-rl": 6,
+ "-wiki": 4,
+ }
+
+ group = str(meta.get("tag", "")).lower()
+ return group_tag.get(group, 5)
+
+ def get_checkboxes(self, meta: Meta) -> list[str]:
+ chinese_audio = 5
+ chinese_subtitle = 6
+ diy = 4
+ english_audio = 9
+ hdr = 7
+ reposting_prohibited = 1
+
+ audio_tracks = meta.get("audio_languages", [])
+ subtitle_tracks = meta.get("subtitle_languages", [])
+ mhdr = meta.get("hdr", "")
+
+ checkboxes = []
+
+ if meta.get("exclusive", False):
+ checkboxes.append(str(reposting_prohibited))
+
+ if "Chinese" in audio_tracks or "Mandarin" in audio_tracks:
+ checkboxes.append(str(chinese_audio))
+
+ if "English" in audio_tracks:
+ checkboxes.append(str(english_audio))
+
+ if "Chinese" in subtitle_tracks or "Mandarin" in subtitle_tracks:
+ checkboxes.append(str(chinese_subtitle))
+
+ if "HDR" in mhdr.upper():
+ checkboxes.append(str(hdr))
+
+ if meta.get("diy_disc", False):
+ checkboxes.append(str(diy))
+
+ return checkboxes
+
+ def get_douban_url(self, meta: Meta) -> str:
+ _ = meta
+ return ""
diff --git a/src/trackers/MTEAM.py b/src/trackers/MTEAM.py
index 0cefa19e8..6dc68b832 100644
--- a/src/trackers/MTEAM.py
+++ b/src/trackers/MTEAM.py
@@ -6,7 +6,6 @@
import aiofiles
import httpx
-from bs4 import BeautifulSoup
from cogs.redaction import Redaction
from src.console import console
@@ -52,8 +51,6 @@ def __init__(self, config: Config):
timeout=30.0,
)
- self.douban_id: int = 0
-
async def get_requests(self, meta: dict[str, Any]) -> list[dict[str, str]]:
requests: list[dict[str, str]] = []
@@ -81,13 +78,11 @@ async def get_requests(self, meta: dict[str, Any]) -> list[dict[str, str]]:
reward = item.get("rewardCurrent", "0")
link = f"{self.base_url}/seekDetail?id={item.get('id')}"
- requests.append(
- {
- "Name": name,
- "Reward": reward,
- "Link": link,
- }
- )
+ requests.append({
+ "Name": name,
+ "Reward": reward,
+ "Link": link,
+ })
if requests:
message = f"\n{self.tracker}: [bold yellow]Your upload may fulfill the following request(s), check it out:[/bold yellow]\n\n"
@@ -140,15 +135,16 @@ def bbcode_to_markdown(self, text):
return text
- async def get_douban_info(self) -> dict[str, Any]:
+ async def get_douban_info(self, meta: Meta) -> dict[str, Any]:
info: dict[str, Any] = {}
- if not self.douban_id:
+ douban_id = meta.get("douban_id")
+ if not douban_id:
return info
api_url = f"{self.api_base_url}/media/douban/infoV2"
params = {
- "code": self.douban_id,
+ "code": douban_id,
"refresh": False,
}
@@ -168,7 +164,7 @@ async def get_douban_info(self) -> dict[str, Any]:
return info
async def mteam_standard_desc(self, meta: Meta):
- db_info = await self.get_douban_info()
+ db_info = await self.get_douban_info(meta)
if db_info and db_info.get("code") == "0":
d = db_info.get("data", {})
@@ -355,55 +351,6 @@ def get_category_id(self, meta: Meta) -> Optional[int]:
# Default to HD
return tv_series_hd if meta["category"] == "TV" else movie_hd
- def get_small_description(self, meta: Meta) -> str:
- resolution = meta.get("resolution", "")
- audio = meta.get("audio", "")
- video_bitrate, audio_bitrate = self.get_bitrates(meta)
-
- return f"{resolution} @ {video_bitrate} kbps - {audio} @ {audio_bitrate} kbps"
-
- def get_bitrates(self, meta) -> tuple[int, int]:
- v_raw = None
- a_raw = None
- is_bdmv = meta.get("is_disc") == "BDMV"
- is_dvd = meta.get("is_disc") == "DVD"
-
- if is_bdmv:
- discs = meta.get("discs", [])
- if discs:
- bdinfo = discs[0].get("bdinfo", {})
- v_tracks = bdinfo.get("video", [])
- a_tracks = bdinfo.get("audio", [])
- if v_tracks:
- v_raw = v_tracks[0].get("bitrate")
- if a_tracks:
- a_raw = a_tracks[0].get("bitrate")
- elif is_dvd:
- pass
- else:
- tracks = meta.get("mediainfo", {}).get("media", {}).get("track", [])
- for track in tracks:
- t_type = track.get("@type")
- if t_type == "Video" and v_raw is None:
- v_raw = track.get("BitRate")
- elif t_type == "Audio" and a_raw is None:
- a_raw = track.get("BitRate")
-
- def clean_to_int(val, bdmv_mode):
- if not val or isinstance(val, dict):
- return 0
-
- try:
- if bdmv_mode:
- numeric_match = re.search(r"\d+", str(val).replace(".", "").replace(",", ""))
- return int(numeric_match.group()) if numeric_match else 0
- else:
- return int(val) // 1000
- except (ValueError, TypeError, AttributeError):
- return 0
-
- return (clean_to_int(v_raw, is_bdmv), clean_to_int(a_raw, is_bdmv))
-
async def get_additional_checks(self, meta: dict[str, Any]):
should_continue = True
@@ -447,51 +394,6 @@ async def get_additional_checks(self, meta: dict[str, Any]):
return should_continue
- async def get_douban_id(self, meta: Meta) -> int:
- douban_id: int = 0
- try:
- douban_manual = int(meta.get("douban_manual") or 0)
- except (ValueError, TypeError):
- console.print(f"{self.tracker}: [bold yellow]Invalid douban_manual value, ignoring.[/bold yellow]")
- douban_manual = 0
-
- if douban_manual:
- console.print(f"{self.tracker}: Using manual Douban ID: {douban_manual}")
- self.douban_id = douban_manual
- return douban_manual
-
- imdb_id = meta.get("imdb_info", {}).get("imdbID")
- if not imdb_id:
- return douban_id
-
- search_url = f"https://m.douban.com/search/?query={imdb_id}"
- headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}
-
- try:
- async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
- response = await client.get(search_url)
- response.raise_for_status()
-
- soup = BeautifulSoup(response.text, "html.parser")
- result = soup.find("ul", class_="search_results_subjects")
-
- if result:
- link_tag = result.find("a")
- if link_tag and "href" in link_tag.attrs:
- link_mobile = str(link_tag["href"])
- match = re.search(r"subject/(\d+)", link_mobile)
- if match:
- douban_id = int(match.group(1))
- self.douban_id = douban_id
- return douban_id
-
- console.print(f"{self.tracker}: [bold yellow]No Douban ID found for IMDb ID {imdb_id}.[/bold yellow]")
- return douban_id
-
- except Exception as e:
- console.print(f"{self.tracker}: [bold yellow]Failed to fetch Douban ID for IMDb ID {imdb_id}: {e}[/bold yellow]")
- return douban_id
-
async def search_existing(self, meta: dict[str, Any], _) -> list[dict[str, Any]]:
dupes: list[dict[str, Any]] = []
@@ -661,13 +563,11 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
"""
https://test2.m-team.cc/api/swagger-ui/index.html#/種子/createOredit
"""
- douban_id = await self.get_douban_id(meta)
-
data = {
# "torrent": 0,
# "offer": 0,
"name": meta["name"],
- "smallDescr": self.get_small_description(meta),
+ "smallDescr": self.common.get_small_description(meta),
"descr": await self.generate_description(meta),
"category": self.get_category_id(meta),
# "source": 0,
@@ -679,7 +579,7 @@ async def fetch_data(self, meta: Meta) -> dict[str, Any]:
# "processing": 0,
# "countries": "",
"imdb": meta.get("imdb_info", {}).get("imdbID", ""),
- "douban": douban_id,
+ "douban": meta.get("douban_id", 0),
# "dmmCode": "",
# "cids": "",
# "aids": "",
diff --git a/src/trackers/NEXUSPHP.py b/src/trackers/NEXUSPHP.py
new file mode 100644
index 000000000..e06c54787
--- /dev/null
+++ b/src/trackers/NEXUSPHP.py
@@ -0,0 +1,489 @@
+# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
+import json
+import os
+import platform
+from typing import Any, cast
+
+import aiofiles
+import httpx
+from bs4 import BeautifulSoup
+
+from src.console import console
+from src.cookie_auth import CookieAuthUploader, CookieValidator
+from src.get_desc import DescriptionBuilder
+from src.tmdb import TmdbManager
+from src.trackers.COMMON import COMMON
+
+Meta = dict[str, Any]
+Config = dict[str, Any]
+
+
+class NEXUSPHP:
+ def __init__(self, config: dict[str, Any], tracker_name: str):
+ self.common = COMMON(config)
+ self.config = config
+ self.cookie_auth_uploader = CookieAuthUploader(config)
+ self.cookie_validator = CookieValidator(config)
+ self.tmdb_manager = TmdbManager(config)
+ self.tracker = tracker_name
+ self.tracker_config: dict[str, Any] = self.config["TRACKERS"].get(self.tracker, {})
+
+ # Normalize announce_url: must be a non-empty string after stripping
+ raw_announce = self.tracker_config.get("announce_url")
+ self.announce_url = raw_announce.strip() if isinstance(raw_announce, str) else ""
+
+ # Default URLs - should be overridden by subclasses
+ self.base_url = ""
+ self.search_url = ""
+ self.source_flag = ""
+ self.torrent_url = ""
+ self.upload_url = ""
+
+ self.session = httpx.AsyncClient(headers={"User-Agent": f"Upload Assistant ({platform.system()} {platform.release()})"}, timeout=60.0)
+
+ async def load_localized_data(self, meta: dict[str, Any]) -> None:
+ localized_data_file = f"{meta['base_dir']}/tmp/{meta['uuid']}/tmdb_localized_data.json"
+ main_ch_data: dict[str, Any] = {}
+ data: dict[str, Any] = {}
+
+ if os.path.isfile(localized_data_file):
+ try:
+ async with aiofiles.open(localized_data_file, encoding="utf-8") as f:
+ content = await f.read()
+ loaded_data = json.loads(content)
+ data = cast(dict[str, Any], loaded_data) if isinstance(loaded_data, dict) else {}
+ except json.JSONDecodeError:
+ console.print(f"Warning: Could not decode JSON from {localized_data_file}", markup=False)
+ data = {}
+ except Exception as e:
+ console.print(f"Error reading file {localized_data_file}: {e}", markup=False)
+ data = {}
+
+ ch_data = data.get("zh-cn")
+ if isinstance(ch_data, dict):
+ ch_dict = cast(dict[str, Any], ch_data)
+ main_value = ch_dict.get("main")
+ main_ch_data = cast(dict[str, Any], main_value) if isinstance(main_value, dict) else {}
+
+ if not main_ch_data:
+ localized_main = await self.tmdb_manager.get_tmdb_localized_data(meta, data_type="main", language="zh-cn", append_to_response="credits")
+ main_ch_data = localized_main or {}
+
+ self.tmdb_data = main_ch_data
+
+ return
+
+ async def search_existing(self, meta: Meta, _) -> list[dict[str, str]]:
+ if not self.announce_url:
+ console.print(f"[red]Announce URL is not set for {self.tracker}[/red]", markup=True)
+ meta["skipping"] = self.tracker
+ return []
+
+ base_url = f"{self.base_url}/torrents.php"
+ params = {
+ f"cat{self.get_category(meta)}": "1",
+ f"medium{self.get_type(meta)}": "1",
+ f"standard{self.get_resolution(meta)}": "1",
+ "incldead": "0",
+ }
+
+ search_name = str(meta.get("title", ""))
+ year = str(meta.get("year", ""))
+ episode = str(meta.get("episode", ""))
+ season = str(meta.get("season", ""))
+ season_episode = f"{season}{episode}" if season or episode else ""
+
+ if meta["category"] == "MOVIE":
+ params["search"] = f"{search_name} {year}"
+ else:
+ if meta.get("tv_pack", False):
+ params["search"] = f"{search_name} {season}"
+ else:
+ params["search"] = f"{search_name} {season_episode}"
+
+ try:
+ cookies = await self.cookie_validator.load_session_cookies(meta, self.tracker)
+ if cookies:
+ self.session.cookies.update(cookies)
+
+ response = await self.session.get(base_url, params=params)
+ response.raise_for_status()
+
+ soup = BeautifulSoup(response.text, "html.parser")
+ table = soup.find("table", class_="torrents")
+ if not table:
+ return []
+
+ rows = table.find_all("tr")[1:] # Skip header row
+ results = []
+ for row in rows:
+ name_link = row.find("table", class_="torrentname")
+ if not name_link:
+ continue
+
+ a_tag = name_link.find("a", href=lambda x: bool(x and "details.php?id=" in x))
+ if a_tag:
+ name_val = a_tag.get("title")
+ name = " ".join(name_val) if isinstance(name_val, list) else name_val or a_tag.get_text(strip=True)
+
+ href = a_tag.get("href")
+ if isinstance(href, list):
+ href = href[0]
+
+ if isinstance(href, str):
+ torrent_id = href.split("id=")[1].split("&")[0]
+ link = f"{self.base_url}/details.php?id={torrent_id}"
+ base_entry = {"name": name, "link": link}
+
+ if meta.get("is_disc") == "BDMV":
+ bdinfo = await self.get_dupe_bdinfo(torrent_id)
+ if bdinfo:
+ base_entry["bd_info"] = bdinfo
+
+ results.append(base_entry)
+
+ return results
+ except Exception as e:
+ console.print(f"[red]Error searching {self.tracker}: {e}[/red]", markup=True)
+ return []
+
+ async def get_dupe_bdinfo(self, torrent_id: str) -> str:
+ try:
+ bdinfo_url = f"{self.base_url}/details.php?id={torrent_id}"
+ response = await self.session.get(bdinfo_url)
+ response.raise_for_status()
+
+ soup = BeautifulSoup(response.text, "html.parser")
+ bdinfo_div = soup.find("div", class_="nexus-media-info-raw")
+ if bdinfo_div:
+ pre_tag = bdinfo_div.find("pre")
+ if pre_tag:
+ return pre_tag.get_text(strip=True)
+
+ return ""
+
+ except Exception as e:
+ console.print(f"Error getting BDInfo for torrent {torrent_id}: {e}", markup=False)
+ return ""
+
+ async def validate_credentials(self, meta: Meta) -> bool:
+ return await self.cookie_validator.cookie_validation(meta=meta, tracker=self.tracker, test_url=f"{self.base_url}/upload.php", success_text="/announce.php")
+
+ async def standard_desc(self, meta: Meta) -> str:
+ data = getattr(self, "tmdb_data", {})
+ if not data:
+ return ""
+
+ desc_parts: list[str] = []
+
+ # Poster
+ poster_path = data.get("poster_path")
+ if poster_path:
+ desc_parts.append(f"[img]https://image.tmdb.org/t/p/w500{poster_path}[/img]")
+ desc_parts.append("")
+
+ name = data.get("name", "")
+ original_name = data.get("original_name", "")
+
+ # Season info for TV
+ season_num = meta.get("season", 0)
+ is_tv = meta.get("category") == "TV"
+
+ if is_tv and season_num:
+ season_info = next((s for s in data.get("seasons", []) if s.get("season_number") == season_num), {})
+ season_name = season_info.get("name", "")
+ if not season_name or season_name == f"第 {season_num} 季":
+ if f"第 {season_num} 季" not in name:
+ name = f"{name} 第 {season_num} 季"
+ else:
+ if season_name not in name:
+ name = f"{name} {season_name}"
+
+ desc_parts.append(f"◎片 名 {name}")
+
+ aka = []
+ if original_name and original_name != name:
+ aka.append(original_name)
+ if aka:
+ desc_parts.append(f"◎译 名 {' / '.join(aka)}")
+
+ release_date = data.get("first_air_date") or data.get("release_date", "")
+ year = release_date[:4] if release_date else str(meta.get("year", ""))
+ if year:
+ desc_parts.append(f"◎年 代 {year}")
+
+ countries = [c.get("name") for c in data.get("production_countries", [])]
+ if countries:
+ desc_parts.append(f"◎产 地 {' / '.join(countries)}")
+
+ genres = [g.get("name") for g in data.get("genres", [])]
+ if genres:
+ desc_parts.append(f"◎类 别 {' / '.join(genres)}")
+
+ languages = [l.get("name") for l in data.get("spoken_languages", [])]
+ if languages:
+ desc_parts.append(f"◎语 言 {' / '.join(languages)}")
+
+ if release_date:
+ country_name = countries[0] if countries else ""
+ date_str = f"{release_date}({country_name})" if country_name else release_date
+ desc_parts.append(f"◎上映日期 {date_str}")
+
+ imdb_info = meta.get("imdb_info", {})
+ imdb_rating = imdb_info.get("rating")
+ imdb_votes = imdb_info.get("votes")
+ imdb_url = imdb_info.get("imdb_url")
+
+ if imdb_rating:
+ votes_str = f" ({imdb_votes} 人评价)" if imdb_votes else ""
+ desc_parts.append(f"◎IMDb评分 {imdb_rating}/10{votes_str}")
+ if imdb_url:
+ desc_parts.append(f"◎IMDb链接 {imdb_url}/")
+
+ # Douban info from meta if available
+ douban_rating = meta.get("douban_rating")
+ douban_votes = meta.get("douban_votes")
+ douban_id = meta.get("douban_id")
+ if douban_rating:
+ votes_str = f" ({douban_votes} 人评价)" if douban_votes else ""
+ desc_parts.append(f"◎豆瓣评分 {douban_rating}/10{votes_str}")
+ if douban_id:
+ desc_parts.append(f"◎豆瓣链接 https://movie.douban.com/subject/{douban_id}/")
+
+ if is_tv:
+ if season_num:
+ season_info = next((s for s in data.get("seasons", []) if s.get("season_number") == season_num), {})
+ ep_count = season_info.get("episode_count")
+ if ep_count:
+ desc_parts.append(f"◎集 数 {ep_count}")
+
+ desc_parts.append(f"◎季 数 {season_num}")
+
+ runtime = data.get("episode_run_time", [])
+ if not runtime and data.get("last_episode_to_air"):
+ runtime = [data["last_episode_to_air"].get("runtime")]
+ if runtime and runtime[0]:
+ desc_parts.append(f"◎片 长 {runtime[0]}分钟")
+ else:
+ runtime = data.get("runtime") or meta.get("runtime")
+ if runtime:
+ desc_parts.append(f"◎片 长 {runtime}分钟")
+
+ credits = data.get("credits", {})
+ crew = credits.get("crew", [])
+
+ directors = [f"{c.get('name')} {c.get('original_name')}" for c in crew if c.get("job") == "Director"]
+ if directors:
+ desc_parts.append(f"◎导 演 {' / '.join(directors)}")
+
+ writers = [f"{c.get('name')} {c.get('original_name')}" for c in crew if c.get("job") in ("Writer", "Screenplay", "Author")]
+ writers = list(dict.fromkeys(writers))
+ if writers:
+ desc_parts.append(f"◎编 剧 {' / '.join(writers)}")
+
+ cast = credits.get("cast", [])
+ if cast:
+ first_actor = cast[0]
+ actor_str = f"{first_actor.get('name')} {first_actor.get('original_name')}"
+ if first_actor.get("character"):
+ actor_str += f" (饰 {first_actor.get('character')})"
+ desc_parts.append(f"◎主 演 {actor_str}")
+
+ for actor in cast[1:25]:
+ actor_str = f"{actor.get('name')} {actor.get('original_name')}"
+ if actor.get("character"):
+ actor_str += f" (饰 {actor.get('character')})"
+ desc_parts.append(f" {actor_str}")
+
+ overview = data.get("overview", "")
+ if overview:
+ desc_parts.append("")
+ desc_parts.append("◎简 介")
+ desc_parts.append("")
+ desc_parts.append(f" {overview}")
+
+ return "\n".join(desc_parts)
+
+ async def get_description(self, meta: Meta) -> str:
+ builder = DescriptionBuilder(self.tracker, self.config)
+ desc_parts: list[str] = []
+
+ # Custom Header
+ custom_header = await builder.get_custom_header()
+ desc_parts.append(custom_header)
+
+ # Standard Description
+ desc_parts.append(await self.standard_desc(meta))
+
+ # User description
+ user_description = await builder.get_user_description(meta)
+ desc_parts.append(user_description)
+
+ # Disc menus screenshots header
+ menu_images_value = meta.get("menu_images", [])
+ menu_images: list[dict[str, Any]] = []
+ if isinstance(menu_images_value, list):
+ menu_images_list = list[Any], menu_images_value
+ menu_images.extend([cast(dict[str, Any], item) for item in menu_images_list if isinstance(item, dict)])
+ if menu_images:
+ desc_parts.append(await builder.menu_screenshot_header(meta))
+
+ # Disc menus screenshots
+ menu_screenshots_block = ""
+ for image in menu_images:
+ menu_raw_url = image.get("raw_url")
+ if menu_raw_url:
+ menu_screenshots_block += f"[img]{menu_raw_url}[/img]"
+ if menu_screenshots_block:
+ desc_parts.append(menu_screenshots_block)
+
+ # Tonemapped Header
+ desc_parts.append(await builder.get_tonemapped_header(meta))
+
+ # Screenshot Header
+ images_value = meta.get("image_list", [])
+ images: list[dict[str, Any]] = []
+ if isinstance(images_value, list):
+ images_list = list[Any], images_value
+ images.extend([cast(dict[str, Any], item) for item in images_list if isinstance(item, dict)])
+ if images:
+ desc_parts.append(await builder.screenshot_header())
+
+ # Screenshots
+ if images:
+ screenshots_block = ""
+ for image in images:
+ raw_url = image.get("raw_url")
+ if raw_url:
+ screenshots_block += f"[img]{raw_url}[/img]"
+ if screenshots_block:
+ desc_parts.append(screenshots_block)
+
+ # Signature
+ desc_parts.append(f"[right][url=https://github.com/Audionut/Upload-Assistant][size=1]{meta['ua_signature']}[/size][/url][/right]")
+
+ description = "\n\n".join(part for part in desc_parts if part.strip())
+
+ from src.bbcode import BBCODE
+
+ bbcode = BBCODE()
+ description = description.strip()
+ description = bbcode.remove_extra_lines(description)
+
+ async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", "w", encoding="utf-8") as description_file:
+ await description_file.write(description)
+
+ return description
+
+ def get_category(self, _meta: Meta) -> int:
+ raise NotImplementedError
+
+ def get_type(self, _meta: Meta) -> int:
+ raise NotImplementedError
+
+ def get_codec(self, _meta: Meta) -> int:
+ raise NotImplementedError
+
+ def get_resolution(self, _meta: Meta) -> int:
+ raise NotImplementedError
+
+ def get_group_tag(self, _meta: Meta) -> int:
+ return 0
+
+ def get_checkboxes(self, _meta: Meta) -> list[str]:
+ return []
+
+ def get_anonymous(self, _meta: Meta) -> bool:
+ return False
+
+ def get_audio_codec(self, _meta: Meta) -> int:
+ return 0
+
+ def get_douban_url(self, meta: Meta) -> str:
+ if meta.get("douban_id", 0):
+ return f"https://movie.douban.com/subject/{meta['douban_id']}/"
+ return ""
+
+ def get_imdb_url(self, meta: Meta) -> str:
+ if meta.get("imdb_id", 0):
+ return f"{meta.get('imdb_info', {}).get('imdb_url', '')}"
+ return ""
+
+ def get_region(self, _meta: Meta) -> int:
+ return 0
+
+ def get_container(self, _meta: Meta) -> int:
+ return 0
+
+ async def get_data(self, meta: Meta):
+ await self.load_localized_data(meta)
+ builder = DescriptionBuilder(self.tracker, self.config)
+ data = {
+ "codec_sel[4]": self.get_codec(meta),
+ "color": 0,
+ "descr": await self.get_description(meta),
+ "font": 0,
+ "medium_sel[4]": self.get_type(meta),
+ "name": meta.get("name"),
+ "size": 0,
+ "small_descr": self.common.get_small_description(meta),
+ "standard_sel[4]": self.get_resolution(meta),
+ "technical_info": await builder.get_mediainfo_section(meta) if meta.get("is_disc", "") != "BDMV" else await builder.get_bdinfo_section(meta),
+ "type": self.get_category(meta),
+ }
+
+ group_tag = self.get_group_tag(meta)
+ if group_tag:
+ data["team_sel[4]"] = group_tag
+
+ checkboxes = self.get_checkboxes(meta)
+ if checkboxes:
+ data["tags[4][]"] = checkboxes
+
+ anonymous = self.get_anonymous(meta)
+ if anonymous:
+ data["uplver"] = "yes"
+
+ imdb_url = self.get_imdb_url(meta)
+ if imdb_url:
+ data["url"] = imdb_url
+
+ douban_url = self.get_douban_url(meta)
+ if douban_url:
+ data["pt_gen"] = douban_url
+
+ audio = self.get_audio_codec(meta)
+ if audio:
+ data["audiocodec_sel[4]"] = audio
+
+ region = self.get_region(meta)
+ if region:
+ data["source_sel[4]"] = region
+
+ container = self.get_container(meta)
+ if container:
+ data["processing_sel[4]"] = container
+
+ return data
+
+ async def upload(self, meta: Meta, _) -> bool:
+ cookies = await self.cookie_validator.load_session_cookies(meta, self.tracker)
+ self.session.cookies.clear()
+ if cookies is not None:
+ self.session.cookies.update(cookies)
+ data = await self.get_data(meta)
+
+ is_uploaded = await self.cookie_auth_uploader.handle_upload(
+ meta=meta,
+ tracker=self.tracker,
+ source_flag=self.source_flag,
+ torrent_url=self.torrent_url,
+ id_pattern=r"download\.php\?id=(\d+)",
+ data=data,
+ torrent_field_name="file",
+ upload_cookies=self.session.cookies,
+ upload_url=f"{self.base_url}/takeupload.php",
+ success_text="download.php?id=",
+ )
+
+ return is_uploaded
diff --git a/src/trackers/PTCAFE.py b/src/trackers/PTCAFE.py
new file mode 100644
index 000000000..7f8e38b3d
--- /dev/null
+++ b/src/trackers/PTCAFE.py
@@ -0,0 +1,310 @@
+# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
+import re
+from typing import Any
+
+from src.trackers.NEXUSPHP import NEXUSPHP
+
+Meta = dict[str, Any]
+Config = dict[str, Any]
+
+
+class PTCAFE(NEXUSPHP):
+ def __init__(self, config: Config) -> None:
+ super().__init__(config, "PTCAFE")
+ self.banned_groups = []
+ self.base_url = "https://ptcafe.club"
+ self.source_flag = "[ptcafe.club] 咖啡"
+ self.torrent_url = f"{self.base_url}/details.php?id="
+
+ def get_category(self, meta: Meta) -> int:
+ animations = 405
+ documentaries = 404
+ movies = 401
+ tv_series = 402
+ tv_shows = 403
+
+ category = str(meta.get("category", "")).upper()
+ genres = str(meta.get("genres", "")).lower()
+ keywords = str(meta.get("keywords", "")).lower()
+
+ if "documentary" in genres or "documentary" in keywords:
+ return documentaries
+ if meta.get("anime") or "animation" in genres or "animation" in keywords:
+ return animations
+
+ if category == "MOVIE":
+ return movies
+ if category == "TV":
+ game_show_keywords = [
+ "award show",
+ "competition",
+ "game show",
+ "music show",
+ "performance",
+ "reality television",
+ "reality tv",
+ "reality",
+ "stand-up",
+ "talk show",
+ "tv show",
+ "variety",
+ ]
+ if any(re.search(rf"(^|,\s*){re.escape(keyword)}(\s*,|$)", genres, re.IGNORECASE) for keyword in game_show_keywords):
+ return tv_shows
+ else:
+ return tv_series
+
+ return movies
+
+ def get_region(self, meta: Meta) -> int:
+ america = [
+ 'AG', 'AI', 'AR', 'AW', 'BB', 'BL', 'BM', 'BO', 'BQ', 'BR', 'BS', 'BV', 'BZ', 'CA', 'CL',
+ 'CO', 'CR', 'CU', 'CW', 'DM', 'DO', 'EC', 'FK', 'GD', 'GF', 'GL', 'GP', 'GS', 'GT', 'GY',
+ 'HN', 'HT', 'JM', 'KN', 'KY', 'LC', 'MF', 'MQ', 'MS', 'MX', 'NI', 'PA', 'PE', 'PM', 'PR',
+ 'PY', 'SR', 'SV', 'SX', 'TC', 'TT', 'US', 'UY', 'VC', 'VE', 'VG', 'VI'
+ ] # fmt: off
+
+ europe = [
+ 'AD', 'AL', 'AT', 'AX', 'BA', 'BE', 'BG', 'BY', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI',
+ 'FO', 'FR', 'GB', 'GG', 'GI', 'GR', 'HR', 'HU', 'IE', 'IM', 'IS', 'IT', 'JE', 'LI', 'LT',
+ 'LU', 'LV', 'MC', 'MD', 'ME', 'MK', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'RU', 'SE',
+ 'SI', 'SJ', 'SK', 'SM', 'SU', 'UA', 'VA', 'XC'
+ ] # fmt: off
+
+ country = meta.get("origin_country", [])[0].upper()
+ if country in america or country in europe:
+ return 3
+ if country == "CN":
+ return 1
+ if country == "TW" or country == "HK":
+ return 2
+ if country == "JP":
+ return 4
+ if country == "KR":
+ return 5
+ if country == "IN":
+ return 6
+
+ return 7
+
+ def get_type(self, meta: Meta) -> int:
+ blu_ray = 4
+ blu_ray_diy = 5
+ dvd = 10
+ encode = 7
+ remux = 6
+ tv = 9
+ uhd_diy = 2
+ uhd_master_disc = 1
+ uhd_remux = 3
+ web_dl = 8
+
+ is_disc = str(meta.get("is_disc", "")).lower()
+ is_diy = meta.get("diy_disc", False)
+ mtype = str(meta.get("type", "")).lower()
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if is_disc == "bdmv":
+ if resolution == "2160p":
+ if is_diy:
+ return uhd_diy
+ return uhd_master_disc
+ else:
+ if is_diy:
+ return blu_ray_diy
+ return blu_ray
+
+ if "dvd" in is_disc:
+ return dvd
+
+ if mtype == "remux":
+ if resolution == "2160p":
+ return uhd_remux
+ return remux
+
+ if mtype in ("webdl", "webrip"):
+ return web_dl
+
+ if "tv" in mtype:
+ return tv
+
+ if mtype == "encode":
+ return encode
+
+ return encode
+
+ def get_codec(self, meta: Meta) -> int:
+ divx = 10
+ h264 = 2
+ h265 = 1
+ mpeg2 = 6
+ mpeg4 = 7
+ other = 11
+ vc1 = 5
+ vp9 = 9
+ x264 = 4
+ x265 = 3
+ xvid = 8
+
+ codec = str(meta.get("video_codec", "")).lower()
+
+ if "h265" in codec or "x265" in codec or "hevc" in codec:
+ return h265
+ if "h264" in codec or "x264" in codec or "avc" in codec:
+ return h264
+ if "vc1" in codec or "vc-1" in codec:
+ return vc1
+ if "mpeg2" in codec or "mpeg-2" in codec:
+ return mpeg2
+ if "mpeg4" in codec or "mpeg-4" in codec:
+ return mpeg4
+ if "xvid" in codec:
+ return xvid
+ if "vp9" in codec:
+ return vp9
+ if "divx" in codec:
+ return divx
+ if "x265" in codec:
+ return x265
+ if "x264" in codec:
+ return x264
+
+ return other
+
+ def get_resolution(self, meta: Meta) -> int:
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if "1080" in resolution:
+ return 3
+ if "720" in resolution:
+ return 4
+ if meta.get("sd", False):
+ return 5
+ if "2160" in resolution:
+ return 2
+ if "4320" in resolution:
+ return 1
+
+ return 6
+
+ def get_audio_codec(self, meta: Meta) -> int:
+ audio_codec = str(meta.get("audio", "")).lower()
+
+ if "dts:x 7.1" in audio_codec:
+ return 1
+ if "hd ma" in audio_codec:
+ return 2
+ if "hd hr" in audio_codec:
+ return 3
+ if "dts-hd" in audio_codec:
+ return 4
+ if "dts:x" in audio_codec:
+ return 5
+ if "lpcm" in audio_codec:
+ return 6
+ if "dd" in audio_codec:
+ return 7
+ if "atmos" in audio_codec:
+ return 8
+ if "aac" in audio_codec:
+ return 9
+ if "true" in audio_codec:
+ return 10
+ if "dts" in audio_codec:
+ return 11
+ if "flac" in audio_codec:
+ return 12
+ if "ape" in audio_codec:
+ return 13
+ if "mp3" in audio_codec:
+ return 14
+ if "wav" in audio_codec:
+ return 15
+ if "opus" in audio_codec:
+ return 16
+ if "ogg" in audio_codec:
+ return 17
+
+ return 18
+
+ def get_group_tag(self, meta: Meta) -> int:
+ group_tag = {
+ "-ade": 1,
+ "-adweb": 2,
+ "-audies": 3,
+ "-beast": 4,
+ "-beitai": 5,
+ "-beyondhd": 6,
+ "-btstv": 7,
+ "-cafetv": 8,
+ "-cafeweb": 9,
+ "-chd": 10,
+ "-chdweb": 11,
+ "-cmct": 12,
+ "-djweb": 13,
+ "-frds": 14,
+ "-hdctv": 15,
+ "-hdh": 16,
+ "-hdhome": 17,
+ "-hdsky": 18,
+ "-hdsweb": 19,
+ "-hhweb": 20,
+ "-mteam": 21,
+ "-mweb": 22,
+ "-ourbits": 23,
+ "-ourtv": 24,
+ "-ptcafe": 25,
+ "-pterweb": 26,
+ "-qhstudio": 27,
+ "-ttg": 28,
+ "-wiki": 29,
+ }
+
+ group = str(meta.get("tag", "")).lower()
+ return group_tag.get(group, 30)
+
+ def get_checkboxes(self, meta: Meta) -> list[str]:
+ cantonese_audio = 8
+ chinese_audio = 7
+ chinese_subtitle = 9
+ diy = 13
+ dv = 11
+ hdr = 12
+ reposting_prohibited = 5
+
+ audio_tracks = meta.get("audio_languages", [])
+ mhdr = meta.get("hdr", "")
+ subtitle_tracks = meta.get("subtitle_languages", [])
+
+ checkboxes = []
+
+ if meta.get("exclusive", False):
+ checkboxes.append(str(reposting_prohibited))
+
+ if "Chinese" in audio_tracks or "Mandarin" in audio_tracks:
+ checkboxes.append(str(chinese_audio))
+
+ if "Cantonese" in audio_tracks:
+ checkboxes.append(str(cantonese_audio))
+
+ if "Chinese" in subtitle_tracks or "Mandarin" in subtitle_tracks:
+ checkboxes.append(str(chinese_subtitle))
+
+ if "HDR" in mhdr.upper():
+ checkboxes.append(str(hdr))
+
+ if "DV" in mhdr.upper():
+ checkboxes.append(str(dv))
+
+ if meta.get("diy_disc", False):
+ checkboxes.append(str(diy))
+
+ return checkboxes
+
+ def get_douban_url(self, meta: Meta) -> str:
+ _ = meta
+ return ""
+
+ def get_imdb_url(self, meta: Meta) -> str:
+ _ = meta
+ return ""
diff --git a/src/trackers/PTFANS.py b/src/trackers/PTFANS.py
new file mode 100644
index 000000000..67a0bf634
--- /dev/null
+++ b/src/trackers/PTFANS.py
@@ -0,0 +1,147 @@
+# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
+import re
+from typing import Any
+
+from src.trackers.NEXUSPHP import NEXUSPHP
+
+Meta = dict[str, Any]
+Config = dict[str, Any]
+
+
+class PTFANS(NEXUSPHP):
+ def __init__(self, config: Config) -> None:
+ super().__init__(config, "PTFANS")
+ self.banned_groups = []
+ self.base_url = "https://ptfans.cc"
+ self.source_flag = "[ptfans.cc] PTFans"
+ self.torrent_url = f"{self.base_url}/details.php?id="
+
+ def get_category(self, meta: Meta) -> int:
+ animations = 414
+ documentaries = 406
+ movies = 401
+ tv_series = 404
+ tv_shows = 405
+
+ category = str(meta.get("category", "")).upper()
+ genres = str(meta.get("genres", "")).lower()
+ keywords = str(meta.get("keywords", "")).lower()
+
+ if "documentary" in genres or "documentary" in keywords:
+ return documentaries
+ if meta.get("anime") or "animation" in genres or "animation" in keywords:
+ return animations
+
+ if category == "MOVIE":
+ return movies
+ if category == "TV":
+ game_show_keywords = [
+ "award show",
+ "competition",
+ "game show",
+ "music show",
+ "performance",
+ "reality television",
+ "reality tv",
+ "reality",
+ "stand-up",
+ "talk show",
+ "tv show",
+ "variety",
+ ]
+ if any(re.search(rf"(^|,\s*){re.escape(keyword)}(\s*,|$)", genres, re.IGNORECASE) for keyword in game_show_keywords):
+ return tv_shows
+ else:
+ return tv_series
+
+ return movies
+
+ def get_type(self, meta: Meta) -> int:
+ blu_ray = 6
+ dvd = 2
+ encode = 8
+ other = 9
+ remux = 3
+ web_dl = 5
+
+ is_disc = str(meta.get("is_disc", "")).lower()
+ mtype = str(meta.get("type", "")).lower()
+
+ if is_disc == "bdmv":
+ return blu_ray
+ if "dvd" in is_disc:
+ return dvd
+
+ if mtype == "remux":
+ return remux
+ if mtype in ("webdl", "webrip"):
+ return web_dl
+ if mtype == "encode":
+ return encode
+
+ return other
+
+ def get_codec(self, meta: Meta) -> int:
+ av1 = 8
+ bluray_avc = 4
+ bluray_hevc = 5
+ bluray_vc1 = 3
+ mpeg2 = 6
+ not_bluray_h264 = 1
+ not_bluray_h265 = 2
+ other = 9
+ xvid = 7
+
+ codec = str(meta.get("video_codec", "")).lower()
+ source = meta.get("source", "").lower()
+ is_bluray_source = "bluray" in source or "blu-ray" in source
+
+ if "av1" in codec:
+ return av1
+ if "h265" in codec or "x265" in codec or "hevc" in codec:
+ if is_bluray_source:
+ return bluray_hevc
+ return not_bluray_h265
+ if "h264" in codec or "x264" in codec or "avc" in codec:
+ if is_bluray_source:
+ return bluray_avc
+ return not_bluray_h264
+ if "mpeg2" in codec or "mpeg-2" in codec:
+ return mpeg2
+ if "vc1" in codec or "vc-1" in codec:
+ if is_bluray_source:
+ return bluray_vc1
+ return other
+ if "xvid" in codec:
+ return xvid
+
+ return other
+
+ def get_resolution(self, meta: Meta) -> int:
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if resolution == "1080p":
+ return 1
+ if resolution == "1080i":
+ return 2
+ if resolution == "720p":
+ return 3
+ if meta.get("sd", False):
+ return 4
+ if resolution == "2160p":
+ return 5
+ if resolution == "4320p":
+ return 6
+
+ return 1
+
+ def get_group_tag(self, meta: Meta) -> int:
+ group_tag = {
+ "-chd": 2,
+ "-hds": 1,
+ "-mysilu": 3,
+ "-wiki": 4,
+ }
+
+ group = str(meta.get("tag", "")).lower()
+ return group_tag.get(group, 5)
diff --git a/src/trackers/PTGTK.py b/src/trackers/PTGTK.py
new file mode 100644
index 000000000..ecb113fb0
--- /dev/null
+++ b/src/trackers/PTGTK.py
@@ -0,0 +1,185 @@
+# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
+import re
+from typing import Any
+
+from src.trackers.NEXUSPHP import NEXUSPHP
+
+Meta = dict[str, Any]
+Config = dict[str, Any]
+
+
+class PTGTK(NEXUSPHP):
+ def __init__(self, config: Config) -> None:
+ super().__init__(config, "PTGTK")
+ self.banned_groups = []
+ self.base_url = "https://pt.gtkpw.xyz"
+ self.source_flag = "[pt.gtkpw.xyz] PT GTK"
+ self.torrent_url = f"{self.base_url}/details.php?id="
+
+ def get_category(self, meta: Meta) -> int:
+ animations = 405
+ documentaries = 404
+ movies = 401
+ tv_series = 402
+ tv_shows = 403
+
+ category = str(meta.get("category", "")).upper()
+ genres = str(meta.get("genres", "")).lower()
+ keywords = str(meta.get("keywords", "")).lower()
+
+ if "documentary" in genres or "documentary" in keywords:
+ return documentaries
+ if meta.get("anime") or "animation" in genres or "animation" in keywords:
+ return animations
+
+ if category == "MOVIE":
+ return movies
+ if category == "TV":
+ game_show_keywords = [
+ "award show",
+ "competition",
+ "game show",
+ "music show",
+ "performance",
+ "reality television",
+ "reality tv",
+ "reality",
+ "stand-up",
+ "talk show",
+ "tv show",
+ "variety",
+ ]
+ if any(re.search(rf"(^|,\s*){re.escape(keyword)}(\s*,|$)", genres, re.IGNORECASE) for keyword in game_show_keywords):
+ return tv_shows
+ else:
+ return tv_series
+
+ return movies
+
+ def get_type(self, meta: Meta) -> int:
+ blu_ray = 1
+ dvdr = 6
+ encode = 7
+ hd_dvd = 2
+ hdtv = 5
+ remux = 3
+ uhd = 10
+ web_dl = 11
+
+ is_disc = str(meta.get("is_disc", "")).lower()
+ mtype = str(meta.get("type", "")).lower()
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if is_disc == "bdmv":
+ if resolution == "2160p":
+ return uhd
+ return blu_ray
+ if is_disc == "dvd":
+ return dvdr
+ if is_disc == "hddvd":
+ return hd_dvd
+
+ if mtype == "remux":
+ return remux
+ if mtype in ("webdl", "webrip"):
+ return web_dl
+ if mtype == "hdtv":
+ return hdtv
+ if mtype == "encode":
+ return encode
+
+ return encode
+
+ def get_codec(self, meta: Meta) -> int:
+ av1 = 7
+ h264 = 1
+ h265 = 6
+ mpeg2 = 4
+ other = 5
+ vc1 = 2
+ vp9 = 8
+ xvid = 3
+
+ codec = str(meta.get("video_codec", "")).lower()
+
+ if "av1" in codec:
+ return av1
+ if "h265" in codec or "x265" in codec or "hevc" in codec:
+ return h265
+ if "h264" in codec or "x264" in codec or "avc" in codec:
+ return h264
+ if "mpeg2" in codec or "mpeg-2" in codec:
+ return mpeg2
+ if "vc1" in codec or "vc-1" in codec:
+ return vc1
+ if "vp9" in codec:
+ return vp9
+ if "xvid" in codec:
+ return xvid
+
+ return other
+
+ def get_resolution(self, meta: Meta) -> int:
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if resolution == "1080p":
+ return 1
+ if resolution == "1080i":
+ return 2
+ if resolution == "720p":
+ return 3
+ if meta.get("sd", False):
+ return 4
+ if resolution == "2160p":
+ return 5
+ if resolution == "4320p":
+ return 6
+
+ return 1
+
+ def get_group_tag(self, meta: Meta) -> int:
+ group_tag = {
+ "-beast": 11,
+ "-chd": 2,
+ "-cmct": 6,
+ "-frds": 9,
+ "-hds": 1,
+ "-mark": 7,
+ "-mteam": 8,
+ "-mysilu": 3,
+ "-pthome": 10,
+ "-wiki": 4,
+ }
+
+ group = str(meta.get("tag", "")).lower()
+ return group_tag.get(group, 5)
+
+ def get_checkboxes(self, meta: Meta) -> list[str]:
+ chinese_audio = 5
+ chinese_subtitle = 6
+ hdr = 7
+ reposting_prohibited = 1
+
+ audio_tracks = meta.get("audio_languages", [])
+ mhdr = meta.get("hdr", "")
+ subtitle_tracks = meta.get("subtitle_languages", [])
+
+ checkboxes = []
+
+ if meta.get("exclusive", False):
+ checkboxes.append(str(reposting_prohibited))
+
+ if "Chinese" in audio_tracks or "Mandarin" in audio_tracks:
+ checkboxes.append(str(chinese_audio))
+
+ if "Chinese" in subtitle_tracks or "Mandarin" in subtitle_tracks:
+ checkboxes.append(str(chinese_subtitle))
+
+ if "HDR" in mhdr.upper():
+ checkboxes.append(str(hdr))
+
+ return checkboxes
+
+ def get_anonymous(self, meta: Meta) -> bool:
+ anon = not (meta["anon"] == 0 and not self.config["TRACKERS"][self.tracker].get("anon", False))
+ return anon
diff --git a/src/trackers/RPT.py b/src/trackers/RPT.py
new file mode 100644
index 000000000..68eceabec
--- /dev/null
+++ b/src/trackers/RPT.py
@@ -0,0 +1,179 @@
+# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
+import re
+from typing import Any
+
+from src.trackers.NEXUSPHP import NEXUSPHP
+
+Meta = dict[str, Any]
+Config = dict[str, Any]
+
+
+class RPT(NEXUSPHP):
+ def __init__(self, config: Config) -> None:
+ super().__init__(config, "RPT")
+ self.banned_groups = []
+ self.base_url = "https://bilibili.download"
+ self.source_flag = "[bilibili.download] RailgunPT"
+ self.torrent_url = f"{self.base_url}/details.php?id="
+
+ def get_category(self, meta: Meta) -> int:
+ animations = 405
+ documentaries = 404
+ movies = 401
+ tv_series = 402
+ tv_shows = 403
+
+ category = str(meta.get("category", "")).upper()
+ genres = str(meta.get("genres", "")).lower()
+ keywords = str(meta.get("keywords", "")).lower()
+
+ if "documentary" in genres or "documentary" in keywords:
+ return documentaries
+ if meta.get("anime") or "animation" in genres or "animation" in keywords:
+ return animations
+
+ if category == "MOVIE":
+ return movies
+ if category == "TV":
+ game_show_keywords = [
+ "award show",
+ "competition",
+ "game show",
+ "music show",
+ "performance",
+ "reality television",
+ "reality tv",
+ "reality",
+ "stand-up",
+ "talk show",
+ "tv show",
+ "variety",
+ ]
+ if any(re.search(rf"(^|,\s*){re.escape(keyword)}(\s*,|$)", genres, re.IGNORECASE) for keyword in game_show_keywords):
+ return tv_shows
+ else:
+ return tv_series
+
+ return movies
+
+ def get_type(self, meta: Meta) -> int:
+ blu_ray = 1
+ dvd = 6
+ encode = 7
+ hdtv = 5
+ remux = 3
+ uhd = 2
+ web_dl = 4
+
+ is_disc = str(meta.get("is_disc", "")).lower()
+ mtype = str(meta.get("type", "")).lower()
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if is_disc == "bdmv":
+ if resolution == "2160p":
+ return uhd
+ return blu_ray
+ if "dvd" in is_disc:
+ return dvd
+
+ if mtype == "remux":
+ return remux
+ if mtype in ("webdl", "webrip"):
+ return web_dl
+ if mtype == "hdtv":
+ return hdtv
+ if mtype == "encode":
+ return encode
+
+ return encode
+
+ def get_codec(self, meta: Meta) -> int:
+ h264 = 1
+ h265 = 2
+ mpeg2 = 4
+ other = 6
+ vc1 = 3
+ xvid = 5
+
+ codec = str(meta.get("video_codec", "")).lower()
+
+ if "h265" in codec or "x265" in codec or "hevc" in codec:
+ return h265
+ if "h264" in codec or "x264" in codec or "avc" in codec:
+ return h264
+ if "mpeg2" in codec or "mpeg-2" in codec:
+ return mpeg2
+ if "vc1" in codec or "vc-1" in codec:
+ return vc1
+ if "xvid" in codec:
+ return xvid
+
+ return other
+
+ def get_resolution(self, meta: Meta) -> int:
+ resolution = str(meta.get("resolution", "")).lower()
+
+ if resolution == "1080p" or resolution == "1080i":
+ return 2
+ if resolution == "720p":
+ return 3
+ if meta.get("sd", False):
+ return 4
+ if resolution == "2160p":
+ return 1
+
+ return 5
+
+ def get_audio_codec(self, meta: Meta) -> int:
+ audio_codec = str(meta.get("audio", "")).lower()
+
+ if "true" in audio_codec or "atmos" in audio_codec:
+ return 1
+ if "dts" in audio_codec:
+ return 2
+ if "dd" in audio_codec:
+ return 3
+ if "lpcm" in audio_codec:
+ return 4
+ if "flac" in audio_codec:
+ return 5
+ if "mp3" in audio_codec:
+ return 6
+ if "aac" in audio_codec:
+ return 7
+ if "ape" in audio_codec:
+ return 8
+ if "wav" in audio_codec:
+ return 10
+
+ return 9
+
+ def get_checkboxes(self, meta: Meta) -> list[str]:
+ chinese_audio = 5
+ chinese_subtitle = 6
+ hdr = 7
+ reposting_prohibited = 1
+
+ audio_tracks = meta.get("audio_languages", [])
+ mhdr = meta.get("hdr", "")
+ subtitle_tracks = meta.get("subtitle_languages", [])
+
+ checkboxes = []
+
+ if meta.get("exclusive", False):
+ checkboxes.append(str(reposting_prohibited))
+
+ if "Chinese" in audio_tracks or "Mandarin" in audio_tracks:
+ checkboxes.append(str(chinese_audio))
+
+ if "Chinese" in subtitle_tracks or "Mandarin" in subtitle_tracks:
+ checkboxes.append(str(chinese_subtitle))
+
+ if "HDR" in mhdr.upper():
+ checkboxes.append(str(hdr))
+
+ return checkboxes
+
+ def get_anonymous(self, meta: Meta) -> bool:
+ anon = not (meta["anon"] == 0 and not self.config["TRACKERS"][self.tracker].get("anon", False))
+ return anon
diff --git a/src/trackersetup.py b/src/trackersetup.py
index ed2d80a8e..e68f93e1f 100644
--- a/src/trackersetup.py
+++ b/src/trackersetup.py
@@ -46,8 +46,10 @@
from src.trackers.IHD import IHD
from src.trackers.IS import IS
from src.trackers.ITT import ITT
+from src.trackers.LAJIDUI import LAJIDUI
from src.trackers.LCD import LCD
from src.trackers.LDU import LDU
+from src.trackers.LPT import LPT
from src.trackers.LST import LST
from src.trackers.LT import LT
from src.trackers.LUME import LUME
@@ -58,13 +60,17 @@
from src.trackers.OTW import OTW
from src.trackers.PHD import PHD
from src.trackers.PT import PT
+from src.trackers.PTCAFE import PTCAFE
from src.trackers.PTER import PTER
+from src.trackers.PTFANS import PTFANS
+from src.trackers.PTGTK import PTGTK
from src.trackers.PTP import PTP
from src.trackers.PTS import PTS
from src.trackers.PTT import PTT
from src.trackers.R4E import R4E
from src.trackers.RAS import RAS
from src.trackers.RF import RF
+from src.trackers.RPT import RPT
from src.trackers.RTF import RTF
from src.trackers.SAM import SAM
from src.trackers.SHRI import SHRI
@@ -1340,8 +1346,8 @@ async def make_trumpable_report(self, meta: Meta, tracker: str) -> bool:
tracker_class_map: dict[str, type[Any]] = {
'A4K': A4K, 'ACM': ACM, 'AITHER': AITHER, 'ANT': ANT, 'AR': AR, 'ASC': ASC, 'AZ': AZ, 'BHD': BHD, 'BHDTV': BHDTV, 'BJS': BJS, 'BLU': BLU, 'BT': BT, 'CBR': CBR,
'CZ': CZ, 'DC': DC, 'DP': DP, 'DT': DT, 'EMUW': EMUW, 'FNP': FNP, 'FF': FF, 'FL': FL, 'FRIKI': FRIKI, 'GPW': GPW, 'HDB': HDB, 'HDS': HDS, 'HDT': HDT, 'HHD': HHD, 'HUNO': HUNO, 'ITT': ITT,
- 'IHD': IHD, 'IS': IS, 'LCD': LCD, 'LDU': LDU, 'LST': LST, 'LT': LT, 'LUME': LUME, 'MTEAM': MTEAM,'MTV': MTV, 'NBL': NBL, 'OE': OE, 'OTW': OTW, 'PHD': PHD, 'PT': PT, 'PTP': PTP, 'PTER': PTER, 'PTS': PTS, 'PTT': PTT,
- 'R4E': R4E, 'RAS': RAS, 'RF': RF, 'RTF': RTF, 'SAM': SAM, 'SHRI': SHRI, 'SN': SN, 'SP': SP, 'SPD': SPD, 'STC': STC, 'THR': THR,
+ 'IHD': IHD, 'IS': IS, 'LAJIDUI': LAJIDUI, 'LCD': LCD, 'LDU': LDU, 'LPT': LPT, 'LST': LST, 'LT': LT, 'LUME': LUME, 'MTEAM': MTEAM,'MTV': MTV, 'NBL': NBL, 'OE': OE, 'OTW': OTW, 'PHD': PHD, 'PTCAFE': PTCAFE, 'PT': PT, 'PTFANS': PTFANS, 'PTGTK': PTGTK, 'PTP': PTP, 'PTER': PTER, 'PTS': PTS, 'PTT': PTT,
+ 'R4E': R4E, 'RAS': RAS, 'RF': RF, 'RPT': RPT,'RTF': RTF, 'SAM': SAM, 'SHRI': SHRI, 'SN': SN, 'SP': SP, 'SPD': SPD, 'STC': STC, 'THR': THR,
'TIK': TIK, 'TL': TL, 'TLZ': TLZ, 'TOS': TOS, 'TVC': TVC, 'TTG': TTG, 'TTR': TTR, 'ULCX': ULCX, 'UTP': UTP, 'YOINK': YOINK, 'YUS': YUS
} # fmt: off
@@ -1355,5 +1361,5 @@ async def make_trumpable_report(self, meta: Meta, tracker: str) -> bool:
} # fmt: off
http_trackers = {
- 'AR', 'ASC', 'AZ', 'BJS', 'BT', 'CZ', 'FF', 'FL', 'HDB', 'HDS', 'HDT', 'IS', 'MTV', 'PHD', 'PTER', 'PTS', 'TTG'
+ 'AR', 'ASC', 'AZ', 'BJS', 'BT', 'CZ', 'FF', 'FL', 'HDB', 'HDS', 'HDT', 'IS', 'LAJIDUI', 'LPT', 'MTV', 'PHD', 'PTCAFE', 'PTER', 'PTFANS', 'PTGTK', 'PTS', 'RPT', 'TTG'
} # fmt: off
diff --git a/src/trackerstatus.py b/src/trackerstatus.py
index 7167ae5a8..463d108de 100644
--- a/src/trackerstatus.py
+++ b/src/trackerstatus.py
@@ -15,6 +15,7 @@
from src.console import console
from src.dupe_checking import DupeChecker
from src.imdb import imdb_manager
+from src.metadata_searching import get_douban_id
from src.torrentcreate import TorrentCreator
from src.trackers.PTP import PTP
from src.trackersetup import TRACKER_SETUP, tracker_class_map
@@ -35,6 +36,8 @@ async def process_all_trackers(self, meta: Meta) -> int:
tracker_setup: Any = TRACKER_SETUP(config=self.config)
helper: Any = UploadHelper(self.config)
dupe_checker = DupeChecker(self.config)
+ if any(tracker in meta["trackers"] for tracker in ["MTEAM", "LAJIDUI", "PTFANS", "PTGTK", "RPT"]):
+ meta["douban_id"] = await get_douban_id(meta)
meta_lock = asyncio.Lock()
for tracker in meta['trackers']:
if 'tracker_status' not in meta:
diff --git a/upload.py b/upload.py
index a6cc273ae..d076fbf7f 100644
--- a/upload.py
+++ b/upload.py
@@ -620,7 +620,33 @@ async def process_meta(meta: Meta, base_dir: str, bot: Any = None) -> None:
trackers = meta['trackers']
audio_prompted = False
- for tracker in ["AITHER", "ASC", "BJS", "BT", "CBR", "DP", "FF", "GPW", "HUNO", "IHD", "LDU", "LT", "OE", "PTS", "SAM", "SHRI", "SPD", "TTR", "TVC", "ULCX"]:
+ for tracker in [
+ "AITHER",
+ "ASC",
+ "BJS",
+ "BT",
+ "CBR",
+ "DP",
+ "FF",
+ "GPW",
+ "HUNO",
+ "IHD",
+ "LAJIDUI",
+ "LDU",
+ "LPT",
+ "LT",
+ "OE",
+ "PTCAFE",
+ "PTGTK",
+ "PTS",
+ "RPT",
+ "SAM",
+ "SHRI",
+ "SPD",
+ "TTR",
+ "TVC",
+ "ULCX",
+ ]:
if tracker in trackers:
if not audio_prompted:
await languages_manager.process_desc_language(meta, tracker=tracker)
From 4879a80f36d1cf04aa30108a0d1db843c2a13cde Mon Sep 17 00:00:00 2001
From: wastaken7 <46319635+wastaken7@users.noreply.github.com>
Date: Fri, 8 May 2026 22:32:06 -0300
Subject: [PATCH 13/13] add announce urls to match_tracker_url
---
data/example-config.py | 2 +-
src/torrent_clients/qbittorrent.py | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/data/example-config.py b/data/example-config.py
index 6b603cf0b..c4ad68468 100644
--- a/data/example-config.py
+++ b/data/example-config.py
@@ -350,7 +350,7 @@
"TRACKERS": {
# Which trackers do you want to upload to?
- # Available tracker: A4K, ACM, AITHER, ANT, AR, ASC, AZ, BHD, BHDTV, BJS, BLU, BT, CBR, CZ, DC, DP, DT, EMUW, FF, FL, FNP, FRIKI, GPW, HDB, HDS, HDT, HHD, HUNO, IHD, IS, ITT, LCD, LDU, LST, LT, LUME, MTEAM, MTV, NBL, OE, OTW, PHD, PT, PTER, PTP, PTS, PTT, R4E, RAS, RF, RTF, SAM, SHRI, SN, SP, SPD, STC, THR, TIK, TL, TLZ, TOS, TTG, TTR, TVC, ULCX, UTP, YOINK, YUS
+ # Available tracker: A4K, ACM, AITHER, ANT, AR, ASC, AZ, BHD, BHDTV, BJS, BLU, BT, CBR, CZ, DC, DP, DT, EMUW, FF, FL, FNP, FRIKI, GPW, HDB, HDS, HDT, HHD, HUNO, IHD, IS, ITT, LAJIDUI, LCD, LDU, LPT, LST, LT, LUME, MTEAM, MTV, NBL, OE, OTW, PHD, PT, PTCAFE, PTER, PTFANS, PTGTK, PTP, PTS, PTT, R4E, RAS, RF, RPT, RTF, SAM, SHRI, SN, SP, SPD, STC, THR, TIK, TL, TLZ, TOS, TTG, TTR, TVC, ULCX, UTP, YOINK, YUS
# Only add the trackers you want to upload to on a regular basis
"default_trackers": "",
diff --git a/src/torrent_clients/qbittorrent.py b/src/torrent_clients/qbittorrent.py
index 011f7bcc9..3c232590d 100644
--- a/src/torrent_clients/qbittorrent.py
+++ b/src/torrent_clients/qbittorrent.py
@@ -1830,8 +1830,10 @@ async def match_tracker_url(tracker_urls: list[str], meta: dict[str, Any]) -> No
"ihd": ["https://infinityhd.net"],
"is": ["https://immortalseed.me"],
"itt": ["https://itatorrents.xyz"],
+ "lajidui": ["https://pt.lajidui.top"],
"lcd": ["locadora.cc"],
"ldu": ["theldu.to"],
+ "lpt": ["https://longpt.org"],
"lst": ["https://lst.gg"],
"lt": ["https://lat-team.com"],
"lume": ["https://luminarr.me"],
@@ -1842,10 +1844,14 @@ async def match_tracker_url(tracker_urls: list[str], meta: dict[str, Any]) -> No
"otw": ["https://oldtoons.world"],
"phd": ["tracker.privatehd"],
"pt": ["https://portugas.org"],
+ "ptcafe": ["https://tracker.ptcafe.club"],
+ "ptfans": ["https://ptfans.cc"],
+ "ptgtk": ["https://t.myaltbox.com"],
"ptp": ["passthepopcorn.me"],
"pts": ["https://tracker.ptskit.com"],
"ras": ["https://rastastugan.org"],
"rf": ["https://reelflix.xyz", "https://reelflix.cc"],
+ "rpt": ["https://bilibili.download"],
"rtf": ["peer.retroflix"],
"sam": ["https://samaritano.cc"],
"sp": ["https://seedpool.org"],