From d131c1351ad7b936de01dab0b6f143ab7230b4fe Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Tue, 10 Feb 2026 07:20:17 +0100 Subject: [PATCH 01/30] add C411 tracker --- data/example-config.py | 7 + src/trackers/C411.py | 553 +++++++++++++++++++++++++++++++++++ src/trackers/FRENCH.py | 645 +++++++++++++++++++++++++++++++++++++++++ src/trackersetup.py | 5 +- 4 files changed, 1208 insertions(+), 2 deletions(-) create mode 100644 src/trackers/C411.py create mode 100644 src/trackers/FRENCH.py diff --git a/data/example-config.py b/data/example-config.py index e6e365374..636736953 100644 --- a/data/example-config.py +++ b/data/example-config.py @@ -466,6 +466,13 @@ "announce_url": "https://t.brasiltracker.org//announce", "anon": False, }, + "C411": { + "link_dir_name": "", + "api_key": "", + "announce_url": "https://c411.org/announce/", + "anon": False, + "modq": False, + }, "CBR": { # 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/trackers/C411.py b/src/trackers/C411.py new file mode 100644 index 000000000..d8e5371cd --- /dev/null +++ b/src/trackers/C411.py @@ -0,0 +1,553 @@ +# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0 +# https://github.com/Audionut/Upload-Assistant/tree/master +# #UA 7.0.0 +# -*- coding: utf-8 -*- +# import discord +import json +from typing import Any +import httpx +from src.console import console +from src.trackers.COMMON import COMMON +import aiofiles +import asyncio +import src.trackers.FRENCH as fr +import unidecode +from typing import Any, Callable, Optional, Union, cast +Meta = dict[str, Any] +Config = dict[str, Any] + + +class C411(): + def __init__(self, config: Config) -> None: + self.config: Config = config + self.common = COMMON(config) + self.tracker = 'C411' + self.base_url = 'https://c411.org' + self.id_url = f'{self.base_url}/api/torrents' + self.upload_url = f'{self.base_url}/api/torrents' + # self.requests_url = f'{self.base_url}/api/requests/filter' + # self.search_url = f'{self.base_url}/api/torrents/filter' + self.torrent_url = f'{self.base_url}/api/' + self.banned_groups: list[str] = [] + pass + + # async def get_cat_id(self, meta: Meta) -> str: + # mediatype video + # return '1' + + async def get_subcat_id(self, meta: Meta) -> str: + sub_cat_id = "0" + + if meta['category'] == 'MOVIE': + if meta.get('mal_id'): + sub_cat_id = '1' + else: + sub_cat_id = '6' + + elif meta['category'] == 'TV': + + if meta.get('mal_id'): + sub_cat_id = '2' + else: + sub_cat_id = '7' + + return sub_cat_id + # unknow return type + + async def get_option_tag(self, meta: Meta): + obj1 = "" + obj2 = None + vff = None + vfq = None + eng = None + audio_track = await fr.get_audio_tracks(meta, True) + source = meta.get('source', "") + type = meta.get('type', "").upper() + + for item in audio_track: + if item['Language'] == "fr-ca": + vfq = True + if item['Language'] == "fr-FR": + vff = True + if item['Language'] == "en" or item['Language'] == "en-us" or item['Language'] == "en-gb": + eng = True + + if eng and not vff or vfq: # vo + obj1 = obj1 + "1," + + # VO VOSTFR + if vff and vfq: + obj1 = obj1 + "4," + if vfq: + obj1 = obj1 + "5," + if vff: + obj1 = obj1 + "2" + + # set quality + if meta['is_disc'] == 'BDMV': + if meta['resolution'] == '2160p': + obj2 = 10 # blu 4k full + else: + obj2 = 11 # blu full + elif meta['is_disc'] == 'DVD': + obj2 = 14 # DVD r5 r9 13 - 14 + + elif type == "REMUX" and source in ("BluRay", "HDDVD"): + if meta['resolution'] == '2160p': + obj2 = 10 # blu 4k remux + else: + obj2 = 12 # blu remux + + # source dvd + elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD", "DVD"): + obj2 = 15 + + # source bluray + elif type == "ENCODE" and source in ("BluRay", "HDDVD"): + if meta['resolution'] == '2160p': + obj2 = 17 + elif meta['resolution'] == '1080p': + obj2 = 16 + elif meta['resolution'] == '720p': + obj2 = 18 + # else: + # obj2 = 25) + + elif type == "WEBDL": + if meta['resolution'] == '2160p': + obj2 = 26 + elif meta['resolution'] == '1080p': + obj2 = 25 + elif meta['resolution'] == '720p': + obj2 = 27 + else: + obj2 = 24 + + elif type == "WEBRIP": + if meta['resolution'] == '2160p': + obj2 = 30 + elif meta['resolution'] == '1080p': + obj2 = 29 + elif meta['resolution'] == '720p': + obj2 = 31 + else: + obj2 = 28 + elif type == "HDTV": + if meta['resolution'] == '2160p': + obj2 = 21 + elif meta['resolution'] == '1080p': + obj2 = 20 + elif meta['resolution'] == '720p': + obj2 = 22 + else: + obj2 = 19 + + elif type == "DVDRIP": + obj2 = 15 # DVDRIP + + # 4klight + # hdlight 1080 + # hdlight 720 + # vcd/vhs + options_dict = {} + options_dict[1] = [obj1] + options_dict[2] = [obj2] + # Let's see if it's a tv show + if meta['category'] == 'TV': + # Let's check for season + if meta.get('no_season', False) is False: + season = str(meta.get('season_int', '')) + if season: + options_dict[7] = 120 + int(season) + # Episode + episode = str(meta.get('episode_int', '')) + if episode: + options_dict[6] = 96 + int(episode) + else: + # pas d'épisode, on suppose que c'est une saison complete ? + options_dict[6] = 96 + return json.dumps(options_dict) + + # https://c411.org/wiki/nommage + async def get_name(self, meta: Meta) -> dict[str, str]: + + type = str(meta.get('type', "")).upper() + title, descr = await fr.get_translation_fr(meta) + alt_title = "" + year = str(meta.get('year', "")) + manual_year_value = meta.get('manual_year') + if manual_year_value is not None and int(manual_year_value) > 0: + year = str(manual_year_value) + resolution = str(meta.get('resolution', "")) + if resolution == "OTHER": + resolution = "" + audio = await fr.get_audio_name(meta) + language = await fr.build_audio_string(meta) + extra_audio = await fr.get_extra_french_tag(meta, True) + if extra_audio: + language = language.replace("FRENCH", "") + " " + extra_audio + service = "" + season = str(meta.get('season', "")) + episode = str(meta.get('episode', "")) + part = str(meta.get('part', "")) + repack = str(meta.get('repack', "")) + three_d = str(meta.get('3D', "")) + tag = str(meta.get('tag', "")) + source = str(meta.get('source', "")) + uhd = str(meta.get('uhd', "")) + hdr = str(meta.get('hdr', "")).replace('HDR10+', 'HDR10PLUS') + hybrid = 'Hybrid' if meta.get('webdv', "") else "" + # if meta.get('manual_episode_title'): + # episode_title = str(meta.get('manual_episode_title', "")) + # elif meta.get('daily_episode_title'): + # episode_title = str(meta.get('daily_episode_title', "")) + # else: + # episode_title = "" + video_codec = "" + video_encode = "" + region = "" + dvd_size = "" + if meta.get('is_disc', "") == "BDMV": # Disk + video_codec = str(meta.get('video_codec', "")) + region = str(meta.get('region', "") or "") + elif meta.get('is_disc', "") == "DVD": + region = str(meta.get('region', "") or "") + dvd_size = str(meta.get('dvd_size', "")) + else: + video_codec = str(meta.get('video_codec', "")).replace('H.264', 'H264').replace('H.265', 'H265') + video_encode = str(meta.get('video_encode', "")).replace('H.264', 'H264').replace('H.265', 'H265') + edition = str(meta.get('edition', "")) + if 'hybrid' in edition.upper(): + edition = edition.replace('Hybrid', '').strip() + + if meta['category'] == "TV": + year = meta['year'] if meta['search_year'] != "" else "" + if meta.get('manual_date'): + # Ignore season and year for --daily flagged shows, just use manual date stored in episode_name + season = '' + episode = '' + if meta.get('no_season', False) is True: + season = '' + if meta.get('no_year', False) is True: + year = '' + if meta.get('no_aka', False) is True: + alt_title = '' + + # YAY NAMING FUN + name = "" + if meta['category'] == "MOVIE": # MOVIE SPECIFIC + if type == "DISC": # Disk + if meta['is_disc'] == 'BDMV': + name = f"{title} {year} {three_d} {edition} {hybrid} {repack} {language} {resolution} {uhd} {region} {source} {hdr} {audio} {video_codec}" + elif meta['is_disc'] == 'DVD': + name = f"{title} {year} {repack} {edition} {region} {source} {dvd_size} {audio}" + elif meta['is_disc'] == 'HDDVD': + name = f"{title} {year} {edition} {repack} {language} {resolution} {source} {video_codec} {audio}" + # BluRay/HDDVD Remux + elif type == "REMUX" and source in ("BluRay", "HDDVD"): + name = f"{title} {year} {three_d} {edition} {hybrid} {repack} {language} {resolution} {uhd} {source} REMUX {hdr} {audio} {video_codec}" + # DVD Remux + elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD", "DVD"): + name = f"{title} {year} {edition} {repack} {source} REMUX {audio}" + elif type == "ENCODE": # Encode + name = f"{title} {year} {edition} {hybrid} {repack} {language} {resolution} {uhd} {source} {hdr} {audio} {video_encode}" + elif type == "WEBDL": # WEB-DL + name = f"{title} {year} {edition} {hybrid} {repack} {language} {resolution} {uhd} {service} WEB {hdr} {audio} {video_encode}" + elif type == "WEBRIP": # WEBRip + name = f"{title} {year} {edition} {hybrid} {repack} {language} {resolution} {uhd} {service} WEBRip {hdr} {audio} {video_encode}" + elif type == "HDTV": # HDTV + name = f"{title} {year} {edition} {repack} {language} {resolution} {source} {audio} {video_encode}" + elif type == "DVDRIP": + name = f"{title} {year} {source} {video_encode} DVDRip {audio}" + + elif meta['category'] == "TV": # TV SPECIFIC + if type == "DISC": # Disk + if meta['is_disc'] == 'BDMV': + name = f"{title} {year} {season}{episode} {three_d} {edition} {hybrid} {repack} {language} {resolution} {uhd} {region} {source} {hdr} {audio} {video_codec}" + if meta['is_disc'] == 'DVD': + name = f"{title} {year} {season}{episode}{three_d} {repack} {edition} {region} {source} {dvd_size} {audio}" + elif meta['is_disc'] == 'HDDVD': + name = f"{title} {year} {edition} {repack} {language} {resolution} {source} {video_codec} {audio}" + # BluRay Remux + elif type == "REMUX" and source in ("BluRay", "HDDVD"): + name = f"{title} {year} {season}{episode} {part} {three_d} {edition} {hybrid} {repack} {language} {resolution} {uhd} {source} REMUX {hdr} {audio} {video_codec}" # SOURCE + # DVD Remux + elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD", "DVD"): + # SOURCE + name = f"{title} {year} {season}{episode} {part} {edition} {repack} {source} REMUX {audio}" + elif type == "ENCODE": # Encode + # SOURCE + name = f"{title} {year} {season}{episode} {part} {edition} {hybrid} {repack} {language} {resolution} {uhd} {source} {hdr} {audio} {video_encode}" + elif type == "WEBDL": # WEB-DL + name = f"{title} {year} {season}{episode} {part} {edition} {hybrid} {repack} {language} {resolution} {uhd} {service} WEB {hdr} {audio} {video_encode}" + elif type == "WEBRIP": # WEBRip + name = f"{title} {year} {season}{episode} {part} {edition} {hybrid} {repack} {language} {resolution} {uhd} {service} WEBRip {hdr} {audio} {video_encode}" + elif type == "HDTV": # HDTV + name = f"{title} {year} {season}{episode} {part} {edition} {repack} {language} {resolution} {source} {audio} {video_encode}" + elif type == "DVDRIP": + name = f"{title} {year} {season} {source} DVDRip {audio} {video_encode}" + + try: + name = ' '.join(name.split()) + except Exception: + console.print( + "[bold red]Unable to generate name. Please re-run and correct any of the following args if needed.") + console.print(f"--category [yellow]{meta['category']}") + console.print(f"--type [yellow]{meta['type']}") + console.print(f"--source [yellow]{meta['source']}") + console.print( + "[bold green]If you specified type, try also specifying source") + + exit() + name_notag = name + name = name_notag + tag + name = fr.clean_name(name) + + if meta['debug']: + console.log("[cyan]get_name cat/type") + console.log(f"CATEGORY: {meta['category']}") + console.log(f"TYPE: {meta['type']}") + console.log("[cyan]get_name meta:") + console.print(f"source : {source}") + console.print(f"type : {type}") + console.print(f"video_codec : {video_codec}") + console.print(f"video_encode : {video_encode}") + console.print(f"NAME : {name}") + + return {'name': name} + + async def get_additional_checks(self, meta: Meta) -> bool: + # Check language requirements: must be French audio OR original audio with French subtitles + french_languages = ["french", "fre", "fra", "fr", + "français", "francais", 'fr-fr', 'fr-ca'] + # check or ignore audio req config + # self.config['TRACKERS'][self.tracker].get('check_for_rules', True): + if not await self.common.check_language_requirements( + meta, + self.tracker, + languages_to_check=french_languages, + check_audio=True, + check_subtitle=True, + require_both=False, + original_language=True, + ): + console.print( + f"[bold red]Language requirements not met for {self.tracker}.[/bold red]") + return False + + return True + + async def search_existing(self, meta: dict[str, Any], _disctype: str) -> list[str]: + if meta['category'] == 'MOVIE': + if meta.get('mal_id'): + console.print(f"https://c411.org/torrents?q={meta['title']}%20{meta['year']}&cat=1&subcat=1") + else: + console.print(f"https://c411.org/torrents?q={meta['title']}%20{meta['year']}&cat=1&subcat=6") + + elif meta['category'] == 'TV': + + if meta.get('mal_id'): + console.print(f"https://c411.org/torrents?q={meta['title']}%20{meta['year']}%20{meta['season_int']}&cat=1&subcat=2") + else: + console.print(f"https://c411.org/torrents?q={meta['title']}%20{meta['year']}%20{meta['season_int']}&cat=1&subcat=7") + + return ['Dupes must be checked Manually'] + # +# curl -X POST "https://c411.org/api/torrents" +# -H "Authorization: Bearer VOTRE_CLE_API" +# -F torrent@ - Fichier .torrent (max 10MB) +# -F nfo@ - Fichier NFO (max 5MB) +# -F title - Titre (3-200 caractères) +# -F description - Description HTML (min 20 caractères) +# -F categoryId - ID de catégorie +# -F subcategoryId - ID de sous-catégorie +# -F options - options={"1": [2, 4], "2": 25, "7": 121, "6": 96} #Options en JSON (langue, qualité, etc.) +# optional +# -F isExclusive - "true" pour release exclusive +# -F uploaderNote - Note pour les modérateurs +# -F tmdbData - Métadonnées TMDB (JSON) +# -F rawgData - Métadonnées RAWG pour jeux (JSON) + + async def upload(self, meta: Meta, _disctype: str) -> bool: + + await self.common.create_torrent_for_upload(meta, self.tracker, 'C411') + torrent_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent" + mediainfo_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt" + + headers = { + "Authorization": f"Bearer {self.config['TRACKERS'][self.tracker]['api_key'].strip()}"} + acm_name = await self.get_name(meta) + dot_name = unidecode.unidecode(acm_name["name"].replace(" ", ".")) + response = None + async with aiofiles.open(torrent_file_path, 'rb') as f: + torrent_bytes = await f.read() + async with aiofiles.open(mediainfo_file_path, 'rb') as f: + mediainfo_bytes = await f.read() + data: dict[str, Any] = { + "title": str(dot_name), + "description": await fr.get_desc_full(meta, self.tracker), + "categoryId": str("1"), + "subcategoryId": str(await self.get_subcat_id(meta)), + # 1 langue , 2 qualite + "options": await self.get_option_tag(meta), + # "isExclusive": "Test Upload-Assistant", + "uploaderNote": "Upload-Assistant", + # "tmdbData": "Test Upload-Assistant", + # "rawgData": "Test Upload-Assistant", + } + if meta["debug"] is False: + response_data = {} + max_retries = 2 + retry_delay = 5 + timeout = 40.0 + + for attempt in range(max_retries): + try: # noqa: PERF203 + async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client: + response = await client.post( + url=self.upload_url, files={"torrent": torrent_bytes, + "nfo": mediainfo_bytes, }, data=data, headers=headers + ) + response.raise_for_status() + + response_data = response.json() + + # Verify API success before proceeding + if not response_data.get("success"): + error_msg = response_data.get( + "message", "Unknown error") + meta["tracker_status"][self.tracker][ + "status_message"] = f"API error: {error_msg}" + console.print( + f"[yellow]Upload to {self.tracker} failed: {error_msg}[/yellow]") + return False + + meta["tracker_status"][self.tracker]["status_message"] = ( + await self.process_response_data(response_data) + ) + # response_data = {'success': True, 'data': {'id': 6216, 'infoHash': '35faeb2c08d7d7448da7c7afd4048f16b02cc4ad', 'status': 'pending'}, 'message': 'Torrent envoyé ! Il sera visible après validation par la Team Pending.'} + + torrent_hash = response_data["data"]["infoHash"] + meta["tracker_status"][self.tracker]["torrent_id"] = torrent_hash + await self.download_torrent(meta, torrent_hash) + return True # Success + + except httpx.HTTPStatusError as e: # noqa: PERF203 + if e.response.status_code in [403, 302]: + # Don't retry auth/permission errors + if e.response.status_code == 403: + meta["tracker_status"][self.tracker][ + "status_message" + ] = f"data error: Forbidden (403). This may indicate that you do not have upload permission. {e.response.text}" + else: + meta["tracker_status"][self.tracker][ + "status_message" + ] = f"data error: Redirect (302). This may indicate a problem with authentication. {e.response.text}" + return False # Auth/permission error + elif e.response.status_code in [401, 404, 422]: + meta["tracker_status"][self.tracker][ + "status_message" + ] = f"data error: HTTP {e.response.status_code} - {e.response.text}" + else: + # Retry other HTTP errors + if attempt < max_retries - 1: + console.print( + f"[yellow]{self.tracker}: HTTP {e.response.status_code} error, retrying in {retry_delay} seconds... (attempt {attempt + 1}/{max_retries})[/yellow]" + ) + await asyncio.sleep(retry_delay) + continue + else: + # Final attempt failed + if e.response.status_code == 520: + meta["tracker_status"][self.tracker][ + "status_message" + ] = "data error: Error (520). This is probably a cloudflare issue on the tracker side." + else: + meta["tracker_status"][self.tracker][ + "status_message" + ] = f"data error: HTTP {e.response.status_code} - {e.response.text}" + return False # HTTP error after all retries + except httpx.TimeoutException: + if attempt < max_retries - 1: + timeout = timeout * 1.5 # Increase timeout by 50% for next retry + console.print( + f"[yellow]{self.tracker}: Request timed out, retrying in {retry_delay} seconds with {timeout}s timeout... (attempt {attempt + 1}/{max_retries})[/yellow]" + ) + await asyncio.sleep(retry_delay) + continue + else: + meta["tracker_status"][self.tracker][ + "status_message" + ] = "data error: Request timed out after multiple attempts" + return False # Timeout after all retries + except httpx.RequestError as e: + if attempt < max_retries - 1: + console.print( + f"[yellow]{self.tracker}: Request error, retrying in {retry_delay} seconds... (attempt {attempt + 1}/{max_retries})[/yellow]" + ) + await asyncio.sleep(retry_delay) + continue + else: + meta["tracker_status"][self.tracker][ + "status_message" + ] = f"data error: Unable to upload. Error: {e}.\nResponse: {response_data}" + return False # Request error after all retries + except json.JSONDecodeError as e: + meta["tracker_status"][self.tracker][ + "status_message" + ] = f"data error: Invalid JSON response from {self.tracker}. Error: {e}" + return False # JSON parsing error + else: + console.print(f"[cyan]{self.tracker} Request Data:") + console.print(data) + meta["tracker_status"][self.tracker][ + "status_message" + ] = f"Debug mode enabled, not uploading: {self.tracker}." + 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 + + return False + + async def download_torrent(self, meta: dict[str, Any], torrent_hash: str, ) -> None: + path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DL.torrent" + params: dict[str, Any] = { + "t": "get", + "id": torrent_hash, + "apikey": self.config['TRACKERS'][self.tracker]['api_key'].strip(), + } +# https://c411.org/api/?t=get&id=35faeb2c08d7d7448da7c7afd4048f16b02cc4ad&apikey=d95f4844860d1d23d0b3907efe098f561e519fe13af3eaa8fcf8949c0ce56645 + # https://c411.org/api/?t=get&id={{infoHash}}&apikey={{config.API_KEY}} + try: + async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client: + r = await client.get(self.torrent_url, params=params) + + r.raise_for_status() + async with aiofiles.open(path, "wb") as f: + async for chunk in r.aiter_bytes(): + await f.write(chunk) + + return None + + except Exception as e: + console.print( + f"[yellow]Warning: Could not download torrent file: {str(e)}[/yellow]") + console.print( + "[yellow]Download manually from the tracker.[/yellow]") + return None + return None + + async def process_response_data(self, response_data: dict[str, Any]) -> str: + """Returns the success message from the response data as a string.""" + if response_data.get("success") is True: + return str(response_data.get("message", "Upload successful")) + + # For non-success responses, format as string + error_msg = response_data.get("message", "") + if error_msg: + return f"API response: {error_msg}" + return f"API response: {response_data}" diff --git a/src/trackers/FRENCH.py b/src/trackers/FRENCH.py new file mode 100644 index 000000000..529a67f87 --- /dev/null +++ b/src/trackers/FRENCH.py @@ -0,0 +1,645 @@ +from typing import Any, Optional, cast +import aiofiles +import re +import httpx +from data.config import config +from src.console import console +from unidecode import unidecode +# UA 7.0.0 + + +async def build_audio_string(meta: dict[str, Any]) -> str: + + # Priority Order: + # 1. MULYi: Exactly 2 audio tracks + # 2. MULTI: 3 audio tracks + # 3. VOSTFR: Single audio (original lang) + French subs + NO French audio + # 4. VO: Single audio (original lang) + NO French subs + NO French audio + + audio_tracks = await get_audio_tracks(meta, True) + if not audio_tracks: + return '' + + audio_langs = await extract_audio_languages(audio_tracks, meta) + if not audio_langs: + return '' + + language = "" + original_lang = await get_original_language(meta) + has_french_audio = 'FRA' in audio_langs + has_French_subs = await has_french_subs(meta) + num_audio_tracks = len(audio_tracks) + + # DUAL - Exactly 2 audios + if num_audio_tracks == 2 and has_french_audio: + language = "MULTi" + + # MULTI - 3+ audios + if num_audio_tracks >= 3 and has_french_audio: + language = "MULTi" + + # VOSTFR - Single audio (original) + French subs + NO French audio + if num_audio_tracks == 1 and original_lang and not has_french_audio and has_French_subs: + if audio_langs[0] == original_lang: + language = "VOSTFR" + + # VO - Single audio (original) + NO French subs + NO French audio + if num_audio_tracks == 1 and original_lang and not has_french_audio and not has_French_subs: + if audio_langs[0] == original_lang: + language = "VO" + + # FRENCH. - Single audio FRENCH + if num_audio_tracks == 1 and has_french_audio: + if audio_langs[0] == original_lang: + language = "FRENCH" + + return language + +# VOF ,VOQ si le pays dorigine est la meme langue + + +async def get_extra_french_tag(meta: dict[str, Any], check_origin: bool) -> str: + audio_track = await get_audio_tracks(meta, True) + + vfq = "" + vff = "" + vf = "" + origincountry = meta.get("origin_country", "") + + for i, item in enumerate(audio_track): + try: + title = item.get("Title", "").lower() + except: + title = '' + lang = item.get('Language', "").lower() + + if lang == "fr-ca" or "vfq" in title: + vfq = True + elif lang == "fr-fr"or "vff" in title: + vff = True + elif lang == "fr" or "vfi" in title: + vf = True + + if vff and vfq: + return 'VF2' + elif vfq: + if "CA" in origincountry and check_origin: + return 'VOQ' + else: + return 'VFQ' + elif vff: + if "FR" in origincountry and check_origin: + return 'VOF' + else: + return 'VFF' + elif vf: + if "FR" in origincountry and check_origin: + return 'VOF' + else: + return 'VFI' + else: + return "" + + +async def get_audio_tracks(meta: dict[str, Any], filter: bool) -> list[dict[str, Any]]: + """Extract audio tracks from mediainfo""" + if 'mediainfo' not in meta or 'media' not in meta['mediainfo']: + return [] + + media_info = meta['mediainfo'] + if not isinstance(media_info, dict): + return [] + media_info_dict = cast(dict[str, Any], media_info) + media = media_info_dict.get('media') + if not isinstance(media, dict): + return [] + + media_dict = cast(dict[str, Any], media) + tracks = media_dict.get('track', []) + if not isinstance(tracks, list): + return [] + + audio_tracks: list[dict[str, Any]] = [] + tracks_list = cast(list[Any], tracks) + for track in tracks_list: + if isinstance(track, dict): + track_dict = cast(dict[str, Any], track) + if track_dict.get('@type') == 'Audio': + if filter: + #or not "audio description" in str(track_dict.get('Title') or '').lower() #audio description + if not "commentary" in str(track_dict.get('Title') or '').lower(): + audio_tracks.append(track_dict) + else: + audio_tracks.append(track_dict) + + return audio_tracks + + +async def get_subtitle_tracks(meta: dict[str, Any]) -> list[dict[str, Any]]: + """Extract audio tracks from mediainfo""" + if 'mediainfo' not in meta or 'media' not in meta['mediainfo']: + return [] + + media_info = meta['mediainfo'] + if not isinstance(media_info, dict): + return [] + media_info_dict = cast(dict[str, Any], media_info) + media = media_info_dict.get('media') + if not isinstance(media, dict): + return [] + + media_dict = cast(dict[str, Any], media) + tracks = media_dict.get('track', []) + if not isinstance(tracks, list): + return [] + + audio_tracks: list[dict[str, Any]] = [] + tracks_list = cast(list[Any], tracks) + for track in tracks_list: + if isinstance(track, dict): + track_dict = cast(dict[str, Any], track) + if track_dict.get('@type') == 'Text': + audio_tracks.append(track_dict) + + return audio_tracks + + +async def get_video_tracks(meta: dict[str, Any]) -> list[dict[str, Any]]: + """Extract audio tracks from mediainfo""" + if 'mediainfo' not in meta or 'media' not in meta['mediainfo']: + return [] + + media_info = meta['mediainfo'] + if not isinstance(media_info, dict): + return [] + media_info_dict = cast(dict[str, Any], media_info) + media = media_info_dict.get('media') + if not isinstance(media, dict): + return [] + + media_dict = cast(dict[str, Any], media) + tracks = media_dict.get('track', []) + if not isinstance(tracks, list): + return [] + + audio_tracks: list[dict[str, Any]] = [] + tracks_list = cast(list[Any], tracks) + for track in tracks_list: + if isinstance(track, dict): + track_dict = cast(dict[str, Any], track) + if track_dict.get('@type') == 'Video': + audio_tracks.append(track_dict) + + return audio_tracks + + +async def extract_audio_languages(audio_tracks: list[dict[str, Any]], meta: dict[str, Any]) -> list[str]: + """Extract and normalize audio languages""" + audio_langs: list[str] = [] + + for track in audio_tracks: + lang = track.get('Language', '') + if lang: + lang_code = await map_language(str(lang)) + if lang_code and lang_code not in audio_langs: + audio_langs.append(lang_code) + + if not audio_langs and meta.get('audio_languages'): + audio_languages = meta.get('audio_languages') + audio_languages_list: list[Any] = cast( + list[Any], audio_languages) if isinstance(audio_languages, list) else [] + for lang in audio_languages_list: + lang_code = await map_language(str(lang)) + if lang_code and lang_code not in audio_langs: + audio_langs.append(lang_code) + + return audio_langs + + +async def map_language(lang: str) -> str: + """Map language codes and names""" + if not lang: + return '' + + lang_map = { + 'spa': 'ESP', 'es': 'ESP', 'spanish': 'ESP', 'español': 'ESP', 'castellano': 'ESP', 'es-es': 'ESP', + 'eng': 'ENG', 'en': 'ENG', 'english': 'ENG', 'en-us': 'ENG', 'en-gb': 'ENG', + 'lat': 'LAT', 'latino': 'LAT', 'latin american spanish': 'LAT', 'es-mx': 'LAT', 'es-419': 'LAT', + 'fre': 'FRA', 'fra': 'FRA', 'fr': 'FRA', 'french': 'FRA', 'français': 'FRA', 'fr-fr': 'FRA', 'fr-ca': 'FRA', + 'ger': 'ALE', 'deu': 'ALE', 'de': 'ALE', 'german': 'ALE', 'deutsch': 'ALE', + 'jpn': 'JAP', 'ja': 'JAP', 'japanese': 'JAP', '日本語': 'JAP', + 'kor': 'COR', 'ko': 'COR', 'korean': 'COR', '한국어': 'COR', + 'ita': 'ITA', 'it': 'ITA', 'italian': 'ITA', 'italiano': 'ITA', + 'por': 'POR', 'pt': 'POR', 'portuguese': 'POR', 'português': 'POR', 'pt-br': 'POR', 'pt-pt': 'POR', + 'chi': 'CHI', 'zho': 'CHI', 'zh': 'CHI', 'chinese': 'CHI', 'mandarin': 'CHI', '中文': 'CHI', 'zh-cn': 'CHI', + 'rus': 'RUS', 'ru': 'RUS', 'russian': 'RUS', 'русский': 'RUS', + 'ara': 'ARA', 'ar': 'ARA', 'arabic': 'ARA', + 'hin': 'HIN', 'hi': 'HIN', 'hindi': 'HIN', + 'tha': 'THA', 'th': 'THA', 'thai': 'THA', + 'vie': 'VIE', 'vi': 'VIE', 'vietnamese': 'VIE', + } + + lang_lower = str(lang).lower().strip() + mapped = lang_map.get(lang_lower) + + if mapped: + return mapped + + return lang.upper()[:3] if len(lang) >= 3 else lang.upper() + + +async def get_original_language(meta: dict[str, Any]) -> Optional[str]: + """Get the original language from existing metadata""" + original_lang = None + + if meta.get('original_language'): + original_lang = str(meta['original_language']) + + if not original_lang: + imdb_info_raw = meta.get('imdb_info') + imdb_info: dict[str, Any] = cast( + dict[str, Any], imdb_info_raw) if isinstance(imdb_info_raw, dict) else {} + imdb_lang: Any = imdb_info.get('language') + + if isinstance(imdb_lang, list): + imdb_lang_list = cast(list[Any], imdb_lang) + imdb_lang = imdb_lang_list[0] if imdb_lang_list else '' + + if imdb_lang: + if isinstance(imdb_lang, dict): + imdb_lang_dict = cast(dict[str, Any], imdb_lang) + imdb_lang_text = imdb_lang_dict.get('text', '') + original_lang = str(imdb_lang_text).strip() + elif isinstance(imdb_lang, str): + original_lang = imdb_lang.strip() + else: + original_lang = str(imdb_lang).strip() + + if original_lang: + return await map_language(str(original_lang)) + + return None + + +async def has_french_subs(meta: dict[str, Any]) -> bool: + """Check if torrent has Spanish subtitles""" + if 'mediainfo' not in meta or 'media' not in meta['mediainfo']: + return False + media_info = meta['mediainfo'] + if not isinstance(media_info, dict): + return False + media_info_dict = cast(dict[str, Any], media_info) + media = media_info_dict.get('media') + if not isinstance(media, dict): + return False + media_dict = cast(dict[str, Any], media) + tracks = media_dict.get('track', []) + if not isinstance(tracks, list): + return False + + tracks_list = cast(list[Any], tracks) + for track in tracks_list: + if not isinstance(track, dict): + continue + track_dict = cast(dict[str, Any], track) + if track_dict.get('@type') == 'Text': + lang = track_dict.get('Language', '') + lang = lang.lower() if isinstance(lang, str) else '' + + title = track_dict.get('Title', '') + title = title.lower() if isinstance(title, str) else '' + + if lang in ["french", "fre", "fra", "fr", "français", "francais", 'fr-fr', 'fr-ca']: + return True + if 'french' in title or 'français' in title or 'francais' in title: + return True + + return False + + +async def map_audio_codec(audio_track: dict[str, Any]) -> str: + codec = str(audio_track.get('Format', '')).upper() + + if 'atmos' in str(audio_track.get('Format_AdditionalFeatures', '')).lower(): + return 'Atmos' + + codec_map = { + 'AAC LC': 'AAC LC', 'AAC': 'AAC', 'AC-3': 'AC3', 'AC3': 'AC3', + 'E-AC-3': 'EAC3', 'EAC3': 'EAC3', 'DTS': 'DTS', + 'DTS-HD MA': 'DTS-HD MA', 'DTS-HD HRA': 'DTS-HD HRA', + 'TRUEHD': 'TrueHD', 'MLP FBA': 'MLP', 'PCM': 'PCM', + 'FLAC': 'FLAC', 'OPUS': 'OPUS', 'MP3': 'MP3', + } + + return codec_map.get(codec, codec) + + +async def get_audio_channels(audio_track: dict[str, Any]) -> str: + """Get audio channel configuration""" + channels = audio_track.get('Channels', '') + channel_map = { + '1': 'Mono', '2': '2.0', '3': '3.0', + '4': '3.1', '5': '5.0', '6': '5.1', '8': '7.1', + } + return channel_map.get(str(channels), '0') + + +async def get_audio_name(meta: dict[str, Any]) -> str: + audio_track = await get_audio_tracks(meta, True) + if not audio_track: + return "" + has_french_audio = "fr" in audio_track or "fr-fr" in audio_track or "fr-ca" in audio_track + audio_parts: list[str] = [] + if has_french_audio: + for i, item in enumerate(audio_track): + if item['Language'] == "fr" or item['Language'] == "fr-fr" or item['Language'] == "fr-ca": + codec = await map_audio_codec(item) + channels = await get_audio_channels(item) + audio_parts.append(f"{codec} {channels}") + audio = ' '.join(audio_parts) + return audio + else: + for i, item in enumerate(audio_track): + if item['Default'] == "Yes": + codec = await map_audio_codec(item) + channels = await get_audio_channels(item) + audio_parts.append(f"{codec} {channels}") + audio = ' '.join(audio_parts) + return audio + return "" + + +async def translate_genre(text: str) -> str: + mapping = { + 'Action': 'Action', + 'Adventure': 'Aventure', + 'Fantasy': 'Fantastique', + 'History': 'Histoire', + 'Horror': 'Horreur', + 'Music ': 'Musique', + 'Romance': 'Romance', + 'Science Fiction': 'Science-fiction', + 'TV Movie': 'Téléfilm', + 'Thriller': 'Thriller', + 'War': 'Guerre', + 'Action & Adventure': 'Action & aventure', + 'Animation': 'Animation', + 'Comedy': 'Comédie', + 'Crime': 'Policier', + 'Documentary': 'Documentaire', + 'Drama': 'Drame', + 'Family': 'Famille', + 'Kids': 'Enfants', + 'Mystery': 'Mystère', + 'News': 'Actualités', + 'Reality': 'Réalité', + 'Sci-Fi & Fantasy': 'Science-fiction & fantastique', + 'Soap': 'Feuilletons', + 'Talk': 'Débats', + 'War & Politics': 'Guerre & politique', + 'Western': 'Western' + } + result = [] + + for word in map(str.strip, text.split(",")): + if word in mapping: + result.append(mapping[word]) + else: + result.append(f"*{word}*") + + return ", ".join(result) + + +def clean_name(input_str: str) -> str: + ascii_str = unidecode(input_str) + invalid_char = set('<>"/\\|?*') #! . , : ; @ # $ % ^ & */ \" '_ + result = [] + for char in ascii_str: + if char in invalid_char: + continue + result.append(char) + + return "".join(result) + + +async def get_translation_fr(meta: dict[str, Any]) -> tuple[str, str]: + """Get Spanish title if available and configured""" + fr_title = meta.get("frtitle") + fr_overwiew = meta.get("froverview") + if fr_title and fr_overwiew: + return fr_title, fr_overwiew + + # Try to get from IMDb with priority: country match, then language match + imdb_info_raw = meta.get('imdb_info') + imdb_info: dict[str, Any] = cast( + dict[str, Any], imdb_info_raw) if isinstance(imdb_info_raw, dict) else {} + akas_raw = imdb_info.get('akas', []) + akas: list[Any] = cast(list[Any], akas_raw) if isinstance( + akas_raw, list) else [] + french_title = None + country_match = None + language_match = None + + for aka in akas: + if isinstance(aka, dict): + aka_dict = cast(dict[str, Any], aka) + if aka_dict.get("country") in ["France", "FR"]: + country_match = aka_dict.get("title") + break # Country match takes priority + elif aka_dict.get("language") in ["France", "French", "FR"] and not language_match: + language_match = aka_dict.get("title") + + french_title = country_match or language_match + + tmdb_id = int(meta["tmdb_id"]) + category = str(meta["category"]) + tmdb_title, tmdb_overview = await get_tmdb_translations(tmdb_id, category, "fr") + meta["frtitle"] = tmdb_title or tmdb_title + meta["froverview"] = tmdb_overview + return french_title if french_title is not None else tmdb_title, tmdb_overview + + +async def get_tmdb_translations(tmdb_id: int, category: str, target_language: str) -> tuple[str, str]: + """Get translations from TMDb API""" + endpoint = "movie" if category == "MOVIE" else "tv" + url = f"https://api.themoviedb.org/3/{endpoint}/{tmdb_id}/translations" + tmdb_api_key = config['DEFAULT'].get('tmdb_api', False) + async with httpx.AsyncClient() as client: + try: + response = await client.get(url, params={"api_key": tmdb_api_key}) + response.raise_for_status() + data = response.json() + + # Look for target language translation + for translation in data.get('translations', []): + if translation.get('iso_639_1') == target_language: + translated_data = translation.get('data', {}) + translated_desc = translated_data.get('overview') + translated_title = translated_data.get( + 'title') or translated_data.get('name') + + return translated_title or "", translated_desc or "" + return "", "" + + except Exception as e: + return "", "" + +# unknow return type + + +async def get_desc_full(meta: dict[str, Any], tracker) -> str: + + video_track = await get_video_tracks(meta) + mbps = int(video_track[0]['BitRate']) / 1_000_000 + title, description = await get_translation_fr(meta) + genre = await translate_genre(meta['combined_genres']) + audio_tracks = await get_audio_tracks(meta, False) + subtitle_tracks = await get_subtitle_tracks(meta) + poster = str(meta.get('poster', "")) + year = str(meta.get('year', "")) + original_title = str(meta.get('original_title', "")) + Pays = str(meta['imdb_info']['country']) + release_date = str(meta.get('release_date', "")) + video_duration = str(meta.get('video_duration', "")) + source = str(meta.get('source', "")) + type = str(meta.get('type', "")) + resolution = str(meta.get('resolution', "")) + container = str(meta.get('container', "")) + video_codec = str(meta.get('video_codec', "")) + hdr = str(meta.get('hdr', "")) + tag = str(meta.get('tag', "")).replace('-', '') + service_longname = str(meta.get('service_longname', "")) + season = str(meta.get('season_int', '')) + episode = str(meta.get('episode_int', '')) + + desc_parts = [] + # if meta['logo']: + # desc_parts.append(f"[img]{meta['logo']}[/img]") + desc_parts.append(f"[img]{poster}[/img]") + + desc_parts.append( + f"[b][font=Verdana][color=#3d85c6][size=29]{title}[/size][/font]") + desc_parts.append(f"[size=18]{year}[/size][/color][/b]") + + if meta['category'] == "TV": + season = f"S{season}" if season else "" + episode = f"E{episode}" if episode else "" + desc_parts.append(f"[b][size=18]{season}{episode}[/size][/b]") + + desc_parts.append( + f"[font=Verdana][size=13][b][color=#3d85c6]Titre original :[/color][/b] [i]{original_title}[/i][/size][/font]") + desc_parts.append( + f"[b][color=#3d85c6]Pays :[/color][/b] [i]{Pays}[/i]") + desc_parts.append(f"[b][color=#3d85c6]Genres :[/color][/b] [i]{genre}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Date de sortie :[/color][/b] [i]{release_date}[/i]") + + if meta['category'] == 'MOVIE': + desc_parts.append( + f"[b][color=#3d85c6]Durée :[/color][/b] [i]{video_duration} Minutes[/i]") + + if meta['imdb_id']: + desc_parts.append( f"{meta.get('imdb_info', {}).get('imdb_url', '')}") + if meta['tmdb']: + desc_parts.append( f"\nhttps://www.themoviedb.org/{str(meta['category'].lower())}/{str(meta['tmdb'])}") + if meta['tvdb_id']: + desc_parts.append( f"\nhttps://www.thetvdb.com/?id={str(meta['tvdb_id'])}&tab=series") + if meta['tvmaze_id']: + desc_parts.append( f"\nhttps://www.tvmaze.com/shows/{str(meta['tvmaze_id'])}") + if meta['mal_id']: + desc_parts.append( f"\nhttps://myanimelist.net/anime/{str(meta['mal_id'])}") + + desc_parts.append(f"[img]https://i.imgur.com/W3pvv6q.png[/img]") + + desc_parts.append(f"{description}") + + desc_parts.append(f"[img]https://i.imgur.com/KMZsqZn.png[/img]") + + #if meta.get('is_disc', '') == 'DVD': + # desc_parts.append(f'[hide=DVD MediaInfo][pre]{await builder.get_mediainfo_section(meta)}[/pre][/hide]') + + #bd_info = await builder.get_bdinfo_section(meta) + #if bd_info: + # desc_parts.append(f'[hide=BDInfo][pre]{bd_info}[/pre][/hide]') + + # User description + #desc_parts.append(await builder.get_user_description(meta)) + + desc_parts.append( + f"[b][color=#3d85c6]Source :[/color][/b] [i]{source} {service_longname}[/i]") + + desc_parts.append( + f"[b][color=#3d85c6]Type :[/color][/b] [i]{type}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Résolution vidéo :[/color][/b][i]{resolution}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Format vidéo :[/color][/b] [i]{container}[/i]") + + desc_parts.append( + f"[b][color=#3d85c6]Codec vidéo :[/color][/b] [i]{video_codec} {hdr}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{mbps:.2f} MB/s[/i]") + + desc_parts.append(f"[b][color=#3d85c6] Audio(s) :[/color][/b]") + for obj in audio_tracks: + kbps = int(obj['BitRate']) / 1_000 + + flags = [] + if obj.get("Forced") == "Yes": + flags.append("Forced") + if obj.get("Default") == "Yes": + flags.append("Default") + if "commentary" in str(obj.get('Title')).lower(): + flags.append("Commentary") + if " ad" in str(obj.get('Title')).lower(): + flags.append("Audio Description") + + line = f"{obj['Language']} / {obj['Format']} / {obj['Channels']}ch / {kbps:.2f}KB/s" + if flags: + line += " / " + " / ".join(flags) + desc_parts.append(line) + + # desc_parts.append(f"{obj['Language']} / {obj['Format']} / {obj['Channels']}ch / {kbps}KB/s") + + desc_parts.append(f"[b][color=#3d85c6]Sous-titres :[/color][/b]") + for obj in subtitle_tracks: + + flags = [] + if obj.get("Forced") == "Yes": + flags.append("Forced") + if obj.get("Default") == "Yes": + flags.append("Default") + line = f"{obj['Language']} / {obj['Format']}" + if flags: + line += " / " + " / ".join(flags) + desc_parts.append(line) + + # desc_parts.append(f" {obj['Language']} / {obj['Format']} / Forced:{obj['Forced']} / Default:{obj['Default']}") + + # desc_parts.append(f"[img]https://i.imgur.com/KFsABlN.png[/img]") + desc_parts.append( + f"[b][color=#3d85c6]Team :[/color][/b] [i]{tag}[/i] ") + # desc_parts.append(f"[b][color=#3d85c6] Taille totale :[/color][/b] {gb} GB") + + # Screenshots + if f'{tracker}_images_key' in meta: + images = meta[f'{tracker}_images_key'] + else: + images = meta['image_list'] + if images: + screenshots_block = '' + for image in images: + screenshots_block += f"[img]{image['raw_url']}[/img]\n" + desc_parts.append(screenshots_block) + + # Signature + desc_parts.append( + f"[url=https://github.com/Audionut/Upload-Assistant]{meta['ua_signature']}[/url]") + + description = '\n'.join(part for part in desc_parts if part.strip()) + + async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{tracker}]DESCRIPTION.json", 'w', encoding='utf-8') as description_file: + await description_file.write(description) + + return description + diff --git a/src/trackersetup.py b/src/trackersetup.py index 8135f3302..0a69f775b 100644 --- a/src/trackersetup.py +++ b/src/trackersetup.py @@ -26,6 +26,7 @@ from src.trackers.BJS import BJS from src.trackers.BLU import BLU from src.trackers.BT import BT +from src.trackers.C411 import C411 from src.trackers.CBR import CBR from src.trackers.COMMON import COMMON from src.trackers.CZ import CZ @@ -1336,7 +1337,7 @@ 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, + 'A4K': A4K, 'ACM': ACM, 'AITHER': AITHER, 'ANT': ANT, 'AR': AR, 'ASC': ASC, 'AZ': AZ, 'BHD': BHD, 'BHDTV': BHDTV, 'BJS': BJS, 'BLU': BLU, 'BT': BT, 'C411': C411, 'CBR': CBR, 'CZ': CZ, 'DC': DC, 'DP': DP, '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, 'R4E': R4E, 'RAS': RAS, 'RF': RF, 'RTF': RTF, 'SAM': SAM, 'SHRI': SHRI, 'SN': SN, 'SP': SP, 'SPD': SPD, 'STC': STC, 'THR': THR, @@ -1349,7 +1350,7 @@ async def make_trumpable_report(self, meta: Meta, tracker: str) -> bool: } other_api_trackers = { - 'ANT', 'BHDTV', 'DC', 'GPW', 'NBL', 'RTF', 'SN', 'SPD', 'TL', 'TVC' + 'ANT', 'BHDTV', 'C411', 'DC', 'GPW', 'NBL', 'RTF', 'SN', 'SPD', 'TL', 'TVC' } http_trackers = { From 2a29a464692a35120f9e674a8943208035efa043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20DEWITTE?= Date: Tue, 10 Feb 2026 09:02:03 +0100 Subject: [PATCH 02/30] Little bugfixes --- data/example-config.py | 2 +- src/trackers/C411.py | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/data/example-config.py b/data/example-config.py index 636736953..f00fab9e1 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, 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, C411, CBR, CZ, DC, DP, 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 # Only add the trackers you want to upload to on a regular basis "default_trackers": "", diff --git a/src/trackers/C411.py b/src/trackers/C411.py index d8e5371cd..35ad20edb 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -55,7 +55,7 @@ async def get_subcat_id(self, meta: Meta) -> str: # unknow return type async def get_option_tag(self, meta: Meta): - obj1 = "" + obj1 = [] obj2 = None vff = None vfq = None @@ -72,16 +72,16 @@ async def get_option_tag(self, meta: Meta): if item['Language'] == "en" or item['Language'] == "en-us" or item['Language'] == "en-gb": eng = True - if eng and not vff or vfq: # vo - obj1 = obj1 + "1," + if eng and not vff and not vfq: # vo + obj1.append(1) # VO VOSTFR if vff and vfq: - obj1 = obj1 + "4," + obj1.append(4) if vfq: - obj1 = obj1 + "5," + obj1.append(5) if vff: - obj1 = obj1 + "2" + obj1.append(2) # set quality if meta['is_disc'] == 'BDMV': @@ -150,7 +150,7 @@ async def get_option_tag(self, meta: Meta): # hdlight 720 # vcd/vhs options_dict = {} - options_dict[1] = [obj1] + options_dict[1] = obj1 options_dict[2] = [obj2] # Let's see if it's a tv show if meta['category'] == 'TV': @@ -371,7 +371,7 @@ async def search_existing(self, meta: dict[str, Any], _disctype: str) -> list[st async def upload(self, meta: Meta, _disctype: str) -> bool: await self.common.create_torrent_for_upload(meta, self.tracker, 'C411') - torrent_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent" + torrent_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}].torrent" mediainfo_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt" headers = { @@ -448,6 +448,8 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: meta["tracker_status"][self.tracker][ "status_message" ] = f"data error: HTTP {e.response.status_code} - {e.response.text}" + return False + ] = f"data error: HTTP {e.response.status_code} - {e.response.text}" else: # Retry other HTTP errors if attempt < max_retries - 1: From a1c5f531df825a0a980befd59788b236b51500cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20DEWITTE?= Date: Tue, 10 Feb 2026 09:11:31 +0100 Subject: [PATCH 03/30] mlinor bugfix --- src/trackers/C411.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 35ad20edb..2f7a8cf7b 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -352,21 +352,6 @@ async def search_existing(self, meta: dict[str, Any], _disctype: str) -> list[st console.print(f"https://c411.org/torrents?q={meta['title']}%20{meta['year']}%20{meta['season_int']}&cat=1&subcat=7") return ['Dupes must be checked Manually'] - # -# curl -X POST "https://c411.org/api/torrents" -# -H "Authorization: Bearer VOTRE_CLE_API" -# -F torrent@ - Fichier .torrent (max 10MB) -# -F nfo@ - Fichier NFO (max 5MB) -# -F title - Titre (3-200 caractères) -# -F description - Description HTML (min 20 caractères) -# -F categoryId - ID de catégorie -# -F subcategoryId - ID de sous-catégorie -# -F options - options={"1": [2, 4], "2": 25, "7": 121, "6": 96} #Options en JSON (langue, qualité, etc.) -# optional -# -F isExclusive - "true" pour release exclusive -# -F uploaderNote - Note pour les modérateurs -# -F tmdbData - Métadonnées TMDB (JSON) -# -F rawgData - Métadonnées RAWG pour jeux (JSON) async def upload(self, meta: Meta, _disctype: str) -> bool: @@ -449,7 +434,6 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: "status_message" ] = f"data error: HTTP {e.response.status_code} - {e.response.text}" return False - ] = f"data error: HTTP {e.response.status_code} - {e.response.text}" else: # Retry other HTTP errors if attempt < max_retries - 1: @@ -522,8 +506,6 @@ async def download_torrent(self, meta: dict[str, Any], torrent_hash: str, ) -> N "id": torrent_hash, "apikey": self.config['TRACKERS'][self.tracker]['api_key'].strip(), } -# https://c411.org/api/?t=get&id=35faeb2c08d7d7448da7c7afd4048f16b02cc4ad&apikey=d95f4844860d1d23d0b3907efe098f561e519fe13af3eaa8fcf8949c0ce56645 - # https://c411.org/api/?t=get&id={{infoHash}}&apikey={{config.API_KEY}} try: async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client: r = await client.get(self.torrent_url, params=params) From d440e4ab7e6c693b1b0785d67ff6214d5e215be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20DEWITTE?= Date: Tue, 10 Feb 2026 17:05:59 +0100 Subject: [PATCH 04/30] temp --- src/trackers/C411.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 2f7a8cf7b..6dee8d50b 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -13,6 +13,7 @@ import src.trackers.FRENCH as fr import unidecode from typing import Any, Callable, Optional, Union, cast +from lxml import etree Meta = dict[str, Any] Config = dict[str, Any] @@ -26,7 +27,7 @@ def __init__(self, config: Config) -> None: self.id_url = f'{self.base_url}/api/torrents' self.upload_url = f'{self.base_url}/api/torrents' # self.requests_url = f'{self.base_url}/api/requests/filter' - # self.search_url = f'{self.base_url}/api/torrents/filter' + self.search_url = f'{self.base_url}/api/?t=search' self.torrent_url = f'{self.base_url}/api/' self.banned_groups: list[str] = [] pass @@ -337,21 +338,34 @@ async def get_additional_checks(self, meta: Meta) -> bool: return True - async def search_existing(self, meta: dict[str, Any], _disctype: str) -> list[str]: - if meta['category'] == 'MOVIE': - if meta.get('mal_id'): - console.print(f"https://c411.org/torrents?q={meta['title']}%20{meta['year']}&cat=1&subcat=1") - else: - console.print(f"https://c411.org/torrents?q={meta['title']}%20{meta['year']}&cat=1&subcat=6") - - elif meta['category'] == 'TV': + async def search_existing(self, meta: dict[str, Any], _) -> list[str]: + dupes: list[str] = [] + params: dict[str, Any] = { + 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'q': unidecode.unidecode(acm_name["name"].replace(" ", ".")) + } + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(url=self.search_url, params=params) + if response.status_code == 200: + root = etree.fromstring(response.text) + #data = response.json() + for each in data['data']: + result = each['attributes']['name'] + dupes.append(result) + else: + console.print(f"[bold red]Failed to search torrents. HTTP Status: {response.status_code}") + except httpx.TimeoutException: + console.print("[bold red]Request timed out after 5 seconds") + except httpx.RequestError as e: + console.print(f"[bold red]Unable to search for existing torrents: {e}") + except Exception as e: + console.print(f"[bold red]Unexpected error: {e}") + await asyncio.sleep(5) - if meta.get('mal_id'): - console.print(f"https://c411.org/torrents?q={meta['title']}%20{meta['year']}%20{meta['season_int']}&cat=1&subcat=2") - else: - console.print(f"https://c411.org/torrents?q={meta['title']}%20{meta['year']}%20{meta['season_int']}&cat=1&subcat=7") + return dupes - return ['Dupes must be checked Manually'] + async def upload(self, meta: Meta, _disctype: str) -> bool: From e8fe2ecf35f20e22787cce67a06ca29e1f74531e Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Wed, 11 Feb 2026 06:14:35 +0100 Subject: [PATCH 05/30] test dupe check --- src/trackers/C411.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 6dee8d50b..7feab5617 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -27,7 +27,7 @@ def __init__(self, config: Config) -> None: self.id_url = f'{self.base_url}/api/torrents' self.upload_url = f'{self.base_url}/api/torrents' # self.requests_url = f'{self.base_url}/api/requests/filter' - self.search_url = f'{self.base_url}/api/?t=search' + self.search_url = f'{self.base_url}/api/' self.torrent_url = f'{self.base_url}/api/' self.banned_groups: list[str] = [] pass @@ -341,6 +341,7 @@ async def get_additional_checks(self, meta: Meta) -> bool: async def search_existing(self, meta: dict[str, Any], _) -> list[str]: dupes: list[str] = [] params: dict[str, Any] = { + 't': search, 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), 'q': unidecode.unidecode(acm_name["name"].replace(" ", ".")) } @@ -348,11 +349,12 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get(url=self.search_url, params=params) if response.status_code == 200: - root = etree.fromstring(response.text) - #data = response.json() - for each in data['data']: - result = each['attributes']['name'] - dupes.append(result) + root = etree.fromstring(response.text.encode('utf-8')) + channel = root[0] + for result in channel: + if result.tag == 'item': + dupe = result[0] + dupes.append(dupe.text) else: console.print(f"[bold red]Failed to search torrents. HTTP Status: {response.status_code}") except httpx.TimeoutException: From 4e562ead92275e027103584ef6c056dbf14786c3 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Wed, 11 Feb 2026 06:18:51 +0100 Subject: [PATCH 06/30] test dupe check --- src/trackers/C411.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 7feab5617..a0d6069e4 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -340,10 +340,11 @@ async def get_additional_checks(self, meta: Meta) -> bool: async def search_existing(self, meta: dict[str, Any], _) -> list[str]: dupes: list[str] = [] + title = str(meta.get('title', '')).strip() params: dict[str, Any] = { - 't': search, + 't': 'search', 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'q': unidecode.unidecode(acm_name["name"].replace(" ", ".")) + 'q': unidecode.unidecode(title.replace(" ", ".")) } try: async with httpx.AsyncClient(timeout=5.0) as client: From b3ceefee2f2b06020ed769a2661855039bcfea42 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Wed, 11 Feb 2026 06:33:12 +0100 Subject: [PATCH 07/30] dupe check ok --- src/trackers/C411.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index a0d6069e4..72945f087 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -340,7 +340,7 @@ async def get_additional_checks(self, meta: Meta) -> bool: async def search_existing(self, meta: dict[str, Any], _) -> list[str]: dupes: list[str] = [] - title = str(meta.get('title', '')).strip() + title, descr = await fr.get_translation_fr(meta) params: dict[str, Any] = { 't': 'search', 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), @@ -350,7 +350,8 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get(url=self.search_url, params=params) if response.status_code == 200: - root = etree.fromstring(response.text.encode('utf-8')) + response_text = response.text.encode('utf-8') + root = etree.fromstring(response_text) channel = root[0] for result in channel: if result.tag == 'item': @@ -365,7 +366,7 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: except Exception as e: console.print(f"[bold red]Unexpected error: {e}") await asyncio.sleep(5) - + print(dupes) return dupes From 80548b082eaf2eee911f84743acbb19fa47a75a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20DEWITTE?= Date: Wed, 11 Feb 2026 09:19:50 +0100 Subject: [PATCH 08/30] remove unuseful print --- src/trackers/C411.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 72945f087..8f29f683a 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -366,7 +366,6 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: except Exception as e: console.print(f"[bold red]Unexpected error: {e}") await asyncio.sleep(5) - print(dupes) return dupes From a5c7afc5dbe11af44fabe2c69dee494e0ed7603f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20DEWITTE?= Date: Wed, 11 Feb 2026 10:30:42 +0100 Subject: [PATCH 09/30] add tmdb to search --- src/trackers/C411.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 8f29f683a..7edf1d376 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -366,6 +366,34 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: except Exception as e: console.print(f"[bold red]Unexpected error: {e}") await asyncio.sleep(5) + if not dupes: + # Nothing came with the name, we'll look using tmdb_id + title, descr = await fr.get_translation_fr(meta) + params: dict[str, Any] = { + 't': 'search', + 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'tmdbid': meta.get('tmdb_id','') + } + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(url=self.search_url, params=params) + if response.status_code == 200: + response_text = response.text.encode('utf-8') + root = etree.fromstring(response_text) + channel = root[0] + for result in channel: + if result.tag == 'item': + dupe = result[0] + dupes.append(dupe.text) + else: + console.print(f"[bold red]Failed to search torrents. HTTP Status: {response.status_code}") + except httpx.TimeoutException: + console.print("[bold red]Request timed out after 5 seconds") + except httpx.RequestError as e: + console.print(f"[bold red]Unable to search for existing torrents: {e}") + except Exception as e: + console.print(f"[bold red]Unexpected error: {e}") + await asyncio.sleep(5) return dupes @@ -394,6 +422,7 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: "options": await self.get_option_tag(meta), # "isExclusive": "Test Upload-Assistant", "uploaderNote": "Upload-Assistant", + "tmdbData": {"id": meta.get('tmdb_id','')} # "tmdbData": "Test Upload-Assistant", # "rawgData": "Test Upload-Assistant", } From 9b90153985b83f056bb961ee1f392d609cedd979 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Wed, 11 Feb 2026 10:58:19 +0100 Subject: [PATCH 10/30] search using tmdb --- src/trackers/C411.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 7edf1d376..d629ab97e 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -368,11 +368,12 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: await asyncio.sleep(5) if not dupes: # Nothing came with the name, we'll look using tmdb_id + tmdb_id = meta.get('tmdb_id','') title, descr = await fr.get_translation_fr(meta) params: dict[str, Any] = { 't': 'search', 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'tmdbid': meta.get('tmdb_id','') + 'tmdbid': tmdb_id } try: async with httpx.AsyncClient(timeout=5.0) as client: @@ -396,10 +397,8 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: await asyncio.sleep(5) return dupes - async def upload(self, meta: Meta, _disctype: str) -> bool: - await self.common.create_torrent_for_upload(meta, self.tracker, 'C411') torrent_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}].torrent" mediainfo_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt" @@ -413,6 +412,7 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: torrent_bytes = await f.read() async with aiofiles.open(mediainfo_file_path, 'rb') as f: mediainfo_bytes = await f.read() + tmdb_data = {"id": meta.get('tmdb_id','')} data: dict[str, Any] = { "title": str(dot_name), "description": await fr.get_desc_full(meta, self.tracker), @@ -422,7 +422,7 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: "options": await self.get_option_tag(meta), # "isExclusive": "Test Upload-Assistant", "uploaderNote": "Upload-Assistant", - "tmdbData": {"id": meta.get('tmdb_id','')} + "tmdbData": str(tmdb_data) # "tmdbData": "Test Upload-Assistant", # "rawgData": "Test Upload-Assistant", } From 4c3b5f1dce54cb121430fc5dd9c68f53f645932e Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Thu, 12 Feb 2026 09:29:02 +0100 Subject: [PATCH 11/30] Add tmdb infos to upload - search dupes first with tmdb id --- src/trackers/C411.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index d629ab97e..556bf77ea 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -339,12 +339,16 @@ async def get_additional_checks(self, meta: Meta) -> bool: return True async def search_existing(self, meta: dict[str, Any], _) -> list[str]: + dupes: list[str] = [] + + # Nothing came with the name, we'll look using tmdb_id + tmdb_id = meta.get('tmdb_id','') title, descr = await fr.get_translation_fr(meta) params: dict[str, Any] = { 't': 'search', 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'q': unidecode.unidecode(title.replace(" ", ".")) + 'tmdbid': tmdb_id } try: async with httpx.AsyncClient(timeout=5.0) as client: @@ -367,13 +371,12 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: console.print(f"[bold red]Unexpected error: {e}") await asyncio.sleep(5) if not dupes: - # Nothing came with the name, we'll look using tmdb_id - tmdb_id = meta.get('tmdb_id','') + # Nothing came with tmdn id, we'll check using names just in case title, descr = await fr.get_translation_fr(meta) params: dict[str, Any] = { 't': 'search', 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'tmdbid': tmdb_id + 'q': unidecode.unidecode(title.replace(" ", ".")) } try: async with httpx.AsyncClient(timeout=5.0) as client: @@ -402,7 +405,16 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: await self.common.create_torrent_for_upload(meta, self.tracker, 'C411') torrent_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}].torrent" mediainfo_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt" - + # Tmdb infos + tmdb_info = {} + tmdb_info['id'] = meta.get("tmdb_id","") + tmdb_info['title'] = meta.get("title","") + tmdb_info['originalTitle'] = meta.get("origial_title","") + tmdb_info['overview'] = meta.get("overview","") + tmdb_info['release_date'] = meta.get("release_date","") + tmdb_info['runtime'] = meta.get("runtime","") + tmdb_info['voteAverage'] = meta.get("vote_average","") + # headers = { "Authorization": f"Bearer {self.config['TRACKERS'][self.tracker]['api_key'].strip()}"} acm_name = await self.get_name(meta) @@ -412,7 +424,6 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: torrent_bytes = await f.read() async with aiofiles.open(mediainfo_file_path, 'rb') as f: mediainfo_bytes = await f.read() - tmdb_data = {"id": meta.get('tmdb_id','')} data: dict[str, Any] = { "title": str(dot_name), "description": await fr.get_desc_full(meta, self.tracker), @@ -422,7 +433,7 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: "options": await self.get_option_tag(meta), # "isExclusive": "Test Upload-Assistant", "uploaderNote": "Upload-Assistant", - "tmdbData": str(tmdb_data) + "tmdbData": json.dumps(tmdb_info) # "tmdbData": "Test Upload-Assistant", # "rawgData": "Test Upload-Assistant", } From 8125476febcc72072287fa15debab1446176e5ac Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Thu, 12 Feb 2026 09:38:41 +0100 Subject: [PATCH 12/30] =?UTF-8?q?Int=C3=A9gration=20code=20Ravager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/trackers/C411.py | 155 ++++++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 84 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 556bf77ea..8516073a6 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -1,24 +1,22 @@ # Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0 # https://github.com/Audionut/Upload-Assistant/tree/master -# #UA 7.0.0 -# -*- coding: utf-8 -*- -# import discord -import json -from typing import Any -import httpx -from src.console import console -from src.trackers.COMMON import COMMON + import aiofiles import asyncio +import json +import httpx import src.trackers.FRENCH as fr -import unidecode -from typing import Any, Callable, Optional, Union, cast +from typing import Any +from src.trackers.COMMON import COMMON +from src.console import console from lxml import etree +import unidecode + Meta = dict[str, Any] Config = dict[str, Any] -class C411(): +class C411: def __init__(self, config: Config) -> None: self.config: Config = config self.common = COMMON(config) @@ -40,24 +38,16 @@ async def get_subcat_id(self, meta: Meta) -> str: sub_cat_id = "0" if meta['category'] == 'MOVIE': - if meta.get('mal_id'): - sub_cat_id = '1' - else: - sub_cat_id = '6' + sub_cat_id = '1' if meta.get('mal_id') else '6' elif meta['category'] == 'TV': - - if meta.get('mal_id'): - sub_cat_id = '2' - else: - sub_cat_id = '7' + sub_cat_id = '2' if meta.get('mal_id') else '7' return sub_cat_id - # unknow return type async def get_option_tag(self, meta: Meta): obj1 = [] - obj2 = None + obj2 = 0 vff = None vfq = None eng = None @@ -66,25 +56,24 @@ async def get_option_tag(self, meta: Meta): type = meta.get('type', "").upper() for item in audio_track: - if item['Language'] == "fr-ca": + lang = str(item.get('Language', '')).lower() + if lang == "fr-ca": vfq = True - if item['Language'] == "fr-FR": + if lang == "fr-fr": vff = True - if item['Language'] == "en" or item['Language'] == "en-us" or item['Language'] == "en-gb": + if lang in ("en", "en-us", "en-gb"): eng = True - if eng and not vff and not vfq: # vo - obj1.append(1) - - # VO VOSTFR if vff and vfq: obj1.append(4) if vfq: obj1.append(5) if vff: obj1.append(2) + if eng and not vff and not vfq: # vo + obj1.append(1) + - # set quality if meta['is_disc'] == 'BDMV': if meta['resolution'] == '2160p': obj2 = 10 # blu 4k full @@ -99,11 +88,9 @@ async def get_option_tag(self, meta: Meta): else: obj2 = 12 # blu remux - # source dvd elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD", "DVD"): obj2 = 15 - # source bluray elif type == "ENCODE" and source in ("BluRay", "HDDVD"): if meta['resolution'] == '2160p': obj2 = 17 @@ -146,26 +133,34 @@ async def get_option_tag(self, meta: Meta): elif type == "DVDRIP": obj2 = 15 # DVDRIP - # 4klight - # hdlight 1080 - # hdlight 720 - # vcd/vhs + uuid = meta.get('uuid', "").lower() + + if "4klight" in uuid: # and type == "ENCODE" + obj2 = 415 + elif "hdlight" in uuid: # and type == "ENCODE" + if meta['resolution'] == '1080p': + obj2 = 413 + else: + obj2 = 414 + + # vcd/vhs ID= 23 + options_dict = {} options_dict[1] = obj1 + # None check is missing, check for correct data structure. options_dict[2] = [obj2] - # Let's see if it's a tv show + if meta['category'] == 'TV': - # Let's check for season if meta.get('no_season', False) is False: season = str(meta.get('season_int', '')) if season: options_dict[7] = 120 + int(season) # Episode episode = str(meta.get('episode_int', '')) - if episode: + if episode: # Episode 0 check is missing options_dict[6] = 96 + int(episode) else: - # pas d'épisode, on suppose que c'est une saison complete ? + # pas d'épisode, on suppose que c'est une saison complete options_dict[6] = 96 return json.dumps(options_dict) @@ -173,8 +168,7 @@ async def get_option_tag(self, meta: Meta): async def get_name(self, meta: Meta) -> dict[str, str]: type = str(meta.get('type', "")).upper() - title, descr = await fr.get_translation_fr(meta) - alt_title = "" + title, _ = await fr.get_translation_fr(meta) year = str(meta.get('year', "")) manual_year_value = meta.get('manual_year') if manual_year_value is not None and int(manual_year_value) > 0: @@ -198,17 +192,11 @@ async def get_name(self, meta: Meta) -> dict[str, str]: uhd = str(meta.get('uhd', "")) hdr = str(meta.get('hdr', "")).replace('HDR10+', 'HDR10PLUS') hybrid = 'Hybrid' if meta.get('webdv', "") else "" - # if meta.get('manual_episode_title'): - # episode_title = str(meta.get('manual_episode_title', "")) - # elif meta.get('daily_episode_title'): - # episode_title = str(meta.get('daily_episode_title', "")) - # else: - # episode_title = "" video_codec = "" video_encode = "" region = "" dvd_size = "" - if meta.get('is_disc', "") == "BDMV": # Disk + if meta.get('is_disc', "") == "BDMV": video_codec = str(meta.get('video_codec', "")) region = str(meta.get('region', "") or "") elif meta.get('is_disc', "") == "DVD": @@ -231,59 +219,53 @@ async def get_name(self, meta: Meta) -> dict[str, str]: season = '' if meta.get('no_year', False) is True: year = '' - if meta.get('no_aka', False) is True: - alt_title = '' + #if meta.get('no_aka', False) is True: + # alt_title = '' # YAY NAMING FUN name = "" if meta['category'] == "MOVIE": # MOVIE SPECIFIC - if type == "DISC": # Disk + if type == "DISC": if meta['is_disc'] == 'BDMV': name = f"{title} {year} {three_d} {edition} {hybrid} {repack} {language} {resolution} {uhd} {region} {source} {hdr} {audio} {video_codec}" elif meta['is_disc'] == 'DVD': name = f"{title} {year} {repack} {edition} {region} {source} {dvd_size} {audio}" elif meta['is_disc'] == 'HDDVD': name = f"{title} {year} {edition} {repack} {language} {resolution} {source} {video_codec} {audio}" - # BluRay/HDDVD Remux elif type == "REMUX" and source in ("BluRay", "HDDVD"): name = f"{title} {year} {three_d} {edition} {hybrid} {repack} {language} {resolution} {uhd} {source} REMUX {hdr} {audio} {video_codec}" - # DVD Remux elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD", "DVD"): name = f"{title} {year} {edition} {repack} {source} REMUX {audio}" - elif type == "ENCODE": # Encode + elif type == "ENCODE": name = f"{title} {year} {edition} {hybrid} {repack} {language} {resolution} {uhd} {source} {hdr} {audio} {video_encode}" - elif type == "WEBDL": # WEB-DL + elif type == "WEBDL": name = f"{title} {year} {edition} {hybrid} {repack} {language} {resolution} {uhd} {service} WEB {hdr} {audio} {video_encode}" - elif type == "WEBRIP": # WEBRip + elif type == "WEBRIP": name = f"{title} {year} {edition} {hybrid} {repack} {language} {resolution} {uhd} {service} WEBRip {hdr} {audio} {video_encode}" - elif type == "HDTV": # HDTV + elif type == "HDTV": name = f"{title} {year} {edition} {repack} {language} {resolution} {source} {audio} {video_encode}" elif type == "DVDRIP": name = f"{title} {year} {source} {video_encode} DVDRip {audio}" elif meta['category'] == "TV": # TV SPECIFIC - if type == "DISC": # Disk + if type == "DISC": if meta['is_disc'] == 'BDMV': name = f"{title} {year} {season}{episode} {three_d} {edition} {hybrid} {repack} {language} {resolution} {uhd} {region} {source} {hdr} {audio} {video_codec}" if meta['is_disc'] == 'DVD': name = f"{title} {year} {season}{episode}{three_d} {repack} {edition} {region} {source} {dvd_size} {audio}" elif meta['is_disc'] == 'HDDVD': name = f"{title} {year} {edition} {repack} {language} {resolution} {source} {video_codec} {audio}" - # BluRay Remux elif type == "REMUX" and source in ("BluRay", "HDDVD"): - name = f"{title} {year} {season}{episode} {part} {three_d} {edition} {hybrid} {repack} {language} {resolution} {uhd} {source} REMUX {hdr} {audio} {video_codec}" # SOURCE - # DVD Remux + name = f"{title} {year} {season}{episode} {part} {three_d} {edition} {hybrid} {repack} {language} {resolution} {uhd} {source} REMUX {hdr} {audio} {video_codec}" elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD", "DVD"): - # SOURCE name = f"{title} {year} {season}{episode} {part} {edition} {repack} {source} REMUX {audio}" - elif type == "ENCODE": # Encode - # SOURCE + elif type == "ENCODE": name = f"{title} {year} {season}{episode} {part} {edition} {hybrid} {repack} {language} {resolution} {uhd} {source} {hdr} {audio} {video_encode}" - elif type == "WEBDL": # WEB-DL + elif type == "WEBDL": name = f"{title} {year} {season}{episode} {part} {edition} {hybrid} {repack} {language} {resolution} {uhd} {service} WEB {hdr} {audio} {video_encode}" - elif type == "WEBRIP": # WEBRip + elif type == "WEBRIP": name = f"{title} {year} {season}{episode} {part} {edition} {hybrid} {repack} {language} {resolution} {uhd} {service} WEBRip {hdr} {audio} {video_encode}" - elif type == "HDTV": # HDTV + elif type == "HDTV": name = f"{title} {year} {season}{episode} {part} {edition} {repack} {language} {resolution} {source} {audio} {video_encode}" elif type == "DVDRIP": name = f"{title} {year} {season} {source} DVDRip {audio} {video_encode}" @@ -298,11 +280,11 @@ async def get_name(self, meta: Meta) -> dict[str, str]: console.print(f"--source [yellow]{meta['source']}") console.print( "[bold green]If you specified type, try also specifying source") + raise - exit() name_notag = name name = name_notag + tag - name = fr.clean_name(name) + name = await fr.clean_name(name) if meta['debug']: console.log("[cyan]get_name cat/type") @@ -339,7 +321,6 @@ async def get_additional_checks(self, meta: Meta) -> bool: return True async def search_existing(self, meta: dict[str, Any], _) -> list[str]: - dupes: list[str] = [] # Nothing came with the name, we'll look using tmdb_id @@ -401,11 +382,9 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: return dupes + async def upload(self, meta: Meta, _disctype: str) -> bool: - await self.common.create_torrent_for_upload(meta, self.tracker, 'C411') - torrent_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}].torrent" - mediainfo_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt" - # Tmdb infos + # Tmdb infos tmdb_info = {} tmdb_info['id'] = meta.get("tmdb_id","") tmdb_info['title'] = meta.get("title","") @@ -414,29 +393,38 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: tmdb_info['release_date'] = meta.get("release_date","") tmdb_info['runtime'] = meta.get("runtime","") tmdb_info['voteAverage'] = meta.get("vote_average","") - # + if not meta["debug"]: + await self.common.create_torrent_for_upload(meta, self.tracker, 'C411') + torrent_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}].torrent" + mediainfo_file_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt" + headers = { "Authorization": f"Bearer {self.config['TRACKERS'][self.tracker]['api_key'].strip()}"} acm_name = await self.get_name(meta) - dot_name = unidecode.unidecode(acm_name["name"].replace(" ", ".")) + dot_name = acm_name["name"].replace(" ", ".") response = None + async with aiofiles.open(torrent_file_path, 'rb') as f: torrent_bytes = await f.read() async with aiofiles.open(mediainfo_file_path, 'rb') as f: mediainfo_bytes = await f.read() + data: dict[str, Any] = { "title": str(dot_name), "description": await fr.get_desc_full(meta, self.tracker), - "categoryId": str("1"), + "categoryId": "1", "subcategoryId": str(await self.get_subcat_id(meta)), # 1 langue , 2 qualite "options": await self.get_option_tag(meta), # "isExclusive": "Test Upload-Assistant", "uploaderNote": "Upload-Assistant", "tmdbData": json.dumps(tmdb_info) - # "tmdbData": "Test Upload-Assistant", # "rawgData": "Test Upload-Assistant", } + # Place holder for potential improvement + # files={"torrent": ("torrent.torrent", torrent_bytes, "application/x-bittorrent"),"nfo": ("MEDIAINFO.txt", mediainfo_bytes, "text/plain"),} + files = {"torrent": torrent_bytes, "nfo": mediainfo_bytes, } + if meta["debug"] is False: response_data = {} max_retries = 2 @@ -447,8 +435,7 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: try: # noqa: PERF203 async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client: response = await client.post( - url=self.upload_url, files={"torrent": torrent_bytes, - "nfo": mediainfo_bytes, }, data=data, headers=headers + url=self.upload_url, files=files, data=data, headers=headers ) response.raise_for_status() @@ -467,7 +454,6 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: meta["tracker_status"][self.tracker]["status_message"] = ( await self.process_response_data(response_data) ) - # response_data = {'success': True, 'data': {'id': 6216, 'infoHash': '35faeb2c08d7d7448da7c7afd4048f16b02cc4ad', 'status': 'pending'}, 'message': 'Torrent envoyé ! Il sera visible après validation par la Team Pending.'} torrent_hash = response_data["data"]["infoHash"] meta["tracker_status"][self.tracker]["torrent_id"] = torrent_hash @@ -563,6 +549,8 @@ async def download_torrent(self, meta: dict[str, Any], torrent_hash: str, ) -> N "id": torrent_hash, "apikey": self.config['TRACKERS'][self.tracker]['api_key'].strip(), } + + # https://c411.org/api/?t=get&id={{infoHash}}&apikey={{config.API_KEY}} try: async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client: r = await client.get(self.torrent_url, params=params) @@ -580,7 +568,6 @@ async def download_torrent(self, meta: dict[str, Any], torrent_hash: str, ) -> N console.print( "[yellow]Download manually from the tracker.[/yellow]") return None - return None async def process_response_data(self, response_data: dict[str, Any]) -> str: """Returns the success message from the response data as a string.""" @@ -591,4 +578,4 @@ async def process_response_data(self, response_data: dict[str, Any]) -> str: error_msg = response_data.get("message", "") if error_msg: return f"API response: {error_msg}" - return f"API response: {response_data}" + return f"API response: {response_data}" \ No newline at end of file From 24a28b4f823598d8bc57f96942cad01d9f6b6a8e Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Thu, 12 Feb 2026 10:13:49 +0100 Subject: [PATCH 13/30] Integration code Ravagerr --- src/trackers/FRENCH.py | 228 ++++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 106 deletions(-) diff --git a/src/trackers/FRENCH.py b/src/trackers/FRENCH.py index 529a67f87..313030fef 100644 --- a/src/trackers/FRENCH.py +++ b/src/trackers/FRENCH.py @@ -1,18 +1,18 @@ -from typing import Any, Optional, cast +# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0 +# https://github.com/Audionut/Upload-Assistant/tree/master + import aiofiles -import re import httpx +from typing import Any, Optional, cast from data.config import config -from src.console import console from unidecode import unidecode -# UA 7.0.0 - +#from src.console import console async def build_audio_string(meta: dict[str, Any]) -> str: # Priority Order: - # 1. MULYi: Exactly 2 audio tracks - # 2. MULTI: 3 audio tracks + # 1. MULYi: Exactly 2 audio tracks Dual would be nice + # 2. MULTi: 3 audio tracks # 3. VOSTFR: Single audio (original lang) + French subs + NO French audio # 4. VO: Single audio (original lang) + NO French subs + NO French audio @@ -39,24 +39,19 @@ async def build_audio_string(meta: dict[str, Any]) -> str: language = "MULTi" # VOSTFR - Single audio (original) + French subs + NO French audio - if num_audio_tracks == 1 and original_lang and not has_french_audio and has_French_subs: - if audio_langs[0] == original_lang: - language = "VOSTFR" + if num_audio_tracks == 1 and original_lang and not has_french_audio and has_French_subs and audio_langs[0] == original_lang: + language = "VOSTFR" # VO - Single audio (original) + NO French subs + NO French audio - if num_audio_tracks == 1 and original_lang and not has_french_audio and not has_French_subs: - if audio_langs[0] == original_lang: - language = "VO" + if num_audio_tracks == 1 and original_lang and not has_french_audio and not has_French_subs and audio_langs[0] == original_lang: + language = "VO" # FRENCH. - Single audio FRENCH - if num_audio_tracks == 1 and has_french_audio: - if audio_langs[0] == original_lang: - language = "FRENCH" + if num_audio_tracks == 1 and has_french_audio and audio_langs[0] == original_lang: + language = "FRENCH" return language -# VOF ,VOQ si le pays dorigine est la meme langue - async def get_extra_french_tag(meta: dict[str, Any], check_origin: bool) -> str: audio_track = await get_audio_tracks(meta, True) @@ -65,17 +60,14 @@ async def get_extra_french_tag(meta: dict[str, Any], check_origin: bool) -> str: vff = "" vf = "" origincountry = meta.get("origin_country", "") - - for i, item in enumerate(audio_track): - try: - title = item.get("Title", "").lower() - except: - title = '' + + for _, item in enumerate(audio_track): + title = (item.get("Title") or "").lower() lang = item.get('Language', "").lower() if lang == "fr-ca" or "vfq" in title: vfq = True - elif lang == "fr-fr"or "vff" in title: + elif lang == "fr-fr" or "vff" in title: vff = True elif lang == "fr" or "vfi" in title: vf = True @@ -102,7 +94,7 @@ async def get_extra_french_tag(meta: dict[str, Any], check_origin: bool) -> str: async def get_audio_tracks(meta: dict[str, Any], filter: bool) -> list[dict[str, Any]]: - """Extract audio tracks from mediainfo""" + if 'mediainfo' not in meta or 'media' not in meta['mediainfo']: return [] @@ -126,8 +118,8 @@ async def get_audio_tracks(meta: dict[str, Any], filter: bool) -> list[dict[str, track_dict = cast(dict[str, Any], track) if track_dict.get('@type') == 'Audio': if filter: - #or not "audio description" in str(track_dict.get('Title') or '').lower() #audio description - if not "commentary" in str(track_dict.get('Title') or '').lower(): + # or not "audio description" in str(track_dict.get('Title') or '').lower() #audio description, AD, description + if "commentary" not in str(track_dict.get('Title') or '').lower(): audio_tracks.append(track_dict) else: audio_tracks.append(track_dict) @@ -136,7 +128,7 @@ async def get_audio_tracks(meta: dict[str, Any], filter: bool) -> list[dict[str, async def get_subtitle_tracks(meta: dict[str, Any]) -> list[dict[str, Any]]: - """Extract audio tracks from mediainfo""" + if 'mediainfo' not in meta or 'media' not in meta['mediainfo']: return [] @@ -165,7 +157,7 @@ async def get_subtitle_tracks(meta: dict[str, Any]) -> list[dict[str, Any]]: async def get_video_tracks(meta: dict[str, Any]) -> list[dict[str, Any]]: - """Extract audio tracks from mediainfo""" + if 'mediainfo' not in meta or 'media' not in meta['mediainfo']: return [] @@ -194,7 +186,7 @@ async def get_video_tracks(meta: dict[str, Any]) -> list[dict[str, Any]]: async def extract_audio_languages(audio_tracks: list[dict[str, Any]], meta: dict[str, Any]) -> list[str]: - """Extract and normalize audio languages""" + audio_langs: list[str] = [] for track in audio_tracks: @@ -217,7 +209,6 @@ async def extract_audio_languages(audio_tracks: list[dict[str, Any]], meta: dict async def map_language(lang: str) -> str: - """Map language codes and names""" if not lang: return '' @@ -249,7 +240,7 @@ async def map_language(lang: str) -> str: async def get_original_language(meta: dict[str, Any]) -> Optional[str]: - """Get the original language from existing metadata""" + original_lang = None if meta.get('original_language'): @@ -282,7 +273,7 @@ async def get_original_language(meta: dict[str, Any]) -> Optional[str]: async def has_french_subs(meta: dict[str, Any]) -> bool: - """Check if torrent has Spanish subtitles""" + if 'mediainfo' not in meta or 'media' not in meta['mediainfo']: return False media_info = meta['mediainfo'] @@ -335,7 +326,6 @@ async def map_audio_codec(audio_track: dict[str, Any]) -> str: async def get_audio_channels(audio_track: dict[str, Any]) -> str: - """Get audio channel configuration""" channels = audio_track.get('Channels', '') channel_map = { '1': 'Mono', '2': '2.0', '3': '3.0', @@ -348,10 +338,11 @@ async def get_audio_name(meta: dict[str, Any]) -> str: audio_track = await get_audio_tracks(meta, True) if not audio_track: return "" - has_french_audio = "fr" in audio_track or "fr-fr" in audio_track or "fr-ca" in audio_track + has_french_audio = any(item.get('Language', '') in ( + 'fr', 'fr-fr', 'fr-ca')for item in audio_track) audio_parts: list[str] = [] if has_french_audio: - for i, item in enumerate(audio_track): + for _, item in enumerate(audio_track): if item['Language'] == "fr" or item['Language'] == "fr-fr" or item['Language'] == "fr-ca": codec = await map_audio_codec(item) channels = await get_audio_channels(item) @@ -359,8 +350,8 @@ async def get_audio_name(meta: dict[str, Any]) -> str: audio = ' '.join(audio_parts) return audio else: - for i, item in enumerate(audio_track): - if item['Default'] == "Yes": + for _, item in enumerate(audio_track): + if item.get('Default') == "Yes": codec = await map_audio_codec(item) channels = await get_audio_channels(item) audio_parts.append(f"{codec} {channels}") @@ -376,7 +367,7 @@ async def translate_genre(text: str) -> str: 'Fantasy': 'Fantastique', 'History': 'Histoire', 'Horror': 'Horreur', - 'Music ': 'Musique', + 'Music': 'Musique', 'Romance': 'Romance', 'Science Fiction': 'Science-fiction', 'TV Movie': 'Téléfilm', @@ -395,6 +386,7 @@ async def translate_genre(text: str) -> str: 'Reality': 'Réalité', 'Sci-Fi & Fantasy': 'Science-fiction & fantastique', 'Soap': 'Feuilletons', + 'Sport': 'Sport', 'Talk': 'Débats', 'War & Politics': 'Guerre & politique', 'Western': 'Western' @@ -410,9 +402,9 @@ async def translate_genre(text: str) -> str: return ", ".join(result) -def clean_name(input_str: str) -> str: +async def clean_name(input_str: str) -> str: ascii_str = unidecode(input_str) - invalid_char = set('<>"/\\|?*') #! . , : ; @ # $ % ^ & */ \" '_ + invalid_char = set('<>"/\\|?*') # ! . , : ; @ # $ % ^ & */ \" '_ result = [] for char in ascii_str: if char in invalid_char: @@ -423,7 +415,7 @@ def clean_name(input_str: str) -> str: async def get_translation_fr(meta: dict[str, Any]) -> tuple[str, str]: - """Get Spanish title if available and configured""" + fr_title = meta.get("frtitle") fr_overwiew = meta.get("froverview") if fr_title and fr_overwiew: @@ -454,17 +446,17 @@ async def get_translation_fr(meta: dict[str, Any]) -> tuple[str, str]: tmdb_id = int(meta["tmdb_id"]) category = str(meta["category"]) tmdb_title, tmdb_overview = await get_tmdb_translations(tmdb_id, category, "fr") - meta["frtitle"] = tmdb_title or tmdb_title + meta["frtitle"] = french_title or tmdb_title meta["froverview"] = tmdb_overview return french_title if french_title is not None else tmdb_title, tmdb_overview async def get_tmdb_translations(tmdb_id: int, category: str, target_language: str) -> tuple[str, str]: - """Get translations from TMDb API""" + endpoint = "movie" if category == "MOVIE" else "tv" url = f"https://api.themoviedb.org/3/{endpoint}/{tmdb_id}/translations" tmdb_api_key = config['DEFAULT'].get('tmdb_api', False) - async with httpx.AsyncClient() as client: + async with httpx.AsyncClient(timeout=30) as client: try: response = await client.get(url, params={"api_key": tmdb_api_key}) response.raise_for_status() @@ -481,7 +473,7 @@ async def get_tmdb_translations(tmdb_id: int, category: str, target_language: st return translated_title or "", translated_desc or "" return "", "" - except Exception as e: + except Exception: return "", "" # unknow return type @@ -490,15 +482,28 @@ async def get_tmdb_translations(tmdb_id: int, category: str, target_language: st async def get_desc_full(meta: dict[str, Any], tracker) -> str: video_track = await get_video_tracks(meta) - mbps = int(video_track[0]['BitRate']) / 1_000_000 + if not video_track: + return '' + mbps = 0.0 + if video_track and video_track[0].get('BitRate'): + try: + mbps = int(video_track[0]['BitRate']) / 1_000_000 + except (ValueError, TypeError): + pass title, description = await get_translation_fr(meta) genre = await translate_genre(meta['combined_genres']) audio_tracks = await get_audio_tracks(meta, False) + if not audio_tracks: + return '' subtitle_tracks = await get_subtitle_tracks(meta) + if not subtitle_tracks: + return '' + size_bytes = int(meta.get('source_size') or 0) + size_gib = size_bytes / (1024 ** 3) poster = str(meta.get('poster', "")) year = str(meta.get('year', "")) original_title = str(meta.get('original_title', "")) - Pays = str(meta['imdb_info']['country']) + pays = str(meta.get('imdb_info', {}).get('country', '')) release_date = str(meta.get('release_date', "")) video_duration = str(meta.get('video_duration', "")) source = str(meta.get('source', "")) @@ -507,6 +512,16 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: container = str(meta.get('container', "")) video_codec = str(meta.get('video_codec', "")) hdr = str(meta.get('hdr', "")) + if "DV" in hdr: + if video_track and video_track[0].get('HDR_Format_Profile'): + try: + dv = str(video_track[0]['HDR_Format_Profile']).replace('dvhe.0', '').replace('/', '').strip() + hdr = hdr.replace('DV', '') + hdr = f"{hdr} DV{dv}" + + except (ValueError, TypeError): + pass + tag = str(meta.get('tag', "")).replace('-', '') service_longname = str(meta.get('service_longname', "")) season = str(meta.get('season_int', '')) @@ -520,7 +535,7 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: desc_parts.append( f"[b][font=Verdana][color=#3d85c6][size=29]{title}[/size][/font]") desc_parts.append(f"[size=18]{year}[/size][/color][/b]") - + if meta['category'] == "TV": season = f"S{season}" if season else "" episode = f"E{episode}" if episode else "" @@ -529,7 +544,7 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: desc_parts.append( f"[font=Verdana][size=13][b][color=#3d85c6]Titre original :[/color][/b] [i]{original_title}[/i][/size][/font]") desc_parts.append( - f"[b][color=#3d85c6]Pays :[/color][/b] [i]{Pays}[/i]") + f"[b][color=#3d85c6]Pays :[/color][/b] [i]{pays}[/i]") desc_parts.append(f"[b][color=#3d85c6]Genres :[/color][/b] [i]{genre}[/i]") desc_parts.append( f"[b][color=#3d85c6]Date de sortie :[/color][/b] [i]{release_date}[/i]") @@ -539,31 +554,35 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: f"[b][color=#3d85c6]Durée :[/color][/b] [i]{video_duration} Minutes[/i]") if meta['imdb_id']: - desc_parts.append( f"{meta.get('imdb_info', {}).get('imdb_url', '')}") + desc_parts.append(f"[url={meta.get('imdb_info', {}).get('imdb_url', '')}]IMDb[/url]") if meta['tmdb']: - desc_parts.append( f"\nhttps://www.themoviedb.org/{str(meta['category'].lower())}/{str(meta['tmdb'])}") + desc_parts.append( + f"[url=https://www.themoviedb.org/{str(meta['category'].lower())}/{str(meta['tmdb'])}]TMDB[/url]") if meta['tvdb_id']: - desc_parts.append( f"\nhttps://www.thetvdb.com/?id={str(meta['tvdb_id'])}&tab=series") + desc_parts.append( + f"[url=https://www.thetvdb.com/?id={str(meta['tvdb_id'])}&tab=series]TVDB[/url]") if meta['tvmaze_id']: - desc_parts.append( f"\nhttps://www.tvmaze.com/shows/{str(meta['tvmaze_id'])}") + desc_parts.append( + f"[url=https://www.tvmaze.com/shows/{str(meta['tvmaze_id'])}]TVmaze[/url]") if meta['mal_id']: - desc_parts.append( f"\nhttps://myanimelist.net/anime/{str(meta['mal_id'])}") - - desc_parts.append(f"[img]https://i.imgur.com/W3pvv6q.png[/img]") + desc_parts.append( + f"[url=https://myanimelist.net/anime/{str(meta['mal_id'])}]MyAnimeList[/url]") + + desc_parts.append("[img]https://i.imgur.com/W3pvv6q.png[/img]") desc_parts.append(f"{description}") - desc_parts.append(f"[img]https://i.imgur.com/KMZsqZn.png[/img]") + desc_parts.append("[img]https://i.imgur.com/KMZsqZn.png[/img]") - #if meta.get('is_disc', '') == 'DVD': + # if meta.get('is_disc', '') == 'DVD': # desc_parts.append(f'[hide=DVD MediaInfo][pre]{await builder.get_mediainfo_section(meta)}[/pre][/hide]') - #bd_info = await builder.get_bdinfo_section(meta) - #if bd_info: + # bd_info = await builder.get_bdinfo_section(meta) + # if bd_info: # desc_parts.append(f'[hide=BDInfo][pre]{bd_info}[/pre][/hide]') # User description - #desc_parts.append(await builder.get_user_description(meta)) + # desc_parts.append(await builder.get_user_description(meta)) desc_parts.append( f"[b][color=#3d85c6]Source :[/color][/b] [i]{source} {service_longname}[/i]") @@ -580,52 +599,50 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: desc_parts.append( f"[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{mbps:.2f} MB/s[/i]") - desc_parts.append(f"[b][color=#3d85c6] Audio(s) :[/color][/b]") + desc_parts.append("[b][color=#3d85c6] Audio(s) :[/color][/b]") for obj in audio_tracks: - kbps = int(obj['BitRate']) / 1_000 - - flags = [] - if obj.get("Forced") == "Yes": - flags.append("Forced") - if obj.get("Default") == "Yes": - flags.append("Default") - if "commentary" in str(obj.get('Title')).lower(): - flags.append("Commentary") - if " ad" in str(obj.get('Title')).lower(): - flags.append("Audio Description") - - line = f"{obj['Language']} / {obj['Format']} / {obj['Channels']}ch / {kbps:.2f}KB/s" - if flags: - line += " / " + " / ".join(flags) - desc_parts.append(line) - - # desc_parts.append(f"{obj['Language']} / {obj['Format']} / {obj['Channels']}ch / {kbps}KB/s") - - desc_parts.append(f"[b][color=#3d85c6]Sous-titres :[/color][/b]") - for obj in subtitle_tracks: - - flags = [] - if obj.get("Forced") == "Yes": - flags.append("Forced") - if obj.get("Default") == "Yes": - flags.append("Default") - line = f"{obj['Language']} / {obj['Format']}" - if flags: - line += " / " + " / ".join(flags) - desc_parts.append(line) + if isinstance(obj, dict): + bitrate = obj.get('BitRate') + kbps = int(bitrate) / 1_000 if bitrate else 0 + + flags = [] + if obj.get("Forced") == "Yes": + flags.append("Forced") + if obj.get("Default") == "Yes": + flags.append("Default") + if "commentary" in str(obj.get('Title')).lower(): + flags.append("Commentary") + if " ad" in str(obj.get('Title')).lower(): + flags.append("Audio Description") + + line = f"{obj['Language']} / {obj['Format']} / {obj['Channels']}ch / {kbps:.2f}KB/s" + if flags: + line += " / " + " / ".join(flags) + desc_parts.append(line) + else: + desc_parts.append(f"*{obj}*") - # desc_parts.append(f" {obj['Language']} / {obj['Format']} / Forced:{obj['Forced']} / Default:{obj['Default']}") + desc_parts.append("[b][color=#3d85c6]Sous-titres :[/color][/b]") + for obj in subtitle_tracks: + if isinstance(obj, dict): + flags = [] + if obj.get("Forced") == "Yes": + flags.append("Forced") + if obj.get("Default") == "Yes": + flags.append("Default") + line = f"{obj['Language']} / {obj['Format']}" + if flags: + line += " / " + " / ".join(flags) + desc_parts.append(line) + else: + desc_parts.append(f"*{obj}*") # desc_parts.append(f"[img]https://i.imgur.com/KFsABlN.png[/img]") - desc_parts.append( - f"[b][color=#3d85c6]Team :[/color][/b] [i]{tag}[/i] ") - # desc_parts.append(f"[b][color=#3d85c6] Taille totale :[/color][/b] {gb} GB") - + desc_parts.append(f"[b][color=#3d85c6]Team :[/color][/b] [i]{tag}[/i]") + desc_parts.append(f"[b][color=#3d85c6] Taille totale :[/color][/b] {size_gib:.2f} GB") + # desc_parts.append(f"[b][color=#3d85c6] Nombre de fichier :[/color][/b]") # Screenshots - if f'{tracker}_images_key' in meta: - images = meta[f'{tracker}_images_key'] - else: - images = meta['image_list'] + images = meta[f'{tracker}_images_key'] if f'{tracker}_images_key' in meta else meta['image_list'] if images: screenshots_block = '' for image in images: @@ -641,5 +658,4 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{tracker}]DESCRIPTION.json", 'w', encoding='utf-8') as description_file: await description_file.write(description) - return description - + return description \ No newline at end of file From 41d41dd65940e991a3feb61a2efb128c3c8fd2bf Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Thu, 12 Feb 2026 12:03:04 +0100 Subject: [PATCH 14/30] add .idea to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 44a37d5a5..ad223aeec 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ data/*.json venv/* .pyright web_ui/static/js/node_modules/ +.idea/* + From c35b91fdefaa78d6b3101e19c0beb09a692ce972 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Thu, 12 Feb 2026 17:10:50 +0100 Subject: [PATCH 15/30] add payload file --- src/trackers/C411.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 8516073a6..08694beb9 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -421,6 +421,9 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: "tmdbData": json.dumps(tmdb_info) # "rawgData": "Test Upload-Assistant", } + async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/c411_payload.json", 'w', encoding='utf-8') as f: + await f.write(json.dumps(data, indent=4)) + # Place holder for potential improvement # files={"torrent": ("torrent.torrent", torrent_bytes, "application/x-bittorrent"),"nfo": ("MEDIAINFO.txt", mediainfo_bytes, "text/plain"),} files = {"torrent": torrent_bytes, "nfo": mediainfo_bytes, } From 6961d46fe3f3a239693e8681dd5c3aa89713614f Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Fri, 13 Feb 2026 10:10:54 +0100 Subject: [PATCH 16/30] add new check for anime --- src/trackers/C411.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 08694beb9..07d51d5be 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -36,10 +36,11 @@ def __init__(self, config: Config) -> None: async def get_subcat_id(self, meta: Meta) -> str: sub_cat_id = "0" - + genres = meta.get("genres","").lower().replace(' ', '').replace('-', '') if meta['category'] == 'MOVIE': sub_cat_id = '1' if meta.get('mal_id') else '6' - + if 'animation' in genres: + sub_cat_id = '6' elif meta['category'] == 'TV': sub_cat_id = '2' if meta.get('mal_id') else '7' From cc74d351f8bd84f3a1e3034b745610e04a60580e Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Tue, 17 Feb 2026 10:12:26 +0100 Subject: [PATCH 17/30] bugfix if no subtitle --- src/trackers/C411.py | 7 +++--- src/trackers/FRENCH.py | 49 ++++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 07d51d5be..07b219999 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -385,11 +385,12 @@ async def search_existing(self, meta: dict[str, Any], _) -> list[str]: async def upload(self, meta: Meta, _disctype: str) -> bool: + description = await fr.get_desc_full(meta, self.tracker) # Tmdb infos tmdb_info = {} tmdb_info['id'] = meta.get("tmdb_id","") tmdb_info['title'] = meta.get("title","") - tmdb_info['originalTitle'] = meta.get("origial_title","") + tmdb_info['originalTitle'] = meta.get("original_title","") tmdb_info['overview'] = meta.get("overview","") tmdb_info['release_date'] = meta.get("release_date","") tmdb_info['runtime'] = meta.get("runtime","") @@ -401,8 +402,8 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: headers = { "Authorization": f"Bearer {self.config['TRACKERS'][self.tracker]['api_key'].strip()}"} - acm_name = await self.get_name(meta) - dot_name = acm_name["name"].replace(" ", ".") + c411_name = await self.get_name(meta) + dot_name = c411_name["name"].replace(" ", ".") response = None async with aiofiles.open(torrent_file_path, 'rb') as f: diff --git a/src/trackers/FRENCH.py b/src/trackers/FRENCH.py index 313030fef..750bca0e0 100644 --- a/src/trackers/FRENCH.py +++ b/src/trackers/FRENCH.py @@ -448,7 +448,11 @@ async def get_translation_fr(meta: dict[str, Any]) -> tuple[str, str]: tmdb_title, tmdb_overview = await get_tmdb_translations(tmdb_id, category, "fr") meta["frtitle"] = french_title or tmdb_title meta["froverview"] = tmdb_overview - return french_title if french_title is not None else tmdb_title, tmdb_overview + + if french_title is not None: + return french_title, tmdb_overview + else: + return tmdb_title, tmdb_overview async def get_tmdb_translations(tmdb_id: int, category: str, target_language: str) -> tuple[str, str]: @@ -480,7 +484,6 @@ async def get_tmdb_translations(tmdb_id: int, category: str, target_language: st async def get_desc_full(meta: dict[str, Any], tracker) -> str: - video_track = await get_video_tracks(meta) if not video_track: return '' @@ -496,8 +499,8 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: if not audio_tracks: return '' subtitle_tracks = await get_subtitle_tracks(meta) - if not subtitle_tracks: - return '' + #if not subtitle_tracks: + # return '' size_bytes = int(meta.get('source_size') or 0) size_gib = size_bytes / (1024 ** 3) poster = str(meta.get('poster', "")) @@ -526,7 +529,6 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: service_longname = str(meta.get('service_longname', "")) season = str(meta.get('season_int', '')) episode = str(meta.get('episode_int', '')) - desc_parts = [] # if meta['logo']: # desc_parts.append(f"[img]{meta['logo']}[/img]") @@ -598,7 +600,6 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: f"[b][color=#3d85c6]Codec vidéo :[/color][/b] [i]{video_codec} {hdr}[/i]") desc_parts.append( f"[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{mbps:.2f} MB/s[/i]") - desc_parts.append("[b][color=#3d85c6] Audio(s) :[/color][/b]") for obj in audio_tracks: if isinstance(obj, dict): @@ -622,20 +623,22 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: else: desc_parts.append(f"*{obj}*") - desc_parts.append("[b][color=#3d85c6]Sous-titres :[/color][/b]") - for obj in subtitle_tracks: - if isinstance(obj, dict): - flags = [] - if obj.get("Forced") == "Yes": - flags.append("Forced") - if obj.get("Default") == "Yes": - flags.append("Default") - line = f"{obj['Language']} / {obj['Format']}" - if flags: - line += " / " + " / ".join(flags) - desc_parts.append(line) - else: - desc_parts.append(f"*{obj}*") + + if subtitle_tracks: + desc_parts.append("[b][color=#3d85c6]Sous-titres :[/color][/b]") + for obj in subtitle_tracks: + if isinstance(obj, dict): + flags = [] + if obj.get("Forced") == "Yes": + flags.append("Forced") + if obj.get("Default") == "Yes": + flags.append("Default") + line = f"{obj['Language']} / {obj['Format']}" + if flags: + line += " / " + " / ".join(flags) + desc_parts.append(line) + else: + desc_parts.append(f"*{obj}*") # desc_parts.append(f"[img]https://i.imgur.com/KFsABlN.png[/img]") desc_parts.append(f"[b][color=#3d85c6]Team :[/color][/b] [i]{tag}[/i]") @@ -648,14 +651,14 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: for image in images: screenshots_block += f"[img]{image['raw_url']}[/img]\n" desc_parts.append(screenshots_block) - + # Signature desc_parts.append( f"[url=https://github.com/Audionut/Upload-Assistant]{meta['ua_signature']}[/url]") - description = '\n'.join(part for part in desc_parts if part.strip()) async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{tracker}]DESCRIPTION.json", 'w', encoding='utf-8') as description_file: await description_file.write(description) + - return description \ No newline at end of file + return description From 83552f09791ea9bc52bac52bae828bd0d3725c30 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Tue, 17 Feb 2026 10:56:46 +0100 Subject: [PATCH 18/30] bugfix on tmdb info --- src/trackers/C411.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 07b219999..00af7c81c 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -410,8 +410,7 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: torrent_bytes = await f.read() async with aiofiles.open(mediainfo_file_path, 'rb') as f: mediainfo_bytes = await f.read() - - data: dict[str, Any] = { + data = { "title": str(dot_name), "description": await fr.get_desc_full(meta, self.tracker), "categoryId": "1", @@ -420,7 +419,7 @@ async def upload(self, meta: Meta, _disctype: str) -> bool: "options": await self.get_option_tag(meta), # "isExclusive": "Test Upload-Assistant", "uploaderNote": "Upload-Assistant", - "tmdbData": json.dumps(tmdb_info) + "tmdbData": str(tmdb_info), # "rawgData": "Test Upload-Assistant", } async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/c411_payload.json", 'w', encoding='utf-8') as f: From 3b1ce2c0a5426c4bd51176ac9d6ca3be443ff146 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Tue, 17 Feb 2026 11:05:47 +0100 Subject: [PATCH 19/30] replace json.dumps with str --- src/trackers/C411.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 00af7c81c..eaa278bc1 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -163,7 +163,7 @@ async def get_option_tag(self, meta: Meta): else: # pas d'épisode, on suppose que c'est une saison complete options_dict[6] = 96 - return json.dumps(options_dict) + return str(options_dict) # https://c411.org/wiki/nommage async def get_name(self, meta: Meta) -> dict[str, str]: From da7060005bd008401fa080a95d24210d456e9ba3 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Wed, 18 Feb 2026 08:46:10 +0100 Subject: [PATCH 20/30] small bugfixes --- src/trackers/C411.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index eaa278bc1..9b60af6e9 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -62,6 +62,10 @@ async def get_option_tag(self, meta: Meta): vfq = True if lang == "fr-fr": vff = True + if lang == 'french': + vff = True + if lang == 'fr': + vff = True if lang in ("en", "en-us", "en-gb"): eng = True @@ -163,7 +167,7 @@ async def get_option_tag(self, meta: Meta): else: # pas d'épisode, on suppose que c'est une saison complete options_dict[6] = 96 - return str(options_dict) + return json.dumps(options_dict) # https://c411.org/wiki/nommage async def get_name(self, meta: Meta) -> dict[str, str]: From 111fc7bd76c54f1123a2cd626c2faa5495a480a4 Mon Sep 17 00:00:00 2001 From: Merrick Date: Thu, 19 Feb 2026 14:18:46 +0100 Subject: [PATCH 21/30] use template --- data/templates/FRENCH.txt | 56 +++++++++ src/trackers/C411.py | 2 +- src/trackers/FRENCH.py | 257 +++++++++++++++++++++++--------------- 3 files changed, 214 insertions(+), 101 deletions(-) create mode 100644 data/templates/FRENCH.txt diff --git a/data/templates/FRENCH.txt b/data/templates/FRENCH.txt new file mode 100644 index 000000000..e7473628d --- /dev/null +++ b/data/templates/FRENCH.txt @@ -0,0 +1,56 @@ +[img]{{ poster }}[/img] + +[b][font=Verdana][color=#3d85c6][size=29]{{ title }}[/size][/font] +[size=18]{{ year }}[/size][/color][/b] + +{% if category == "TV" %} +[b][size=18]S{{ season }}E{{ episode }}[/size][/b] +{% endif %} + +[font=Verdana][size=13][b][color=#3d85c6]Titre original :[/color][/b] [i]{{ original_title }}[/i][/size][/font] +[b][color=#3d85c6]Pays :[/color][/b] [i]{{ pays }}[/i] +[b][color=#3d85c6]Genres :[/color][/b] [i]{{ genre }}[/i] +[b][color=#3d85c6]Date de sortie :[/color][/b] [i]{{ release_date }}[/i] + +{% if category == 'MOVIE' %} +[b][color=#3d85c6]Durée :[/color][/b] [i]{{ video_duration }} Minutes[/i] +{% endif %} + +{% if imdb_url %}[url={{ imdb_url }}]IMDb[/url]{% endif %} +{% if tmdb %}[url=https://www.themoviedb.org/{{ category.lower() }}/{{ tmdb }}]TMDB[/url]{% endif %} +{% if tvdb_id %}[url=https://www.thetvdb.com/?id={{ tvdb_id }}&tab=series]TVDB[/url]{% endif %} +{% if tvmaze_id %}[url=https://www.tvmaze.com/shows/{{ tvmaze_id }}]TVmaze[/url]{% endif %} +{% if mal_id %}[url=https://myanimelist.net/anime/{{ mal_id }}]MyAnimeList[/url]{% endif %} + +[img]https://i.imgur.com/W3pvv6q.png[/img] + +{{ description }} + +[img]https://i.imgur.com/KMZsqZn.png[/img] + +[b][color=#3d85c6]Source :[/color][/b] [i]{{ source }} {{ service_longname }}[/i] +[b][color=#3d85c6]Type :[/color][/b] [i]{{ type }}[/i] +[b][color=#3d85c6]Résolution vidéo :[/color][/b][i]{{ resolution }}[/i] +[b][color=#3d85c6]Format vidéo :[/color][/b] [i]{{ container }}[/i] +[b][color=#3d85c6]Codec vidéo :[/color][/b] [i]{{ video_codec }} {{ hdr }}[/i] +[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{{ mbps:.2f }} MB/s[/i] + +[b][color=#3d85c6] Audio(s) :[/color][/b] +{% for line in audio_lines %} +{{ line }} +{% endfor %} + +{% if subtitle_lines %}[b][color=#3d85c6]Sous-titres :[/color][/b] +{% for line in subtitle_lines %} +{{ line }} +{% endfor %} +{% endif %} + +[b][color=#3d85c6]Team :[/color][/b] [i]{{ tag }}[/i] +[b][color=#3d85c6] Taille totale :[/color][/b] {{ size_gib:.2f }} GB + +{% if images %}{% for image in images %} +[img]{{ image['raw_url'] }}[/img] +{% endfor %}{% endif %} + +[url=https://github.com/Audionut/Upload-Assistant]{{ signature }}[/url] \ No newline at end of file diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 9b60af6e9..242ed2799 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -1,4 +1,4 @@ -# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0 + # Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0 # https://github.com/Audionut/Upload-Assistant/tree/master import aiofiles diff --git a/src/trackers/FRENCH.py b/src/trackers/FRENCH.py index 750bca0e0..7c9d2a41d 100644 --- a/src/trackers/FRENCH.py +++ b/src/trackers/FRENCH.py @@ -484,23 +484,41 @@ async def get_tmdb_translations(tmdb_id: int, category: str, target_language: st async def get_desc_full(meta: dict[str, Any], tracker) -> str: + """Return a full tracker description. + + The function used to build the description piece by piece, but now we prefer + to render a Jinja2 template. A few points: + + * If ``meta['description_template']`` is set it will be used first. + * Otherwise a default template named after the tracker (e.g. ``C411``) is + looked up under ``data/templates``. A generic ``FRENCH`` template is also + provided for shared structure. + * If no template is found we fall back to the original hard‑coded logic so + existing behaviour remains unchanged. + """ + import os + from jinja2 import Template + + # gather information that will be useful to both the template and the + # legacy builder video_track = await get_video_tracks(meta) if not video_track: return '' + mbps = 0.0 if video_track and video_track[0].get('BitRate'): try: mbps = int(video_track[0]['BitRate']) / 1_000_000 except (ValueError, TypeError): pass + title, description = await get_translation_fr(meta) genre = await translate_genre(meta['combined_genres']) audio_tracks = await get_audio_tracks(meta, False) if not audio_tracks: return '' + subtitle_tracks = await get_subtitle_tracks(meta) - #if not subtitle_tracks: - # return '' size_bytes = int(meta.get('source_size') or 0) size_gib = size_bytes / (1024 ** 3) poster = str(meta.get('poster', "")) @@ -521,7 +539,6 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: dv = str(video_track[0]['HDR_Format_Profile']).replace('dvhe.0', '').replace('/', '').strip() hdr = hdr.replace('DV', '') hdr = f"{hdr} DV{dv}" - except (ValueError, TypeError): pass @@ -529,84 +546,15 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: service_longname = str(meta.get('service_longname', "")) season = str(meta.get('season_int', '')) episode = str(meta.get('episode_int', '')) - desc_parts = [] - # if meta['logo']: - # desc_parts.append(f"[img]{meta['logo']}[/img]") - desc_parts.append(f"[img]{poster}[/img]") - - desc_parts.append( - f"[b][font=Verdana][color=#3d85c6][size=29]{title}[/size][/font]") - desc_parts.append(f"[size=18]{year}[/size][/color][/b]") - - if meta['category'] == "TV": - season = f"S{season}" if season else "" - episode = f"E{episode}" if episode else "" - desc_parts.append(f"[b][size=18]{season}{episode}[/size][/b]") - - desc_parts.append( - f"[font=Verdana][size=13][b][color=#3d85c6]Titre original :[/color][/b] [i]{original_title}[/i][/size][/font]") - desc_parts.append( - f"[b][color=#3d85c6]Pays :[/color][/b] [i]{pays}[/i]") - desc_parts.append(f"[b][color=#3d85c6]Genres :[/color][/b] [i]{genre}[/i]") - desc_parts.append( - f"[b][color=#3d85c6]Date de sortie :[/color][/b] [i]{release_date}[/i]") - - if meta['category'] == 'MOVIE': - desc_parts.append( - f"[b][color=#3d85c6]Durée :[/color][/b] [i]{video_duration} Minutes[/i]") - - if meta['imdb_id']: - desc_parts.append(f"[url={meta.get('imdb_info', {}).get('imdb_url', '')}]IMDb[/url]") - if meta['tmdb']: - desc_parts.append( - f"[url=https://www.themoviedb.org/{str(meta['category'].lower())}/{str(meta['tmdb'])}]TMDB[/url]") - if meta['tvdb_id']: - desc_parts.append( - f"[url=https://www.thetvdb.com/?id={str(meta['tvdb_id'])}&tab=series]TVDB[/url]") - if meta['tvmaze_id']: - desc_parts.append( - f"[url=https://www.tvmaze.com/shows/{str(meta['tvmaze_id'])}]TVmaze[/url]") - if meta['mal_id']: - desc_parts.append( - f"[url=https://myanimelist.net/anime/{str(meta['mal_id'])}]MyAnimeList[/url]") - desc_parts.append("[img]https://i.imgur.com/W3pvv6q.png[/img]") - - desc_parts.append(f"{description}") - - desc_parts.append("[img]https://i.imgur.com/KMZsqZn.png[/img]") - - # if meta.get('is_disc', '') == 'DVD': - # desc_parts.append(f'[hide=DVD MediaInfo][pre]{await builder.get_mediainfo_section(meta)}[/pre][/hide]') - - # bd_info = await builder.get_bdinfo_section(meta) - # if bd_info: - # desc_parts.append(f'[hide=BDInfo][pre]{bd_info}[/pre][/hide]') - - # User description - # desc_parts.append(await builder.get_user_description(meta)) - - desc_parts.append( - f"[b][color=#3d85c6]Source :[/color][/b] [i]{source} {service_longname}[/i]") - - desc_parts.append( - f"[b][color=#3d85c6]Type :[/color][/b] [i]{type}[/i]") - desc_parts.append( - f"[b][color=#3d85c6]Résolution vidéo :[/color][/b][i]{resolution}[/i]") - desc_parts.append( - f"[b][color=#3d85c6]Format vidéo :[/color][/b] [i]{container}[/i]") - - desc_parts.append( - f"[b][color=#3d85c6]Codec vidéo :[/color][/b] [i]{video_codec} {hdr}[/i]") - desc_parts.append( - f"[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{mbps:.2f} MB/s[/i]") - desc_parts.append("[b][color=#3d85c6] Audio(s) :[/color][/b]") + # pre‑compute the lines that were previously appended to ``desc_parts`` + audio_lines: list[str] = [] for obj in audio_tracks: if isinstance(obj, dict): bitrate = obj.get('BitRate') kbps = int(bitrate) / 1_000 if bitrate else 0 - flags = [] + flags: list[str] = [] if obj.get("Forced") == "Yes": flags.append("Forced") if obj.get("Default") == "Yes": @@ -619,16 +567,15 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: line = f"{obj['Language']} / {obj['Format']} / {obj['Channels']}ch / {kbps:.2f}KB/s" if flags: line += " / " + " / ".join(flags) - desc_parts.append(line) + audio_lines.append(line) else: - desc_parts.append(f"*{obj}*") + audio_lines.append(f"*{obj}*") - + subtitle_lines: list[str] = [] if subtitle_tracks: - desc_parts.append("[b][color=#3d85c6]Sous-titres :[/color][/b]") for obj in subtitle_tracks: if isinstance(obj, dict): - flags = [] + flags: list[str] = [] if obj.get("Forced") == "Yes": flags.append("Forced") if obj.get("Default") == "Yes": @@ -636,29 +583,139 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: line = f"{obj['Language']} / {obj['Format']}" if flags: line += " / " + " / ".join(flags) - desc_parts.append(line) + subtitle_lines.append(line) else: - desc_parts.append(f"*{obj}*") + subtitle_lines.append(f"*{obj}*") - # desc_parts.append(f"[img]https://i.imgur.com/KFsABlN.png[/img]") - desc_parts.append(f"[b][color=#3d85c6]Team :[/color][/b] [i]{tag}[/i]") - desc_parts.append(f"[b][color=#3d85c6] Taille totale :[/color][/b] {size_gib:.2f} GB") - # desc_parts.append(f"[b][color=#3d85c6] Nombre de fichier :[/color][/b]") - # Screenshots images = meta[f'{tracker}_images_key'] if f'{tracker}_images_key' in meta else meta['image_list'] - if images: - screenshots_block = '' - for image in images: - screenshots_block += f"[img]{image['raw_url']}[/img]\n" - desc_parts.append(screenshots_block) - - # Signature - desc_parts.append( - f"[url=https://github.com/Audionut/Upload-Assistant]{meta['ua_signature']}[/url]") - description = '\n'.join(part for part in desc_parts if part.strip()) + context = { + 'poster': poster, + 'title': title, + 'year': year, + 'season': season, + 'episode': episode, + 'original_title': original_title, + 'pays': pays, + 'genre': genre, + 'release_date': release_date, + 'video_duration': video_duration, + 'imdb_url': meta.get('imdb_info', {}).get('imdb_url', ''), + 'tmdb': meta.get('tmdb', ''), + 'category': meta.get('category', ''), + 'tvdb_id': meta.get('tvdb_id', ''), + 'tvmaze_id': meta.get('tvmaze_id', ''), + 'mal_id': meta.get('mal_id', ''), + 'description': description, + 'audio_lines': audio_lines, + 'subtitle_lines': subtitle_lines, + 'source': source, + 'service_longname': service_longname, + 'type': type, + 'resolution': resolution, + 'container': container, + 'video_codec': video_codec, + 'hdr': hdr, + 'mbps': mbps, + 'tag': tag, + 'size_gib': size_gib, + 'images': images, + 'signature': meta.get('ua_signature', ''), + } + + # try to render a template if one exists + # determine which template to use; prefer explicit setting, then + # tracker-specific file, then fall back to a generic "FRENCH" template. + description_text = '' + primary = meta.get('description_template') or tracker + template_path = os.path.abspath(f"{meta['base_dir']}/data/templates/{primary}.txt") + + if not os.path.exists(template_path): + # try the shared french template + template_path = os.path.abspath(f"{meta['base_dir']}/data/templates/FRENCH.txt") + + if os.path.exists(template_path): + async with aiofiles.open(template_path, 'r', encoding='utf-8') as description_file: + template_content = await description_file.read() + try: + description_text = Template(template_content).render(**context) + except Exception: + # if rendering fails fall back to the old builder below + description_text = '' + + if not description_text: + # fallback to the original behaviour (preserve before change) + desc_parts: list[str] = [] + desc_parts.append(f"[img]{poster}[/img]") + desc_parts.append( + f"[b][font=Verdana][color=#3d85c6][size=29]{title}[/size][/font]") + desc_parts.append(f"[size=18]{year}[/size][/color][/b]") + + if meta['category'] == "TV": + season = f"S{season}" if season else "" + episode = f"E{episode}" if episode else "" + desc_parts.append(f"[b][size=18]{season}{episode}[/size][/b]") + + desc_parts.append( + f"[font=Verdana][size=13][b][color=#3d85c6]Titre original :[/color][/b] [i]{original_title}[/i][/size][/font]") + desc_parts.append( + f"[b][color=#3d85c6]Pays :[/color][/b] [i]{pays}[/i]") + desc_parts.append(f"[b][color=#3d85c6]Genres :[/color][/b] [i]{genre}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Date de sortie :[/color][/b] [i]{release_date}[/i]") + + if meta['category'] == 'MOVIE': + desc_parts.append( + f"[b][color=#3d85c6]Durée :[/color][/b] [i]{video_duration} Minutes[/i]") + + if meta['imdb_id']: + desc_parts.append(f"[url={meta.get('imdb_info', {}).get('imdb_url', '')}]IMDb[/url]") + if meta['tmdb']: + desc_parts.append( + f"[url=https://www.themoviedb.org/{str(meta['category'].lower())}/{str(meta['tmdb'])}]TMDB[/url]") + if meta['tvdb_id']: + desc_parts.append( + f"[url=https://www.thetvdb.com/?id={str(meta['tvdb_id'])}&tab=series]TVDB[/url]") + if meta['tvmaze_id']: + desc_parts.append( + f"[url=https://www.tvmaze.com/shows/{str(meta['tvmaze_id'])}]TVmaze[/url]") + if meta['mal_id']: + desc_parts.append( + f"[url=https://myanimelist.net/anime/{str(meta['mal_id'])}]MyAnimeList[/url]") + + desc_parts.append("[img]https://i.imgur.com/W3pvv6q.png[/img]") + desc_parts.append(f"{description}") + desc_parts.append("[img]https://i.imgur.com/KMZsqZn.png[/img]") + desc_parts.append( + f"[b][color=#3d85c6]Source :[/color][/b] [i]{source} {service_longname}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Type :[/color][/b] [i]{type}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Résolution vidéo :[/color][/b][i]{resolution}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Format vidéo :[/color][/b] [i]{container}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Codec vidéo :[/color][/b] [i]{video_codec} {hdr}[/i]") + desc_parts.append( + f"[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{mbps:.2f} MB/s[/i]") + desc_parts.append("[b][color=#3d85c6] Audio(s) :[/color][/b]") + desc_parts.extend(audio_lines) + if subtitle_lines: + desc_parts.append("[b][color=#3d85c6]Sous-titres :[/color][/b]") + desc_parts.extend(subtitle_lines) + desc_parts.append(f"[b][color=#3d85c6]Team :[/color][/b] [i]{tag}[/i]") + desc_parts.append(f"[b][color=#3d85c6] Taille totale :[/color][/b] {size_gib:.2f} GB") + if images: + screenshots_block = '' + for image in images: + screenshots_block += f"[img]{image['raw_url']}[/img]\n" + desc_parts.append(screenshots_block) + desc_parts.append( + f"[url=https://github.com/Audionut/Upload-Assistant]{meta['ua_signature']}[/url]") + description_text = '\n'.join(part for part in desc_parts if part.strip()) + + # persist to disk for debugging/inspection async with aiofiles.open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{tracker}]DESCRIPTION.json", 'w', encoding='utf-8') as description_file: - await description_file.write(description) - + await description_file.write(description_text) - return description + return description_text From e4fbb6f847458421f86413312d41d98080a44214 Mon Sep 17 00:00:00 2001 From: Merrick Date: Thu, 19 Feb 2026 14:33:40 +0100 Subject: [PATCH 22/30] typo --- src/trackers/C411.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 242ed2799..9b60af6e9 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -1,4 +1,4 @@ - # Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0 +# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0 # https://github.com/Audionut/Upload-Assistant/tree/master import aiofiles From c314b993fc96965cc48c37c7331611abf9da1451 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Thu, 19 Feb 2026 15:27:45 +0100 Subject: [PATCH 23/30] bugfix template --- data/templates/C411.txt | 56 +++++++++++++++++++++++++++++++++++++++ data/templates/FRENCH.txt | 4 +-- src/trackers/FRENCH.py | 2 ++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 data/templates/C411.txt diff --git a/data/templates/C411.txt b/data/templates/C411.txt new file mode 100644 index 000000000..00abb11de --- /dev/null +++ b/data/templates/C411.txt @@ -0,0 +1,56 @@ +[img]{{ poster }}[/img] + +[b][font=Verdana][color=#3d85c6][size=29]{{ title }}[/size][/font] +[size=18]{{ year }}[/size][/color][/b] + +{% if category == "TV" %} +[b][size=18]S{{ season }}E{{ episode }}[/size][/b] +{% endif %} + +[font=Verdana][size=13][b][color=#3d85c6]Titre original :[/color][/b] [i]{{ original_title }}[/i][/size][/font] +[b][color=#3d85c6]Pays :[/color][/b] [i]{{ pays }}[/i] +[b][color=#3d85c6]Genres :[/color][/b] [i]{{ genre }}[/i] +[b][color=#3d85c6]Date de sortie :[/color][/b] [i]{{ release_date }}[/i] + +{% if category == 'MOVIE' %} +[b][color=#3d85c6]Durée :[/color][/b] [i]{{ video_duration }} Minutes[/i] +{% endif %} + +{% if imdb_url %}[url={{ imdb_url }}]IMDb[/url]{% endif %} +{% if tmdb %}[url=https://www.themoviedb.org/{{ category.lower() }}/{{ tmdb }}]TMDB[/url]{% endif %} +{% if tvdb_id %}[url=https://www.thetvdb.com/?id={{ tvdb_id }}&tab=series]TVDB[/url]{% endif %} +{% if tvmaze_id %}[url=https://www.tvmaze.com/shows/{{ tvmaze_id }}]TVmaze[/url]{% endif %} +{% if mal_id %}[url=https://myanimelist.net/anime/{{ mal_id }}]MyAnimeList[/url]{% endif %} + +[img]https://i.imgur.com/W3pvv6q.png[/img] + +{{ description }} + +[img]https://i.imgur.com/KMZsqZn.png[/img] + +[b][color=#3d85c6]Source :[/color][/b] [i]{{ source }} {{ service_longname }}[/i] +[b][color=#3d85c6]Type :[/color][/b] [i]{{ type }}[/i] +[b][color=#3d85c6]Résolution vidéo :[/color][/b][i]{{ resolution }}[/i] +[b][color=#3d85c6]Format vidéo :[/color][/b] [i]{{ container }}[/i] +[b][color=#3d85c6]Codec vidéo :[/color][/b] [i]{{ video_codec }} {{ hdr }}[/i] +[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{{ mbps|round(2) }} MB/s[/i] + +[b][color=#3d85c6] Audio(s) :[/color][/b] +{% for line in audio_lines %} +{{ line }} +{% endfor %} + +{% if subtitle_lines %}[b][color=#3d85c6]Sous-titres :[/color][/b] +{% for line in subtitle_lines %} +{{ line }} +{% endfor %} +{% endif %} + +[b][color=#3d85c6]Team :[/color][/b] [i]{{ tag }}[/i] +[b][color=#3d85c6] Taille totale :[/color][/b] {{ size_gib|round(2) }} GB + +{% if images %}{% for image in images %} +[img]{{ image['raw_url'] }}[/img] +{% endfor %}{% endif %} + +[url=https://github.com/Audionut/Upload-Assistant]{{ signature }}[/url] \ No newline at end of file diff --git a/data/templates/FRENCH.txt b/data/templates/FRENCH.txt index e7473628d..efa304e28 100644 --- a/data/templates/FRENCH.txt +++ b/data/templates/FRENCH.txt @@ -33,7 +33,7 @@ [b][color=#3d85c6]Résolution vidéo :[/color][/b][i]{{ resolution }}[/i] [b][color=#3d85c6]Format vidéo :[/color][/b] [i]{{ container }}[/i] [b][color=#3d85c6]Codec vidéo :[/color][/b] [i]{{ video_codec }} {{ hdr }}[/i] -[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{{ mbps:.2f }} MB/s[/i] +[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{{ mbps|round(2) }} MB/s[/i] [b][color=#3d85c6] Audio(s) :[/color][/b] {% for line in audio_lines %} @@ -47,7 +47,7 @@ {% endif %} [b][color=#3d85c6]Team :[/color][/b] [i]{{ tag }}[/i] -[b][color=#3d85c6] Taille totale :[/color][/b] {{ size_gib:.2f }} GB +[b][color=#3d85c6] Taille totale :[/color][/b] {{ size_gib|round(2) }} GB {% if images %}{% for image in images %} [img]{{ image['raw_url'] }}[/img] diff --git a/src/trackers/FRENCH.py b/src/trackers/FRENCH.py index 7c9d2a41d..f21837201 100644 --- a/src/trackers/FRENCH.py +++ b/src/trackers/FRENCH.py @@ -635,8 +635,10 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: template_path = os.path.abspath(f"{meta['base_dir']}/data/templates/FRENCH.txt") if os.path.exists(template_path): + async with aiofiles.open(template_path, 'r', encoding='utf-8') as description_file: template_content = await description_file.read() + description_text = Template(template_content).render(**context) try: description_text = Template(template_content).render(**context) except Exception: From 9ba21a8c7eff23445bd4f3d66c1f2f424b0e21f3 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Mon, 23 Feb 2026 06:53:19 +0100 Subject: [PATCH 24/30] use c411 template --- data/templates/C411.txt | 37 ++++++++++++++++++++----------------- src/trackers/FRENCH.py | 1 - 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/data/templates/C411.txt b/data/templates/C411.txt index 00abb11de..89a9599d7 100644 --- a/data/templates/C411.txt +++ b/data/templates/C411.txt @@ -1,20 +1,19 @@ -[img]{{ poster }}[/img] +[h1]{{ title }} ({{ year }})[/h1] -[b][font=Verdana][color=#3d85c6][size=29]{{ title }}[/size][/font] -[size=18]{{ year }}[/size][/color][/b] {% if category == "TV" %} -[b][size=18]S{{ season }}E{{ episode }}[/size][/b] +[h2]Saison {{ season }} {% if episode %}Episode {{ episode }}{% endif %}[/h2] {% endif %} -[font=Verdana][size=13][b][color=#3d85c6]Titre original :[/color][/b] [i]{{ original_title }}[/i][/size][/font] -[b][color=#3d85c6]Pays :[/color][/b] [i]{{ pays }}[/i] -[b][color=#3d85c6]Genres :[/color][/b] [i]{{ genre }}[/i] -[b][color=#3d85c6]Date de sortie :[/color][/b] [i]{{ release_date }}[/i] +[img]{{ poster }}[/img] + -{% if category == 'MOVIE' %} -[b][color=#3d85c6]Durée :[/color][/b] [i]{{ video_duration }} Minutes[/i] -{% endif %} +[h2]🎬 Informations[/h2] + +[b]Pays :[/b] {{pays}} +[b]Genres :[/b] {{genre}} +[b]Date de sortie :[/b] {{release_date}} +{% if video_duration %}[b]Durée :[/b] {{video_duration}}{% endif %} {% if imdb_url %}[url={{ imdb_url }}]IMDb[/url]{% endif %} {% if tmdb %}[url=https://www.themoviedb.org/{{ category.lower() }}/{{ tmdb }}]TMDB[/url]{% endif %} @@ -22,25 +21,29 @@ {% if tvmaze_id %}[url=https://www.tvmaze.com/shows/{{ tvmaze_id }}]TVmaze[/url]{% endif %} {% if mal_id %}[url=https://myanimelist.net/anime/{{ mal_id }}]MyAnimeList[/url]{% endif %} -[img]https://i.imgur.com/W3pvv6q.png[/img] +[h3]📖 Synopsis[/h3] -{{ description }} +{{description}} -[img]https://i.imgur.com/KMZsqZn.png[/img] +[h2]⚙️ Détails Techniques[/h2] [b][color=#3d85c6]Source :[/color][/b] [i]{{ source }} {{ service_longname }}[/i] [b][color=#3d85c6]Type :[/color][/b] [i]{{ type }}[/i] [b][color=#3d85c6]Résolution vidéo :[/color][/b][i]{{ resolution }}[/i] [b][color=#3d85c6]Format vidéo :[/color][/b] [i]{{ container }}[/i] [b][color=#3d85c6]Codec vidéo :[/color][/b] [i]{{ video_codec }} {{ hdr }}[/i] -[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{{ mbps|round(2) }} MB/s[/i] +[b][color=#3d85c6]Débit vidéo :[/color][/b] [i]{{ mbps|round(2) }} MB/s[/i] + +[h3]🔊 Langue(s)[/h3] -[b][color=#3d85c6] Audio(s) :[/color][/b] {% for line in audio_lines %} {{ line }} {% endfor %} -{% if subtitle_lines %}[b][color=#3d85c6]Sous-titres :[/color][/b] +{% if subtitle_lines %} + +[h3]💬 Sous-titre(s)[/h3] + {% for line in subtitle_lines %} {{ line }} {% endfor %} diff --git a/src/trackers/FRENCH.py b/src/trackers/FRENCH.py index f21837201..0cd430271 100644 --- a/src/trackers/FRENCH.py +++ b/src/trackers/FRENCH.py @@ -638,7 +638,6 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: async with aiofiles.open(template_path, 'r', encoding='utf-8') as description_file: template_content = await description_file.read() - description_text = Template(template_content).render(**context) try: description_text = Template(template_content).render(**context) except Exception: From 62ffc9068cc386e6e3235e7e785cb1ca65babe2c Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Mon, 23 Feb 2026 06:55:33 +0100 Subject: [PATCH 25/30] template c411 --- data/templates/C411.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/data/templates/C411.txt b/data/templates/C411.txt index 89a9599d7..d9cd9b93b 100644 --- a/data/templates/C411.txt +++ b/data/templates/C411.txt @@ -1,6 +1,5 @@ [h1]{{ title }} ({{ year }})[/h1] - {% if category == "TV" %} [h2]Saison {{ season }} {% if episode %}Episode {{ episode }}{% endif %}[/h2] {% endif %} From 9770f1305467f7bd04c71a2d0e5f26d62b3eca2a Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Wed, 25 Feb 2026 08:38:31 +0100 Subject: [PATCH 26/30] Upgrade C411 template and bugfix if no translated title --- data/templates/C411.txt | 15 +++++++++++---- src/trackers/FRENCH.py | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/data/templates/C411.txt b/data/templates/C411.txt index d9cd9b93b..a64a3c46a 100644 --- a/data/templates/C411.txt +++ b/data/templates/C411.txt @@ -35,17 +35,24 @@ [h3]🔊 Langue(s)[/h3] -{% for line in audio_lines %} -{{ line }} + +[table][tr][th]#[/th][th]Langue[/th][th]Canaux[/th][th]Codec[/th][/tr] + +{% for line in audio_lines_dict %} +[tr][td]{{ loop.index }}[/td][td]{{ line.language }}[/td][td]{{ line.channels }}[/td][td]{{ line.format }} ({{ line.bitrate|round(2) }} KB/s)[/td][/tr] {% endfor %} +[/table] {% if subtitle_lines %} [h3]💬 Sous-titre(s)[/h3] +[table][tr][th]#[/th][th]Langue[/th][th]Format[/th][th]Type[/th][/tr] +{% for line in subtitle_lines_dict %} +[tr][td]{{ loop.index }}[/td][td]{{ line.language }}[/td][td]{{ line.format }}[/td][td]{{ line.type -{% for line in subtitle_lines %} -{{ line }} +}}[/td][/tr] {% endfor %} +[/table] {% endif %} [b][color=#3d85c6]Team :[/color][/b] [i]{{ tag }}[/i] diff --git a/src/trackers/FRENCH.py b/src/trackers/FRENCH.py index 0cd430271..4f0062162 100644 --- a/src/trackers/FRENCH.py +++ b/src/trackers/FRENCH.py @@ -446,6 +446,10 @@ async def get_translation_fr(meta: dict[str, Any]) -> tuple[str, str]: tmdb_id = int(meta["tmdb_id"]) category = str(meta["category"]) tmdb_title, tmdb_overview = await get_tmdb_translations(tmdb_id, category, "fr") + # fallback in case the translated title is empty + if tmdb_title == '': + tmdb_title = meta.get("title","") + meta["frtitle"] = french_title or tmdb_title meta["froverview"] = tmdb_overview @@ -549,6 +553,7 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: # pre‑compute the lines that were previously appended to ``desc_parts`` audio_lines: list[str] = [] + audio_lines_dict = [] for obj in audio_tracks: if isinstance(obj, dict): bitrate = obj.get('BitRate') @@ -563,15 +568,23 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: flags.append("Commentary") if " ad" in str(obj.get('Title')).lower(): flags.append("Audio Description") + line_dict = {} + line_dict['language'] = obj['Language'] + line_dict['format'] = obj['Format'] + line_dict['channels'] = obj['Channels'] + line_dict['bitrate'] = kbps + line = f"{obj['Language']} / {obj['Format']} / {obj['Channels']}ch / {kbps:.2f}KB/s" if flags: line += " / " + " / ".join(flags) audio_lines.append(line) + audio_lines_dict.append(line_dict) else: audio_lines.append(f"*{obj}*") subtitle_lines: list[str] = [] + subtitle_lines_dict = [] if subtitle_tracks: for obj in subtitle_tracks: if isinstance(obj, dict): @@ -583,7 +596,13 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: line = f"{obj['Language']} / {obj['Format']}" if flags: line += " / " + " / ".join(flags) + line_dict = {} + line_dict['language'] = obj['Language'] + line_dict['format'] = obj['Format'] + line_dict['type'] = ", ".join(flags) subtitle_lines.append(line) + subtitle_lines_dict.append(line_dict) + else: subtitle_lines.append(f"*{obj}*") @@ -608,7 +627,9 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: 'mal_id': meta.get('mal_id', ''), 'description': description, 'audio_lines': audio_lines, + 'audio_lines_dict': audio_lines_dict, 'subtitle_lines': subtitle_lines, + 'subtitle_lines_dict': subtitle_lines_dict, 'source': source, 'service_longname': service_longname, 'type': type, From c7021bb41e3c626901b3876745df68bb58680f75 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Thu, 26 Feb 2026 08:52:36 +0100 Subject: [PATCH 27/30] bugfix if no language on audio --- src/trackers/FRENCH.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/trackers/FRENCH.py b/src/trackers/FRENCH.py index 4f0062162..4ef36b659 100644 --- a/src/trackers/FRENCH.py +++ b/src/trackers/FRENCH.py @@ -63,8 +63,10 @@ async def get_extra_french_tag(meta: dict[str, Any], check_origin: bool) -> str: for _, item in enumerate(audio_track): title = (item.get("Title") or "").lower() - lang = item.get('Language', "").lower() - + try: + lang = item.get('Language', "").lower() + except: + lang = "" if lang == "fr-ca" or "vfq" in title: vfq = True elif lang == "fr-fr" or "vff" in title: From d3a0f38621ac290d73f5c5816f193173588c40a7 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Fri, 27 Feb 2026 11:20:43 +0100 Subject: [PATCH 28/30] Allow multiple subdirectory depth --- src/video.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/video.py b/src/video.py index 9f8cad2a0..0111aee18 100644 --- a/src/video.py +++ b/src/video.py @@ -153,7 +153,9 @@ async def get_video(self, videoloc: str, mode: str, sorted_filelist: bool = Fals if debug: console.print("[blue]Scanning directory for video files...[/blue]") try: - entries = [e for e in os.listdir(videoloc) if os.path.isfile(os.path.join(videoloc, e))] + # entries = [e for e in os.listdir(videoloc) if os.path.isfile(os.path.join(videoloc, e))] + entries = [os.path.join(dp, f) for dp, dn, filenames in os.walk(videoloc) for f in filenames ] + #entries = list (os.walk(videoloc)) except Exception: entries = [] From c232d71f91b598d560fa124fb9091bbd983ceac0 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Mon, 2 Mar 2026 08:58:01 +0100 Subject: [PATCH 29/30] bugfix on webdl and x264/x265 --- src/trackers/C411.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/trackers/C411.py b/src/trackers/C411.py index 9b60af6e9..d7eb24909 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -210,6 +210,11 @@ async def get_name(self, meta: Meta) -> dict[str, str]: else: video_codec = str(meta.get('video_codec', "")).replace('H.264', 'H264').replace('H.265', 'H265') video_encode = str(meta.get('video_encode', "")).replace('H.264', 'H264').replace('H.265', 'H265') + temp = meta.get('type',"") + if meta.get('type',"") == "WEBDL" and '264' in video_encode: + video_encode = "x264" + if meta.get('type',"") == "WEBDL" and '265' in video_encode: + video_encode = "x265" edition = str(meta.get('edition', "")) if 'hybrid' in edition.upper(): edition = edition.replace('Hybrid', '').strip() From d9fe2c5d64115c562fd5fc2f43638c4525433fc1 Mon Sep 17 00:00:00 2001 From: Merrick28 Date: Fri, 6 Mar 2026 14:23:21 +0100 Subject: [PATCH 30/30] bugfix template et gestion emission tv --- data/templates/C411.txt | 2 +- src/trackers/C411.py | 8 +++----- src/trackers/FRENCH.py | 2 ++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/templates/C411.txt b/data/templates/C411.txt index a64a3c46a..1f3621770 100644 --- a/data/templates/C411.txt +++ b/data/templates/C411.txt @@ -1,7 +1,7 @@ [h1]{{ title }} ({{ year }})[/h1] {% if category == "TV" %} -[h2]Saison {{ season }} {% if episode %}Episode {{ episode }}{% endif %}[/h2] +[h2]Saison {{ season }} {% if episode %}{% if episode != '0' %}Episode {{ episode }}{% endif %}{% endif %}[/h2] {% endif %} [img]{{ poster }}[/img] diff --git a/src/trackers/C411.py b/src/trackers/C411.py index d7eb24909..6a8d1733b 100644 --- a/src/trackers/C411.py +++ b/src/trackers/C411.py @@ -43,6 +43,8 @@ async def get_subcat_id(self, meta: Meta) -> str: sub_cat_id = '6' elif meta['category'] == 'TV': sub_cat_id = '2' if meta.get('mal_id') else '7' + if "reality" in genres: + sub_cat_id = 5 return sub_cat_id @@ -210,11 +212,7 @@ async def get_name(self, meta: Meta) -> dict[str, str]: else: video_codec = str(meta.get('video_codec', "")).replace('H.264', 'H264').replace('H.265', 'H265') video_encode = str(meta.get('video_encode', "")).replace('H.264', 'H264').replace('H.265', 'H265') - temp = meta.get('type',"") - if meta.get('type',"") == "WEBDL" and '264' in video_encode: - video_encode = "x264" - if meta.get('type',"") == "WEBDL" and '265' in video_encode: - video_encode = "x265" + #video_encode = "x264" edition = str(meta.get('edition', "")) if 'hybrid' in edition.upper(): edition = edition.replace('Hybrid', '').strip() diff --git a/src/trackers/FRENCH.py b/src/trackers/FRENCH.py index 4ef36b659..e28c65211 100644 --- a/src/trackers/FRENCH.py +++ b/src/trackers/FRENCH.py @@ -552,6 +552,8 @@ async def get_desc_full(meta: dict[str, Any], tracker) -> str: service_longname = str(meta.get('service_longname', "")) season = str(meta.get('season_int', '')) episode = str(meta.get('episode_int', '')) + if episode == '0': + episode = '' # pre‑compute the lines that were previously appended to ``desc_parts`` audio_lines: list[str] = []