fix(scrobbling): require own Last.fm API keys in env and compose

This commit is contained in:
joshpatra committed 2026-05-22 17:22:49 -04:00
1 parent 6b421f44e9
commit ffd91f742b
9 files changed
+139 -50

No files matched your search

+3 -3
View File
@@ -285,9 +285,9 @@ SCROBBLING_SYNTHETIC_LOCAL_PLAYED_SIGNAL_ENABLED=false
# Enable Last.fm scrobbling (default: false)
SCROBBLING_LASTFM_ENABLED=false
# Last.fm API credentials (OPTIONAL - uses hardcoded credentials by default)
# Only set these if you want to use your own API account
# Get from: https://www.last.fm/api/account/create
# Last.fm API credentials (REQUIRED when SCROBBLING_LASTFM_ENABLED=true)
# The old shared Jellyfin plugin key is suspended by Last.fm — you must use your own app.
# Create at: https://www.last.fm/api/account/create
SCROBBLING_LASTFM_API_KEY=
SCROBBLING_LASTFM_SHARED_SECRET=
+20 -8
View File
@@ -225,6 +225,8 @@ Track your listening history to Last.fm and/or ListenBrainz. Allstarr automatica
| `Scrobbling:Enabled` | Enable scrobbling globally (default: `false`) |
| `Scrobbling:LocalTracksEnabled` | Enable scrobbling for local library tracks (default: `false`) - See note below |
| `Scrobbling:LastFm:Enabled` | Enable Last.fm scrobbling (default: `false`) |
| `Scrobbling:LastFm:ApiKey` | Your Last.fm API key (required when enabled) — [create an app](https://www.last.fm/api/account/create) |
| `Scrobbling:LastFm:SharedSecret` | Your Last.fm shared secret (required when enabled) |
| `Scrobbling:LastFm:Username` | Your Last.fm username |
| `Scrobbling:LastFm:Password` | Your Last.fm password (only used for authentication) |
| `Scrobbling:LastFm:SessionKey` | Last.fm session key (auto-generated via Web UI) |
@@ -242,11 +244,13 @@ SCROBBLING_ENABLED=true
# - ListenBrainz: https://github.com/lyarenei/jellyfin-plugin-listenbrainz
SCROBBLING_LOCAL_TRACKS_ENABLED=false
# Last.fm configuration
# Last.fm configuration (API key + secret required — shared Jellyfin plugin key is suspended)
SCROBBLING_LASTFM_ENABLED=true
SCROBBLING_LASTFM_API_KEY=your-api-key
SCROBBLING_LASTFM_SHARED_SECRET=your-shared-secret
SCROBBLING_LASTFM_USERNAME=your-username
SCROBBLING_LASTFM_PASSWORD=your-password
# Session key is auto-generated via Web UI
# Session key is auto-generated via Web UI after you set API credentials
# ListenBrainz configuration
SCROBBLING_LISTENBRAINZ_ENABLED=true
@@ -258,12 +262,14 @@ SCROBBLING_LISTENBRAINZ_USER_TOKEN=your-token-here
The easiest way to configure scrobbling is through the Web UI at `http://localhost:5275`:
**Last.fm Setup:**
1. Navigate to the **Scrobbling** tab
2. Toggle "Last.fm Enabled" to enable
3. Click "Edit" next to Username and enter your Last.fm username
4. Click "Edit" next to Password and enter your Last.fm password
5. Click "Authenticate & Save" to generate a session key
6. Restart the container for changes to take effect
1. Create a Last.fm API account at https://www.last.fm/api/account/create and note the API key and shared secret
2. Set `SCROBBLING_LASTFM_API_KEY` and `SCROBBLING_LASTFM_SHARED_SECRET` in your `.env` file
3. Navigate to the **Scrobbling** tab
4. Toggle "Last.fm Enabled" to enable
5. Click "Edit" next to Username and enter your Last.fm username
6. Click "Edit" next to Password and enter your Last.fm password
7. Click "Authenticate & Save" to generate a session key (must be done again if you change API key)
8. Restart the container for changes to take effect
**ListenBrainz Setup:**
1. Get your user token from [ListenBrainz Settings](https://listenbrainz.org/settings/)
@@ -288,8 +294,14 @@ The easiest way to configure scrobbling is through the Web UI at `http://localho
#### Troubleshooting
**Last.fm "API Key Suspended" or "not allowed to make requests":**
- Last.fm suspended the old shared Jellyfin plugin API key that Allstarr used by default
- Create your own application at https://www.last.fm/api/account/create
- Set `SCROBBLING_LASTFM_API_KEY` and `SCROBBLING_LASTFM_SHARED_SECRET` in `.env`, restart, then authenticate again in Admin → Scrobbling
**Last.fm authentication fails:**
- Verify your username and password are correct
- Ensure API key and shared secret are set (not empty)
- Check that there are no extra spaces in your credentials
- Try re-authenticating via the Web UI
+1 -1
View File
@@ -9,5 +9,5 @@ public static class AppVersion
/// <summary>
/// Current application version.
/// </summary>
public const string Version = "2.0.0";
public const string Version = "5.0.1";
}
@@ -59,8 +59,9 @@ public class ScrobblingAdminController : ControllerBase
HasApiKey = hasApiCredentials,
HasSessionKey = !string.IsNullOrEmpty(_settings.LastFm.SessionKey),
Username = _settings.LastFm.Username,
UsingHardcodedCredentials = hasApiCredentials &&
_settings.LastFm.ApiKey == LastFmSettings.DefaultApiKey
UsingHardcodedCredentials = LastFmSettings.IsLegacyJellyfinPluginApiKey(_settings.LastFm.ApiKey),
RequiresOwnApiAccount = hasApiCredentials &&
LastFmSettings.IsLegacyJellyfinPluginApiKey(_settings.LastFm.ApiKey)
},
ListenBrainz = new
{
@@ -73,7 +74,7 @@ public class ScrobblingAdminController : ControllerBase
/// <summary>
/// Authenticate with Last.fm using credentials from .env file.
/// Uses hardcoded API credentials from Jellyfin Last.fm plugin for convenience.
/// Requires your own Last.fm API application credentials in .env.
/// </summary>
[HttpPost("lastfm/authenticate")]
public async Task<IActionResult> AuthenticateLastFm()
@@ -87,10 +88,23 @@ public class ScrobblingAdminController : ControllerBase
return BadRequest(new { error = "Username and password must be set in .env file (SCROBBLING_LASTFM_USERNAME and SCROBBLING_LASTFM_PASSWORD)" });
}
// Check if API credentials are available
if (LastFmSettings.IsLegacyJellyfinPluginApiKey(_settings.LastFm.ApiKey))
{
return BadRequest(new
{
error = "The built-in Jellyfin Last.fm API key is suspended by Last.fm. " +
"Create your own application at https://www.last.fm/api/account/create, " +
"set SCROBBLING_LASTFM_API_KEY and SCROBBLING_LASTFM_SHARED_SECRET, then authenticate again."
});
}
if (string.IsNullOrEmpty(_settings.LastFm.ApiKey) || string.IsNullOrEmpty(_settings.LastFm.SharedSecret))
{
return BadRequest(new { error = "Last.fm API credentials not configured. This should not happen - please report this bug." });
return BadRequest(new
{
error = "Last.fm API credentials are required. Create an application at https://www.last.fm/api/account/create " +
"and set SCROBBLING_LASTFM_API_KEY and SCROBBLING_LASTFM_SHARED_SECRET in your .env file."
});
}
try
+20 -14
View File
@@ -40,13 +40,22 @@ public class ScrobblingSettings
/// </summary>
public class LastFmSettings
{
// These defaults match the Jellyfin Last.fm plugin credentials.
// Stored base64-encoded to avoid plain-text source exposure.
private const string DefaultApiKeyBase64 = "Y2IzYmRjZDQxNWZjYjQwY2Q1NzJiMTM3YjJiMjU1ZjU=";
private const string DefaultSharedSecretBase64 = "M2EwOGY5ZmFkNmRkYzRjMzViMGRjZTAwNjJjZWNiNWU=";
// Legacy Jellyfin Last.fm plugin credentials (suspended by Last.fm — do not use).
private const string LegacyJellyfinPluginApiKeyBase64 = "Y2IzYmRjZDQxNWZjYjQwY2Q1NzJiMTM3YjJiMjU1ZjU=";
private const string LegacyJellyfinPluginSharedSecretBase64 = "M2EwOGY5ZmFkNmRkYzRjMzViMGRjZTAwNjJjZWNiNWU=";
public static string DefaultApiKey => DecodeBase64(DefaultApiKeyBase64);
public static string DefaultSharedSecret => DecodeBase64(DefaultSharedSecretBase64);
public static string LegacyJellyfinPluginApiKey => DecodeBase64(LegacyJellyfinPluginApiKeyBase64);
public static string LegacyJellyfinPluginSharedSecret => DecodeBase64(LegacyJellyfinPluginSharedSecretBase64);
[Obsolete("Use LegacyJellyfinPluginApiKey. The shared Jellyfin plugin key is suspended by Last.fm.")]
public static string DefaultApiKey => LegacyJellyfinPluginApiKey;
[Obsolete("Use LegacyJellyfinPluginSharedSecret.")]
public static string DefaultSharedSecret => LegacyJellyfinPluginSharedSecret;
public static bool IsLegacyJellyfinPluginApiKey(string? apiKey) =>
!string.IsNullOrEmpty(apiKey) &&
string.Equals(apiKey, LegacyJellyfinPluginApiKey, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether Last.fm scrobbling is enabled.
@@ -54,18 +63,15 @@ public class LastFmSettings
public bool Enabled { get; set; }
/// <summary>
/// Last.fm API key (32-character hex string).
/// Uses hardcoded credentials from Jellyfin Last.fm plugin for convenience.
/// Users can override by setting SCROBBLING_LASTFM_API_KEY in .env
/// Last.fm API key (32-character hex string). Required when Last.fm is enabled.
/// Create an application at https://www.last.fm/api/account/create
/// </summary>
public string ApiKey { get; set; } = DefaultApiKey;
public string ApiKey { get; set; } = string.Empty;
/// <summary>
/// Last.fm shared secret (32-character hex string).
/// Uses hardcoded credentials from Jellyfin Last.fm plugin for convenience.
/// Users can override by setting SCROBBLING_LASTFM_SHARED_SECRET in .env
/// Last.fm shared secret (32-character hex string). Required when Last.fm is enabled.
/// </summary>
public string SharedSecret { get; set; } = DefaultSharedSecret;
public string SharedSecret { get; set; } = string.Empty;
/// <summary>
/// Last.fm session key (obtained via Mobile Authentication).
@@ -22,9 +22,10 @@ public class LastFmScrobblingService : IScrobblingService
private readonly ILogger<LastFmScrobblingService> _logger;
public string ServiceName => "Last.fm";
public bool IsEnabled => _settings.Enabled &&
!string.IsNullOrEmpty(_settings.ApiKey) &&
!string.IsNullOrEmpty(_settings.SharedSecret) &&
public bool IsEnabled => _settings.Enabled &&
!string.IsNullOrEmpty(_settings.ApiKey) &&
!string.IsNullOrEmpty(_settings.SharedSecret) &&
!LastFmSettings.IsLegacyJellyfinPluginApiKey(_settings.ApiKey) &&
!string.IsNullOrEmpty(_settings.SessionKey);
public LastFmScrobblingService(
@@ -37,10 +38,31 @@ public class LastFmScrobblingService : IScrobblingService
_httpClient = httpClientFactory.CreateClient("LastFm");
_logger = logger;
if (IsEnabled)
if (_settings.Enabled)
{
_logger.LogInformation("🎵 Last.fm scrobbling enabled for user: {Username}",
_settings.Username ?? "Unknown");
if (LastFmSettings.IsLegacyJellyfinPluginApiKey(_settings.ApiKey))
{
_logger.LogError(
"Last.fm is enabled but uses the suspended shared Jellyfin plugin API key. " +
"Register your own app at https://www.last.fm/api/account/create, set " +
"SCROBBLING_LASTFM_API_KEY and SCROBBLING_LASTFM_SHARED_SECRET, then re-authenticate in Admin → Scrobbling.");
}
else if (string.IsNullOrEmpty(_settings.ApiKey) || string.IsNullOrEmpty(_settings.SharedSecret))
{
_logger.LogError(
"Last.fm is enabled but API credentials are missing. Set SCROBBLING_LASTFM_API_KEY and " +
"SCROBBLING_LASTFM_SHARED_SECRET from https://www.last.fm/api/account/create");
}
else if (string.IsNullOrEmpty(_settings.SessionKey))
{
_logger.LogWarning(
"Last.fm API credentials are set but SCROBBLING_LASTFM_SESSION_KEY is missing — authenticate in Admin → Scrobbling");
}
else
{
_logger.LogInformation("🎵 Last.fm scrobbling enabled for user: {Username}",
_settings.Username ?? "Unknown");
}
}
}
@@ -340,7 +362,12 @@ public class LastFmScrobblingService : IScrobblingService
{
_logger.LogError("❌ Last.fm session key is invalid - please re-authenticate");
}
if (IsSuspendedApiKeyError(errorMessage))
{
LogSuspendedApiKeyGuidance();
}
return ScrobbleResult.CreateError(errorMessage, errorCode, shouldRetry);
}
@@ -387,7 +414,12 @@ public class LastFmScrobblingService : IScrobblingService
var errorCode = int.Parse(errorElement?.Attribute("code")?.Value ?? "0");
var errorMessage = errorElement?.Value ?? "Unknown error";
var shouldRetry = errorCode == 11 || errorCode == 16;
if (IsSuspendedApiKeyError(errorMessage))
{
LogSuspendedApiKeyGuidance();
}
return Enumerable.Repeat(ScrobbleResult.CreateError(errorMessage, errorCode, shouldRetry), expectedCount).ToList();
}
@@ -453,6 +485,18 @@ public class LastFmScrobblingService : IScrobblingService
return result;
}
private static bool IsSuspendedApiKeyError(string errorMessage) =>
errorMessage.Contains("suspended", StringComparison.OrdinalIgnoreCase) ||
errorMessage.Contains("not allowed to make requests", StringComparison.OrdinalIgnoreCase);
private void LogSuspendedApiKeyGuidance()
{
_logger.LogError(
"Last.fm rejected requests because the API key is suspended. Register your own app at " +
"https://www.last.fm/api/account/create, set SCROBBLING_LASTFM_API_KEY and " +
"SCROBBLING_LASTFM_SHARED_SECRET, restart Allstarr, then re-authenticate in Admin → Scrobbling.");
}
#endregion
}
+21 -10
View File
@@ -58,7 +58,10 @@ function parseBoolean(value) {
async function loadScrobblingConfig() {
try {
const data = await API.fetchConfig();
const [data, status] = await Promise.all([
API.fetchConfig(),
API.fetchScrobblingStatus(),
]);
document.getElementById("scrobbling-enabled-value").textContent = data
.scrobbling.enabled
@@ -118,10 +121,18 @@ async function loadScrobblingConfig() {
const hasSessionKey =
sessionKey && sessionKey !== "(not set)" && sessionKey.length > 0;
let status = "";
if (data.scrobbling.lastFm.enabled && hasSessionKey) {
status =
const lastFmStatus = status?.lastFm;
const usingLegacyKey = lastFmStatus?.usingHardcodedCredentials === true;
let statusHtml = "";
if (usingLegacyKey) {
statusHtml =
'<span style="color: var(--error);">✗ Suspended API key — set SCROBBLING_LASTFM_API_KEY and SCROBBLING_LASTFM_SHARED_SECRET in .env</span>';
} else if (data.scrobbling.lastFm.enabled && hasApiKey && hasSecret && hasSessionKey) {
statusHtml =
'<span style="color: var(--success);">✓ Configured & Enabled</span>';
} else if (data.scrobbling.lastFm.enabled && hasSessionKey && (!hasApiKey || !hasSecret)) {
statusHtml =
'<span style="color: var(--error);">✗ Missing API key/secret in .env — add SCROBBLING_LASTFM_API_KEY and SCROBBLING_LASTFM_SHARED_SECRET</span>';
} else if (
hasApiKey &&
hasSecret &&
@@ -129,18 +140,18 @@ async function loadScrobblingConfig() {
hasPassword &&
!hasSessionKey
) {
status =
statusHtml =
'<span style="color: var(--warning);">⚠️ Ready to Authenticate</span>';
} else if (hasApiKey && hasSecret && (!hasUsername || !hasPassword)) {
status =
statusHtml =
'<span style="color: var(--warning);">⚠️ Needs Username & Password</span>';
} else if (!hasApiKey || !hasSecret) {
status =
'<span style="color: var(--success);">✓ Using hardcoded credentials</span>';
statusHtml =
'<span style="color: var(--warning);">⚠️ Set SCROBBLING_LASTFM_API_KEY and SCROBBLING_LASTFM_SHARED_SECRET in .env</span>';
} else {
status = '<span style="color: var(--muted);">○ Not Configured</span>';
statusHtml = '<span style="color: var(--muted);">○ Not Configured</span>';
}
document.getElementById("lastfm-status-value").innerHTML = status;
document.getElementById("lastfm-status-value").innerHTML = statusHtml;
document.getElementById("listenbrainz-enabled-value").textContent = data
.scrobbling.listenBrainz.enabled
+1
View File
@@ -207,6 +207,7 @@ services:
- Scrobbling__LocalTracksEnabled=${SCROBBLING_LOCAL_TRACKS_ENABLED:-false}
- Scrobbling__SyntheticLocalPlayedSignalEnabled=${SCROBBLING_SYNTHETIC_LOCAL_PLAYED_SIGNAL_ENABLED:-false}
- Scrobbling__LastFm__Enabled=${SCROBBLING_LASTFM_ENABLED:-false}
# Required when Last.fm is enabled — create at https://www.last.fm/api/account/create
- Scrobbling__LastFm__ApiKey=${SCROBBLING_LASTFM_API_KEY:-}
- Scrobbling__LastFm__SharedSecret=${SCROBBLING_LASTFM_SHARED_SECRET:-}
- Scrobbling__LastFm__SessionKey=${SCROBBLING_LASTFM_SESSION_KEY:-}
+1
View File
@@ -140,6 +140,7 @@ services:
- Scrobbling__LocalTracksEnabled=${SCROBBLING_LOCAL_TRACKS_ENABLED:-false}
- Scrobbling__SyntheticLocalPlayedSignalEnabled=${SCROBBLING_SYNTHETIC_LOCAL_PLAYED_SIGNAL_ENABLED:-false}
- Scrobbling__LastFm__Enabled=${SCROBBLING_LASTFM_ENABLED:-false}
# Required when Last.fm is enabled — create at https://www.last.fm/api/account/create
- Scrobbling__LastFm__ApiKey=${SCROBBLING_LASTFM_API_KEY:-}
- Scrobbling__LastFm__SharedSecret=${SCROBBLING_LASTFM_SHARED_SECRET:-}
- Scrobbling__LastFm__SessionKey=${SCROBBLING_LASTFM_SESSION_KEY:-}