mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Add local/external track columns to Jellyfin playlists, remove libraries filter
This commit is contained in:
@@ -697,7 +697,7 @@ public class AdminController : ControllerBase
|
|||||||
/// Get all playlists from Jellyfin
|
/// Get all playlists from Jellyfin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("jellyfin/playlists")]
|
[HttpGet("jellyfin/playlists")]
|
||||||
public async Task<IActionResult> GetJellyfinPlaylists([FromQuery] string? userId = null, [FromQuery] string? parentId = null)
|
public async Task<IActionResult> GetJellyfinPlaylists([FromQuery] string? userId = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_jellyfinSettings.Url) || string.IsNullOrEmpty(_jellyfinSettings.ApiKey))
|
if (string.IsNullOrEmpty(_jellyfinSettings.Url) || string.IsNullOrEmpty(_jellyfinSettings.ApiKey))
|
||||||
{
|
{
|
||||||
@@ -706,7 +706,7 @@ public class AdminController : ControllerBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Build URL with optional userId and parentId (library) filters
|
// Build URL with optional userId filter
|
||||||
var url = $"{_jellyfinSettings.Url}/Items?IncludeItemTypes=Playlist&Recursive=true&Fields=ProviderIds,ChildCount,RecursiveItemCount,SongCount";
|
var url = $"{_jellyfinSettings.Url}/Items?IncludeItemTypes=Playlist&Recursive=true&Fields=ProviderIds,ChildCount,RecursiveItemCount,SongCount";
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(userId))
|
if (!string.IsNullOrEmpty(userId))
|
||||||
@@ -714,11 +714,6 @@ public class AdminController : ControllerBase
|
|||||||
url += $"&UserId={userId}";
|
url += $"&UserId={userId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(parentId))
|
|
||||||
{
|
|
||||||
url += $"&ParentId={parentId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
||||||
|
|
||||||
@@ -758,13 +753,19 @@ public class AdminController : ControllerBase
|
|||||||
var isConfigured = configuredPlaylist != null;
|
var isConfigured = configuredPlaylist != null;
|
||||||
var linkedSpotifyId = configuredPlaylist?.Id;
|
var linkedSpotifyId = configuredPlaylist?.Id;
|
||||||
|
|
||||||
|
// Fetch track details to categorize local vs external
|
||||||
|
var trackStats = await GetPlaylistTrackStats(id!);
|
||||||
|
|
||||||
playlists.Add(new
|
playlists.Add(new
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
trackCount = childCount,
|
trackCount = childCount,
|
||||||
linkedSpotifyId,
|
linkedSpotifyId,
|
||||||
isConfigured
|
isConfigured,
|
||||||
|
localTracks = trackStats.LocalTracks,
|
||||||
|
externalTracks = trackStats.ExternalTracks,
|
||||||
|
externalAvailable = trackStats.ExternalAvailable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -778,6 +779,94 @@ public class AdminController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get track statistics for a playlist (local vs external)
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(int LocalTracks, int ExternalTracks, int ExternalAvailable)> GetPlaylistTrackStats(string playlistId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"{_jellyfinSettings.Url}/Playlists/{playlistId}/Items?Fields=ProviderIds,Path,MediaSources";
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
|
||||||
|
|
||||||
|
var response = await _jellyfinHttpClient.SendAsync(request);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return (0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
|
var localTracks = 0;
|
||||||
|
var externalTracks = 0;
|
||||||
|
var externalAvailable = 0;
|
||||||
|
|
||||||
|
if (doc.RootElement.TryGetProperty("Items", out var items))
|
||||||
|
{
|
||||||
|
foreach (var item in items.EnumerateArray())
|
||||||
|
{
|
||||||
|
// Check if track has a local path (is in user's library)
|
||||||
|
var hasPath = item.TryGetProperty("Path", out var path) &&
|
||||||
|
path.ValueKind == JsonValueKind.String &&
|
||||||
|
!string.IsNullOrEmpty(path.GetString());
|
||||||
|
|
||||||
|
// Check MediaSources to see if it's a local file vs external
|
||||||
|
var isLocal = false;
|
||||||
|
if (item.TryGetProperty("MediaSources", out var mediaSources) &&
|
||||||
|
mediaSources.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var source in mediaSources.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (source.TryGetProperty("Protocol", out var protocol))
|
||||||
|
{
|
||||||
|
var protocolStr = protocol.GetString();
|
||||||
|
if (protocolStr == "File")
|
||||||
|
{
|
||||||
|
isLocal = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also check if Path exists in MediaSource
|
||||||
|
if (source.TryGetProperty("Path", out var sourcePath) &&
|
||||||
|
sourcePath.ValueKind == JsonValueKind.String &&
|
||||||
|
!string.IsNullOrEmpty(sourcePath.GetString()))
|
||||||
|
{
|
||||||
|
isLocal = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to checking Path property
|
||||||
|
if (!isLocal && hasPath)
|
||||||
|
{
|
||||||
|
isLocal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocal)
|
||||||
|
{
|
||||||
|
localTracks++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
externalTracks++;
|
||||||
|
// For now, if it's in the playlist but not local, it means a provider found it
|
||||||
|
externalAvailable++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (localTracks, externalTracks, externalAvailable);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to get track stats for playlist {PlaylistId}", playlistId);
|
||||||
|
return (0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Link a Jellyfin playlist to a Spotify playlist
|
/// Link a Jellyfin playlist to a Spotify playlist
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -575,19 +575,14 @@
|
|||||||
<option value="">All Users</option>
|
<option value="">All Users</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin: 0; flex: 1; min-width: 200px;">
|
|
||||||
<label style="display: block; margin-bottom: 4px; color: var(--text-secondary); font-size: 0.85rem;">Library</label>
|
|
||||||
<select id="jellyfin-library-select" onchange="fetchJellyfinPlaylists()" style="width: 100%; padding: 8px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary);">
|
|
||||||
<option value="">All Libraries</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="playlist-table">
|
<table class="playlist-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Tracks</th>
|
<th>Local</th>
|
||||||
|
<th>External</th>
|
||||||
<th>Linked Spotify ID</th>
|
<th>Linked Spotify ID</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
@@ -595,7 +590,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="jellyfin-playlist-table-body">
|
<tbody id="jellyfin-playlist-table-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="loading">
|
<td colspan="6" class="loading">
|
||||||
<span class="spinner"></span> Loading Jellyfin playlists...
|
<span class="spinner"></span> Loading Jellyfin playlists...
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1076,46 +1071,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJellyfinLibraries() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/admin/jellyfin/libraries');
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
const select = document.getElementById('jellyfin-library-select');
|
|
||||||
select.innerHTML = '<option value="">All Libraries</option>' +
|
|
||||||
data.libraries.map(l => `<option value="${l.id}">${escapeHtml(l.name)}${l.collectionType ? ' (' + l.collectionType + ')' : ''}</option>`).join('');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch libraries:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchJellyfinPlaylists() {
|
async function fetchJellyfinPlaylists() {
|
||||||
const tbody = document.getElementById('jellyfin-playlist-table-body');
|
const tbody = document.getElementById('jellyfin-playlist-table-body');
|
||||||
tbody.innerHTML = '<tr><td colspan="5" class="loading"><span class="spinner"></span> Loading Jellyfin playlists...</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" class="loading"><span class="spinner"></span> Loading Jellyfin playlists...</td></tr>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build URL with optional filters
|
// Build URL with optional user filter
|
||||||
const userId = document.getElementById('jellyfin-user-select').value;
|
const userId = document.getElementById('jellyfin-user-select').value;
|
||||||
const parentId = document.getElementById('jellyfin-library-select').value;
|
|
||||||
|
|
||||||
let url = '/api/admin/jellyfin/playlists';
|
let url = '/api/admin/jellyfin/playlists';
|
||||||
const params = new URLSearchParams();
|
if (userId) url += '?userId=' + encodeURIComponent(userId);
|
||||||
if (userId) params.append('userId', userId);
|
|
||||||
if (parentId) params.append('parentId', parentId);
|
|
||||||
if (params.toString()) url += '?' + params.toString();
|
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json();
|
const errorData = await res.json();
|
||||||
tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--error);padding:40px;">${errorData.error || 'Failed to fetch playlists'}</td></tr>`;
|
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:var(--error);padding:40px;">${errorData.error || 'Failed to fetch playlists'}</td></tr>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.playlists.length === 0) {
|
if (data.playlists.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-secondary);padding:40px;">No playlists found in Jellyfin</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-secondary);padding:40px;">No playlists found in Jellyfin</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1128,10 +1107,15 @@
|
|||||||
? `<button class="danger" onclick="unlinkPlaylist('${escapeHtml(p.name)}')">Unlink</button>`
|
? `<button class="danger" onclick="unlinkPlaylist('${escapeHtml(p.name)}')">Unlink</button>`
|
||||||
: `<button class="primary" onclick="openLinkPlaylist('${escapeHtml(p.id)}', '${escapeHtml(p.name)}')">Link to Spotify</button>`;
|
: `<button class="primary" onclick="openLinkPlaylist('${escapeHtml(p.id)}', '${escapeHtml(p.name)}')">Link to Spotify</button>`;
|
||||||
|
|
||||||
|
const localCount = p.localTracks || 0;
|
||||||
|
const externalCount = p.externalTracks || 0;
|
||||||
|
const externalAvail = p.externalAvailable || 0;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>${escapeHtml(p.name)}</strong></td>
|
<td><strong>${escapeHtml(p.name)}</strong></td>
|
||||||
<td class="track-count">${p.trackCount || 0}</td>
|
<td class="track-count">${localCount}</td>
|
||||||
|
<td class="track-count">${externalCount > 0 ? `${externalAvail}/${externalCount}` : '-'}</td>
|
||||||
<td style="font-family:monospace;font-size:0.85rem;color:var(--text-secondary);">${p.linkedSpotifyId || '-'}</td>
|
<td style="font-family:monospace;font-size:0.85rem;color:var(--text-secondary);">${p.linkedSpotifyId || '-'}</td>
|
||||||
<td>${statusBadge}</td>
|
<td>${statusBadge}</td>
|
||||||
<td>${actionButton}</td>
|
<td>${actionButton}</td>
|
||||||
@@ -1140,7 +1124,7 @@
|
|||||||
}).join('');
|
}).join('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch Jellyfin playlists:', error);
|
console.error('Failed to fetch Jellyfin playlists:', error);
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--error);padding:40px;">Failed to fetch playlists</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--error);padding:40px;">Failed to fetch playlists</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1468,7 +1452,6 @@
|
|||||||
fetchStatus();
|
fetchStatus();
|
||||||
fetchPlaylists();
|
fetchPlaylists();
|
||||||
fetchJellyfinUsers();
|
fetchJellyfinUsers();
|
||||||
fetchJellyfinLibraries();
|
|
||||||
fetchJellyfinPlaylists();
|
fetchJellyfinPlaylists();
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user