chore: sync with upstream canary and merge 25 open upstream fixes - #9
Merged
Conversation
…randing deleted downloads and blocking re-grabs with 409 Deleting a torrent directly in the client when it was the only active download left its Download record stranded forever, and every re-grab returned HTTP 409 "already active". RemoveOrphansAsync bailed whenever a snapshot returned 0 items while downloads were tracked, on the theory the client might be unreachable, but that condition is already fully excluded upstream by the UsedCachedSnapshot and IsUnavailable guards: the poller surfaces every timeout/cancel/error as a cached or unavailable snapshot, never as a live empty one, so a live empty queue genuinely means the items are gone. Removing the redundant guard lets cleanup proceed on live empty snapshots while preserving the Listenarrs#640 protections unchanged: unreachable clients are still skipped, ImportPending/Ready/ImportBlocked are never terminalized (only Queued/Downloading/Paused are considered), and the per-item 5-minute grace period still shields fresh adds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er's results An HttpClient timeout surfaces as TaskCanceledException, a subclass of OperationCanceledException. The per-indexer catch in the parallel search loop excluded OperationCanceledException, so a single slow/timing-out indexer's exception escaped the catch, propagated out of Task.WhenAll, and discarded every healthy indexer's results for the whole search (zeroing the automatic cycle for the book). No workflow-level cancellation token flows into SearchIndexersAsync, so any OCE there is a per-request timeout: catch it per-indexer, log a warning naming the indexer, and return an empty result for that indexer only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s into the real grab pipeline When no indexers were configured/enabled, SearchIndexersAsync returned GenerateMockIndexerResults: five synthetic releases (fabricated titles, sizes, and magnet/NZB URLs) that flowed into real scoring and the automatic-grab decision and could be handed to a download client. Return an empty result set with a clear warning instead, and remove the now-dead GenerateMockIndexerResults helpers. Nothing in the frontend or tests depended on them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o Default, stranding them unimported; warn when category missing
SABnzbd silently reassigns jobs with an unknown category to Default (*), but
every SABnzbd read mapper filtered slots by the configured category before
matching nzo_id, so a job Listenarr itself grabbed became invisible ("tracked
but 0 in client") and never imported. The queue/history mappers now bypass the
category filter for any slot whose nzo_id is in the monitored set, reconciling
grabbed items by download ID while category filtering still scopes untracked
discovery. The connection tester also probes mode=get_cats and returns an
advisory (not a hard failure) when the configured category is absent from
SABnzbd, so misconfiguration surfaces before jobs strand.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
qBittorrent's download client only had Host/Port, so it always connected at the root path and couldn't reach instances served behind a path-prefixed reverse proxy (Transmission already had this field). Unlike Transmission's urlBase, which replaces the whole RPC path to match Transmission's own configurable --rpc-url-base daemon setting, qBittorrent has no equivalent server-side base path setting (upstream qbittorrent/qBittorrent#21471 and #23467 are both unmerged), so this prepends urlBase as a plain prefix before the fixed /api/v2/... routes instead of replacing them. Closes Listenarrs#690. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All filter options (Active, Freeleech, FreeleechOrVip, Vip, NotVip) were sending unrecognised boolean parameters that MaM silently ignores, causing filters to have no effect on search results. MaM's API uses a single tor[searchType] parameter with specific string values. Replace the five separate boolean parameters with the correct tor[searchType] values matching Prowlarr's working implementation: Active -> tor[searchType]=active Freeleech -> tor[searchType]=fl FreeleechOrVip -> tor[searchType]=fl-VIP Vip -> tor[searchType]=VIP NotVip -> tor[searchType]=nVIP Empirically confirmed: Not VIP filter now returns 1 result instead of 5 (including 3 VIP torrents) for the same query. Fixes Listenarrs#805
…e poll A throw while mapping torrent N escaped the loop walking the response, so torrents N..end were dropped while the poll still reported itself as a healthy live snapshot. The queue simply looked shorter, with nothing to say a row had been lost, and the only warning claimed the client might be unreachable when it had answered fine and answered completely. Guard each torrent individually in both loops, logging the hash and continuing. SabnzbdQueueFetchWorkflow and TransmissionQueueFetchWorkflow already do exactly this, so qBittorrent converges on what its two closest neighbours share rather than introducing a third approach. The hash read now checks ValueKind before GetString(), so a non-string hash cannot throw before the guard is entered. Refs Listenarrs#829
Release titles containing " (very common in usenet subject lines, e.g. embedded sub-titles) crashed the SABnzbd add-file submission with System.ArgumentException from ContentDispositionHeaderValue, because GenericUsenetSourceResolver.SanitizeFileName() only stripped filesystem-invalid characters and left '"'/'\\' untouched. Those are valid on Linux/macOS filesystems but break the multipart Content-Disposition "filename" quoting used when submitting to SABnzbd, so the download silently never reached the client. Fixes Listenarrs#808. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SABnzbd briefly reports an item as status "Completed" in its active
queue before archiving it to history with the real storage path.
MapQueueSlotToQueueItem mapped that slot into a completed QueueItem
regardless, which let QueueItemConverter mark the download Completed
with an empty DownloadPath - permanently blocking import with
"Inconsistency: Download {id} has no path set", since nothing ever
backfills the path afterward.
When status is "completed" but no storage path is present yet, return
null instead of a pathless completed item. Excluding it from the
active-queue result set makes the poller treat the download as
missing, which triggers a same-cycle history lookup - and history
reliably has the storage path by then. This resolves the download via
the code path that already works correctly, rather than completing
early on unreliable telemetry.
Fixes Listenarrs#839.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ExtractTitleStem strips a leading track number and a trailing Part/CD/Disc/Chapter/pt number, but nothing recognises an "N of M" index. A book split as "Title 001 of 498.mp3" therefore produces a distinct stem per file, and BuildStemGroups returns one group per file, so Library Import asks the operator to identify the same book once per chapter. Unlike a bare "(N)", this form is not ambiguous with the series marker the existing comment describes: it carries its own total, so it names a part of a set rather than one work among several. The strip runs before the leading-track strip. A file named "001 of 498.mp3" would otherwise lose its leading "001 " first and be left as "of 498", which is both different in every file and no longer numeric, so it would never reach the folder-name fallback that exists to group exactly this case. Titles are unaffected where the pattern does not fit: "of" needs digits on both sides, so "Jack of Shadows.mp3" keeps its stem, and a trailing plain number still separates two works in one folder.
matchToMetadata collapsed a search result's series array to its first entry and sent only the legacy scalar series/seriesNumber. AudibleBookMetadata already carries SeriesMemberships, and /library/add applies it via AudiobookSeriesMembershipHelper.ApplyToAudiobook, falling back to the single scalar only when no memberships are supplied. So a book in more than one series lost every membership after the first on this path, while the Add New path (AddLibraryModal) preserved them. Build one membership per series entry, ordered, with the first marked primary. The scalar fields stay populated from the primary so nothing downstream changes. The search endpoint's fallback branch fills a series entry's asin with the series name when it cannot re-fetch the book by ASIN. That value used to be discarded because AudibleBookMetadata has no top-level SeriesAsin; a membership would persist it, so only a value shaped like an ASIN is kept.
Two independent faults stop the post-import ASIN write from reaching the file. The import reports success either way, because the enrichment step is deliberately non-fatal, so neither is visible without reading the destination file's tags. The stream is write-only. TagLibAudioTagWriter's file abstraction hands TagLib the lease's metadata write stream, which reaches OpenIndependentWriteStream and opens O_WRONLY on Unix, or NtCreateFile without GenericRead on Windows. Mpeg4.File.Save() parses the existing box headers through that same stream before it writes, so it throws NotSupportedException mid-parse. Open read+write instead, via a new UnixOpenFlags.OpenReadWriteNoFollow() alongside the existing OpenWriteNoFollow(). The tag lookup never matches on MPEG-4. ApplyAsinTag tests `file.Tag is AppleTag`, but an MPEG-4 file's Tag is a CombinedTag wrapping the Apple tag, so the branch is never taken. The Id3v2 and Xiph branches do not match an m4b either, so Save() rewrites an unchanged file and the writer logs success. Ask for the tag by type instead. Only MPEG-4 answers to Apple, so mp3 and flac fall through as before. The second fault matters for how the first is judged: fixing only the stream turns a logged failure into a silent one. That was observed on a build carrying just the stream change, not predicted. Pinning is unaffected. The handle is still opened relative to the pinned parent, still refuses to follow a link, and is still checked against the validated file object. Widening the access mode does not widen what the lease will open. Rebuilt on top of Listenarrs#828, which replaced the hardcoded open-flag constants with UnixOpenFlags. The new method inherits that commit's per-architecture noFollow detection rather than reintroducing a literal.
… parallel ScoreSearchResults fans out with Task.WhenAll, and each task built a SearchResultScorer over the same scoped IIndexerRepository and queried it for the result's indexer. IIndexerRepository is scoped and the ListenArrDbContext behind it is scoped, so a batch of N results issued N concurrent queries against one context. EF rejects a second operation started on a context while another is in flight. The symptom is not a failed request. The scorer catches the exception and logs at Debug, leaving indexerRetention at 0 and skipping the Usenet detection that sets isNzb. So age and retention checks silently score against the wrong assumptions for whichever results lost the race, and the batch comes back plausibly ordered and quietly wrong. Resolve each distinct indexer once, sequentially, before fanning out, and pass the results into the scorer. Three callers reach this: the quality profile controller, download submission, and the six-hour automatic search sweep. This is also fewer queries rather than merely safer ones. A batch commonly carries many results across a handful of indexers, so it goes from one query per result to one per distinct indexer. IndexerSearchWorkflow already fetches its indexers before its own fan-out, so this makes the two agree. The single-result ScoreSearchResult keeps its old behaviour and still queries, since there is nothing to batch there. Asserting on the EF exception would mean racing it, so the test counts the overlap directly: a stub repository records the highest number of calls in flight at once. It reports 12 without the change and 1 with it, and also pins the query count at one per distinct indexer rather than one per result.
…he first A file-mutation journal left on an older protocol version disables filesystem mutations for the whole application. Scan, import and move all return 503 and the startup error is logged once per restart. There is no in-app route to clear the state: git grep FileMutationJournal across listenarr.api returns nothing, so this exception message is the entire brief an operator gets. It named unsupported[0] only, while the query that produced it collects every affected journal and the update marks every one of them NeedsAttention. With three stuck rows an operator learns about one, resolves it, restarts, and meets the next, with no way to know how many remain. Report the count and the identifiers, capped at ten with a remainder, and fold the stored Error text into the message so the reason travels with the list rather than living only in a column nothing surfaces. This does not add a repair mechanism and does not change the refusal. Refusing to proceed on a state that cannot be safely resumed is the right instinct and is deliberate. What is missing is a way out, and designing that is a maintainer's call rather than something to infer. This makes the position knowable in one restart instead of N while that is decided.
…l of them AddDownloadClientHttpClients built a single circuit breaker and passed the same instance to the generic DownloadClient registration and to all four adapter clients. Polly's circuit breaker is stateful: the open and closed state and the failure count live in the policy object. Five registrations sharing one instance is one global breaker, not five, so three transient failures against any one client opened the circuit for every other one, including client types the deployment may not use. It compounds. Polly's simple breaker returns straight to Open on a single failed half-open trial, and with the breaker shared, that trial can be consumed by any of the five clients' next poll. Intermittent flakiness on one can keep re-tripping the breaker and starve the rest long after the original trigger cleared. Each client now builds its own via CreateCircuitBreakerPolicy(). The retry policy is deliberately still shared, because a retry policy is stateless and sharing one is both correct and how Polly is intended to be used. Only the stateful policy needed splitting, and the comment says which is which so the difference is not read as an oversight later. The test drives the policies directly rather than through HttpClient. Going through the named clients would also pass through the retry policy, whose backoff is 2, 4 and 8 seconds, so opening a breaker that way costs about 45 seconds for a property observable in milliseconds. It asserts the instances differ, opens the first, and then asserts the second still executes.
…per item DownloadClientGateway.GetQueueAsync fans out over every queue item, and each item translated its paths by calling IRemotePathMappingService.TranslatePathAsync, which queries the repository for that client's mappings on every call. The service, the repository and the ListenArrDbContext behind them are all scoped, so a queue of N items issued up to 2N concurrent queries against a context that permits one at a time. DownloadClientQueuePoller then runs that whole thing inside its own Task.WhenAll across every enabled client, so the fan-out is nested. This is the origin of the trace in upstream Listenarrs#783. The exception surfaces at DownloadClientQueuePoller.FetchAsync because that is where the await unwinds; the class itself holds no repository and no context and never did. Split the lookup from the translation. IRemotePathMappingService gains a TranslatePath overload that takes mappings the caller has already resolved and does no I/O, and TranslatePathAsync keeps its behaviour by fetching and delegating to it. The gateway resolves once per client before each fan-out and passes the result down. The single-item import path also resolves once, where it is one query either way. Asserting on the EF exception would mean racing it, so the test counts overlap directly: a stub records the highest number of lookups in flight. Ten items carrying two translatable paths each report 10 concurrent lookups without the change and 1 with it, and the lookup count is pinned at 1 so a regression fails even if it somehow avoids overlapping.
…d on Importing indexers from a Prowlarr instance that runs under a URL base stored every proxy URL without that base. The discovery request to /api/v1/indexer is redirected onto the base and Listenarr follows it, so the import reports success, but each indexer's URL was composed from the address that was typed in rather than the one that answered. SendWithValidatedRedirectsAsync already returns the final URI and the workflow discarded it. Keep it, strip the discovery path off the end, and use what is left as the base for BuildProxyUrl and for the tag lookup.
Fold the leading-article tolerance from the previous version of this PR into Listenarrs#717's new book-boundary matcher: SegmentMatchesExpectedTitle now treats a segment and an expected title as equal when they differ only by a leading article ("The"/"A"/"An") on either side, so a folder "Language of Emotions" still attributes to the audiobook "The Language of Emotions" (and vice versa). This stays a full-title equality modulo the article -- it does not reintroduce substring or author-based matching -- so the same-author / different-book boundary guards are preserved. Covered by three new ScanFileDiscoveryTests (dropped article, added article, and a same-author sibling-book guard). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(imports): support weak storage moves - copy and retain sources when durable identity is unavailable - journal compatibility publications and recover registration state - disclose effective move policy across API and UI * test(storage): isolate mount probe tests - Keep identity-state tests independent of Linux mount probing - Preserve dedicated coverage for storage access capability * fix(storage): capture recovery owner id - Avoid re-reading a nullable journal property inside the EF query - Make committed-publication ownership checks statically non-null
[skip ci] Merge: Bump version to 1.3.3
… the book folder ManualImportCompanionImporter passed destinationRoot to EnsureCreatedHierarchyAsync as the managed boundary. destinationRoot is DetermineScanPath over the batch's destination paths, so for a single-book import it is the book folder. LibraryDirectoryOwnershipBoundaryAuthorizer matches the boundary against configured root folders by equivalence rather than containment, so it refused every companion with 'The requested directory boundary is not a configured root folder'. The exception was caught and logged as a warning, so the import reported success and silently dropped the companions. Select the boundary the way the primary audio file's import already does, via LibraryDirectoryOwnershipPlanning.SelectMostSpecificBoundary over the configured roots, falling back to the destination resolution's boundary path. The authorizer is unchanged and keeps its existing strictness; the companion is now authorized against the same root as the audio file it sits beside.
…p store
The four existing companion tests all mock ILibraryDirectoryOwnershipStore with
It.IsAny<string>() for the boundary, so the real authorizer never runs and nothing in the
suite could tell the book folder from the root folder. All five pass unchanged either way,
which is why this shipped.
ImportAsync_ManagedBoundaryIsTheConfiguredRootFolderRatherThanTheBookFolder captures the
boundary argument instead of the outcome and asserts it is the configured root. It fails
with destinationRoot restored ("library/Author/Book" against the expected "library") and
passes with the fix. The four existing call sites gain the new rootFolders argument.
…introduced Listenarrs#864 reshaped the companion pass. It now chooses between EnsureCreatedHierarchyAsync and EnsureAdditiveHierarchyAsync on the publication plan, and with no capability resolver injected a non-durable source takes the additive branch, which this test never stubbed. Capture the boundary from both so the assertion holds whichever runs. It also routes publication through IFileMover.PrepareActionForRegistrationDetailedAsync and a registration lease. Standing that up would mean asserting the mock graph rather than the boundary, which is the one thing this test exists to pin and which is captured before publication is attempted. The count assertion goes, with the reason recorded in place; ManualImportCompanionOwnershipTests still covers the end-to-end path. Verified by reintroducing only the buggy argument: the store then receives the book folder rather than the root folder, and this test fails on exactly that difference.
…ed mappings too The batch lookup added alongside this only covered RemotePath and ContentPath. The SourceFiles loop still called TranslatePathAsync, which queries the repository for the client's mappings on every file. That loop is sequential within an item, but it runs inside GetQueueAsync's Task.WhenAll over items, so the overlap the batch lookup was meant to remove is still there whenever items carry source files. qBittorrent's queue mapper populates SourceFiles from the torrent's file list (QbittorrentResponseMapper.MapQueueItem), and Transmission's does the same, so this is the normal case on a torrent client rather than an edge case. The new test covers items carrying source files, which the existing one did not: it reports 10 concurrent lookups without this change and 1 with it. DownloadClientGatewayTests' mapping mock only stubbed TranslatePathAsync, so it went stale when the source-file loop moved onto TranslatePath and returned null for every path. Stub both.
The registration lease deliberately separates stable byte access (ReadPath)
from public media identity (PublicPath). On Linux the lease's metadata path
is a /proc/{pid}/fd/{fd} descriptor link, and two consumers treat it as if
it were the file.
The scan's embedded-metadata pass called the single-path overload of
ExtractFileMetadataAsync, which builds MetadataFileSource(path, path). The
probe guard tests the public half for an audio extension, a descriptor link
has none, and so the candidate was rejected before ffprobe ran. That pass is
the fallback for candidates path attribution could not claim, so on Linux a
correctly tagged file in an unrecognised folder shape could never be claimed
by any route.
The registered length was stat'ed from the same descriptor path. Stat on the
link reports the length of the link rather than of its target, a constant 64
bytes, so every registered file on Linux recorded Size = 64. Reading the
length from the pinned handle keeps the lease's generation guarantee, since
it never consults the visible path.
Refs Listenarrs#818
Both of these mocked only the single-path overload of ExtractFileMetadataAsync, so once the scan routes through MetadataFileSource the strict mock saw no matching setup, the extractor returned nothing, and every file read as unreadable. That turned a passing suite into two failures that looked like behaviour regressions and were not. ScanAsync_CaseDistinctMetadataFolders_RemainConflicting just needed the overload. ScanAsync_MetadataReplacementAndRestore_ReadsPinnedFileGeneration needed the overload plus a decision about which half of the source its callback reads. It reads ReadPath, because the point of the test is that the scan sees the original generation even while the visible file is swapped underneath it. It now also asserts the other half. PublicPath must still be the candidate as a person sees it, extension included, because on Linux ReadPath is a /proc descriptor link with no extension and anything deriving media identity from it loses the extension entirely. Confirmed load-bearing by collapsing both halves onto the descriptor path: the assertion fails with the real path expected and /proc/<pid>/fd/<fd> observed, which is the defect this branch exists to fix, previously only demonstrable against a running container.
Ported onto Listenarrs#717, where BuildNamingMetadata has moved into DownloadImportService.Naming.cs and the culture-dependent parse is reintroduced there. A position arrives from Audnexus as a string and is not always a decimal: an omnibus sits at "1-4", a prequel at "0", a novella at "1.5". Squeezing it through decimal.TryParse loses the ones that do not parse, and naming then falls through to the track number and writes that into the filename as if it were the series number. The parse also used the server's culture, so "1.5" read as 15 wherever '.' is the group separator. SeriesPositionRaw keeps the original string, naming prefers it, and the remaining decimal parses are pinned to InvariantCulture. On the import side the {SeriesNumber} token is built by a small helper in DownloadImportService.Naming.cs rather than written inline. DownloadImportService.cs is already at the 500 line architecture cap, and the naming partial is where the rest of this logic lives.
AudnexusSeries carries Asin, Name and Position. ConvertAudnexusToMetadata mapped the name and the position but dropped the ASIN, so AudiobookSeriesMembership.SeriesAsin was null for every audiobook even though the column exists for it and the repository preserves it when set. The series ASIN is the stable identifier for a series. A book ASIN is not: it is per-marketplace and per-narrator, and the same work carries different book ASINs in different catalogues. A series name is free text that varies between editions and translations. The series ASIN is stable across both, so (SeriesAsin, SeriesNumber) is the durable way to say "book N of that series". Both the primary and the secondary membership now carry it. The test class inherits BaseTests and carries the Name and Category traits that BackendArchitectureTests.TestClasses_FollowRepositoryConventions now requires. That test landed after this branch was cut. Fixes Listenarrs#767
Rework of this PR onto Listenarrs#717's rewritten file-registration flow. - FfprobeTagMetadataMapper now reads ASIN / AUDIBLE_ASIN / ISBN tags into AudioMetadata, so a scanned file's embedded identifier is available during registration. - New AudiobookFileService.IdentifierAdoption partial adopts that identifier onto a bare audiobook from INSIDE the per-audiobook operation lock (EnsureAudiobookFileCoreAsync), so the write cannot race file ownership or a concurrent audiobook update. A unanimity guard refuses to adopt when the book's linked files carry disagreeing ASINs (a sign of mis-attribution). - The upstream metadata refresh (Audible lookup) runs AFTER the lock is released -- signalled out via a StrongBox -- so the network call never holds the global filesystem lock. It fills only empty fields and never fails the scan. Restores the IAudiobookMetadataRefreshService dropped in the rebase. Tests: FfprobeTagMetadataMapper (incl. AUDIBLE_ASIN dialect + no-overwrite), AudiobookMetadataRefreshService.FillMissingFields, and the coordination test updated for the new constructor dependency. Build + suite green, no regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…xed as "N of M" into one unmatched-scan item
…urnal blocking startup, not just the first
…from the Audnexus series record
…sition that isn't a plain number
…N after an import
… scan # Conflicts: # listenarr.infrastructure/DependencyInjection/Metadata/MetadataRegistrationExtensions.cs
…ies membership, not just the first # Conflicts: # fe/src/__tests__/libraryImport.store.spec.ts
…rs returned five fake mock releases into the real grab pipeline
… longer discards every other indexer's results
…ped live empty-queue snapshots, stranding deleted downloads and blocking re-grabs with 409
…lter hid tracked jobs reassigned to Default, stranding them unimported
…th no path set (race between active queue and history)
… containing quote characters # Conflicts: # tests/Mocks/Api/SabnzbdApiMock.cs
…rrent should not truncate the queue poll
…ort for reverse-proxied instances
…meter for filter options
… per batch, not once per result in parallel
…breaker per client, not one for all of them
…path mappings once per batch, not per item
…RLs from the base Prowlarr answered on
…nion files against the root folder, not the book folder
…atter The aligned `=>` arms in MyAnonamouseRequestFactory fail `format:backend:check`, which the fork's run-tests workflow gates on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… rules CI on ubuntu-24.04 ran the backend suite for the first time: 3 failures out of 3264, all from merged PRs that had not been through upstream review. - Conform AudiobookMetadataRefreshServiceTests (Listenarrs#781), FfprobeTagMetadataMapperTests (Listenarrs#781) and SabnzbdResponseMapperTests (Listenarrs#840) to TestClasses_FollowRepositoryConventions. - Split the claim diagnostics helpers out of AudiobookFileService.cs, which Listenarrs#849 and Listenarrs#781 together pushed to 503 lines against the 500-line cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d the ASIN after an import" This reverts commit ae63b0a.
…client's path mappings once per batch, not per item" This reverts commit f3ca823.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Syncs the fork with
upstream/canaryand pulls in ten open upstream PRs that fixbugs this deployment will actually hit. Nothing fork-only changes here.
Upstream catch-up
Merges
upstream/canary(3 commits, including Listenarrs#864 weak-storage moves). The onlyconflicts were the version numbers — the fork is on 1.4.0, upstream on 1.3.3 — and
1.4.0 is kept everywhere.
Note this brings a new EF migration with it:
20260821141235_AddCompatibilityFilePublication.Upstream PRs merged
Linux/container correctness — each verified present in this tree before merging:
/proc/<pid>/fd/<n>. The scan collapsed both halves onto it, so the extension guard rejected the file and the embedded-metadata pass never ran;FileInfoon the magic link also reports a constant 64 bytesClonePhysicalGenerationre-applied a DB-loaded timestamp that SQLite materializes asUnspecified, tripping the UTC guard — any rescan of a book with an existing physical identity threwArgumentExceptionand failed the scan jobseries[0], so the backend took the legacy scalar path and stored one membershipScan and import quality, ahead of the first large library scan:
----:com.apple.iTunes:ASINatom during scan, so Reload Metadata works on already-tagged files001 of 006into one scan item instead of sixCombinedTagtype test that never matched)SeriesAsinfrom the Audnexus series record"1-4","1.5") were lost through adecimaland reached filenamesDeliberately not taken
Listenarrs#901 fixes the same 64-byte size bug as Listenarrs#849 by a different route and conflicts
with it. Listenarrs#849 is the broader fix and covers both size sites, so Listenarrs#901 adds nothing here.
Listenarrs#892 / Listenarrs#893 (invariant-culture parses) only bite when
LANG/LC_ALLis set; thecontainer runs invariant.
Download-client and indexer fixes (Listenarrs#840, Listenarrs#837, Listenarrs#830, Listenarrs#868, Listenarrs#871, Listenarrs#876, Listenarrs#863, Listenarrs#832)
are real but we have not configured a client yet. Worth revisiting then — Listenarrs#840 in
particular strands SABnzbd downloads permanently.
Listenarrs#878 (weak-storage safety, +1833/-461) is held: large, layered on Listenarrs#864, and worth
understanding on its own before merging, since unraid's
/mnt/useris a FUSE overlayand the weak-storage identity path plausibly applies to us.
Conflict resolutions
Three merges needed hand-resolution:
AddScoped<IAudiobookMetadataRefreshService>landed on the linethe fork had already changed to
AddHttpClient<IOpenLibraryService>with therate-limit retry policy. Both are kept.
libraryImport.store.spec.ts.Both tests are kept.
Verification
dotnet format --verify-no-changescleanfails 687 filesystem tests on the platform gate regardless of this branch)
vue-tsc --build tsconfig.app.jsoncleanAlso confirmed
feat/collection-authors-and-series-groupingstill merges cleanly ontop of this branch.
Second batch: indexer and download-client fixes
Merged after the first batch, on request: Listenarrs#756, Listenarrs#757, Listenarrs#755, Listenarrs#759, Listenarrs#840, Listenarrs#837, Listenarrs#830,
Listenarrs#772, Listenarrs#806, Listenarrs#863, Listenarrs#868, Listenarrs#871, Listenarrs#876, Listenarrs#832.
Two matter regardless of which client we end up configuring:
into the real grab pipeline. That is our current state.
One conflict, in
tests/Mocks/Api/SabnzbdApiMock.cs: Listenarrs#837 and Listenarrs#759 each added abranch to the same
modedispatch chain (addfilevsget_cats). Both kept.Listenarrs#760 was left out. It is the most relevant of the batch — profile-less adds are
silently never searched — but it is
CONFLICTING/DIRTYagainst upstream itself andconflicts here across
LibraryAddWorkflow,LibraryAddService,ILibraryAddServiceand
StartupDbNormalizer. Rebasing someone else's stale PR through the add path doesnot belong in a sync PR.
Listenarrs#803 (MyAnonamouse freeleech wedge) was left out as a feature that spends account
currency, not a fix.
Verification status
Frontend is verified:
vue-tscclean, 601 tests passing.The backend suite could not be run locally. macOS fails 687 filesystem tests on the
platform gate, and running in the Linux dev container against a bind-mounted macOS
worktree fails differently (
Interop.ThrowExceptionForIoErrnoacross the FileMoverand relocation suites) before hanging outright — Docker Desktop's FUSE mount does not
provide the
openat/hardlink/rename semantics those tests exercise.run-tests.ymlonubuntu-24.04is the real verification for this branch.