mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-08 14:05:02 -04:00
fix(scrobbling): require own Last.fm API keys in env and compose
This commit is contained in:
9 files changed
+139
-50
No files matched your search
+3
-3
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
Reference in new issue
Block a user