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_specificoption toTargetOptions(defaultfalse)- Add
EditionKey::is_less_specific_thanto detect when a source edition
has empty metadata fields where an existing edition does not- Make
ExistingFormatProviderinjectable with two-pass format detection:
exact edition match, then less-specific duplicate exclusion with debug logging- Inject
ExistingFormatProviderintoSourceProvider
add OSC 8 terminal hyperlinks to `Source` display 8c4178b
- Add
urlfield toSourcewith the tracker permalink- Add
Hyperlinkextension trait inhyperlink.rsthat wraps
display text in OSC 8 escape sequences, respectingNO_COLOR,
CLICOLOR, and tty detection via thecoloredcrateSource::Displaynow 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_pathandfile_listcontain 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
IsEmptyto 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_passwordvalidation whenqbit_url
contains/proxy/, since qui authenticates via an API key embedded
in the URL path and ignores forwarded credentials- Add
UrlInvalidSuffixcheck onqbit_urlto catch trailing slashes
that would produce malformed//api/v2/...paths at runtime- Extract
validate_connectionhelper onQbitOptionssoqueue 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 fetchsubcommand 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.torrentfiles from disk, so
subsequent runs only touch newly added torrents.
QueueFetchCommandruns 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.QueueFetchOptionsholds a requiredqbit_queue_categories: Vec<String>driven by#[options(required)].QueueItem::from_qbit_torrentprefersinfohash_v1over the
hashfield so hybrid torrents resolve to their v1 SHA-1 info
hash (qBittorrent'shashfield is the truncated SHA-256 v2 for
hybrid and v2-only torrents); falls back tohashwhen
infohash_v1is empty; skips torrents whose chosen hash cannot
be parsed as a valid SHA-1 hex.get_indexer_from_urlidentifies the indexer from the comment
URL prefix (redacted.sh/redacted.ch->red,
orpheus.network->ops).queue fetchrefuses to run withoutqbit_url,qbit_username,
andqbit_passwordconfigured.- Bumps
qbittorrent_apito0.6.0for the new hash fields on
Torrentand the category filter.
Bug Fixes
inspect: tolerate non-ISO-8601 ID3v2 timestamp frames 6a382ce
Try
Strict,BestAttempt, thenRelaxedparsing when reading MPEG
files. Return the result from the strictest mode that succeeds and warn
about each mode that failed.
- lofty writes
ItemKey::ReleaseDateas a plain text TDRL frame but
reads TDRL as aTimestampFrame, causing stricter modes to error on
non-digit characters- Add
FlacGenerator::with_vorbis_tagfor arbitrary Vorbis comments
verify: write source torrent files atomically 2033505
get_source_torrentcalledFile::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 andlava_torrentfailed parsing with "Torrent
should contain 1 and only 1 top-level element, 0 found".
- Write to
{path}.tmpthenrenameto the final path after flush- Add
VerifyAction::RenameTorrentFilevariant- 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 asOptionRulevariants
sohas_errors()blocks the build.
- Add
ConfigDeserializeandCliExtractvariants toOptionRule- Add
OptionsProvider::from_yamlconstructor for testing
Dependencies
upgrade
loftyto 0.24 29210b3
Documentation
document torrent client integration options 4e8bde9
- Add "Torrent client integration" section to
SETUP.mdcovering qui,
direct qBittorrent API, and autoadd/watch directory methods- Shrink the
uploadautoadd tip inCOMMANDS.mdto a pointer and add
aqueue fetchsubsection under queue management- Switch
docker-compose.ymlexample to qBittorrent API injection via
a qui reverse proxy URL- Note the qui assumption in
DOCKER.mdand drop the now-stale
BT_backup/autoadd mount bullets- Update
README.mdUpload and Batch/Queue feature bullets to mention
API injection andqueue fetch
enforce `missing_docs` lint and fill in gaps 5086304
- Enable
missing_docsrustc lint plusbroken_intra_doc_links,invalid_html_tags, andbare_urlsrustdoc lints workspace-wide- Add doc comments to previously undocumented
pub/pub(crate)items acrosscaesura,caesura_macros, andcaesura_options- Inject doc comments into code generated by the
CommandEnumderive macro so generatedCommandArgs,Cli, and flattened fields satisfy the lint- Fix style issues in existing docs
Code Style
alphabetize
Cargo.tomldependencies 26c7737
Refactor
formats: store `Media` enum instead of `String` in `EditionKey` d4785a7
- Bump
gazelle_apito 0.17.0 forEqandHashderives onMedia
formats: replace regex with
trim_start_matchesinEditionKeyd1259e4
source: store target formats on `Source` instead of existing formats 39a1287
Source.existingreplaced withSource.targetspre-computed bySourceProvider- Commands read
source.targetsdirectly instead of callingTargetFormatProvider- Deprecate
SourceIssue::Existing, addSourceIssue::NoTargets- Remove
TargetFormatProvider::get_max_path_length
verify: extract `ApiVerifier` from `VerifyCommand` a264ebc
Move API-only verification checks into a standalone injectable
ApiVerifierstruct.VerifyCommandnow delegates to it via DI,
removing the inlineapi_checksmethod.
verify: extract `TorrentFileProvider` from `VerifyCommand` d1c3c10
Move torrent download/cache logic into a standalone
TorrentFileProvider
inutils/source/.VerifyCommandnow delegates to it via DI.
Also simplifiesPathManager::get_source_torrent_pathto accept a
torrent_id: u32instead of&Source, and addsDebugto
VerifyStatus.
formats: extract `EditionKey` from `ExistingFormatProvider` 3acf28a
Replace inline
is_same_release/is_equal_numericfunctions with a
reusableEditionKeystruct that implementsEqandHash. The
ExistingFormatProvidernow comparesEditionKeyvalues directly.
verify: extract inline checks as individual functions #173 b164890
- Extract 8 API checks from
api_checks()intoapi_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_lengthfromflac_checks()- Change
TagVerifier::executeto returnOption<SourceIssue>and propagate errors instead of silently swallowing- Remove dead
Shortenermethods (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]fromQueuestruct to impl block so#[inject]onfrom_optionsis recognized bymore-di- Replace manual
Queueregistration withQueue::singleton()- Extract inline logger closure to
logger_factoryfn
Warning
Breaking Change: options: extract `QbitUploadOptions` from `QbitOptions` 68a479f
QbitOptionsnow only holds the connection fields (qbit_url,
qbit_username,qbit_password) used by any command that talks to
qBittorrent- New
QbitUploadOptionsholdsinject_torrentand the five injection
fields consumed byuploadandbatch - 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_categoriesto tie it
directly to thequeue fetchcommand and distinguish it from the
injection category queue fetchno longer resolves the five injection-only fields since
it declares onlyQbitOptions;uploadandbatchdeclare both
structs to get both halves of the config- Upload validates its qBittorrent connection at
execute()time via
QbitOptions::validate_connectionrather than the previous build-time
inject_torrentgate, matching howqueue fetchvalidates
replace tracker ID strings with typed `Indexer` enum c98ea63
- Add
Indexerenum (Red,Pth,Ops,Other(String)) inoptions/indexer.rswithserde(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 manualOrd/PartialOrd- Normalize
Indexer::Otherto 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.indexerStringwithIndexer; updateQueue::get_unprocessedandQueueSummaryto use the enum- Replace
SharedOptions::indexer_lowercase()withget_indexer() -> Indexer- Migrate
TorrentCreator::create/duplicateandTorrentExt::is_source_equalto takeIndexerinstead of&str/String- Use
as_lowercase()explicitly inPathManager::get_source_torrent_pathso cached filenames stay lowercase- Rename
path_manager::TRACKER_SUFFIXEStoKNOWN_INDEXERSand switch it to[Indexer; 3]- Add tests for case-insensitive parsing, unknown values routed to
Other, round-trips, legacy YAML compat, andmatch_with_alts
upload: reshape qBittorrent injection to use DI and dedicated options 61445b1
Refactor of #206 using
qbittorrent_apiv0.5.0 which adds factory and
trait abstractions that enable DI registration and mock testing.
- Replace
UploadOptionsfields with dedicatedQbitOptionsstruct- Register qBittorrent client via DI as a singleton instead of per-call instantiation
- Use explicit
qbit_prefix for all options- Gate injection on
--inject-torrentflag 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_torrentwith enum pattern matching- Use
Category::Music,TargetFormat::to_format(),TargetFormat::to_quality()inUploadForm- Change
Metadata.mediafromStringtoMedia, sanitize onlyOthervariant- Call
.to_string()forSourceIssue::NotSourceto 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