mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-04-21 19:16:01 +02:00
fix: preserve composer metadata across qobuz and history
This commit is contained in:
@@ -55,6 +55,7 @@ const (
|
||||
qobuzTrackPlayBaseURL = "https://play.qobuz.com/track/"
|
||||
qobuzStoreBaseURL = "https://www.qobuz.com/us-en"
|
||||
qobuzDownloadAPIURL = "https://dl.musicdl.me/qobuz/download"
|
||||
qobuzZarzDownloadAPIURL = "https://api.zarz.moe/dl/qbz"
|
||||
qobuzDabMusicAPIURL = "https://dabmusic.xyz/api/stream?trackId="
|
||||
qobuzDeebAPIURL = "https://dab.yeet.su/api/stream?trackId="
|
||||
qobuzAfkarAPIURL = "https://qbz.afkarxyz.qzz.io/api/track/"
|
||||
@@ -105,6 +106,10 @@ type QobuzTrack struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"performer"`
|
||||
Composer struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"composer"`
|
||||
}
|
||||
|
||||
type qobuzImageSet struct {
|
||||
@@ -349,6 +354,7 @@ func qobuzTrackToTrackMetadata(track *QobuzTrack) TrackMetadata {
|
||||
AlbumID: qobuzPrefixedID(track.Album.ID),
|
||||
ArtistID: qobuzTrackArtistID(track),
|
||||
AlbumType: qobuzTrackAlbumType(track),
|
||||
Composer: strings.TrimSpace(track.Composer.Name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,6 +379,7 @@ func qobuzTrackToAlbumTrackMetadata(track *QobuzTrack) AlbumTrackMetadata {
|
||||
AlbumID: qobuzPrefixedID(track.Album.ID),
|
||||
AlbumURL: fmt.Sprintf("https://play.qobuz.com/album/%s", strings.TrimSpace(track.Album.ID)),
|
||||
AlbumType: qobuzTrackAlbumType(track),
|
||||
Composer: strings.TrimSpace(track.Composer.Name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,6 +1140,7 @@ func (q *QobuzDownloader) GetArtistMetadata(resourceID string) (*ArtistResponseP
|
||||
func (q *QobuzDownloader) GetAvailableAPIs() []string {
|
||||
return []string{
|
||||
qobuzDownloadAPIURL,
|
||||
qobuzZarzDownloadAPIURL,
|
||||
qobuzDabMusicAPIURL,
|
||||
qobuzDeebAPIURL,
|
||||
qobuzAfkarAPIURL,
|
||||
@@ -1154,6 +1162,7 @@ const (
|
||||
func (q *QobuzDownloader) GetAvailableProviders() []qobuzAPIProvider {
|
||||
return []qobuzAPIProvider{
|
||||
{Name: "musicdl", URL: qobuzDownloadAPIURL, Kind: qobuzAPIKindMusicDL},
|
||||
{Name: "zarz", URL: qobuzZarzDownloadAPIURL, Kind: qobuzAPIKindMusicDL},
|
||||
{Name: "dabmusic", URL: qobuzDabMusicAPIURL, Kind: qobuzAPIKindStandard},
|
||||
{Name: "deeb", URL: qobuzDeebAPIURL, Kind: qobuzAPIKindStandard},
|
||||
{Name: "qbz", URL: qobuzAfkarAPIURL, Kind: qobuzAPIKindStandard},
|
||||
|
||||
@@ -241,12 +241,13 @@ func TestExtractQobuzAlbumIDsFromArtistHTML(t *testing.T) {
|
||||
|
||||
func TestQobuzAvailableProviders(t *testing.T) {
|
||||
providers := NewQobuzDownloader().GetAvailableProviders()
|
||||
if len(providers) != 5 {
|
||||
t.Fatalf("expected 5 Qobuz providers, got %d", len(providers))
|
||||
if len(providers) != 6 {
|
||||
t.Fatalf("expected 6 Qobuz providers, got %d", len(providers))
|
||||
}
|
||||
|
||||
want := map[string]string{
|
||||
"musicdl": qobuzAPIKindMusicDL,
|
||||
"zarz": qobuzAPIKindMusicDL,
|
||||
"dabmusic": qobuzAPIKindStandard,
|
||||
"deeb": qobuzAPIKindStandard,
|
||||
"qbz": qobuzAPIKindStandard,
|
||||
@@ -518,3 +519,37 @@ func TestQobuzTrackMatchesRequest_SongLinkBypassesArtistAndTitle(t *testing.T) {
|
||||
t.Fatal("expected SongLink Qobuz source to bypass artist/title verification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQobuzTrackMetadataIncludesComposer(t *testing.T) {
|
||||
track := &QobuzTrack{
|
||||
ID: 40681594,
|
||||
Title: "Sign of the Times",
|
||||
ISRC: "USSM11703595",
|
||||
Duration: 340,
|
||||
TrackNumber: 1,
|
||||
MediaNumber: 1,
|
||||
}
|
||||
track.Performer.ID = 729886
|
||||
track.Performer.Name = "Harry Styles"
|
||||
track.Composer.ID = 729886
|
||||
track.Composer.Name = "Harry Styles"
|
||||
track.Album.ID = "0886446451985"
|
||||
track.Album.Title = "Harry Styles"
|
||||
track.Album.ReleaseDate = "2017-05-12"
|
||||
track.Album.TracksCount = 10
|
||||
track.Album.ReleaseType = "album"
|
||||
track.Album.ProductType = "album"
|
||||
track.Album.Artist.ID = 729886
|
||||
track.Album.Artist.Name = "Harry Styles"
|
||||
track.Album.Artists = []qobuzArtistRef{{ID: 729886, Name: "Harry Styles"}}
|
||||
|
||||
trackMeta := qobuzTrackToTrackMetadata(track)
|
||||
if trackMeta.Composer != "Harry Styles" {
|
||||
t.Fatalf("track composer = %q", trackMeta.Composer)
|
||||
}
|
||||
|
||||
albumTrackMeta := qobuzTrackToAlbumTrackMetadata(track)
|
||||
if albumTrackMeta.Composer != "Harry Styles" {
|
||||
t.Fatalf("album track composer = %q", albumTrackMeta.Composer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class DownloadHistoryItem {
|
||||
final int? bitDepth;
|
||||
final int? sampleRate;
|
||||
final String? genre;
|
||||
final String? composer;
|
||||
final String? label;
|
||||
final String? copyright;
|
||||
|
||||
@@ -87,6 +88,7 @@ class DownloadHistoryItem {
|
||||
this.bitDepth,
|
||||
this.sampleRate,
|
||||
this.genre,
|
||||
this.composer,
|
||||
this.label,
|
||||
this.copyright,
|
||||
});
|
||||
@@ -116,6 +118,7 @@ class DownloadHistoryItem {
|
||||
'bitDepth': bitDepth,
|
||||
'sampleRate': sampleRate,
|
||||
'genre': genre,
|
||||
'composer': composer,
|
||||
'label': label,
|
||||
'copyright': copyright,
|
||||
};
|
||||
@@ -146,6 +149,7 @@ class DownloadHistoryItem {
|
||||
bitDepth: json['bitDepth'] as int?,
|
||||
sampleRate: json['sampleRate'] as int?,
|
||||
genre: json['genre'] as String?,
|
||||
composer: json['composer'] as String?,
|
||||
label: json['label'] as String?,
|
||||
copyright: json['copyright'] as String?,
|
||||
);
|
||||
@@ -172,6 +176,7 @@ class DownloadHistoryItem {
|
||||
int? bitDepth,
|
||||
int? sampleRate,
|
||||
String? genre,
|
||||
String? composer,
|
||||
String? label,
|
||||
String? copyright,
|
||||
}) {
|
||||
@@ -200,6 +205,7 @@ class DownloadHistoryItem {
|
||||
bitDepth: bitDepth ?? this.bitDepth,
|
||||
sampleRate: sampleRate ?? this.sampleRate,
|
||||
genre: genre ?? this.genre,
|
||||
composer: composer ?? this.composer,
|
||||
label: label ?? this.label,
|
||||
copyright: copyright ?? this.copyright,
|
||||
);
|
||||
@@ -578,12 +584,17 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
trimmedPath.startsWith('content://'));
|
||||
|
||||
if (hasResolvedSpecs && !isPlaceholderQualityLabel(item.quality)) {
|
||||
return false;
|
||||
final needsComposerBackfill =
|
||||
normalizeOptionalString(item.composer) == null;
|
||||
return needsComposerBackfill;
|
||||
}
|
||||
|
||||
final needsComposerBackfill =
|
||||
normalizeOptionalString(item.composer) == null;
|
||||
return needsLosslessSpecProbe ||
|
||||
isPlaceholderQualityLabel(item.quality) ||
|
||||
normalizeOptionalString(item.quality) == null;
|
||||
normalizeOptionalString(item.quality) == null ||
|
||||
needsComposerBackfill;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _probeAudioMetadata(
|
||||
@@ -607,8 +618,12 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
sampleRate: sampleRate,
|
||||
storedQuality: fallbackQuality,
|
||||
);
|
||||
final composer = normalizeOptionalString(result['composer']?.toString());
|
||||
|
||||
if (quality == null && bitDepth == null && sampleRate == null) {
|
||||
if (quality == null &&
|
||||
bitDepth == null &&
|
||||
sampleRate == null &&
|
||||
composer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -616,6 +631,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
'quality': quality,
|
||||
'bitDepth': bitDepth,
|
||||
'sampleRate': sampleRate,
|
||||
'composer': composer,
|
||||
};
|
||||
} catch (e) {
|
||||
_historyLog.d('Audio metadata probe failed for $filePath: $e');
|
||||
@@ -682,6 +698,9 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
);
|
||||
final resolvedBitDepth = probed['bitDepth'] as int?;
|
||||
final resolvedSampleRate = probed['sampleRate'] as int?;
|
||||
final resolvedComposer = normalizeOptionalString(
|
||||
probed['composer'] as String?,
|
||||
);
|
||||
|
||||
final qualityChanged =
|
||||
resolvedQuality != null && resolvedQuality != item.quality;
|
||||
@@ -689,8 +708,13 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
resolvedBitDepth != null && resolvedBitDepth != item.bitDepth;
|
||||
final sampleRateChanged =
|
||||
resolvedSampleRate != null && resolvedSampleRate != item.sampleRate;
|
||||
final composerChanged =
|
||||
resolvedComposer != null && resolvedComposer != item.composer;
|
||||
|
||||
if (!qualityChanged && !bitDepthChanged && !sampleRateChanged) {
|
||||
if (!qualityChanged &&
|
||||
!bitDepthChanged &&
|
||||
!sampleRateChanged &&
|
||||
!composerChanged) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -698,6 +722,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
quality: resolvedQuality,
|
||||
bitDepth: resolvedBitDepth,
|
||||
sampleRate: resolvedSampleRate,
|
||||
composer: resolvedComposer,
|
||||
);
|
||||
updatedItems ??= [...items];
|
||||
updatedItems[index] = updated;
|
||||
@@ -746,6 +771,9 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
genre:
|
||||
normalizeOptionalString(item.genre) ??
|
||||
normalizeOptionalString(existing.genre),
|
||||
composer:
|
||||
normalizeOptionalString(item.composer) ??
|
||||
normalizeOptionalString(existing.composer),
|
||||
label:
|
||||
normalizeOptionalString(item.label) ??
|
||||
normalizeOptionalString(existing.label),
|
||||
@@ -851,6 +879,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
int? discNumber,
|
||||
String? releaseDate,
|
||||
String? genre,
|
||||
String? composer,
|
||||
String? label,
|
||||
String? copyright,
|
||||
}) async {
|
||||
@@ -868,6 +897,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
discNumber: discNumber,
|
||||
releaseDate: releaseDate,
|
||||
genre: genre,
|
||||
composer: composer,
|
||||
label: label,
|
||||
copyright: copyright,
|
||||
);
|
||||
@@ -3224,6 +3254,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final backendAlbumArtist = normalizeOptionalString(
|
||||
backendResult['album_artist'] as String?,
|
||||
);
|
||||
final backendComposer = normalizeOptionalString(
|
||||
backendResult['composer']?.toString(),
|
||||
);
|
||||
|
||||
final hasOverrides =
|
||||
backendTrackNum != null ||
|
||||
@@ -3232,7 +3265,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
backendAlbum != null ||
|
||||
backendIsrc != null ||
|
||||
backendCoverUrl != null ||
|
||||
backendAlbumArtist != null;
|
||||
backendAlbumArtist != null ||
|
||||
backendComposer != null;
|
||||
|
||||
if (!hasOverrides) {
|
||||
return baseTrack;
|
||||
@@ -3257,7 +3291,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
availability: baseTrack.availability,
|
||||
albumType: baseTrack.albumType,
|
||||
totalTracks: baseTrack.totalTracks,
|
||||
composer: baseTrack.composer,
|
||||
composer: backendComposer ?? baseTrack.composer,
|
||||
source: baseTrack.source,
|
||||
);
|
||||
}
|
||||
@@ -5511,6 +5545,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
bitDepth: historyBitDepth,
|
||||
sampleRate: historySampleRate,
|
||||
genre: effectiveGenre,
|
||||
composer: trackToDownload.composer,
|
||||
label: effectiveLabel,
|
||||
copyright: effectiveCopyright,
|
||||
),
|
||||
|
||||
@@ -2770,6 +2770,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
discNumber: discNumber,
|
||||
releaseDate: normalizedOrNull(releaseDate),
|
||||
genre: normalizedOrNull(genre),
|
||||
composer: normalizedOrNull(composer),
|
||||
label: normalizedOrNull(label),
|
||||
copyright: normalizedOrNull(copyright),
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ class HistoryDatabase {
|
||||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 3,
|
||||
version: 4,
|
||||
onConfigure: (db) async {
|
||||
await db.rawQuery('PRAGMA journal_mode = WAL');
|
||||
await db.execute('PRAGMA synchronous = NORMAL');
|
||||
@@ -70,6 +70,7 @@ class HistoryDatabase {
|
||||
bit_depth INTEGER,
|
||||
sample_rate INTEGER,
|
||||
genre TEXT,
|
||||
composer TEXT,
|
||||
label TEXT,
|
||||
copyright TEXT
|
||||
)
|
||||
@@ -98,6 +99,15 @@ class HistoryDatabase {
|
||||
if (oldVersion < 3) {
|
||||
await db.execute('ALTER TABLE history ADD COLUMN saf_repaired INTEGER');
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
final columns = await db.rawQuery('PRAGMA table_info(history)');
|
||||
final hasComposer = columns.any(
|
||||
(row) => (row['name']?.toString().toLowerCase() ?? '') == 'composer',
|
||||
);
|
||||
if (!hasComposer) {
|
||||
await db.execute('ALTER TABLE history ADD COLUMN composer TEXT');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final _iosContainerPattern = RegExp(
|
||||
@@ -265,6 +275,7 @@ class HistoryDatabase {
|
||||
'bit_depth': json['bitDepth'],
|
||||
'sample_rate': json['sampleRate'],
|
||||
'genre': json['genre'],
|
||||
'composer': json['composer'],
|
||||
'label': json['label'],
|
||||
'copyright': json['copyright'],
|
||||
};
|
||||
@@ -296,6 +307,7 @@ class HistoryDatabase {
|
||||
'bitDepth': row['bit_depth'],
|
||||
'sampleRate': row['sample_rate'],
|
||||
'genre': row['genre'],
|
||||
'composer': row['composer'],
|
||||
'label': row['label'],
|
||||
'copyright': row['copyright'],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user