feat: Fork octo-fiestarr as allstarr with Jellyfin proxy improvements

Major changes:
- Rename project from octo-fiesta to allstarr
- Add Jellyfin proxy support alongside Subsonic/Navidrome
- Implement fuzzy search with relevance scoring and Levenshtein distance
- Add POST body logging for debugging playback progress issues
- Separate local and external artists in search results
- Add +5 score boost for external results to prioritize larger catalog(probably gonna reverse it)
- Create FuzzyMatcher utility for intelligent search result scoring
- Add ConvertPlaylistToJellyfinItem method for playlist support
- Rename keys folder to apis and update gitignore
- Filter search results by relevance score (>= 40)
- Add Redis caching support with configurable settings
- Update environment configuration with backend selection
- Improve external provider integration (SquidWTF, Deezer, Qobuz)
- Add tests for all services
This commit is contained in:
2026-01-29 17:36:53 -05:00
parent ed9cec1cde
commit e18840cddf
87 changed files with 166973 additions and 607 deletions

View File

@@ -0,0 +1,20 @@
namespace allstarr.Models.Domain;
/// <summary>
/// Represents an album
/// </summary>
public class Album
{
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Artist { get; set; } = string.Empty;
public string? ArtistId { get; set; }
public int? Year { get; set; }
public int? SongCount { get; set; }
public string? CoverArtUrl { get; set; }
public string? Genre { get; set; }
public bool IsLocal { get; set; }
public string? ExternalProvider { get; set; }
public string? ExternalId { get; set; }
public List<Song> Songs { get; set; } = new();
}

View File

@@ -0,0 +1,15 @@
namespace allstarr.Models.Domain;
/// <summary>
/// Represents an artist
/// </summary>
public class Artist
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? ImageUrl { get; set; }
public int? AlbumCount { get; set; }
public bool IsLocal { get; set; }
public string? ExternalProvider { get; set; }
public string? ExternalId { get; set; }
}

View File

@@ -0,0 +1,97 @@
namespace allstarr.Models.Domain;
/// <summary>
/// Represents a song (local or external)
/// </summary>
public class Song
{
/// <summary>
/// Unique ID. For external songs, prefixed with "ext-" + provider + "-" + external id
/// Example: "ext-deezer-123456" or "local-789"
/// </summary>
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Artist { get; set; } = string.Empty;
public string? ArtistId { get; set; }
public string Album { get; set; } = string.Empty;
public string? AlbumId { get; set; }
public int? Duration { get; set; } // In seconds
public int? Track { get; set; }
public int? DiscNumber { get; set; }
public int? TotalTracks { get; set; }
public int? Year { get; set; }
public string? Genre { get; set; }
public string? CoverArtUrl { get; set; }
/// <summary>
/// High-resolution cover art URL (for embedding)
/// </summary>
public string? CoverArtUrlLarge { get; set; }
/// <summary>
/// BPM (beats per minute) if available
/// </summary>
public int? Bpm { get; set; }
/// <summary>
/// ISRC (International Standard Recording Code)
/// </summary>
public string? Isrc { get; set; }
/// <summary>
/// Full release date (format: YYYY-MM-DD)
/// </summary>
public string? ReleaseDate { get; set; }
/// <summary>
/// Album artist name (may differ from track artist)
/// </summary>
public string? AlbumArtist { get; set; }
/// <summary>
/// Composer(s)
/// </summary>
public string? Composer { get; set; }
/// <summary>
/// Album label
/// </summary>
public string? Label { get; set; }
/// <summary>
/// Copyright
/// </summary>
public string? Copyright { get; set; }
/// <summary>
/// Contributing artists (features, etc.)
/// </summary>
public List<string> Contributors { get; set; } = new();
/// <summary>
/// Indicates whether the song is available locally or needs to be downloaded
/// </summary>
public bool IsLocal { get; set; }
/// <summary>
/// External provider (deezer, spotify, etc.) - null if local
/// </summary>
public string? ExternalProvider { get; set; }
/// <summary>
/// ID on the external provider (for downloading)
/// </summary>
public string? ExternalId { get; set; }
/// <summary>
/// Local file path (if available)
/// </summary>
public string? LocalPath { get; set; }
/// <summary>
/// Deezer explicit content lyrics value
/// 0 = Naturally clean, 1 = Explicit, 2 = Not applicable, 3 = Clean/edited version, 6/7 = Unknown
/// </summary>
public int? ExplicitContentLyrics { get; set; }
}

View File

@@ -0,0 +1,17 @@
namespace allstarr.Models.Download;
/// <summary>
/// Information about an ongoing or completed download
/// </summary>
public class DownloadInfo
{
public string SongId { get; set; } = string.Empty;
public string ExternalId { get; set; } = string.Empty;
public string ExternalProvider { get; set; } = string.Empty;
public DownloadStatus Status { get; set; }
public double Progress { get; set; } // 0.0 to 1.0
public string? LocalPath { get; set; }
public string? ErrorMessage { get; set; }
public DateTime StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace allstarr.Models.Download;
/// <summary>
/// Download status of a song
/// </summary>
public enum DownloadStatus
{
NotStarted,
InProgress,
Completed,
Failed
}

View File

@@ -0,0 +1,13 @@
namespace allstarr.Models.Search;
using allstarr.Models.Domain;
/// <summary>
/// Search result combining local and external results
/// </summary>
public class SearchResult
{
public List<Song> Songs { get; set; } = new();
public List<Album> Albums { get; set; } = new();
public List<Artist> Artists { get; set; } = new();
}

View File

@@ -0,0 +1,25 @@
namespace allstarr.Models.Settings;
/// <summary>
/// Configuration for the Deezer downloader and metadata service
/// </summary>
public class DeezerSettings
{
/// <summary>
/// Deezer ARL token (required for downloading)
/// Obtained from browser cookies after logging into deezer.com
/// </summary>
public string? Arl { get; set; }
/// <summary>
/// Fallback ARL token (optional)
/// Used if the primary ARL token fails
/// </summary>
public string? ArlFallback { get; set; }
/// <summary>
/// Preferred audio quality: FLAC, MP3_320, MP3_128
/// If not specified or unavailable, the highest available quality will be used.
/// </summary>
public string? Quality { get; set; }
}

View File

@@ -0,0 +1,67 @@
namespace allstarr.Models.Settings;
/// <summary>
/// Configuration for Jellyfin media server backend
/// </summary>
public class JellyfinSettings
{
/// <summary>
/// URL of the Jellyfin server
/// Environment variable: JELLYFIN_URL
/// </summary>
public string? Url { get; set; }
/// <summary>
/// API key for authenticating with Jellyfin server
/// Environment variable: JELLYFIN_API_KEY
/// </summary>
public string? ApiKey { get; set; }
/// <summary>
/// User ID for accessing Jellyfin library
/// Environment variable: JELLYFIN_USER_ID
/// </summary>
public string? UserId { get; set; }
/// <summary>
/// Username that clients must provide to authenticate
/// Environment variable: JELLYFIN_CLIENT_USERNAME
/// </summary>
public string? ClientUsername { get; set; }
/// <summary>
/// Music library ID in Jellyfin (optional, auto-detected if not specified)
/// Environment variable: JELLYFIN_LIBRARY_ID
/// </summary>
public string? LibraryId { get; set; }
/// <summary>
/// Client name reported to Jellyfin
/// </summary>
public string ClientName { get; set; } = "Allstarr";
/// <summary>
/// Client version reported to Jellyfin
/// </summary>
public string ClientVersion { get; set; } = "1.0.0";
/// <summary>
/// Device ID reported to Jellyfin
/// </summary>
public string DeviceId { get; set; } = "allstarrrr-proxy";
/// <summary>
/// Device name reported to Jellyfin
/// </summary>
public string DeviceName { get; set; } = "Allstarr Proxy";
// Shared settings (same as SubsonicSettings)
public ExplicitFilter ExplicitFilter { get; set; } = ExplicitFilter.All;
public DownloadMode DownloadMode { get; set; } = DownloadMode.Track;
public MusicService MusicService { get; set; } = MusicService.SquidWTF;
public StorageMode StorageMode { get; set; } = StorageMode.Permanent;
public int CacheDurationHours { get; set; } = 1;
public bool EnableExternalPlaylists { get; set; } = true;
public string PlaylistsDirectory { get; set; } = "playlists";
}

View File

@@ -0,0 +1,25 @@
namespace allstarr.Models.Settings;
/// <summary>
/// Configuration for the Qobuz downloader and metadata service
/// </summary>
public class QobuzSettings
{
/// <summary>
/// Qobuz user authentication token
/// Obtained from browser's localStorage after logging into play.qobuz.com
/// </summary>
public string? UserAuthToken { get; set; }
/// <summary>
/// Qobuz user ID
/// Obtained from browser's localStorage after logging into play.qobuz.com
/// </summary>
public string? UserId { get; set; }
/// <summary>
/// Preferred audio quality: FLAC, MP3_320, MP3_128
/// If not specified or unavailable, the highest available quality will be used.
/// </summary>
public string? Quality { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace allstarr.Models.Settings;
public class RedisSettings
{
public bool Enabled { get; set; } = true;
public string ConnectionString { get; set; } = "localhost:6379";
}

View File

@@ -0,0 +1,17 @@
namespace allstarr.Models.Settings;
/// <summary>
/// Configuration for the SquidWTF downloader and metadata service
/// </summary>
public class SquidWTFSettings
{
/// <summary>
/// No user auth should be needed for this site.
/// </summary>
/// <summary>
/// Preferred audio quality: FLAC, MP3_320, MP3_128
/// If not specified or unavailable, the highest available quality will be used.
/// </summary>
public string? Quality { get; set; }
}

View File

@@ -0,0 +1,155 @@
namespace allstarr.Models.Settings;
/// <summary>
/// Media server backend type
/// </summary>
public enum BackendType
{
/// <summary>
/// Subsonic-compatible server (Navidrome, Airsonic, etc.)
/// </summary>
Subsonic,
/// <summary>
/// Jellyfin media server
/// </summary>
Jellyfin
}
/// <summary>
/// Download mode for tracks
/// </summary>
public enum DownloadMode
{
/// <summary>
/// Download only the requested track (default behavior)
/// </summary>
Track,
/// <summary>
/// When a track is played, download the entire album in background
/// The requested track is downloaded first, then remaining tracks are queued
/// </summary>
Album
}
/// <summary>
/// Explicit content filter mode for Deezer tracks
/// </summary>
public enum ExplicitFilter
{
/// <summary>
/// Show all tracks (no filtering)
/// </summary>
All,
/// <summary>
/// Exclude clean/edited versions (explicit_content_lyrics == 3)
/// Shows original explicit content and naturally clean content
/// </summary>
ExplicitOnly,
/// <summary>
/// Only show clean content (explicit_content_lyrics == 0 or 3)
/// Excludes tracks with explicit_content_lyrics == 1
/// </summary>
CleanOnly
}
/// <summary>
/// Storage mode for downloaded tracks
/// </summary>
public enum StorageMode
{
/// <summary>
/// Files are permanently stored in the library and registered in the database
/// </summary>
Permanent,
/// <summary>
/// Files are stored in a temporary cache and automatically cleaned up
/// Not registered in the database, no Navidrome scan triggered
/// </summary>
Cache
}
/// <summary>
/// Music service provider
/// </summary>
public enum MusicService
{
/// <summary>
/// Deezer music service
/// </summary>
Deezer,
/// <summary>
/// Qobuz music service
/// </summary>
Qobuz,
/// <summary>
/// SquidWTF music service
/// </summary>
SquidWTF
}
public class SubsonicSettings
{
public string? Url { get; set; }
/// <summary>
/// Explicit content filter mode (default: All)
/// Environment variable: EXPLICIT_FILTER
/// Values: "All", "ExplicitOnly", "CleanOnly"
/// Note: Only works with Deezer
/// </summary>
public ExplicitFilter ExplicitFilter { get; set; } = ExplicitFilter.All;
/// <summary>
/// Download mode for tracks (default: Track)
/// Environment variable: DOWNLOAD_MODE
/// Values: "Track" (download only played track), "Album" (download full album when playing a track)
/// </summary>
public DownloadMode DownloadMode { get; set; } = DownloadMode.Track;
/// <summary>
/// Music service to use (default: Deezer)
/// Environment variable: MUSIC_SERVICE
/// Values: "Deezer", "Qobuz", "SquidWTF"
/// </summary>
public MusicService MusicService { get; set; } = MusicService.SquidWTF;
/// <summary>
/// Storage mode for downloaded files (default: Permanent)
/// Environment variable: STORAGE_MODE
/// Values: "Permanent" (files saved to library), "Cache" (temporary files, auto-cleanup)
/// </summary>
public StorageMode StorageMode { get; set; } = StorageMode.Permanent;
/// <summary>
/// Cache duration in hours for Cache storage mode (default: 1)
/// Environment variable: CACHE_DURATION_HOURS
/// Files older than this duration will be automatically deleted
/// Only applies when StorageMode is Cache
/// </summary>
public int CacheDurationHours { get; set; } = 1;
/// <summary>
/// Enable external playlist search and streaming (default: true)
/// Environment variable: ENABLE_EXTERNAL_PLAYLISTS
/// When enabled, users can search for playlists from the configured music provider
/// Playlists appear as "albums" in search results with genre "Playlist"
/// </summary>
public bool EnableExternalPlaylists { get; set; } = true;
/// <summary>
/// Directory name for storing playlist .m3u files (default: "playlists")
/// Environment variable: PLAYLISTS_DIRECTORY
/// Relative to the music library root directory
/// Playlist files will be stored in {MusicDirectory}/{PlaylistsDirectory}/
/// </summary>
public string PlaylistsDirectory { get; set; } = "playlists";
}

View File

@@ -0,0 +1,58 @@
namespace allstarr.Models.Subsonic;
/// <summary>
/// Represents a playlist from an external music provider (Deezer, Qobuz).
/// </summary>
public class ExternalPlaylist
{
/// <summary>
/// Unique identifier in the format "pl-{provider}-{externalId}"
/// Example: "pl-deezer-123456" or "pl-qobuz-789"
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Playlist name
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Playlist description
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Name of the playlist creator/curator
/// </summary>
public string? CuratorName { get; set; }
/// <summary>
/// Provider name ("deezer" or "qobuz")
/// </summary>
public string Provider { get; set; } = string.Empty;
/// <summary>
/// External ID from the provider (without "pl-" prefix)
/// </summary>
public string ExternalId { get; set; } = string.Empty;
/// <summary>
/// Number of tracks in the playlist
/// </summary>
public int TrackCount { get; set; }
/// <summary>
/// Total duration in seconds
/// </summary>
public int Duration { get; set; }
/// <summary>
/// Cover art URL from the provider
/// </summary>
public string? CoverUrl { get; set; }
/// <summary>
/// Playlist creation date
/// </summary>
public DateTime? CreatedDate { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace allstarr.Models.Subsonic;
/// <summary>
/// Subsonic library scan status
/// </summary>
public class ScanStatus
{
public bool Scanning { get; set; }
public int? Count { get; set; }
}