From 902eefe3539830a3e6acb7fe9e95d2af48668f7c Mon Sep 17 00:00:00 2001 From: Lusephur Date: Fri, 6 Feb 2026 23:43:28 +0000 Subject: [PATCH 01/11] Refactor comments and improve description formatting TVC changed the site use of the bbc description box to something hybrid. if a /n appears after [center] it injects
breaking the centring /n/n becomes

This is a quick fix, removing the errors the new site parser produces. --- src/trackers/TVC.py | 85 +++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index 0415c8249..28c11f93b 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -604,14 +604,16 @@ async def unit3d_edit_desc( base = await self.read_file(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt") except FileNotFoundError: base = "" + # Ensure tmp/ directory exists desc_dir = os.path.join(meta['base_dir'], "tmp", meta['uuid']) os.makedirs(desc_dir, exist_ok=True) descfile_path = os.path.join(desc_dir, f"[{tracker}]DESCRIPTION.txt") + bbcode = BBCODE() desc = "" - # Discs + # DISCS if meta.get('discs', []): discs = meta['discs'] if discs[0]['type'] == "DVD": @@ -626,7 +628,7 @@ async def unit3d_edit_desc( f"[spoiler={os.path.basename(each['ifo'])}][code]{each['ifo_mi']}[/code][/spoiler]\n\n" ) - # Release info for movies + # MOVIE RELEASE INFO rd_info = "" if meta['category'] == "MOVIE": if 'release_dates' in meta: @@ -640,26 +642,26 @@ async def unit3d_edit_desc( ) else: rd_info = meta.get('release_date', '') + if rd_info: desc += f"[center]{rd_info}[/center]\n\n" - # TV pack layout + # TV PACK LAYOUT if meta['category'] == "TV" and meta.get('tv_pack') == 1 and 'season_air_first_date' in meta: + channel = meta.get('networks', 'N/A') airdate = self.format_date_ddmmyyyy(meta.get('season_air_first_date') or "") - desc += "[center]\n" + desc += "[center]" if meta.get("logo"): desc += f"[img={self.config['DEFAULT'].get('logo_size', '300')}]" - desc += f"{meta['logo']}[/img]\n\n" + desc += f"{meta['logo']}[/img]\n" - # UK terminology: Series not Season - desc += f"[b]Series Title:[/b] {meta.get('season_name', 'Unknown Series')}\n\n" + desc += f"[b]Series Title:[/b] {meta.get('season_name', 'Unknown Series')}\n" desc += f"[b]This series premiered on:[/b] {channel} on {airdate}\n" - # Episode list if meta.get('episodes'): - desc += "\n\n[b]Episode List[/b]\n\n" + desc += "\n[b]Episode List[/b]\n" for ep in meta['episodes']: ep_num = ep.get('code', '') ep_title = ep.get('title', '').strip() @@ -675,88 +677,91 @@ async def unit3d_edit_desc( desc += "\n" if ep_overview: - desc += f"{ep_overview}\n\n" + desc += f"{ep_overview}\n" desc += self.get_links(meta) screens_count = int(meta.get('screens', 0) or 0) if image_list and screens_count >= self.config['TRACKERS'][self.tracker].get('image_count', 2): - desc += "\n\n[b]Screenshots[/b]\n\n" + desc += "\n[b]Screenshots[/b]\n" for each in image_list[:self.config['TRACKERS'][self.tracker]['image_count']]: web_url = each['web_url'] img_url = each['img_url'] desc += f"[url={web_url}][img=350]{img_url}[/img][/url]" - desc += "[/center]\n\n" + desc += "\n[/center]\n\n" - # Episode layout + # EPISODE LAYOUT elif meta['category'] == "TV" and meta.get('tv_pack') != 1 and 'episode_overview' in meta: - desc += "[center]\n" + + desc += "[center]" if meta.get("logo"): desc += f"[img={self.config['DEFAULT'].get('logo_size', '300')}]" - desc += f"{meta['logo']}[/img]\n\n" + desc += f"{meta['logo']}[/img]\n" + episode_name = str(meta.get('episode_name', '')).strip() overview = str(meta.get('episode_overview', '')).strip() - # Note: regex may mis-split on abbreviations (e.g. "Dr. Smith") or ellipses ("..."). - # This is a heuristic; fallback is to treat the whole overview as one block. + sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', overview) if s.strip()] if not sentences and overview: sentences = [overview] if episode_name: - desc += f"[b]Episode Title:[/b] {episode_name}\n\n" + desc += f"[b]Episode Title:[/b] {episode_name}\n" + for s in sentences: desc += s.rstrip() + "\n" + if 'episode_airdate' in meta: channel = meta.get('networks', 'N/A') formatted_date = self.format_date_ddmmyyyy(meta['episode_airdate']) - desc += f"\n[b]Broadcast on:[/b] {channel} on {formatted_date}\n" + desc += f"[b]Broadcast on:[/b] {channel} on {formatted_date}\n" desc += self.get_links(meta) screens_count = int(meta.get('screens', 0) or 0) if image_list and screens_count >= self.config['TRACKERS'][self.tracker].get('image_count', 2): - desc += "\n\n[b]Screenshots[/b]\n\n" + desc += "\n[b]Screenshots[/b]\n" for each in image_list[:self.config['TRACKERS'][self.tracker]['image_count']]: web_url = each['web_url'] img_url = each['img_url'] desc += f"[url={web_url}][img=350]{img_url}[/img][/url]" - desc += "[/center]\n\n" - # Movie / fallback overview + desc += "\n[/center]\n\n" + + # MOVIE / FALLBACK else: - # Fallback path: for non‑movie categories with only a generic overview available. overview = str(meta.get('overview', '')).strip() - desc += "[center]\n" + desc += "[center]" if meta['category'].upper() == "MOVIE" and meta.get("logo"): desc += f"[img={self.config['DEFAULT'].get('logo_size', '300')}]" - desc += f"{meta['logo']}[/img]\n\n" + desc += f"{meta['logo']}[/img]\n" if meta['category'].upper() == "MOVIE": - desc += f"[b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}\n\n" + desc += f"[b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}\n" desc += overview + "\n" if 'release_date' in meta: formatted_date = self.format_date_ddmmyyyy(meta['release_date']) - desc += f"\n[b]Released on:[/b] {formatted_date}\n" + desc += f"[b]Released on:[/b] {formatted_date}\n" + desc += self.get_links(meta) - # Screenshots block for movies screens_count = int(meta.get('screens', 0) or 0) if image_list and screens_count >= self.config['TRACKERS'][self.tracker].get('image_count', 2): - desc += "\n\n[b]Screenshots[/b]\n\n" + desc += "\n[b]Screenshots[/b]\n" for each in image_list[:self.config['TRACKERS'][self.tracker]['image_count']]: web_url = each['web_url'] img_url = each['img_url'] desc += f"[url={web_url}][img=350]{img_url}[/img][/url]" - desc += "[/center]\n\n" + desc += "\n[/center]\n\n" else: desc += overview + "\n[/center]\n\n" - # Notes/Extra Info + # NOTES notes_content = base.strip() if notes_content and notes_content.lower() != "ptp": - desc += f"[center][b]Notes / Extra Info[/b]\n\n{notes_content}\n\n[/center]\n\n" + desc += f"[center][b]Notes / Extra Info[/b]\n{notes_content}\n[/center]\n\n" # BBCode conversions desc = bbcode.convert_pre_to_code(desc) @@ -764,11 +769,14 @@ async def unit3d_edit_desc( if not comparison: desc = bbcode.convert_comparison_to_collapse(desc, 1000) - # Ensure fallback content if description is empty + # MINIMAL TVC FIX + desc = desc.replace("[center]\n", "[center]") + desc = desc.replace("\n\n", "\n") + + # Fallback if not desc.strip(): desc = "[center][i]No description available[/i][/center]\n" - # Append signature if provided if signature: desc += f"\n{signature}\n" @@ -777,15 +785,10 @@ def _write(): with open(descfile_path, "w", encoding="utf-8") as f: f.write(desc) - try: - await asyncio.to_thread(_write) - if meta['debug']: - console.print(f"[cyan]Wrote DESCRIPTION file to {descfile_path} ({len(desc)} chars)") - except Exception as e: - console.print(f"[bold red]Failed to write DESCRIPTION file: {e}") - + await asyncio.to_thread(_write) return desc + def get_links(self, meta: Meta) -> str: """ Returns a BBCode string with an 'External Info Sources' heading and icon links. From e95b4b96b8fce1e0a99ebc82862f78413b94ef29 Mon Sep 17 00:00:00 2001 From: Lusephur Date: Sat, 7 Feb 2026 08:15:18 +0000 Subject: [PATCH 02/11] Fix minimal TVC by collapsing newlines Collapse multiple newlines into a single newline for cleaner output. --- src/trackers/TVC.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index 28c11f93b..fd71575b2 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -771,7 +771,9 @@ async def unit3d_edit_desc( # MINIMAL TVC FIX desc = desc.replace("[center]\n", "[center]") - desc = desc.replace("\n\n", "\n") + + # Collapse any run of 2+ newlines into a single newline + desc = re.sub(r"\n{2,}", "\n", desc) # Fallback if not desc.strip(): From 86908e48d59fd69bb30ae0c567c1e24f21ba2255 Mon Sep 17 00:00:00 2001 From: Lusephur Date: Sat, 7 Feb 2026 12:18:00 +0000 Subject: [PATCH 03/11] Fix newline collapsing in TVC description --- src/trackers/TVC.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index fd71575b2..c032632be 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -772,8 +772,8 @@ async def unit3d_edit_desc( # MINIMAL TVC FIX desc = desc.replace("[center]\n", "[center]") - # Collapse any run of 2+ newlines into a single newline - desc = re.sub(r"\n{2,}", "\n", desc) + # Collapse any run of 2+ newlines into a single newline + desc = re.sub(r"\n{2,}", "\n", desc) # Fallback if not desc.strip(): From 69d8a8501c084442396c0b75bbb5e51d84942422 Mon Sep 17 00:00:00 2001 From: Audionut Date: Wed, 11 Feb 2026 20:16:32 +1000 Subject: [PATCH 04/11] lint --- src/trackers/TVC.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index c032632be..619809e7c 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -771,7 +771,7 @@ async def unit3d_edit_desc( # MINIMAL TVC FIX desc = desc.replace("[center]\n", "[center]") - + # Collapse any run of 2+ newlines into a single newline desc = re.sub(r"\n{2,}", "\n", desc) @@ -782,12 +782,16 @@ async def unit3d_edit_desc( if signature: desc += f"\n{signature}\n" - # Write description asynchronously - def _write(): - with open(descfile_path, "w", encoding="utf-8") as f: - f.write(desc) + try: + # Write description asynchronously + def _write(): + with open(descfile_path, "w", encoding="utf-8") as f: + f.write(desc) + + await asyncio.to_thread(_write) + except Exception as e: + console.print(f"[yellow]Warning: Failed to write description file: {e}[/yellow]") - await asyncio.to_thread(_write) return desc From 3627e288231c9ea211f4a52c63bce4e63d3c8971 Mon Sep 17 00:00:00 2001 From: Lusephur Date: Thu, 12 Feb 2026 15:21:00 +0000 Subject: [PATCH 05/11] [TVC] Add constants and refactor description building methods TVC changed the site use of the bbc description box to something hybrid. if a/nappears after [center] it injects
breaking the centring/n/nbecomes

This is a quick fix, removing the errors the new site parser produces. and some refactoring for ease of editing for others --- src/trackers/TVC.py | 569 ++++++++++++++++++++++++++++---------------- 1 file changed, 367 insertions(+), 202 deletions(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index 619809e7c..4bb3a0737 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -24,6 +24,12 @@ class TVC: + # Constants for the class + DEFAULT_LOGO_SIZE = '300' + SCREENSHOT_THUMB_SIZE = '350' + COMPARISON_COLLAPSE_THRESHOLD = 1000 + MIN_SCREENSHOTS_REQUIRED = 2 + def __init__(self, config: Config) -> None: self.config: Config = config self.rehost_images_manager = RehostImagesManager(config) @@ -71,6 +77,293 @@ def format_date_ddmmyyyy(self, date_str: str) -> str: except (ValueError, TypeError): return date_str + async def _read_base_description(self, meta: Meta) -> str: + """Read the base DESCRIPTION.txt file if it exists.""" + try: + return await self.read_file( + f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt" + ) + except FileNotFoundError: + return "" + + def _ensure_desc_directory(self, meta: Meta, tracker: str) -> str: + """Create description directory and return file path.""" + desc_dir = os.path.join(meta['base_dir'], "tmp", meta['uuid']) + os.makedirs(desc_dir, exist_ok=True) + return os.path.join(desc_dir, f"[{tracker}]DESCRIPTION.txt") + + def _build_disc_info(self, discs: list[dict[str, Any]]) -> str: + """Build disc information section.""" + parts = [] + + # First disc (DVD VOB) + if discs[0]['type'] == "DVD": + parts.append( + f"[spoiler=VOB MediaInfo][code]{discs[0]['vob_mi']}[/code][/spoiler]\n\n" + ) + + # Additional discs + for disc in discs[1:]: + if disc['type'] == "BDMV": + name = disc.get('name', 'BDINFO') + parts.append( + f"[spoiler={name}][code]{disc['summary']}[/code][/spoiler]\n\n" + ) + elif disc['type'] == "DVD": + parts.append(f"{disc['name']}:\n") + vob_name = os.path.basename(disc['vob']) + ifo_name = os.path.basename(disc['ifo']) + parts.append( + f"[spoiler={vob_name}][code]{disc['vob_mi']}[/code][/spoiler] " + f"[spoiler={ifo_name}][code]{disc['ifo_mi']}[/code][/spoiler]\n\n" + ) + + return "".join(parts) + + def _build_movie_desc( + self, + meta: Meta, + image_list: list[dict[str, Any]] + ) -> str: + """Build description for movie releases.""" + parts = [] + + # Release date info (before center tag) + rd_info = self._get_movie_release_info(meta) + if rd_info: + parts.append(f"[center]{rd_info}[/center]\n\n") + + parts.append("[center]") + + # Logo + if meta.get("logo"): + logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) + parts.append(f"[img={logo_size}]{meta['logo']}[/img]\n") + + # Title and overview + parts.append(f"[b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}\n") + parts.append(f"{meta.get('overview', '').strip()}\n") + + # Release date + if 'release_date' in meta: + formatted_date = self.format_date_ddmmyyyy(meta['release_date']) + parts.append(f"[b]Released on:[/b] {formatted_date}\n") + + # External links + parts.append(self.get_links(meta)) + + # Screenshots + parts.append(self._add_screenshots(meta, image_list)) + + parts.append("[/center]\n\n") + return "".join(parts) + + def _build_tv_pack_desc( + self, + meta: Meta, + image_list: list[dict[str, Any]] + ) -> str: + """Build description for TV pack releases.""" + if 'season_air_first_date' not in meta: + return "" + + parts = ["[center]"] + + # Logo + if meta.get("logo"): + logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) + parts.append(f"[img={logo_size}]{meta['logo']}[/img]\n") + + # Series info + channel = meta.get('networks', 'N/A') + airdate = self.format_date_ddmmyyyy(meta.get('season_air_first_date') or "") + + parts.append(f"[b]Series Title:[/b] {meta.get('season_name', 'Unknown Series')}\n") + parts.append(f"[b]This series premiered on:[/b] {channel} on {airdate}\n") + + # Episode list + if meta.get('episodes'): + parts.append("\n[b]Episode List[/b]\n") + parts.append(self._build_episode_list(meta['episodes'])) + + # External links + parts.append(self.get_links(meta)) + + # Screenshots + parts.append(self._add_screenshots(meta, image_list)) + + parts.append("[/center]\n\n") + return "".join(parts) + + def _build_episode_desc( + self, + meta: Meta, + image_list: list[dict[str, Any]] + ) -> str: + """Build description for single episode releases.""" + if 'episode_overview' not in meta: + return "" + + parts = ["[center]"] + + # Logo + if meta.get("logo"): + logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) + parts.append(f"[img={logo_size}]{meta['logo']}[/img]\n\n") + + # Episode title + episode_name = meta.get('episode_name', '').strip() + if episode_name: + parts.append(f"[b]Episode Title:[/b] {episode_name}\n\n") + + # Overview (sentence by sentence) + overview = meta.get('episode_overview', '').strip() + parts.append(self._format_overview(overview)) + + # Airdate + if 'episode_airdate' in meta: + channel = meta.get('networks', 'N/A') + formatted_date = self.format_date_ddmmyyyy(meta['episode_airdate']) + parts.append(f"[b]Broadcast on:[/b] {channel} on {formatted_date}\n") + + # External links + parts.append(self.get_links(meta)) + + # Screenshots + parts.append(self._add_screenshots(meta, image_list)) + + parts.append("[/center]\n\n") + return "".join(parts) + + def _build_fallback_desc(self, meta: Meta) -> str: + """Build fallback description for other categories.""" + overview = meta.get('overview', '').strip() + return f"[center]{overview}[/center]\n\n" + + def _get_movie_release_info(self, meta: Meta) -> str: + """Extract movie release date information.""" + if 'release_dates' not in meta: + return meta.get('release_date', '') + + parts = [] + for cc in meta['release_dates']['results']: + for rd in cc['release_dates']: + if rd['type'] == 6: # TV release + channel = rd['note'] if rd['note'] else "N/A Channel" + parts.append( + f"[color=orange][size=15]{cc['iso_3166_1']} TV Release info [/size][/color]\n" + f"{str(rd['release_date'])[:10]} on {channel}\n" + ) + + return "".join(parts) + + def _build_episode_list(self, episodes: list[dict[str, Any]]) -> str: + """Build formatted episode list.""" + parts = [] + + for ep in episodes: + ep_num = ep.get('code', '') + ep_title = ep.get('title', '').strip() + ep_date = ep.get('airdate', '') + ep_overview = ep.get('overview', '').strip() + + # Episode number and title + parts.append(f"[b]{ep_num}[/b]") + if ep_title: + parts.append(f" - {ep_title}") + if ep_date: + formatted_date = self.format_date_ddmmyyyy(ep_date) + parts.append(f" ({formatted_date})") + parts.append("\n") + + # Overview + if ep_overview: + parts.append(f"{ep_overview}\n") + + return "".join(parts) + + def _format_overview(self, overview: str) -> str: + """Format overview text sentence by sentence.""" + if not overview: + return "" + + sentences = [ + s.strip() + for s in re.split(r'(?<=[.!?])\s+', overview) + if s.strip() + ] + + if not sentences: + sentences = [overview] + + return "".join(s.rstrip() + "\n" for s in sentences) + "\n" + + def _add_screenshots( + self, + meta: Meta, + image_list: list[dict[str, Any]] + ) -> str: + """Add screenshots section if requirements are met.""" + screens_count = int(meta.get('screens', 0) or 0) + required_count = self.config['TRACKERS'][self.tracker].get( + 'image_count', + self.MIN_SCREENSHOTS_REQUIRED + ) + + if not image_list or screens_count < required_count: + return "" + + parts = ["\n[b]Screenshots[/b]\n"] + + for img in image_list[:required_count]: + web_url = img['web_url'] + img_url = img['img_url'] + parts.append( + f"[url={web_url}][img={self.SCREENSHOT_THUMB_SIZE}]{img_url}[/img][/url]" + ) + + return "".join(parts) + + def _build_notes_section(self, base: str) -> str: + """Build notes/extra info section.""" + return f"[center][b]Notes / Extra Info[/b]\n{base.strip()}\n[/center]\n\n" + + def _apply_bbcode_transforms(self, desc: str, comparison: bool) -> str: + """Apply BBCode transformations.""" + bbcode = BBCODE() + desc = bbcode.convert_pre_to_code(desc) + desc = bbcode.convert_hide_to_spoiler(desc) + + if not comparison: + desc = bbcode.convert_comparison_to_collapse( + desc, + self.COMPARISON_COLLAPSE_THRESHOLD + ) + + return desc + + def _normalize_tvc_formatting(self, desc: str) -> str: + """Normalize whitespace and formatting for TVC.""" + # TVC-specific: remove newline immediately after [center] + desc = desc.replace("[center]\n", "[center]") + + # Collapse any run of 2+ newlines into a single newline + desc = re.sub(r"\n{2,}", "\n", desc) + + return desc + + async def _write_description_file(self, filepath: str, content: str) -> None: + """Write description content to file asynchronously.""" + try: + def _write(): + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + + await asyncio.to_thread(_write) + except Exception as e: + from src.console import console + console.print(f"[yellow]Warning: Failed to write description file: {e}[/yellow]") + async def get_cat_id(self, genres: str) -> str: """ Determine TVC category ID based on genre list. @@ -591,235 +884,107 @@ async def unit3d_edit_desc( a non-empty description file to tmp//[TVC]DESCRIPTION.txt. Args: - meta (dict): Metadata dictionary for the release. - tracker (str): Tracker name (e.g. "TVC"). - signature (str): Optional signature string to append. - image_list (list): List of screenshot image dicts. - comparison (bool): Whether to include comparison collapse blocks. + meta: Metadata dictionary for the release. + tracker: Tracker name (e.g. "TVC"). + signature: Optional signature string to append. + image_list: List of screenshot image dicts. + comparison: Whether to include comparison collapse blocks. Returns: - str: The final BBCode description string (also written to file). + The final BBCode description string (also written to file). """ - try: - base = await self.read_file(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt") - except FileNotFoundError: - base = "" + # Read base description file + base = await self._read_base_description(meta) - # Ensure tmp/ directory exists - desc_dir = os.path.join(meta['base_dir'], "tmp", meta['uuid']) - os.makedirs(desc_dir, exist_ok=True) - descfile_path = os.path.join(desc_dir, f"[{tracker}]DESCRIPTION.txt") - - bbcode = BBCODE() - desc = "" - - # DISCS - if meta.get('discs', []): - discs = meta['discs'] - if discs[0]['type'] == "DVD": - desc += f"[spoiler=VOB MediaInfo][code]{discs[0]['vob_mi']}[/code][/spoiler]\n\n" - for each in discs[1:]: - if each['type'] == "BDMV": - desc += f"[spoiler={each.get('name', 'BDINFO')}][code]{each['summary']}[/code][/spoiler]\n\n" - if each['type'] == "DVD": - desc += f"{each['name']}:\n" - desc += ( - f"[spoiler={os.path.basename(each['vob'])}][code]{each['vob_mi']}[/code][/spoiler] " - f"[spoiler={os.path.basename(each['ifo'])}][code]{each['ifo_mi']}[/code][/spoiler]\n\n" - ) - - # MOVIE RELEASE INFO - rd_info = "" - if meta['category'] == "MOVIE": - if 'release_dates' in meta: - for cc in meta['release_dates']['results']: - for rd in cc['release_dates']: - if rd['type'] == 6: - channel = str(rd['note']) if str(rd['note']) != "" else "N/A Channel" - rd_info += ( - f"[color=orange][size=15]{cc['iso_3166_1']} TV Release info [/size][/color]\n" - f"{str(rd['release_date'])[:10]} on {channel}\n" - ) - else: - rd_info = meta.get('release_date', '') + # Ensure output directory exists + descfile_path = self._ensure_desc_directory(meta, tracker) - if rd_info: - desc += f"[center]{rd_info}[/center]\n\n" + # Build description content + desc_parts = [] - # TV PACK LAYOUT - if meta['category'] == "TV" and meta.get('tv_pack') == 1 and 'season_air_first_date' in meta: + # Add disc information + if meta.get('discs'): + desc_parts.append(self._build_disc_info(meta['discs'])) - channel = meta.get('networks', 'N/A') - airdate = self.format_date_ddmmyyyy(meta.get('season_air_first_date') or "") - - desc += "[center]" - if meta.get("logo"): - desc += f"[img={self.config['DEFAULT'].get('logo_size', '300')}]" - desc += f"{meta['logo']}[/img]\n" - - desc += f"[b]Series Title:[/b] {meta.get('season_name', 'Unknown Series')}\n" - desc += f"[b]This series premiered on:[/b] {channel} on {airdate}\n" - - if meta.get('episodes'): - desc += "\n[b]Episode List[/b]\n" - for ep in meta['episodes']: - ep_num = ep.get('code', '') - ep_title = ep.get('title', '').strip() - ep_date = ep.get('airdate', '') - ep_overview = ep.get('overview', '').strip() - - desc += f"[b]{ep_num}[/b]" - if ep_title: - desc += f" - {ep_title}" - if ep_date: - formatted_date = self.format_date_ddmmyyyy(ep_date) - desc += f" ({formatted_date})" - desc += "\n" - - if ep_overview: - desc += f"{ep_overview}\n" - - desc += self.get_links(meta) - - screens_count = int(meta.get('screens', 0) or 0) - if image_list and screens_count >= self.config['TRACKERS'][self.tracker].get('image_count', 2): - desc += "\n[b]Screenshots[/b]\n" - for each in image_list[:self.config['TRACKERS'][self.tracker]['image_count']]: - web_url = each['web_url'] - img_url = each['img_url'] - desc += f"[url={web_url}][img=350]{img_url}[/img][/url]" - - desc += "\n[/center]\n\n" - - # EPISODE LAYOUT - elif meta['category'] == "TV" and meta.get('tv_pack') != 1 and 'episode_overview' in meta: - - desc += "[center]" - if meta.get("logo"): - desc += f"[img={self.config['DEFAULT'].get('logo_size', '300')}]" - desc += f"{meta['logo']}[/img]\n" - - episode_name = str(meta.get('episode_name', '')).strip() - overview = str(meta.get('episode_overview', '')).strip() - - sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', overview) if s.strip()] - if not sentences and overview: - sentences = [overview] - - if episode_name: - desc += f"[b]Episode Title:[/b] {episode_name}\n" - - for s in sentences: - desc += s.rstrip() + "\n" - - if 'episode_airdate' in meta: - channel = meta.get('networks', 'N/A') - formatted_date = self.format_date_ddmmyyyy(meta['episode_airdate']) - desc += f"[b]Broadcast on:[/b] {channel} on {formatted_date}\n" - - desc += self.get_links(meta) - - screens_count = int(meta.get('screens', 0) or 0) - if image_list and screens_count >= self.config['TRACKERS'][self.tracker].get('image_count', 2): - desc += "\n[b]Screenshots[/b]\n" - for each in image_list[:self.config['TRACKERS'][self.tracker]['image_count']]: - web_url = each['web_url'] - img_url = each['img_url'] - desc += f"[url={web_url}][img=350]{img_url}[/img][/url]" - - desc += "\n[/center]\n\n" - - # MOVIE / FALLBACK + # Add content-specific sections + if meta['category'] == "MOVIE": + desc_parts.append(self._build_movie_desc(meta, image_list)) + elif meta['category'] == "TV" and meta.get('tv_pack') == 1: + desc_parts.append(self._build_tv_pack_desc(meta, image_list)) + elif meta['category'] == "TV" and meta.get('tv_pack') != 1: + desc_parts.append(self._build_episode_desc(meta, image_list)) else: - overview = str(meta.get('overview', '')).strip() - desc += "[center]" - if meta['category'].upper() == "MOVIE" and meta.get("logo"): - desc += f"[img={self.config['DEFAULT'].get('logo_size', '300')}]" - desc += f"{meta['logo']}[/img]\n" - - if meta['category'].upper() == "MOVIE": - desc += f"[b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}\n" - desc += overview + "\n" - if 'release_date' in meta: - formatted_date = self.format_date_ddmmyyyy(meta['release_date']) - desc += f"[b]Released on:[/b] {formatted_date}\n" - - desc += self.get_links(meta) - - screens_count = int(meta.get('screens', 0) or 0) - if image_list and screens_count >= self.config['TRACKERS'][self.tracker].get('image_count', 2): - desc += "\n[b]Screenshots[/b]\n" - for each in image_list[:self.config['TRACKERS'][self.tracker]['image_count']]: - web_url = each['web_url'] - img_url = each['img_url'] - desc += f"[url={web_url}][img=350]{img_url}[/img][/url]" - - desc += "\n[/center]\n\n" - else: - desc += overview + "\n[/center]\n\n" + desc_parts.append(self._build_fallback_desc(meta)) - # NOTES - notes_content = base.strip() - if notes_content and notes_content.lower() != "ptp": - desc += f"[center][b]Notes / Extra Info[/b]\n{notes_content}\n[/center]\n\n" + # Add notes section + if base.strip() and base.strip().lower() != "ptp": + desc_parts.append(self._build_notes_section(base)) - # BBCode conversions - desc = bbcode.convert_pre_to_code(desc) - desc = bbcode.convert_hide_to_spoiler(desc) - if not comparison: - desc = bbcode.convert_comparison_to_collapse(desc, 1000) + # Combine all parts + desc = "".join(desc_parts) - # MINIMAL TVC FIX - desc = desc.replace("[center]\n", "[center]") + # Apply BBCode transformations + desc = self._apply_bbcode_transforms(desc, comparison) - # Collapse any run of 2+ newlines into a single newline - desc = re.sub(r"\n{2,}", "\n", desc) + # Normalize formatting (TVC-specific) + desc = self._normalize_tvc_formatting(desc) - # Fallback + # Ensure non-empty description if not desc.strip(): desc = "[center][i]No description available[/i][/center]\n" + # Add signature if signature: desc += f"\n{signature}\n" - try: - # Write description asynchronously - def _write(): - with open(descfile_path, "w", encoding="utf-8") as f: - f.write(desc) - - await asyncio.to_thread(_write) - except Exception as e: - console.print(f"[yellow]Warning: Failed to write description file: {e}[/yellow]") + # Write to file + await self._write_description_file(descfile_path, desc) return desc - def get_links(self, meta: Meta) -> str: """ Returns a BBCode string with an 'External Info Sources' heading and icon links. No [center] tags are included; callers control layout. """ - parts: list[str] = [] - - parts.append("\n[b]External Info Sources:[/b]\n\n") - - if meta.get('imdb_id', 0): - parts.append(f"[URL={meta.get('imdb_info', {}).get('imdb_url', '')}][img]{self.config['IMAGES']['imdb_75']}[/img][/URL]") - - if meta.get('tmdb_id', 0): - parts.append(f"[URL=https://www.themoviedb.org/{meta.get('category', '').lower()}/{meta['tmdb_id']}][img]{self.config['IMAGES']['tmdb_75']}[/img][/URL]") - - if meta.get('tvdb_id', 0): - parts.append(f"[URL=https://www.thetvdb.com/?id={meta['tvdb_id']}&tab=series][img]{self.config['IMAGES']['tvdb_75']}[/img][/URL]") - - if meta.get('tvmaze_id', 0): - parts.append(f"[URL=https://www.tvmaze.com/shows/{meta['tvmaze_id']}][img]{self.config['IMAGES']['tvmaze_75']}[/img][/URL]") - - if meta.get('mal_id', 0): - parts.append(f"[URL=https://myanimelist.net/anime/{meta['mal_id']}][img]{self.config['IMAGES']['mal_75']}[/img][/URL]") - - return " ".join(parts) + parts = ["\n[b]External Info Sources:[/b]\n\n"] + + # Configuration for each link type: (meta_key, url_builder, config_image_key) + link_configs = [ + ( + 'imdb_id', + lambda m: m.get('imdb_info', {}).get('imdb_url', ''), + 'imdb_75' + ), + ( + 'tmdb_id', + lambda m: f"https://www.themoviedb.org/{m.get('category', '').lower()}/{m['tmdb_id']}", + 'tmdb_75' + ), + ( + 'tvdb_id', + lambda m: f"https://www.thetvdb.com/?id={m['tvdb_id']}&tab=series", + 'tvdb_75' + ), + ( + 'tvmaze_id', + lambda m: f"https://www.tvmaze.com/shows/{m['tvmaze_id']}", + 'tvmaze_75' + ), + ( + 'mal_id', + lambda m: f"https://myanimelist.net/anime/{m['mal_id']}", + 'mal_75' + ), + ] + + for id_key, url_func, img_key in link_configs: + if meta.get(id_key, 0): + url = url_func(meta) + img = self.config['IMAGES'][img_key] + parts.append(f"[URL={url}][img]{img}[/img][/URL]") + + return " ".join(parts) + "\n" # get subs function # used in naming conventions From 2007f29bbb511f275fff6217fd1c052801f794d2 Mon Sep 17 00:00:00 2001 From: Lusephur Date: Thu, 12 Feb 2026 16:37:38 +0000 Subject: [PATCH 06/11] Refactor disc info building for uniform processing fixing first disc BDMV summary is silently skipped in description. And added note that presently TVC doesn't allow BDMV uploads --- src/trackers/TVC.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index 4bb3a0737..06a974040 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -93,30 +93,36 @@ def _ensure_desc_directory(self, meta: Meta, tracker: str) -> str: return os.path.join(desc_dir, f"[{tracker}]DESCRIPTION.txt") def _build_disc_info(self, discs: list[dict[str, Any]]) -> str: - """Build disc information section.""" - parts = [] + """ + Build disc information section. - # First disc (DVD VOB) - if discs[0]['type'] == "DVD": - parts.append( - f"[spoiler=VOB MediaInfo][code]{discs[0]['vob_mi']}[/code][/spoiler]\n\n" - ) + Note: TVC does not currently accept BDMV/Blu-ray disc releases (only HDTV and WEB-DL). + This method exists for code compatibility/future use and will not be called during + normal TVC uploads due to the disc blocking in search_existing(). + """ + parts = [] - # Additional discs - for disc in discs[1:]: + # Process all discs uniformly + for disc in discs: if disc['type'] == "BDMV": name = disc.get('name', 'BDINFO') parts.append( f"[spoiler={name}][code]{disc['summary']}[/code][/spoiler]\n\n" ) elif disc['type'] == "DVD": - parts.append(f"{disc['name']}:\n") - vob_name = os.path.basename(disc['vob']) - ifo_name = os.path.basename(disc['ifo']) - parts.append( - f"[spoiler={vob_name}][code]{disc['vob_mi']}[/code][/spoiler] " - f"[spoiler={ifo_name}][code]{disc['ifo_mi']}[/code][/spoiler]\n\n" - ) + # For first DVD disc, use VOB MediaInfo label + if not parts: # First disc + parts.append( + f"[spoiler=VOB MediaInfo][code]{disc['vob_mi']}[/code][/spoiler]\n\n" + ) + else: # Subsequent DVD discs + parts.append(f"{disc['name']}:\n") + vob_name = os.path.basename(disc['vob']) + ifo_name = os.path.basename(disc['ifo']) + parts.append( + f"[spoiler={vob_name}][code]{disc['vob_mi']}[/code][/spoiler] " + f"[spoiler={ifo_name}][code]{disc['ifo_mi']}[/code][/spoiler]\n\n" + ) return "".join(parts) From d08d0a269eb0160056839e0a948d971b22853261 Mon Sep 17 00:00:00 2001 From: Lusephur Date: Thu, 12 Feb 2026 17:11:15 +0000 Subject: [PATCH 07/11] Remove unused import of console in TVC.py Remove unused import statement for console. --- src/trackers/TVC.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index 06a974040..7b4a3501a 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -367,7 +367,6 @@ def _write(): await asyncio.to_thread(_write) except Exception as e: - from src.console import console console.print(f"[yellow]Warning: Failed to write description file: {e}[/yellow]") async def get_cat_id(self, genres: str) -> str: From f733fa0c5023e0dfb156c2516d217f13f546dc60 Mon Sep 17 00:00:00 2001 From: Lusephur Date: Sun, 15 Feb 2026 08:58:38 +0000 Subject: [PATCH 08/11] Refactor description building for multi-block style Refactor description building for movie and TV pack releases to use multi-block style with [center] and [pre] tags. Update various parts to improve formatting and consistency. --- src/trackers/TVC.py | 146 +++++++++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 64 deletions(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index 7b4a3501a..b2d1656a2 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -107,21 +107,21 @@ def _build_disc_info(self, discs: list[dict[str, Any]]) -> str: if disc['type'] == "BDMV": name = disc.get('name', 'BDINFO') parts.append( - f"[spoiler={name}][code]{disc['summary']}[/code][/spoiler]\n\n" + f"[center][spoiler={name}][code]{disc['summary']}[/code][/spoiler][/center]\n\n" ) elif disc['type'] == "DVD": # For first DVD disc, use VOB MediaInfo label if not parts: # First disc parts.append( - f"[spoiler=VOB MediaInfo][code]{disc['vob_mi']}[/code][/spoiler]\n\n" + f"[center][spoiler=VOB MediaInfo][code]{disc['vob_mi']}[/code][/spoiler][/center]\n\n" ) else: # Subsequent DVD discs - parts.append(f"{disc['name']}:\n") vob_name = os.path.basename(disc['vob']) ifo_name = os.path.basename(disc['ifo']) parts.append( + f"[center]{disc['name']}:\n" f"[spoiler={vob_name}][code]{disc['vob_mi']}[/code][/spoiler] " - f"[spoiler={ifo_name}][code]{disc['ifo_mi']}[/code][/spoiler]\n\n" + f"[spoiler={ifo_name}][code]{disc['ifo_mi']}[/code][/spoiler][/center]\n\n" ) return "".join(parts) @@ -131,37 +131,42 @@ def _build_movie_desc( meta: Meta, image_list: list[dict[str, Any]] ) -> str: - """Build description for movie releases.""" + """Build description for movie releases (multi-block style).""" parts = [] - # Release date info (before center tag) + # Release date info in its own center block rd_info = self._get_movie_release_info(meta) if rd_info: parts.append(f"[center]{rd_info}[/center]\n\n") - parts.append("[center]") - - # Logo + # Logo in its own center block if meta.get("logo"): logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) - parts.append(f"[img={logo_size}]{meta['logo']}[/img]\n") + parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") + + # Title in pre tags + parts.append(f"[center][pre][b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}[/pre][/center]\n\n") - # Title and overview - parts.append(f"[b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}\n") - parts.append(f"{meta.get('overview', '').strip()}\n") + # Overview in pre tags + overview = meta.get('overview', '').strip() + if overview: + parts.append(f"[center][pre]{overview}[/pre][/center]\n\n") # Release date if 'release_date' in meta: formatted_date = self.format_date_ddmmyyyy(meta['release_date']) - parts.append(f"[b]Released on:[/b] {formatted_date}\n") + parts.append(f"[center][b]Released on:[/b] {formatted_date}[/center]\n\n") # External links - parts.append(self.get_links(meta)) + links = self.get_links(meta).strip() + if links: + parts.append(f"[center]{links}[/center]\n\n") # Screenshots - parts.append(self._add_screenshots(meta, image_list)) + screenshots = self._add_screenshots(meta, image_list).strip() + if screenshots: + parts.append(f"[center]{screenshots}[/center]\n\n") - parts.append("[/center]\n\n") return "".join(parts) def _build_tv_pack_desc( @@ -169,36 +174,40 @@ def _build_tv_pack_desc( meta: Meta, image_list: list[dict[str, Any]] ) -> str: - """Build description for TV pack releases.""" + """Build description for TV pack releases (multi-block style).""" if 'season_air_first_date' not in meta: return "" - parts = ["[center]"] + parts = [] - # Logo + # Logo in its own center block if meta.get("logo"): logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) - parts.append(f"[img={logo_size}]{meta['logo']}[/img]\n") + parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") - # Series info + # Series info in pre tags channel = meta.get('networks', 'N/A') airdate = self.format_date_ddmmyyyy(meta.get('season_air_first_date') or "") + series_name = meta.get('season_name', 'Unknown Series') - parts.append(f"[b]Series Title:[/b] {meta.get('season_name', 'Unknown Series')}\n") - parts.append(f"[b]This series premiered on:[/b] {channel} on {airdate}\n") + parts.append(f"[center][pre][b]Series Title:[/b] {series_name}[/pre][/center]\n\n") + parts.append(f"[center][b]This series premiered on:[/b] {channel} on {airdate}[/center]\n\n") # Episode list if meta.get('episodes'): - parts.append("\n[b]Episode List[/b]\n") - parts.append(self._build_episode_list(meta['episodes'])) + episode_list = self._build_episode_list(meta['episodes']) + parts.append(f"[center][pre][b]Episode List[/b]\n{episode_list}[/pre][/center]\n\n") # External links - parts.append(self.get_links(meta)) + links = self.get_links(meta).strip() + if links: + parts.append(f"[center]{links}[/center]\n\n") # Screenshots - parts.append(self._add_screenshots(meta, image_list)) + screenshots = self._add_screenshots(meta, image_list).strip() + if screenshots: + parts.append(f"[center]{screenshots}[/center]\n\n") - parts.append("[/center]\n\n") return "".join(parts) def _build_episode_desc( @@ -206,45 +215,51 @@ def _build_episode_desc( meta: Meta, image_list: list[dict[str, Any]] ) -> str: - """Build description for single episode releases.""" + """Build description for single episode releases (FNP-style multi-block).""" if 'episode_overview' not in meta: return "" - parts = ["[center]"] + parts = [] - # Logo + # Logo in its own center block if meta.get("logo"): logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) - parts.append(f"[img={logo_size}]{meta['logo']}[/img]\n\n") + parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") - # Episode title + # Episode title in pre tags with its own center block episode_name = meta.get('episode_name', '').strip() if episode_name: - parts.append(f"[b]Episode Title:[/b] {episode_name}\n\n") + parts.append(f"[center][pre]{episode_name}[/pre][/center]\n\n") - # Overview (sentence by sentence) + # Overview in pre tags with its own center block overview = meta.get('episode_overview', '').strip() - parts.append(self._format_overview(overview)) + if overview: + parts.append(f"[center][pre]{overview}[/pre][/center]\n\n") - # Airdate + # Broadcast info (without pre tags) if 'episode_airdate' in meta: channel = meta.get('networks', 'N/A') formatted_date = self.format_date_ddmmyyyy(meta['episode_airdate']) - parts.append(f"[b]Broadcast on:[/b] {channel} on {formatted_date}\n") + parts.append(f"[center][b]Broadcast on:[/b] {channel} on {formatted_date}[/center]\n\n") # External links - parts.append(self.get_links(meta)) + links = self.get_links(meta).strip() + if links: + parts.append(f"[center]{links}[/center]\n\n") # Screenshots - parts.append(self._add_screenshots(meta, image_list)) + screenshots = self._add_screenshots(meta, image_list).strip() + if screenshots: + parts.append(f"[center]{screenshots}[/center]\n\n") - parts.append("[/center]\n\n") return "".join(parts) def _build_fallback_desc(self, meta: Meta) -> str: """Build fallback description for other categories.""" overview = meta.get('overview', '').strip() - return f"[center]{overview}[/center]\n\n" + if overview: + return f"[center][pre]{overview}[/pre][/center]\n\n" + return "" def _get_movie_release_info(self, meta: Meta) -> str: """Extract movie release date information.""" @@ -302,7 +317,7 @@ def _format_overview(self, overview: str) -> str: if not sentences: sentences = [overview] - return "".join(s.rstrip() + "\n" for s in sentences) + "\n" + return "".join(s.rstrip() + "\n" for s in sentences) def _add_screenshots( self, @@ -319,25 +334,25 @@ def _add_screenshots( if not image_list or screens_count < required_count: return "" - parts = ["\n[b]Screenshots[/b]\n"] + parts = ["[b]Screenshots[/b]\n"] for img in image_list[:required_count]: web_url = img['web_url'] img_url = img['img_url'] parts.append( - f"[url={web_url}][img={self.SCREENSHOT_THUMB_SIZE}]{img_url}[/img][/url]" + f"[url={web_url}][img={self.SCREENSHOT_THUMB_SIZE}]{img_url}[/img][/url] " ) return "".join(parts) def _build_notes_section(self, base: str) -> str: """Build notes/extra info section.""" - return f"[center][b]Notes / Extra Info[/b]\n{base.strip()}\n[/center]\n\n" + return f"[center][b]Notes / Extra Info[/b]\n[pre]{base.strip()}[/pre][/center]\n\n" def _apply_bbcode_transforms(self, desc: str, comparison: bool) -> str: """Apply BBCode transformations.""" bbcode = BBCODE() - desc = bbcode.convert_pre_to_code(desc) +# desc = bbcode.convert_pre_to_code(desc) desc = bbcode.convert_hide_to_spoiler(desc) if not comparison: @@ -349,12 +364,9 @@ def _apply_bbcode_transforms(self, desc: str, comparison: bool) -> str: return desc def _normalize_tvc_formatting(self, desc: str) -> str: - """Normalize whitespace and formatting for TVC.""" - # TVC-specific: remove newline immediately after [center] - desc = desc.replace("[center]\n", "[center]") - - # Collapse any run of 2+ newlines into a single newline - desc = re.sub(r"\n{2,}", "\n", desc) + """Normalize whitespace for TVC (multi-block style).""" + # Collapse any run of 3+ newlines into exactly 2 (preserve spacing between blocks) + desc = re.sub(r"\n{3,}", "\n\n", desc) return desc @@ -882,11 +894,11 @@ async def unit3d_edit_desc( comparison: bool = False, ) -> str: """ - Build and write the tracker-specific DESCRIPTION.txt file. + Build and write the tracker-specific DESCRIPTION.txt file (FNP multi-block style). Constructs BBCode-formatted description text for discs, TV packs, - episodes, or movies, including screenshots and notes. Always writes - a non-empty description file to tmp//[TVC]DESCRIPTION.txt. + episodes, or movies using multiple separate [center] blocks with [pre] tags. + Always writes a non-empty description file to tmp//[TVC]DESCRIPTION.txt. Args: meta: Metadata dictionary for the release. @@ -949,10 +961,10 @@ async def unit3d_edit_desc( def get_links(self, meta: Meta) -> str: """ - Returns a BBCode string with an 'External Info Sources' heading and icon links. - No [center] tags are included; callers control layout. + Returns a BBCode string with icon links (for multi-block layout). + No [center] tags or extra newlines - caller handles layout. """ - parts = ["\n[b]External Info Sources:[/b]\n\n"] + parts = ["[b]External Info Sources:[/b]\n\n"] # Configuration for each link type: (meta_key, url_builder, config_image_key) link_configs = [ @@ -985,11 +997,17 @@ def get_links(self, meta: Meta) -> str: for id_key, url_func, img_key in link_configs: if meta.get(id_key, 0): - url = url_func(meta) - img = self.config['IMAGES'][img_key] - parts.append(f"[URL={url}][img]{img}[/img][/URL]") + # Safe config access with fallback + if 'IMAGES' in self.config and img_key in self.config['IMAGES']: + url = url_func(meta) + img = self.config['IMAGES'][img_key] + parts.append(f"[URL={url}][img]{img}[/img][/URL] ") + else: + # Fallback: just include the URL without an image icon + url = url_func(meta) + parts.append(f"[URL={url}]{id_key.replace('_id', '').upper()}[/URL] ") - return " ".join(parts) + "\n" + return "".join(parts) # get subs function # used in naming conventions From 44b67759bb4ef2886274c6a1fc594ddc4f1868fa Mon Sep 17 00:00:00 2001 From: Lusephur Date: Sun, 15 Feb 2026 09:03:41 +0000 Subject: [PATCH 09/11] Refactor output formatting by removing 'pre' tags Removed 'pre' tags from various parts of the output for a cleaner presentation. TVC doesn't support them --- src/trackers/TVC.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index b2d1656a2..87010f2c2 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -145,12 +145,12 @@ def _build_movie_desc( parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") # Title in pre tags - parts.append(f"[center][pre][b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}[/pre][/center]\n\n") + parts.append(f"[center][b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}[/center]\n\n") # Overview in pre tags overview = meta.get('overview', '').strip() if overview: - parts.append(f"[center][pre]{overview}[/pre][/center]\n\n") + parts.append(f"[center]{overview}[/center]\n\n") # Release date if 'release_date' in meta: @@ -190,13 +190,13 @@ def _build_tv_pack_desc( airdate = self.format_date_ddmmyyyy(meta.get('season_air_first_date') or "") series_name = meta.get('season_name', 'Unknown Series') - parts.append(f"[center][pre][b]Series Title:[/b] {series_name}[/pre][/center]\n\n") + parts.append(f"[center][b]Series Title:[/b] {series_name}[/center]\n\n") parts.append(f"[center][b]This series premiered on:[/b] {channel} on {airdate}[/center]\n\n") # Episode list if meta.get('episodes'): episode_list = self._build_episode_list(meta['episodes']) - parts.append(f"[center][pre][b]Episode List[/b]\n{episode_list}[/pre][/center]\n\n") + parts.append(f"[center][b]Episode List[/b]\n{episode_list}[/center]\n\n") # External links links = self.get_links(meta).strip() @@ -229,12 +229,12 @@ def _build_episode_desc( # Episode title in pre tags with its own center block episode_name = meta.get('episode_name', '').strip() if episode_name: - parts.append(f"[center][pre]{episode_name}[/pre][/center]\n\n") + parts.append(f"[center]{episode_name}[/center]\n\n") # Overview in pre tags with its own center block overview = meta.get('episode_overview', '').strip() if overview: - parts.append(f"[center][pre]{overview}[/pre][/center]\n\n") + parts.append(f"[center]{overview}[/center]\n\n") # Broadcast info (without pre tags) if 'episode_airdate' in meta: @@ -258,7 +258,7 @@ def _build_fallback_desc(self, meta: Meta) -> str: """Build fallback description for other categories.""" overview = meta.get('overview', '').strip() if overview: - return f"[center][pre]{overview}[/pre][/center]\n\n" + return f"[center]{overview}[/center]\n\n" return "" def _get_movie_release_info(self, meta: Meta) -> str: @@ -347,7 +347,7 @@ def _add_screenshots( def _build_notes_section(self, base: str) -> str: """Build notes/extra info section.""" - return f"[center][b]Notes / Extra Info[/b]\n[pre]{base.strip()}[/pre][/center]\n\n" + return f"[center][b]Notes / Extra Info[/b]\n{base.strip()}[/center]\n\n" def _apply_bbcode_transforms(self, desc: str, comparison: bool) -> str: """Apply BBCode transformations.""" @@ -897,7 +897,7 @@ async def unit3d_edit_desc( Build and write the tracker-specific DESCRIPTION.txt file (FNP multi-block style). Constructs BBCode-formatted description text for discs, TV packs, - episodes, or movies using multiple separate [center] blocks with [pre] tags. + episodes, or movies using multiple separate [center] blocks. Always writes a non-empty description file to tmp//[TVC]DESCRIPTION.txt. Args: From 935f9c298160b17f440eedefa0b6830498847c9c Mon Sep 17 00:00:00 2001 From: Lusephur Date: Sun, 15 Feb 2026 15:21:36 +0000 Subject: [PATCH 10/11] Refactor description building for releases Updated description formatting for movie and TV releases to remove pre tags and improve clarity. --- src/trackers/TVC.py | 65 +++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index 87010f2c2..15657d916 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -131,7 +131,7 @@ def _build_movie_desc( meta: Meta, image_list: list[dict[str, Any]] ) -> str: - """Build description for movie releases (multi-block style).""" + """Build description for movie releases (multi-block, no pre tags).""" parts = [] # Release date info in its own center block @@ -144,10 +144,10 @@ def _build_movie_desc( logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") - # Title in pre tags + # Title - plain text parts.append(f"[center][b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}[/center]\n\n") - # Overview in pre tags + # Overview - plain text overview = meta.get('overview', '').strip() if overview: parts.append(f"[center]{overview}[/center]\n\n") @@ -174,10 +174,7 @@ def _build_tv_pack_desc( meta: Meta, image_list: list[dict[str, Any]] ) -> str: - """Build description for TV pack releases (multi-block style).""" - if 'season_air_first_date' not in meta: - return "" - + """Build description for TV pack releases (multi-block, no pre tags).""" parts = [] # Logo in its own center block @@ -185,25 +182,26 @@ def _build_tv_pack_desc( logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") - # Series info in pre tags - channel = meta.get('networks', 'N/A') - airdate = self.format_date_ddmmyyyy(meta.get('season_air_first_date') or "") - series_name = meta.get('season_name', 'Unknown Series') + # Series info (optional - only if season data exists) + if 'season_air_first_date' in meta: + channel = meta.get('networks', 'N/A') + airdate = self.format_date_ddmmyyyy(meta.get('season_air_first_date') or "") + series_name = meta.get('season_name', 'Unknown Series') - parts.append(f"[center][b]Series Title:[/b] {series_name}[/center]\n\n") - parts.append(f"[center][b]This series premiered on:[/b] {channel} on {airdate}[/center]\n\n") + parts.append(f"[center][b]Series Title:[/b] {series_name}[/center]\n\n") + parts.append(f"[center][b]This series premiered on:[/b] {channel} on {airdate}[/center]\n\n") - # Episode list + # Episode list (optional) if meta.get('episodes'): episode_list = self._build_episode_list(meta['episodes']) parts.append(f"[center][b]Episode List[/b]\n{episode_list}[/center]\n\n") - # External links + # External links (always attempt to add) links = self.get_links(meta).strip() if links: parts.append(f"[center]{links}[/center]\n\n") - # Screenshots + # Screenshots (always attempt to add) screenshots = self._add_screenshots(meta, image_list).strip() if screenshots: parts.append(f"[center]{screenshots}[/center]\n\n") @@ -215,10 +213,7 @@ def _build_episode_desc( meta: Meta, image_list: list[dict[str, Any]] ) -> str: - """Build description for single episode releases (FNP-style multi-block).""" - if 'episode_overview' not in meta: - return "" - + """Build description for single episode releases (multi-block, no pre tags).""" parts = [] # Logo in its own center block @@ -226,28 +221,28 @@ def _build_episode_desc( logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") - # Episode title in pre tags with its own center block + # Episode title - plain text in center block (optional) episode_name = meta.get('episode_name', '').strip() if episode_name: parts.append(f"[center]{episode_name}[/center]\n\n") - # Overview in pre tags with its own center block + # Overview - plain text in center block (optional) overview = meta.get('episode_overview', '').strip() if overview: parts.append(f"[center]{overview}[/center]\n\n") - # Broadcast info (without pre tags) + # Broadcast info (optional) if 'episode_airdate' in meta: channel = meta.get('networks', 'N/A') formatted_date = self.format_date_ddmmyyyy(meta['episode_airdate']) parts.append(f"[center][b]Broadcast on:[/b] {channel} on {formatted_date}[/center]\n\n") - # External links + # External links (always attempt to add) links = self.get_links(meta).strip() if links: parts.append(f"[center]{links}[/center]\n\n") - # Screenshots + # Screenshots (always attempt to add) screenshots = self._add_screenshots(meta, image_list).strip() if screenshots: parts.append(f"[center]{screenshots}[/center]\n\n") @@ -303,22 +298,6 @@ def _build_episode_list(self, episodes: list[dict[str, Any]]) -> str: return "".join(parts) - def _format_overview(self, overview: str) -> str: - """Format overview text sentence by sentence.""" - if not overview: - return "" - - sentences = [ - s.strip() - for s in re.split(r'(?<=[.!?])\s+', overview) - if s.strip() - ] - - if not sentences: - sentences = [overview] - - return "".join(s.rstrip() + "\n" for s in sentences) - def _add_screenshots( self, meta: Meta, @@ -352,7 +331,7 @@ def _build_notes_section(self, base: str) -> str: def _apply_bbcode_transforms(self, desc: str, comparison: bool) -> str: """Apply BBCode transformations.""" bbcode = BBCODE() -# desc = bbcode.convert_pre_to_code(desc) + desc = bbcode.convert_pre_to_code(desc) desc = bbcode.convert_hide_to_spoiler(desc) if not comparison: @@ -897,7 +876,7 @@ async def unit3d_edit_desc( Build and write the tracker-specific DESCRIPTION.txt file (FNP multi-block style). Constructs BBCode-formatted description text for discs, TV packs, - episodes, or movies using multiple separate [center] blocks. + episodes, or movies using multiple separate [center] blocks with [pre] tags. Always writes a non-empty description file to tmp//[TVC]DESCRIPTION.txt. Args: From 32c25c8959a75194145cdd16c21594e2f85bb46d Mon Sep 17 00:00:00 2001 From: Lusephur Date: Sun, 15 Feb 2026 23:12:21 +0000 Subject: [PATCH 11/11] Fix formatting issues in TVC.py bloody tvc and whatever changes they made to the description box. Still ain't rendering right, the site parses the DESCRIPTION.txt and places

in where a blank line should be, breaking centering and other additions. --- src/trackers/TVC.py | 52 ++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/trackers/TVC.py b/src/trackers/TVC.py index 15657d916..bd0da103c 100644 --- a/src/trackers/TVC.py +++ b/src/trackers/TVC.py @@ -107,13 +107,13 @@ def _build_disc_info(self, discs: list[dict[str, Any]]) -> str: if disc['type'] == "BDMV": name = disc.get('name', 'BDINFO') parts.append( - f"[center][spoiler={name}][code]{disc['summary']}[/code][/spoiler][/center]\n\n" + f"[center][spoiler={name}][code]{disc['summary']}[/code][/spoiler][/center]\n" ) elif disc['type'] == "DVD": # For first DVD disc, use VOB MediaInfo label if not parts: # First disc parts.append( - f"[center][spoiler=VOB MediaInfo][code]{disc['vob_mi']}[/code][/spoiler][/center]\n\n" + f"[center][spoiler=VOB MediaInfo][code]{disc['vob_mi']}[/code][/spoiler][/center]\n" ) else: # Subsequent DVD discs vob_name = os.path.basename(disc['vob']) @@ -121,7 +121,7 @@ def _build_disc_info(self, discs: list[dict[str, Any]]) -> str: parts.append( f"[center]{disc['name']}:\n" f"[spoiler={vob_name}][code]{disc['vob_mi']}[/code][/spoiler] " - f"[spoiler={ifo_name}][code]{disc['ifo_mi']}[/code][/spoiler][/center]\n\n" + f"[spoiler={ifo_name}][code]{disc['ifo_mi']}[/code][/spoiler][/center]\n" ) return "".join(parts) @@ -137,35 +137,35 @@ def _build_movie_desc( # Release date info in its own center block rd_info = self._get_movie_release_info(meta) if rd_info: - parts.append(f"[center]{rd_info}[/center]\n\n") + parts.append(f"[center]{rd_info}[/center]\n") # Logo in its own center block if meta.get("logo"): logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) - parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") + parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n") # Title - plain text - parts.append(f"[center][b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}[/center]\n\n") + parts.append(f"[center][b]Movie Title:[/b] {meta.get('title', 'Unknown Movie')}[/center]\n") # Overview - plain text overview = meta.get('overview', '').strip() if overview: - parts.append(f"[center]{overview}[/center]\n\n") + parts.append(f"[center]{overview}[/center]\n") # Release date if 'release_date' in meta: formatted_date = self.format_date_ddmmyyyy(meta['release_date']) - parts.append(f"[center][b]Released on:[/b] {formatted_date}[/center]\n\n") + parts.append(f"[center][b]Released on:[/b] {formatted_date}[/center]\n") # External links links = self.get_links(meta).strip() if links: - parts.append(f"[center]{links}[/center]\n\n") + parts.append(f"[center]{links}[/center]\n") # Screenshots screenshots = self._add_screenshots(meta, image_list).strip() if screenshots: - parts.append(f"[center]{screenshots}[/center]\n\n") + parts.append(f"[center]{screenshots}[/center]\n") return "".join(parts) @@ -180,31 +180,31 @@ def _build_tv_pack_desc( # Logo in its own center block if meta.get("logo"): logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) - parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") + parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n") # Series info (optional - only if season data exists) if 'season_air_first_date' in meta: channel = meta.get('networks', 'N/A') airdate = self.format_date_ddmmyyyy(meta.get('season_air_first_date') or "") series_name = meta.get('season_name', 'Unknown Series') - - parts.append(f"[center][b]Series Title:[/b] {series_name}[/center]\n\n") - parts.append(f"[center][b]This series premiered on:[/b] {channel} on {airdate}[/center]\n\n") + + parts.append(f"[center][b]Series Title:[/b] {series_name}[/center]\n") + parts.append(f"[center][b]This series premiered on:[/b] {channel} on {airdate}[/center]\n") # Episode list (optional) if meta.get('episodes'): episode_list = self._build_episode_list(meta['episodes']) - parts.append(f"[center][b]Episode List[/b]\n{episode_list}[/center]\n\n") + parts.append(f"[center][b]Episode List[/b]\n{episode_list}[/center]\n") # External links (always attempt to add) links = self.get_links(meta).strip() if links: - parts.append(f"[center]{links}[/center]\n\n") + parts.append(f"[center]{links}[/center]\n") # Screenshots (always attempt to add) screenshots = self._add_screenshots(meta, image_list).strip() if screenshots: - parts.append(f"[center]{screenshots}[/center]\n\n") + parts.append(f"[center]{screenshots}[/center]\n") return "".join(parts) @@ -219,33 +219,33 @@ def _build_episode_desc( # Logo in its own center block if meta.get("logo"): logo_size = self.config['DEFAULT'].get('logo_size', self.DEFAULT_LOGO_SIZE) - parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n\n") + parts.append(f"[center][img={logo_size}]{meta['logo']}[/img][/center]\n") # Episode title - plain text in center block (optional) episode_name = meta.get('episode_name', '').strip() if episode_name: - parts.append(f"[center]{episode_name}[/center]\n\n") + parts.append(f"[center][b]Episode Title:[/b] {episode_name}[/center]\n") # Overview - plain text in center block (optional) overview = meta.get('episode_overview', '').strip() if overview: - parts.append(f"[center]{overview}[/center]\n\n") + parts.append(f"[center]{overview}[/center]\n") # Broadcast info (optional) if 'episode_airdate' in meta: channel = meta.get('networks', 'N/A') formatted_date = self.format_date_ddmmyyyy(meta['episode_airdate']) - parts.append(f"[center][b]Broadcast on:[/b] {channel} on {formatted_date}[/center]\n\n") + parts.append(f"[center][b]Broadcast on:[/b] {channel} on {formatted_date}[/center]\n") # External links (always attempt to add) links = self.get_links(meta).strip() if links: - parts.append(f"[center]{links}[/center]\n\n") + parts.append(f"[center]{links}[/center]\n") # Screenshots (always attempt to add) screenshots = self._add_screenshots(meta, image_list).strip() if screenshots: - parts.append(f"[center]{screenshots}[/center]\n\n") + parts.append(f"[center]{screenshots}[/center]\n") return "".join(parts) @@ -253,7 +253,7 @@ def _build_fallback_desc(self, meta: Meta) -> str: """Build fallback description for other categories.""" overview = meta.get('overview', '').strip() if overview: - return f"[center]{overview}[/center]\n\n" + return f"[center]{overview}[/center]\n" return "" def _get_movie_release_info(self, meta: Meta) -> str: @@ -326,7 +326,7 @@ def _add_screenshots( def _build_notes_section(self, base: str) -> str: """Build notes/extra info section.""" - return f"[center][b]Notes / Extra Info[/b]\n{base.strip()}[/center]\n\n" + return f"[center][b]Notes / Extra Info[/b]\n{base.strip()}[/center]\n" def _apply_bbcode_transforms(self, desc: str, comparison: bool) -> str: """Apply BBCode transformations.""" @@ -346,7 +346,7 @@ def _normalize_tvc_formatting(self, desc: str) -> str: """Normalize whitespace for TVC (multi-block style).""" # Collapse any run of 3+ newlines into exactly 2 (preserve spacing between blocks) desc = re.sub(r"\n{3,}", "\n\n", desc) - + return desc async def _write_description_file(self, filepath: str, content: str) -> None: