Skip to content

deps: Bump Xunit.SkippableFact from 1.5.23 to 1.5.85 - #360

Closed
dependabot[bot] wants to merge 608 commits into
mainfrom
dependabot/nuget/main/Xunit.SkippableFact-1.5.85
Closed

dependabot[bot] wants to merge 608 commits into
mainfrom
dependabot/nuget/main/Xunit.SkippableFact-1.5.85

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 31, 2026

Copy link
Copy Markdown
Contributor

Updated Xunit.SkippableFact from 1.5.23 to 1.5.85.

Release notes

Sourced from Xunit.SkippableFact's releases.

1.5.85

What's Changed

New Contributors

Full Changelog: AArnott/Xunit.SkippableFact@v1.5.61...v1.5.85

1.5.61

What's Changed

New Contributors

Full Changelog: AArnott/Xunit.SkippableFact@v1.5.23...v1.5.61

Commits viewable in compare view.

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

…in/RicherTunes/Lidarr.Plugin.Common/dot-github/workflows/packaging-gates.yml-009c622e0a7165ad7d624b243d1bbab5a31b5761

deps(actions)(deps): bump RicherTunes/Lidarr.Plugin.Common/.github/workflows/packaging-gates.yml from b1781427f176d1b6e9a022960bdf6e5a8e3f6369 to 009c622e0a7165ad7d624b243d1bbab5a31b5761
Add DockerSmokeTests that mount the merged plugin DLL into a real Lidarr
container and verify the Tidalarr indexer registers in the API schema.
Tests skip gracefully when Docker is unavailable via [SkippableFact].
test: Docker smoke test infrastructure
…lowing

The indexer's SearchAlbumsAsync, SearchTracksInternalAsync,
GetAlbumDetailsInternalAsync, and SearchEnhancedAsync methods caught
exceptions, reported them via bridge reporters, then returned empty
results. This silently hid failures from callers — Lidarr would show
zero results with no indication that an error occurred.

After this change, all four methods still report errors through the
bridge reporters (IIndexerStatusReporter) but rethrow instead of
returning empty collections. This lets Lidarr surface auth failures,
rate limits, and API errors to users. The happy path is unchanged.

GetAlbumDetailsInternalAsync also gains bridge reporter calls in its
catch block (previously only logged) and SearchEnhancedAsync gains the
same reporter integration for consistency.
fix: stop silently swallowing exceptions in indexer search methods
chore(submodule): bump Common to v1.7.0
- OAuthTokenProviderAdapter: replace bare catch blocks with NLog warnings
  that capture the exception, aiding auth failure diagnosis
- CLAUDE.md / TFM_RATIONALE.md: update stale net6.0 references to net8.0
  to match the actual target framework
- TidalSearchService: replace hardcoded "US" market with configurable
  parameter (default "US"); TidalIndexer now passes Settings.TidalMarket
fix: wave 1 hardening — OAuth logging, net8 docs, market config
…ceCollection

Expand test coverage for three utility classes:

- ConfigPathDefaultsTests: +5 tests covering HOME fallback, last-resort
  default, constant value, and multi-app-name scenarios
- PathValidationExtensionsTests: +4 tests covering whitespace-only input,
  paths with spaces, delegation parity with Common library, root-only
  paths, and typical music file paths
- ServiceCollectionExtensionsTests: +6 new tests verifying TidalModule DI
  registration builds without errors, registers expected service descriptors
  (core, bridge defaults, shared library, HTTP client factories), and
  handles idempotent double-registration

All 13 Wave2 tests pass. Full suite: 305 passed, 0 failed.
test: wave 2 — coverage for ConfigPathDefaults, PathValidation, ServiceCollection
- Add historical notice to 22 docs/ files that are iteration plans,
  proposals, or status snapshots from initial development. Each notice
  directs readers to CLAUDE.md for current guidance.
- Fix TFM_RATIONALE.md: rewrite to reflect net8.0 (was incorrectly
  claiming net6.0 for core and net9.0 for CLI).
- Fix CLAUDE.md: update Production Deployment path from net6.0 to net8.0.
- Fix deployment-smoke-test.md: replace Lidarr 2.14.2.4786 (net6)
  reference with current net8 plugins-branch guidance.
- Fix tidalarr-qobuzarr-architecture.md: add historical notice noting
  net6.0 claims are outdated.
- Update TECH_DEBT_BACKLOG.md: note TidalResiliencePolicy has been
  removed from source; fix net6.0 refs; mark TFM item as done.

Dead code check:
- TidalResiliencePolicy: class removed from src/; only referenced in
  docs/scripts/test.trx artifacts. Noted in TECH_DEBT_BACKLOG for
  cleanup of remaining references.
- FailOnIOTokenStore: actively used in TidalModule.cs and
  TidalOAuthService.cs — NOT dead code.
Take main's more comprehensive net8.0 content (includes CLI info and
future multi-targeting plan), add the net6.0 warning and Docker image
reference from our branch.
docs: wave 3 — audit stale docs, add historical notices
Add 13 new edge-case tests for HostBridge settings mapping:
- Full round-trip tests for all three settings types (TidalarrHostSettings,
  TidalIndexerHostSettings, TidalDownloadClientHostSettings) verifying every
  field is faithfully mapped including boolean fields missed by original tests
- Negative enum value (-1) fallback to Lossless default
- Empty string pass-through for all string fields
- Zero concurrency/delay boundary values
- TidalIndexerHostSettings default constructor defaults
- ToCoreObject extension method dispatch for all three types plus unknown
  type passthrough

Remove stale TidalResiliencePolicy/Polly tech-debt issue from
create-tech-debt-issues.ps1 — both TidalResiliencePolicy and Polly were
already deleted from source.
test: wave 4 — HostBridge mapping edge cases + script cleanup
The plugin has targeted net8.0 since the Lidarr 3.x migration, but several
scripts and docs still referenced net6.0 in code samples, issue titles, and
build paths. This cleans up the stragglers so nothing misleads contributors.
fix: drop remaining net6.0 references
- Add 19 tests for OAuthTokenProviderAdapter covering constructor null
  guard, delegation to ITidalAuth, error swallowing (returns empty/false
  instead of throwing), cache lifecycle, and static properties
- Add null guard to OAuthTokenProviderAdapter constructor (primary
  constructor field initializer)
- Add 5 tests for TidalarrValidationCodes verifying uniqueness, TID-
  prefix convention, uppercase format, and expected code values
- Add NLog PackageReference to test project (required at runtime since
  Tidalarr.csproj excludes NLog runtime assets for ILRepack)
- Doc audit: all net6.0 references in Tidalarr-owned *.md files already
  have historical banners or are in warning context — no changes needed
test: wave 5 — OAuthTokenProvider tests, validation codes, doc audit
Add two new SkippableFact tests that verify the plugin can create
indexer and download client instances after applying valid settings.
This proves DI container resolution works through the ILRepack-merged
DLL, not just that the lifecycle completes.

- Plugin_CreateIndexerAsync_ReturnsNonNull
- Plugin_CreateDownloadClientAsync_ReturnsNonNull_OrNull

Both tests skip gracefully on ReflectionTypeLoadException in case
the isolated ALC cannot resolve host dependencies (Finding 1).
Currently all Runtime tests fail on Tidalarr due to missing host
assemblies (FluentValidation 9.0.0.0, Lidarr.Core) in the sandbox
— the Common-level fix will address this.
…rtions

test: add capability assertions to runtime sandbox tests
…dStatusReporter)

Updates ext/Lidarr.Plugin.Common from d41d2d4 to 62e1aff, picking up:
- fix: harden PluginSandbox — findings 1, 2, and 5 (#448)
- feat: add DefaultDownloadStatusReporter + register in AddBridgeDefaults (#449)
- test: wave 4+5 compliance and coverage improvements (#446, #447)
- chore: wave 1 tech debt cleanup (#443)
- docs: README v1.7.0 update (#445)
…rdening

chore(submodule): bump Common to latest (sandbox hardening)
…st catch narrowing

- Add _settingsLock to synchronize _settings write + RebuildServiceProvider
  atomically in Apply(), ApplySettingsWithDiagnostics(), and Services getter
- Refactor ApplySettingsWithDiagnostics to call MapToSettings once instead of
  twice (was delegating to ValidateSettingsWithDiagnostics which mapped, then
  mapping again)
- Narrow bare catch in Manifest getter to FileNotFoundException and JsonException;
  log warning via NLog for unexpected exceptions
fix: thread safety on Apply + eliminate double MapToSettings + manifest catch narrowing
TidalSearchService accepted a market parameter but only used it for
query optimizer context — the actual API call never received it. The
Tidal API's countryCode query parameter controls market filtering and
was always hardcoded to the OAuth token's country.

Add a countryCode-aware SearchAsync overload to ITidalCore with a
default interface implementation that delegates to the existing method
(no existing implementors broken). TidalApiClient overrides it to pass
countryCode to the API when non-null, falling back to the token's
country. TidalSearchService now forwards the market parameter through
both SearchWithQualityDetectionAsync and SearchByTypeAsync call sites.
fix: plumb market parameter through to Tidal search API
…rename, download logging

- S3: Replace weak >= 4 assertions with exact Assert.Equal(16, ...) in
  PluginSandboxRuntimeTests for both Describe and GetDefaults
- S6: Cache TidalModule as class-level field instead of recreating on
  every RebuildServiceProvider call
- I1: Rename TidalarrHostSettings_ToCore_MapsAllFields to
  _MapsIndexerFields and add explicit assertion that BaseUrl is NOT
  mapped (documenting the intentional gap)
- S4: Add ILogger-based debug logging to bare catch blocks in
  TidalChunkDownloader (file/directory delete) and
  AudioFormatHandler.ProcessAudioFileAsync (file delete cleanup)
chore: review suggestions — assertion strength, module caching, test rename, download logging
RicherTunes and others added 23 commits June 1, 2026 09:03
The Governance workflow's "Allowlist Expiration Check" fails on any
.github/sha-pin-allowlist.json entry whose expiresOn is in the past. Both
entries (multi-plugin-smoke-test.yml and single-plugin-e2e.yml, each
expiresOn 2026-04-01) deferred SHA-pinning the Common reusable workflow
until the smoke test stabilized.

That transition is already complete: both .github/workflows/multi-plugin-smoke-test.yml
and .github/workflows/single-plugin-e2e.yml already pin the Common reusable
workflow to a full 40-hex SHA (@c2aca69b8c94bbba2ff9a4884291f0cc501eb623),
so both allowlist entries are now orphaned. Remove them so the expiration
check passes. The valid (2026-08-01) sync-over-async-allowlist entries are
untouched.
… LidarrNative) (#319)

* fix(ci): build hostless in Governance Quarantine Visibility job

The "Restore and build" step built the full plugin WITHOUT -p:SkipHostBridge,
yet the job prepares only STUB host assemblies. Stub mode excludes
LidarrNative/** while TidalModule still references it -> CS0234, which has
failed the Governance run (Quarantined Test Visibility) since at least
2026-05-25. The job only runs State=Quarantined unit tests (no host-bridge
needed), so build hostless to match the test-project step
(-p:SkipHostBridge=true -p:ExcludeHostBridge=true).

* fix(ci): build Governance QTV job against real Lidarr host assemblies

The "Quarantined Test Visibility" job prepared STUB host assemblies, which
trip Directory.Build.props auto-detect into SkipHostBridge mode (no Lidarr.dll
present) and exclude LidarrNative/**. TidalModule.cs imports the
Tidalarr.Integration.LidarrNative namespace unconditionally, so stub mode fails
with CS0234 — the plugin cannot build hostless. My prior attempt added
-p:SkipHostBridge=true -p:ExcludeHostBridge=true, which excludes the folder
while the dangling reference remained: still CS0234.

Mirror the established real-host-assembly approach used by ci.yml,
test-and-coverage.yml, and nightly.yml (and the fix/nightly-real-host-assemblies
fix): replace the stub-prep step with the Docker-extract of
ghcr.io/hotio/lidarr:pr-plugins-3.1.2.4913 into ext/Lidarr/_output/net8.0, then
build the FULL plugin and test project (incl. the LidarrNative host-bridge) with
no SkipHostBridge/ExcludeHostBridge. Drop -p:SkipHostBridge from the test-run
step so build and run stay internally consistent (run is --no-build).

Validated locally: full plugin + test project build with 0 errors against the
extracted assemblies; `dotnet test --no-build --filter State=Quarantined`
discovers cleanly (0 matches, exit 0).

---------
…ttings, clean markers (#321)

Applied the verified path-to-99 checklist: corrected stale version/metric/
framework claims, removed changelog + wiki duplication, documented remaining
settings/env-vars, cleaned TODO(docval)/phantom blocks, fixed dead links.
Surgical, verified vs source, no code.
Re-pins ext/Lidarr.Plugin.Common to RicherTunes/Lidarr.Plugin.Common@ed3c27b
(current main HEAD) to converge the streaming trio onto one Common SHA and
satisfy the Ecosystem Pin Drift guard. Supersedes the stale bump to 79d2140
(#317). Additive forward bump (manifest parsers, hostbridge size-estimator
seam, MSBuild deps.json-race fix); commonVersion stays 1.18.0-dev.

Both the gitlink and ext-common-sha.txt now equal ed3c27b8edfd2a125665daf7b7d754b3ecf5c341.
…aseBuilder (#320)

Consumer-side dedup, behavior-preserving. Replaces the hand-rolled release-size
and per-quality release-construction logic in TidalLidarrParser with the shared
Common helpers (Lidarr.Plugin.Common.HostBridge, added in Common #595):

- ConvertToReleaseInfosStatic now drives MultiQualityReleaseBuilder (album-level
  fields set once; per-tier format/extra/quality + bitrate via AddQuality). The
  Tidal-specific bits stay local: the 4-tier quality ladder, DetermineTitleMarkers
  bracket tokens, and the per-quality bitrate.
- EstimateAlbumSize delegates the duration*bitrate->bytes arithmetic to
  AlbumSizeEstimator.EstimateBytesFromBitrate; the Tidal duration ladder and
  bitrate-per-quality switch are extracted to BitrateKbpsForQuality /
  EstimateAlbumDurationSeconds (still local, still Tidal-specific). Double
  arithmetic in Common also removes the latent int*int*125 overflow on long
  hi-res albums.

Byte-for-byte equivalent to the prior formulas (pinned by Common's equivalence
tests + tidal's existing parser/size tests: 85 passed / 2 skipped locally).

PIN-FREE BY DESIGN: this PR touches only the source file — no ext-common-sha.txt
or submodule-gitlink change — so it does not collide with the open Common-bump
PRs. It REQUIRES the Common pin to reach a SHA containing #595 (>= 4f44a79);
CI stays red until the coordinating agent's lockstep repin lands, then this
rebases green. Verified locally against Common 4f44a79.
…s DownloadPayloadValidator (#323)

TidalDownloadPayloadValidator was a near-verbatim fork of Common's canonical
DownloadPayloadValidator (same fLaC/OggS/RIFF/ID3/ftyp magic + text-payload rejection +
ValidateOrThrow(span, ext, mime) signature). Delete the fork and call Common's validator
directly at both download call sites (the file already imports Lidarr.Plugin.Common.Utilities).
Common's is a strict superset (adds m4a min-header + JSON/XML detection + file overload), so
behavior is preserved for Tidal's payloads.

The existing characterization tests are repointed to DownloadPayloadValidator and renamed
(TidalDownloadPayloadValidationTests). All four cases (HTML/JSON text reject, FLAC accept,
ftyp/m4a accept, ftyp-as-flac reject) pass unchanged against Common's validator, proving the
migration preserved behavior.

Verified locally: full Tidalarr.Tests suite green (1306 passed, 0 failed, 17 skipped).
First of the cross-plugin audio-payload-validator consolidation (qobuz/amazon next; a Common
EcosystemParityTestBase guard lands once all forks are gone).
…#324)

The wiki had only Home.md (which correctly defers to the README). This adds
three Tidal-specific pages that document operational detail beyond the README,
without duplicating it or Common's wiki — every claim is verified against the
codebase:

- Authentication.md — OAuth 2.0 + PKCE sign-in (UI & CLI), token storage/refresh
  (single-flight), region/market (US default; US/UK/DE/FR/CA/AU/JP), and the
  single-use-code error (TidalOAuthService / TidalInvalidGrantException).
- Quality-and-Formats.md — quality tiers → API params + codecs
  (LOW/HIGH/LOSSLESS/HI_RES_LOSSLESS), chunked DASH/BTS assembly, FLAC extraction
  via FFmpeg stream-copy (default on, skipped if FFmpeg absent), ISRC tags via
  Common's TagLibAudioMetadataApplier, synced lyrics (.lrc via LRCLIB fallback).
- Troubleshooting.md — typed exceptions mapped to cause + fix (auth-code reuse,
  empty/encrypted/unsupported manifests, rate limits, stream-unavailable, FFmpeg
  missing) with the exact error strings from the source, plus backend-health
  buckets (tidal:auth/api/cdn) and the *-validate CLI diagnostics.

Home.md gains a navigation table to the new pages (keeps its canonical-source
deferral to the README and the performance-tuning table). README's Documentation
→ Wiki table is updated to list all four pages.

Verification note: confirmed the README's "writes ISRC tags" claim is TRUE — it
is performed by Common's TagLibAudioMetadataApplier (TSRC/Xiph/MP4), which
tidalarr inherits via BaseStreamingDownloadClient; left it unchanged.

Docs-only; no code touched.
Re-verified vs origin/main:
- docs/README.md + TFM_RATIONALE.md: CLI is net8.0, not net9.0 (TidalCLI.csproj:11).
- TFM_RATIONALE.md + hostbridge-integration.md: removed "plus Common runtime"
  shipping claim — Common/Abstractions are ILRepack-merged + internalized into
  Lidarr.Plugin.Tidalarr.dll and are FORBIDDEN as sidecars (packaging/expected-contents.txt).
- TECH_DEBT_BACKLOG.md: packaging allowlist no longer lists Lidarr.Plugin.Common.dll
  (it's merged/forbidden); allowed set is the plugin DLL + plugin.json.
- hostbridge-integration.md: clarified ToCoreObject is an extension method on
  IHostSettingsMapper, not an interface member (HostSettingsMapper.cs:30).

Note: the DISCOVER finding about TidalarrIndexerPlugin/TidalarrDownloadPlugin was a
misread — the live migration checklist correctly references the *HostSettings types.
Docs-only; no net deletions.
…rack result (#297)

MED (harden campaign). MapToTidalTrackInfo threw InvalidOperationException when a
track DTO had no nested `album`. It runs via .Select(...) over search results
(MapToTidalTrackInfo over trackDtos) and album track lists, so a SINGLE album-less
track threw mid-enumeration and discarded the ENTIRE result (all albums/tracks/
artists in a search; the whole track list for an album). The album-derived fields
were already null-safe (dto.album?...), so the throw was the only fatal part —
removed it; an album-less track now maps with empty album info. Updated the prior
GetTrackAsync_NullAlbum_Throws test (which codified the harmful throw) and added an
album-track batch regression test.
…b) (#326)

Bumps ext/Lidarr.Plugin.Common from ed3c27b to current Common main HEAD
975f275d9ee76be965b57214decaa4c3cb9191cd. Additive forward bump — keeps the
streaming trio (qobuz/tidal/apple) on the same Common pin so the cross-repo
ecosystem-pin-drift guard stays green and shared-behavior fixes reach every
plugin.

New Common since ed3c27b includes the shared CENC sample decryptor (#601,
NIST-verified), #567 (ConfidenceNotProvided triage reason), #568 (release
VERSION stamping), #550/#554/#606 (CI flake/test fixes) and related docs.

Build-sanity: verify-local.ps1 -SkipTests PASS (0 errors, 2 warnings; package +
packaging-closure green). commonVersion stays 1.18.0-dev (Common's current
version) so VersionContractTests remain in lockstep.
…cQueryOptimizer (#328)

TidalSearchService has always accepted an optional IQueryOptimizer and, when
present, runs an optimize -> search -> learn loop. Nothing ever registered the
interface in DI, so the parameter resolved to null and the whole feedback loop
was dead code.

Register Common's dependency-free HeuristicQueryOptimizer (#611) as the
IQueryOptimizer in TidalModule, lighting up that consumer.

Safety: HeuristicQueryOptimizer.OptimizeQueryAsync sets OptimizedQuery.Query
(the PRIMARY search string TidalSearchService actually searches and learns from)
to the whitespace-normalized raw query only -- never a term-dropped rewrite. All
recall-adding rewrites (edition-strip, featured-artist-drop, keyword-reduce) go
into OptimizedQuery.Alternatives, which TidalSearchService does not fan out into.
Net effect: the user-visible primary result path is unchanged (modulo benign
whitespace collapse) while the learning loop becomes live. Live recall
measurement deferred (needs Tidal creds).

Re-pins the Common submodule to 83aa0491 (gitlink + ext-common-sha.txt) since
#611 is newer than the prior pin (975f275d); adoption inherently bumps the pin.

TDD: TidalModuleDiTests gains three tests -- one pinning the dead wire is now
registered (HeuristicQueryOptimizer resolves), one that TidalSearchService
resolves with the optimizer satisfiable, and one asserting the primary query
preserves every essential token (the safety invariant). All 1305 non-Docker
unit tests pass; packaging closure clean (0 forbidden refs).
…-flow (#329)

A user could never re-authenticate Tidal — every attempt failed "OAuth
state mismatch". Root cause: TidalLidarrIndexer.TryExchangeAuthorizationCode
compared the pasted redirect's `state` against the stored PKCE state and, on
any mismatch, called PKCEStateStore.RegenerateCodes() (minting a fresh
code_verifier/code_challenge/state) before aborting. In a manual copy/paste
OAuth flow the stored RedirectUrl is stale, so every indexer Test (Lidarr's
periodic auto-tests + manual clicks) re-entered this path, mismatched, and
regenerated — invalidating whatever auth URL the user had open before they
could finish pasting the redirect. An unwinnable loop (live logs showed the
OAuth URL `state` changing on every Test).

The method's own comment documents the intent to SKIP state validation ("CSRF
protection which isn't relevant in a manual copy/paste OAuth flow"), but the
code contradicted it. Remove the state-mismatch validate+regenerate block so a
mismatched-state callback proceeds straight to ExchangeCodeAsync using the
STORED code_verifier. The pending PKCE state is now STABLE across Tests, so the
real flow converges. PKCE code_verifier<->code_challenge security is fully
preserved (the exchange always uses the stored verifier); the only thing
dropped is the meaningless CSRF state check. Genuinely used/expired codes are
still handled by the existing TidalInvalidGrantException (invalid_grant) path,
and the post-SUCCESS RegenerateCodes is kept. ParseCallbackUrl still requires
`state` to be present (Tidal always returns it).

TDD: TidalLidarrIndexerOAuthStateExchangeTests drives the (now internal)
TryExchangeAuthorizationCode against a real temp-dir PKCEStateStore with a
mocked ITidalAuth. RED before the fix (mismatch returned false, never called
ExchangeCodeAsync); GREEN after (mismatch proceeds and exchanges with the
stored verifier). Full suite: 1255 passed / 7 skipped / 0 failed.
… the parity suite (#330)

Tidalarr now pins a Common that ships Check_DownloadClientStampsRegisteredClientId (#598)
and Check_DownloadClientUsesCommonPayloadValidator (#602). Wire both as [Fact]s so they
actually run in CI and enforce the contracts going forward:
- download-client-id: GetItems() stamps DownloadClientInfo.Id from Definition (never 0) —
  TidalLidarrDownloadClient uses DownloadClientItemClientInfo.FromDownloadClient(this, ...).
- payload-validator: audio validation goes through Common's DownloadPayloadValidator — the
  local TidalDownloadPayloadValidator fork was removed in #323.

Pin-free (no submodule bump); both guards already pass against current source.
…timizer) (#331)

PR #328 lit up tidalarr's previously dead-wired IQueryOptimizer seam by
registering Common's HeuristicQueryOptimizer (TidalModule.cs:196; consumed by
TidalSearchService). Reflect this new capability in the Key features list.
Docs-only.
…rolled docker extraction) (#332)

tidal's CodeQL workflow hand-rolled an inline docker pull/create/cp of the Lidarr plugins-branch
image with a HARDCODED tag (pr-plugins-3.1.2.4913), duplicating extraction logic that drifts from
the rest of the pipeline. Replace it with the shared shim (scripts/extract-lidarr-assemblies.sh,
which delegates to Common's superset extractor) plus the host-FluentValidation removal — matching
qobuzarr's codeql.yml exactly. The Docker image tag is now sourced centrally by the shim, so it
can no longer drift in this workflow.
* chore(submodule): converge Common pin to ed3c27b (current HEAD)

Re-pins ext/Lidarr.Plugin.Common to RicherTunes/Lidarr.Plugin.Common@ed3c27b
(current main HEAD) to converge the streaming trio onto one Common SHA and
satisfy the Ecosystem Pin Drift guard. Supersedes the stale bump to 79d2140
(#317). Additive forward bump (manifest parsers, hostbridge size-estimator
seam, MSBuild deps.json-race fix); commonVersion stays 1.18.0-dev.

Both the gitlink and ext-common-sha.txt now equal ed3c27b8edfd2a125665daf7b7d754b3ecf5c341.

* docs: point CLAUDE.md to Common's canonical ecosystem consolidation & parity discipline

Adds a short pointer (not a restatement — consolidation applies to docs too) to
ext/Lidarr.Plugin.Common/AGENTS.md 'Ecosystem Consolidation & Parity Discipline':
find-once -> sweep-five across the 5 copy-paste-adjacent plugins, push shared logic into
Common (plugins adopt it; the out-of-tree DRM seam stays plugin-owned+public), the parity
matrix is a contract, verify architecture before assuming a bug class sweeps.

* fix(tidal): SnapshotSettings dropped SaveSyncedLyrics + UseLRCLIB (cross-plugin field-drop sweep)

Cross-plugin sweep of the SnapshotSettings field-drop bug class (found first in amazonmusicarr, where the
host snapshot dropped EnableDrm and DRM never activated in background downloads).

Tidal's inline snapshotter copied 8 of the settings' 10 read-write properties — it silently dropped
SaveSyncedLyrics + UseLRCLIB, so the background download (which reads the pre-await snapshot) ignored the
user's lyric settings and used defaults. The existing host->core parity test didn't catch it (it checks a
different mapping, not the snapshotter).

Extracted the snapshotter into an internal SnapshotSettings that reflection-copies EVERY read-write
property — structurally cannot drop a field. Added a unit test pinning the full contract (a reflection
sweep over all properties) + the two previously-dropped lyric fields.

Sweep results: apple's SnapshotSettings already copies all 10 fields (and has a test) — not vulnerable;
qobuz/brainarr have no such snapshot pattern. Candidate to consolidate into a Common helper later.

Build 0-error; 187 download-area tests pass + 2 new snapshot tests.

* fix(download): refuse output paths outside the download root (F-09)

Adversarial review F-09 (MEDIUM): TidalDownloadClient's public write boundaries
(DownloadTrackEnhancedAsync, DownloadTrackWithMetadataAsync) created directories
and wrote/moved/deleted files based on a caller-provided outputPath without
checking it stays inside the configured DownloadPath — a hostile-controlled path
(e.g. via crafted filename/segment) could write outside the download root.

Fix: add IsOutputPathAllowed (canonical-form containment via Common's
PathTraversalGuard.IsPathWithinRoot — resolves "..", defends sibling-prefix +
case-twin) and call EnsureOutputPathAllowed as the first statement in both
methods, before any mkdir/write/move/delete/tag. The system temp dir is allowed
as an alternate root because DownloadTrackAsync stages there before the host
imports.

Mirrors amazon's existing IsPathWithinRoot guards (parity). Sibling sweep of the
"download write containment" class: amazon already guarded, qobuz cleanup is the
separate F-10, apple is parallel-AI-owned, brainarr has no download client.

Tests: TidalDownloadClientPathContainmentTests — 5 pure-helper cases + 2 wired
integration tests (escape path refused with a download-path error, no file
written). RED→GREEN; full download-client suite 99/99.

* perf(download): stream enhanced track assembly to file, not whole-track MemoryStream (F-08)

Adversarial review F-08 (MEDIUM): DownloadTrackEnhancedAsync assembled the full
track via DownloadAndAssembleAsync(manifest), which buffers the entire decrypted
track in a MemoryStream (File.ReadAllBytes + in-memory decrypt), then copied that
to the output FileStream — pinning a large hi-res track's full size on the managed
heap and double-buffering (disk → memory → disk).

Fix: switch to the already-existing file-backed path DownloadAndAssembleToFileStreamAsync,
which assembles to a temp file (DeleteOnClose), decrypts in place on the file
stream, and returns a seekable FileStream. The downstream header-validate + copy
logic is unchanged (works on any seekable stream). Sequential (maxConcurrency 1)
to preserve this path's historical ordering contract.

The MemoryStream overload stays — it remains public API and is covered by the chunk
downloader's own tests; only the enhanced client path moves off it. Tidal-specific
class (amazon/qobuz already stream to disk; apple parallel-AI-owned).

Verified: Enhanced + ChunkDownloader suites 69/69, full Tidalarr.Tests 1315/1332
(17 pre-existing skips), 0 failures.

* docs(lint): make sync-over-async allowlist semantic, drop stale line numbers (F-12)

Identify allowlist entries by file + pattern + method instead of drift-prone line
numbers (the linter matches by file/pattern and reports the accurate current line
at scan time — the JSON 'line' field was redundant + stale, e.g. TidalLidarrIndexer
listed 506 vs actual 596). Part of the cross-plugin F-12 sweep.

* chore(common): converge Common pin to merged #618 SHA 961dfae (F-05)

Adversarial review F-05 (HIGH): re-pin tidal's ext/Lidarr.Plugin.Common submodule +
ext-common-sha.txt from ed3c27b to 961dfaea60710f61f885f101f3c233baf7a650cb
(origin/main after security PR #618 merged). commonVersion stays 1.18.0-dev (matches
merged Common — truthful).

Adoption fix for F-01's strict-by-default SSRF guard: the merged Common makes
ChunkedHttpAssembler SSRF-strict by default. TidalChunkDownloader now constructs it
with RemoteMediaUriPolicy { AllowHttp = true, ResolveDns = false } — the guard's value
for Tidal is blocking literal private/loopback/CGNAT/metadata-host targets (the real
SSRF vectors). AllowHttp preserves the pre-#618 behaviour (Tidal manifests can serve
http segment URLs; the assembler had no scheme restriction before), and ResolveDns is
skipped on the hot path (manifest source is authenticated/trusted). The production
orchestrator uses the stream-provider path, which doesn't hit the URL guard, so no
change needed there.

Verified against merged Common: download/chunk/orchestrator 160/160, full Tidalarr.Tests
1315/1332 (17 pre-existing skips), 0 failures.

* test(fix): use synchronous IProgress in the file-stream progress test (de-flake)

DownloadAndAssembleToFileStreamAsync_WithProgress_ReportsProgress used
System.Progress<T>, whose callbacks marshal asynchronously with no ordering
guarantee, so progressReports[0] could observe the count-2 report before count-1
(Assert.Equal Expected:1 Actual:2 — flaked in CI's Test-with-Coverage, passed
locally). Switched to the file's existing SyncProgress<T> (synchronous, inline)
which the other ordered-assert tests already use, making order + count
deterministic. Pre-existing flake; not introduced by the F-05/F-08 work.

---------
Bumps ext/Lidarr.Plugin.Common + ext-common-sha.txt 961dfae→a398fc4 (origin/main after
Wave-P2: #619 date-parse Retry-After InvariantCulture, #612 OAuth 401-retry body replay,
#608 DASH malformed-manifest memory-exhaustion guard, #614 clamp negative Retry-After).
#608 lands in tidal's DASH manifest path. commonVersion stays 1.18.0-dev. Manifest/chunk/
download suites 262/262 against the new Common.
…2-02) (#342)

TidalChunkDownloader hardcoded RemoteMediaUriPolicy{AllowHttp=true, ResolveDns=false}
for segment fetches — the ResolveDns=false was a Wave-P0 test-driven shortcut the
Round-2 review flagged as weakening SSRF. AllowHttp=true is retained (Tidal serves
http segment URLs); ResolveDns returns to its strict default (true) so a segment host
that resolves to an internal address (rebinding / compromised CDN record) is rejected.

ValidateChunkAccessibilityAsync issued a raw _httpClient.GetAsync(chunkUrls[0]) with
no SSRF guard — now routed through Common's MediaRedirectSafeSender, which validates
the target (and every redirect hop) against the policy before the fetch.

The downloader takes an optional segmentPolicy; tests inject TidalTestPolicies.Resolving
(AllowHttp=true + a deterministic DnsResolver) so synthetic non-resolving hosts classify
as public — instead of weakening the production policy.

Re-pins ext/Lidarr.Plugin.Common to 3f8c4df (DnsResolver seam + MediaRedirectSafeSender +
label-boundary host-suffix). 1321 unit + 36 parity green.
…a plugin subdir (R2-11) (#343)

IsOutputPathAllowed accepted any path under the system temp dir as a legitimate write
root (besides the configured DownloadPath). %TEMP% is world-writable and shared across
processes, so that allowed a caller-supplied outputPath to land anywhere in it. Narrow
the alternate root to a plugin-owned %TEMP%/tidalarr subdir (PluginTempRoot), and stage
GetTempFilePath there so the legitimate staging path stays inside the allowed root.

Containment stays canonical-form (PathTraversalGuard), so the sibling-prefix case
"%TEMP%/tidalarr_evil" is refused, not just "%TEMP%/other".

TDD: under-subdir accepted; elsewhere-in-%TEMP% and sibling-prefix rejected. 51 download
tests green.
…(LOOP-001) (#344)

Advances the Common pin 3f8c4df → 9691bd8, adopting the security PRs that landed after
the R2-02 pin: MediaRedirectSafeSender pre-send validation (#622), SafeDirectoryCleanup
reparse-point refusal (#623), TimeParsing helpers (#624).

Adds CommonPinDriftTests (parity): asserts ext-common-sha.txt equals the checked-out
ext/Lidarr.Plugin.Common HEAD, so the sentinel and the Common actually built can't drift
apart (the apple failure mode). Compares against the checked-out submodule HEAD (not the
committed gitlink) so it holds in a dirty local re-pin and still catches a forgot-to-stage
submodule in a clean CI checkout. Skips when git is unavailable.

The #622 pre-send validation correctly rejects the stub host "https://chunk" in the DI
end-to-end download-validation test (it doesn't resolve under the Strict guard); injected
TidalTestPolicies.Resolving there to match the R2-02 test sweep.

1323 unit + 38 parity green (incl. the 2 new drift gates).
…brainarr parity) (#346)

Replace the 104-line inline notify-failure body with the 35-line thin shim
pattern brainarr already uses: keep the plugin-specific workflow_run trigger
and monitored-workflows list (preserved verbatim), delegate the notification
body to RicherTunes/Lidarr.Plugin.Common notify-failure.yml@workflows/v1.

Safety analysis: the reusable already carries the secrets||vars webhook
fallback at job-level env, and workflows/v1 (76bb178) is byte-identical to
Common main for this file. No DISCORD_WEBHOOK/SLACK_WEBHOOK secret or
variable exists in this repo (or org-level), so behavior today is the
step-summary fallback in both old and new versions.
* chore(ci): sync GitHub mirror with Gitea main

* chore(ci): sync GitHub mirror with Gitea main f64c128

* chore(ci): sync GitHub mirror with Gitea main 0707bed

* chore(ci): sync GitHub mirror with Gitea main 4d4234c

---------
---
updated-dependencies:
- dependency-name: Xunit.SkippableFact
  dependency-version: 1.5.85
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot @github

dependabot Bot commented on behalf of github Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Assignees

The following users could not be added as assignees: richertunes. Either the username does not exist or it does not have the correct permissions to be added as an assignee.

Labels

The following labels could not be found: automated, dependencies, nuget. Please create them before Dependabot can add them to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

@dependabot @github

dependabot Bot commented on behalf of github Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting @dependabot ignore this major version or @dependabot ignore this minor version. You can also ignore all major, minor, or patch releases for a dependency by adding an ignore condition with the desired update_types to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

@dependabot
dependabot Bot deleted the dependabot/nuget/main/Xunit.SkippableFact-1.5.85 branch September 7, 2026 23:57
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.

1 participant