mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Add Jellyfin playlist discovery and linking feature
- Added GET /api/admin/jellyfin/playlists to fetch all playlists from Jellyfin
- Added POST /api/admin/jellyfin/playlists/{id}/link to link playlist to Spotify
- Added DELETE /api/admin/jellyfin/playlists/{name}/unlink to remove link
- Added new 'Jellyfin Playlists' tab in admin UI showing all playlists
- Shows link status for each playlist (Linked/Not Linked)
- Link modal accepts Spotify playlist ID or full URL
- Renamed 'Playlists' tab to 'Configured Playlists' for clarity
This commit is contained in:
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using allstarr.Models.Settings;
|
||||
using allstarr.Services.Spotify;
|
||||
using allstarr.Services.Jellyfin;
|
||||
using allstarr.Services.Common;
|
||||
using allstarr.Filters;
|
||||
using System.Text.Json;
|
||||
@@ -30,6 +31,7 @@ public class AdminController : ControllerBase
|
||||
private readonly SpotifyApiClient _spotifyClient;
|
||||
private readonly SpotifyPlaylistFetcher _playlistFetcher;
|
||||
private readonly RedisCacheService _cache;
|
||||
private readonly HttpClient _jellyfinHttpClient;
|
||||
private const string EnvFilePath = "/app/.env";
|
||||
private const string CacheDirectory = "/app/cache/spotify";
|
||||
|
||||
@@ -44,7 +46,8 @@ public class AdminController : ControllerBase
|
||||
IOptions<SquidWTFSettings> squidWtfSettings,
|
||||
SpotifyApiClient spotifyClient,
|
||||
SpotifyPlaylistFetcher playlistFetcher,
|
||||
RedisCacheService cache)
|
||||
RedisCacheService cache,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
@@ -57,6 +60,7 @@ public class AdminController : ControllerBase
|
||||
_spotifyClient = spotifyClient;
|
||||
_playlistFetcher = playlistFetcher;
|
||||
_cache = cache;
|
||||
_jellyfinHttpClient = httpClientFactory.CreateClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -528,6 +532,150 @@ public class AdminController : ControllerBase
|
||||
return await UpdateConfig(updateRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all playlists from Jellyfin
|
||||
/// </summary>
|
||||
[HttpGet("jellyfin/playlists")]
|
||||
public async Task<IActionResult> GetJellyfinPlaylists()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_jellyfinSettings.Url) || string.IsNullOrEmpty(_jellyfinSettings.ApiKey))
|
||||
{
|
||||
return BadRequest(new { error = "Jellyfin URL or API key not configured" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Call Jellyfin API to get all playlists
|
||||
var url = $"{_jellyfinSettings.Url}/Items?IncludeItemTypes=Playlist&Recursive=true&Fields=ProviderIds,ChildCount";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
|
||||
|
||||
var response = await _jellyfinHttpClient.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorBody = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("Failed to fetch Jellyfin playlists: {StatusCode} - {Body}", response.StatusCode, errorBody);
|
||||
return StatusCode((int)response.StatusCode, new { error = "Failed to fetch playlists from Jellyfin" });
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
|
||||
var playlists = new List<object>();
|
||||
|
||||
if (doc.RootElement.TryGetProperty("Items", out var items))
|
||||
{
|
||||
foreach (var item in items.EnumerateArray())
|
||||
{
|
||||
var id = item.GetProperty("Id").GetString();
|
||||
var name = item.GetProperty("Name").GetString();
|
||||
var childCount = item.TryGetProperty("ChildCount", out var cc) ? cc.GetInt32() : 0;
|
||||
|
||||
// Check if this playlist has a linked Spotify ID in ProviderIds
|
||||
string? linkedSpotifyId = null;
|
||||
if (item.TryGetProperty("ProviderIds", out var providerIds))
|
||||
{
|
||||
if (providerIds.TryGetProperty("Spotify", out var spotifyId))
|
||||
{
|
||||
linkedSpotifyId = spotifyId.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this playlist is already configured in allstarr
|
||||
var isConfigured = _spotifyImportSettings.Playlists.Any(p =>
|
||||
p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
playlists.Add(new
|
||||
{
|
||||
id,
|
||||
name,
|
||||
trackCount = childCount,
|
||||
linkedSpotifyId,
|
||||
isConfigured
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new { playlists });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching Jellyfin playlists");
|
||||
return StatusCode(500, new { error = "Failed to fetch playlists", details = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Link a Jellyfin playlist to a Spotify playlist
|
||||
/// </summary>
|
||||
[HttpPost("jellyfin/playlists/{jellyfinPlaylistId}/link")]
|
||||
public async Task<IActionResult> LinkPlaylist(string jellyfinPlaylistId, [FromBody] LinkPlaylistRequest request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.SpotifyPlaylistId))
|
||||
{
|
||||
return BadRequest(new { error = "SpotifyPlaylistId is required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.Name))
|
||||
{
|
||||
return BadRequest(new { error = "Name is required" });
|
||||
}
|
||||
|
||||
_logger.LogInformation("Linking Jellyfin playlist {JellyfinId} to Spotify playlist {SpotifyId} with name {Name}",
|
||||
jellyfinPlaylistId, request.SpotifyPlaylistId, request.Name);
|
||||
|
||||
// Check if already configured
|
||||
var existingPlaylist = _spotifyImportSettings.Playlists
|
||||
.FirstOrDefault(p => p.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingPlaylist != null)
|
||||
{
|
||||
return BadRequest(new { error = $"Playlist '{request.Name}' is already configured" });
|
||||
}
|
||||
|
||||
// Add the playlist to configuration
|
||||
var currentPlaylists = _spotifyImportSettings.Playlists.ToList();
|
||||
currentPlaylists.Add(new SpotifyPlaylistConfig
|
||||
{
|
||||
Name = request.Name,
|
||||
Id = request.SpotifyPlaylistId,
|
||||
LocalTracksPosition = LocalTracksPosition.First // Use Spotify order
|
||||
});
|
||||
|
||||
// Convert to JSON format for env var
|
||||
var playlistsJson = JsonSerializer.Serialize(
|
||||
currentPlaylists.Select(p => new[] { p.Name, p.Id, p.LocalTracksPosition.ToString().ToLower() }).ToArray()
|
||||
);
|
||||
|
||||
// Update .env file
|
||||
var updateRequest = new ConfigUpdateRequest
|
||||
{
|
||||
Updates = new Dictionary<string, string>
|
||||
{
|
||||
["SPOTIFY_IMPORT_PLAYLISTS"] = playlistsJson
|
||||
}
|
||||
};
|
||||
|
||||
return await UpdateConfig(updateRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlink a playlist (remove from configuration)
|
||||
/// </summary>
|
||||
[HttpDelete("jellyfin/playlists/{name}/unlink")]
|
||||
public async Task<IActionResult> UnlinkPlaylist(string name)
|
||||
{
|
||||
var decodedName = Uri.UnescapeDataString(name);
|
||||
return await RemovePlaylist(decodedName);
|
||||
}
|
||||
|
||||
private string GetJellyfinAuthHeader()
|
||||
{
|
||||
return $"MediaBrowser Client=\"Allstarr\", Device=\"Server\", DeviceId=\"allstarr-admin\", Version=\"1.0.0\", Token=\"{_jellyfinSettings.ApiKey}\"";
|
||||
}
|
||||
|
||||
private static string MaskValue(string? value, int showLast = 0)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "(not set)";
|
||||
@@ -558,3 +706,9 @@ public class AddPlaylistRequest
|
||||
public string SpotifyId { get; set; } = string.Empty;
|
||||
public string LocalTracksPosition { get; set; } = "first";
|
||||
}
|
||||
|
||||
public class LinkPlaylistRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string SpotifyPlaylistId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user