Skip to content

[TVC] Refactor TVC description generation and fix BBCode parser issues'#1247

Closed
Lusephur wants to merge 14 commits intoAudionut:masterfrom
Lusephur:TVC-new-bbcode-box-rules
Closed

[TVC] Refactor TVC description generation and fix BBCode parser issues'#1247
Lusephur wants to merge 14 commits intoAudionut:masterfrom
Lusephur:TVC-new-bbcode-box-rules

Conversation

@Lusephur
Copy link

@Lusephur Lusephur commented Feb 12, 2026

TVC changed the site use of the bbc description box to something hybrid. if a /n appears after [center] it injects <br> breaking the centring /n/n becomes <br><br>

This is a quick fix, removing the errors the new site parser produces. and some refactoring for easy updating

Summary by CodeRabbit

  • New Features

    • Rich, category-aware descriptions for movies, TV packs, and episodes.
    • Automatic disc details and conditional screenshot galleries when sufficient images exist.
    • Data-driven external links and language-aware categorization during upload.
  • Improvements

    • More consistent BBCode and TVC-specific formatting for cleaner posts.
    • Safer description file handling with asynchronous read/write for more reliable generation and fewer empty or malformed descriptions.

Lusephur and others added 6 commits February 6, 2026 23:43
TVC changed the site use of the bbc description box to something hybrid.
if a /n appears after [center] it injects <br> breaking the centring
/n/n becomes <br><br>

This is a quick fix, removing the errors the new site parser produces.
Collapse multiple newlines into a single newline for cleaner output.
TVC changed the site use of the bbc description box to something hybrid. if a/nappears after [center] it injects <br> breaking the centring/n/nbecomes <br><br>

This is a quick fix, removing the errors the new site parser produces. and some refactoring for ease of editing for others
@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors TVC to modularize DESCRIPTION generation and I/O: adds class constants, many helper methods for movie/TV/episode desc assembly, enhanced BBCode normalization, data-driven get_links, and async read/write for DESCRIPTION files; updates unit3d_edit_desc to the new flow. (34 words)

Changes

Cohort / File(s) Summary
TVC Tracker refactor
src/trackers/TVC.py
Single-file refactor: added constants (DEFAULT_LOGO_SIZE, SCREENSHOT_THUMB_SIZE, COMPARISON_COLLAPSE_THRESHOLD, MIN_SCREENSHOTS_REQUIRED); introduced numerous helper methods for description building and I/O (_read_base_description, _ensure_desc_directory, _build_disc_info, _build_movie_desc, _build_tv_pack_desc, _build_episode_desc, _build_fallback_desc, _get_movie_release_info, _build_episode_list, _format_overview, _add_screenshots, _build_notes_section, _apply_bbcode_transforms, _normalize_tvc_formatting, _write_description_file, etc.); rewrote get_links to use link configs and refactored unit3d_edit_desc. Review focus: BBCode transforms/normalization, async file reads/writes and directory handling, screenshot inclusion/collapse thresholds, and link_configs shape/usage.

Sequence Diagram(s)

(No sequence diagrams generated — changes are internal refactoring of description generation and I/O without a new multi-component sequential feature requiring visualization.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested reviewers

  • Audionut
  • wastaken7

Poem

🐰
I nibble lines and hop through code,
Helpers bloom where once was mode,
Screenshots tucked in tidy rows,
BBCode sings and tidy grows,
A hoppity description ode.

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (11 files):

⚔️ Dockerfile (content)
⚔️ docker-compose.yml (content)
⚔️ docs/docker-gui-wiki-full.md (content)
⚔️ docs/docker-wiki-full.md (content)
⚔️ docs/web-ui-basic.md (content)
⚔️ src/trackers/TVC.py (content)
⚔️ upload.py (content)
⚔️ web_ui/auth.py (content)
⚔️ web_ui/server.py (content)
⚔️ web_ui/static/js/config_app.js (content)
⚔️ web_ui/templates/config.html (content)

These conflicts must be resolved before merging into master.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changes: TVC description generation refactoring and BBCode parser issue fixes, which match the changeset's substantial restructuring of description methods and BBCode handling.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/trackers/TVC.py`:
- Around line 95-121: In _build_disc_info, the first-disc special-case only
handles DVD and skips when discs[0]['type'] == "BDMV", losing its summary;
modify _build_disc_info to handle a BDMV first disc (e.g., if discs[0]['type']
== "BDMV" append its summary in a spoiler like the loop does) or simplify by
removing the special-case and iterating over all discs (use the same BDMV/DVD
handling logic used in the existing for disc in discs[1:] loop) so the BDMV
summary for discs[0] is included; make changes around the first-disc check and
the for loop in _build_disc_info to ensure BDMV summaries are appended.
🧹 Nitpick comments (4)
src/trackers/TVC.py (4)

89-93: Synchronous os.makedirs in async flow.

The rest of the codebase provides COMMON.makedirs as an async wrapper (see src/trackers/COMMON.py line 41). This call is unlikely to block noticeably for local dirs, but for consistency you could use the async version.


355-365: Redundant import of console inside exception handler.

console is already imported at the module level (line 18). The local import on line 364 is unnecessary.

♻️ Proposed fix
         except Exception as e:
-            from src.console import console
             console.print(f"[yellow]Warning: Failed to write description file: {e}[/yellow]")

301-325: Variable required_count serves dual purpose (minimum threshold and display cap).

On line 313 it acts as a minimum (screens_count < required_count), and on line 318 it acts as a maximum (image_list[:required_count]). This works but may confuse future readers. Consider renaming to something like screenshot_count to better reflect its dual role.


800-837: Duplicated episode-list construction logic.

Lines 809–820 and 826–837 are nearly identical blocks building the episodes list from season_info. Consider extracting this into a small helper to reduce duplication.

♻️ Proposed refactor

Extract a helper method:

def _extract_episodes(self, season_info: dict[str, Any]) -> list[dict[str, str]]:
    episodes = []
    for ep in cast(list[dict[str, Any]], season_info.get('episodes', [])):
        season_num = str(ep.get('season_number', 0))
        episode_num = str(ep.get('episode_number', 0))
        code = f"S{season_num.zfill(2)}E{episode_num.zfill(2)}"
        episodes.append({
            "code": code,
            "title": (ep.get("name") or "").strip(),
            "airdate": ep.get("air_date") or "",
            "overview": (ep.get("overview") or "").strip()
        })
    return episodes

Then replace both blocks with meta['episodes'] = self._extract_episodes(season_info).

fixing first disc BDMV summary is silently skipped in description.

And added note that presently TVC doesn't allow BDMV uploads
@Lusephur Lusephur changed the title Tvc new bbcode box rules [TVC] Refactor TVC description generation and fix BBCode parser issues' Feb 12, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/trackers/TVC.py`:
- Around line 307-331: The config value required_count fetched in
_add_screenshots may be a string; coerce it to an int (with try/except) before
using it for comparison and slicing: read required_count from
self.config['TRACKERS'][self.tracker].get('image_count',
self.MIN_SCREENSHOTS_REQUIRED), attempt int(required_count), on ValueError or
TypeError fall back to self.MIN_SCREENSHOTS_REQUIRED, and ensure it's >= 0; then
use that integer for the screens_count < required_count check and for slicing
image_list[:required_count].
- Around line 956-993: The code accesses self.config['IMAGES'][img_key] without
ensuring the IMAGES section or the specific icon key exists, which can raise
KeyError; update the loop that builds parts (the link_configs / for id_key,
url_func, img_key in link_configs) to first check if 'IMAGES' in self.config and
img_key in self.config['IMAGES'] before reading self.config['IMAGES'][img_key],
and if the check fails either skip appending the image link or use a safe
fallback URL/string so missing config entries don't crash the method.
🧹 Nitpick comments (5)
src/trackers/TVC.py (5)

351-359: Double-newlines in description builders are immediately collapsed — consider using single \n for clarity.

Multiple helper methods (e.g., _build_disc_info, _build_movie_desc, _build_notes_section) deliberately emit \n\n, but _normalize_tvc_formatting then collapses all \n{2,}\n. The code works correctly for TVC's parser requirements, but the intentional double-newlines in builders are misleading since they'll never survive to the output. Using single \n in the builders (or adding a comment explaining the collapse) would improve readability.


361-371: Redundant import of console on line 370.

console is already imported at module level (line 18). The local import is unnecessary.

Proposed fix
         except Exception as e:
-            from src.console import console
             console.print(f"[yellow]Warning: Failed to write description file: {e}[/yellow]")

806-843: Duplicated episode-list construction logic between the two branches.

Lines 815–826 and 832–843 are nearly identical — both build the episodes list from season_info. Consider extracting the episode parsing into a small helper to reduce duplication.

Proposed helper extraction
+    def _parse_episodes_from_season(self, season_info: dict[str, Any]) -> list[dict[str, str]]:
+        episodes: list[dict[str, str]] = []
+        for ep in cast(list[dict[str, Any]], season_info.get('episodes', [])):
+            season_num = str(ep.get('season_number', 0))
+            episode_num = str(ep.get('episode_number', 0))
+            code = f"S{season_num.zfill(2)}E{episode_num.zfill(2)}"
+            episodes.append({
+                "code": code,
+                "title": (ep.get("name") or "").strip(),
+                "airdate": ep.get("air_date") or "",
+                "overview": (ep.get("overview") or "").strip()
+            })
+        return episodes

Then both branches become:

meta['episodes'] = self._parse_episodes_from_season(season_info)

291-305: Minor: sentence splitting may break on abbreviations (e.g., "U.S.", "Dr.").

The regex (?<=[.!?])\s+ will split after periods in abbreviations. This is an edge case that rarely matters in TV/movie overviews but worth noting.


550-555: Variable type shadows the Python built-in.

Consider renaming to release_type or media_type to avoid shadowing type().

Comment on lines +307 to +331
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

required_count from config may be a string, causing TypeError on comparison and slicing.

self.config['TRACKERS'][self.tracker].get('image_count', ...) could return a string if loaded from a config file (YAML/INI). Line 319 compares screens_count (int) to required_count (potentially str), and line 324 uses it as a slice bound — both will raise TypeError in Python 3.

Proposed fix
-        required_count = self.config['TRACKERS'][self.tracker].get(
+        required_count = int(self.config['TRACKERS'][self.tracker].get(
             'image_count',
             self.MIN_SCREENSHOTS_REQUIRED
-        )
+        ))
🤖 Prompt for AI Agents
In `@src/trackers/TVC.py` around lines 307 - 331, The config value required_count
fetched in _add_screenshots may be a string; coerce it to an int (with
try/except) before using it for comparison and slicing: read required_count from
self.config['TRACKERS'][self.tracker].get('image_count',
self.MIN_SCREENSHOTS_REQUIRED), attempt int(required_count), on ValueError or
TypeError fall back to self.MIN_SCREENSHOTS_REQUIRED, and ensure it's >= 0; then
use that integer for the screens_count < required_count check and for slicing
image_list[:required_count].

Remove unused import statement for console.
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.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/trackers/TVC.py`:
- Around line 213-255: _build_episode_desc currently returns early when
'episode_overview' is missing which causes logo, broadcast info, external links
and screenshots to be dropped; modify _build_episode_desc (and similarly
_build_tv_pack_desc) to remove the early return and instead treat
episode_overview (or season_air_first_date) as optional: keep the parts list
construction for logo, episode_name, broadcast info, links (get_links) and
screenshots (_add_screenshots) regardless of overview presence, only skip adding
the overview block when it's missing, and return "".join(parts) so
links/screenshots always appear even if TMDB metadata is absent.
🧹 Nitpick comments (3)
src/trackers/TVC.py (3)

355-355: Remove commented-out code.

Line 355 has convert_pre_to_code commented out. If this was intentionally disabled for the new multi-block style using [pre] tags, remove the dead line to keep the code clean.


967-1010: get_links returns a header even when no links are generated.

If none of the ID keys (imdb_id, tmdb_id, etc.) are present in meta, the method returns "[b]External Info Sources:[/b]\n\n" with no actual links. This orphaned header would appear in the description. Consider building the header conditionally or moving it after the loop.

Proposed fix
-        parts = ["[b]External Info Sources:[/b]\n\n"]
+        parts = []
 
         # Configuration for each link type: (meta_key, url_builder, config_image_key)
         link_configs = [
             ...
         ]
 
         for id_key, url_func, img_key in link_configs:
             if meta.get(id_key, 0):
                 ...
 
-        return "".join(parts)
+        if parts:
+            return "[b]External Info Sources:[/b]\n\n" + "".join(parts)
+        return ""

306-320: Remove _format_overview as it is unused code.

This method is defined on line 306 but is never called anywhere in the codebase. No description builders or other methods reference it.

Comment on lines +213 to +255
def _build_episode_desc(
self,
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 ""

parts = []

# 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")

# 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")

# 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")

# 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"[center][b]Broadcast on:[/b] {channel} on {formatted_date}[/center]\n\n")

# External links
links = self.get_links(meta).strip()
if links:
parts.append(f"[center]{links}[/center]\n\n")

# Screenshots
screenshots = self._add_screenshots(meta, image_list).strip()
if screenshots:
parts.append(f"[center]{screenshots}[/center]\n\n")

return "".join(parts)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Early return drops screenshots and links when episode_overview is missing.

If episode_overview is not in meta (e.g., TMDB fetch failed), _build_episode_desc returns "", which silently omits not only the overview but also the logo, broadcast info, external links, and screenshots. The same pattern exists in _build_tv_pack_desc (line 178–179) for missing season_air_first_date.

Consider restructuring so that links and screenshots are always included, and only the missing metadata is skipped:

Proposed approach for _build_episode_desc
     def _build_episode_desc(
         self,
         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 ""
-
         parts = []
 
         # 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")
 
         # 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")
 
         # 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")
🤖 Prompt for AI Agents
In `@src/trackers/TVC.py` around lines 213 - 255, _build_episode_desc currently
returns early when 'episode_overview' is missing which causes logo, broadcast
info, external links and screenshots to be dropped; modify _build_episode_desc
(and similarly _build_tv_pack_desc) to remove the early return and instead treat
episode_overview (or season_air_first_date) as optional: keep the parts list
construction for logo, episode_name, broadcast info, links (get_links) and
screenshots (_add_screenshots) regardless of overview presence, only skip adding
the overview block when it's missing, and return "".join(parts) so
links/screenshots always appear even if TMDB metadata is absent.

Removed 'pre' tags from various parts of the output for a cleaner presentation. TVC doesn't support them
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/trackers/TVC.py (1)

553-558: ⚠️ Potential issue | 🟡 Minor

The not desc check is unreachable.

unit3d_edit_desc guarantees a non-empty return via the fallback on lines 950–951 ("[center][i]No description available[/i][/center]\n"). The warning on line 557 about "DESCRIPTION file not found" is also misleading since unit3d_edit_desc builds the description in-memory — a falsy return would not indicate a missing file.

Proposed fix — remove the dead check
         desc = await self.unit3d_edit_desc(meta, self.tracker, self.signature, image_list)
-        if not desc:
-            console.print(f"[yellow]Warning: DESCRIPTION file not found at {descfile_path}")
-            desc = ""
🧹 Nitpick comments (5)
src/trackers/TVC.py (5)

352-364: Remove commented-out code.

Line 355 contains a commented-out call to convert_pre_to_code. Per the PR objective (removing pre tags), this line should be removed rather than left as a comment.

Proposed fix
     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)

373-382: _write_description_file uses synchronous I/O wrapped in asyncio.to_thread despite aiofiles being available.

The file already imports and uses aiofiles elsewhere (line 662). Consider using it here for consistency.

Proposed fix
     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)
+            async with aiofiles.open(filepath, "w", encoding="utf-8") as f:
+                await f.write(content)
         except Exception as e:
             console.print(f"[yellow]Warning: Failed to write description file: {e}[/yellow]")

927-934: Inconsistent truthiness check for tv_pack.

Lines 929/931 use meta.get('tv_pack') == 1 / != 1, while line 579 uses meta['tv_pack'] as a boolean directly. Since meta['tv_pack'] is always initialized for TV content, prefer consistent boolean truthiness checks.

Proposed fix
-        elif meta['category'] == "TV" and meta.get('tv_pack') == 1:
+        elif meta['category'] == "TV" and meta['tv_pack']:
             desc_parts.append(self._build_tv_pack_desc(meta, image_list))
-        elif meta['category'] == "TV" and meta.get('tv_pack') != 1:
+        elif meta['category'] == "TV" and not meta['tv_pack']:
             desc_parts.append(self._build_episode_desc(meta, image_list))

Based on learnings, meta['tv_pack'] is always initialized for TV content in src/getseasonep.py before tracker upload methods are called.


817-854: Duplicated episode-list construction logic in both branches.

The tv_pack branch (lines 818–837 and 838–854) builds the episodes list identically regardless of whether the data comes from a TMDB API call or meta['tmdb_season_data']. Extract the episode-building loop into a small helper to reduce duplication.

Proposed approach
+    def _extract_episodes_from_season(self, season_info: dict[str, Any]) -> list[dict[str, str]]:
+        """Extract episode list from season info dict."""
+        episodes: list[dict[str, str]] = []
+        for ep in cast(list[dict[str, Any]], season_info.get('episodes', [])):
+            season_num = str(ep.get('season_number', 0))
+            episode_num = str(ep.get('episode_number', 0))
+            code = f"S{season_num.zfill(2)}E{episode_num.zfill(2)}"
+            episodes.append({
+                "code": code,
+                "title": (ep.get("name") or "").strip(),
+                "airdate": ep.get("air_date") or "",
+                "overview": (ep.get("overview") or "").strip()
+            })
+        return episodes

Then both branches simply call:

meta['episodes'] = self._extract_episodes_from_season(season_info)

306-320: Remove unused _format_overview method.

This method is defined but never called anywhere in the codebase. All overview data throughout the file (in _build_movie_desc, _build_tv_pack_desc, and _build_episode_list) is appended directly without formatting.

Either apply this formatting to the actual overview usage sites or remove the method entirely.

Updated description formatting for movie and TV releases to remove pre tags and improve clarity.
bloody tvc and whatever changes they made to the description box. 
Still ain't rendering right, the site parses the DESCRIPTION.txt and places <br><br> in where a blank line should be, breaking centering and other additions.
@Lusephur Lusephur closed this Feb 21, 2026
@Lusephur Lusephur deleted the TVC-new-bbcode-box-rules branch February 21, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants