fix: correct season list from guessit and add AD fallback via release name - #1385
fix: correct season list from guessit and add AD fallback via release name#1385yippee0903 wants to merge 544 commits into
Conversation
The stale we_checked_them_all flag in meta.json from a previous failed run (where the client torrent was rejected due to skip_nfo) prevented find_existing_torrent from ever being called again on re-run, even though the skip_nfo rejection was removed. Reset both stale flags when BASE.torrent doesn't exist.
is_valid_torrent compared len(torrent.files) with len(filelist), but filelist only contains media files (mkv/mp4) while the torrent may also contain .nfo files. A torrent with 8 mkv + 1 nfo (9 files) vs filelist with 8 mkv would always fail the count check. Filter out .nfo files from the torrent file list before comparing counts, so torrents containing NFO alongside media files can be reused without triggering a full rehash.
The skip_nfo detection was checking ALL trackers in meta['trackers'], including ones the user later skips (e.g. FNP, HHD, STC). Since these UNIT3D trackers are in nfo_skip_trackers, skip_nfo was set to True even when uploading only to C411 + NST. This triggered a full rehash to create BASE_NONFO.torrent that was never used. Changes: - Only consider trackers where tracker_status[tracker]['upload'] is True - Always recompute skip_nfo (don't cache from previous runs via meta.json) because the user may confirm different trackers on re-runs
When a linked directory already exists from a previous run that excluded NFO files (skip_nfo=True), re-running with skip_nfo=False would skip the entire directory (early-return on os.path.exists). The NFO file was never linked, causing 'missing files' in qBit. Changes: - Only early-return for single-file destinations, not directories - Directories always enter the file-by-file linking loop - _try_hardlink skips files that already exist at destination instead of failing with OSError
- Replace Unicode block-drawing characters from CP437 scene NFO art with ASCII equivalents (#, =, ., +) for API/web compatibility - Move NFO content AFTER MediaInfo text so UNIT3D parser can still extract metadata from the field - Add --- NFO --- separator between MediaInfo and NFO sections
Fix/nfo in base torrent
Feat/nst nfo mediainfo
chore(deps): update dependency pillow to v12.2.0
When re-running with --keep-nfo on content that was previously processed without it, BASE.torrent exists but lacks the NFO file. This caused NST's _recreated_torrent_if_nfo cascade to fall through to a full rehash of all content (e.g. 27GB for a season pack). Fix: before the torrent creation block, detect when keep_nfo=True but BASE.torrent has no NFO while NFO files exist on disk. Remove the stale BASE.torrent so the normal client-reuse path (find_existing_torrent) can export the download torrent from qBittorrent — which already includes the NFO — and clone from it without any rehashing.
These CLI boolean flags were not in the overwrite_list, so when meta.json existed from a previous run (keep_meta=True), the saved value (False) would overwrite the CLI --keep-nfo flag (True). This was the actual root cause of the unnecessary rehash: keep_nfo was silently reset to False by merge_meta, so the stale BASE.torrent invalidation check never triggered.
When a release has an NFO file but the existing torrent was created without it, instead of rehashing the entire content (e.g. 27GB at ~51 MiB/s = ~9 min), append the NFO at the end of the torrent file list and recompute only the last piece hash (~6 MB read, <0.2s). BitTorrent v1 allows arbitrary file ordering in the info dict. By placing the NFO after all media files, all existing piece hashes remain valid. Only the last piece needs recomputing. Added _patch_torrent_with_nfo to FrenchTrackerMixin (FRENCH.py) so both NST and C411 benefit. Full rehash remains as fallback.
Two fixes for the NFO display on NST's torrent page: 1. Override get_additional_files to return empty dict — prevents sending the raw CP437 .nfo file as a separate multipart field. NST's UNIT3D renders it with wrong encoding (showing chars like U+00DB instead of block art) and it can hide the MediaInfo text. The NFO content is already embedded in the mediainfo field (sanitized and appended after MediaInfo text). 2. Add Latin-1 byte equivalents to the block char sanitization map so the sanitizer handles both correct CP437 decoding AND wrong Latin-1 decoding of the same bytes.
- FRENCH.py: wrap open/seek/read in last_piece_data loop with try/except, return None on failure (consistent with NFO reader) - upload.py: clear meta['base_torrent_piece_mb'] when invalidating stale BASE.torrent so downstream piece-size gates recalculate - C411.py: allow [C411].torrent as patch_source fallback when BASE.torrent is missing
Torrent.copy(src) carries top-level keys like announce-list, url-list, httpseeds, and nodes from the source torrent. Remove them before writing so only the intended announce URL remains in the patched output.
…te safety - Validate on-disk file size matches torrent metadata before reading tail - Early-return when tracker_name/source_flag/announce_url are missing or placeholder - Wrap patched.write() in try/except to return None on disk errors
Bail out if any NFO filename already exists in the source torrent's file list to prevent duplicate entries and corrupted piece hashes.
Remove get_mediainfo, get_additional_files, and _read_nfo_content overrides from NST — let UNIT3D handle the raw .nfo file directly.
Fix/nfo stale base torrent
- FRENCH.py: Create `_recreated_torrent_if_nfo` function
function and refaco code of some other FRENCH
trackers who needs NFO
- UNIT3D.py: add a call to this function in get_data in
order to simplify code and use it in NST,
TOS, GF, G3MINI
- C411. py: Call function `_recreated_torrent_if_nfo`
instead of duplicate code
FRENCH (fix): Create `_recreated_torrent_if_nfo` function
chore(deps): update dependency click to v8.3.2
…ix mkbrr --include paths, log mkbrr errors - qbittorrent: when torrent_storage_dir is configured but the .torrent file is absent for a given hash, attempt API export (torrents_export) before giving up instead of silently skipping - torrentcreate: strip leading 'FolderName/' prefix from --include patterns passed to mkbrr; mkbrr matches patterns relative to the input directory itself, not its parent, so the prefix caused exit 1 with 'contains no files or only empty files' - torrentcreate: capture mkbrr stdout lines that are not progress/Wrote and print them when the process exits non-zero for easier diagnosis
…mkbrr-include fix: fall back to API export when torrent missing from storage dir, f…
* fix(prep): suppress AKA when nearly identical to main title (case-only diff) When TMDB pre-sets meta['aka'] the IMDb similarity check is skipped (guarded by 'not meta.get(aka)'). Add a post-processing step after that block that clears meta['aka'] when the bare AKA and the main title have a case-insensitive SequenceMatcher ratio >= 0.9 (i.e. they only differ by case or trivial punctuation like 'vs' vs 'Vs'). * fix(ULCX): use case-insensitive comparison for AKA vs IMDb title imdb_aka and imdb_name can differ only by case (e.g. 'Sapne vs Everyone' vs 'Sapne Vs Everyone'). The previous strict '!=' passed this through and injected a redundant 'AKA' into the name. Switch to casefold() comparison.
* fix(G3MINI): reject x264 ENCODE/WEBRIP when Encoded_Library_Settings absent
Scene releases (e.g. x264-TSuNaMi) do not embed encoding parameters in
mediainfo. The previous code silently broke out of the preset check loop,
allowing unverifiable releases to pass G3MINI's 'slow' preset requirement.
Change the silent break to a hard rejection with a descriptive message.
Add TestG3MINIAdditionalChecksX264Preset (8 tests) covering:
- absent / empty Encoded_Library_Settings → rejected
- subme=7 (medium) → rejected
- trellis=1 (medium) → rejected
- subme=8 + trellis=2 (slow) → passes
- subme=10 + trellis=2 (veryslow) → passes
- REMUX / full disc → preset check skipped
* fix(G3MINI): check video_encode not video_codec for x264 preset guard
video.py returns video_codec = format ("AVC"), never the encoder name.
The condition meta["video_codec"] == "x264" therefore never matched,
silently bypassing the slow-preset quality check for every x264 encode.
Fix: use '"x264" in meta["video_encode"].lower()' which reflects the
value actually populated by get_video_encode() for AVC ENCODE/WEBRIP.
Update TestG3MINIAdditionalChecksX264Preset fixtures to use realistic
video_codec="AVC" / video_encode=" x264" values (matching real prep output).
* test(G3MINI): sharpen subme/REMUX preset check test cases
- test_medium_preset_subme7_is_rejected: use trellis=2 so only subme is
the failing value; previously trellis=1 also failed, obscuring the
subme-specific check.
- test_remux_skips_preset_check: pass video_encode=' x264' so the test
proves the REMUX type guard itself bypasses the preset check, not an
absence of x264 in video_encode.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* fix: improve unattended mode * fix: coderabbit
* fix: bloat trackers skip unattended * fix: coderabbit findings
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
) * fix: suppress language-check verbose messages in unattended mode In --unattended mode, trackerstatus.py already prints a concise summary 'Skipped due to specific tracker conditions: TRACKER' after all trackers finish processing. The three console.print blocks in check_language_requirements were printing unconditionally, producing noisy 'Language requirement not met for...' output alongside the summary line. Guard all three print sites with: if not meta.get('unattended') or meta.get('debug'): This silences the verbose output in --unattended mode while keeping it fully visible in interactive mode and in --debug mode. * fix(G3MINI): suppress x264 preset check messages in unattended mode Two console.print calls before 'return False' in get_additional_checks were unconditional, producing noise in --unattended mode: - 'No encoding settings found in mediainfo' - 'x264 encode quality is below the slow preset minimum' Guard both with: if not meta.get('unattended') or meta.get('debug'):
* fix: correct nogroup WEB-DL naming across trackers
Regression: filenames like Cyclo.1995.1080p.WEB-DL.AAC.2.0.H.264.mkv
caused get_tag() to extract '-DL.AAC.2.0.H.264' as a false release
group, which was then appended verbatim to every tracker's release name,
producing duplicated audio/codec tokens.
Root cause: the non-anime regex used (?<=-) as a lookbehind anchor,
positioning after the hyphen in 'WEB-DL'. The negative lookahead
(?!WEB-DL) then checked from 'DL...' and did not fire. Fix: add
(?<!WEB-)(?<!Blu-) lookbehinds so the match is rejected entirely for
source-name hyphens.
Per-tracker nogroup policy enforcement:
- RF: strip invalid placeholder tokens, no suffix (policy: accept without tag)
- LUME: append -NOGROUP when tag is empty/invalid
- YUS: reject in get_additional_checks (no stated policy for untagged releases)
- HHD: reject in get_additional_checks (same)
- HDS: reject in search_existing alongside existing resolution gate
Tests added:
- tests/test_tags.py: 15 regression tests for get_tag (WEB-DL, Blu-ray, real groups)
- TestNogroupWebDL in test_torr9/g3mini/c411/tos/nst/nxm.py (French trackers)
- tests/test_rf.py, test_lume.py, test_yus.py (new files)
- TestNogroupRejection in test_hhd.py
- TestNogroupWebDL in test_lst.py (passthrough)
* fix: case-insensitive source guards and exact invalid-tag matching
- tags.py: add re.IGNORECASE to non-anime regex so Web-DL, BLU-ray etc.
are guarded the same as the canonical casing
- LUME.py, RF.py: replace substring invalid_tags check with exact
normalized_tag membership against {nogrp,nogroup,unknown,unk} and
fix malformed '-unk-' entry; removal uses a single anchored re.sub
- tests: add mixed-case Web-DL/Blu-ray regression tests to test_tags.py;
add -unknown/-unk coverage to test_rf, test_yus, test_hhd
'1080p.UHD.BluRay' is not a valid token combination: UHD denotes a 2160p-capable source, not a resolution modifier. When meta['uhd'] is set but resolution is not '2160p', the UHD token is now cleared before building the release name, producing '1080p.BluRay' instead. Tests added (TestG3MINIUhdStripping): - 1080p REMUX with uhd='UHD' → UHD stripped - 1080p ENCODE with uhd='UHD' → UHD stripped - 2160p REMUX with uhd='UHD' → UHD preserved - 1080p with uhd='' → no UHD introduced
TOS.get_additional_checks rejects scene releases that have no NFO (neither meta['nfo'] nor meta['auto_nfo']). The behaviour was already implemented but had no test coverage. TestTosSceneNfoRequirement (4 cases): - scene=True, nfo=False, auto_nfo=False → rejected (False) - scene=True, nfo=True, auto_nfo=False → accepted (True) - scene=True, nfo=False, auto_nfo=True → accepted (True) - scene=False, nfo=False, auto_nfo=False → accepted (True)
…#222) * fix(C411): strip Criterion Collection edition token from release name C411's server has CRITERION in its banned streaming-platform list. The Criterion Collection is a physical-media label, not a streaming service, but guessit parses it as edition='Criterion' which then lands in the release name and triggers a server-side rejection. Strip .Criterion and .Criterion.Collection (case-insensitive) from the name in C411.get_name after the parent class builds it. Regression: L'argent.1983.Criterion.1080p.BluRay.FLAC.x264-BMF.mkv → C411 upload failed: 'Les noms de plateforme de streaming (CRITERION) ne doivent pas apparaître dans le nom de release.' * fix(C411): scope Criterion stripping to edition field, not release name string Previously the fix stripped '.Criterion' anywhere in the dot-separated release name via re.sub on result['name']. This risked mangling titles that literally contain the word 'Criterion' (e.g. a film called 'Criterion Something' -> 'Something.Criterion.Year...' would have had '.Criterion' stripped from the title). New approach: strip 'Criterion' (or 'Criterion Collection') from meta['edition'] *before* calling super().get_name(), so the logic is scoped to the edition field and cannot touch the title. - Pattern changed from r'\.Criterion(?:\.Collection)?' on the name string to r'\bCriterion(?:\s+Collection)?\b' on the edition string - meta is shallow-copied to avoid mutating the caller's dict - Added test: title containing 'Criterion' with empty edition is preserved intact in the output name
… releases (#226) * fix(torrent): exclude sample files and auto-strip DP folder structure - torrentcreate.py: use explicit per-file relative-path patterns (folder/file.mkv) as include_globs so torf never includes sample files via include-precedence; single-file-in-folder case now sets path to the file directly (no folder) - DP.py: auto-reset keep_folder=False for single-file releases instead of rejecting the upload; print a warning in interactive mode - tests/test_dp.py: add TestDpKeepFolderStrip (4 tests) - tests/test_sample_exclusion.py: add 11 tests covering single-file, multi-file, keep_nfo and keep_folder torrent creation paths * fix(qbit): use per-tracker skip_nfo and trust torrent structure for hardlink src - UNIT3D.upload(): use getattr(self, 'skip_nfo', False) instead of meta['skip_nfo'] so non-skip_nfo trackers (e.g. G3MINI) correctly use BASE.torrent even when another tracker in the same batch is skip_nfo - _resolve_src_and_save_path: remove 'not tracker_wants_nfo' from the single-file guard; torrent_is_multi_file is the ground truth — if the actual torrent is single-file, link just the file regardless of what the tracker nominally wants for NFO - add test_nfo_tracker_single_file_torrent_uses_file_src regression test
* fix(tmdb): include tmdb_id in localized data cache key
tmdb_localized_data.json was keyed only by language+data_type, so
correcting meta['tmdb'] after a wrong auto-detection had no effect —
the cached response for the old ID was still returned and used to
build the TORR9/C411 description.
Add the tmdb_id as a third level in the cache dict:
{language: {data_type: {tmdb_id: response}}}
This ensures a changed TMDB ID always hits the API and the correct
title/overview/cast appear in the description.
* fix(tmdb): always refresh poster from API when tmdb_id is corrected in edit mode
In set_tmdb_metadata, tmdb_other_meta was called with poster=meta.get('poster').
When an existing poster URL was already set (from the first run with the wrong TMDB
ID), the guard 'if poster is None and poster_path:' in tmdb_other_meta prevented
the new poster from being fetched, even though the TMDB API returned the correct
poster_path for the corrected TMDB ID.
Fix: pass poster=None when meta['edit'] is True, forcing tmdb_other_meta to always
derive the poster URL from the API's poster_path for the corrected TMDB entry.
French tracker convention (and TORR9 requirement) is: ...Source.HDR.VideoCodec.AudioCodec-Group but FrenchTrackerMixin was producing: ...Source.HDR.AudioCodec.VideoCodec-Group Add AUDIO_BEFORE_VIDEO class attribute (default True) to FrenchTrackerMixin.get_name() with a local _av() helper that respects the flag. Set AUDIO_BEFORE_VIDEO = False on TORR9 so only TORR9 names swap the order; all other French trackers (C411, HDF, GF, NST, G3MINI, TOS, NXM) keep the existing audio-first order unchanged. Tests: add TestTorr9VideoBeforeAudio to verify video codec precedes audio for ENCODE and WEBDL types, and that the class attribute is explicitly False.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
#233) * fix: four bug fixes — TV pack NFO torrent, accent normalization, NOTAG fallback, AD-only audio Bug 1 – Chicago.Fire.S12 NFO-only torrent (torrentcreate.py, trackersetup.py, upload.py) - keep_nfo branch with empty filelist fell back to include=['*.nfo'] only; fix: fall back to extension globs (*.mkv, *.mp4, *.ts, *.nfo) when filelist is empty. - determine_keep_nfo() used os.listdir() (non-recursive); fix: use glob with recursive=True so NFO files in TV-pack episode sub-folders are detected. - Stale BASE.torrent check in upload.py had the same os.listdir() deficiency; replaced with the same recursive glob pattern. Bug 2 – 'Bon Appétit' → 'Bon.Apptit' in _fr_clean (trackers/FRENCH.py) - TMDB sometimes returns titles in NFD unicode form (combining accents). unidecode() turned the bare combining accent U+0301 into '' silently. - Fix: add unicodedata.normalize('NFC', text) as the very first step of _fr_clean() so that both NFC and NFD inputs produce the same output. Bug 2b – NOTAG when TV-pack folder has no group tag (tags.py) - tv_pack path used meta['uuid'] as basename_stripped → regex never matched. - Fix: use the parent folder name first; if that yields no tag AND the release is a tv_pack or keep_folder file, retry with the episode filename as a fallback so that per-episode group tags are correctly detected. Bug 3 – AD-only audio: warn + skip in unattended mode (audio.py, upload.py) - When every non-commentary/non-compat audio track is an Audio Description track, meta['ad_only_audio'] = True is set in _get_audio_v2(). - upload.py checks the flag early: prints a prominent warning, skips automatically in unattended mode, and prompts for confirmation otherwise. Tests: added TestGetTagTVPackFallback (test_tags.py), TestFrCleanNFC (test_fr_nfc.py), TestKeepNfoEmptyFilelist (test_keep_nfo_empty_filelist.py). 1278 passed, 1 skipped. * fix: address review findings — AD flag scoping, recursive torrent walk, case-insensitive NFO, sidecar detection audio.py: scope has_ad to non-commentary/non-compat Audio tracks so that an 'Audio Description commentary' doesn't falsely trigger the AD-only flag. Explicitly set meta['ad_only_audio'] = False when the condition is not met to prevent stale values from a prior gather_prep run blocking future uploads. torrentcreate.py: replace flat extension globs ('*.mkv' etc.) in both empty-filelist fallback branches with an os.walk recursive discovery that builds explicit relative paths (FolderName/Season 01/ep.mkv), so TV packs with nested episode sub-folders are correctly included in the torrent. trackersetup.py: post-filter glob results with .lower().endswith('.nfo') for case-insensitive detection on Linux; catches 'Release.NFO' / 'Release.Nfo' that the glob pattern '*.nfo' would miss. upload.py: extend the stale-torrent nfo_on_disk check to handle single-file uploads by checking for a stem-matched sidecar .nfo next to the file, mirroring determine_keep_nfo(); apply the same case-insensitive post-filter to the directory branch. tests/test_keep_nfo_empty_filelist.py: add test_empty_filelist_nested_episodes_included to assert that MKV files placed under a 'Season 01/' sub-directory are included when filelist is empty. 1279 passed, 1 skipped. * fix: case-insensitive sidecar NFO, exact nested-path assertions trackersetup.py: replace os.path.isfile(stem + '.nfo') with a case-insensitive directory scan so 'Movie.NFO' / 'Movie.Nfo' are detected on Linux alongside the lowercase form. upload.py: apply the same case-insensitive sidecar scan for the single-file nfo_on_disk branch, replacing the hard-coded '.nfo' suffix check. tests/test_keep_nfo_empty_filelist.py: replace loose count-based assertions with endswith() checks on the exact nested paths ('Season 01/Show.S01E01.mkv', 'Season 01/Show.S01E02.mkv', '<pack_name>.nfo') so the test validates the actual file layout rather than just the count.
… name - getseasonep: guessit may return a list for `season` when a title word like "Seasons" and a year both parse as season candidates (e.g. The.Four.Seasons.2025.S02 → [2025, 2]). Normalise by stripping the year-valued entry and taking the first remainder, so S02 is preserved instead of silently falling back to S01 via the TypeError handler. - audio: add path-based AD-only fallback for releases where MediaInfo track Title fields are empty (common on ATVP/streamer WEB-DL). When the release folder name matches the AD_TRACK_RE pattern and there is only one non-special audio track, set ad_only_audio=True so the upload is blocked/warned in upload.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
os.path.basename returns "" for paths ending with a separator (e.g. "/dir/"), causing the AD release-name fallback to silently no-op. Use os.path.normpath first to strip trailing separators before calling basename, consistent with how paths are handled elsewhere. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thanks for taking the time to contribute to this project. Upload Assistant is currently in a complete rewrite, and no new development is being conducted on this python source at this time. If you have come this far, please feel free to leave open, any pull requests regarding new sites being added to the source, as these can serve as the baseline for later conversion. If your pull request relates to a critical bug, this will be addressed in this code base, and a new release published as needed. If your pull request only addresses a quite minor bug, it is not likely to be addressed in this code base. Details for the new code base will follow at a later date. |
|
Important Review skippedToo many files! This PR contains 209 files, which is 59 over the limit of 150. To get a review, narrow the scope: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (209)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
smh wrong repo |
Summary
getseasonep: guessit may return alistforseasonwhen a title word like "Seasons" and a year both parse as season candidates (e.g.The.Four.Seasons.2025.S02→[2025, 2]). The previous code didint([2025, 2])which raisesTypeError, silently caught by theexcepthandler, falling back to S01. Fix normalises the list by stripping the year-valued entry and taking the first remainder → S02 is now correctly preserved.audio: adds a path-based AD-only fallback for releases where MediaInfo trackTitlefields are empty (common on ATVP/streamer WEB-DL). When the release folder name matchesAD_TRACK_REand there is only one non-special audio track,ad_only_audio=Trueis set so the upload is blocked/warned inupload.py. Path is normalised withos.path.normpathbeforebasenameto handle directory-style paths with trailing separators.Test plan
The.Four.Seasons.2025.S02) — season should be detected as S02, not S01Titletag in MediaInfo — should be flagged as AD-only and blocked in unattended mode/🤖 Generated with Claude Code