Skip to content

v0.29.0

Latest

Choose a tag to compare

@github-actions github-actions released this 15 Apr 01:10
d4785a7

Highlights

qui/qBittorrent integration is finally here.

Torrent injection

Transcoded torrents can be injected directly into qBittorrent after upload, with optional category and tags. #206

See the setup guide for configuration.

Queue fetch

The new queue fetch command discovers fully downloaded torrents via the qBittorrent API and adds them to the queue.

  • Optionally filter by category for faster discovery.
  • Reading BT_backup is no longer necessary.

See the commands guide for full details.

Terminal hyperlinks

Source display now includes clickable links in supported terminals.

Scene detection

Verify now flags releases that appear to be unmarked scene uploads (PossibleScene), helping avoid uploading transcodes of scene sources.

Smarter duplicate detection

Existing format detection uses less-specific matching, preventing transcoding when the source has empty edition fields but an existing torrent does not. This can be disabled with --allow-less-specific.

Changes

New Features

formats: add less-specific duplicate detection to `ExistingFormatProvider` dd1f18d
  • Add allow_less_specific option to TargetOptions (default false)
  • Add EditionKey::is_less_specific_than to detect when a source edition
    has empty metadata fields where an existing edition does not
  • Make ExistingFormatProvider injectable with two-pass format detection:
    exact edition match, then less-specific duplicate exclusion with debug logging
  • Inject ExistingFormatProvider into SourceProvider
add OSC 8 terminal hyperlinks to `Source` display 8c4178b
  • Add url field to Source with the tracker permalink
  • Add Hyperlink extension trait in hyperlink.rs that wraps
    display text in OSC 8 escape sequences, respecting NO_COLOR,
    CLICOLOR, and tty detection via the colored crate
  • Source::Display now renders the name as a clickable link in
    supported terminals, degrading gracefully in unsupported ones
verify: add `PossibleScene` check for unmarked scene releases 7e2c5c1

Detect torrents where both file_path and file_list contain no
spaces, which is a strong indicator of scene naming conventions.

options: validate `qbit_fetch_categories` and document empty string entries 94d0c70
  • Reject an empty categories list with IsEmpty to prevent a silent no-op
  • Document that an empty string ("") entry fetches torrents without a category
options: support qui reverse proxy URLs for qBittorrent connection 2930ea1
  • Skip qbit_username/qbit_password validation when qbit_url
    contains /proxy/, since qui authenticates via an API key embedded
    in the URL path and ignores forwarded credentials
  • Add UrlInvalidSuffix check on qbit_url to catch trailing slashes
    that would produce malformed //api/v2/... paths at runtime
  • Extract validate_connection helper on QbitOptions so queue fetch
    can reuse the same checks at command execution time
queue: discover torrents via qBittorrent API in `queue fetch` #215 31ca392

Add a new queue fetch subcommand that queries the qBittorrent API
filtered by category and inserts any fully downloaded torrents that
are not already in the queue. Builds queue items directly from API
response data without reading any .torrent files from disk, so
subsequent runs only touch newly added torrents.

  • QueueFetchCommand runs one HTTP call per configured category,
    filters to fully downloaded torrents, dedupes against the existing
    queue by hash, and inserts new items in a single pass.
  • QueueFetchOptions holds a required qbit_queue_categories: Vec<String> driven by #[options(required)].
  • QueueItem::from_qbit_torrent prefers infohash_v1 over the
    hash field so hybrid torrents resolve to their v1 SHA-1 info
    hash (qBittorrent's hash field is the truncated SHA-256 v2 for
    hybrid and v2-only torrents); falls back to hash when
    infohash_v1 is empty; skips torrents whose chosen hash cannot
    be parsed as a valid SHA-1 hex.
  • get_indexer_from_url identifies the indexer from the comment
    URL prefix (redacted.sh/redacted.ch -> red,
    orpheus.network -> ops).
  • queue fetch refuses to run without qbit_url, qbit_username,
    and qbit_password configured.
  • Bumps qbittorrent_api to 0.6.0 for the new hash fields on
    Torrent and the category filter.

upload: inject torrents into qBittorrent #206 8ec691a

Bug Fixes

inspect: tolerate non-ISO-8601 ID3v2 timestamp frames 6a382ce

Try Strict, BestAttempt, then Relaxed parsing when reading MPEG
files. Return the result from the strictest mode that succeeds and warn
about each mode that failed.

  • lofty writes ItemKey::ReleaseDate as a plain text TDRL frame but
    reads TDRL as a TimestampFrame, causing stricter modes to error on
    non-digit characters
  • Add FlacGenerator::with_vorbis_tag for arbitrary Vorbis comments
verify: write source torrent files atomically 2033505

get_source_torrent called File::create_new(&path) before the
download, so any failure after that (download error, SIGKILL, power
loss) left a zero-byte or partial file at the cache path. Subsequent
runs then reused it and lava_torrent failed parsing with "Torrent
should contain 1 and only 1 top-level element, 0 found".

  • Write to {path}.tmp then rename to the final path after flush
  • Add VerifyAction::RenameTorrentFile variant
  • Add test covering the download-failure path
options: propagate config deserialization errors instead of silently continuing 58fe6e8

YAML deserialization and CLI extraction failures in OptionsProvider
were printed to stderr but otherwise ignored, allowing the app to
continue with default values. Collect them as OptionRule variants
so has_errors() blocks the build.

  • Add ConfigDeserialize and CliExtract variants to OptionRule
  • Add OptionsProvider::from_yaml constructor for testing
upload: skip copy operations during dry run 6a80732

Extracted from #206

Co-authored-by: Tomer Horowitz <tomerh@realcommerce.co.il>

Dependencies

upgrade lofty to 0.24 29210b3

Documentation

document torrent client integration options 4e8bde9
  • Add "Torrent client integration" section to SETUP.md covering qui,
    direct qBittorrent API, and autoadd/watch directory methods
  • Shrink the upload autoadd tip in COMMANDS.md to a pointer and add
    a queue fetch subsection under queue management
  • Switch docker-compose.yml example to qBittorrent API injection via
    a qui reverse proxy URL
  • Note the qui assumption in DOCKER.md and drop the now-stale
    BT_backup/autoadd mount bullets
  • Update README.md Upload and Batch/Queue feature bullets to mention
    API injection and queue fetch
enforce `missing_docs` lint and fill in gaps 5086304
  • Enable missing_docs rustc lint plus broken_intra_doc_links, invalid_html_tags, and bare_urls rustdoc lints workspace-wide
  • Add doc comments to previously undocumented pub/pub(crate) items across caesura, caesura_macros, and caesura_options
  • Inject doc comments into code generated by the CommandEnum derive macro so generated CommandArgs, Cli, and flattened fields satisfy the lint
  • Fix style issues in existing docs

Code Style

alphabetize Cargo.toml dependencies 26c7737

Refactor

formats: store `Media` enum instead of `String` in `EditionKey` d4785a7
  • Bump gazelle_api to 0.17.0 for Eq and Hash derives on Media

formats: replace regex with trim_start_matches in EditionKey d1259e4

source: store target formats on `Source` instead of existing formats 39a1287
  • Source.existing replaced with Source.targets pre-computed by SourceProvider
  • Commands read source.targets directly instead of calling TargetFormatProvider
  • Deprecate SourceIssue::Existing, add SourceIssue::NoTargets
  • Remove TargetFormatProvider::get_max_path_length
verify: extract `ApiVerifier` from `VerifyCommand` a264ebc

Move API-only verification checks into a standalone injectable
ApiVerifier struct. VerifyCommand now delegates to it via DI,
removing the inline api_checks method.

verify: extract `TorrentFileProvider` from `VerifyCommand` d1c3c10

Move torrent download/cache logic into a standalone TorrentFileProvider
in utils/source/. VerifyCommand now delegates to it via DI.
Also simplifies PathManager::get_source_torrent_path to accept a
torrent_id: u32 instead of &Source, and adds Debug to
VerifyStatus.

formats: extract `EditionKey` from `ExistingFormatProvider` 3acf28a

Replace inline is_same_release/is_equal_numeric functions with a
reusable EditionKey struct that implements Eq and Hash. The
ExistingFormatProvider now compares EditionKey values directly.

verify: extract inline checks as individual functions #173 b164890
  • Extract 8 API checks from api_checks() into api_verifier.rs
  • Extract 5 stream checks from StreamVerifier::execute() as free functions
  • Extract 6 tag checks from TagVerifier::execute() as free functions
  • Extract check_directory_exists, check_flac_count, check_path_length from flac_checks()
  • Change TagVerifier::execute to return Option<SourceIssue> and propagate errors instead of silently swallowing
  • Remove dead Shortener methods (shorten_album, suggest_track_name, suggest_album_name)
  • Add tests for all extracted check functions
hosting: fix `Queue` DI registration and extract `logger_factory` 8e649b9
  • Move #[injectable] from Queue struct to impl block so #[inject] on from_options is recognized by more-di
  • Replace manual Queue registration with Queue::singleton()
  • Extract inline logger closure to logger_factory fn

Warning

Breaking Change: options: extract `QbitUploadOptions` from `QbitOptions` 68a479f
  • QbitOptions now only holds the connection fields (qbit_url,
    qbit_username, qbit_password) used by any command that talks to
    qBittorrent
  • New QbitUploadOptions holds inject_torrent and the five injection
    fields consumed by upload and batch
  • Rename injection fields with the qbit_inject_ prefix for clarity:
    qbit_category -> qbit_inject_category, qbit_tags ->
    qbit_inject_tags, qbit_savepath -> qbit_inject_savepath,
    qbit_paused -> qbit_inject_paused, qbit_skip_checking ->
    qbit_inject_skip_checking
  • Rename qbit_queue_categories -> qbit_fetch_categories to tie it
    directly to the queue fetch command and distinguish it from the
    injection category
  • queue fetch no longer resolves the five injection-only fields since
    it declares only QbitOptions; upload and batch declare both
    structs to get both halves of the config
  • Upload validates its qBittorrent connection at execute() time via
    QbitOptions::validate_connection rather than the previous build-time
    inject_torrent gate, matching how queue fetch validates
replace tracker ID strings with typed `Indexer` enum c98ea63
  • Add Indexer enum (Red, Pth, Ops, Other(String)) in options/indexer.rs with serde(from = "String", into = "String") to preserve the on-disk YAML format
  • Implement as_lowercase, to_uppercase, match_with_alts, Display, FromStr, From<&str>/From<String>, and manual Ord/PartialOrd
  • Normalize Indexer::Other to lowercase on construction and document the invariant on the variant
  • Add tracker URL constants (RED_URL, OPS_URL, RED_TRACKER_URL, OPS_TRACKER_URL) alongside the enum and replace literal URLs in tests and fixtures
  • Replace QueueItem.indexer String with Indexer; update Queue::get_unprocessed and QueueSummary to use the enum
  • Replace SharedOptions::indexer_lowercase() with get_indexer() -> Indexer
  • Migrate TorrentCreator::create/duplicate and TorrentExt::is_source_equal to take Indexer instead of &str/String
  • Use as_lowercase() explicitly in PathManager::get_source_torrent_path so cached filenames stay lowercase
  • Rename path_manager::TRACKER_SUFFIXES to KNOWN_INDEXERS and switch it to [Indexer; 3]
  • Add tests for case-insensitive parsing, unknown values routed to Other, round-trips, legacy YAML compat, and match_with_alts
upload: reshape qBittorrent injection to use DI and dedicated options 61445b1

Refactor of #206 using qbittorrent_api v0.5.0 which adds factory and
trait abstractions that enable DI registration and mock testing.

  • Replace UploadOptions fields with dedicated QbitOptions struct
  • Register qBittorrent client via DI as a singleton instead of per-call instantiation
  • Use explicit qbit_ prefix for all options
  • Gate injection on --inject-torrent flag instead of presence of URL
  • Add to_add_torrent_options() to encapsulate option mapping

hosting: extract factories for shared services and simplify registrations 97ba4b8

use gazelle_api `Format`, `Quality`, `Media`, and `Category` enums 42551f9
  • Replace string matching in ExistingFormat::from_torrent with enum pattern matching
  • Use Category::Music, TargetFormat::to_format(), TargetFormat::to_quality() in UploadForm
  • Change Metadata.media from String to Media, sanitize only Other variant
  • Call .to_string() for SourceIssue::NotSource to preserve serialization compatibility
  • Update deps to gazelle_api 0.15.1

Tests

follow consistent test conventions 5c665fd
  • Introduce Source::mock()
  • Remove test_ prefixes and double-underscore separators
  • Add struct/function name prefix to all test functions
  • Remove orphaned snapshot files