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"![]({raw_url})" + 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'![](\1)', 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'![](\1)'), + (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"![]({poster_url})", + "", + 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"![]({menu_raw_url})" + 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"![]({raw_url})" - 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"![]({raw_url})" + 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'![](\1)', text, flags=re.IGNORECASE) + specific_img_pattern = r"\[url=[^\]]*\]\[img(?:=[^\]]*)?\](.*?)\[/img\]\[/url\]" + text = re.sub(specific_img_pattern, r"![](\1)", 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'![](\1)'), - (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"![](\1)"), + (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"![]({poster_url})", @@ -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"![]({d.get('coverUrl', '')})", + "", + 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"],