Improve favorite/unfavorite logic - copy from cache, avoid re-downloads

This commit is contained in:
2026-02-04 22:34:11 -05:00
parent 8fad6d8c4e
commit f44d8652b4

View File

@@ -1455,11 +1455,25 @@ public class JellyfinController : ControllerBase
_logger.LogInformation("UnmarkFavorite called: userId={UserId}, itemId={ItemId}, route={Route}",
userId, itemId, Request.Path);
// External items can't be unfavorited (they're not really favorited in Jellyfin)
var (isExternal, _, _) = _localLibraryService.ParseSongId(itemId);
// External items - remove from kept folder if it exists
var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId);
if (isExternal || PlaylistIdHelper.IsExternalPlaylist(itemId))
{
_logger.LogInformation("Unfavoriting external item {ItemId} - returning success", itemId);
_logger.LogInformation("Unfavoriting external item {ItemId} - removing from kept folder", itemId);
// Remove from kept folder in background
_ = Task.Run(async () =>
{
try
{
await RemoveExternalTrackFromKeptAsync(itemId, provider!, externalId!);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to remove external track {ItemId} from kept folder", itemId);
}
});
return Ok(new
{
IsFavorite = false,
@@ -3446,7 +3460,7 @@ public class JellyfinController : ControllerBase
{
try
{
// Get the song metadata first to check if already in kept folder
// Get the song metadata first to build paths
var song = await _metadataService.GetSongAsync(provider, externalId);
if (song == null)
{
@@ -3459,12 +3473,11 @@ public class JellyfinController : ControllerBase
var keptArtistPath = Path.Combine(keptBasePath, PathHelper.SanitizeFileName(song.Artist));
var keptAlbumPath = Path.Combine(keptArtistPath, PathHelper.SanitizeFileName(song.Album));
// Check if track already exists in kept folder BEFORE downloading
// Look for any file matching the song title pattern (any extension)
// Check if track already exists in kept folder
if (Directory.Exists(keptAlbumPath))
{
var sanitizedTitle = PathHelper.SanitizeFileName(song.Title);
var existingFiles = Directory.GetFiles(keptAlbumPath, $"{sanitizedTitle}.*");
var existingFiles = Directory.GetFiles(keptAlbumPath, $"*{sanitizedTitle}*");
if (existingFiles.Length > 0)
{
_logger.LogInformation("Track already exists in kept folder: {Path}", existingFiles[0]);
@@ -3472,25 +3485,44 @@ public class JellyfinController : ControllerBase
}
}
// Track not in kept folder - download it
_logger.LogInformation("Downloading track for kept folder: {ItemId}", itemId);
string downloadPath;
// Look for the track in cache folder first
var cacheBasePath = "/tmp/allstarr-cache";
var cacheArtistPath = Path.Combine(cacheBasePath, PathHelper.SanitizeFileName(song.Artist));
var cacheAlbumPath = Path.Combine(cacheArtistPath, PathHelper.SanitizeFileName(song.Album));
string? sourceFilePath = null;
if (Directory.Exists(cacheAlbumPath))
{
var sanitizedTitle = PathHelper.SanitizeFileName(song.Title);
var cacheFiles = Directory.GetFiles(cacheAlbumPath, $"*{sanitizedTitle}*");
if (cacheFiles.Length > 0)
{
sourceFilePath = cacheFiles[0];
_logger.LogInformation("Found track in cache folder: {Path}", sourceFilePath);
}
}
// If not in cache, download it first
if (sourceFilePath == null)
{
_logger.LogInformation("Track not in cache, downloading: {ItemId}", itemId);
try
{
downloadPath = await _downloadService.DownloadSongAsync(provider, externalId);
sourceFilePath = await _downloadService.DownloadSongAsync(provider, externalId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to download track {ItemId}", itemId);
return;
}
}
// Create the kept folder structure
Directory.CreateDirectory(keptAlbumPath);
// Copy file to kept folder
var fileName = Path.GetFileName(downloadPath);
var fileName = Path.GetFileName(sourceFilePath);
var keptFilePath = Path.Combine(keptAlbumPath, fileName);
// Double-check in case of race condition (multiple favorite clicks)
@@ -3500,17 +3532,17 @@ public class JellyfinController : ControllerBase
return;
}
System.IO.File.Copy(downloadPath, keptFilePath, overwrite: false);
_logger.LogInformation("✓ Copied favorited track to kept folder: {Path}", keptFilePath);
System.IO.File.Copy(sourceFilePath, keptFilePath, overwrite: false);
_logger.LogInformation("✓ Copied track to kept folder: {Path}", keptFilePath);
// Also copy cover art if it exists
var coverPath = Path.Combine(Path.GetDirectoryName(downloadPath)!, "cover.jpg");
if (System.IO.File.Exists(coverPath))
var sourceCoverPath = Path.Combine(Path.GetDirectoryName(sourceFilePath)!, "cover.jpg");
if (System.IO.File.Exists(sourceCoverPath))
{
var keptCoverPath = Path.Combine(keptAlbumPath, "cover.jpg");
if (!System.IO.File.Exists(keptCoverPath))
{
System.IO.File.Copy(coverPath, keptCoverPath, overwrite: false);
System.IO.File.Copy(sourceCoverPath, keptCoverPath, overwrite: false);
_logger.LogDebug("Copied cover art to kept folder");
}
}
@@ -3521,6 +3553,70 @@ public class JellyfinController : ControllerBase
}
}
/// <summary>
/// Removes an external track from the kept folder when unfavorited.
/// </summary>
private async Task RemoveExternalTrackFromKeptAsync(string itemId, string provider, string externalId)
{
try
{
// Get the song metadata to build paths
var song = await _metadataService.GetSongAsync(provider, externalId);
if (song == null)
{
_logger.LogWarning("Could not find song metadata for {ItemId}", itemId);
return;
}
// Build kept folder path: /app/kept/Artist/Album/
var keptBasePath = "/app/kept";
var keptArtistPath = Path.Combine(keptBasePath, PathHelper.SanitizeFileName(song.Artist));
var keptAlbumPath = Path.Combine(keptArtistPath, PathHelper.SanitizeFileName(song.Album));
if (!Directory.Exists(keptAlbumPath))
{
_logger.LogInformation("Track not in kept folder (album folder doesn't exist): {ItemId}", itemId);
return;
}
// Find and remove the track file
var sanitizedTitle = PathHelper.SanitizeFileName(song.Title);
var trackFiles = Directory.GetFiles(keptAlbumPath, $"*{sanitizedTitle}*");
if (trackFiles.Length == 0)
{
_logger.LogInformation("Track not found in kept folder: {ItemId}", itemId);
return;
}
foreach (var trackFile in trackFiles)
{
System.IO.File.Delete(trackFile);
_logger.LogInformation("✓ Removed track from kept folder: {Path}", trackFile);
}
// Clean up empty directories
if (Directory.GetFiles(keptAlbumPath).Length == 0 && Directory.GetDirectories(keptAlbumPath).Length == 0)
{
Directory.Delete(keptAlbumPath);
_logger.LogDebug("Removed empty album folder: {Path}", keptAlbumPath);
// Also remove artist folder if empty
if (Directory.Exists(keptArtistPath) &&
Directory.GetFiles(keptArtistPath).Length == 0 &&
Directory.GetDirectories(keptArtistPath).Length == 0)
{
Directory.Delete(keptArtistPath);
_logger.LogDebug("Removed empty artist folder: {Path}", keptArtistPath);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing external track {ItemId} from kept folder", itemId);
}
}
/// <summary>
/// Loads missing tracks from file cache as fallback when Redis is empty.
/// </summary>