Add favorite-to-keep feature for external tracks

- When favoriting an external track, automatically copy to /kept folder
- Organized as Artist/Album/Track structure
- Includes cover art if available
- Downloads track first if not already cached
- Add KEPT_PATH and CACHE_PATH volumes to docker-compose
- Update .env.example and README with new feature
This commit is contained in:
2026-02-01 11:09:18 -05:00
parent ae8afa20f8
commit 8da0bef481
4 changed files with 95 additions and 4 deletions

View File

@@ -30,6 +30,12 @@ MUSIC_SERVICE=SquidWTF
# Path where downloaded songs will be stored on the host (only applies if STORAGE_MODE=Permanent) # Path where downloaded songs will be stored on the host (only applies if STORAGE_MODE=Permanent)
DOWNLOAD_PATH=./downloads DOWNLOAD_PATH=./downloads
# Path where favorited external tracks are permanently kept
KEPT_PATH=./kept
# Path for cache files (Spotify missing tracks, etc.)
CACHE_PATH=./cache
# ===== SQUIDWTF CONFIGURATION ===== # ===== SQUIDWTF CONFIGURATION =====
# Different quality options for SquidWTF. Only FLAC supported right now # Different quality options for SquidWTF. Only FLAC supported right now
SQUIDWTF_QUALITY=FLAC SQUIDWTF_QUALITY=FLAC

View File

@@ -83,6 +83,7 @@ This project brings together all the music streaming providers into one unified
- **Transparent Proxy**: Sits between your music clients and media server - **Transparent Proxy**: Sits between your music clients and media server
- **Automatic Search**: Searches streaming providers when songs aren't local - **Automatic Search**: Searches streaming providers when songs aren't local
- **On-the-Fly Downloads**: Songs download and cache for future use - **On-the-Fly Downloads**: Songs download and cache for future use
- **Favorite to Keep**: When you favorite an external track, it's automatically copied to a permanent `/kept` folder separate from the cache
- **External Playlist Support**: Search and download playlists from Deezer, Qobuz, and SquidWTF with M3U generation - **External Playlist Support**: Search and download playlists from Deezer, Qobuz, and SquidWTF with M3U generation
- **Hi-Res Audio**: SquidWTF supports up to 24-bit/192kHz FLAC - **Hi-Res Audio**: SquidWTF supports up to 24-bit/192kHz FLAC
- **Full Metadata**: Downloaded files include complete ID3 tags (title, artist, album, track number, year, genre, BPM, ISRC, etc.) and cover art - **Full Metadata**: Downloaded files include complete ID3 tags (title, artist, album, track number, year, genre, BPM, ISRC, etc.) and cover art

View File

@@ -1118,12 +1118,24 @@ public class JellyfinController : ControllerBase
} }
// Check if this is an external song/album // Check if this is an external song/album
var (isExternal, _, _) = _localLibraryService.ParseSongId(itemId); var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId);
if (isExternal) if (isExternal)
{ {
// External items don't exist in Jellyfin, so we can't favorite them there _logger.LogInformation("Favoriting external item {ItemId}, copying to kept folder", itemId);
// Just return success - the client will show it as favorited
_logger.LogDebug("Favoriting external item {ItemId} (not synced to Jellyfin)", itemId); // Copy the track to kept folder in background
_ = Task.Run(async () =>
{
try
{
await CopyExternalTrackToKeptAsync(itemId, provider!, externalId!);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to copy external track {ItemId} to kept folder", itemId);
}
});
return Ok(new { IsFavorite = true }); return Ok(new { IsFavorite = true });
} }
@@ -2083,6 +2095,76 @@ public class JellyfinController : ControllerBase
} }
} }
/// <summary>
/// Copies an external track to the kept folder when favorited.
/// </summary>
private async Task CopyExternalTrackToKeptAsync(string itemId, string provider, string externalId)
{
try
{
// Get the song metadata
var song = await _metadataService.GetSongAsync(provider, externalId);
if (song == null)
{
_logger.LogWarning("Could not find song metadata for {ItemId}", itemId);
return;
}
// Check if file exists in downloads folder
var downloadPath = PathHelper.GetSongPath(song.Artist, song.Album, song.Title);
if (!System.IO.File.Exists(downloadPath))
{
_logger.LogInformation("Track not yet downloaded, triggering download for {ItemId}", itemId);
// Download the track first
var downloadResult = await _downloadService.DownloadSongAsync(provider, externalId);
if (!downloadResult.IsSuccess)
{
_logger.LogWarning("Failed to download track {ItemId}: {Error}", itemId, downloadResult.Error);
return;
}
downloadPath = downloadResult.Value!.FilePath;
}
// Create kept folder structure: /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));
Directory.CreateDirectory(keptAlbumPath);
// Copy file to kept folder
var fileName = Path.GetFileName(downloadPath);
var keptFilePath = Path.Combine(keptAlbumPath, fileName);
if (System.IO.File.Exists(keptFilePath))
{
_logger.LogInformation("Track already exists in kept folder: {Path}", keptFilePath);
return;
}
System.IO.File.Copy(downloadPath, keptFilePath, overwrite: false);
_logger.LogInformation("✓ Copied favorited 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 keptCoverPath = Path.Combine(keptAlbumPath, "cover.jpg");
if (!System.IO.File.Exists(keptCoverPath))
{
System.IO.File.Copy(coverPath, keptCoverPath, overwrite: false);
_logger.LogDebug("Copied cover art to kept folder");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error copying external track {ItemId} to kept folder", itemId);
}
}
/// <summary> /// <summary>
/// Loads missing tracks from file cache as fallback when Redis is empty. /// Loads missing tracks from file cache as fallback when Redis is empty.
/// </summary> /// </summary>

View File

@@ -93,6 +93,8 @@ services:
- Qobuz__Quality=${QOBUZ_QUALITY:-FLAC} - Qobuz__Quality=${QOBUZ_QUALITY:-FLAC}
volumes: volumes:
- ${DOWNLOAD_PATH:-./downloads}:/app/downloads - ${DOWNLOAD_PATH:-./downloads}:/app/downloads
- ${KEPT_PATH:-./kept}:/app/kept
- ${CACHE_PATH:-./cache}:/app/cache
networks: networks:
allstarr-network: allstarr-network: