fix: preserve composer metadata across qobuz and history

This commit is contained in:
zarzet
2026-04-06 01:58:36 +07:00
parent cd6a4594fa
commit 7d330fb2ec
5 changed files with 101 additions and 9 deletions
+9
View File
@@ -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},
+37 -2
View File
@@ -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)
}
}
+41 -6
View File
@@ -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,
),
+1
View File
@@ -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),
);
+13 -1
View File
@@ -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'],
};