mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Fix: Add UserId parameter for Jellyfin playlist operations + UI improvements
- CRITICAL FIX: Add UserId parameter to all Jellyfin playlist item fetches (fixes 400 BadRequest errors) - Fix GetPlaylists to correctly count local/missing tracks - Fix GetSpotifyPlaylistTracksOrderedAsync to find local tracks (was serving external tracks for everything) - Fix SpotifyTrackMatchingService to skip tracks already in Jellyfin - Add detailed debug logging for track matching (LOCAL by ISRC/Spotify ID, EXTERNAL match, NO MATCH) - Add 'Match Tracks' button for individual playlists (not all playlists) - Add 'Match All Tracks' button for matching all playlists at once - Add JELLYFIN_USER_ID to web UI configuration tab for easy setup - Add /api/admin/playlists/match-all endpoint This fixes the issue where local tracks weren't being used - the system was downloading from SquidWTF even when files existed locally in Jellyfin.
This commit is contained in:
@@ -201,7 +201,35 @@ public class AdminController : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var url = $"{_jellyfinSettings.Url}/Playlists/{config.JellyfinId}/Items?Fields=Path";
|
// Jellyfin requires UserId parameter to fetch playlist items
|
||||||
|
var userId = _jellyfinSettings.UserId;
|
||||||
|
|
||||||
|
// If no user configured, try to get the first user
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
var usersResponse = await _jellyfinHttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{_jellyfinSettings.Url}/Users")
|
||||||
|
{
|
||||||
|
Headers = { { "X-Emby-Authorization", GetJellyfinAuthHeader() } }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (usersResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var usersJson = await usersResponse.Content.ReadAsStringAsync();
|
||||||
|
using var usersDoc = JsonDocument.Parse(usersJson);
|
||||||
|
if (usersDoc.RootElement.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
userId = usersDoc.RootElement[0].GetProperty("Id").GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No user ID available to fetch playlist items for {Name}", config.Name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var url = $"{_jellyfinSettings.Url}/Playlists/{config.JellyfinId}/Items?UserId={userId}&Fields=Path";
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
|
request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
|
||||||
|
|
||||||
@@ -232,6 +260,7 @@ public class AdminController : ControllerBase
|
|||||||
config.Name, response.StatusCode);
|
config.Name, response.StatusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to get Jellyfin playlist tracks for {Name}", config.Name);
|
_logger.LogWarning(ex, "Failed to get Jellyfin playlist tracks for {Name}", config.Name);
|
||||||
@@ -302,7 +331,7 @@ public class AdminController : ControllerBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _matchingService.TriggerMatchingAsync();
|
await _matchingService.TriggerMatchingForPlaylistAsync(decodedName);
|
||||||
return Ok(new { message = $"Track matching triggered for {decodedName}", timestamp = DateTime.UtcNow });
|
return Ok(new { message = $"Track matching triggered for {decodedName}", timestamp = DateTime.UtcNow });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -312,6 +341,31 @@ public class AdminController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger track matching for all playlists
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("playlists/match-all")]
|
||||||
|
public async Task<IActionResult> MatchAllPlaylistTracks()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Manual track matching triggered for all playlists");
|
||||||
|
|
||||||
|
if (_matchingService == null)
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "Track matching service is not available" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _matchingService.TriggerMatchingAsync();
|
||||||
|
return Ok(new { message = "Track matching triggered for all playlists", timestamp = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to trigger track matching for all playlists");
|
||||||
|
return StatusCode(500, new { error = "Failed to trigger track matching", details = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get current configuration (safe values only)
|
/// Get current configuration (safe values only)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -346,6 +400,7 @@ public class AdminController : ControllerBase
|
|||||||
{
|
{
|
||||||
url = _jellyfinSettings.Url,
|
url = _jellyfinSettings.Url,
|
||||||
apiKey = MaskValue(_jellyfinSettings.ApiKey),
|
apiKey = MaskValue(_jellyfinSettings.ApiKey),
|
||||||
|
userId = _jellyfinSettings.UserId ?? "(not set)",
|
||||||
libraryId = _jellyfinSettings.LibraryId
|
libraryId = _jellyfinSettings.LibraryId
|
||||||
},
|
},
|
||||||
deezer = new
|
deezer = new
|
||||||
|
|||||||
@@ -2856,8 +2856,21 @@ public class JellyfinController : ControllerBase
|
|||||||
orderedTracks.Count, spotifyPlaylistName);
|
orderedTracks.Count, spotifyPlaylistName);
|
||||||
|
|
||||||
// Get existing Jellyfin playlist items (tracks the Spotify Import plugin already found)
|
// Get existing Jellyfin playlist items (tracks the Spotify Import plugin already found)
|
||||||
|
// CRITICAL: Must include UserId parameter or Jellyfin returns empty results
|
||||||
|
var userId = _settings.UserId;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No UserId configured - attempting to fetch existing playlist tracks may fail");
|
||||||
|
}
|
||||||
|
|
||||||
|
var playlistItemsUrl = $"Playlists/{playlistId}/Items";
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
playlistItemsUrl += $"?UserId={userId}";
|
||||||
|
}
|
||||||
|
|
||||||
var (existingTracksResponse, _) = await _proxyService.GetJsonAsync(
|
var (existingTracksResponse, _) = await _proxyService.GetJsonAsync(
|
||||||
$"Playlists/{playlistId}/Items",
|
playlistItemsUrl,
|
||||||
null,
|
null,
|
||||||
Request.Headers);
|
Request.Headers);
|
||||||
|
|
||||||
@@ -2881,6 +2894,7 @@ public class JellyfinController : ControllerBase
|
|||||||
if (!string.IsNullOrEmpty(spotifyId))
|
if (!string.IsNullOrEmpty(spotifyId))
|
||||||
{
|
{
|
||||||
existingBySpotifyId[spotifyId] = song;
|
existingBySpotifyId[spotifyId] = song;
|
||||||
|
_logger.LogDebug("Indexed local track by Spotify ID: {SpotifyId} -> {Title}", spotifyId, song.Title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2888,11 +2902,16 @@ public class JellyfinController : ControllerBase
|
|||||||
if (!string.IsNullOrEmpty(song.Isrc))
|
if (!string.IsNullOrEmpty(song.Isrc))
|
||||||
{
|
{
|
||||||
existingByIsrc[song.Isrc] = song;
|
existingByIsrc[song.Isrc] = song;
|
||||||
|
_logger.LogDebug("Indexed local track by ISRC: {Isrc} -> {Title}", song.Isrc, song.Title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_logger.LogInformation("Found {Count} existing tracks in Jellyfin playlist ({SpotifyIds} with Spotify IDs, {Isrcs} with ISRCs)",
|
_logger.LogInformation("Found {Count} existing tracks in Jellyfin playlist ({SpotifyIds} with Spotify IDs, {Isrcs} with ISRCs)",
|
||||||
existingTracks.Count, existingBySpotifyId.Count, existingByIsrc.Count);
|
existingTracks.Count, existingBySpotifyId.Count, existingByIsrc.Count);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No existing tracks found in Jellyfin playlist {PlaylistId} - may need UserId parameter", playlistId);
|
||||||
|
}
|
||||||
|
|
||||||
// Get the full playlist from Spotify to know the correct order
|
// Get the full playlist from Spotify to know the correct order
|
||||||
var spotifyTracks = await _spotifyPlaylistFetcher!.GetPlaylistTracksAsync(spotifyPlaylistName);
|
var spotifyTracks = await _spotifyPlaylistFetcher!.GetPlaylistTracksAsync(spotifyPlaylistName);
|
||||||
@@ -2915,12 +2934,16 @@ public class JellyfinController : ControllerBase
|
|||||||
if (existingBySpotifyId.TryGetValue(spotifyTrack.SpotifyId, out var trackBySpotifyId))
|
if (existingBySpotifyId.TryGetValue(spotifyTrack.SpotifyId, out var trackBySpotifyId))
|
||||||
{
|
{
|
||||||
localTrack = trackBySpotifyId;
|
localTrack = trackBySpotifyId;
|
||||||
|
_logger.LogDebug("#{Pos} {Title} - Found LOCAL by Spotify ID: {SpotifyId}",
|
||||||
|
spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.SpotifyId);
|
||||||
}
|
}
|
||||||
// Try to find by ISRC (most reliable for matching)
|
// Try to find by ISRC (most reliable for matching)
|
||||||
else if (!string.IsNullOrEmpty(spotifyTrack.Isrc) &&
|
else if (!string.IsNullOrEmpty(spotifyTrack.Isrc) &&
|
||||||
existingByIsrc.TryGetValue(spotifyTrack.Isrc, out var trackByIsrc))
|
existingByIsrc.TryGetValue(spotifyTrack.Isrc, out var trackByIsrc))
|
||||||
{
|
{
|
||||||
localTrack = trackByIsrc;
|
localTrack = trackByIsrc;
|
||||||
|
_logger.LogDebug("#{Pos} {Title} - Found LOCAL by ISRC: {Isrc}",
|
||||||
|
spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.Isrc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we found a local track, use it
|
// If we found a local track, use it
|
||||||
@@ -2937,6 +2960,14 @@ public class JellyfinController : ControllerBase
|
|||||||
{
|
{
|
||||||
finalTracks.Add(matched.MatchedSong);
|
finalTracks.Add(matched.MatchedSong);
|
||||||
externalUsedCount++;
|
externalUsedCount++;
|
||||||
|
_logger.LogDebug("#{Pos} {Title} - Using EXTERNAL match: {Provider}/{Id}",
|
||||||
|
spotifyTrack.Position, spotifyTrack.Title,
|
||||||
|
matched.MatchedSong.ExternalProvider, matched.MatchedSong.ExternalId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("#{Pos} {Title} - NO MATCH (skipping)",
|
||||||
|
spotifyTrack.Position, spotifyTrack.Title);
|
||||||
}
|
}
|
||||||
// If no match, the track is simply omitted (not available from any source)
|
// If no match, the track is simply omitted (not available from any source)
|
||||||
}
|
}
|
||||||
@@ -2986,8 +3017,20 @@ public class JellyfinController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get existing Jellyfin playlist items (tracks the plugin already found)
|
// Get existing Jellyfin playlist items (tracks the plugin already found)
|
||||||
|
// CRITICAL: Must include UserId parameter or Jellyfin returns empty results
|
||||||
|
var userId = _settings.UserId;
|
||||||
|
var playlistItemsUrl = $"Playlists/{playlistId}/Items";
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
playlistItemsUrl += $"?UserId={userId}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No UserId configured - may not be able to fetch existing playlist tracks");
|
||||||
|
}
|
||||||
|
|
||||||
var (existingTracksResponse, _) = await _proxyService.GetJsonAsync(
|
var (existingTracksResponse, _) = await _proxyService.GetJsonAsync(
|
||||||
$"Playlists/{playlistId}/Items",
|
playlistItemsUrl,
|
||||||
null,
|
null,
|
||||||
Request.Headers);
|
Request.Headers);
|
||||||
|
|
||||||
@@ -3011,6 +3054,10 @@ public class JellyfinController : ControllerBase
|
|||||||
}
|
}
|
||||||
_logger.LogInformation("Found {Count} existing tracks in Jellyfin playlist", existingTracks.Count);
|
_logger.LogInformation("Found {Count} existing tracks in Jellyfin playlist", existingTracks.Count);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No existing tracks found in Jellyfin playlist - may need UserId parameter");
|
||||||
|
}
|
||||||
|
|
||||||
var missingTracksKey = $"spotify:missing:{spotifyPlaylistName}";
|
var missingTracksKey = $"spotify:missing:{spotifyPlaylistName}";
|
||||||
var missingTracks = await _cache.GetAsync<List<MissingTrack>>(missingTracksKey);
|
var missingTracks = await _cache.GetAsync<List<MissingTrack>>(missingTracksKey);
|
||||||
|
|||||||
@@ -87,14 +87,62 @@ public class SpotifyTrackMatchingService : BackgroundService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Public method to trigger matching manually (called from controller).
|
/// Public method to trigger matching manually for all playlists (called from controller).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task TriggerMatchingAsync()
|
public async Task TriggerMatchingAsync()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Manual track matching triggered");
|
_logger.LogInformation("Manual track matching triggered for all playlists");
|
||||||
await MatchAllPlaylistsAsync(CancellationToken.None);
|
await MatchAllPlaylistsAsync(CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Public method to trigger matching for a specific playlist (called from controller).
|
||||||
|
/// </summary>
|
||||||
|
public async Task TriggerMatchingForPlaylistAsync(string playlistName)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Manual track matching triggered for playlist: {Playlist}", playlistName);
|
||||||
|
|
||||||
|
var playlist = _spotifySettings.Playlists
|
||||||
|
.FirstOrDefault(p => p.Name.Equals(playlistName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (playlist == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Playlist {Playlist} not found in configuration", playlistName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var metadataService = scope.ServiceProvider.GetRequiredService<IMusicMetadataService>();
|
||||||
|
|
||||||
|
// Check if we should use the new SpotifyPlaylistFetcher
|
||||||
|
SpotifyPlaylistFetcher? playlistFetcher = null;
|
||||||
|
if (_spotifyApiSettings.Enabled)
|
||||||
|
{
|
||||||
|
playlistFetcher = scope.ServiceProvider.GetService<SpotifyPlaylistFetcher>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (playlistFetcher != null)
|
||||||
|
{
|
||||||
|
// Use new direct API mode with ISRC support
|
||||||
|
await MatchPlaylistTracksWithIsrcAsync(
|
||||||
|
playlist.Name, playlistFetcher, metadataService, CancellationToken.None);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fall back to legacy mode
|
||||||
|
await MatchPlaylistTracksLegacyAsync(
|
||||||
|
playlist.Name, metadataService, CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error matching tracks for playlist {Playlist}", playlist.Name);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task MatchAllPlaylistsAsync(CancellationToken cancellationToken)
|
private async Task MatchAllPlaylistsAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("=== STARTING TRACK MATCHING ===");
|
_logger.LogInformation("=== STARTING TRACK MATCHING ===");
|
||||||
@@ -176,13 +224,26 @@ public class SpotifyTrackMatchingService : BackgroundService
|
|||||||
// Get existing tracks from Jellyfin playlist to avoid re-matching
|
// Get existing tracks from Jellyfin playlist to avoid re-matching
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
var proxyService = scope.ServiceProvider.GetService<JellyfinProxyService>();
|
var proxyService = scope.ServiceProvider.GetService<JellyfinProxyService>();
|
||||||
|
var jellyfinSettings = scope.ServiceProvider.GetService<IOptions<JellyfinSettings>>()?.Value;
|
||||||
|
|
||||||
if (proxyService != null)
|
if (proxyService != null && jellyfinSettings != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// CRITICAL: Must include UserId parameter or Jellyfin returns empty results
|
||||||
|
var userId = jellyfinSettings.UserId;
|
||||||
|
var playlistItemsUrl = $"Playlists/{playlistConfig.JellyfinId}/Items";
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
playlistItemsUrl += $"?UserId={userId}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No UserId configured - may not be able to fetch existing playlist tracks for {Playlist}", playlistName);
|
||||||
|
}
|
||||||
|
|
||||||
var (existingTracksResponse, _) = await proxyService.GetJsonAsync(
|
var (existingTracksResponse, _) = await proxyService.GetJsonAsync(
|
||||||
$"Playlists/{playlistConfig.JellyfinId}/Items",
|
playlistItemsUrl,
|
||||||
null,
|
null,
|
||||||
null);
|
null);
|
||||||
|
|
||||||
@@ -204,6 +265,10 @@ public class SpotifyTrackMatchingService : BackgroundService
|
|||||||
_logger.LogInformation("Found {Count} tracks already in Jellyfin playlist {Playlist}, will skip matching these",
|
_logger.LogInformation("Found {Count} tracks already in Jellyfin playlist {Playlist}, will skip matching these",
|
||||||
existingSpotifyIds.Count, playlistName);
|
existingSpotifyIds.Count, playlistName);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No Items found in Jellyfin playlist response for {Playlist} - may need UserId parameter", playlistName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -646,6 +646,7 @@
|
|||||||
<h2>
|
<h2>
|
||||||
Active Spotify Playlists
|
Active Spotify Playlists
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
<button onclick="matchAllPlaylists()">Match All Tracks</button>
|
||||||
<button onclick="refreshPlaylists()">Refresh All</button>
|
<button onclick="refreshPlaylists()">Refresh All</button>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -665,7 +666,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="playlist-table-body">
|
<tbody id="playlist-table-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="loading">
|
<td colspan="6" class="loading">
|
||||||
<span class="spinner"></span> Loading playlists...
|
<span class="spinner"></span> Loading playlists...
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -762,6 +763,11 @@
|
|||||||
<span class="value" id="config-jellyfin-api-key">-</span>
|
<span class="value" id="config-jellyfin-api-key">-</span>
|
||||||
<button onclick="openEditSetting('JELLYFIN_API_KEY', 'Jellyfin API Key', 'password')">Update</button>
|
<button onclick="openEditSetting('JELLYFIN_API_KEY', 'Jellyfin API Key', 'password')">Update</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<span class="label">User ID</span>
|
||||||
|
<span class="value" id="config-jellyfin-user-id">-</span>
|
||||||
|
<button onclick="openEditSetting('JELLYFIN_USER_ID', 'Jellyfin User ID', 'text', 'Required for playlist operations. Get from Jellyfin user profile URL: userId=...')">Edit</button>
|
||||||
|
</div>
|
||||||
<div class="config-item">
|
<div class="config-item">
|
||||||
<span class="label">Library ID</span>
|
<span class="label">Library ID</span>
|
||||||
<span class="value" id="config-jellyfin-library-id">-</span>
|
<span class="value" id="config-jellyfin-library-id">-</span>
|
||||||
@@ -1126,6 +1132,7 @@
|
|||||||
// Jellyfin settings
|
// Jellyfin settings
|
||||||
document.getElementById('config-jellyfin-url').textContent = data.jellyfin.url || '-';
|
document.getElementById('config-jellyfin-url').textContent = data.jellyfin.url || '-';
|
||||||
document.getElementById('config-jellyfin-api-key').textContent = data.jellyfin.apiKey;
|
document.getElementById('config-jellyfin-api-key').textContent = data.jellyfin.apiKey;
|
||||||
|
document.getElementById('config-jellyfin-user-id').textContent = data.jellyfin.userId || '(not set)';
|
||||||
document.getElementById('config-jellyfin-library-id').textContent = data.jellyfin.libraryId || '-';
|
document.getElementById('config-jellyfin-library-id').textContent = data.jellyfin.libraryId || '-';
|
||||||
|
|
||||||
// Sync settings
|
// Sync settings
|
||||||
@@ -1347,6 +1354,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function matchAllPlaylists() {
|
||||||
|
try {
|
||||||
|
showToast('Matching tracks for all playlists...', 'success');
|
||||||
|
const res = await fetch('/api/admin/playlists/match-all', { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
} else {
|
||||||
|
showToast(data.error || 'Failed to match tracks', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Failed to match tracks', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function clearCache() {
|
async function clearCache() {
|
||||||
if (!confirm('Clear all cached playlist data?')) return;
|
if (!confirm('Clear all cached playlist data?')) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user