mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-06 21:55:39 -04:00
feat(deploy): add optional Spotify lyrics sidecar
This commit is contained in:
10 files changed
+180
-1
No files matched your search
@@ -84,7 +84,10 @@ jobs:
|
||||
run: dotnet test --configuration Release --no-build --verbosity normal
|
||||
|
||||
- name: Validate Compose contracts
|
||||
env:
|
||||
SPOTIFY_API_SESSION_COOKIE: ci-placeholder
|
||||
run: |
|
||||
docker compose -f docker-compose.yml config --quiet
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml config --quiet
|
||||
docker compose -f docker-compose.yml -f docker-compose.aio.yml config --quiet
|
||||
docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml config --quiet
|
||||
@@ -62,6 +62,11 @@ dashboard or as `APPLE_DOWNLOAD_URL`. The URL must be the gateway's Allstarr-com
|
||||
wrapper-v2 address. Adding or removing it does not replace the database, Valkey, application state, or media
|
||||
volumes. Follow [the Apple download provider procedure](docs/operations/apple-download-provider.md).
|
||||
|
||||
The optional Spotify lyrics service is likewise absent from Standard and AIO. Add the pinned
|
||||
`docker-compose.spotify-lyrics.yml` overlay only when needed, following
|
||||
[docs/operations/spotify-lyrics-sidecar.md](docs/operations/spotify-lyrics-sidecar.md). Its cookie stays in the host
|
||||
`.env`; the dashboard migration imports endpoint configuration but never manages Docker or exports provider secrets.
|
||||
|
||||
Custom manual deployments may select SQLite explicitly. SQLite bootstrap has an intentional one-shot confirmation requirement, and no automatic Postgres-to-SQLite fallback exists. Follow [docs/operations/storage.md](docs/operations/storage.md) instead of guessing these settings.
|
||||
|
||||
### Identity and provider-account ownership
|
||||
|
||||
@@ -54,6 +54,11 @@ separately, then give Allstarr its URL through the dashboard or `APPLE_DOWNLOAD_
|
||||
gateway API, not directly to wrapper-v2. Removing that URL disables Apple download routes without changing Postgres
|
||||
or media volumes. See [Apple download provider setup](docs/operations/apple-download-provider.md).
|
||||
|
||||
Spotify lyrics are optional too. To run the pinned private-network sidecar, add
|
||||
`docker-compose.spotify-lyrics.yml` to the Compose command and follow the
|
||||
[Spotify lyrics sidecar guide](docs/operations/spotify-lyrics-sidecar.md). Importing an old `.env` can restore the
|
||||
endpoint URL, but it cannot start the sidecar or pass a cookie to it.
|
||||
|
||||
Client traffic uses `http://localhost:5274`. The separate dashboard is on `http://localhost:5275`. Standard Compose publishes the dashboard on host loopback and only trusts the container gateway needed to cross that mapping. LAN or reverse-proxy access requires `ADMIN_BIND_ADDRESS=0.0.0.0`, `ADMIN_BIND_ANY_IP=true`, and an explicit `ADMIN_TRUSTED_SUBNETS` CIDR. Please keep it behind a private network, VPN, or authenticated access proxy. This software has meaningful access to your media server and provider accounts.
|
||||
|
||||
The complete install, backup, restore, and rollback instructions live in [the storage runbook](docs/operations/storage.md). Configuration keys are explained in [CONFIGURATION.md](CONFIGURATION.md).
|
||||
|
||||
@@ -88,6 +88,20 @@ public sealed class ComposeContractTests
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpotifyLyricsOverlay_IsOptionalPinnedAndPrivate()
|
||||
{
|
||||
RenderCompose("docker-compose.yml", "docker-compose.spotify-lyrics.yml");
|
||||
var overlay = File.ReadAllText(Path.Combine(_repositoryRoot, "docker-compose.spotify-lyrics.yml"));
|
||||
|
||||
Assert.Contains("akashrchandran/spotify-lyrics-api@sha256:", overlay, StringComparison.Ordinal);
|
||||
Assert.Contains("SP_DC: ${SPOTIFY_API_SESSION_COOKIE:-}", overlay, StringComparison.Ordinal);
|
||||
Assert.Contains("SpotifyApi__LyricsApiUrl:", overlay, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ports:", overlay, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(":latest", overlay, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("/var/run/docker.sock", overlay, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeImage_ContainsBackupToolsAndPinnedDotnetBases()
|
||||
{
|
||||
@@ -126,6 +140,10 @@ public sealed class ComposeContractTests
|
||||
"docker compose -f docker-compose.yml -f docker-compose.aio.yml config --quiet",
|
||||
workflow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml config --quiet",
|
||||
workflow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("continue-on-error: true", workflow, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,24 @@ public sealed class LegacyEnvParserTests
|
||||
Assert.DoesNotContain("deezer-secret", document.SourceSha256, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ImportsOptionalProviderEndpointsAsDurableSettings()
|
||||
{
|
||||
var document = Parse("""
|
||||
SPOTIFY_LYRICS_API_URL=http://spotify-lyrics:8080
|
||||
APPLE_DOWNLOAD_URL=http://apple-gateway:8000
|
||||
""");
|
||||
|
||||
AssertEntry(document, "SPOTIFY_LYRICS_API_URL", LegacyEnvDisposition.DurableSetting,
|
||||
"import_if_absent", false);
|
||||
AssertEntry(document, "APPLE_DOWNLOAD_URL", LegacyEnvDisposition.DurableSetting,
|
||||
"import_if_absent", false);
|
||||
Assert.Equal("SpotifyApi:LyricsApiUrl",
|
||||
document.Entries.Single(item => item.Key == "SPOTIFY_LYRICS_API_URL").DurableKey);
|
||||
Assert.Equal("AppleDownload:BaseUrl",
|
||||
document.Entries.Single(item => item.Key == "APPLE_DOWNLOAD_URL").DurableKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_RejectsIncompleteProviderBundlesAndIgnoresEmptyValues()
|
||||
{
|
||||
|
||||
@@ -122,6 +122,23 @@ public sealed class WebUiEnvMigrationContractTests
|
||||
Assert.Contains("No container restart is required for this migration", _script, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionalRuntimeRows_ShowRedactedPostImportDeploymentGuidance()
|
||||
{
|
||||
Assert.Contains("migrationOptionalRuntimeServices()", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("SPOTIFY_LYRICS_API_URL", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("APPLE_DOWNLOAD_URL", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("APPLE_MUSIC_AIO_URL", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("Optional services still need setup", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("No URL, login, token, or session value is shown here.", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("docs/operations/spotify-lyrics-sidecar.md", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("docs/operations/apple-download-provider.md", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("docker compose -f docker-compose.yml", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("docker-compose.spotify-lyrics.yml up -d", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("target=\"_blank\" rel=\"noopener noreferrer\"", _script, StringComparison.Ordinal);
|
||||
Assert.Contains("this.renderMigrationOptionalRuntimeGuidance()", _script, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Preview_RedactsSecretsAndGroupsChangesWithWarnings()
|
||||
{
|
||||
|
||||
@@ -66,6 +66,7 @@ public static class LegacyEnvParser
|
||||
["SPOTIFY_API_CACHE_DURATION_MINUTES"] = "SpotifyApi:CacheDurationMinutes",
|
||||
["SPOTIFY_API_RATE_LIMIT_DELAY_MS"] = "SpotifyApi:RateLimitDelayMs",
|
||||
["SPOTIFY_API_PREFER_ISRC_MATCHING"] = "SpotifyApi:PreferIsrcMatching",
|
||||
["SPOTIFY_LYRICS_API_URL"] = "SpotifyApi:LyricsApiUrl",
|
||||
["SCROBBLING_ENABLED"] = "Scrobbling:Enabled",
|
||||
["SCROBBLING_LOCAL_TRACKS_ENABLED"] = "Scrobbling:LocalTracksEnabled",
|
||||
["SCROBBLING_SYNTHETIC_LOCAL_PLAYED_SIGNAL_ENABLED"] = "Scrobbling:SyntheticLocalPlayedSignalEnabled",
|
||||
@@ -150,7 +151,6 @@ public static class LegacyEnvParser
|
||||
"ALLSTARR_ALLOW_GLOBAL_ACCOUNTS", "ALLSTARR_ALLOW_GLOBAL_PERSONAL_ACCOUNTS",
|
||||
"ALLSTARR_SHARED_DOWNLOADER_ACCOUNT_ID", "LIBRARY_DOWNLOAD_PATH", "LIBRARY_KEPT_PATH",
|
||||
"DOWNLOAD_PATH", "KEPT_PATH", "CACHE_PATH", "REDIS_ENABLED", "REDIS_CONNECTION_STRING",
|
||||
"APPLE_DOWNLOAD_URL", "APPLE_MUSIC_AIO_URL", "SPOTIFY_LYRICS_API_URL",
|
||||
"DEBUG_LOG_ALL_REQUESTS", "DEBUG_REDACT_SENSITIVE_REQUEST_VALUES", "MUSICBRAINZ_USERNAME",
|
||||
"MUSICBRAINZ_PASSWORD"
|
||||
};
|
||||
|
||||
@@ -3681,6 +3681,48 @@ class AllstarrApp extends LitElement {
|
||||
["deployment", "deployment_only", "deployment_checklist"].includes(section.id));
|
||||
}
|
||||
|
||||
migrationOptionalRuntimeServices() {
|
||||
const preview = this.envMigration.preview || {};
|
||||
const keys = new Set(asArray(preview.items || preview.Items)
|
||||
.map((item) => String(item.key || item.Key || "").toUpperCase()));
|
||||
const services = [];
|
||||
if (keys.has("SPOTIFY_LYRICS_API_URL")) {
|
||||
services.push({
|
||||
id: "spotify-lyrics",
|
||||
title: "Spotify lyrics sidecar",
|
||||
text: "The endpoint URL is imported as a runtime setting, but the WebUI does not start containers or give the sidecar your Spotify cookie. Add the optional Compose overlay on the host.",
|
||||
guide: "https://github.com/SoPat712/allstarr/blob/dev/docs/operations/spotify-lyrics-sidecar.md",
|
||||
command: "docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml config --quiet\ndocker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml up -d\ndocker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml ps",
|
||||
});
|
||||
}
|
||||
if (["APPLE_DOWNLOAD_URL", "APPLE_MUSIC_AIO_URL", "APPLE_DOWNLOAD_QUALITY", "APPLE_MUSIC_QUALITY"]
|
||||
.some((key) => keys.has(key))) {
|
||||
services.push({
|
||||
id: "apple-download",
|
||||
title: "Apple download gateway",
|
||||
text: "The endpoint URL is imported as a runtime setting. Deploy a compatible gateway separately and verify it under Sources > Apple download. Do not point Allstarr directly at wrapper-v2.",
|
||||
guide: "https://github.com/SoPat712/allstarr/blob/dev/docs/operations/apple-download-provider.md",
|
||||
command: "docker compose ps\n# Then open Sources > Apple download and verify the gateway",
|
||||
});
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
renderMigrationOptionalRuntimeGuidance() {
|
||||
const services = this.migrationOptionalRuntimeServices();
|
||||
if (!services.length) return nothing;
|
||||
return html`<aside class="callout warning" aria-label="Optional service setup required">
|
||||
<h4>Optional services still need setup</h4>
|
||||
<p>These endpoints stay outside Allstarr. No URL, login, token, or session value is shown here.</p>
|
||||
${services.map((service) => html`<section>
|
||||
<strong>${service.title}</strong>
|
||||
<p>${service.text}</p>
|
||||
<p><a href=${service.guide} target="_blank" rel="noopener noreferrer">Open the setup guide</a></p>
|
||||
<pre><code>${service.command}</code></pre>
|
||||
</section>`)}
|
||||
</aside>`;
|
||||
}
|
||||
|
||||
migrationEntryIsSensitive(entry) {
|
||||
if (entry.sensitive ?? entry.Sensitive ?? entry.isSecret ?? entry.IsSecret) return true;
|
||||
const key = String(entry.key || entry.Key || entry.sourceKey || entry.SourceKey || "");
|
||||
@@ -3788,6 +3830,7 @@ class AllstarrApp extends LitElement {
|
||||
</section>`;
|
||||
}) : html`<div class="empty">No supported legacy settings were found. Nothing will be changed.</div>`}
|
||||
<div class="callout"><strong>What confirmation means</strong><p>Only rows marked for durable import are applied automatically. Disabled shared accounts remain disabled, users reconnect personal accounts themselves, deployment-only values stay on the host checklist, and playlists requiring a target or owner remain handoffs.</p></div>
|
||||
${this.renderMigrationOptionalRuntimeGuidance()}
|
||||
<form class="env-migration-confirm" @submit=${(event) => this.applyEnvMigration(event)}>
|
||||
<label class="inline-check">
|
||||
<input name="confirmMigration" type="checkbox" required ?disabled=${migration.state === "applying"}>
|
||||
@@ -3808,6 +3851,7 @@ class AllstarrApp extends LitElement {
|
||||
<p>Imported durable settings are active immediately. They do not require an Allstarr restart.</p>
|
||||
${hasDeploymentChecklist ? html`<p>Deployment-owned values were not copied to the server. Review the deployment checklist, update Compose or the host <code>.env</code>, then recreate the Allstarr container to apply those separate changes.</p>` : html`<p>No container restart is required for this migration.</p>`}
|
||||
</div>
|
||||
${this.renderMigrationOptionalRuntimeGuidance()}
|
||||
<dl><div><dt>Durable settings</dt><dd>${this.migrationResultCount(result.settingsImported ?? result.SettingsImported ?? result.importedSettings ?? result.ImportedSettings)}</dd></div><div><dt>Disabled accounts created</dt><dd>${this.migrationResultCount(result.providerAccountsCreated ?? result.ProviderAccountsCreated)}</dd></div><div><dt>Skipped</dt><dd>${this.migrationResultCount(result.settingsSkipped ?? result.SettingsSkipped) + this.migrationResultCount(result.providerAccountsSkipped ?? result.ProviderAccountsSkipped)}</dd></div><div><dt>Manual checklist</dt><dd>${this.migrationResultCount(result.manualChecklistItems ?? result.ManualChecklistItems)}</dd></div><div><dt>Playlist handoffs</dt><dd>${this.migrationResultCount(result.playlistHandoffsPending ?? result.PlaylistHandoffsPending)}</dd></div></dl>
|
||||
${resultSections.map((section) => html`<section class="env-migration-result-section" aria-labelledby=${`migration-result-${section.id}`}>
|
||||
<h5 id=${`migration-result-${section.id}`}>${section.label}</h5>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Optional Spotify lyrics sidecar.
|
||||
# Usage: docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml up -d
|
||||
|
||||
services:
|
||||
spotify-lyrics:
|
||||
image: akashrchandran/spotify-lyrics-api@sha256:c95749ad939ba136893f16604e90e7f3f070beb2dea3aaa955646ec08330b4c6
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SP_DC: ${SPOTIFY_API_SESSION_COOKIE:-}
|
||||
networks:
|
||||
- allstarr
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
allstarr:
|
||||
environment:
|
||||
SpotifyApi__LyricsApiUrl: ${SPOTIFY_LYRICS_API_URL:-http://spotify-lyrics:8080}
|
||||
@@ -0,0 +1,51 @@
|
||||
# Spotify lyrics sidecar
|
||||
|
||||
Spotify lyrics are optional. The normal and AIO installs stay healthy without this service. Add it only when you
|
||||
have a Spotify web session cookie and want Spotify to participate in the lyrics route.
|
||||
|
||||
The sidecar is pinned by digest, stays on Allstarr's private Docker network, and does not publish a host port. Its
|
||||
upstream image currently runs on `linux/amd64`; ARM hosts need Docker's emulation support.
|
||||
|
||||
## Add it
|
||||
|
||||
Keep the cookie in the stack's host `.env`:
|
||||
|
||||
```dotenv
|
||||
SPOTIFY_API_SESSION_COOKIE=replace-with-your-sp-dc-cookie
|
||||
SPOTIFY_LYRICS_API_URL=http://spotify-lyrics:8080
|
||||
```
|
||||
|
||||
Then validate and apply the optional overlay:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml config --quiet
|
||||
docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml up -d
|
||||
docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml ps
|
||||
```
|
||||
|
||||
This does not replace Postgres, Valkey, application state, downloads, or kept media. Compose may recreate the
|
||||
Allstarr container to attach the endpoint setting, but it reuses the same volumes.
|
||||
|
||||
If a legacy `.env` import contains `SPOTIFY_LYRICS_API_URL`, Allstarr imports that URL as a durable runtime setting.
|
||||
The WebUI never starts containers or copies the Spotify cookie into a sidecar. The administrator must still add the
|
||||
overlay so Docker creates the service and supplies its cookie.
|
||||
|
||||
## Update or remove it
|
||||
|
||||
Normal updates use the same overlay:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml pull
|
||||
docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml up -d
|
||||
```
|
||||
|
||||
To remove only the optional service:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.spotify-lyrics.yml rm -s -f spotify-lyrics
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
Remove or clear the Spotify lyrics URL in Sources if you do not want Allstarr to probe the absent endpoint. Removing
|
||||
the sidecar never removes music or durable Allstarr data.
|
||||
|
||||
Reference in new issue
Block a user