From 62b4d1701816faab935cd84aab4d728c6bd122b1 Mon Sep 17 00:00:00 2001
From: InstaZDLL
Date: Mon, 26 Oct 2026 17:43:13 +0100
Subject: [PATCH 01/13] refactor(src): move the root modules into their domain
directories
Renames only, so history follows each file: auth/, library/, remote/,
http/ and storage/ receive the flat modules, and media/ receives
replay_gain. A module that carries unit tests moves to foo/mod.rs, ready
for its tests to leave it. The tree compiles again with the next commit,
which declares the new parents and rewrites the paths.
Signed-off-by: InstaZDLL
---
src/{ => auth}/authentication.rs | 0
src/{oauth.rs => auth/oauth/mod.rs} | 0
src/{security.rs => auth/security/mod.rs} | 0
src/{setup_token.rs => auth/setup_token/mod.rs} | 0
src/{sign_in.rs => auth/sign_in/mod.rs} | 0
src/{stream_ticket.rs => auth/stream_ticket/mod.rs} | 0
src/{public_share.rs => http/public_share/mod.rs} | 0
src/{reference.rs => http/reference/mod.rs} | 0
src/{security_headers.rs => http/security_headers/mod.rs} | 0
src/{ => http}/webui.rs | 0
src/{credit_split.rs => library/credit_split/mod.rs} | 0
src/{link_roots.rs => library/link_roots/mod.rs} | 0
src/{lyrics.rs => library/lyrics/mod.rs} | 0
src/{pid.rs => library/pid/mod.rs} | 0
src/{playlist_file.rs => library/playlist_file/mod.rs} | 0
src/{ => library}/scanner/discovery/mod.rs | 0
src/{ => library}/scanner/discovery/tests.rs | 0
src/{ => library}/scanner/extract/mod.rs | 0
src/{ => library}/scanner/extract/tests.rs | 0
src/{ => library}/scanner/job.rs | 0
src/{ => library}/scanner/mod.rs | 0
src/{tags.rs => library/tags/mod.rs} | 0
src/{ => library}/watcher/mod.rs | 0
src/{ => library}/watcher/tests.rs | 0
src/{replay_gain.rs => media/replay_gain/mod.rs} | 0
src/{enrichment.rs => remote/enrichment/mod.rs} | 0
src/{lrclib.rs => remote/lrclib/mod.rs} | 0
src/{outbound.rs => remote/outbound/mod.rs} | 0
src/{podcast.rs => remote/podcast/mod.rs} | 0
src/{portrait.rs => remote/portrait/mod.rs} | 0
src/{ => remote}/scrobblers/lastfm.rs | 0
src/{ => remote}/scrobblers/listenbrainz.rs | 0
src/{ => remote}/scrobblers/maloja.rs | 0
src/{ => remote}/scrobblers/mod.rs | 0
src/{ => storage}/catalog/apply.rs | 0
src/{ => storage}/catalog/availability.rs | 0
src/{ => storage}/catalog/credits.rs | 0
src/{ => storage}/catalog/derivations.rs | 0
src/{ => storage}/catalog/identity.rs | 0
src/{ => storage}/catalog/libraries.rs | 0
src/{ => storage}/catalog/mod.rs | 0
src/{ => storage}/catalog/overrides.rs | 0
src/{ => storage}/catalog/scan_jobs.rs | 0
src/{ => storage}/catalog/tests.rs | 0
src/{ => storage}/catalog/tracks.rs | 0
src/{ => storage}/database/accounts.rs | 0
src/{ => storage}/database/keys.rs | 0
src/{ => storage}/database/libraries.rs | 0
src/{ => storage}/database/metadata.rs | 0
src/{ => storage}/database/mod.rs | 0
src/{ => storage}/database/sessions.rs | 0
src/{ => storage}/database/subsonic_credentials.rs | 0
src/{ => storage}/database/sync_operations.rs | 0
src/{ => storage}/database/tests.rs | 0
src/{ => storage}/migration_backup.rs | 0
src/{natural_order.rs => storage/natural_order/mod.rs} | 0
src/{sync.rs => storage/sync/mod.rs} | 0
57 files changed, 0 insertions(+), 0 deletions(-)
rename src/{ => auth}/authentication.rs (100%)
rename src/{oauth.rs => auth/oauth/mod.rs} (100%)
rename src/{security.rs => auth/security/mod.rs} (100%)
rename src/{setup_token.rs => auth/setup_token/mod.rs} (100%)
rename src/{sign_in.rs => auth/sign_in/mod.rs} (100%)
rename src/{stream_ticket.rs => auth/stream_ticket/mod.rs} (100%)
rename src/{public_share.rs => http/public_share/mod.rs} (100%)
rename src/{reference.rs => http/reference/mod.rs} (100%)
rename src/{security_headers.rs => http/security_headers/mod.rs} (100%)
rename src/{ => http}/webui.rs (100%)
rename src/{credit_split.rs => library/credit_split/mod.rs} (100%)
rename src/{link_roots.rs => library/link_roots/mod.rs} (100%)
rename src/{lyrics.rs => library/lyrics/mod.rs} (100%)
rename src/{pid.rs => library/pid/mod.rs} (100%)
rename src/{playlist_file.rs => library/playlist_file/mod.rs} (100%)
rename src/{ => library}/scanner/discovery/mod.rs (100%)
rename src/{ => library}/scanner/discovery/tests.rs (100%)
rename src/{ => library}/scanner/extract/mod.rs (100%)
rename src/{ => library}/scanner/extract/tests.rs (100%)
rename src/{ => library}/scanner/job.rs (100%)
rename src/{ => library}/scanner/mod.rs (100%)
rename src/{tags.rs => library/tags/mod.rs} (100%)
rename src/{ => library}/watcher/mod.rs (100%)
rename src/{ => library}/watcher/tests.rs (100%)
rename src/{replay_gain.rs => media/replay_gain/mod.rs} (100%)
rename src/{enrichment.rs => remote/enrichment/mod.rs} (100%)
rename src/{lrclib.rs => remote/lrclib/mod.rs} (100%)
rename src/{outbound.rs => remote/outbound/mod.rs} (100%)
rename src/{podcast.rs => remote/podcast/mod.rs} (100%)
rename src/{portrait.rs => remote/portrait/mod.rs} (100%)
rename src/{ => remote}/scrobblers/lastfm.rs (100%)
rename src/{ => remote}/scrobblers/listenbrainz.rs (100%)
rename src/{ => remote}/scrobblers/maloja.rs (100%)
rename src/{ => remote}/scrobblers/mod.rs (100%)
rename src/{ => storage}/catalog/apply.rs (100%)
rename src/{ => storage}/catalog/availability.rs (100%)
rename src/{ => storage}/catalog/credits.rs (100%)
rename src/{ => storage}/catalog/derivations.rs (100%)
rename src/{ => storage}/catalog/identity.rs (100%)
rename src/{ => storage}/catalog/libraries.rs (100%)
rename src/{ => storage}/catalog/mod.rs (100%)
rename src/{ => storage}/catalog/overrides.rs (100%)
rename src/{ => storage}/catalog/scan_jobs.rs (100%)
rename src/{ => storage}/catalog/tests.rs (100%)
rename src/{ => storage}/catalog/tracks.rs (100%)
rename src/{ => storage}/database/accounts.rs (100%)
rename src/{ => storage}/database/keys.rs (100%)
rename src/{ => storage}/database/libraries.rs (100%)
rename src/{ => storage}/database/metadata.rs (100%)
rename src/{ => storage}/database/mod.rs (100%)
rename src/{ => storage}/database/sessions.rs (100%)
rename src/{ => storage}/database/subsonic_credentials.rs (100%)
rename src/{ => storage}/database/sync_operations.rs (100%)
rename src/{ => storage}/database/tests.rs (100%)
rename src/{ => storage}/migration_backup.rs (100%)
rename src/{natural_order.rs => storage/natural_order/mod.rs} (100%)
rename src/{sync.rs => storage/sync/mod.rs} (100%)
diff --git a/src/authentication.rs b/src/auth/authentication.rs
similarity index 100%
rename from src/authentication.rs
rename to src/auth/authentication.rs
diff --git a/src/oauth.rs b/src/auth/oauth/mod.rs
similarity index 100%
rename from src/oauth.rs
rename to src/auth/oauth/mod.rs
diff --git a/src/security.rs b/src/auth/security/mod.rs
similarity index 100%
rename from src/security.rs
rename to src/auth/security/mod.rs
diff --git a/src/setup_token.rs b/src/auth/setup_token/mod.rs
similarity index 100%
rename from src/setup_token.rs
rename to src/auth/setup_token/mod.rs
diff --git a/src/sign_in.rs b/src/auth/sign_in/mod.rs
similarity index 100%
rename from src/sign_in.rs
rename to src/auth/sign_in/mod.rs
diff --git a/src/stream_ticket.rs b/src/auth/stream_ticket/mod.rs
similarity index 100%
rename from src/stream_ticket.rs
rename to src/auth/stream_ticket/mod.rs
diff --git a/src/public_share.rs b/src/http/public_share/mod.rs
similarity index 100%
rename from src/public_share.rs
rename to src/http/public_share/mod.rs
diff --git a/src/reference.rs b/src/http/reference/mod.rs
similarity index 100%
rename from src/reference.rs
rename to src/http/reference/mod.rs
diff --git a/src/security_headers.rs b/src/http/security_headers/mod.rs
similarity index 100%
rename from src/security_headers.rs
rename to src/http/security_headers/mod.rs
diff --git a/src/webui.rs b/src/http/webui.rs
similarity index 100%
rename from src/webui.rs
rename to src/http/webui.rs
diff --git a/src/credit_split.rs b/src/library/credit_split/mod.rs
similarity index 100%
rename from src/credit_split.rs
rename to src/library/credit_split/mod.rs
diff --git a/src/link_roots.rs b/src/library/link_roots/mod.rs
similarity index 100%
rename from src/link_roots.rs
rename to src/library/link_roots/mod.rs
diff --git a/src/lyrics.rs b/src/library/lyrics/mod.rs
similarity index 100%
rename from src/lyrics.rs
rename to src/library/lyrics/mod.rs
diff --git a/src/pid.rs b/src/library/pid/mod.rs
similarity index 100%
rename from src/pid.rs
rename to src/library/pid/mod.rs
diff --git a/src/playlist_file.rs b/src/library/playlist_file/mod.rs
similarity index 100%
rename from src/playlist_file.rs
rename to src/library/playlist_file/mod.rs
diff --git a/src/scanner/discovery/mod.rs b/src/library/scanner/discovery/mod.rs
similarity index 100%
rename from src/scanner/discovery/mod.rs
rename to src/library/scanner/discovery/mod.rs
diff --git a/src/scanner/discovery/tests.rs b/src/library/scanner/discovery/tests.rs
similarity index 100%
rename from src/scanner/discovery/tests.rs
rename to src/library/scanner/discovery/tests.rs
diff --git a/src/scanner/extract/mod.rs b/src/library/scanner/extract/mod.rs
similarity index 100%
rename from src/scanner/extract/mod.rs
rename to src/library/scanner/extract/mod.rs
diff --git a/src/scanner/extract/tests.rs b/src/library/scanner/extract/tests.rs
similarity index 100%
rename from src/scanner/extract/tests.rs
rename to src/library/scanner/extract/tests.rs
diff --git a/src/scanner/job.rs b/src/library/scanner/job.rs
similarity index 100%
rename from src/scanner/job.rs
rename to src/library/scanner/job.rs
diff --git a/src/scanner/mod.rs b/src/library/scanner/mod.rs
similarity index 100%
rename from src/scanner/mod.rs
rename to src/library/scanner/mod.rs
diff --git a/src/tags.rs b/src/library/tags/mod.rs
similarity index 100%
rename from src/tags.rs
rename to src/library/tags/mod.rs
diff --git a/src/watcher/mod.rs b/src/library/watcher/mod.rs
similarity index 100%
rename from src/watcher/mod.rs
rename to src/library/watcher/mod.rs
diff --git a/src/watcher/tests.rs b/src/library/watcher/tests.rs
similarity index 100%
rename from src/watcher/tests.rs
rename to src/library/watcher/tests.rs
diff --git a/src/replay_gain.rs b/src/media/replay_gain/mod.rs
similarity index 100%
rename from src/replay_gain.rs
rename to src/media/replay_gain/mod.rs
diff --git a/src/enrichment.rs b/src/remote/enrichment/mod.rs
similarity index 100%
rename from src/enrichment.rs
rename to src/remote/enrichment/mod.rs
diff --git a/src/lrclib.rs b/src/remote/lrclib/mod.rs
similarity index 100%
rename from src/lrclib.rs
rename to src/remote/lrclib/mod.rs
diff --git a/src/outbound.rs b/src/remote/outbound/mod.rs
similarity index 100%
rename from src/outbound.rs
rename to src/remote/outbound/mod.rs
diff --git a/src/podcast.rs b/src/remote/podcast/mod.rs
similarity index 100%
rename from src/podcast.rs
rename to src/remote/podcast/mod.rs
diff --git a/src/portrait.rs b/src/remote/portrait/mod.rs
similarity index 100%
rename from src/portrait.rs
rename to src/remote/portrait/mod.rs
diff --git a/src/scrobblers/lastfm.rs b/src/remote/scrobblers/lastfm.rs
similarity index 100%
rename from src/scrobblers/lastfm.rs
rename to src/remote/scrobblers/lastfm.rs
diff --git a/src/scrobblers/listenbrainz.rs b/src/remote/scrobblers/listenbrainz.rs
similarity index 100%
rename from src/scrobblers/listenbrainz.rs
rename to src/remote/scrobblers/listenbrainz.rs
diff --git a/src/scrobblers/maloja.rs b/src/remote/scrobblers/maloja.rs
similarity index 100%
rename from src/scrobblers/maloja.rs
rename to src/remote/scrobblers/maloja.rs
diff --git a/src/scrobblers/mod.rs b/src/remote/scrobblers/mod.rs
similarity index 100%
rename from src/scrobblers/mod.rs
rename to src/remote/scrobblers/mod.rs
diff --git a/src/catalog/apply.rs b/src/storage/catalog/apply.rs
similarity index 100%
rename from src/catalog/apply.rs
rename to src/storage/catalog/apply.rs
diff --git a/src/catalog/availability.rs b/src/storage/catalog/availability.rs
similarity index 100%
rename from src/catalog/availability.rs
rename to src/storage/catalog/availability.rs
diff --git a/src/catalog/credits.rs b/src/storage/catalog/credits.rs
similarity index 100%
rename from src/catalog/credits.rs
rename to src/storage/catalog/credits.rs
diff --git a/src/catalog/derivations.rs b/src/storage/catalog/derivations.rs
similarity index 100%
rename from src/catalog/derivations.rs
rename to src/storage/catalog/derivations.rs
diff --git a/src/catalog/identity.rs b/src/storage/catalog/identity.rs
similarity index 100%
rename from src/catalog/identity.rs
rename to src/storage/catalog/identity.rs
diff --git a/src/catalog/libraries.rs b/src/storage/catalog/libraries.rs
similarity index 100%
rename from src/catalog/libraries.rs
rename to src/storage/catalog/libraries.rs
diff --git a/src/catalog/mod.rs b/src/storage/catalog/mod.rs
similarity index 100%
rename from src/catalog/mod.rs
rename to src/storage/catalog/mod.rs
diff --git a/src/catalog/overrides.rs b/src/storage/catalog/overrides.rs
similarity index 100%
rename from src/catalog/overrides.rs
rename to src/storage/catalog/overrides.rs
diff --git a/src/catalog/scan_jobs.rs b/src/storage/catalog/scan_jobs.rs
similarity index 100%
rename from src/catalog/scan_jobs.rs
rename to src/storage/catalog/scan_jobs.rs
diff --git a/src/catalog/tests.rs b/src/storage/catalog/tests.rs
similarity index 100%
rename from src/catalog/tests.rs
rename to src/storage/catalog/tests.rs
diff --git a/src/catalog/tracks.rs b/src/storage/catalog/tracks.rs
similarity index 100%
rename from src/catalog/tracks.rs
rename to src/storage/catalog/tracks.rs
diff --git a/src/database/accounts.rs b/src/storage/database/accounts.rs
similarity index 100%
rename from src/database/accounts.rs
rename to src/storage/database/accounts.rs
diff --git a/src/database/keys.rs b/src/storage/database/keys.rs
similarity index 100%
rename from src/database/keys.rs
rename to src/storage/database/keys.rs
diff --git a/src/database/libraries.rs b/src/storage/database/libraries.rs
similarity index 100%
rename from src/database/libraries.rs
rename to src/storage/database/libraries.rs
diff --git a/src/database/metadata.rs b/src/storage/database/metadata.rs
similarity index 100%
rename from src/database/metadata.rs
rename to src/storage/database/metadata.rs
diff --git a/src/database/mod.rs b/src/storage/database/mod.rs
similarity index 100%
rename from src/database/mod.rs
rename to src/storage/database/mod.rs
diff --git a/src/database/sessions.rs b/src/storage/database/sessions.rs
similarity index 100%
rename from src/database/sessions.rs
rename to src/storage/database/sessions.rs
diff --git a/src/database/subsonic_credentials.rs b/src/storage/database/subsonic_credentials.rs
similarity index 100%
rename from src/database/subsonic_credentials.rs
rename to src/storage/database/subsonic_credentials.rs
diff --git a/src/database/sync_operations.rs b/src/storage/database/sync_operations.rs
similarity index 100%
rename from src/database/sync_operations.rs
rename to src/storage/database/sync_operations.rs
diff --git a/src/database/tests.rs b/src/storage/database/tests.rs
similarity index 100%
rename from src/database/tests.rs
rename to src/storage/database/tests.rs
diff --git a/src/migration_backup.rs b/src/storage/migration_backup.rs
similarity index 100%
rename from src/migration_backup.rs
rename to src/storage/migration_backup.rs
diff --git a/src/natural_order.rs b/src/storage/natural_order/mod.rs
similarity index 100%
rename from src/natural_order.rs
rename to src/storage/natural_order/mod.rs
diff --git a/src/sync.rs b/src/storage/sync/mod.rs
similarity index 100%
rename from src/sync.rs
rename to src/storage/sync/mod.rs
From 6cbb103873e886dd45c2cb27ed70381640bcfc8a Mon Sep 17 00:00:00 2001
From: InstaZDLL
Date: Mon, 26 Oct 2026 17:47:14 +0100
Subject: [PATCH 02/13] refactor(src): declare the domain parents and rewrite
every path to them
lib.rs now names fourteen modules instead of thirty-nine. Each new parent
(auth, library, remote, http, storage) has a mod.rs saying what the domain
holds, and media declares replay_gain. Every crate:: and waveflow_server::
path, in src/ and tests/ alike, names the module's new home; the Scalar
bundle is included from its new depth. No compatibility re-export is left
at the root.
Signed-off-by: InstaZDLL
---
src/api/access.rs | 13 +++---
src/api/auth.rs | 21 +++++----
src/api/error.rs | 10 ++--
src/api/libraries.rs | 26 ++++++-----
src/api/mod.rs | 4 +-
src/api/oauth.rs | 13 +++---
src/api/playback.rs | 2 +-
src/api/setup.rs | 2 +-
src/api/subsonic_credential.rs | 4 +-
src/api/sync.rs | 41 ++++++++++------
src/api/tokens.rs | 10 ++--
src/api/tracks.rs | 16 +++----
src/api/users.rs | 6 +--
src/api/web_session.rs | 8 ++--
src/app/boot.rs | 5 +-
src/app/openapi.rs | 7 ++-
src/app/router.rs | 2 +-
src/app/state.rs | 3 +-
src/auth/authentication.rs | 6 +--
src/auth/mod.rs | 10 ++++
src/auth/oauth/mod.rs | 2 +-
src/auth/security/mod.rs | 2 +-
src/auth/setup_token/mod.rs | 2 +-
src/auth/stream_ticket/mod.rs | 2 +-
src/cli/account.rs | 7 +--
src/cli/database.rs | 2 +-
src/cli/key.rs | 6 +--
src/cli/library.rs | 6 +--
src/cli/mod.rs | 4 +-
src/config/access.rs | 2 +-
src/config/for_data_dir.rs | 6 +--
src/config/from_env.rs | 10 ++--
src/config/metadata.rs | 16 +++----
src/config/mod.rs | 4 +-
src/config/pid.rs | 13 +++---
src/config/podcasts.rs | 6 +--
src/config/scrobbling.rs | 10 ++--
src/config/tests.rs | 4 +-
src/http/mod.rs | 8 ++++
src/http/public_share/mod.rs | 8 ++--
src/http/reference/mod.rs | 11 +++--
src/http/security_headers/mod.rs | 4 +-
src/http/webui.rs | 10 ++--
src/lib.rs | 35 ++------------
src/library/credit_split/mod.rs | 6 +--
src/library/mod.rs | 12 +++++
src/library/scanner/discovery/mod.rs | 8 ++--
src/library/scanner/extract/mod.rs | 18 +++----
src/library/scanner/job.rs | 6 +--
src/library/scanner/mod.rs | 10 ++--
src/library/watcher/mod.rs | 8 ++--
src/library/watcher/tests.rs | 2 +-
src/main.rs | 6 +--
src/media/canvas.rs | 17 ++++---
src/media/mod.rs | 9 ++--
src/media/stream.rs | 27 ++++++-----
src/media/tests.rs | 20 ++++----
src/media/transcode.rs | 2 +-
src/remote/enrichment/mod.rs | 10 ++--
src/remote/lrclib/mod.rs | 14 +++---
src/remote/mod.rs | 10 ++++
src/remote/podcast/mod.rs | 10 ++--
src/remote/portrait/mod.rs | 22 ++++-----
src/remote/scrobblers/lastfm.rs | 30 ++++++------
src/remote/scrobblers/maloja.rs | 12 ++---
src/remote/scrobblers/mod.rs | 8 ++--
src/services/admin.rs | 2 +-
src/services/artist_enrichment/mod.rs | 4 +-
src/services/artist_enrichment/tests.rs | 2 +-
src/services/artist_image_search/mod.rs | 4 +-
src/services/artist_images.rs | 12 ++---
src/services/artwork.rs | 7 ++-
src/services/canvas/mod.rs | 2 +-
src/services/canvas/placement.rs | 13 +++---
src/services/catalog.rs | 2 +-
src/services/credentials.rs | 15 +++---
src/services/credit_decisions.rs | 32 ++++++-------
src/services/lastfm.rs | 8 ++--
src/services/library_events.rs | 2 +-
src/services/library_folders/mod.rs | 4 +-
src/services/library_lifecycle/edit.rs | 2 +-
src/services/library_tree/mod.rs | 2 +-
src/services/lyrics_uploads.rs | 6 +--
src/services/mod.rs | 47 ++++++++++---------
src/services/model/accounts.rs | 4 +-
src/services/model/playlists.rs | 2 +-
src/services/playlists/files.rs | 2 +-
src/services/podcasts/episodes.rs | 10 ++--
src/services/podcasts/mod.rs | 2 +-
src/services/quality/analysis.rs | 2 +-
src/services/quality/mod.rs | 2 +-
src/services/quality/reads.rs | 5 +-
src/services/radio/mod.rs | 2 +-
src/services/remote_artwork/mod.rs | 8 ++--
src/services/remote_lyrics/mod.rs | 2 +-
src/services/remote_lyrics/tests.rs | 2 +-
src/services/scan.rs | 2 +-
src/services/search.rs | 2 +-
src/services/shares/tests.rs | 2 +-
src/services/track_metadata.rs | 31 +++++++------
src/services/uploads/negotiate.rs | 2 +-
src/services/uploads/transfer.rs | 9 ++--
src/storage/catalog/apply.rs | 24 +++++-----
src/storage/catalog/availability.rs | 2 +-
src/storage/catalog/credits.rs | 49 ++++++++++----------
src/storage/catalog/derivations.rs | 2 +-
src/storage/catalog/identity.rs | 20 ++++----
src/storage/catalog/libraries.rs | 14 +++---
src/storage/catalog/overrides.rs | 2 +-
src/storage/catalog/scan_jobs.rs | 4 +-
src/storage/catalog/tracks.rs | 10 ++--
src/storage/database/keys.rs | 8 ++--
src/storage/database/metadata.rs | 40 ++++++++--------
src/storage/database/mod.rs | 8 ++--
src/storage/database/sessions.rs | 2 +-
src/storage/database/subsonic_credentials.rs | 2 +-
src/storage/migration_backup.rs | 13 ++++--
src/storage/mod.rs | 9 ++++
src/storage/sync/mod.rs | 4 +-
src/subsonic/auth.rs | 5 +-
src/subsonic/lyrics.rs | 2 +-
src/subsonic/media.rs | 2 +-
src/subsonic/mod.rs | 8 ++--
tests/auth/main.rs | 10 ++--
tests/auth/oauth.rs | 2 +-
tests/auth/sessions.rs | 2 +-
tests/auth/setup.rs | 6 +--
tests/auth/subsonic_credential.rs | 4 +-
tests/auth/throttling.rs | 14 +++---
tests/auth/tokens.rs | 10 ++--
tests/canvas.rs | 18 +++----
tests/catalog/credits.rs | 4 +-
tests/catalog/main.rs | 8 ++--
tests/cli.rs | 16 +++----
tests/credits.rs | 6 +--
tests/discovery.rs | 20 ++++----
tests/enrichment/banners.rs | 2 +-
tests/enrichment/image_search.rs | 12 ++---
tests/enrichment/lastfm.rs | 4 +-
tests/enrichment/main.rs | 14 +++---
tests/identity.rs | 28 +++++------
tests/media/main.rs | 10 ++--
tests/media/stream_tickets.rs | 8 ++--
tests/migrations.rs | 20 ++++----
tests/native_api/artists.rs | 4 +-
tests/native_api/folders.rs | 4 +-
tests/native_api/libraries.rs | 6 +--
tests/native_api/lyrics.rs | 2 +-
tests/native_api/main.rs | 10 ++--
tests/native_api/playlists.rs | 2 +-
tests/podcasts/covers.rs | 8 ++--
tests/podcasts/main.rs | 6 +--
tests/quality.rs | 6 +--
tests/scanner/indexing.rs | 2 +-
tests/scanner/lifecycle.rs | 2 +-
tests/scanner/main.rs | 16 +++----
tests/scanner/purges.rs | 4 +-
tests/scanner/scheduling.rs | 15 ++++--
tests/scrobbling/adapters.rs | 20 ++++----
tests/scrobbling/listenbrainz.rs | 4 +-
tests/scrobbling/main.rs | 10 ++--
tests/service.rs | 21 +++++----
tests/smart_playlists.rs | 8 ++--
tests/subsonic_browse.rs | 10 ++--
tests/subsonic_contract/compatibility.rs | 2 +-
tests/subsonic_contract/main.rs | 10 ++--
tests/subsonic_fields.rs | 14 +++---
tests/subsonic_methods/main.rs | 10 ++--
tests/subsonic_methods/transcoding.rs | 6 +--
tests/support/mod.rs | 10 ++--
tests/sync.rs | 18 +++----
tests/uploads/listings.rs | 2 +-
tests/uploads/main.rs | 10 ++--
173 files changed, 849 insertions(+), 754 deletions(-)
create mode 100644 src/auth/mod.rs
create mode 100644 src/http/mod.rs
create mode 100644 src/library/mod.rs
create mode 100644 src/remote/mod.rs
create mode 100644 src/storage/mod.rs
diff --git a/src/api/access.rs b/src/api/access.rs
index a100fbd2..efa879bc 100644
--- a/src/api/access.rs
+++ b/src/api/access.rs
@@ -76,14 +76,15 @@ pub(crate) async fn authenticated(
state: &AppState,
headers: &HeaderMap,
access: Access,
-) -> Result {
+) -> Result {
let token = bearer_token(headers).ok_or(ApiError::Unauthorized)?;
let user = state
.auth
.authenticate(token)
.await
.map_err(ApiError::from)?;
- let role_ok = access != Access::Admin || user.role == crate::database::AccountRole::Admin;
+ let role_ok =
+ access != Access::Admin || user.role == crate::storage::database::AccountRole::Admin;
if role_ok && access.granted_by(&user.scopes) {
Ok(user)
} else {
@@ -97,11 +98,11 @@ pub(crate) async fn authenticated(
/// [`Access::Admin`]. A route that admits a narrower credential must not let
/// the account's role stand in for what the credential lacks.
pub(crate) fn library_actor(
- user: &crate::authentication::AuthUser,
+ user: &crate::auth::authentication::AuthUser,
) -> crate::services::LibraryActor {
crate::services::LibraryActor {
id: user.id,
- admin_credential: user.role == crate::database::AccountRole::Admin
+ admin_credential: user.role == crate::storage::database::AccountRole::Admin
&& Access::Admin.granted_by(&user.scopes),
}
}
@@ -127,11 +128,11 @@ pub(super) async fn mutation_context(
state: &AppState,
headers: &HeaderMap,
user_id: Uuid,
-) -> Result {
+) -> Result {
let operation_id =
optional_uuid_header(headers, OPERATION_ID_HEADER)?.unwrap_or_else(Uuid::new_v4);
let origin_device_id = origin_device(state, headers, user_id).await?;
- Ok(crate::sync::MutationContext {
+ Ok(crate::storage::sync::MutationContext {
operation_id,
origin_device_id,
})
diff --git a/src/api/auth.rs b/src/api/auth.rs
index cd782567..7600f17b 100644
--- a/src/api/auth.rs
+++ b/src/api/auth.rs
@@ -22,7 +22,7 @@ pub struct RefreshRequest {
tag = "authentication",
request_body = LoginRequest,
responses(
- (status = 200, body = crate::authentication::AuthTokens),
+ (status = 200, body = crate::auth::authentication::AuthTokens),
(status = 401, body = ErrorResponse),
(status = 422, body = ErrorResponse),
(status = 429, body = ErrorResponse, description = "Too many failed sign-ins from this address for this account (RFC-017)",
@@ -34,14 +34,14 @@ pub async fn login(
State(state): State,
client: SignInAddress,
Json(request): Json,
-) -> Result, ApiError> {
+) -> Result, ApiError> {
state
.auth
.login(
&request.username,
&request.password,
&request.device_name,
- &client.on(crate::sign_in::Surface::Native),
+ &client.on(crate::auth::sign_in::Surface::Native),
)
.await
.map(Json)
@@ -54,7 +54,7 @@ pub async fn login(
tag = "authentication",
request_body = RefreshRequest,
responses(
- (status = 200, body = crate::authentication::AuthTokens),
+ (status = 200, body = crate::auth::authentication::AuthTokens),
(status = 401, body = ErrorResponse),
(status = 503, body = ErrorResponse)
)
@@ -62,7 +62,7 @@ pub async fn login(
pub async fn refresh(
State(state): State,
Json(request): Json,
-) -> Result, ApiError> {
+) -> Result, ApiError> {
state
.auth
.refresh(&request.refresh_token)
@@ -125,7 +125,7 @@ pub async fn web_login(
&request.username,
&request.password,
&request.device_name,
- &client.on(crate::sign_in::Surface::Web),
+ &client.on(crate::auth::sign_in::Surface::Web),
)
.await
.map_err(ApiError::from)?;
@@ -198,8 +198,11 @@ pub async fn web_logout(
pub struct SignInAddress(String);
impl SignInAddress {
- fn on(self, surface: crate::sign_in::Surface) -> crate::authentication::SignInClient {
- crate::authentication::SignInClient {
+ fn on(
+ self,
+ surface: crate::auth::sign_in::Surface,
+ ) -> crate::auth::authentication::SignInClient {
+ crate::auth::authentication::SignInClient {
surface,
address: self.0,
}
@@ -213,7 +216,7 @@ impl axum::extract::FromRequestParts for SignInAddress {
parts: &mut axum::http::request::Parts,
state: &AppState,
) -> Result {
- Ok(Self(crate::sign_in::request_bucket(
+ Ok(Self(crate::auth::sign_in::request_bucket(
&parts.extensions,
&parts.headers,
&state.trusted_proxies,
diff --git a/src/api/error.rs b/src/api/error.rs
index ee4d7ae4..91a015c6 100644
--- a/src/api/error.rs
+++ b/src/api/error.rs
@@ -113,12 +113,12 @@ pub(super) fn db_error(error: sqlx::Error) -> ApiError {
ApiError::Unavailable
}
-pub(super) fn sync_error(error: crate::sync::SyncError) -> ApiError {
+pub(super) fn sync_error(error: crate::storage::sync::SyncError) -> ApiError {
match error {
- crate::sync::SyncError::Invalid => ApiError::Validation,
- crate::sync::SyncError::Conflict => ApiError::Conflict,
- crate::sync::SyncError::CursorExpired => ApiError::CursorExpired,
- crate::sync::SyncError::Database(error) => db_error(error),
+ crate::storage::sync::SyncError::Invalid => ApiError::Validation,
+ crate::storage::sync::SyncError::Conflict => ApiError::Conflict,
+ crate::storage::sync::SyncError::CursorExpired => ApiError::CursorExpired,
+ crate::storage::sync::SyncError::Database(error) => db_error(error),
}
}
diff --git a/src/api/libraries.rs b/src/api/libraries.rs
index 963e98fe..2fa0a749 100644
--- a/src/api/libraries.rs
+++ b/src/api/libraries.rs
@@ -13,7 +13,7 @@ pub struct ScanQueuedResponse {
pub struct CreateLibraryRequest {
pub name: String,
pub path: String,
- pub visibility: crate::database::LibraryVisibility,
+ pub visibility: crate::storage::database::LibraryVisibility,
}
#[derive(Debug, Serialize, ToSchema)]
@@ -24,7 +24,7 @@ pub struct CreateLibraryResponse {
#[derive(Debug, Deserialize, ToSchema)]
pub struct SetLibraryMemberRequest {
- pub role: crate::database::LibraryRole,
+ pub role: crate::storage::database::LibraryRole,
}
#[utoipa::path(post, path = "/api/v2/libraries/{library_id}/scans", tag = "catalog", params(("library_id" = Uuid, Path)), responses((status = 202, body = ScanQueuedResponse), (status = 401, body = ErrorResponse), (status = 404, body = ErrorResponse)))]
@@ -42,11 +42,11 @@ pub async fn start_scan(
Ok((StatusCode::ACCEPTED, Json(ScanQueuedResponse { scan_id })))
}
-#[utoipa::path(get, path = "/api/v2/libraries", tag = "catalog", responses((status = 200, body = [crate::catalog::LibraryAccess]), (status = 401, body = ErrorResponse)))]
+#[utoipa::path(get, path = "/api/v2/libraries", tag = "catalog", responses((status = 200, body = [crate::storage::catalog::LibraryAccess]), (status = 401, body = ErrorResponse)))]
pub async fn list_libraries(
State(state): State,
headers: HeaderMap,
-) -> Result>, ApiError> {
+) -> Result>, ApiError> {
let user = authenticated(&state, &headers, Access::Read).await?;
state
.db
@@ -80,7 +80,7 @@ pub async fn create_library(
&request.name,
&canonical,
request.visibility,
- crate::authentication::now_ms(),
+ crate::auth::authentication::now_ms(),
)
.await
.map_err(db_error)?;
@@ -88,7 +88,7 @@ pub async fn create_library(
let scan_id = state
.scanner
.trigger(
- crate::catalog::LibraryRecord {
+ crate::storage::catalog::LibraryRecord {
id: library_id,
name: request.name,
root_path: canonical,
@@ -118,7 +118,7 @@ pub async fn set_library_member(
Json(request): Json,
) -> Result {
let actor = authenticated(&state, &headers, Access::Admin).await?;
- if request.role == crate::database::LibraryRole::Owner
+ if request.role == crate::storage::database::LibraryRole::Owner
|| state
.db
.account_by_id(user_id)
@@ -142,7 +142,7 @@ pub async fn set_library_member(
library_id,
user_id,
request.role,
- crate::authentication::now_ms(),
+ crate::auth::authentication::now_ms(),
)
.await
.map_err(db_error)?;
@@ -162,7 +162,7 @@ pub async fn remove_library_member(
actor.id,
library_id,
user_id,
- crate::authentication::now_ms(),
+ crate::auth::authentication::now_ms(),
)
.await
.map_err(db_error)?
@@ -173,12 +173,12 @@ pub async fn remove_library_member(
}
}
-#[utoipa::path(get, path = "/api/v2/scans/{scan_id}", tag = "catalog", params(("scan_id" = Uuid, Path)), responses((status = 200, body = crate::catalog::ScanJobRecord), (status = 401, body = ErrorResponse), (status = 404, body = ErrorResponse)))]
+#[utoipa::path(get, path = "/api/v2/scans/{scan_id}", tag = "catalog", params(("scan_id" = Uuid, Path)), responses((status = 200, body = crate::storage::catalog::ScanJobRecord), (status = 401, body = ErrorResponse), (status = 404, body = ErrorResponse)))]
pub async fn scan_status(
State(state): State,
Path(scan_id): Path,
headers: HeaderMap,
-) -> Result, ApiError> {
+) -> Result, ApiError> {
let user = authenticated(&state, &headers, Access::Read).await?;
state
.db
@@ -327,7 +327,9 @@ pub async fn library_events(
user.id,
library_id,
query.after.unwrap_or(0),
- query.limit.unwrap_or(crate::sync::DEFAULT_SYNC_LIMIT),
+ query
+ .limit
+ .unwrap_or(crate::storage::sync::DEFAULT_SYNC_LIMIT),
)
.await
.map(Json)
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 784cebd3..3a6d8533 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
-use crate::{authentication::AuthError, AppState};
+use crate::{auth::authentication::AuthError, AppState};
pub(crate) mod access;
mod attention;
@@ -256,7 +256,7 @@ pub fn router(state: AppState) -> Router {
.route(
"/api/v2/artists/{artist_id}/image/{kind}",
put(put_artist_image).delete(delete_artist_image).layer(
- axum::extract::DefaultBodyLimit::max(crate::portrait::MAX_IMAGE_BYTES),
+ axum::extract::DefaultBodyLimit::max(crate::remote::portrait::MAX_IMAGE_BYTES),
),
)
.route(
diff --git a/src/api/oauth.rs b/src/api/oauth.rs
index a01706dc..b14a7566 100644
--- a/src/api/oauth.rs
+++ b/src/api/oauth.rs
@@ -68,24 +68,25 @@ pub async fn oauth_authorize(
Ok(Json(AuthorizeResponse { redirect_to }))
}
-#[utoipa::path(post, path = "/api/v2/oauth/token", tag = "authentication", request_body = TokenRequest, responses((status = 200, body = crate::authentication::AuthTokens), (status = 401, body = ErrorResponse), (status = 503, body = ErrorResponse)))]
+#[utoipa::path(post, path = "/api/v2/oauth/token", tag = "authentication", request_body = TokenRequest, responses((status = 200, body = crate::auth::authentication::AuthTokens), (status = 401, body = ErrorResponse), (status = 503, body = ErrorResponse)))]
pub async fn oauth_token(
State(state): State,
Json(request): Json,
-) -> Result, ApiError> {
+) -> Result, ApiError> {
// Mounted without authentication by design: the code plus the verifier are
// the credential. Every rejection below is the same 401 so a caller cannot
// learn whether a code existed, expired, or was already spent.
- let now = crate::authentication::now_ms();
+ let now = crate::auth::authentication::now_ms();
let grant = state
.db
- .redeem_authorization(&crate::security::token_hash(&request.code), now)
+ .redeem_authorization(&crate::auth::security::token_hash(&request.code), now)
.await
.map_err(db_error)?
.ok_or(ApiError::Unauthorized)?;
if grant.client_id != request.client_id.trim()
|| grant.redirect_uri != request.redirect_uri
- || crate::oauth::verify_challenge(&grant.code_challenge, &request.code_verifier).is_err()
+ || crate::auth::oauth::verify_challenge(&grant.code_challenge, &request.code_verifier)
+ .is_err()
{
return Err(ApiError::Unauthorized);
}
@@ -95,7 +96,7 @@ pub async fn oauth_token(
.await
.map(Json)
.map_err(|error| match error {
- crate::authentication::AuthError::Unavailable => ApiError::Unavailable,
+ crate::auth::authentication::AuthError::Unavailable => ApiError::Unavailable,
_ => ApiError::Unauthorized,
})
}
diff --git a/src/api/playback.rs b/src/api/playback.rs
index 2d8c36d5..4413aa9c 100644
--- a/src/api/playback.rs
+++ b/src/api/playback.rs
@@ -81,7 +81,7 @@ pub async fn list_history(
) -> Result>, ApiError> {
let user = authenticated(&state, &headers, Access::Read).await?;
let limit = query.limit.unwrap_or(200);
- if !(1..=crate::sync::MAX_SYNC_LIMIT).contains(&limit) {
+ if !(1..=crate::storage::sync::MAX_SYNC_LIMIT).contains(&limit) {
return Err(ApiError::Validation);
}
state
diff --git a/src/api/setup.rs b/src/api/setup.rs
index 00ef3386..c050038c 100644
--- a/src/api/setup.rs
+++ b/src/api/setup.rs
@@ -3,7 +3,7 @@
//! Split out of `http.rs`; `mod.rs` re-exports it, so `crate::api::*` paths are unchanged.
use super::*;
-use crate::setup_token::Admission;
+use crate::auth::setup_token::Admission;
#[derive(Debug, Serialize, ToSchema)]
pub struct SetupStatusResponse {
diff --git a/src/api/subsonic_credential.rs b/src/api/subsonic_credential.rs
index 174ff18c..708e479d 100644
--- a/src/api/subsonic_credential.rs
+++ b/src/api/subsonic_credential.rs
@@ -28,11 +28,11 @@ use super::*;
/// A `404` would be indistinguishable from a route that is not there, and a
/// flag beside the timestamps would tell one fact twice — the body already
/// says it by being absent.
-#[utoipa::path(get, path = "/api/v2/subsonic-credential", tag = "account", responses((status = 200, body = Option), (status = 401, body = ErrorResponse)))]
+#[utoipa::path(get, path = "/api/v2/subsonic-credential", tag = "account", responses((status = 200, body = Option), (status = 401, body = ErrorResponse)))]
pub async fn own_subsonic_credential(
State(state): State,
headers: HeaderMap,
-) -> Result>, ApiError> {
+) -> Result>, ApiError> {
let user = authenticated(&state, &headers, Access::Read).await?;
state
.services
diff --git a/src/api/sync.rs b/src/api/sync.rs
index 7fca7c6b..a6b9be17 100644
--- a/src/api/sync.rs
+++ b/src/api/sync.rs
@@ -34,7 +34,7 @@ pub struct SyncSnapshot {
tag = "sync",
params(("after" = Option, Query), ("limit" = Option, Query)),
responses(
- (status = 200, body = crate::sync::SyncPage),
+ (status = 200, body = crate::storage::sync::SyncPage),
(status = 401, body = ErrorResponse),
(
status = 409,
@@ -51,11 +51,13 @@ pub async fn sync_changes(
State(state): State,
headers: HeaderMap,
Query(query): Query,
-) -> Result, ApiError> {
+) -> Result, ApiError> {
let user = authenticated(&state, &headers, Access::Read).await?;
let after = query.after.unwrap_or(0);
- let limit = query.limit.unwrap_or(crate::sync::DEFAULT_SYNC_LIMIT);
- if after < 0 || limit <= 0 || limit > crate::sync::MAX_SYNC_LIMIT {
+ let limit = query
+ .limit
+ .unwrap_or(crate::storage::sync::DEFAULT_SYNC_LIMIT);
+ if after < 0 || limit <= 0 || limit > crate::storage::sync::MAX_SYNC_LIMIT {
return Err(ApiError::Validation);
}
state
@@ -79,7 +81,7 @@ pub async fn sync_snapshot(
let user = authenticated(&state, &headers, Access::Read).await?;
let snapshot = state
.services
- .sync_snapshot(user.id, crate::sync::MAX_SYNC_LIMIT)
+ .sync_snapshot(user.id, crate::storage::sync::MAX_SYNC_LIMIT)
.await
.map_err(service_error)?;
let favorites = snapshot
@@ -232,9 +234,12 @@ pub(super) enum SyncNoticeAction {
}
pub(super) async fn sync_notice_action(
- sync: &crate::sync::SyncService,
+ sync: &crate::storage::sync::SyncService,
user_id: Uuid,
- notice: Result<(Uuid, crate::sync::SyncNotice), tokio::sync::broadcast::error::RecvError>,
+ notice: Result<
+ (Uuid, crate::storage::sync::SyncNotice),
+ tokio::sync::broadcast::error::RecvError,
+ >,
) -> Result {
match notice {
Ok((notice_user, notice)) if notice_user == user_id => {
@@ -253,8 +258,8 @@ pub(super) async fn send_sync_notice(
sender: &mut futures_util::stream::SplitSink,
cursor: i64,
) -> Result<(), axum::Error> {
- let body =
- serde_json::to_string(&crate::sync::SyncNotice { cursor }).expect("sync notice serializes");
+ let body = serde_json::to_string(&crate::storage::sync::SyncNotice { cursor })
+ .expect("sync notice serializes");
sender.send(Message::Text(body.into())).await
}
@@ -273,20 +278,22 @@ mod tests {
async fn lagged_sync_socket_recovers_from_the_durable_cursor() {
let temp = tempfile::tempdir().unwrap();
let config = crate::Config::for_data_dir(temp.path().join("data"));
- let db = crate::database::Database::open(&config).await.unwrap();
+ let db = crate::storage::database::Database::open(&config)
+ .await
+ .unwrap();
db.migrate().await.unwrap();
let user_id = db
.create_account(
"lagged",
"hash",
- crate::database::AccountRole::User,
- crate::authentication::now_ms(),
+ crate::storage::database::AccountRole::User,
+ crate::auth::authentication::now_ms(),
)
.await
.unwrap();
- let sync = crate::sync::SyncService::new(db.clone());
+ let sync = crate::storage::sync::SyncService::new(db.clone());
- let context = crate::sync::MutationContext::server_generated();
+ let context = crate::storage::sync::MutationContext::server_generated();
let writer = db.writer_guard().await;
let mut tx = db.begin_write(&writer).await.unwrap();
sync.claim_operation(
@@ -294,7 +301,11 @@ mod tests {
&mut tx,
user_id,
context,
- crate::sync::MutationIntent::new("set-rating", "track:seed", &serde_json::json!({})),
+ crate::storage::sync::MutationIntent::new(
+ "set-rating",
+ "track:seed",
+ &serde_json::json!({}),
+ ),
)
.await
.unwrap();
diff --git a/src/api/tokens.rs b/src/api/tokens.rs
index b9aa6f34..6731b49b 100644
--- a/src/api/tokens.rs
+++ b/src/api/tokens.rs
@@ -18,17 +18,17 @@ pub struct CreateApiTokenRequest {
#[derive(Debug, Serialize, ToSchema)]
pub struct CreateApiTokenResponse {
#[serde(flatten)]
- pub token: crate::database::ApiTokenRecord,
+ pub token: crate::storage::database::ApiTokenRecord,
/// Shown once. Only its SHA-256 hash is stored, so it cannot be recovered.
pub secret: String,
}
-#[utoipa::path(get, path = "/api/v2/admin/users/{username}/tokens", tag = "administration", params(("username" = String, Path)), responses((status = 200, body = [crate::database::ApiTokenRecord]), (status = 401, body = ErrorResponse), (status = 403, body = ErrorResponse), (status = 404, body = ErrorResponse)))]
+#[utoipa::path(get, path = "/api/v2/admin/users/{username}/tokens", tag = "administration", params(("username" = String, Path)), responses((status = 200, body = [crate::storage::database::ApiTokenRecord]), (status = 401, body = ErrorResponse), (status = 403, body = ErrorResponse), (status = 404, body = ErrorResponse)))]
pub async fn list_api_tokens(
State(state): State,
Path(username): Path,
headers: HeaderMap,
-) -> Result>, ApiError> {
+) -> Result>, ApiError> {
let actor = authenticated(&state, &headers, Access::Admin).await?;
state
.services
@@ -104,11 +104,11 @@ pub async fn revoke_api_token(
/// Until now it needed the instance's administrator. A listener could not find
/// out which clients held a credential on their own library, let alone revoke
/// one — the material was there and visible nowhere.
-#[utoipa::path(get, path = "/api/v2/api-tokens", tag = "account", responses((status = 200, body = [crate::database::ApiTokenRecord]), (status = 401, body = ErrorResponse)))]
+#[utoipa::path(get, path = "/api/v2/api-tokens", tag = "account", responses((status = 200, body = [crate::storage::database::ApiTokenRecord]), (status = 401, body = ErrorResponse)))]
pub async fn list_own_api_tokens(
State(state): State,
headers: HeaderMap,
-) -> Result>, ApiError> {
+) -> Result>, ApiError> {
let user = authenticated(&state, &headers, Access::Read).await?;
state
.services
diff --git a/src/api/tracks.rs b/src/api/tracks.rs
index 0e350b9f..8e727707 100644
--- a/src/api/tracks.rs
+++ b/src/api/tracks.rs
@@ -11,13 +11,13 @@ pub struct TrackQuery {
pub limit: Option,
}
-#[utoipa::path(get, path = "/api/v2/libraries/{library_id}/tracks", tag = "catalog", params(("library_id" = Uuid, Path), ("q" = Option, Query), ("offset" = Option, Query), ("limit" = Option, Query)), responses((status = 200, body = [crate::catalog::TrackRecord]), (status = 401, body = ErrorResponse), (status = 404, body = ErrorResponse), (status = 422, body = ErrorResponse)))]
+#[utoipa::path(get, path = "/api/v2/libraries/{library_id}/tracks", tag = "catalog", params(("library_id" = Uuid, Path), ("q" = Option, Query), ("offset" = Option, Query), ("limit" = Option, Query)), responses((status = 200, body = [crate::storage::catalog::TrackRecord]), (status = 401, body = ErrorResponse), (status = 404, body = ErrorResponse), (status = 422, body = ErrorResponse)))]
pub async fn list_tracks(
State(state): State,
Path(library_id): Path,
Query(query): Query,
headers: HeaderMap,
-) -> Result>, ApiError> {
+) -> Result>, ApiError> {
let user = authenticated(&state, &headers, Access::Read).await?;
if state
.db
@@ -92,12 +92,12 @@ pub async fn get_track(
.ok_or(ApiError::NotFound)
}
-#[utoipa::path(get, path = "/api/v2/tracks/{track_id}/lyrics", tag = "catalog", params(("track_id" = Uuid, Path)), responses((status = 200, body = crate::lyrics::LyricsList), (status = 401, body = ErrorResponse), (status = 404, body = ErrorResponse)))]
+#[utoipa::path(get, path = "/api/v2/tracks/{track_id}/lyrics", tag = "catalog", params(("track_id" = Uuid, Path)), responses((status = 200, body = crate::library::lyrics::LyricsList), (status = 401, body = ErrorResponse), (status = 404, body = ErrorResponse)))]
pub async fn get_track_lyrics(
State(state): State,
Path(track_id): Path,
headers: HeaderMap,
-) -> Result, ApiError> {
+) -> Result, ApiError> {
let user = authenticated(&state, &headers, Access::Read).await?;
// The native answer: uploads included, in RFC-012's order, each sheet
// naming its source. The Subsonic façade reads `lyrics` instead.
@@ -109,8 +109,8 @@ pub async fn get_track_lyrics(
.map_err(service_error)
}
-fn lyrics_format(value: &str) -> Result {
- crate::lyrics::UploadFormat::parse(value).ok_or(ApiError::NotFound)
+fn lyrics_format(value: &str) -> Result {
+ crate::library::lyrics::UploadFormat::parse(value).ok_or(ApiError::NotFound)
}
/// Uploads a lyrics sheet to a track (RFC-012 decision 6).
@@ -120,13 +120,13 @@ fn lyrics_format(value: &str) -> Result {
/// again in the same format replaces it. A sheet with nothing usable in it is
/// refused with `422`. Owners and managers only; anyone else gets the `404` a
/// track they could not see would give.
-#[utoipa::path(put, path = "/api/v2/tracks/{track_id}/lyrics/{format}", tag = "catalog", params(("track_id" = Uuid, Path), ("format" = String, Path, description = "lrc, txt or ttml")), request_body(content = Vec, content_type = "application/octet-stream"), responses((status = 200, body = crate::lyrics::LyricsList), (status = 401, body = ErrorResponse), (status = 404, body = ErrorResponse), (status = 413), (status = 422, body = ErrorResponse)))]
+#[utoipa::path(put, path = "/api/v2/tracks/{track_id}/lyrics/{format}", tag = "catalog", params(("track_id" = Uuid, Path), ("format" = String, Path, description = "lrc, txt or ttml")), request_body(content = Vec, content_type = "application/octet-stream"), responses((status = 200, body = crate::library::lyrics::LyricsList), (status = 401, body = ErrorResponse), (status = 404, body = ErrorResponse), (status = 413), (status = 422, body = ErrorResponse)))]
pub async fn put_track_lyrics(
State(state): State,
Path((track_id, format)): Path<(Uuid, String)>,
headers: HeaderMap,
body: axum::body::Bytes,
-) -> Result, ApiError> {
+) -> Result, ApiError> {
let user = authenticated(&state, &headers, Access::Write).await?;
let format = lyrics_format(&format)?;
state
diff --git a/src/api/users.rs b/src/api/users.rs
index c86c0bb5..ae553d98 100644
--- a/src/api/users.rs
+++ b/src/api/users.rs
@@ -8,12 +8,12 @@ use super::*;
pub struct CreateUserRequest {
pub username: String,
pub web_password: String,
- pub role: crate::database::AccountRole,
+ pub role: crate::storage::database::AccountRole,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateUserRequest {
- pub role: Option,
+ pub role: Option,
pub disabled: Option,
pub library_ids: Option>,
pub subsonic_password: Option,
@@ -81,7 +81,7 @@ pub async fn update_user(
crate::services::UserUpdate {
admin: request
.role
- .map(|role| role == crate::database::AccountRole::Admin),
+ .map(|role| role == crate::storage::database::AccountRole::Admin),
disabled: request.disabled,
folder_ids: request.library_ids.as_deref(),
subsonic_password: request.subsonic_password.as_deref(),
diff --git a/src/api/web_session.rs b/src/api/web_session.rs
index 3f168c3e..34a4f72b 100644
--- a/src/api/web_session.rs
+++ b/src/api/web_session.rs
@@ -9,16 +9,16 @@ pub struct WebAuthResponse {
pub access_token: String,
pub token_type: &'static str,
pub expires_in: u64,
- pub user: crate::authentication::AuthUser,
+ pub user: crate::auth::authentication::AuthUser,
pub device_id: Uuid,
}
pub(super) fn web_auth_response(
state: &AppState,
_headers: &HeaderMap,
- tokens: crate::authentication::AuthTokens,
+ tokens: crate::auth::authentication::AuthTokens,
) -> Result {
- let csrf_token = crate::security::generate_token("wfcsrf_");
+ let csrf_token = crate::auth::security::generate_token("wfcsrf_");
let secure = secure_cookies(state);
let refresh_cookie = format!(
"{WEB_REFRESH_COOKIE}={}; Path=/api/v2/web/auth; HttpOnly; SameSite=Strict; Max-Age={}{}",
@@ -86,7 +86,7 @@ pub(super) fn validate_web_request(state: &AppState, headers: &HeaderMap) -> Res
.get(WEB_CSRF_HEADER)
.and_then(|value| value.to_str().ok())
.ok_or(ApiError::Forbidden)?;
- if !crate::security::constant_time_bytes_eq(cookie.as_bytes(), supplied.as_bytes()) {
+ if !crate::auth::security::constant_time_bytes_eq(cookie.as_bytes(), supplied.as_bytes()) {
return Err(ApiError::Forbidden);
}
Ok(())
diff --git a/src/app/boot.rs b/src/app/boot.rs
index dedabc2a..c06a4593 100644
--- a/src/app/boot.rs
+++ b/src/app/boot.rs
@@ -5,8 +5,9 @@ use std::{sync::Arc, time::Duration};
use super::state::{chunk_body_limit, negotiation_body_limit, AppState};
use crate::{
- authentication, database, enrichment, link_roots, lrclib, media, migration_backup, quality,
- scanner, scrobblers, security, services, setup_token, sync, Config,
+ auth::authentication, auth::security, auth::setup_token, library::link_roots, library::scanner,
+ media, quality, remote::enrichment, remote::lrclib, remote::scrobblers, services,
+ storage::database, storage::migration_backup, storage::sync, Config,
};
pub async fn initialize(config: &Config) -> anyhow::Result {
diff --git a/src/app/openapi.rs b/src/app/openapi.rs
index eb56d758..e87cd174 100644
--- a/src/app/openapi.rs
+++ b/src/app/openapi.rs
@@ -5,7 +5,10 @@
use axum::response::{IntoResponse, Response};
use utoipa::OpenApi;
-use crate::{api, authentication, catalog, database, lyrics, media, services, sync};
+use crate::{
+ api, auth::authentication, library::lyrics, media, services, storage::catalog,
+ storage::database, storage::sync,
+};
pub const OPENAPI_JSON_PATH: &str = "/openapi.json";
pub const SCALAR_PATH: &str = "/reference";
@@ -339,7 +342,7 @@ pub const SCALAR_PATH: &str = "/reference";
api::UpdateUserRequest,
api::SetSubsonicCredentialRequest,
api::SubsonicCredentialResponse,
- crate::database::SubsonicCredentialStatus,
+ crate::storage::database::SubsonicCredentialStatus,
api::BookmarkRequest,
api::RadioStationRequest,
api::CreateApiTokenRequest,
diff --git a/src/app/router.rs b/src/app/router.rs
index c6593b5e..57b44e97 100644
--- a/src/app/router.rs
+++ b/src/app/router.rs
@@ -12,7 +12,7 @@ use utoipa::OpenApi;
use super::openapi::{openapi_response, ApiDoc, OPENAPI_JSON_PATH};
use super::state::AppState;
-use crate::{api, media, public_share, reference, subsonic, webui, Config};
+use crate::{api, http::public_share, http::reference, http::webui, media, subsonic, Config};
const REQUEST_ID_HEADER: &str = "x-request-id";
diff --git a/src/app/state.rs b/src/app/state.rs
index a0e8e91d..db6272e3 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -4,7 +4,8 @@
use std::sync::Arc;
use crate::{
- authentication, config, database, media, scanner, security, services, setup_token, sync,
+ auth::authentication, auth::security, auth::setup_token, config, library::scanner, media,
+ services, storage::database, storage::sync,
};
#[derive(Clone)]
diff --git a/src/auth/authentication.rs b/src/auth/authentication.rs
index 3f7ba2f0..d4e78f27 100644
--- a/src/auth/authentication.rs
+++ b/src/auth/authentication.rs
@@ -5,10 +5,10 @@ use utoipa::ToSchema;
use uuid::Uuid;
use crate::{
+ auth::security,
+ auth::sign_in::{self, Refusal, SignInGuard, Surface},
config::Config,
- database::{AccountRole, Database, NewSession},
- security,
- sign_in::{self, Refusal, SignInGuard, Surface},
+ storage::database::{AccountRole, Database, NewSession},
};
#[derive(Clone)]
diff --git a/src/auth/mod.rs b/src/auth/mod.rs
new file mode 100644
index 00000000..3a88da11
--- /dev/null
+++ b/src/auth/mod.rs
@@ -0,0 +1,10 @@
+//! Who is calling: accounts and sessions, the password guard, native-client
+//! OAuth, the first-run setup token, the instance key's cryptography and the
+//! stream tickets browser playback is authorised with.
+
+pub mod authentication;
+pub mod oauth;
+pub mod security;
+pub mod setup_token;
+pub mod sign_in;
+pub mod stream_ticket;
diff --git a/src/auth/oauth/mod.rs b/src/auth/oauth/mod.rs
index 39fcceb6..76ca43ed 100644
--- a/src/auth/oauth/mod.rs
+++ b/src/auth/oauth/mod.rs
@@ -82,7 +82,7 @@ pub fn verify_challenge(challenge: &str, verifier: &str) -> Result<(), GrantErro
let expected = challenge_for(verifier);
// The challenge is public, but constant-time keeps the comparison uniform
// with the rest of the credential handling in this codebase.
- if crate::security::constant_time_bytes_eq(expected.as_bytes(), challenge.as_bytes()) {
+ if crate::auth::security::constant_time_bytes_eq(expected.as_bytes(), challenge.as_bytes()) {
Ok(())
} else {
Err(GrantError::InvalidVerifier)
diff --git a/src/auth/security/mod.rs b/src/auth/security/mod.rs
index c5b3a362..5e49da8c 100644
--- a/src/auth/security/mod.rs
+++ b/src/auth/security/mod.rs
@@ -191,7 +191,7 @@ pub fn token_matches(token: &str, expected_hash: &[u8]) -> bool {
/// `false` means the path was already taken. [`SecretBox::load_or_create`]
/// treats that as a race it lost and re-reads the winner's key;
/// [`SecretBox::create_instance_key`] treats it as a refusal, because it is
-/// staging a file that must not exist. [`crate::setup_token`] writes the
+/// staging a file that must not exist. [`crate::auth::setup_token`] writes the
/// setup token through it too, for the owner-only mode on Unix.
pub(crate) fn write_new_secret_file(path: &Path, bytes: &[u8]) -> Result {
if let Some(parent) = path.parent() {
diff --git a/src/auth/setup_token/mod.rs b/src/auth/setup_token/mod.rs
index e4bc83d0..10fd4efe 100644
--- a/src/auth/setup_token/mod.rs
+++ b/src/auth/setup_token/mod.rs
@@ -27,8 +27,8 @@
use std::path::{Path, PathBuf};
use std::sync::Mutex;
+use crate::auth::security;
use crate::config::{Config, SETUP_TOKEN_MIN_CHARS};
-use crate::security;
/// The file a drawn token is written to, inside the data directory.
pub const FILE_NAME: &str = "setup-token";
diff --git a/src/auth/stream_ticket/mod.rs b/src/auth/stream_ticket/mod.rs
index 90a30210..529d1610 100644
--- a/src/auth/stream_ticket/mod.rs
+++ b/src/auth/stream_ticket/mod.rs
@@ -18,7 +18,7 @@
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use uuid::Uuid;
-use crate::security::{SecretBox, SecurityError};
+use crate::auth::security::{SecretBox, SecurityError};
/// What a ticket opens.
///
diff --git a/src/cli/account.rs b/src/cli/account.rs
index 5956f1e6..120c6deb 100644
--- a/src/cli/account.rs
+++ b/src/cli/account.rs
@@ -8,9 +8,10 @@ use super::{
RevokeCredentialArgs, SetCredentialArgs,
};
use crate::{
- authentication::now_ms,
- database::{AccountRole, Database},
- security, AppState,
+ auth::authentication::now_ms,
+ auth::security,
+ storage::database::{AccountRole, Database},
+ AppState,
};
pub(super) async fn create_account(
diff --git a/src/cli/database.rs b/src/cli/database.rs
index 263b6140..5a1d471c 100644
--- a/src/cli/database.rs
+++ b/src/cli/database.rs
@@ -2,7 +2,7 @@
//! bundle.
use super::{BackupArgs, RestoreArgs};
-use crate::{database::Database, security, AppState};
+use crate::{auth::security, storage::database::Database, AppState};
pub(super) async fn backup(state: &AppState, args: BackupArgs) -> anyhow::Result<()> {
if args.output.exists() {
diff --git a/src/cli/key.rs b/src/cli/key.rs
index a1039d65..fa1f77b1 100644
--- a/src/cli/key.rs
+++ b/src/cli/key.rs
@@ -8,9 +8,9 @@ use sqlx::Connection;
use super::{KeyArgs, KeyCommand};
use crate::{
- authentication::now_ms,
- database::{Database, KeyMaterial, Rekey, SchemaState},
- security,
+ auth::authentication::now_ms,
+ auth::security,
+ storage::database::{Database, KeyMaterial, Rekey, SchemaState},
};
pub async fn key(config: &crate::Config, command: KeyCommand) -> anyhow::Result<()> {
diff --git a/src/cli/library.rs b/src/cli/library.rs
index bc6bd0f5..b6533efa 100644
--- a/src/cli/library.rs
+++ b/src/cli/library.rs
@@ -11,9 +11,9 @@ use super::{
SetWatchArgs, UnavailableArgs,
};
use crate::{
- authentication::now_ms,
- catalog::LibraryRecord,
- database::{Database, LibraryRole, LibraryVisibility},
+ auth::authentication::now_ms,
+ storage::catalog::LibraryRecord,
+ storage::database::{Database, LibraryRole, LibraryVisibility},
AppState,
};
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 11d9d923..cfe7919f 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -31,7 +31,7 @@ use self::scrobble::{
authorize_lastfm, exchange_lastfm, link_scrobble, scrobble_status, unlink_scrobble,
};
use crate::{
- database::{AccountRole, Database},
+ storage::database::{AccountRole, Database},
AppState,
};
@@ -114,7 +114,7 @@ pub async fn execute(command: Command, state: &AppState) -> anyhow::Result<()> {
async fn require_admin(
db: &Database,
username: &str,
-) -> anyhow::Result {
+) -> anyhow::Result {
let account = db
.account_by_username(username)
.await?
diff --git a/src/config/access.rs b/src/config/access.rs
index 73cf8d78..a1d3434c 100644
--- a/src/config/access.rs
+++ b/src/config/access.rs
@@ -46,7 +46,7 @@ pub(super) fn parse_proxy_addresses(list: &str) -> anyhow::Result()
.or_else(|_| entry.parse::().map(ipnet::IpNet::from))
- .map(crate::sign_in::trusted_network)
+ .map(crate::auth::sign_in::trusted_network)
.map_err(|_| anyhow::anyhow!("invalid WAVEFLOW_TRUSTED_PROXIES entry {entry:?}"))
})
.collect()
diff --git a/src/config/for_data_dir.rs b/src/config/for_data_dir.rs
index 77e8ad43..a351a27f 100644
--- a/src/config/for_data_dir.rs
+++ b/src/config/for_data_dir.rs
@@ -50,7 +50,7 @@ impl Config {
// Production's default: the suite purges the way a server does.
purge_grace: Duration::from_secs(168 * 60 * 60),
link_roots: Vec::new(),
- subsonic_replay_gain: crate::replay_gain::ReplayGainMode::Off,
+ subsonic_replay_gain: crate::media::replay_gain::ReplayGainMode::Off,
ffmpeg_path: PathBuf::from("ffmpeg"),
ffprobe_path: PathBuf::from("ffprobe"),
transcode_cache_dir,
@@ -140,7 +140,7 @@ impl Config {
feed_timeout: Duration::from_secs(10),
download_timeout: Duration::from_secs(30),
dir: podcast_dir_for_tests,
- addresses: crate::portrait::AddressPolicy::AllowLoopback,
+ addresses: crate::remote::portrait::AddressPolicy::AllowLoopback,
},
// On, as production defaults, and admitting loopback for the same
// reason the podcast policy above does: the image hosts the suite
@@ -149,7 +149,7 @@ impl Config {
remote_artwork: RemoteArtworkSettings {
enabled: true,
timeout: Duration::from_secs(10),
- addresses: crate::portrait::AddressPolicy::AllowLoopback,
+ addresses: crate::remote::portrait::AddressPolicy::AllowLoopback,
},
allowed_origins: Vec::new(),
trusted_proxies: Vec::new(),
diff --git a/src/config/from_env.rs b/src/config/from_env.rs
index be238551..d11f1a9e 100644
--- a/src/config/from_env.rs
+++ b/src/config/from_env.rs
@@ -81,9 +81,9 @@ impl Config {
let purge_grace = Duration::from_secs(purge_grace_hours.saturating_mul(60 * 60));
let link_roots = parse_link_roots(std::env::var_os("WAVEFLOW_LINK_ROOTS"))?;
let subsonic_replay_gain = match std::env::var("WAVEFLOW_SUBSONIC_REPLAYGAIN") {
- Err(_) => crate::replay_gain::ReplayGainMode::Off,
- Ok(raw) if raw.trim().is_empty() => crate::replay_gain::ReplayGainMode::Off,
- Ok(raw) => crate::replay_gain::ReplayGainMode::parse(&raw).ok_or_else(|| {
+ Err(_) => crate::media::replay_gain::ReplayGainMode::Off,
+ Ok(raw) if raw.trim().is_empty() => crate::media::replay_gain::ReplayGainMode::Off,
+ Ok(raw) => crate::media::replay_gain::ReplayGainMode::parse(&raw).ok_or_else(|| {
anyhow::anyhow!(
"invalid WAVEFLOW_SUBSONIC_REPLAYGAIN: expected off, track or album, found {raw:?}"
)
@@ -243,7 +243,7 @@ impl Config {
let remote_artwork = RemoteArtworkSettings {
enabled: parse_bool_env("WAVEFLOW_REMOTE_ARTWORK", true)?,
timeout: Duration::from_secs(20),
- addresses: crate::portrait::AddressPolicy::Public,
+ addresses: crate::remote::portrait::AddressPolicy::Public,
};
// Absent is empty; a value that is not UTF-8 refuses to boot, rather
// than starting with no trusted proxy and counting every client behind
@@ -279,7 +279,7 @@ impl Config {
// Parsed here rather than at first use: a spec that cannot be parsed —
// a recursive `albumid` above all — would otherwise re-identify the
// whole catalogue silently at the next scan. Refusing to boot says so.
- let pid = crate::pid::PidSpecs {
+ let pid = crate::library::pid::PidSpecs {
album: parse_pid_spec(
"WAVEFLOW_PID_ALBUM",
DEFAULT_PID_ALBUM,
diff --git a/src/config/metadata.rs b/src/config/metadata.rs
index ef3638fd..210d1ed6 100644
--- a/src/config/metadata.rs
+++ b/src/config/metadata.rs
@@ -18,7 +18,7 @@ pub struct MetadataSettings {
/// `WAVEFLOW_METADATA_PROVIDERS`: an ordered, comma-separated list, empty
/// by default, of `lastfm`, `deezer` and `theaudiodb`. The order is the
/// order fields are merged in when read.
- pub providers: Vec,
+ pub providers: Vec,
/// `WAVEFLOW_METADATA_IMAGES`, on by default, `off` to keep the lookups and
/// refuse decision 8's image fetch. A narrowing only: it switches nothing
/// on while `providers` is empty.
@@ -95,7 +95,7 @@ impl std::fmt::Debug for MetadataSettings {
#[derive(Debug, Clone)]
pub struct LyricsSettings {
/// `WAVEFLOW_LYRICS_PROVIDERS`: empty by default, or `lrclib`.
- pub providers: Vec,
+ pub providers: Vec,
/// `WAVEFLOW_LYRICS_TTL_DAYS`: how long lyrics found are kept.
pub ttl: Duration,
/// `WAVEFLOW_LYRICS_MISS_TTL_DAYS`: how long "nothing for this track" is
@@ -179,14 +179,14 @@ pub(super) fn parse_lyrics_settings() -> anyhow::Result {
/// to boot, for the reason [`parse_metadata_providers`] gives.
pub(super) fn parse_lyrics_providers(
list: &str,
-) -> anyhow::Result> {
+) -> anyhow::Result> {
let mut providers = Vec::new();
for name in list
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
{
- let provider = crate::lrclib::LyricsProvider::parse(name)
+ let provider = crate::remote::lrclib::LyricsProvider::parse(name)
.ok_or_else(|| anyhow::anyhow!("invalid WAVEFLOW_LYRICS_PROVIDERS entry {name:?}"))?;
if providers.contains(&provider) {
anyhow::bail!("WAVEFLOW_LYRICS_PROVIDERS names {name:?} twice");
@@ -207,24 +207,24 @@ pub(super) fn parse_metadata_providers(
list: &str,
has_lastfm_key: bool,
has_theaudiodb_key: bool,
-) -> anyhow::Result> {
+) -> anyhow::Result> {
let mut providers = Vec::new();
for name in list
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
{
- let provider = crate::enrichment::Provider::parse(name)
+ let provider = crate::remote::enrichment::Provider::parse(name)
.ok_or_else(|| anyhow::anyhow!("invalid WAVEFLOW_METADATA_PROVIDERS entry {name:?}"))?;
if providers.contains(&provider) {
anyhow::bail!("WAVEFLOW_METADATA_PROVIDERS names {name:?} twice");
}
- if provider == crate::enrichment::Provider::LastFm && !has_lastfm_key {
+ if provider == crate::remote::enrichment::Provider::LastFm && !has_lastfm_key {
anyhow::bail!(
"WAVEFLOW_METADATA_PROVIDERS names lastfm without WAVEFLOW_METADATA_LASTFM_KEY"
);
}
- if provider == crate::enrichment::Provider::TheAudioDb && !has_theaudiodb_key {
+ if provider == crate::remote::enrichment::Provider::TheAudioDb && !has_theaudiodb_key {
anyhow::bail!(
"WAVEFLOW_METADATA_PROVIDERS names theaudiodb without WAVEFLOW_METADATA_THEAUDIODB_KEY"
);
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 28d2606a..7687fdad 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -116,7 +116,7 @@ pub struct Config {
/// `WAVEFLOW_SUBSONIC_REPLAYGAIN`: `off`, the default, `track` or
/// `album`. A request overrides it with `replayGain=`; the original file
/// is never transcoded to apply it.
- pub subsonic_replay_gain: crate::replay_gain::ReplayGainMode,
+ pub subsonic_replay_gain: crate::media::replay_gain::ReplayGainMode,
pub ffmpeg_path: PathBuf,
pub ffprobe_path: PathBuf,
pub transcode_cache_dir: PathBuf,
@@ -265,7 +265,7 @@ pub struct Config {
/// instance configured differently from the run that built its catalogue
/// schedules a full rescan rather than serving a catalogue keyed under a
/// rule it no longer follows.
- pub pid: crate::pid::PidSpecs,
+ pub pid: crate::library::pid::PidSpecs,
}
impl std::fmt::Debug for Config {
diff --git a/src/config/pid.rs b/src/config/pid.rs
index fa5310f5..47899665 100644
--- a/src/config/pid.rs
+++ b/src/config/pid.rs
@@ -30,18 +30,19 @@ pub(super) fn parse_pid_spec(
name: &str,
default: &str,
kind: PidSpecKind,
-) -> anyhow::Result {
+) -> anyhow::Result {
let raw = std::env::var(name).unwrap_or_else(|_| default.to_owned());
- crate::pid::PidSpec::parse(&raw, kind.allows_album_id())
+ crate::library::pid::PidSpec::parse(&raw, kind.allows_album_id())
.map_err(|error| anyhow::anyhow!("invalid {name}: {error}"))
}
-fn default_pid_spec(default: &str, kind: PidSpecKind) -> crate::pid::PidSpec {
- crate::pid::PidSpec::parse(default, kind.allows_album_id()).expect("built-in pid spec default")
+fn default_pid_spec(default: &str, kind: PidSpecKind) -> crate::library::pid::PidSpec {
+ crate::library::pid::PidSpec::parse(default, kind.allows_album_id())
+ .expect("built-in pid spec default")
}
-pub(super) fn default_pid_specs() -> crate::pid::PidSpecs {
- crate::pid::PidSpecs {
+pub(super) fn default_pid_specs() -> crate::library::pid::PidSpecs {
+ crate::library::pid::PidSpecs {
album: default_pid_spec(DEFAULT_PID_ALBUM, PidSpecKind::MayNotReferenceAlbumId),
track: default_pid_spec(DEFAULT_PID_TRACK, PidSpecKind::MayReferenceAlbumId),
artist: default_pid_spec(DEFAULT_PID_ARTIST, PidSpecKind::MayNotReferenceAlbumId),
diff --git a/src/config/podcasts.rs b/src/config/podcasts.rs
index 3b7d1412..30418b95 100644
--- a/src/config/podcasts.rs
+++ b/src/config/podcasts.rs
@@ -45,7 +45,7 @@ pub struct PodcastSettings {
/// Which addresses a fetch may reach. Public ones in production, always;
/// not an environment variable. The test suite's feeds live on loopback,
/// and its fixture admits loopback for them alone.
- pub addresses: crate::portrait::AddressPolicy,
+ pub addresses: crate::remote::portrait::AddressPolicy,
}
/// Remote artwork: the cover a podcast feed names, and the image an account
@@ -65,7 +65,7 @@ pub struct RemoteArtworkSettings {
pub timeout: Duration,
/// Which addresses a fetch may reach, as for podcasts: public ones in
/// production, always, and loopback as well for the test suite's hosts.
- pub addresses: crate::portrait::AddressPolicy,
+ pub addresses: crate::remote::portrait::AddressPolicy,
}
/// The podcast switch and its bounds (RFC-025 decision 7).
@@ -106,6 +106,6 @@ pub(super) fn parse_podcast_settings(
1_800u64,
)?),
dir: data_dir.join("podcasts"),
- addresses: crate::portrait::AddressPolicy::Public,
+ addresses: crate::remote::portrait::AddressPolicy::Public,
})
}
diff --git a/src/config/scrobbling.rs b/src/config/scrobbling.rs
index 390875d9..1326a109 100644
--- a/src/config/scrobbling.rs
+++ b/src/config/scrobbling.rs
@@ -150,9 +150,9 @@ pub(super) fn test_destinations() -> Vec {
]
.into_iter()
.map(|(provider, base)| {
- let url = crate::scrobblers::validate_destination(base, true)
+ let url = crate::remote::scrobblers::validate_destination(base, true)
.expect("a loopback destination nothing is listening on");
- let fingerprint = crate::scrobblers::destination_fingerprint(&url);
+ let fingerprint = crate::remote::scrobblers::destination_fingerprint(&url);
ScrobbleDestination {
provider,
name: DEFAULT_SCROBBLE_DESTINATION.to_owned(),
@@ -214,14 +214,14 @@ pub(super) fn parse_destinations(
Some((name, raw)) => (name.trim(), raw.trim()),
None => (DEFAULT_SCROBBLE_DESTINATION, entry),
};
- crate::scrobblers::validate_destination_name(name)
+ crate::remote::scrobblers::validate_destination_name(name)
.map_err(|error| anyhow::anyhow!("invalid {variable} entry {name:?}: {error}"))?;
// **Never the URL itself in the message.** `validate_destination`
// refuses a destination carrying credentials, so the one thing this
// branch is most likely to be handed is the one thing that must not be
// printed: `http://user:secret@host` would land in a startup log, or a
// support paste, in full. The name and the variant say enough.
- let url = crate::scrobblers::validate_destination(raw, allow_plaintext)
+ let url = crate::remote::scrobblers::validate_destination(raw, allow_plaintext)
.map_err(|error| anyhow::anyhow!("invalid {variable} entry {name:?}: {error}"))?;
if destinations.iter().any(|existing| existing.name == name) {
anyhow::bail!("{variable} declares {name:?} twice");
@@ -229,7 +229,7 @@ pub(super) fn parse_destinations(
// Two names for one machine would give a member two ways to reach the
// same profile and two links to keep in step, and the second would look
// like a different destination in every counter.
- let fingerprint = crate::scrobblers::destination_fingerprint(&url);
+ let fingerprint = crate::remote::scrobblers::destination_fingerprint(&url);
if let Some(twin) = destinations
.iter()
.find(|existing| existing.fingerprint == fingerprint)
diff --git a/src/config/tests.rs b/src/config/tests.rs
index 51a85102..01e441a2 100644
--- a/src/config/tests.rs
+++ b/src/config/tests.rs
@@ -37,7 +37,7 @@ fn trusted_proxies_are_read_strictly() {
/// than starting enrichment narrower than the operator wrote.
#[test]
fn metadata_providers_are_read_strictly() {
- use crate::enrichment::Provider;
+ use crate::remote::enrichment::Provider;
assert_eq!(parse_metadata_providers("", false, false).unwrap(), []);
assert_eq!(parse_metadata_providers(" , ", false, false).unwrap(), []);
@@ -72,7 +72,7 @@ fn metadata_providers_are_read_strictly() {
/// rather than leaving lyrics quietly off.
#[test]
fn lyrics_providers_are_read_strictly() {
- use crate::lrclib::LyricsProvider;
+ use crate::remote::lrclib::LyricsProvider;
assert_eq!(super::metadata::parse_lyrics_providers("").unwrap(), []);
assert_eq!(super::metadata::parse_lyrics_providers(" , ").unwrap(), []);
diff --git a/src/http/mod.rs b/src/http/mod.rs
new file mode 100644
index 00000000..fd752c12
--- /dev/null
+++ b/src/http/mod.rs
@@ -0,0 +1,8 @@
+//! HTTP pieces outside the two API surfaces: the security headers on every
+//! response, the public share page, the `/reference` page and the embedded
+//! web client.
+
+pub mod public_share;
+pub mod reference;
+pub mod security_headers;
+pub mod webui;
diff --git a/src/http/public_share/mod.rs b/src/http/public_share/mod.rs
index 9db9ea60..1aebe7d2 100644
--- a/src/http/public_share/mod.rs
+++ b/src/http/public_share/mod.rs
@@ -27,10 +27,10 @@ use serde_json::Value;
use uuid::Uuid;
use crate::{
- authentication::now_ms,
+ auth::authentication::now_ms,
+ auth::stream_ticket::{self, TicketKind},
media::{OutputFormat, StreamQuery},
services::ServiceError,
- stream_ticket::{self, TicketKind},
AppState,
};
@@ -76,7 +76,7 @@ pub fn router(state: AppState, request_timeout: std::time::Duration) -> Router {
.route("/share/{token}/tracks/{track_id}/cover", get(cover))
.merge(opening)
.layer(axum::middleware::map_response(
- crate::security_headers::share,
+ crate::http::security_headers::share,
))
.with_state(state)
}
@@ -151,7 +151,7 @@ async fn page(state: &AppState, token: &str) -> Response {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
- let mut response = crate::webui::share_page();
+ let mut response = crate::http::webui::share_page();
if !open && response.status() == StatusCode::OK {
*response.status_mut() = StatusCode::NOT_FOUND;
}
diff --git a/src/http/reference/mod.rs b/src/http/reference/mod.rs
index d24e8f33..9f0c4277 100644
--- a/src/http/reference/mod.rs
+++ b/src/http/reference/mod.rs
@@ -29,7 +29,7 @@
//! in the repository and the binary. Every browser that can run the page
//! accepts gzip; a client that does not is handed the bytes decompressed.
//!
-//! **Under a policy of its own** ([`crate::security_headers::reference_policy`]):
+//! **Under a policy of its own** ([`crate::http::security_headers::reference_policy`]):
//! scripts from this origin only, and no connection anywhere else.
use std::{io::Read as _, sync::OnceLock};
@@ -50,7 +50,7 @@ pub const SCALAR_VERSION: &str = "1.73.1";
pub const SCALAR_SHA256: &str = "62c34a0dda38945864011ebaf6e7fa7812a332429120d18922b34d01c4f7ccc8";
/// The bundle, as `gzip -9n` left it.
-static SCALAR_GZIP: &[u8] = include_bytes!("../assets/scalar/api-reference-1.73.1.js.gz");
+static SCALAR_GZIP: &[u8] = include_bytes!("../../../assets/scalar/api-reference-1.73.1.js.gz");
/// Where the page loads the bundle from. Versioned, so it can be cached for
/// good.
@@ -119,7 +119,10 @@ fn serve_page(page: Bytes) -> Response {
let mut response = ([(header::CONTENT_TYPE, "text/html; charset=utf-8")], page).into_response();
let headers = response.headers_mut();
headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
- crate::security_headers::apply_reference(headers, &crate::security_headers::reference_policy());
+ crate::http::security_headers::apply_reference(
+ headers,
+ &crate::http::security_headers::reference_policy(),
+ );
response
}
@@ -178,7 +181,7 @@ async fn serve_script(request_headers: HeaderMap) -> Response {
if gzip {
headers.insert(header::CONTENT_ENCODING, HeaderValue::from_static("gzip"));
}
- crate::security_headers::apply_reference(
+ crate::http::security_headers::apply_reference(
headers,
&HeaderValue::from_static("default-src 'none'; frame-ancestors 'none'"),
);
diff --git a/src/http/security_headers/mod.rs b/src/http/security_headers/mod.rs
index cfc2d2f4..4f7bac30 100644
--- a/src/http/security_headers/mod.rs
+++ b/src/http/security_headers/mod.rs
@@ -3,11 +3,11 @@
//!
//! Three surfaces carry them:
//!
-//! - **the embedded web client** ([`crate::webui`]), every response of the
+//! - **the embedded web client** ([`crate::http::webui`]), every response of the
//! router fallback: the shell, its assets, and its refusals;
//! - **the public shares** under `/share/`, whose token *is* the path, so a
//! referrer would hand it to whatever host a page linked to;
-//! - **the API reference** at `/reference` ([`crate::reference`]), since
+//! - **the API reference** at `/reference` ([`crate::http::reference`]), since
//! RFC-023 serves its script from the binary and can therefore hold it to a
//! policy that names no other host.
//!
diff --git a/src/http/webui.rs b/src/http/webui.rs
index f941134d..bca2a195 100644
--- a/src/http/webui.rs
+++ b/src/http/webui.rs
@@ -6,7 +6,7 @@
//! client-side route resolves by returning the shell.
//!
//! Every response here carries the web client's security headers, a
-//! Content-Security-Policy among them ([`crate::security_headers`]).
+//! Content-Security-Policy among them ([`crate::http::security_headers`]).
use axum::{
body::Body,
@@ -73,7 +73,7 @@ const ASSET_ONLY_PREFIXES: [&str; 2] = ["icons/", "brand/"];
pub async fn handler(uri: Uri) -> Response {
let mut response = respond(uri.path().trim_start_matches('/'));
- crate::security_headers::apply_web_client(response.headers_mut(), policy());
+ crate::http::security_headers::apply_web_client(response.headers_mut(), policy());
response
}
@@ -83,7 +83,7 @@ pub async fn handler(uri: Uri) -> Response {
fn policy() -> &'static HeaderValue {
static POLICY: OnceLock = OnceLock::new();
POLICY.get_or_init(|| {
- HeaderValue::from_str(&crate::security_headers::web_client_policy(
+ HeaderValue::from_str(&crate::http::security_headers::web_client_policy(
&embedded_shell(),
))
// A digest is base64 and every directive is ASCII, so this cannot
@@ -124,12 +124,12 @@ fn shell() -> Response {
/// client by accident, and the one address that does is chosen by its own
/// route. It carries the client's policy, which the shell's inline blocks need
/// to run, with framing refused as on every share response
-/// ([`crate::security_headers::share_page_policy`]); the share route's own
+/// ([`crate::http::security_headers::share_page_policy`]); the share route's own
/// layer adds the rest of the share headers.
pub fn share_page() -> Response {
static POLICY: OnceLock = OnceLock::new();
let policy = POLICY.get_or_init(|| {
- HeaderValue::from_str(&crate::security_headers::share_page_policy(
+ HeaderValue::from_str(&crate::http::security_headers::share_page_policy(
&embedded_shell(),
))
.unwrap_or(HeaderValue::from_static("default-src 'none'"))
diff --git a/src/lib.rs b/src/lib.rs
index 71928351..42c6ac74 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,42 +2,17 @@
pub mod api;
mod app;
-pub mod authentication;
-pub mod catalog;
+pub mod auth;
pub mod cli;
pub mod config;
-pub mod credit_split;
-pub mod database;
-pub mod enrichment;
-pub mod link_roots;
-pub mod lrclib;
-pub mod lyrics;
+pub mod http;
+pub mod library;
pub mod media;
-pub mod migration_backup;
-pub mod natural_order;
-pub mod oauth;
-pub mod outbound;
-pub mod pid;
-pub mod playlist_file;
-pub mod podcast;
-pub mod portrait;
-pub mod public_share;
pub mod quality;
-pub mod reference;
-pub mod replay_gain;
-pub mod scanner;
-pub mod scrobblers;
-pub mod security;
-pub mod security_headers;
+pub mod remote;
pub mod services;
-pub mod setup_token;
-pub mod sign_in;
-pub mod stream_ticket;
+pub mod storage;
pub mod subsonic;
-pub mod sync;
-pub mod tags;
-pub mod watcher;
-pub mod webui;
pub use app::{app, initialize, shutdown_signal, ApiDoc, AppState, OPENAPI_JSON_PATH, SCALAR_PATH};
pub use config::Config;
diff --git a/src/library/credit_split/mod.rs b/src/library/credit_split/mod.rs
index b0c2a581..f434c196 100644
--- a/src/library/credit_split/mod.rs
+++ b/src/library/credit_split/mod.rs
@@ -1,6 +1,6 @@
//! Splitting a joined artist credit, by decision (RFC-015).
//!
-//! The tag mapper (`crate::tags`) cuts an artist value on the separators no
+//! The tag mapper (`crate::library::tags`) cuts an artist value on the separators no
//! band name carries. A comma is not one of them — `Tyler, The Creator` is one
//! artist, `Zedd, Alessia Cara` two — so a credit holding one is left whole,
//! and an owner decides. This is the second step, applied to each name the
@@ -19,7 +19,7 @@ use std::collections::{HashMap, HashSet};
use uuid::Uuid;
-use crate::tags::{Credit, Role};
+use crate::library::tags::{Credit, Role};
/// The characters a joined credit is cut on: the desktop's set, ASCII and
/// fullwidth commas and the two ideographic ones. Unpadded, as the desktop's
@@ -31,7 +31,7 @@ pub const FRAGMENT_SEPARATORS: [char; 4] = [',', ',', '、', '、'];
/// artist row under the default spec — and survives a change of spec, which
/// an artist id would not.
pub fn credit_key(name: &str) -> String {
- crate::pid::str_clear(&name.trim().to_lowercase())
+ crate::library::pid::str_clear(&name.trim().to_lowercase())
}
/// The name cut on [`FRAGMENT_SEPARATORS`], never inside parentheses, trimmed,
diff --git a/src/library/mod.rs b/src/library/mod.rs
new file mode 100644
index 00000000..1eed2637
--- /dev/null
+++ b/src/library/mod.rs
@@ -0,0 +1,12 @@
+//! Reading the music on disk: the scanner and the watcher that wakes it, the
+//! tag mapper, the identifiers derived from tags, credit splitting, the roots a
+//! followed link may reach, playlist files and lyrics sheets.
+
+pub mod credit_split;
+pub mod link_roots;
+pub mod lyrics;
+pub mod pid;
+pub mod playlist_file;
+pub mod scanner;
+pub mod tags;
+pub mod watcher;
diff --git a/src/library/scanner/discovery/mod.rs b/src/library/scanner/discovery/mod.rs
index aab1bc6a..882393d9 100644
--- a/src/library/scanner/discovery/mod.rs
+++ b/src/library/scanner/discovery/mod.rs
@@ -175,7 +175,7 @@ impl Reach<'_> {
/// Whether a followed link may lead to `target`, a canonical path.
fn admits_target(&self, target: &Path) -> bool {
target.starts_with(self.root)
- || crate::link_roots::admits(self.link_roots, self.libraries, target)
+ || crate::library::link_roots::admits(self.link_roots, self.libraries, target)
}
/// What the walk reached at `path`: the canonical path to read it from,
@@ -196,7 +196,7 @@ impl Reach<'_> {
let name = relative_to(self.root, &canonical);
return Some((canonical, name));
}
- (crate::link_roots::admits(self.link_roots, self.libraries, &canonical)
+ (crate::library::link_roots::admits(self.link_roots, self.libraries, &canonical)
&& path.starts_with(self.root))
.then(|| (canonical, relative_to(self.root, path)))
}
@@ -654,10 +654,10 @@ impl Walked<'_> {
/// a relocation is only recorded when exactly one thing did, because moving a
/// favourite onto the wrong row is worse than not moving it at all.
pub(super) fn unclaimed(
- candidates: Vec,
+ candidates: Vec,
walked: &Walked<'_>,
claimed: &HashSet,
-) -> Vec {
+) -> Vec {
candidates
.into_iter()
.filter(|candidate| {
diff --git a/src/library/scanner/extract/mod.rs b/src/library/scanner/extract/mod.rs
index d25b7ec9..e49f102d 100644
--- a/src/library/scanner/extract/mod.rs
+++ b/src/library/scanner/extract/mod.rs
@@ -10,8 +10,8 @@ use lofty::{
use uuid::Uuid;
use crate::{
- catalog::{ArtworkInput, CatalogTrackInput},
- lyrics::{self, LyricsInput},
+ library::lyrics::{self, LyricsInput},
+ storage::catalog::{ArtworkInput, CatalogTrackInput},
};
#[cfg(test)]
@@ -38,7 +38,7 @@ pub(super) enum Scanned {
pub(super) fn scan_file(
path: &Path,
artwork_dir: &Path,
- known: Option,
+ known: Option,
full: bool,
) -> Scanned {
let signature = sidecar_signature(path);
@@ -393,11 +393,11 @@ struct ExtendedTags {
/// `PERFORMER` is the one role whose credit carries a second value. ID3 stores
/// it as a musician-credits frame lofty exposes only through its own tag type,
/// so what reaches here is the generic spelling — `Name (instrument)` — which
-/// [`crate::tags::split_performer`] takes apart.
-fn raw_credits(tag: Option<&lofty::tag::Tag>) -> crate::tags::RawCredits {
- use crate::tags::Role;
+/// [`crate::library::tags::split_performer`] takes apart.
+fn raw_credits(tag: Option<&lofty::tag::Tag>) -> crate::library::tags::RawCredits {
+ use crate::library::tags::Role;
let Some(tag) = tag else {
- return crate::tags::RawCredits::default();
+ return crate::library::tags::RawCredits::default();
};
let values = |key: ItemKey| -> Vec {
tag.get_strings(key)
@@ -438,10 +438,10 @@ fn raw_credits(tag: Option<&lofty::tag::Tag>) -> crate::tags::RawCredits {
let performer_pairs = values(ItemKey::Performer)
.into_iter()
- .map(|value| crate::tags::split_performer(&value))
+ .map(|value| crate::library::tags::split_performer(&value))
.collect();
- crate::tags::RawCredits {
+ crate::library::tags::RawCredits {
artist: values(ItemKey::TrackArtist),
artists: values(ItemKey::TrackArtists),
album_artist: values(ItemKey::AlbumArtist),
diff --git a/src/library/scanner/job.rs b/src/library/scanner/job.rs
index 1921c534..4db5fa94 100644
--- a/src/library/scanner/job.rs
+++ b/src/library/scanner/job.rs
@@ -9,7 +9,7 @@ use uuid::Uuid;
use super::discovery::{discover_audio, unclaimed, Discovery, Reach, Walked};
use super::extract::{scan_file, Scanned};
use super::{normalised_scope, ScanManager, ScanProgress};
-use crate::catalog::{ApplyOutcome, CatalogApply, LibraryRecord};
+use crate::storage::catalog::{ApplyOutcome, CatalogApply, LibraryRecord};
impl ScanManager {
pub(super) async fn run(&self, scan_id: Uuid, library: LibraryRecord) -> anyhow::Result<()> {
@@ -227,7 +227,7 @@ impl ScanManager {
if existing.pid.is_none() {
hints.push((
existing.id,
- crate::catalog::track_pid(
+ crate::storage::catalog::track_pid(
self.db.pid(),
library.id,
&input,
@@ -263,7 +263,7 @@ impl ScanManager {
self.db
.relocation_candidates_by_pid(
library.id,
- crate::catalog::track_pid(
+ crate::storage::catalog::track_pid(
self.db.pid(),
library.id,
&input,
diff --git a/src/library/scanner/mod.rs b/src/library/scanner/mod.rs
index e5a7fb5a..b212b6ba 100644
--- a/src/library/scanner/mod.rs
+++ b/src/library/scanner/mod.rs
@@ -15,8 +15,8 @@ use tokio::sync::{broadcast, watch, Mutex};
use uuid::Uuid;
use crate::{
- catalog::{CatalogTrackInput, LibraryRecord, ScanJobRecord},
- database::Database,
+ storage::catalog::{CatalogTrackInput, LibraryRecord, ScanJobRecord},
+ storage::database::Database,
};
/// One reading of a scan in flight, as it travels between the scanning task and
@@ -94,7 +94,7 @@ pub struct ScanManager {
libraries: Arc>,
/// The folders outside every library a followed link may reach into
/// (`WAVEFLOW_LINK_ROOTS`).
- link_roots: crate::link_roots::LinkRoots,
+ link_roots: crate::library::link_roots::LinkRoots,
}
/// One scan counted as running, for as long as this lives.
@@ -150,13 +150,13 @@ impl ScanManager {
in_flight: Arc::new(watch::Sender::new(0)),
scanning: Arc::new(DashMap::new()),
libraries: Arc::new(watch::Sender::new(0)),
- link_roots: crate::link_roots::LinkRoots::default(),
+ link_roots: crate::library::link_roots::LinkRoots::default(),
}
}
/// The folders outside every library that a library following links may
/// reach into.
- pub fn with_link_roots(mut self, link_roots: crate::link_roots::LinkRoots) -> Self {
+ pub fn with_link_roots(mut self, link_roots: crate::library::link_roots::LinkRoots) -> Self {
self.link_roots = link_roots;
self
}
diff --git a/src/library/watcher/mod.rs b/src/library/watcher/mod.rs
index 61c65aac..e65a760f 100644
--- a/src/library/watcher/mod.rs
+++ b/src/library/watcher/mod.rs
@@ -27,7 +27,9 @@ use notify::{
use tokio::{sync::mpsc, time::Instant};
use uuid::Uuid;
-use crate::{catalog::LibraryRecord, database::Database, scanner::ScanManager};
+use crate::{
+ library::scanner::ScanManager, storage::catalog::LibraryRecord, storage::database::Database,
+};
/// How often the watched set is re-read even when nothing in this process said
/// it changed: the CLI adds and removes libraries from another process.
@@ -89,7 +91,7 @@ impl Settle {
/// The scan this settles into walks those folders only, which is what
/// RFC-018's rules allow a scan that did not look everywhere: only what it
/// walked may be marked missing, and relocation claims nothing outside it.
- /// Past [`crate::scanner::MAX_SCOPE_FOLDERS`] folders the changes were
+ /// Past [`crate::library::scanner::MAX_SCOPE_FOLDERS`] folders the changes were
/// everywhere, and the whole library is walked instead.
pub fn touch_folder(&mut self, library_id: Uuid, folder: String, now: Instant) {
let pending = self.pending.entry(library_id).or_insert(Pending {
@@ -100,7 +102,7 @@ impl Settle {
pending.last = now;
if let Some(folders) = pending.folders.as_mut() {
folders.insert(folder);
- if folders.len() > crate::scanner::MAX_SCOPE_FOLDERS {
+ if folders.len() > crate::library::scanner::MAX_SCOPE_FOLDERS {
pending.folders = None;
}
}
diff --git a/src/library/watcher/tests.rs b/src/library/watcher/tests.rs
index 817fdefc..85d0366f 100644
--- a/src/library/watcher/tests.rs
+++ b/src/library/watcher/tests.rs
@@ -203,7 +203,7 @@ fn a_settled_library_names_the_folders_its_changes_were_in() {
settle.touch_folder(whole, "Rock".into(), start);
settle.touch(whole, start);
settle.touch_folder(whole, "Jazz".into(), start);
- for index in 0..=crate::scanner::MAX_SCOPE_FOLDERS {
+ for index in 0..=crate::library::scanner::MAX_SCOPE_FOLDERS {
settle.touch_folder(crowded, format!("f{index}"), start);
}
let mut due = settle.due_with_scope(start + delay, |_| false);
diff --git a/src/main.rs b/src/main.rs
index 3b39bf02..204db17d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -56,11 +56,11 @@ async fn serve(config: Config, state: waveflow_server::AppState) -> anyhow::Resu
// output is not the place for it. Where to find the token, never the token
// (RFC-019 decision 1).
match state.setup.source() {
- Some(waveflow_server::setup_token::TokenSource::File(path)) => tracing::warn!(
+ Some(waveflow_server::auth::setup_token::TokenSource::File(path)) => tracing::warn!(
path = %path.display(),
"no account exists yet: the first administrator is created with the setup token stored in this file"
),
- Some(waveflow_server::setup_token::TokenSource::Operator) => tracing::warn!(
+ Some(waveflow_server::auth::setup_token::TokenSource::Operator) => tracing::warn!(
"no account exists yet: the first administrator is created with the value of WAVEFLOW_SETUP_TOKEN"
),
None => {}
@@ -68,7 +68,7 @@ async fn serve(config: Config, state: waveflow_server::AppState) -> anyhow::Resu
state.scanner.spawn_background(config.scan_interval);
// A change scanned once it settles, on top of the interval (RFC-021).
if config.watch {
- waveflow_server::watcher::spawn(
+ waveflow_server::library::watcher::spawn(
state.scanner.clone(),
state.db.clone(),
config.watch_delay,
diff --git a/src/media/canvas.rs b/src/media/canvas.rs
index 64358ebe..72a88762 100644
--- a/src/media/canvas.rs
+++ b/src/media/canvas.rs
@@ -232,14 +232,14 @@ pub async fn create_canvas_ticket(
.ok_or(MediaError::NotFound)?;
let expires_at = i64::try_from(state.stream_ticket_ttl.as_millis())
.ok()
- .and_then(|ttl| crate::authentication::now_ms().checked_add(ttl))
+ .and_then(|ttl| crate::auth::authentication::now_ms().checked_add(ttl))
.ok_or_else(|| {
tracing::error!("stream ticket TTL does not fit in an epoch timestamp");
MediaError::Internal
})?;
- let ticket = crate::stream_ticket::mint(
+ let ticket = crate::auth::stream_ticket::mint(
&state.secret_box,
- crate::stream_ticket::TicketKind::Canvas,
+ crate::auth::stream_ticket::TicketKind::Canvas,
user.id,
track_id,
expires_at,
@@ -276,10 +276,13 @@ pub async fn canvas_with_ticket(
// This route serves a canvas, and it says so. Without the assertion an
// audio ticket would open a canvas and the reverse — which is the whole
// reason the kind is inside the sealed payload.
- let (_, user_id, track_id) =
- crate::stream_ticket::verify(&state.secret_box, &ticket, crate::authentication::now_ms())
- .filter(|(kind, ..)| *kind == crate::stream_ticket::TicketKind::Canvas)
- .ok_or(MediaError::NotFound)?;
+ let (_, user_id, track_id) = crate::auth::stream_ticket::verify(
+ &state.secret_box,
+ &ticket,
+ crate::auth::authentication::now_ms(),
+ )
+ .filter(|(kind, ..)| *kind == crate::auth::stream_ticket::TicketKind::Canvas)
+ .ok_or(MediaError::NotFound)?;
// Re-checked on every redemption, so a ticket cannot outlive the membership
// that justified it.
let blob = state
diff --git a/src/media/mod.rs b/src/media/mod.rs
index db6ce7f5..07f21fae 100644
--- a/src/media/mod.rs
+++ b/src/media/mod.rs
@@ -2,6 +2,7 @@
mod artwork;
mod canvas;
+pub mod replay_gain;
mod stream;
#[cfg(test)]
pub(crate) mod tests;
@@ -45,7 +46,7 @@ use tokio::{
};
use uuid::Uuid;
-use crate::{config::Config, replay_gain::ReplayGainMode, AppState};
+use crate::{config::Config, media::replay_gain::ReplayGainMode, AppState};
/// Audio is tenant-scoped and reached through a bearer token or a stream
/// ticket, so the same URL means different things to different callers. No
@@ -109,7 +110,7 @@ struct MediaInner {
ffmpeg_digest: String,
/// The folders outside every library a stored path may lead into through
/// a link (`WAVEFLOW_LINK_ROOTS`).
- link_roots: crate::link_roots::LinkRoots,
+ link_roots: crate::library::link_roots::LinkRoots,
/// What a Subsonic transcode applies of ReplayGain unless the request
/// says otherwise (`WAVEFLOW_SUBSONIC_REPLAYGAIN`).
subsonic_replay_gain: ReplayGainMode,
@@ -196,7 +197,7 @@ impl MediaService {
cache_locks: DashMap::new(),
cache_access: DashMap::new(),
pending_fills: DashSet::new(),
- link_roots: crate::link_roots::LinkRoots::new(config.link_roots.clone()),
+ link_roots: crate::library::link_roots::LinkRoots::new(config.link_roots.clone()),
subsonic_replay_gain: config.subsonic_replay_gain,
active_transcodes: AtomicUsize::new(0),
transcoding_available: true,
@@ -372,7 +373,7 @@ pub fn router(state: AppState) -> Router {
async fn bearer_principal(
state: &AppState,
headers: &HeaderMap,
-) -> Result {
+) -> Result {
crate::api::authenticated(state, headers, crate::api::Access::Read)
.await
.map_err(|_| MediaError::Unauthorized)
diff --git a/src/media/stream.rs b/src/media/stream.rs
index eab18b51..daaf072e 100644
--- a/src/media/stream.rs
+++ b/src/media/stream.rs
@@ -27,7 +27,7 @@ use super::transcode::{cache_key, extension, fill_deadline, mime_for_format};
use super::{
bearer_principal, MediaError, MediaService, OutputFormat, StreamQuery, MEDIA_CACHE_CONTROL,
};
-use crate::{catalog::StreamTrack, replay_gain::ReplayGainMode, AppState};
+use crate::{media::replay_gain::ReplayGainMode, storage::catalog::StreamTrack, AppState};
const MAX_RANGE_BYTES: u64 = 8 * 1024 * 1024;
@@ -73,7 +73,7 @@ impl MediaService {
range: Option<&str>,
mode: ReplayGainMode,
) -> Result {
- let gain = crate::replay_gain::gain(track.replay_gain, mode);
+ let gain = crate::media::replay_gain::gain(track.replay_gain, mode);
if !track.available {
return Err(MediaError::NotFound);
}
@@ -230,14 +230,14 @@ pub async fn create_stream_ticket(
// cannot be represented is a misconfiguration, not something to round off.
let expires_at = i64::try_from(state.stream_ticket_ttl.as_millis())
.ok()
- .and_then(|ttl| crate::authentication::now_ms().checked_add(ttl))
+ .and_then(|ttl| crate::auth::authentication::now_ms().checked_add(ttl))
.ok_or_else(|| {
tracing::error!("stream ticket TTL does not fit in an epoch timestamp");
MediaError::Internal
})?;
- let ticket = crate::stream_ticket::mint(
+ let ticket = crate::auth::stream_ticket::mint(
&state.secret_box,
- crate::stream_ticket::TicketKind::Audio,
+ crate::auth::stream_ticket::TicketKind::Audio,
user.id,
track_id,
expires_at,
@@ -289,10 +289,13 @@ pub async fn stream_with_ticket(
// track, so probing tells an attacker nothing. A ticket of another kind
// joins them: this route serves audio, and it says so rather than trusting
// that only audio tickets reach it.
- let (_, user_id, track_id) =
- crate::stream_ticket::verify(&state.secret_box, &ticket, crate::authentication::now_ms())
- .filter(|(kind, ..)| *kind == crate::stream_ticket::TicketKind::Audio)
- .ok_or(MediaError::NotFound)?;
+ let (_, user_id, track_id) = crate::auth::stream_ticket::verify(
+ &state.secret_box,
+ &ticket,
+ crate::auth::authentication::now_ms(),
+ )
+ .filter(|(kind, ..)| *kind == crate::auth::stream_ticket::TicketKind::Audio)
+ .ok_or(MediaError::NotFound)?;
let track = state
.services
.stream_source_for_user(user_id, track_id)
@@ -473,8 +476,8 @@ pub async fn stream_track(
pub(crate) async fn secure_track_path(
root: &Path,
relative: &str,
- link_roots: &crate::link_roots::LinkRoots,
- policy: &crate::link_roots::LinkPolicy,
+ link_roots: &crate::library::link_roots::LinkRoots,
+ policy: &crate::library::link_roots::LinkPolicy,
) -> io::Result {
let canonical_root = tokio::fs::canonicalize(root).await?;
let relative = Path::new(relative);
@@ -517,7 +520,7 @@ pub(crate) async fn secure_track_path(
let (link_roots, policy, target) =
(link_roots.clone(), policy.clone(), canonical_file.clone());
tokio::task::spawn_blocking(move || {
- crate::link_roots::admits(
+ crate::library::link_roots::admits(
&link_roots.resolved(),
&policy.other_libraries_resolved(),
&target,
diff --git a/src/media/tests.rs b/src/media/tests.rs
index 781d3c47..028a2695 100644
--- a/src/media/tests.rs
+++ b/src/media/tests.rs
@@ -82,8 +82,8 @@ async fn media_paths_reject_parent_components_and_symlinks() {
let root = temp.path().join("library");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("safe.wav"), b"audio").unwrap();
- let none = crate::link_roots::LinkRoots::default();
- let policy = crate::link_roots::LinkPolicy::default();
+ let none = crate::library::link_roots::LinkRoots::default();
+ let policy = crate::library::link_roots::LinkPolicy::default();
assert!(secure_track_path(&root, "safe.wav", &none, &policy)
.await
.is_ok());
@@ -139,8 +139,8 @@ async fn a_link_is_followed_into_an_allowed_folder_only() {
eprintln!("skipped: this system will not let the test make a symbolic link");
return;
}
- let allowed = crate::link_roots::LinkRoots::new(vec![farm.clone()]);
- let following = crate::link_roots::LinkPolicy {
+ let allowed = crate::library::link_roots::LinkRoots::new(vec![farm.clone()]);
+ let following = crate::library::link_roots::LinkPolicy {
follow_links: true,
other_libraries: vec![neighbour.clone()],
};
@@ -162,11 +162,11 @@ async fn a_link_is_followed_into_an_allowed_folder_only() {
.is_err(),
"a link inside the root is stored by its target, never by itself"
);
- let none = crate::link_roots::LinkRoots::default();
+ let none = crate::library::link_roots::LinkRoots::default();
assert!(secure_track_path(&root, "farmed.wav", &none, &following)
.await
.is_err());
- let not_following = crate::link_roots::LinkPolicy {
+ let not_following = crate::library::link_roots::LinkPolicy {
follow_links: false,
..following
};
@@ -203,8 +203,8 @@ async fn cache_pruning_removes_the_least_recently_used_file() {
active_transcodes: std::sync::atomic::AtomicUsize::new(0),
transcoding_available: true,
ffmpeg_digest: String::new(),
- link_roots: crate::link_roots::LinkRoots::default(),
- subsonic_replay_gain: crate::replay_gain::ReplayGainMode::Off,
+ link_roots: crate::library::link_roots::LinkRoots::default(),
+ subsonic_replay_gain: crate::media::replay_gain::ReplayGainMode::Off,
};
prune_cache(&inner).await;
assert!(!old.exists());
@@ -352,8 +352,8 @@ fn a_media_service(dir: &std::path::Path) -> MediaService {
active_transcodes: std::sync::atomic::AtomicUsize::new(0),
transcoding_available: true,
ffmpeg_digest: String::new(),
- link_roots: crate::link_roots::LinkRoots::default(),
- subsonic_replay_gain: crate::replay_gain::ReplayGainMode::Off,
+ link_roots: crate::library::link_roots::LinkRoots::default(),
+ subsonic_replay_gain: crate::media::replay_gain::ReplayGainMode::Off,
}),
}
}
diff --git a/src/media/transcode.rs b/src/media/transcode.rs
index 94577dcd..e5a7bc58 100644
--- a/src/media/transcode.rs
+++ b/src/media/transcode.rs
@@ -29,7 +29,7 @@ use tokio_stream::wrappers::ReceiverStream;
use uuid::Uuid;
use super::{file_input, MediaError, MediaInner, MediaService, OutputFormat, MEDIA_CACHE_CONTROL};
-use crate::replay_gain::Gain;
+use crate::media::replay_gain::Gain;
const STREAM_CHUNK_SIZE: usize = 64 * 1024;
const TRANSCODE_PROFILE_VERSION: &str = "waveflow-ffmpeg-v1";
diff --git a/src/remote/enrichment/mod.rs b/src/remote/enrichment/mod.rs
index 14e29bad..543eecba 100644
--- a/src/remote/enrichment/mod.rs
+++ b/src/remote/enrichment/mod.rs
@@ -37,8 +37,8 @@ use waveflow_core::metadata::{
use crate::{
config::MetadataSettings,
- database::Database,
- portrait::{self, AddressPolicy, Refusal},
+ remote::portrait::{self, AddressPolicy, Refusal},
+ storage::database::Database,
};
/// Last.fm's API root. A constant, not a setting (decision 9): there is no
@@ -325,7 +325,7 @@ impl Enrichment {
) -> anyhow::Result {
let mut sources = Vec::new();
for &provider in &settings.providers {
- let http = crate::outbound::client(settings.request_timeout)
+ let http = crate::remote::outbound::client(settings.request_timeout)
.map_err(|_| anyhow::anyhow!("the outbound client could not be built"))?;
let client = match provider {
Provider::LastFm => {
@@ -507,7 +507,7 @@ impl Enrichment {
return Ok(Vec::new());
}
let rows = engine.db.metadata_artist_rows(&canonical).await?;
- let now = crate::authentication::now_ms();
+ let now = crate::auth::authentication::now_ms();
let mut ordered = Vec::new();
for (index, source) in engine.sources.iter().enumerate() {
let row = rows.iter().find(|row| row.provider == source.provider);
@@ -608,7 +608,7 @@ impl Engine {
return;
}
};
- let fetched_at = crate::authentication::now_ms();
+ let fetched_at = crate::auth::authentication::now_ms();
let outcome = answer.outcome();
let ttl = match outcome {
Outcome::Hit => self.ttl_ms,
diff --git a/src/remote/lrclib/mod.rs b/src/remote/lrclib/mod.rs
index 7574390a..09b7e04f 100644
--- a/src/remote/lrclib/mod.rs
+++ b/src/remote/lrclib/mod.rs
@@ -27,7 +27,7 @@ use std::{sync::Arc, time::Duration};
use dashmap::DashMap;
use serde::Deserialize;
-use crate::{config::LyricsSettings, database::Database, enrichment::Pace};
+use crate::{config::LyricsSettings, remote::enrichment::Pace, storage::database::Database};
/// LRCLIB's root. A constant, not a setting: there is no operator field, so
/// there is no URL to validate.
@@ -218,7 +218,7 @@ impl LyricsLookup {
let Some(&provider) = settings.providers.first() else {
return Ok(Self::disabled());
};
- let http = crate::outbound::builder(settings.request_timeout)
+ let http = crate::remote::outbound::builder(settings.request_timeout)
.user_agent(USER_AGENT)
.build()
.map_err(|_| anyhow::anyhow!("the outbound client could not be built"))?;
@@ -275,7 +275,7 @@ impl LyricsLookup {
let Some(key) = track.key() else {
return Ok(None);
};
- let now = crate::authentication::now_ms();
+ let now = crate::auth::authentication::now_ms();
if let Some(row) = engine
.db
.lyrics_lookup_row(engine.provider.as_str(), &key)
@@ -380,7 +380,7 @@ impl Engine {
return;
}
};
- let fetched_at = crate::authentication::now_ms();
+ let fetched_at = crate::auth::authentication::now_ms();
let ttl = match answer.outcome {
LyricsOutcome::Hit | LyricsOutcome::Instrumental => self.ttl_ms,
LyricsOutcome::Miss => self.miss_ttl_ms,
@@ -516,11 +516,11 @@ fn judge(record: Record, duration_ms: i64) -> Answer {
/// (`outbound::read_bounded_body`), under the lyrics cap rather than its
/// general one: a sheet is held to what an upload or a sidecar may weigh.
async fn read_bounded(response: reqwest::Response, limit: usize) -> Result, Verdict> {
- crate::outbound::read_bounded_body(response, limit)
+ crate::remote::outbound::read_bounded_body(response, limit)
.await
.map_err(|error| match error {
- crate::outbound::BodyError::TooLarge => Verdict::TooLarge,
- crate::outbound::BodyError::Unread => Verdict::Transport,
+ crate::remote::outbound::BodyError::TooLarge => Verdict::TooLarge,
+ crate::remote::outbound::BodyError::Unread => Verdict::Transport,
})
}
diff --git a/src/remote/mod.rs b/src/remote/mod.rs
new file mode 100644
index 00000000..4c824913
--- /dev/null
+++ b/src/remote/mod.rs
@@ -0,0 +1,10 @@
+//! Everything that talks to another host: the outbound HTTP client, metadata
+//! enrichment and the portraits it fetches, LRCLIB lyrics, podcast feeds and
+//! the scrobbling adapters.
+
+pub mod enrichment;
+pub mod lrclib;
+pub mod outbound;
+pub mod podcast;
+pub mod portrait;
+pub mod scrobblers;
diff --git a/src/remote/podcast/mod.rs b/src/remote/podcast/mod.rs
index 0caebc3c..8d25c7b2 100644
--- a/src/remote/podcast/mod.rs
+++ b/src/remote/podcast/mod.rs
@@ -32,7 +32,7 @@ use std::{
time::Duration,
};
-use crate::portrait::AddressPolicy;
+use crate::remote::portrait::AddressPolicy;
/// How many redirects one fetch follows. Enclosures commonly pass through one
/// or two measurement prefixes on their way to a CDN; five leaves room for
@@ -182,7 +182,7 @@ pub async fn open(url: &url::Url, policy: FetchPolicy) -> Result FetchError::TooLarge,
- crate::outbound::BodyError::Unread => FetchError::Unreached,
+ crate::remote::outbound::BodyError::TooLarge => FetchError::TooLarge,
+ crate::remote::outbound::BodyError::Unread => FetchError::Unreached,
})
};
tokio::time::timeout(policy.timeout, within)
diff --git a/src/remote/portrait/mod.rs b/src/remote/portrait/mod.rs
index adeffff1..c47c8d97 100644
--- a/src/remote/portrait/mod.rs
+++ b/src/remote/portrait/mod.rs
@@ -237,7 +237,7 @@ async fn fetch_within(
if plain && !pinned.ip().is_loopback() {
return Err(Refusal::Destination);
}
- let client = crate::outbound::builder(timeout)
+ let client = crate::remote::outbound::builder(timeout)
.resolve(&host, pinned)
.build()
.map_err(|_| Refusal::Unreached)?;
@@ -291,11 +291,11 @@ async fn checked_image(response: reqwest::Response, rule: Declared) -> Result Refusal::TooLarge,
- crate::outbound::BodyError::Unread => Refusal::Unreached,
+ crate::remote::outbound::BodyError::TooLarge => Refusal::TooLarge,
+ crate::remote::outbound::BodyError::Unread => Refusal::Unreached,
})?;
let format = sniff(&bytes).ok_or(Refusal::NotAnImage)?;
if declared.is_some_and(|declared| declared != format) {
@@ -314,19 +314,19 @@ async fn checked_image(response: reqwest::Response, rule: Declared) -> Result Result {
let within = async {
- let response = crate::podcast::open(url, policy)
+ let response = crate::remote::podcast::open(url, policy)
.await
.map_err(Refusal::from)?;
checked_image(response, Declared::ImageOrUnlabelled).await
@@ -336,9 +336,9 @@ pub async fn fetch_followed(
.unwrap_or(Err(Refusal::Unreached))
}
-impl From for Refusal {
- fn from(error: crate::podcast::FetchError) -> Self {
- use crate::podcast::FetchError;
+impl From for Refusal {
+ fn from(error: crate::remote::podcast::FetchError) -> Self {
+ use crate::remote::podcast::FetchError;
match error {
FetchError::Destination => Self::Destination,
FetchError::Address => Self::Address,
diff --git a/src/remote/scrobblers/lastfm.rs b/src/remote/scrobblers/lastfm.rs
index fd89b865..1730de8f 100644
--- a/src/remote/scrobblers/lastfm.rs
+++ b/src/remote/scrobblers/lastfm.rs
@@ -70,7 +70,7 @@ impl LastFm {
///
/// Bounded twice, which is the whole of RFC-023 decision 1 for this path:
/// in time by [`Self::deadline`], and in size by
- /// [`crate::outbound::read_bounded_body`]. Either refusal is
+ /// [`crate::remote::outbound::read_bounded_body`]. Either refusal is
/// `Unavailable`, a failure of the way to Last.fm rather than of what the
/// person brought.
///
@@ -100,13 +100,15 @@ impl LastFm {
tracing::warn!(call = what, %status, "last.fm refused the call");
return Err(ServiceError::Invalid);
}
- let body =
- crate::outbound::read_bounded_body(response, crate::outbound::MAX_ANSWER_BYTES)
- .await
- .map_err(|error| {
- tracing::warn!(call = what, %error, "last.fm's answer was not read");
- ServiceError::Unavailable
- })?;
+ let body = crate::remote::outbound::read_bounded_body(
+ response,
+ crate::remote::outbound::MAX_ANSWER_BYTES,
+ )
+ .await
+ .map_err(|error| {
+ tracing::warn!(call = what, %error, "last.fm's answer was not read");
+ ServiceError::Unavailable
+ })?;
serde_json::from_slice::(&body).map_err(|_| {
tracing::warn!(call = what, "last.fm's answer is not the expected JSON");
ServiceError::Unavailable
@@ -252,9 +254,9 @@ impl ScrobbleTarget for LastFm {
if !status.is_success() {
return status_verdict(status, retry_after);
}
- let answer = match crate::outbound::read_bounded_body(
+ let answer = match crate::remote::outbound::read_bounded_body(
response,
- crate::outbound::MAX_ANSWER_BYTES,
+ crate::remote::outbound::MAX_ANSWER_BYTES,
)
.await
{
@@ -708,7 +710,7 @@ mod tests {
/// An adapter aimed at a listener of the test's own.
async fn reaching(body: axum::body::Body) -> (LastFm, tokio::task::JoinHandle<()>) {
- let (url, server) = crate::outbound::tests::answering(body).await;
+ let (url, server) = crate::remote::outbound::tests::answering(body).await;
let base = super::super::validate_destination(&url, true).expect("a destination");
let client = super::super::outbound_client(Duration::from_secs(5)).expect("a client");
let application = crate::config::LastFmApplication {
@@ -731,7 +733,7 @@ mod tests {
async fn a_session_answer_over_the_ceiling_is_not_read() {
use crate::services::{LastFmSessionExchange, ServiceError};
let key = uuid::Uuid::new_v4().simple().to_string();
- let padding = " ".repeat(crate::outbound::MAX_ANSWER_BYTES);
+ let padding = " ".repeat(crate::remote::outbound::MAX_ANSWER_BYTES);
let (target, server) = reaching(axum::body::Body::from(format!(
"{{\"session\":{{\"key\":\"{key}\"}}{padding}}}"
)))
@@ -760,7 +762,7 @@ mod tests {
async fn a_token_answer_over_the_ceiling_is_not_read() {
use crate::services::{LastFmSessionExchange, ServiceError};
let token = uuid::Uuid::new_v4().simple().to_string();
- let padding = " ".repeat(crate::outbound::MAX_ANSWER_BYTES);
+ let padding = " ".repeat(crate::remote::outbound::MAX_ANSWER_BYTES);
let (target, server) = reaching(axum::body::Body::from(format!(
"{{\"token\":\"{token}\"{padding}}}"
)))
@@ -778,7 +780,7 @@ mod tests {
/// apart.
#[tokio::test]
async fn a_scrobble_answer_over_the_ceiling_leaves_the_status_line_deciding() {
- let padding = " ".repeat(crate::outbound::MAX_ANSWER_BYTES);
+ let padding = " ".repeat(crate::remote::outbound::MAX_ANSWER_BYTES);
let (target, server) =
reaching(axum::body::Body::from(format!("{{\"error\":9{padding}}}"))).await;
let envelope = an_envelope();
diff --git a/src/remote/scrobblers/maloja.rs b/src/remote/scrobblers/maloja.rs
index de28f7f7..0868e378 100644
--- a/src/remote/scrobblers/maloja.rs
+++ b/src/remote/scrobblers/maloja.rs
@@ -154,9 +154,9 @@ impl ScrobbleTarget for Maloja {
// answer over it, or one that breaks off, is not read at all, and
// the status line decides — exactly as for a body that will not
// parse. The verdict is logged; the body never is.
- let refused = match crate::outbound::read_bounded_body(
+ let refused = match crate::remote::outbound::read_bounded_body(
response,
- crate::outbound::MAX_ANSWER_BYTES,
+ crate::remote::outbound::MAX_ANSWER_BYTES,
)
.await
{
@@ -398,10 +398,10 @@ mod tests {
/// which is the difference this tells apart.
#[tokio::test]
async fn an_answer_over_the_ceiling_leaves_the_status_line_deciding() {
- let padding = " ".repeat(crate::outbound::MAX_ANSWER_BYTES);
- let (url, server) = crate::outbound::tests::answering(axum::body::Body::from(format!(
- "{{\"status\":\"failure\"{padding}}}"
- )))
+ let padding = " ".repeat(crate::remote::outbound::MAX_ANSWER_BYTES);
+ let (url, server) = crate::remote::outbound::tests::answering(axum::body::Body::from(
+ format!("{{\"status\":\"failure\"{padding}}}"),
+ ))
.await;
let base = super::super::validate_destination(&url, true).expect("a destination");
let client = super::super::outbound_client(Duration::from_secs(5)).expect("a client");
diff --git a/src/remote/scrobblers/mod.rs b/src/remote/scrobblers/mod.rs
index fe70ab5c..fcf2b480 100644
--- a/src/remote/scrobblers/mod.rs
+++ b/src/remote/scrobblers/mod.rs
@@ -7,7 +7,7 @@
//!
//! Decision 10 calls outbound requests the real risk of the whole RFC — a
//! server that calls a URL is a server somebody can make call a URL — so the
-//! rules live once, in [`crate::outbound`], rather than in each adapter. That
+//! rules live once, in [`crate::remote::outbound`], rather than in each adapter. That
//! module is shared with metadata enrichment (RFC-011), the server's only other
//! outbound surface.
@@ -114,15 +114,15 @@ pub fn canonical_destination(url: &url::Url) -> String {
/// keep `scrobble_link` free of destination addresses, which decision 10 says
/// it holds none of.
pub fn destination_fingerprint(url: &url::Url) -> String {
- hex::encode(crate::security::bytes_hash(
+ hex::encode(crate::auth::security::bytes_hash(
canonical_destination(url).as_bytes(),
))
}
-/// The client every adapter shares: [`crate::outbound::client`], which holds
+/// The client every adapter shares: [`crate::remote::outbound::client`], which holds
/// decision 10's rules and is shared with metadata enrichment.
pub fn outbound_client(timeout: Duration) -> Result {
- crate::outbound::client(timeout).map_err(|_| OutboundError::Unbuildable)
+ crate::remote::outbound::client(timeout).map_err(|_| OutboundError::Unbuildable)
}
/// Checks a destination an operator configured, before it is ever called.
diff --git a/src/services/admin.rs b/src/services/admin.rs
index 5733f7c0..bd716386 100644
--- a/src/services/admin.rs
+++ b/src/services/admin.rs
@@ -397,7 +397,7 @@ impl DomainServices {
let record = self
.db
.create_api_token(
- crate::database::NewApiToken {
+ crate::storage::database::NewApiToken {
actor_id,
user_id: account_id,
name,
diff --git a/src/services/artist_enrichment/mod.rs b/src/services/artist_enrichment/mod.rs
index 70212c09..35ca7e47 100644
--- a/src/services/artist_enrichment/mod.rs
+++ b/src/services/artist_enrichment/mod.rs
@@ -15,7 +15,7 @@
//! handed out with an id a client could not follow.
use super::*;
-use crate::enrichment::{CachedArtist, Outcome, Provider};
+use crate::remote::enrichment::{CachedArtist, Outcome, Provider};
/// How many neighbours the native artist page carries.
pub const ARTIST_PAGE_SIMILAR: usize = 20;
@@ -93,7 +93,7 @@ impl DomainServices {
// refresh scheduled, and an expired miss is not "nobody knows" but
// "being asked again" — `pending`, as decision 11 defines it. Stale
// content still shows, and `ready` wins over both.
- let now = crate::authentication::now_ms();
+ let now = crate::auth::authentication::now_ms();
let answered = rows.iter().filter(|row| row.expires_at > now).count();
let state = state_of(
bio.is_some() || image.is_some() || banner.is_some() || similar.is_some(),
diff --git a/src/services/artist_enrichment/tests.rs b/src/services/artist_enrichment/tests.rs
index 3d2ed233..44fee857 100644
--- a/src/services/artist_enrichment/tests.rs
+++ b/src/services/artist_enrichment/tests.rs
@@ -1,5 +1,5 @@
use super::{merged_bio, state_of};
-use crate::enrichment::{CachedArtist, Outcome, Provider};
+use crate::remote::enrichment::{CachedArtist, Outcome, Provider};
use crate::services::EnrichmentState;
fn row(outcome: Outcome, bio: Option<&str>, fetched_at: i64) -> CachedArtist {
diff --git a/src/services/artist_image_search/mod.rs b/src/services/artist_image_search/mod.rs
index f3b9ccd0..0e4de3da 100644
--- a/src/services/artist_image_search/mod.rs
+++ b/src/services/artist_image_search/mod.rs
@@ -23,8 +23,8 @@ use uuid::Uuid;
use super::artist_images::{chooser_library, record_artist_change, write_choice};
use super::{ArtistImageKind, DomainServices, ServiceError};
-use crate::authentication::now_ms;
-use crate::enrichment::{DeezerCandidate, Enrichment};
+use crate::auth::authentication::now_ms;
+use crate::remote::enrichment::{DeezerCandidate, Enrichment};
/// How many candidates one search offers: two rows of a picker, and twenty
/// image fetches at most behind one request.
diff --git a/src/services/artist_images.rs b/src/services/artist_images.rs
index e4e29568..c9612cef 100644
--- a/src/services/artist_images.rs
+++ b/src/services/artist_images.rs
@@ -12,9 +12,9 @@ use sqlx::{Row, Sqlite, Transaction};
use uuid::Uuid;
use super::{DomainServices, ImageChoice, ImageChoices, ServiceError};
-use crate::authentication::now_ms;
-use crate::database::LibraryRole;
-use crate::portrait::Portrait;
+use crate::auth::authentication::now_ms;
+use crate::remote::portrait::Portrait;
+use crate::storage::database::LibraryRole;
/// Which of the two images.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -74,9 +74,9 @@ pub(super) async fn record_artist_change(
artist_id: Uuid,
now: i64,
) -> Result<(), sqlx::Error> {
- crate::catalog::record_library_event(
+ crate::storage::catalog::record_library_event(
tx,
- crate::catalog::LibraryChange {
+ crate::storage::catalog::LibraryChange {
library_id,
entity_type: "artist",
entity_id: artist_id,
@@ -181,7 +181,7 @@ impl DomainServices {
let mut read = self.db.pool().begin().await?;
chooser_library(&mut read, user_id, artist_id).await?;
}
- crate::portrait::store(&self.artwork_dir, &image)
+ crate::remote::portrait::store(&self.artwork_dir, &image)
.await
.map_err(|_| ServiceError::Unavailable)?;
let _writer = self.db.writer_guard().await;
diff --git a/src/services/artwork.rs b/src/services/artwork.rs
index ce4566b7..803b9be9 100644
--- a/src/services/artwork.rs
+++ b/src/services/artwork.rs
@@ -234,8 +234,11 @@ impl DomainServices {
// The spent portrait searches first (RFC-013), in the same
// transaction: their previews become unreferenced here, and a preview
// a choice already names is kept by the clause below.
- super::artist_image_search::forget_spent_searches(&mut tx, crate::authentication::now_ms())
- .await?;
+ super::artist_image_search::forget_spent_searches(
+ &mut tx,
+ crate::auth::authentication::now_ms(),
+ )
+ .await?;
let removed = sqlx::query(
"DELETE FROM artwork WHERE \
NOT EXISTS (SELECT 1 FROM artist WHERE artist.artwork_hash = artwork.hash) \
diff --git a/src/services/canvas/mod.rs b/src/services/canvas/mod.rs
index 7ce04036..84ec048e 100644
--- a/src/services/canvas/mod.rs
+++ b/src/services/canvas/mod.rs
@@ -36,7 +36,7 @@ use sqlx::Row;
use uuid::Uuid;
use super::{parse_uuid, CanvasBlob, DomainServices, ServiceError};
-use crate::authentication::now_ms;
+use crate::auth::authentication::now_ms;
mod placement;
mod probe;
diff --git a/src/services/canvas/placement.rs b/src/services/canvas/placement.rs
index 74de14c6..5acfcc3b 100644
--- a/src/services/canvas/placement.rs
+++ b/src/services/canvas/placement.rs
@@ -193,7 +193,7 @@ impl DomainServices {
.fetch_optional(&mut *tx)
.await?
.ok_or(ServiceError::NotFound)?;
- let role = crate::database::LibraryRole::from_str(row.try_get::<&str, _>("role")?)
+ let role = crate::storage::database::LibraryRole::from_str(row.try_get::<&str, _>("role")?)
.map_err(|_| ServiceError::Invalid)?;
if !role.may_upload() {
return Err(ServiceError::Forbidden);
@@ -311,9 +311,9 @@ impl DomainServices {
origin_device_id: Option,
now: i64,
) -> Result<(), ServiceError> {
- crate::catalog::record_library_event(
+ crate::storage::catalog::record_library_event(
tx,
- crate::catalog::LibraryChange {
+ crate::storage::catalog::LibraryChange {
library_id: parse_uuid(library_id.to_owned())?,
entity_type: "track",
entity_id: track_id,
@@ -350,8 +350,9 @@ impl DomainServices {
.fetch_optional(&mut *tx)
.await?
.ok_or(ServiceError::NotFound)?;
- let role = crate::database::LibraryRole::from_str(row.try_get::<&str, _>("role")?)
- .map_err(|_| ServiceError::Invalid)?;
+ let role =
+ crate::storage::database::LibraryRole::from_str(row.try_get::<&str, _>("role")?)
+ .map_err(|_| ServiceError::Invalid)?;
if !role.may_upload() {
return Err(ServiceError::Forbidden);
}
@@ -507,7 +508,7 @@ impl DomainServices {
.fetch_optional(self.db.pool())
.await?
.ok_or(ServiceError::NotFound)?;
- let role = crate::database::LibraryRole::from_str(row.try_get::<&str, _>("role")?)
+ let role = crate::storage::database::LibraryRole::from_str(row.try_get::<&str, _>("role")?)
.map_err(|_| ServiceError::Invalid)?;
if !role.may_upload() || row.try_get::("accepts_canvas")? == 0 {
return Err(ServiceError::Forbidden);
diff --git a/src/services/catalog.rs b/src/services/catalog.rs
index a7fd1860..01270ebc 100644
--- a/src/services/catalog.rs
+++ b/src/services/catalog.rs
@@ -148,7 +148,7 @@ impl DomainServices {
album_page: BrowsePage,
song_page: BrowsePage,
) -> Result {
- let Some(fts) = crate::catalog::fts_prefix_query(query) else {
+ let Some(fts) = crate::storage::catalog::fts_prefix_query(query) else {
return Ok(CatalogSearch {
artists: Vec::new(),
albums: Vec::new(),
diff --git a/src/services/credentials.rs b/src/services/credentials.rs
index b8733a02..f01c4973 100644
--- a/src/services/credentials.rs
+++ b/src/services/credentials.rs
@@ -80,10 +80,13 @@ impl DomainServices {
user_id: Uuid,
request: AuthorizationRequest<'_>,
) -> Result {
- crate::oauth::validate_redirect_uri(request.redirect_uri)
- .map_err(|_| ServiceError::Invalid)?;
- crate::oauth::validate_challenge(request.code_challenge_method, request.code_challenge)
+ crate::auth::oauth::validate_redirect_uri(request.redirect_uri)
.map_err(|_| ServiceError::Invalid)?;
+ crate::auth::oauth::validate_challenge(
+ request.code_challenge_method,
+ request.code_challenge,
+ )
+ .map_err(|_| ServiceError::Invalid)?;
let client_id = request.client_id.trim();
let device_name = request.device_name.trim();
// Checked before the code exists: a name the session issuer would
@@ -95,7 +98,7 @@ impl DomainServices {
let code = security::generate_token("wfc_");
let now = now_ms();
self.db
- .create_authorization(crate::database::NewAuthorization {
+ .create_authorization(crate::storage::database::NewAuthorization {
code_hash: security::token_hash(&code),
user_id,
client_id,
@@ -103,11 +106,11 @@ impl DomainServices {
code_challenge: request.code_challenge,
device_name,
now_ms: now,
- expires_at: now + crate::oauth::AUTHORIZATION_CODE_TTL_MS,
+ expires_at: now + crate::auth::oauth::AUTHORIZATION_CODE_TTL_MS,
scopes: request.scopes,
})
.await?;
- Ok(crate::oauth::redirect_with_code(
+ Ok(crate::auth::oauth::redirect_with_code(
request.redirect_uri,
&code,
request.state,
diff --git a/src/services/credit_decisions.rs b/src/services/credit_decisions.rs
index e33ebe6f..06d8aa48 100644
--- a/src/services/credit_decisions.rs
+++ b/src/services/credit_decisions.rs
@@ -3,7 +3,7 @@
//! A decision belongs to the library: taken by its owners and managers, read
//! back by every scan, applied at once to the stored catalogue without
//! reading a file, and undone the same way. The rule itself is
-//! `crate::credit_split`; this is who may invoke it, and what follows from it
+//! `crate::library::credit_split`; this is who may invoke it, and what follows from it
//! — the library feed, the derivations a scan would rerun, and the favourites
//! and ratings that would otherwise point at an artist nobody credits.
@@ -13,9 +13,9 @@ use sqlx::{Sqlite, Transaction};
use tokio::sync::OwnedMutexGuard;
use super::*;
-use crate::authentication::now_ms;
-use crate::credit_split::{credit_key, fragments, is_candidate};
-use crate::database::LibraryRole;
+use crate::auth::authentication::now_ms;
+use crate::library::credit_split::{credit_key, fragments, is_candidate};
+use crate::storage::database::LibraryRole;
/// One fragment of a credit an owner is asked about, and whether the library
/// already has an artist of that name — the strongest hint that a split is
@@ -109,8 +109,8 @@ fn record_event(
action: &'static str,
payload: serde_json::Value,
now: i64,
-) -> crate::catalog::LibraryChange<'static> {
- crate::catalog::LibraryChange {
+) -> crate::storage::catalog::LibraryChange<'static> {
+ crate::storage::catalog::LibraryChange {
library_id,
entity_type,
entity_id,
@@ -301,7 +301,7 @@ impl DomainServices {
.map(|part| self.db.pid().artist_id(library_id, part.trim()))
.collect();
for part in &part_ids {
- crate::catalog::record_library_event(
+ crate::storage::catalog::record_library_event(
&mut tx,
record_event(
library_id,
@@ -318,7 +318,7 @@ impl DomainServices {
receipts = self
.carry_user_data(&writer, &mut tx, artist_id, &part_ids)
.await?;
- crate::catalog::record_library_event(
+ crate::storage::catalog::record_library_event(
&mut tx,
record_event(
library_id,
@@ -408,7 +408,7 @@ impl DomainServices {
self.record_moved(&mut tx, library_id, &moved, now).await?;
let joined = self.db.pid().artist_id(library_id, credit.trim());
if still_credited(&mut tx, joined).await? {
- crate::catalog::record_library_event(
+ crate::storage::catalog::record_library_event(
&mut tx,
record_event(
library_id,
@@ -427,7 +427,7 @@ impl DomainServices {
} else {
"delete"
};
- crate::catalog::record_library_event(
+ crate::storage::catalog::record_library_event(
&mut tx,
record_event(
library_id,
@@ -456,11 +456,11 @@ impl DomainServices {
tracks: &[String],
now: i64,
) -> Result {
- let decisions = crate::catalog::credit_decisions(tx, library_id).await?;
+ let decisions = crate::storage::catalog::credit_decisions(tx, library_id).await?;
let mut moved = Moved::default();
for track in tracks {
let track_id = parse_uuid(track.clone())?;
- let outcome = crate::catalog::recompute_track_participants(
+ let outcome = crate::storage::catalog::recompute_track_participants(
tx,
self.db.pid(),
library_id,
@@ -487,7 +487,7 @@ impl DomainServices {
now: i64,
) -> Result<(), ServiceError> {
for (track, full_hash) in &moved.tracks {
- crate::catalog::record_library_event(
+ crate::storage::catalog::record_library_event(
tx,
record_event(
library_id,
@@ -501,7 +501,7 @@ impl DomainServices {
.await?;
}
for album in &moved.albums {
- crate::catalog::record_library_event(
+ crate::storage::catalog::record_library_event(
tx,
record_event(
library_id,
@@ -720,8 +720,8 @@ impl DomainServices {
/// the feed has already announced as deleted, and a retry would only answer
/// `409`. Together, the two land or neither does.
async fn rederive(tx: &mut Transaction<'_, Sqlite>, library_id: Uuid) -> Result<(), sqlx::Error> {
- crate::database::Database::consolidate_sort_names_in(tx, library_id).await?;
- crate::database::Database::consolidate_catalog_derivations_in(tx, library_id).await
+ crate::storage::database::Database::consolidate_sort_names_in(tx, library_id).await?;
+ crate::storage::database::Database::consolidate_catalog_derivations_in(tx, library_id).await
}
/// Whether anything still credits an artist: a participant, or an album that
diff --git a/src/services/lastfm.rs b/src/services/lastfm.rs
index c208acaf..faf00a78 100644
--- a/src/services/lastfm.rs
+++ b/src/services/lastfm.rs
@@ -195,8 +195,8 @@ impl DomainServices {
// state is the only thing separating a legitimate return from a
// fabricated one, and the cookie is the only thing separating *this*
// browser's return from anybody who came by the URL afterwards.
- let state = crate::security::generate_token("");
- let cookie = crate::security::generate_token("");
+ let state = crate::auth::security::generate_token("");
+ let cookie = crate::auth::security::generate_token("");
let now = now_ms();
let expires_at =
now.saturating_add(i64::try_from(JOURNEY_TTL.as_millis()).unwrap_or(i64::MAX));
@@ -218,7 +218,7 @@ impl DomainServices {
.bind(user_id.to_string())
.bind(destination)
.bind(&fingerprint)
- .bind(crate::security::token_hash(&cookie).as_slice())
+ .bind(crate::auth::security::token_hash(&cookie).as_slice())
.bind(now)
.bind(expires_at)
.execute(&mut *tx)
@@ -300,7 +300,7 @@ impl DomainServices {
// One refusal for every way of being wrong: expired, or the wrong
// browser. Distinguishing them would tell whoever found the URL
// which half they are missing.
- if now >= expires_at || !crate::security::token_matches(cookie, &cookie_hash) {
+ if now >= expires_at || !crate::auth::security::token_matches(cookie, &cookie_hash) {
return Err(ServiceError::NotFound);
}
(
diff --git a/src/services/library_events.rs b/src/services/library_events.rs
index 5d10efe1..7b9a4502 100644
--- a/src/services/library_events.rs
+++ b/src/services/library_events.rs
@@ -4,7 +4,7 @@
//! no feed at all. Written by scans in `catalog.rs`; read here.
use super::*;
-use crate::sync::MAX_SYNC_LIMIT;
+use crate::storage::sync::MAX_SYNC_LIMIT;
/// How often the feed is trimmed.
///
diff --git a/src/services/library_folders/mod.rs b/src/services/library_folders/mod.rs
index 8e9043c0..ce7ec2be 100644
--- a/src/services/library_folders/mod.rs
+++ b/src/services/library_folders/mod.rs
@@ -18,8 +18,8 @@ use uuid::Uuid;
use super::{
attach_song_relations, song_from_row, BrowsePage, DomainServices, ServiceError, SongItem,
};
-use crate::authentication::now_ms;
-use crate::database::LibraryRole;
+use crate::auth::authentication::now_ms;
+use crate::storage::database::LibraryRole;
/// The deepest a folder may sit: the root is 0, a folder at the root is 1.
pub const MAX_FOLDER_DEPTH: i64 = 16;
diff --git a/src/services/library_lifecycle/edit.rs b/src/services/library_lifecycle/edit.rs
index 6f26808a..eb8efa72 100644
--- a/src/services/library_lifecycle/edit.rs
+++ b/src/services/library_lifecycle/edit.rs
@@ -209,7 +209,7 @@ impl DomainServices {
// the job queued, and the next boot runs it (`ScanManager::boot`).
self.scanner.spawn(
scan_id,
- crate::catalog::LibraryRecord {
+ crate::storage::catalog::LibraryRecord {
id: library_id,
name: name.unwrap_or(before.library.name),
root_path: root.unwrap_or(before.library.root_path),
diff --git a/src/services/library_tree/mod.rs b/src/services/library_tree/mod.rs
index 6d3a2e73..1f27d82f 100644
--- a/src/services/library_tree/mod.rs
+++ b/src/services/library_tree/mod.rs
@@ -126,7 +126,7 @@ impl DomainServices {
/// of an empty library is an empty listing. `Invalid` for a malformed path.
///
/// Folders and tracks sort by name in natural order — numbers by value,
- /// letters case-folded (`crate::natural_order`) — so `2 …` comes before
+ /// letters case-folded (`crate::storage::natural_order`) — so `2 …` comes before
/// `10 …`. Tracks sort by file name, except in the received folder, whose file names are hashes: there they
/// sort by artist then title, a missing artist last.
pub async fn disk_tree(
diff --git a/src/services/lyrics_uploads.rs b/src/services/lyrics_uploads.rs
index f21db405..b36a92dd 100644
--- a/src/services/lyrics_uploads.rs
+++ b/src/services/lyrics_uploads.rs
@@ -14,8 +14,8 @@ use sqlx::Row;
use uuid::Uuid;
use super::{DomainServices, ServiceError};
-use crate::authentication::now_ms;
-use crate::lyrics::{self, LyricsList, StructuredLyrics, UploadFormat};
+use crate::auth::authentication::now_ms;
+use crate::library::lyrics::{self, LyricsList, StructuredLyrics, UploadFormat};
/// The sidecar's cap, so an upload is held to what a scan would read.
pub const MAX_UPLOAD_BYTES: usize = 1024 * 1024;
@@ -313,7 +313,7 @@ impl DomainServices {
.fetch_optional(&mut **tx)
.await?
.ok_or(ServiceError::NotFound)?;
- let role = crate::database::LibraryRole::from_str(row.try_get::<&str, _>("role")?)
+ let role = crate::storage::database::LibraryRole::from_str(row.try_get::<&str, _>("role")?)
.map_err(|_| ServiceError::Invalid)?;
if !role.may_write_metadata() {
return Err(ServiceError::Forbidden);
diff --git a/src/services/mod.rs b/src/services/mod.rs
index a7390884..7f4b2240 100644
--- a/src/services/mod.rs
+++ b/src/services/mod.rs
@@ -8,12 +8,16 @@ use utoipa::ToSchema;
use uuid::Uuid;
use crate::{
- authentication::now_ms,
- database::{AccountRecord, AccountRole, ApiTokenRecord, Database, SubsonicCredentialStatus},
- lyrics::{self, LyricsList, StructuredLyrics},
- playlist_file::{self, PlaylistFileEntry},
- security::{self, EncryptedSecret, SecretBox},
- sync::{MutationContext, MutationIntent, MutationReceipt, OperationClaim, SyncService},
+ auth::authentication::now_ms,
+ auth::security::{self, EncryptedSecret, SecretBox},
+ library::lyrics::{self, LyricsList, StructuredLyrics},
+ library::playlist_file::{self, PlaylistFileEntry},
+ storage::database::{
+ AccountRecord, AccountRole, ApiTokenRecord, Database, SubsonicCredentialStatus,
+ },
+ storage::sync::{
+ MutationContext, MutationIntent, MutationReceipt, OperationClaim, SyncService,
+ },
};
#[macro_use]
@@ -24,7 +28,7 @@ pub struct DomainServices {
db: Database,
secret_box: Arc,
sync: SyncService,
- scanner: crate::scanner::ScanManager,
+ scanner: crate::library::scanner::ScanManager,
uploads: crate::config::UploadLimits,
library_event_retention: crate::config::LibraryEventRetention,
/// How long a missing track is kept before the retention pass purges it,
@@ -36,7 +40,7 @@ pub struct DomainServices {
/// The folders outside every library a stored path may lead into through
/// a link (`WAVEFLOW_LINK_ROOTS`): the quality analysis reads those files
/// as the stream does.
- link_roots: crate::link_roots::LinkRoots,
+ link_roots: crate::library::link_roots::LinkRoots,
/// Bounds a credential an account mints for itself. See the field on
/// [`crate::config::Config`] for why the administrative path is not
/// bounded by it.
@@ -95,7 +99,7 @@ pub struct DomainServices {
/// Metadata enrichment — RFC-011. Disabled at construction and placed by
/// [`Self::use_enrichment`], like the scrobbling adapters and for the same
/// reason: a test drives it against a provider of its own.
- enrichment: Arc>,
+ enrichment: Arc>,
/// How long a portrait search's task may run, in milliseconds (RFC-013):
/// [`ARTIST_IMAGE_SEARCH_DEADLINE`], except where a test shortens it
/// rather than wait five minutes.
@@ -105,7 +109,7 @@ pub struct DomainServices {
quality: quality::QualityRuntime,
/// Lyrics from LRCLIB — RFC-024. Disabled at construction and placed by
/// [`Self::use_lyrics_lookup`], like enrichment and for the same reason.
- lyrics_lookup: Arc>,
+ lyrics_lookup: Arc>,
/// Podcasts (RFC-025): the settings, and the one download permit and one
/// refresh lock the background work shares.
podcasts: podcasts::PodcastRuntime,
@@ -141,17 +145,16 @@ pub enum ServiceError {
Security(#[from] security::SecurityError),
}
-impl From for ServiceError {
- fn from(error: crate::sync::SyncError) -> Self {
+impl From for ServiceError {
+ fn from(error: crate::storage::sync::SyncError) -> Self {
match error {
- crate::sync::SyncError::Invalid => Self::Invalid,
+ crate::storage::sync::SyncError::Invalid => Self::Invalid,
// Mutations claim operations; they never read the journal, so
// CursorExpired cannot reach this conversion. Folded into Conflict
// rather than given a domain variant nothing would ever construct.
- crate::sync::SyncError::Conflict | crate::sync::SyncError::CursorExpired => {
- Self::Conflict
- }
- crate::sync::SyncError::Database(error) => Self::Database(error),
+ crate::storage::sync::SyncError::Conflict
+ | crate::storage::sync::SyncError::CursorExpired => Self::Conflict,
+ crate::storage::sync::SyncError::Database(error) => Self::Database(error),
}
}
}
@@ -305,7 +308,7 @@ impl DomainServices {
db: Database,
secret_box: Arc,
sync: SyncService,
- scanner: crate::scanner::ScanManager,
+ scanner: crate::library::scanner::ScanManager,
config: &crate::config::Config,
) -> Self {
// Copied here rather than borrowed, which is why mutating a `Config`
@@ -320,7 +323,7 @@ impl DomainServices {
library_event_retention: config.library_event_retention,
unavailable_retention: config.unavailable_retention,
purge_grace: config.purge_grace,
- link_roots: crate::link_roots::LinkRoots::new(config.link_roots.clone()),
+ link_roots: crate::library::link_roots::LinkRoots::new(config.link_roots.clone()),
api_token_ttl: config.api_token_ttl,
upload_locks: Arc::new(dashmap::DashMap::new()),
canvas: config.canvas,
@@ -347,7 +350,7 @@ impl DomainServices {
lastfm_unavailable: lastfm::lastfm_unavailability(config),
lastfm_exchanges: Arc::new(dashmap::DashMap::new()),
enrichment: Arc::new(std::sync::RwLock::new(
- crate::enrichment::Enrichment::disabled(),
+ crate::remote::enrichment::Enrichment::disabled(),
)),
image_search_deadline_ms: Arc::new(std::sync::atomic::AtomicU64::new(
u64::try_from(ARTIST_IMAGE_SEARCH_DEADLINE.as_millis()).unwrap_or(u64::MAX),
@@ -357,7 +360,7 @@ impl DomainServices {
config.request_timeout,
),
lyrics_lookup: Arc::new(std::sync::RwLock::new(
- crate::lrclib::LyricsLookup::disabled(),
+ crate::remote::lrclib::LyricsLookup::disabled(),
)),
podcasts: podcasts::PodcastRuntime::new(config.podcasts.clone()),
remote_artwork: config.remote_artwork,
@@ -367,7 +370,7 @@ impl DomainServices {
}
/// Places the enrichment engine every reader goes through.
- pub fn use_enrichment(&self, enrichment: crate::enrichment::Enrichment) {
+ pub fn use_enrichment(&self, enrichment: crate::remote::enrichment::Enrichment) {
*self
.enrichment
.write()
diff --git a/src/services/model/accounts.rs b/src/services/model/accounts.rs
index 8d392c2b..ff36243f 100644
--- a/src/services/model/accounts.rs
+++ b/src/services/model/accounts.rs
@@ -6,8 +6,8 @@ use utoipa::ToSchema;
use uuid::Uuid;
use crate::{
- database::{AccountRecord, AccountRole},
- security::EncryptedSecret,
+ auth::security::EncryptedSecret,
+ storage::database::{AccountRecord, AccountRole},
};
pub struct SubsonicCredentialRecord {
diff --git a/src/services/model/playlists.rs b/src/services/model/playlists.rs
index fe3a43a6..e06f2551 100644
--- a/src/services/model/playlists.rs
+++ b/src/services/model/playlists.rs
@@ -6,7 +6,7 @@ use utoipa::ToSchema;
use uuid::Uuid;
use super::catalog::SongItem;
-use crate::playlist_file::TextEncoding;
+use crate::library::playlist_file::TextEncoding;
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct PlaylistItem {
diff --git a/src/services/playlists/files.rs b/src/services/playlists/files.rs
index e301706a..39555716 100644
--- a/src/services/playlists/files.rs
+++ b/src/services/playlists/files.rs
@@ -5,7 +5,7 @@ use super::*;
impl DomainServices {
/// Imports an `.m3u` or `.m3u8` file as a playlist of this account's.
///
- /// The file is read by [`crate::playlist_file`]; what is left here is the
+ /// The file is read by [`crate::library::playlist_file`]; what is left here is the
/// part that needs the catalogue — turning what an entry *says* into a
/// track this account can actually reach.
///
diff --git a/src/services/podcasts/episodes.rs b/src/services/podcasts/episodes.rs
index 15bbea2c..4f44bc97 100644
--- a/src/services/podcasts/episodes.rs
+++ b/src/services/podcasts/episodes.rs
@@ -263,7 +263,7 @@ impl DomainServices {
pub async fn podcast_stream_source(
&self,
episode_id: Uuid,
- ) -> Result
]]>
- https://example.org/
-
- -
- First
- ep-1
- Mon, 01 Jan 2024 10:00:00 +0000
- 1:02:03
-
-
- -
- Second
- Tue, 02 Jan 2024 10:00:00 +0000
-
-
- - No audio
-
-"#,
- &base(),
- )
- .unwrap();
- assert_eq!(feed.title, "The Show");
- assert_eq!(feed.description.as_deref(), Some("About things"));
- assert_eq!(
- feed.image_url.as_deref(),
- Some("https://example.org/cover.jpg")
- );
- assert_eq!(feed.episodes.len(), 2);
- assert_eq!(feed.episodes[0].title, "Second");
- assert_eq!(feed.episodes[0].guid, "https://cdn.example.org/2.mp3");
- assert_eq!(
- feed.episodes[1].enclosure_url,
- "https://feeds.example.org/audio/1.mp3"
- );
- assert_eq!(feed.episodes[1].duration_ms, Some(3_723_000));
- assert_eq!(feed.episodes[1].enclosure_length, Some(1234));
- }
-
- #[test]
- fn an_atom_feed_is_read() {
- let feed = parse_feed(
- br#"
- Atom Show
-
- urn:1One2024-03-01T00:00:00Z
-
-
-"#,
- &base(),
- )
- .unwrap();
- assert_eq!(feed.title, "Atom Show");
- assert_eq!(feed.episodes[0].guid, "urn:1");
- assert_eq!(feed.episodes[0].published_at, Some(1_709_251_200_000));
- }
-
- /// The billion laughs, and its quieter relatives: any DTD is refused, so no
- /// entity is declared and nothing expands.
- #[test]
- fn a_document_type_declaration_is_refused_outright() {
- let laughs = br#"
-]>
-&lol2;"#;
- assert_eq!(parse_feed(laughs, &base()), Err(FeedError::Invalid));
- let external = br#"
-]>
-&xxe;"#;
- assert_eq!(parse_feed(external, &base()), Err(FeedError::Invalid));
- assert_eq!(parse_feed(b"not xml", &base()), Err(FeedError::Invalid));
- assert_eq!(
- parse_feed(b"