Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 580e2b6ab8 | |||
| 298b89acf1 | |||
| b6e2675b86 | |||
| 7786501cd1 | |||
| bc4b5a5b17 | |||
| 5d160f71f1 | |||
| 20cf7d49e5 | |||
| 88d22477d5 | |||
| b77def62f4 | |||
| a15313e573 | |||
| 4a90d3f38a | |||
| d4e56567a2 | |||
| 277a7f24fa | |||
| 3735aaf3bd | |||
| 3bbe8553ab | |||
| ca0cfa4524 | |||
| 8b185e964a | |||
| c104a5d8a3 | |||
| 8615cde898 | |||
| 207c0653cc | |||
| de756e5d86 | |||
| fd5db3f7b6 | |||
| d087da9409 | |||
| 43469a7ef2 | |||
| add4af831e | |||
| 4e530ffbc3 | |||
| 14f6776fdc | |||
| da1c6e9171 | |||
| 9c3e934395 | |||
| 15d2c3b465 | |||
| 8aaa6d5cbe | |||
| 9158d0228d | |||
| 2bbcda3320 | |||
| a7622676dd | |||
| 5779f910a2 | |||
| 030f44a444 | |||
| 1248270fb4 | |||
| 413e3b0686 | |||
| ac711efadc | |||
| 59f2fe880a | |||
| 355f2eba2a | |||
| f2f45fa31d | |||
| 042937a8ed | |||
| 674e9af3d0 | |||
| 76d50fab3a | |||
| 81e25d7dab | |||
| 26f26f792a | |||
| 4dfa76b49e | |||
| f511f30ad0 | |||
| a1aa1319ce | |||
| c936bd7dd0 | |||
| 3a60ea2f4e | |||
| 7dba938299 | |||
| 93e77aeb84 | |||
| dd750b95ca | |||
| e42e44f28b | |||
| 67daefdf60 | |||
| fabaf0a3ff | |||
| fb90c73f42 | |||
| c6cf65f075 | |||
| 25de009ebc | |||
| 8918d74bb5 | |||
| f9de8d45d9 | |||
| 48eef0853d | |||
| fc70a912bf | |||
| cd3e5b4b28 | |||
| 482ca82eb4 | |||
| 6d87ae5484 | |||
| bd3e2b999b | |||
| 186196e12b |
@@ -168,7 +168,7 @@ Interested in contributing? Check out the [Contributing Guide](CONTRIBUTING.md)
|
||||
|---|---|---|---|---|
|
||||
| [hifi-api](https://github.com/binimum/hifi-api) | [music.binimum.org](https://music.binimum.org) | [qqdl.site](https://qqdl.site) | [squid.wtf](https://squid.wtf) | [spotisaver.net](https://spotisaver.net) |
|
||||
| [dabmusic.xyz](https://dabmusic.xyz) | [AfkarXYZ](https://github.com/afkarxyz) | [LRCLib](https://lrclib.net) | [Paxsenix](https://lyrics.paxsenix.org) | [Cobalt](https://cobalt.tools) |
|
||||
| [qwkuns.me](https://qwkuns.me) | [SpotubeDL](https://spotubedl.com) | [Song.link](https://song.link) | [IDHS](https://github.com/sjdonado/idonthavespotify) | [Monochrome](https://monochrome.tf) |
|
||||
| [qwkuns.me](https://qwkuns.me) | [SpotubeDL](https://spotubedl.com) | [Song.link](https://song.link) | [IDHS](https://github.com/sjdonado/idonthavespotify) | |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
"name": "SpotiFLAC",
|
||||
"bundleIdentifier": "com.zarzet.spotiflac",
|
||||
"developerName": "zarzet",
|
||||
"version": "4.3.1",
|
||||
"versionDate": "2026-04-14",
|
||||
"downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v4.3.1/SpotiFLAC-v4.3.1-ios-unsigned.ipa",
|
||||
"version": "3.9.0",
|
||||
"versionDate": "2026-03-25",
|
||||
"downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v3.9.0/SpotiFLAC-v3.9.0-ios-unsigned.ipa",
|
||||
"localizedDescription": "Mobile version of SpotiFLAC written in Flutter. Download Tracks in true FLAC from Tidal, Qobuz, & Amazon Music.",
|
||||
"iconURL": "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/main/assets/images/logo.png",
|
||||
"size": 34773598
|
||||
"size": 34477323
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 70 KiB |
@@ -118,16 +118,9 @@ type ExtDownloadResult struct {
|
||||
AlbumArtist string `json:"album_artist,omitempty"`
|
||||
TrackNumber int `json:"track_number,omitempty"`
|
||||
DiscNumber int `json:"disc_number,omitempty"`
|
||||
TotalTracks int `json:"total_tracks,omitempty"`
|
||||
TotalDiscs int `json:"total_discs,omitempty"`
|
||||
ReleaseDate string `json:"release_date,omitempty"`
|
||||
CoverURL string `json:"cover_url,omitempty"`
|
||||
ISRC string `json:"isrc,omitempty"`
|
||||
Genre string `json:"genre,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Copyright string `json:"copyright,omitempty"`
|
||||
Composer string `json:"composer,omitempty"`
|
||||
LyricsLRC string `json:"lyrics_lrc,omitempty"`
|
||||
DecryptionKey string `json:"decryption_key,omitempty"`
|
||||
Decryption *DownloadDecryptionInfo `json:"decryption,omitempty"`
|
||||
}
|
||||
@@ -958,19 +951,6 @@ func isBuiltInDownloadProvider(providerID string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeQualityForBuiltIn(quality string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(quality)) {
|
||||
case "alac", "hi_res_lossless", "lossless":
|
||||
return "HI_RES_LOSSLESS"
|
||||
case "atmos", "ac3", "dolby_atmos":
|
||||
return "LOSSLESS"
|
||||
case "aac", "aac-legacy":
|
||||
return "LOSSLESS"
|
||||
default:
|
||||
return quality
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBuiltInMetadataTrack(track TrackMetadata, providerID string) ExtTrackMetadata {
|
||||
deezerID := ""
|
||||
tidalID := ""
|
||||
@@ -1339,8 +1319,8 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
|
||||
if req.Source != "" &&
|
||||
!isBuiltInProvider(strings.ToLower(req.Source)) &&
|
||||
selectedProvider == req.Source {
|
||||
GoLog("[DownloadWithExtensionFallback] Track source is extension '%s' matching selected provider, trying it first\n", req.Source)
|
||||
(!strictMode || selectedProvider == "" || strings.EqualFold(selectedProvider, req.Source)) {
|
||||
GoLog("[DownloadWithExtensionFallback] Track source is extension '%s', trying it first\n", req.Source)
|
||||
|
||||
ext, err := extManager.GetExtension(req.Source)
|
||||
if err == nil && ext.Enabled && ext.Error == "" && ext.Manifest.IsDownloadProvider() {
|
||||
@@ -1422,12 +1402,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
if result.DiscNumber > 0 {
|
||||
resp.DiscNumber = result.DiscNumber
|
||||
}
|
||||
if result.TotalTracks > 0 {
|
||||
resp.TotalTracks = result.TotalTracks
|
||||
}
|
||||
if result.TotalDiscs > 0 {
|
||||
resp.TotalDiscs = result.TotalDiscs
|
||||
}
|
||||
if result.ReleaseDate != "" {
|
||||
resp.ReleaseDate = result.ReleaseDate
|
||||
}
|
||||
@@ -1437,29 +1411,8 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
if result.ISRC != "" {
|
||||
resp.ISRC = result.ISRC
|
||||
}
|
||||
if result.Genre != "" {
|
||||
resp.Genre = result.Genre
|
||||
}
|
||||
if result.Label != "" {
|
||||
resp.Label = result.Label
|
||||
}
|
||||
if result.Copyright != "" {
|
||||
resp.Copyright = result.Copyright
|
||||
}
|
||||
if result.Composer != "" {
|
||||
resp.Composer = result.Composer
|
||||
}
|
||||
if result.LyricsLRC != "" {
|
||||
resp.LyricsLRC = result.LyricsLRC
|
||||
}
|
||||
}
|
||||
|
||||
if req.TrackName != "" && resp.Title == "" {
|
||||
resp.Title = req.TrackName
|
||||
}
|
||||
if req.ArtistName != "" && resp.Artist == "" {
|
||||
resp.Artist = req.ArtistName
|
||||
}
|
||||
if req.AlbumName != "" && resp.Album == "" {
|
||||
resp.Album = req.AlbumName
|
||||
}
|
||||
@@ -1478,18 +1431,9 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
if req.DiscNumber > 0 && resp.DiscNumber == 0 {
|
||||
resp.DiscNumber = req.DiscNumber
|
||||
}
|
||||
if req.TotalTracks > 0 && resp.TotalTracks == 0 {
|
||||
resp.TotalTracks = req.TotalTracks
|
||||
}
|
||||
if req.TotalDiscs > 0 && resp.TotalDiscs == 0 {
|
||||
resp.TotalDiscs = req.TotalDiscs
|
||||
}
|
||||
if req.CoverURL != "" && resp.CoverURL == "" {
|
||||
resp.CoverURL = req.CoverURL
|
||||
}
|
||||
if req.Composer != "" && resp.Composer == "" {
|
||||
resp.Composer = req.Composer
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -1546,17 +1490,13 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
GoLog("[DownloadWithExtensionFallback] Trying provider: %s\n", providerID)
|
||||
|
||||
if isBuiltInDownloadProvider(providerIDNormalized) {
|
||||
req.OutputExt = ""
|
||||
if (req.Genre == "" || req.Label == "" || req.Copyright == "") &&
|
||||
req.ISRC != "" {
|
||||
GoLog("[DownloadWithExtensionFallback] Enriching extra metadata from ISRC: %s\n", req.ISRC)
|
||||
enrichExtraMetadataByISRC("DownloadWithExtensionFallback", req.ISRC, &req.Genre, &req.Label, &req.Copyright)
|
||||
}
|
||||
|
||||
origQuality := req.Quality
|
||||
req.Quality = normalizeQualityForBuiltIn(req.Quality)
|
||||
result, err := tryBuiltInProvider(providerIDNormalized, req)
|
||||
req.Quality = origQuality
|
||||
if err == nil && result.Success {
|
||||
result.Service = providerIDNormalized
|
||||
if req.Label != "" {
|
||||
@@ -1607,7 +1547,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
continue
|
||||
}
|
||||
|
||||
req.OutputExt = ""
|
||||
outputPath := buildOutputPathForExtension(req, ext)
|
||||
if req.ItemID != "" {
|
||||
StartItemProgress(req.ItemID)
|
||||
@@ -1920,9 +1859,6 @@ func canEmbedGenreLabel(filePath string) bool {
|
||||
if path == "" || strings.HasPrefix(path, "content://") || strings.HasPrefix(path, "/proc/self/fd/") {
|
||||
return false
|
||||
}
|
||||
if strings.ToLower(filepath.Ext(path)) != ".flac" {
|
||||
return false
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -185,10 +185,6 @@ func TestCanEmbedGenreLabelRequiresExistingAbsoluteLocalFile(t *testing.T) {
|
||||
if err := os.WriteFile(tempFile, []byte("fLaC"), 0644); err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
tempM4A := filepath.Join(t.TempDir(), "track.m4a")
|
||||
if err := os.WriteFile(tempM4A, []byte("not-flac"), 0644); err != nil {
|
||||
t.Fatalf("failed to create temp m4a file: %v", err)
|
||||
}
|
||||
|
||||
if canEmbedGenreLabel("relative.flac") {
|
||||
t.Fatal("expected relative path to be rejected")
|
||||
@@ -199,9 +195,6 @@ func TestCanEmbedGenreLabelRequiresExistingAbsoluteLocalFile(t *testing.T) {
|
||||
if canEmbedGenreLabel(filepath.Join(t.TempDir(), "missing.flac")) {
|
||||
t.Fatal("expected missing file to be rejected")
|
||||
}
|
||||
if canEmbedGenreLabel(tempM4A) {
|
||||
t.Fatalf("expected non-FLAC file %q to be rejected", tempM4A)
|
||||
}
|
||||
if !canEmbedGenreLabel(tempFile) {
|
||||
t.Fatalf("expected existing absolute file %q to be accepted", tempFile)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -137,60 +136,12 @@ func newExtensionRuntime(ext *loadedExtension) *extensionRuntime {
|
||||
storageFlushDelay: defaultStorageFlushDelay,
|
||||
}
|
||||
|
||||
runtime.httpClient = newExtensionHTTPClient(ext, jar, extensionHTTPTimeout(ext, 30*time.Second))
|
||||
runtime.httpClient = newExtensionHTTPClient(ext, jar, 30*time.Second)
|
||||
runtime.downloadClient = newExtensionHTTPClient(ext, jar, DownloadTimeout)
|
||||
|
||||
return runtime
|
||||
}
|
||||
|
||||
func extensionHTTPTimeout(ext *loadedExtension, fallback time.Duration) time.Duration {
|
||||
if ext == nil || ext.Manifest == nil || ext.Manifest.Capabilities == nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
raw, ok := ext.Manifest.Capabilities["networkTimeoutSeconds"]
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
|
||||
seconds := parseExtensionTimeoutSeconds(raw)
|
||||
if seconds <= 0 {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if seconds < 5 {
|
||||
seconds = 5
|
||||
}
|
||||
if seconds > 300 {
|
||||
seconds = 300
|
||||
}
|
||||
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
func parseExtensionTimeoutSeconds(raw interface{}) int {
|
||||
switch v := raw.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int32:
|
||||
return int(v)
|
||||
case int64:
|
||||
return int(v)
|
||||
case float32:
|
||||
return int(v)
|
||||
case float64:
|
||||
return int(v)
|
||||
case string:
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(v))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return parsed
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) setActiveDownloadItemID(itemID string) {
|
||||
r.activeDownloadMu.Lock()
|
||||
defer r.activeDownloadMu.Unlock()
|
||||
|
||||
@@ -2655,8 +2655,17 @@ func resolveQobuzTrackForRequest(req DownloadRequest, downloader *QobuzDownloade
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 5: Metadata search with strict matching (duration tolerance: 10 seconds)
|
||||
if track == nil {
|
||||
errMsg := "could not find matching track on Qobuz without identifier match"
|
||||
GoLog("[%s] Trying metadata search: '%s' by '%s'\n", logPrefix, req.TrackName, req.ArtistName)
|
||||
track, err = qobuzSearchTrackByMetadataWithDurationFunc(downloader, req.TrackName, req.ArtistName, expectedDurationSec)
|
||||
if track != nil && !qobuzTrackMatchesRequest(req, track, logPrefix, "metadata search", false) {
|
||||
track = nil
|
||||
}
|
||||
}
|
||||
|
||||
if track == nil {
|
||||
errMsg := "could not find matching track on Qobuz (artist/duration mismatch)"
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
|
||||
@@ -429,9 +429,11 @@ func TestResolveQobuzTrackForRequestRejectsOdesliMismatch(t *testing.T) {
|
||||
t.Fatal("ISRC fallback should not run without an ISRC")
|
||||
return nil, nil
|
||||
}
|
||||
qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, _, _ string, _ int) (*QobuzTrack, error) {
|
||||
t.Fatal("metadata fallback should not run")
|
||||
return nil, nil
|
||||
qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, trackName, artistName string, expectedDurationSec int) (*QobuzTrack, error) {
|
||||
if trackName != "Taste Back" || artistName != "Harry Styles" || expectedDurationSec != 181 {
|
||||
t.Fatalf("unexpected metadata fallback arguments: %q / %q / %d", trackName, artistName, expectedDurationSec)
|
||||
}
|
||||
return testQobuzTrack(444, "Taste Back", "Harry Styles", 181), nil
|
||||
}
|
||||
songLinkCheckTrackAvailabilityFunc = func(_ *SongLinkClient, _, _ string) (*TrackAvailability, error) {
|
||||
t.Fatal("SongLink should not run when Odesli QobuzID is provided")
|
||||
@@ -446,11 +448,11 @@ func TestResolveQobuzTrackForRequestRejectsOdesliMismatch(t *testing.T) {
|
||||
}
|
||||
|
||||
track, err := resolveQobuzTrackForRequest(req, &QobuzDownloader{}, "Test")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got track %+v", track)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if track != nil {
|
||||
t.Fatalf("expected nil track, got %+v", track)
|
||||
if track == nil || track.ID != 444 || track.Title != "Taste Back" {
|
||||
t.Fatalf("unexpected resolved track: %+v", track)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ type TidalTrack struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
ISRC string `json:"isrc"`
|
||||
Copyright string `json:"copyright"`
|
||||
AudioQuality string `json:"audioQuality"`
|
||||
TrackNumber int `json:"trackNumber"`
|
||||
VolumeNumber int `json:"volumeNumber"`
|
||||
@@ -136,7 +135,6 @@ type tidalPublicAlbum struct {
|
||||
Type string `json:"type"`
|
||||
Cover string `json:"cover"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
Copyright string `json:"copyright"`
|
||||
URL string `json:"url"`
|
||||
NumberOfTracks int `json:"numberOfTracks"`
|
||||
Explicit bool `json:"explicit"`
|
||||
@@ -308,29 +306,6 @@ func tidalTrackArtistsDisplay(track *TidalTrack) string {
|
||||
return strings.TrimSpace(track.Artist.Name)
|
||||
}
|
||||
|
||||
func tidalTrackAlbumArtistDisplay(track *TidalTrack) string {
|
||||
if track == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(track.Artists) > 0 {
|
||||
names := make([]string, 0, len(track.Artists))
|
||||
for _, artist := range track.Artists {
|
||||
if strings.ToUpper(strings.TrimSpace(artist.Type)) != "MAIN" {
|
||||
continue
|
||||
}
|
||||
if trimmed := strings.TrimSpace(artist.Name); trimmed != "" {
|
||||
names = append(names, trimmed)
|
||||
}
|
||||
}
|
||||
if len(names) > 0 {
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(track.Artist.Name)
|
||||
}
|
||||
|
||||
func tidalAlbumArtistsDisplay(album *tidalPublicAlbum) string {
|
||||
if album == nil {
|
||||
return ""
|
||||
@@ -379,7 +354,7 @@ func tidalTrackToTrackMetadata(track *TidalTrack) TrackMetadata {
|
||||
Artists: tidalTrackArtistsDisplay(track),
|
||||
Name: strings.TrimSpace(track.Title),
|
||||
AlbumName: strings.TrimSpace(track.Album.Title),
|
||||
AlbumArtist: tidalTrackAlbumArtistDisplay(track),
|
||||
AlbumArtist: strings.TrimSpace(track.Artist.Name),
|
||||
DurationMS: track.Duration * 1000,
|
||||
Images: tidalImageURL(track.Album.Cover, "1280x1280"),
|
||||
ReleaseDate: strings.TrimSpace(track.Album.ReleaseDate),
|
||||
@@ -402,7 +377,7 @@ func tidalTrackToAlbumTrackMetadata(track *TidalTrack) AlbumTrackMetadata {
|
||||
Artists: tidalTrackArtistsDisplay(track),
|
||||
Name: strings.TrimSpace(track.Title),
|
||||
AlbumName: strings.TrimSpace(track.Album.Title),
|
||||
AlbumArtist: tidalTrackAlbumArtistDisplay(track),
|
||||
AlbumArtist: strings.TrimSpace(track.Artist.Name),
|
||||
DurationMS: track.Duration * 1000,
|
||||
Images: tidalImageURL(track.Album.Cover, "1280x1280"),
|
||||
ReleaseDate: strings.TrimSpace(track.Album.ReleaseDate),
|
||||
@@ -432,7 +407,6 @@ func tidalAlbumToAlbumInfo(album *tidalPublicAlbum) AlbumInfoMetadata {
|
||||
Artists: tidalAlbumArtistsDisplay(album),
|
||||
ArtistId: artistID,
|
||||
Images: tidalImageURL(album.Cover, "1280x1280"),
|
||||
Copyright: strings.TrimSpace(album.Copyright),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,10 +688,6 @@ func findTidalArtistPageModule(page *tidalPublicArtistPage, moduleType string) *
|
||||
|
||||
func (t *TidalDownloader) GetAvailableAPIs() []string {
|
||||
return []string{
|
||||
"https://eu-central.monochrome.tf",
|
||||
"https://us-west.monochrome.tf",
|
||||
"https://api.monochrome.tf",
|
||||
"https://monochrome-api.samidy.com",
|
||||
"https://tidal-api.binimum.org",
|
||||
"https://tidal.kinoplus.online",
|
||||
"https://triton.squid.wtf",
|
||||
@@ -1766,7 +1736,6 @@ type TidalDownloadResult struct {
|
||||
TrackNumber int
|
||||
DiscNumber int
|
||||
ISRC string
|
||||
Copyright string
|
||||
LyricsLRC string // LRC content for embedding in converted files
|
||||
}
|
||||
|
||||
@@ -2080,6 +2049,18 @@ func resolveTidalTrackForRequest(req DownloadRequest, downloader *TidalDownloade
|
||||
}
|
||||
}
|
||||
|
||||
if !gotTidalID && req.ISRC != "" {
|
||||
GoLog("[%s] Trying direct Tidal ISRC search: %s\n", logPrefix, req.ISRC)
|
||||
directTrack, directErr := downloader.SearchTrackByISRC(req.ISRC)
|
||||
if directErr == nil && directTrack != nil && directTrack.ID > 0 {
|
||||
trackID = directTrack.ID
|
||||
gotTidalID = true
|
||||
GoLog("[%s] Got Tidal ID %d from direct ISRC search\n", logPrefix, trackID)
|
||||
} else if directErr != nil {
|
||||
GoLog("[%s] Direct Tidal ISRC search failed: %v\n", logPrefix, directErr)
|
||||
}
|
||||
}
|
||||
|
||||
if !gotTidalID && req.ISRC != "" && req.TrackName != "" && req.ArtistName != "" {
|
||||
GoLog("[%s] Trying Tidal public metadata search with ISRC\n", logPrefix)
|
||||
searchTrack, searchErr := downloader.SearchTrackByMetadataWithISRC(
|
||||
@@ -2375,10 +2356,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) {
|
||||
if actualDiscNumber == 0 {
|
||||
actualDiscNumber = track.VolumeNumber
|
||||
}
|
||||
copyright := strings.TrimSpace(req.Copyright)
|
||||
if copyright == "" {
|
||||
copyright = strings.TrimSpace(track.Copyright)
|
||||
}
|
||||
|
||||
metadata := Metadata{
|
||||
Title: req.TrackName,
|
||||
@@ -2394,7 +2371,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) {
|
||||
ISRC: track.ISRC,
|
||||
Genre: req.Genre,
|
||||
Label: req.Label,
|
||||
Copyright: copyright,
|
||||
Copyright: req.Copyright,
|
||||
Composer: req.Composer,
|
||||
}
|
||||
|
||||
@@ -2505,7 +2482,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) {
|
||||
TrackNumber: resultTrackNumber,
|
||||
DiscNumber: resultDiscNumber,
|
||||
ISRC: track.ISRC,
|
||||
Copyright: copyright,
|
||||
LyricsLRC: lyricsLRC,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 22 KiB |
@@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart';
|
||||
/// App version and info constants
|
||||
/// Update version here only - all other files will reference this
|
||||
class AppInfo {
|
||||
static const String version = '4.3.1';
|
||||
static const String buildNumber = '126';
|
||||
static const String version = '4.3.0';
|
||||
static const String buildNumber = '125';
|
||||
static const String fullVersion = '$version+$buildNumber';
|
||||
|
||||
/// Shows "Internal" in debug builds, actual version in release.
|
||||
|
||||
@@ -574,7 +574,6 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
if (trimmed.startsWith('content://')) return true;
|
||||
return trimmed.endsWith('.flac') ||
|
||||
trimmed.endsWith('.m4a') ||
|
||||
trimmed.endsWith('.mp4') ||
|
||||
trimmed.endsWith('.aac') ||
|
||||
trimmed.endsWith('.mp3') ||
|
||||
trimmed.endsWith('.opus') ||
|
||||
@@ -596,7 +595,6 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
!hasResolvedSpecs &&
|
||||
(trimmedPath.endsWith('.flac') ||
|
||||
trimmedPath.endsWith('.m4a') ||
|
||||
trimmedPath.endsWith('.mp4') ||
|
||||
trimmedPath.endsWith('.aac') ||
|
||||
trimmedPath.startsWith('content://'));
|
||||
|
||||
@@ -2361,9 +2359,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final normalized = preferred.startsWith('.')
|
||||
? preferred.toLowerCase()
|
||||
: '.${preferred.toLowerCase()}';
|
||||
if (normalized == '.mp4') {
|
||||
return '.m4a';
|
||||
}
|
||||
const allowed = <String>{'.flac', '.m4a', '.mp3', '.opus'};
|
||||
if (allowed.contains(normalized)) {
|
||||
return normalized;
|
||||
@@ -2374,21 +2369,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _extensionPreservesNativeOutputExt(String service, String ext) {
|
||||
final normalizedService = service.trim().toLowerCase();
|
||||
final normalizedExt = ext.trim().toLowerCase();
|
||||
if (normalizedService.isEmpty || normalizedExt.isEmpty) return false;
|
||||
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
return extensionState.extensions.any(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasDownloadProvider &&
|
||||
ext.id.toLowerCase() == normalizedService &&
|
||||
ext.preservedNativeOutputExtensions.contains(normalizedExt),
|
||||
);
|
||||
}
|
||||
|
||||
String _determineOutputExt(String quality, String service) {
|
||||
final extensionPreferred = _extensionPreferredOutputExt(service);
|
||||
if (extensionPreferred != null) {
|
||||
@@ -2407,7 +2387,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
String _mimeTypeForExt(String ext) {
|
||||
switch (ext.toLowerCase()) {
|
||||
case '.m4a':
|
||||
case '.mp4':
|
||||
return 'audio/mp4';
|
||||
case '.mp3':
|
||||
return 'audio/mpeg';
|
||||
@@ -3710,8 +3689,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
) {
|
||||
final backendTrackNum = _parsePositiveInt(backendResult['track_number']);
|
||||
final backendDiscNum = _parsePositiveInt(backendResult['disc_number']);
|
||||
final backendTotalTracks = _parsePositiveInt(backendResult['total_tracks']);
|
||||
final backendTotalDiscs = _parsePositiveInt(backendResult['total_discs']);
|
||||
final backendYear = normalizeOptionalString(
|
||||
backendResult['release_date'] as String?,
|
||||
);
|
||||
@@ -3739,9 +3716,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
backendIsrc != null ||
|
||||
backendCoverUrl != null ||
|
||||
backendAlbumArtist != null ||
|
||||
backendComposer != null ||
|
||||
backendTotalTracks != null ||
|
||||
backendTotalDiscs != null;
|
||||
backendComposer != null;
|
||||
|
||||
if (!hasOverrides) {
|
||||
return baseTrack;
|
||||
@@ -3760,12 +3735,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
isrc: backendIsrc ?? baseTrack.isrc,
|
||||
trackNumber: backendTrackNum ?? baseTrack.trackNumber,
|
||||
discNumber: backendDiscNum ?? baseTrack.discNumber,
|
||||
totalDiscs: backendTotalDiscs ?? baseTrack.totalDiscs,
|
||||
totalDiscs: baseTrack.totalDiscs,
|
||||
releaseDate: backendYear ?? baseTrack.releaseDate,
|
||||
deezerId: baseTrack.deezerId,
|
||||
availability: baseTrack.availability,
|
||||
albumType: baseTrack.albumType,
|
||||
totalTracks: backendTotalTracks ?? baseTrack.totalTracks,
|
||||
totalTracks: baseTrack.totalTracks,
|
||||
composer: backendComposer ?? baseTrack.composer,
|
||||
source: baseTrack.source,
|
||||
);
|
||||
@@ -4898,10 +4873,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final actualService =
|
||||
((result['service'] as String?)?.toLowerCase()) ??
|
||||
item.service.toLowerCase();
|
||||
final preferredOutputExt = _extensionPreferredOutputExt(actualService);
|
||||
final shouldPreserveNativeM4a =
|
||||
preferredOutputExt == '.m4a' ||
|
||||
_extensionPreservesNativeOutputExt(actualService, '.m4a');
|
||||
final decryptionDescriptor =
|
||||
DownloadDecryptionDescriptor.fromDownloadResult(result);
|
||||
trackToDownload = _buildTrackForMetadataEmbedding(
|
||||
@@ -5027,7 +4998,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final isM4aFile =
|
||||
filePath != null &&
|
||||
(filePath.endsWith('.m4a') ||
|
||||
filePath.endsWith('.mp4') ||
|
||||
(mimeType != null && mimeType.contains('mp4')));
|
||||
final isFlacFile =
|
||||
filePath != null &&
|
||||
@@ -5043,7 +5013,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
|
||||
if (shouldForceTidalSafM4aHandling) {
|
||||
_log.w(
|
||||
'Tidal SAF file is labeled FLAC but backend returned DASH/M4A stream; converting it back to FLAC.',
|
||||
'Tidal SAF file is labeled FLAC but backend returned DASH/M4A stream; preserving it as M4A instead.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5160,7 +5130,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (shouldPreserveNativeM4a) {
|
||||
} else {
|
||||
_log.d('M4A file detected (SAF), preserving native container...');
|
||||
final tempPath = await _copySafToTemp(currentFilePath);
|
||||
if (tempPath != null) {
|
||||
@@ -5218,85 +5188,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_log.d('M4A file detected (SAF), converting to FLAC...');
|
||||
final tempPath = await _copySafToTemp(currentFilePath);
|
||||
if (tempPath != null) {
|
||||
String? flacPath;
|
||||
try {
|
||||
final length = await File(tempPath).length();
|
||||
if (length < 1024) {
|
||||
_log.w('Temp M4A is too small (<1KB), skipping conversion');
|
||||
} else {
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
DownloadStatus.finalizing,
|
||||
progress: 0.95,
|
||||
);
|
||||
flacPath = await FFmpegService.convertM4aToFlac(tempPath);
|
||||
if (flacPath != null) {
|
||||
_log.d('Converted to FLAC (temp): $flacPath');
|
||||
_log.d(
|
||||
'Embedding metadata and cover to converted FLAC...',
|
||||
);
|
||||
final finalTrack = _buildTrackForMetadataEmbedding(
|
||||
trackToDownload,
|
||||
result,
|
||||
resolvedAlbumArtist,
|
||||
);
|
||||
|
||||
final backendGenre = result['genre'] as String?;
|
||||
final backendLabel = result['label'] as String?;
|
||||
final backendCopyright = result['copyright'] as String?;
|
||||
|
||||
await _embedMetadataToFile(
|
||||
flacPath,
|
||||
finalTrack,
|
||||
format: 'flac',
|
||||
genre: backendGenre ?? genre,
|
||||
label: backendLabel ?? label,
|
||||
copyright: backendCopyright,
|
||||
downloadService: item.service,
|
||||
writeExternalLrc: false,
|
||||
);
|
||||
|
||||
final newFileName = '${safBaseName ?? 'track'}.flac';
|
||||
final newUri = await _writeTempToSaf(
|
||||
treeUri: settings.downloadTreeUri,
|
||||
relativeDir: effectiveOutputDir,
|
||||
fileName: newFileName,
|
||||
mimeType: _mimeTypeForExt('.flac'),
|
||||
srcPath: flacPath,
|
||||
);
|
||||
|
||||
if (newUri != null) {
|
||||
if (newUri != currentFilePath) {
|
||||
await _deleteSafFile(currentFilePath);
|
||||
}
|
||||
filePath = newUri;
|
||||
finalSafFileName = newFileName;
|
||||
} else {
|
||||
_log.w('Failed to write FLAC to SAF, keeping M4A');
|
||||
}
|
||||
} else {
|
||||
_log.w(
|
||||
'FFmpeg conversion returned null, keeping M4A file',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w('SAF M4A->FLAC conversion failed: $e');
|
||||
} finally {
|
||||
try {
|
||||
await File(tempPath).delete();
|
||||
} catch (_) {}
|
||||
if (flacPath != null) {
|
||||
try {
|
||||
await File(flacPath).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (quality == 'HIGH') {
|
||||
@@ -5373,7 +5264,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
_log.w('M4A conversion process failed: $e, keeping M4A file');
|
||||
actualQuality = 'AAC 320kbps';
|
||||
}
|
||||
} else if (shouldPreserveNativeM4a) {
|
||||
} else {
|
||||
_log.d('M4A file detected, preserving native container...');
|
||||
|
||||
try {
|
||||
@@ -5382,8 +5273,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
if (!await file.exists()) {
|
||||
_log.e('File does not exist at path: $filePath');
|
||||
} else {
|
||||
if (!(targetPath.toLowerCase().endsWith('.m4a') ||
|
||||
targetPath.toLowerCase().endsWith('.mp4'))) {
|
||||
if (!targetPath.toLowerCase().endsWith('.m4a')) {
|
||||
final renamedPath = targetPath.replaceAll(
|
||||
RegExp(r'\.[^.]+$'),
|
||||
'.m4a',
|
||||
@@ -5428,84 +5318,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
} catch (e) {
|
||||
_log.w('Native M4A handling failed: $e');
|
||||
}
|
||||
} else {
|
||||
_log.d(
|
||||
'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...',
|
||||
);
|
||||
|
||||
try {
|
||||
final file = File(currentFilePath);
|
||||
if (!await file.exists()) {
|
||||
_log.e('File does not exist at path: $filePath');
|
||||
} else {
|
||||
final length = await file.length();
|
||||
_log.i('File size before conversion: ${length / 1024} KB');
|
||||
|
||||
if (length < 1024) {
|
||||
_log.w(
|
||||
'File is too small (<1KB), skipping conversion. Download might be corrupt.',
|
||||
);
|
||||
} else {
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
DownloadStatus.finalizing,
|
||||
progress: 0.95,
|
||||
);
|
||||
final flacPath = await FFmpegService.convertM4aToFlac(
|
||||
currentFilePath,
|
||||
);
|
||||
|
||||
if (flacPath != null) {
|
||||
filePath = flacPath;
|
||||
_log.d('Converted to FLAC: $flacPath');
|
||||
|
||||
_log.d(
|
||||
'Embedding metadata and cover to converted FLAC...',
|
||||
);
|
||||
try {
|
||||
final finalTrack = _buildTrackForMetadataEmbedding(
|
||||
trackToDownload,
|
||||
result,
|
||||
resolvedAlbumArtist,
|
||||
);
|
||||
|
||||
final backendGenre = result['genre'] as String?;
|
||||
final backendLabel = result['label'] as String?;
|
||||
final backendCopyright = result['copyright'] as String?;
|
||||
|
||||
if (backendGenre != null ||
|
||||
backendLabel != null ||
|
||||
backendCopyright != null) {
|
||||
_log.d(
|
||||
'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright',
|
||||
);
|
||||
}
|
||||
|
||||
await _embedMetadataToFile(
|
||||
flacPath,
|
||||
finalTrack,
|
||||
format: 'flac',
|
||||
genre: backendGenre ?? genre,
|
||||
label: backendLabel ?? label,
|
||||
copyright: backendCopyright,
|
||||
downloadService: item.service,
|
||||
);
|
||||
_log.d('Metadata and cover embedded successfully');
|
||||
} catch (e) {
|
||||
_log.w('Warning: Failed to embed metadata/cover: $e');
|
||||
}
|
||||
} else {
|
||||
_log.w(
|
||||
'FFmpeg conversion returned null, keeping M4A file',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w(
|
||||
'FFmpeg conversion process failed: $e, keeping M4A file',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (metadataEmbeddingEnabled &&
|
||||
@@ -5820,15 +5632,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final backendYear = result['release_date'] as String?;
|
||||
final backendTrackNum = result['track_number'] as int?;
|
||||
final backendDiscNum = result['disc_number'] as int?;
|
||||
final backendTotalTracks = result['total_tracks'] as int?;
|
||||
final backendTotalDiscs = result['total_discs'] as int?;
|
||||
final backendBitDepth = result['actual_bit_depth'] as int?;
|
||||
final backendSampleRate = result['actual_sample_rate'] as int?;
|
||||
final backendISRC = result['isrc'] as String?;
|
||||
final backendGenre = result['genre'] as String?;
|
||||
final backendLabel = result['label'] as String?;
|
||||
final backendCopyright = result['copyright'] as String?;
|
||||
final backendComposer = result['composer'] as String?;
|
||||
final effectiveGenre =
|
||||
normalizeOptionalString(backendGenre) ??
|
||||
normalizeOptionalString(genre) ??
|
||||
@@ -5849,7 +5658,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
filePath.startsWith('content://') ||
|
||||
lowerFilePath.endsWith('.flac') ||
|
||||
lowerFilePath.endsWith('.m4a') ||
|
||||
lowerFilePath.endsWith('.mp4') ||
|
||||
lowerFilePath.endsWith('.aac') ||
|
||||
lowerFilePath.endsWith('.mp3') ||
|
||||
lowerFilePath.endsWith('.opus') ||
|
||||
@@ -5937,17 +5745,11 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
trackNumber: (backendTrackNum != null && backendTrackNum > 0)
|
||||
? backendTrackNum
|
||||
: trackToDownload.trackNumber,
|
||||
totalTracks:
|
||||
(backendTotalTracks != null && backendTotalTracks > 0)
|
||||
? backendTotalTracks
|
||||
: trackToDownload.totalTracks,
|
||||
totalTracks: trackToDownload.totalTracks,
|
||||
discNumber: (backendDiscNum != null && backendDiscNum > 0)
|
||||
? backendDiscNum
|
||||
: trackToDownload.discNumber,
|
||||
totalDiscs:
|
||||
(backendTotalDiscs != null && backendTotalDiscs > 0)
|
||||
? backendTotalDiscs
|
||||
: trackToDownload.totalDiscs,
|
||||
totalDiscs: trackToDownload.totalDiscs,
|
||||
duration: trackToDownload.duration,
|
||||
releaseDate: (backendYear != null && backendYear.isNotEmpty)
|
||||
? backendYear
|
||||
@@ -5956,10 +5758,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
bitDepth: historyBitDepth,
|
||||
sampleRate: historySampleRate,
|
||||
genre: effectiveGenre,
|
||||
composer:
|
||||
(backendComposer != null && backendComposer.isNotEmpty)
|
||||
? backendComposer
|
||||
: trackToDownload.composer,
|
||||
composer: trackToDownload.composer,
|
||||
label: effectiveLabel,
|
||||
copyright: effectiveCopyright,
|
||||
),
|
||||
|
||||
@@ -179,20 +179,6 @@ class Extension {
|
||||
final trimmed = value.trim();
|
||||
return trimmed.isEmpty ? null : trimmed;
|
||||
}
|
||||
|
||||
List<String> get preservedNativeOutputExtensions {
|
||||
final value = capabilities['preserveNativeOutputExtensions'];
|
||||
if (value is! List) return const [];
|
||||
|
||||
final normalized = <String>[];
|
||||
for (final item in value) {
|
||||
if (item is! String) continue;
|
||||
final trimmed = item.trim().toLowerCase();
|
||||
if (trimmed.isEmpty) continue;
|
||||
normalized.add(trimmed.startsWith('.') ? trimmed : '.$trimmed');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
class SearchFilter {
|
||||
|
||||
@@ -609,16 +609,17 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull;
|
||||
}
|
||||
resolvedProvider ??= 'tidal';
|
||||
}
|
||||
|
||||
final isEnabledExtensionProvider =
|
||||
resolvedProvider != null &&
|
||||
resolvedProvider.isNotEmpty &&
|
||||
extensionState.extensions.any(
|
||||
(ext) => ext.enabled && ext.id == resolvedProvider,
|
||||
);
|
||||
|
||||
if (resolvedProvider.isNotEmpty &&
|
||||
if (resolvedProvider != null &&
|
||||
resolvedProvider.isNotEmpty &&
|
||||
resolvedProvider != 'tidal' &&
|
||||
resolvedProvider != 'qobuz' &&
|
||||
!isEnabledExtensionProvider &&
|
||||
@@ -638,10 +639,10 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull;
|
||||
resolvedProvider ??= 'tidal';
|
||||
}
|
||||
|
||||
if (resolvedProvider.isNotEmpty &&
|
||||
if (resolvedProvider != null &&
|
||||
resolvedProvider.isNotEmpty &&
|
||||
resolvedProvider != 'tidal' &&
|
||||
resolvedProvider != 'qobuz' &&
|
||||
extensionState.extensions.any(
|
||||
@@ -662,9 +663,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
final effectiveBuiltInProvider =
|
||||
resolvedProvider == 'tidal' || resolvedProvider == 'qobuz'
|
||||
? resolvedProvider
|
||||
: (builtInSearchProvider?.isNotEmpty == true
|
||||
? builtInSearchProvider
|
||||
: 'tidal');
|
||||
: builtInSearchProvider;
|
||||
|
||||
if (effectiveBuiltInProvider == null || effectiveBuiltInProvider.isEmpty) {
|
||||
state = TrackState(
|
||||
|
||||
@@ -480,7 +480,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
))) {
|
||||
return explicit;
|
||||
}
|
||||
return _defaultSearchExtension(extensions)?.id ?? 'tidal';
|
||||
return _defaultSearchExtension(extensions)?.id;
|
||||
}
|
||||
|
||||
String? _sanitizeSearchFilterForProvider(
|
||||
@@ -524,7 +524,8 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
_canonicalSearchFilterId(candidate.label!) ==
|
||||
canonicalFilter) ||
|
||||
(candidate.icon != null &&
|
||||
_canonicalSearchFilterId(candidate.icon!) == canonicalFilter),
|
||||
_canonicalSearchFilterId(candidate.icon!) ==
|
||||
canonicalFilter),
|
||||
)
|
||||
.firstOrNull;
|
||||
return match?.id;
|
||||
@@ -1288,28 +1289,28 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
(hasHomeFeedExtension || hasExploreContent) &&
|
||||
hasExploreContent;
|
||||
|
||||
ref.listen<String>(settingsProvider.select((s) => s.defaultSearchTab), (
|
||||
previous,
|
||||
next,
|
||||
) {
|
||||
if (previous == next) return;
|
||||
final selectedSearchFilter = ref.read(
|
||||
trackProvider.select((s) => s.selectedSearchFilter),
|
||||
);
|
||||
if (selectedSearchFilter != null && selectedSearchFilter.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
ref.listen<String>(
|
||||
settingsProvider.select((s) => s.defaultSearchTab),
|
||||
(previous, next) {
|
||||
if (previous == next) return;
|
||||
final selectedSearchFilter = ref.read(
|
||||
trackProvider.select((s) => s.selectedSearchFilter),
|
||||
);
|
||||
if (selectedSearchFilter != null && selectedSearchFilter.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final text = _urlController.text.trim();
|
||||
if (text.isEmpty || text.length < _minLiveSearchChars) return;
|
||||
if (text.startsWith('http') || text.startsWith('spotify:')) return;
|
||||
final text = _urlController.text.trim();
|
||||
if (text.isEmpty || text.length < _minLiveSearchChars) return;
|
||||
if (text.startsWith('http') || text.startsWith('spotify:')) return;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_lastSearchQuery = null;
|
||||
_performSearch(text);
|
||||
});
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_lastSearchQuery = null;
|
||||
_performSearch(text);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (hasActualResults &&
|
||||
isShowingRecentAccess &&
|
||||
@@ -3553,10 +3554,8 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.toList();
|
||||
final primarySearchExtension = _defaultSearchExtension(searchProviders);
|
||||
final defaultProviderTarget =
|
||||
primarySearchExtension?.displayName ?? 'Tidal';
|
||||
final defaultProviderLabel =
|
||||
'${context.l10n.extensionsHomeFeedAuto} ($defaultProviderTarget)';
|
||||
primarySearchExtension?.displayName ?? 'Deezer';
|
||||
final defaultProviderIconPath = primarySearchExtension?.iconPath;
|
||||
final currentProvider =
|
||||
rawCurrentProvider != null &&
|
||||
|
||||
@@ -4333,40 +4333,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
),
|
||||
|
||||
if (filterMode == 'all' &&
|
||||
totalTrackCount == 0 &&
|
||||
!showFilteringIndicator &&
|
||||
(_activeFilterCount > 0 || unifiedItems.isNotEmpty))
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
if (!_isSelectionMode)
|
||||
_buildFilterButton(context, unifiedItems),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (filterMode == 'singles' &&
|
||||
totalTrackCount == 0 &&
|
||||
!showFilteringIndicator &&
|
||||
(_activeFilterCount > 0 || unifiedItems.isNotEmpty))
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
if (!_isSelectionMode)
|
||||
_buildFilterButton(context, unifiedItems),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (historyItems.isNotEmpty && hasQueueItems)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
|
||||
@@ -742,10 +742,8 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
final rawSearchProvider = settings.searchProvider?.trim() ?? '';
|
||||
final isValidBuiltIn = _builtInProviders.containsKey(rawSearchProvider);
|
||||
final primarySearchExtension = _defaultSearchExtension(extState.extensions);
|
||||
final defaultProviderTarget =
|
||||
primarySearchExtension?.displayName ?? 'Tidal';
|
||||
final defaultProviderLabel =
|
||||
'${context.l10n.extensionsHomeFeedAuto} ($defaultProviderTarget)';
|
||||
primarySearchExtension?.displayName ?? 'Deezer';
|
||||
final searchProvider =
|
||||
isValidBuiltIn ||
|
||||
extState.extensions.any(
|
||||
|
||||
@@ -1978,14 +1978,8 @@ class FFmpegService {
|
||||
break;
|
||||
case 'DATE':
|
||||
vorbis['DATE'] = value;
|
||||
final yearMatch = RegExp(r'^(\d{4})').firstMatch(value);
|
||||
if (yearMatch != null &&
|
||||
(!vorbis.containsKey('YEAR') || vorbis['YEAR']!.isEmpty)) {
|
||||
vorbis['YEAR'] = yearMatch.group(1)!;
|
||||
}
|
||||
break;
|
||||
case 'YEAR':
|
||||
vorbis['YEAR'] = value;
|
||||
if (!vorbis.containsKey('DATE') || vorbis['DATE']!.isEmpty) {
|
||||
vorbis['DATE'] = value;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="#000000" d="M0 0h1024v1024H0z"/><g fill="none" stroke="#1DB954" stroke-linecap="round" stroke-linejoin="round" transform="matrix(.9 0 0 .9 51.2 1.2)"><path stroke-width="38" d="M512 148v592"/><path stroke-width="36.1" d="M422 217.4v241.2m180-241.2v241.2"/><path stroke-width="34.2" d="M341 290v96m342-96v96"/><path stroke-width="38" d="m420 642 92 110 92-110M290 762v90q0 72 72 72h300q72 0 72-72v-90"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 488 B |
@@ -1,7 +1,7 @@
|
||||
name: spotiflac_android
|
||||
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer
|
||||
publish_to: "none"
|
||||
version: 4.3.1+126
|
||||
version: 4.3.0+125
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.0
|
||||
|
||||