feat: enrich composer and track totals metadata

This commit is contained in:
zarzet
2026-04-04 18:50:05 +07:00
parent 8aaa6d5cbe
commit 15d2c3b465
26 changed files with 640 additions and 70 deletions
+10 -8
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"io"
"os"
"strconv"
"strings"
)
@@ -367,12 +366,9 @@ func APETagToAudioMetadata(tag *APETag) *AudioMetadata {
case "DATE":
metadata.Date = value
case "TRACK", "TRACKNUMBER":
// APE track format can be "3" or "3/12"
trackNum, _ := strconv.Atoi(strings.Split(value, "/")[0])
metadata.TrackNumber = trackNum
metadata.TrackNumber, metadata.TotalTracks = parseIndexPair(value)
case "DISC", "DISCNUMBER":
discNum, _ := strconv.Atoi(strings.Split(value, "/")[0])
metadata.DiscNumber = discNum
metadata.DiscNumber, metadata.TotalDiscs = parseIndexPair(value)
case "ISRC":
metadata.ISRC = value
case "LYRICS", "UNSYNCEDLYRICS":
@@ -425,10 +421,10 @@ func AudioMetadataToAPEItems(metadata *AudioMetadata) []APETagItem {
addItem("Year", metadata.Year)
}
if metadata.TrackNumber > 0 {
addItem("Track", strconv.Itoa(metadata.TrackNumber))
addItem("Track", formatIndexValue(metadata.TrackNumber, metadata.TotalTracks))
}
if metadata.DiscNumber > 0 {
addItem("Disc", strconv.Itoa(metadata.DiscNumber))
addItem("Disc", formatIndexValue(metadata.DiscNumber, metadata.TotalDiscs))
}
addItem("ISRC", metadata.ISRC)
addItem("Lyrics", metadata.Lyrics)
@@ -484,9 +480,15 @@ func apeKeysFromFields(fields map[string]string) map[string]struct{} {
if _, present := fields["disc_number"]; present {
result["DISCNUMBER"] = struct{}{}
}
if _, present := fields["disc_total"]; present {
result["DISCNUMBER"] = struct{}{}
}
if _, present := fields["track_number"]; present {
result["TRACKNUMBER"] = struct{}{}
}
if _, present := fields["track_total"]; present {
result["TRACKNUMBER"] = struct{}{}
}
if _, present := fields["album_artist"]; present {
result["ALBUMARTIST"] = struct{}{}
}
+27 -11
View File
@@ -21,7 +21,9 @@ type AudioMetadata struct {
Year string
Date string
TrackNumber int
TotalTracks int
DiscNumber int
TotalDiscs int
ISRC string
Lyrics string
Label string
@@ -173,9 +175,9 @@ func parseID3v22Frames(data []byte, metadata *AudioMetadata, tagUnsync bool) {
case "TCO":
metadata.Genre = cleanGenre(value)
case "TRK":
metadata.TrackNumber = parseTrackNumber(value)
metadata.TrackNumber, metadata.TotalTracks = parseIndexPair(value)
case "TPA":
metadata.DiscNumber = parseTrackNumber(value)
metadata.DiscNumber, metadata.TotalDiscs = parseIndexPair(value)
case "TCM":
metadata.Composer = value
case "TPB":
@@ -292,9 +294,9 @@ func parseID3v23Frames(data []byte, metadata *AudioMetadata, version byte, tagUn
case "TCON":
metadata.Genre = cleanGenre(value)
case "TRCK":
metadata.TrackNumber = parseTrackNumber(value)
metadata.TrackNumber, metadata.TotalTracks = parseIndexPair(value)
case "TPOS":
metadata.DiscNumber = parseTrackNumber(value)
metadata.DiscNumber, metadata.TotalDiscs = parseIndexPair(value)
case "TSRC":
metadata.ISRC = value
case "TCOM":
@@ -580,14 +582,28 @@ func cleanGenre(genre string) string {
}
func parseTrackNumber(s string) int {
s = strings.TrimSpace(s)
if idx := strings.Index(s, "/"); idx > 0 {
s = s[:idx]
}
num, _ := strconv.Atoi(s)
num, _ := parseIndexPair(s)
return num
}
func parseIndexPair(s string) (int, int) {
s = strings.TrimSpace(s)
if s == "" {
return 0, 0
}
first := s
second := ""
if idx := strings.Index(s, "/"); idx > 0 {
first = s[:idx]
second = s[idx+1:]
}
num, _ := strconv.Atoi(strings.TrimSpace(first))
total, _ := strconv.Atoi(strings.TrimSpace(second))
return num, total
}
func removeUnsync(data []byte) []byte {
if len(data) == 0 {
return data
@@ -1037,9 +1053,9 @@ func parseVorbisComments(data []byte, metadata *AudioMetadata) {
case "GENRE":
metadata.Genre = value
case "TRACKNUMBER", "TRACK":
metadata.TrackNumber = parseTrackNumber(value)
metadata.TrackNumber, metadata.TotalTracks = parseIndexPair(value)
case "DISCNUMBER", "DISC":
metadata.DiscNumber = parseTrackNumber(value)
metadata.DiscNumber, metadata.TotalDiscs = parseIndexPair(value)
case "ISRC":
metadata.ISRC = value
case "COMPOSER":
+8
View File
@@ -513,6 +513,11 @@ func scanCueSheetForLibrary(cuePath string, sheet *CueSheet, audioPath, virtualP
album = "Unknown Album"
}
composer := track.Composer
if composer == "" {
composer = sheet.Composer
}
var duration int
if i+1 < len(sheet.Tracks) {
nextStart := sheet.Tracks[i+1].StartTime
@@ -539,12 +544,15 @@ func scanCueSheetForLibrary(cuePath string, sheet *CueSheet, audioPath, virtualP
ScannedAt: scanTime,
ISRC: track.ISRC,
TrackNumber: track.Number,
TotalTracks: len(sheet.Tracks),
DiscNumber: 1,
TotalDiscs: 1,
Duration: duration,
ReleaseDate: sheet.Date,
BitDepth: bitDepth,
SampleRate: sampleRate,
Genre: sheet.Genre,
Composer: composer,
Format: "cue+" + strings.TrimPrefix(audioExt, "."),
}
+7
View File
@@ -630,6 +630,12 @@ func (c *DeezerClient) GetAlbum(ctx context.Context, albumID string) (*AlbumResp
}
isrcMap := c.fetchISRCsParallel(ctx, allTracks)
totalDiscs := 0
for _, track := range allTracks {
if track.DiskNumber > totalDiscs {
totalDiscs = track.DiskNumber
}
}
tracks := make([]AlbumTrackMetadata, 0, len(allTracks))
albumType := album.RecordType
@@ -658,6 +664,7 @@ func (c *DeezerClient) GetAlbum(ctx context.Context, albumID string) (*AlbumResp
TrackNumber: trackNum,
TotalTracks: album.NbTracks,
DiscNumber: track.DiskNumber,
TotalDiscs: totalDiscs,
ExternalURL: track.Link,
ISRC: isrc,
AlbumID: fmt.Sprintf("deezer:%d", album.ID),
+2
View File
@@ -369,10 +369,12 @@ func downloadFromDeezer(req DownloadRequest) (DeezerDownloadResult, error) {
TrackNumber: req.TrackNumber,
TotalTracks: req.TotalTracks,
DiscNumber: req.DiscNumber,
TotalDiscs: req.TotalDiscs,
ISRC: req.ISRC,
Genre: req.Genre,
Label: req.Label,
Copyright: req.Copyright,
Composer: req.Composer,
}
var coverData []byte
+94 -2
View File
@@ -55,6 +55,7 @@ type DownloadRequest struct {
TrackNumber int `json:"track_number"`
DiscNumber int `json:"disc_number"`
TotalTracks int `json:"total_tracks"`
TotalDiscs int `json:"total_discs,omitempty"`
ReleaseDate string `json:"release_date"`
ItemID string `json:"item_id"`
DurationMS int `json:"duration_ms"`
@@ -62,6 +63,7 @@ type DownloadRequest struct {
Genre string `json:"genre,omitempty"`
Label string `json:"label,omitempty"`
Copyright string `json:"copyright,omitempty"`
Composer string `json:"composer,omitempty"`
TidalID string `json:"tidal_id,omitempty"`
QobuzID string `json:"qobuz_id,omitempty"`
DeezerID string `json:"deezer_id,omitempty"`
@@ -88,11 +90,14 @@ type DownloadResponse struct {
ReleaseDate string `json:"release_date,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"`
ISRC string `json:"isrc,omitempty"`
CoverURL string `json:"cover_url,omitempty"`
Genre string `json:"genre,omitempty"`
Label string `json:"label,omitempty"`
Copyright string `json:"copyright,omitempty"`
Composer string `json:"composer,omitempty"`
SkipMetadataEnrichment bool `json:"skip_metadata_enrichment,omitempty"`
LyricsLRC string `json:"lyrics_lrc,omitempty"`
DecryptionKey string `json:"decryption_key,omitempty"`
@@ -107,12 +112,15 @@ type DownloadResult struct {
Album string
ReleaseDate string
TrackNumber int
TotalTracks int
DiscNumber int
TotalDiscs int
ISRC string
CoverURL string
Genre string
Label string
Copyright string
Composer string
LyricsLRC string
DecryptionKey string
}
@@ -130,11 +138,14 @@ type reEnrichRequest struct {
AlbumArtist string `json:"album_artist"`
TrackNumber int `json:"track_number"`
DiscNumber int `json:"disc_number"`
TotalTracks int `json:"total_tracks,omitempty"`
TotalDiscs int `json:"total_discs,omitempty"`
ReleaseDate string `json:"release_date"`
ISRC string `json:"isrc"`
Genre string `json:"genre"`
Label string `json:"label"`
Copyright string `json:"copyright"`
Composer string `json:"composer"`
DurationMs int64 `json:"duration_ms"`
SearchOnline bool `json:"search_online"`
UpdateFields []string `json:"update_fields,omitempty"`
@@ -183,9 +194,15 @@ func applyReEnrichTrackMetadata(req *reEnrichRequest, track ExtTrackMetadata) {
if track.TrackNumber > 0 {
req.TrackNumber = track.TrackNumber
}
if track.TotalTracks > 0 {
req.TotalTracks = track.TotalTracks
}
if track.DiscNumber > 0 {
req.DiscNumber = track.DiscNumber
}
if track.TotalDiscs > 0 {
req.TotalDiscs = track.TotalDiscs
}
}
if req.shouldUpdateField("release_info") {
if track.ReleaseDate != "" {
@@ -213,6 +230,9 @@ func applyReEnrichTrackMetadata(req *reEnrichRequest, track ExtTrackMetadata) {
if track.Copyright != "" {
req.Copyright = track.Copyright
}
if track.Composer != "" {
req.Composer = track.Composer
}
}
}
@@ -225,6 +245,11 @@ func reEnrichDownloadRequest(req reEnrichRequest) DownloadRequest {
ISRC: req.ISRC,
DurationMS: int(req.DurationMs),
ArtistTagMode: req.ArtistTagMode,
TrackNumber: req.TrackNumber,
TotalTracks: req.TotalTracks,
DiscNumber: req.DiscNumber,
TotalDiscs: req.TotalDiscs,
Composer: req.Composer,
}
}
@@ -256,13 +281,16 @@ func buildReEnrichFFmpegMetadata(req *reEnrichRequest, lyricsLRC string) map[str
if req.Copyright != "" {
metadata["COPYRIGHT"] = req.Copyright
}
if req.Composer != "" {
metadata["COMPOSER"] = req.Composer
}
}
if req.shouldUpdateField("track_info") {
if req.TrackNumber > 0 {
metadata["TRACKNUMBER"] = fmt.Sprintf("%d", req.TrackNumber)
metadata["TRACKNUMBER"] = formatIndexValue(req.TrackNumber, req.TotalTracks)
}
if req.DiscNumber > 0 {
metadata["DISCNUMBER"] = fmt.Sprintf("%d", req.DiscNumber)
metadata["DISCNUMBER"] = formatIndexValue(req.DiscNumber, req.TotalDiscs)
}
}
if req.shouldUpdateField("lyrics") {
@@ -367,11 +395,14 @@ func extTrackFromTrackMetadata(track *TrackMetadata, providerID string) *ExtTrac
Images: track.Images,
ReleaseDate: track.ReleaseDate,
TrackNumber: track.TrackNumber,
TotalTracks: track.TotalTracks,
DiscNumber: track.DiscNumber,
TotalDiscs: track.TotalDiscs,
ISRC: track.ISRC,
ProviderID: providerID,
DeezerID: deezerID,
SpotifyID: track.SpotifyID,
Composer: track.Composer,
}
}
@@ -533,6 +564,11 @@ func buildDownloadSuccessResponse(
copyright = req.Copyright
}
composer := result.Composer
if composer == "" {
composer = req.Composer
}
coverURL := strings.TrimSpace(result.CoverURL)
if coverURL == "" {
coverURL = strings.TrimSpace(req.CoverURL)
@@ -552,12 +588,15 @@ func buildDownloadSuccessResponse(
AlbumArtist: req.AlbumArtist,
ReleaseDate: releaseDate,
TrackNumber: trackNumber,
TotalTracks: req.TotalTracks,
DiscNumber: discNumber,
TotalDiscs: req.TotalDiscs,
ISRC: isrc,
CoverURL: coverURL,
Genre: genre,
Label: label,
Copyright: copyright,
Composer: composer,
LyricsLRC: result.LyricsLRC,
DecryptionKey: result.DecryptionKey,
}
@@ -1005,7 +1044,9 @@ func ReadFileMetadata(filePath string) (string, error) {
"album_artist": "",
"date": "",
"track_number": 0,
"total_tracks": 0,
"disc_number": 0,
"total_discs": 0,
"isrc": "",
"lyrics": "",
"genre": "",
@@ -1033,7 +1074,9 @@ func ReadFileMetadata(filePath string) (string, error) {
result["date"] = oggMeta.Year
}
result["track_number"] = oggMeta.TrackNumber
result["total_tracks"] = oggMeta.TotalTracks
result["disc_number"] = oggMeta.DiscNumber
result["total_discs"] = oggMeta.TotalDiscs
result["isrc"] = oggMeta.ISRC
result["lyrics"] = oggMeta.Lyrics
result["genre"] = oggMeta.Genre
@@ -1054,7 +1097,9 @@ func ReadFileMetadata(filePath string) (string, error) {
result["album_artist"] = metadata.AlbumArtist
result["date"] = metadata.Date
result["track_number"] = metadata.TrackNumber
result["total_tracks"] = metadata.TotalTracks
result["disc_number"] = metadata.DiscNumber
result["total_discs"] = metadata.TotalDiscs
result["isrc"] = metadata.ISRC
result["lyrics"] = metadata.Lyrics
result["genre"] = metadata.Genre
@@ -1088,7 +1133,9 @@ func ReadFileMetadata(filePath string) (string, error) {
result["date"] = meta.Year
}
result["track_number"] = meta.TrackNumber
result["total_tracks"] = meta.TotalTracks
result["disc_number"] = meta.DiscNumber
result["total_discs"] = meta.TotalDiscs
result["isrc"] = meta.ISRC
result["lyrics"] = meta.Lyrics
result["genre"] = meta.Genre
@@ -1118,7 +1165,9 @@ func ReadFileMetadata(filePath string) (string, error) {
result["date"] = meta.Year
}
result["track_number"] = meta.TrackNumber
result["total_tracks"] = meta.TotalTracks
result["disc_number"] = meta.DiscNumber
result["total_discs"] = meta.TotalDiscs
result["isrc"] = meta.ISRC
result["lyrics"] = meta.Lyrics
result["genre"] = meta.Genre
@@ -1149,7 +1198,9 @@ func ReadFileMetadata(filePath string) (string, error) {
result["date"] = meta.Year
}
result["track_number"] = meta.TrackNumber
result["total_tracks"] = meta.TotalTracks
result["disc_number"] = meta.DiscNumber
result["total_discs"] = meta.TotalDiscs
result["isrc"] = meta.ISRC
result["lyrics"] = meta.Lyrics
result["genre"] = meta.Genre
@@ -1182,7 +1233,9 @@ func ReadFileMetadata(filePath string) (string, error) {
result["date"] = meta.Year
}
result["track_number"] = meta.TrackNumber
result["total_tracks"] = meta.TotalTracks
result["disc_number"] = meta.DiscNumber
result["total_discs"] = meta.TotalDiscs
result["isrc"] = meta.ISRC
result["lyrics"] = meta.Lyrics
result["genre"] = meta.Genre
@@ -1281,13 +1334,21 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
// APE/WV/MPC: write APEv2 tags natively
if isApeFile {
trackNum := 0
totalTracks := 0
discNum := 0
totalDiscs := 0
if v, ok := fields["track_number"]; ok && v != "" {
fmt.Sscanf(v, "%d", &trackNum)
}
if v, ok := fields["track_total"]; ok && v != "" {
fmt.Sscanf(v, "%d", &totalTracks)
}
if v, ok := fields["disc_number"]; ok && v != "" {
fmt.Sscanf(v, "%d", &discNum)
}
if v, ok := fields["disc_total"]; ok && v != "" {
fmt.Sscanf(v, "%d", &totalDiscs)
}
meta := &AudioMetadata{
Title: fields["title"],
@@ -1296,7 +1357,9 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
AlbumArtist: fields["album_artist"],
Date: fields["date"],
TrackNumber: trackNum,
TotalTracks: totalTracks,
DiscNumber: discNum,
TotalDiscs: totalDiscs,
ISRC: fields["isrc"],
Genre: fields["genre"],
Label: fields["label"],
@@ -1930,11 +1993,13 @@ func buildDeezerISRCSearchResult(track *TrackMetadata) map[string]interface{} {
"track_number": track.TrackNumber,
"total_tracks": track.TotalTracks,
"disc_number": track.DiscNumber,
"total_discs": track.TotalDiscs,
"external_urls": track.ExternalURL,
"isrc": track.ISRC,
"album_id": track.AlbumID,
"artist_id": track.ArtistID,
"album_type": track.AlbumType,
"composer": track.Composer,
}
if deezerID := strings.TrimSpace(strings.TrimPrefix(track.SpotifyID, "deezer:")); deezerID != "" {
@@ -2379,7 +2444,9 @@ func ReEnrichFile(requestJSON string) (string, error) {
}
if req.shouldUpdateField("track_info") {
enrichedMeta["track_number"] = req.TrackNumber
enrichedMeta["total_tracks"] = req.TotalTracks
enrichedMeta["disc_number"] = req.DiscNumber
enrichedMeta["total_discs"] = req.TotalDiscs
}
if req.shouldUpdateField("release_info") {
enrichedMeta["release_date"] = req.ReleaseDate
@@ -2392,6 +2459,7 @@ func ReEnrichFile(requestJSON string) (string, error) {
enrichedMeta["genre"] = req.Genre
enrichedMeta["label"] = req.Label
enrichedMeta["copyright"] = req.Copyright
enrichedMeta["composer"] = req.Composer
}
if isFlac {
@@ -2408,7 +2476,9 @@ func ReEnrichFile(requestJSON string) (string, error) {
}
if req.shouldUpdateField("track_info") {
metadata.TrackNumber = req.TrackNumber
metadata.TotalTracks = req.TotalTracks
metadata.DiscNumber = req.DiscNumber
metadata.TotalDiscs = req.TotalDiscs
}
if req.shouldUpdateField("release_info") {
metadata.Date = req.ReleaseDate
@@ -2421,6 +2491,7 @@ func ReEnrichFile(requestJSON string) (string, error) {
metadata.Genre = req.Genre
metadata.Label = req.Label
metadata.Copyright = req.Copyright
metadata.Composer = req.Composer
}
if len(coverDataBytes) > 0 {
@@ -2910,11 +2981,14 @@ func CustomSearchWithExtensionJSON(extensionID, query string, optionsJSON string
"images": track.ResolvedCoverURL(),
"release_date": track.ReleaseDate,
"track_number": track.TrackNumber,
"total_tracks": track.TotalTracks,
"disc_number": track.DiscNumber,
"total_discs": track.TotalDiscs,
"isrc": track.ISRC,
"provider_id": track.ProviderID,
"item_type": track.ItemType,
"album_type": track.AlbumType,
"composer": track.Composer,
}
}
@@ -2981,9 +3055,12 @@ func HandleURLWithExtensionJSON(url string) (string, error) {
"images": result.Track.ResolvedCoverURL(),
"release_date": result.Track.ReleaseDate,
"track_number": result.Track.TrackNumber,
"total_tracks": result.Track.TotalTracks,
"disc_number": result.Track.DiscNumber,
"total_discs": result.Track.TotalDiscs,
"isrc": result.Track.ISRC,
"provider_id": result.Track.ProviderID,
"composer": result.Track.Composer,
}
}
@@ -3000,11 +3077,14 @@ func HandleURLWithExtensionJSON(url string) (string, error) {
"images": track.ResolvedCoverURL(),
"release_date": track.ReleaseDate,
"track_number": track.TrackNumber,
"total_tracks": track.TotalTracks,
"disc_number": track.DiscNumber,
"total_discs": track.TotalDiscs,
"isrc": track.ISRC,
"provider_id": track.ProviderID,
"item_type": track.ItemType,
"album_type": track.AlbumType,
"composer": track.Composer,
}
}
response["tracks"] = tracks
@@ -3090,10 +3170,13 @@ func HandleURLWithExtensionJSON(url string) (string, error) {
"images": track.ResolvedCoverURL(),
"release_date": track.ReleaseDate,
"track_number": track.TrackNumber,
"total_tracks": track.TotalTracks,
"disc_number": track.DiscNumber,
"total_discs": track.TotalDiscs,
"isrc": track.ISRC,
"provider_id": track.ProviderID,
"spotify_id": track.SpotifyID,
"composer": track.Composer,
}
}
artistResponse["top_tracks"] = topTracks
@@ -3163,11 +3246,14 @@ func GetAlbumWithExtensionJSON(extensionID, albumID string) (string, error) {
"cover_url": trackCover,
"release_date": track.ReleaseDate,
"track_number": trackNum,
"total_tracks": track.TotalTracks,
"disc_number": track.DiscNumber,
"total_discs": track.TotalDiscs,
"isrc": track.ISRC,
"provider_id": track.ProviderID,
"item_type": track.ItemType,
"album_type": track.AlbumType,
"composer": track.Composer,
}
}
@@ -3264,11 +3350,14 @@ func GetPlaylistWithExtensionJSON(extensionID, playlistID string) (string, error
"cover_url": trackCover,
"release_date": track.ReleaseDate,
"track_number": track.TrackNumber,
"total_tracks": track.TotalTracks,
"disc_number": track.DiscNumber,
"total_discs": track.TotalDiscs,
"isrc": track.ISRC,
"provider_id": track.ProviderID,
"item_type": track.ItemType,
"album_type": track.AlbumType,
"composer": track.Composer,
}
}
@@ -3375,10 +3464,13 @@ func GetArtistWithExtensionJSON(extensionID, artistID string) (string, error) {
"images": track.ResolvedCoverURL(),
"release_date": track.ReleaseDate,
"track_number": track.TrackNumber,
"total_tracks": track.TotalTracks,
"disc_number": track.DiscNumber,
"total_discs": track.TotalDiscs,
"isrc": track.ISRC,
"provider_id": track.ProviderID,
"spotify_id": track.SpotifyID,
"composer": track.Composer,
}
}
response["top_tracks"] = topTracks
+44
View File
@@ -224,3 +224,47 @@ func TestBuildReEnrichFFmpegMetadataOmitsEmptyFields(t *testing.T) {
}
}
}
func TestApplyReEnrichTrackMetadataCopiesComposerAndTotals(t *testing.T) {
req := reEnrichRequest{}
applyReEnrichTrackMetadata(&req, ExtTrackMetadata{
TrackNumber: 7,
TotalTracks: 12,
DiscNumber: 2,
TotalDiscs: 3,
Composer: "Composer",
})
if req.TrackNumber != 7 || req.TotalTracks != 12 {
t.Fatalf("track metadata = %d/%d", req.TrackNumber, req.TotalTracks)
}
if req.DiscNumber != 2 || req.TotalDiscs != 3 {
t.Fatalf("disc metadata = %d/%d", req.DiscNumber, req.TotalDiscs)
}
if req.Composer != "Composer" {
t.Fatalf("composer = %q", req.Composer)
}
}
func TestBuildReEnrichFFmpegMetadataFormatsTotalsAndComposer(t *testing.T) {
req := reEnrichRequest{
TrackNumber: 7,
TotalTracks: 12,
DiscNumber: 2,
TotalDiscs: 3,
Composer: "Composer",
}
metadata := buildReEnrichFFmpegMetadata(&req, "")
if metadata["TRACKNUMBER"] != "7/12" {
t.Fatalf("TRACKNUMBER = %q", metadata["TRACKNUMBER"])
}
if metadata["DISCNUMBER"] != "2/3" {
t.Fatalf("DISCNUMBER = %q", metadata["DISCNUMBER"])
}
if metadata["COMPOSER"] != "Composer" {
t.Fatalf("COMPOSER = %q", metadata["COMPOSER"])
}
}
+36
View File
@@ -26,7 +26,9 @@ type ExtTrackMetadata struct {
Images string `json:"images,omitempty"`
ReleaseDate string `json:"release_date,omitempty"`
TrackNumber int `json:"track_number,omitempty"`
TotalTracks int `json:"total_tracks,omitempty"`
DiscNumber int `json:"disc_number,omitempty"`
TotalDiscs int `json:"total_discs,omitempty"`
ISRC string `json:"isrc,omitempty"`
ProviderID string `json:"provider_id"`
ItemType string `json:"item_type,omitempty"`
@@ -41,6 +43,7 @@ type ExtTrackMetadata struct {
Label string `json:"label,omitempty"`
Copyright string `json:"copyright,omitempty"`
Genre string `json:"genre,omitempty"`
Composer string `json:"composer,omitempty"`
}
func (t *ExtTrackMetadata) ResolvedCoverURL() string {
@@ -775,7 +778,9 @@ func normalizeBuiltInMetadataTrack(track TrackMetadata, providerID string) ExtTr
Images: track.Images,
ReleaseDate: track.ReleaseDate,
TrackNumber: track.TrackNumber,
TotalTracks: track.TotalTracks,
DiscNumber: track.DiscNumber,
TotalDiscs: track.TotalDiscs,
ISRC: track.ISRC,
ProviderID: providerID,
SpotifyID: prefixedID,
@@ -783,6 +788,7 @@ func normalizeBuiltInMetadataTrack(track TrackMetadata, providerID string) ExtTr
TidalID: tidalID,
QobuzID: qobuzID,
AlbumType: track.AlbumType,
Composer: track.Composer,
}
}
@@ -975,8 +981,11 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
ISRC: req.ISRC,
ReleaseDate: req.ReleaseDate,
TrackNumber: req.TrackNumber,
TotalTracks: req.TotalTracks,
DiscNumber: req.DiscNumber,
TotalDiscs: req.TotalDiscs,
ProviderID: req.Source,
Composer: req.Composer,
}
enrichedTrack, err := provider.EnrichTrack(trackMeta)
@@ -1041,10 +1050,22 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
GoLog("[DownloadWithExtensionFallback] TrackNumber from enrichment: %d\n", enrichedTrack.TrackNumber)
req.TrackNumber = enrichedTrack.TrackNumber
}
if enrichedTrack.TotalTracks > 0 && req.TotalTracks == 0 {
GoLog("[DownloadWithExtensionFallback] TotalTracks from enrichment: %d\n", enrichedTrack.TotalTracks)
req.TotalTracks = enrichedTrack.TotalTracks
}
if enrichedTrack.DiscNumber > 0 && req.DiscNumber == 0 {
GoLog("[DownloadWithExtensionFallback] DiscNumber from enrichment: %d\n", enrichedTrack.DiscNumber)
req.DiscNumber = enrichedTrack.DiscNumber
}
if enrichedTrack.TotalDiscs > 0 && req.TotalDiscs == 0 {
GoLog("[DownloadWithExtensionFallback] TotalDiscs from enrichment: %d\n", enrichedTrack.TotalDiscs)
req.TotalDiscs = enrichedTrack.TotalDiscs
}
if enrichedTrack.Composer != "" && req.Composer == "" {
GoLog("[DownloadWithExtensionFallback] Composer from enrichment: %s\n", enrichedTrack.Composer)
req.Composer = enrichedTrack.Composer
}
}
}
}
@@ -1077,9 +1098,18 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
if track.TrackNumber > 0 && req.TrackNumber == 0 {
req.TrackNumber = track.TrackNumber
}
if track.TotalTracks > 0 && req.TotalTracks == 0 {
req.TotalTracks = track.TotalTracks
}
if track.DiscNumber > 0 && req.DiscNumber == 0 {
req.DiscNumber = track.DiscNumber
}
if track.TotalDiscs > 0 && req.TotalDiscs == 0 {
req.TotalDiscs = track.TotalDiscs
}
if track.Composer != "" && req.Composer == "" {
req.Composer = track.Composer
}
if track.CoverURL != "" && req.CoverURL == "" {
req.CoverURL = track.CoverURL
}
@@ -1594,12 +1624,15 @@ func buildOutputPath(req DownloadRequest) string {
"album_artist": req.AlbumArtist,
"track": req.TrackNumber,
"track_number": req.TrackNumber,
"total_tracks": req.TotalTracks,
"disc": req.DiscNumber,
"disc_number": req.DiscNumber,
"total_discs": req.TotalDiscs,
"year": extractYear(req.ReleaseDate),
"date": req.ReleaseDate,
"release_date": req.ReleaseDate,
"isrc": req.ISRC,
"composer": req.Composer,
}
filename := buildFilenameFromTemplate(req.FilenameFormat, metadata)
@@ -1644,12 +1677,15 @@ func buildOutputPathForExtension(req DownloadRequest, ext *LoadedExtension) stri
"album_artist": req.AlbumArtist,
"track": req.TrackNumber,
"track_number": req.TrackNumber,
"total_tracks": req.TotalTracks,
"disc": req.DiscNumber,
"disc_number": req.DiscNumber,
"total_discs": req.TotalDiscs,
"year": extractYear(req.ReleaseDate),
"date": req.ReleaseDate,
"release_date": req.ReleaseDate,
"isrc": req.ISRC,
"composer": req.Composer,
}
filename := buildFilenameFromTemplate(req.FilenameFormat, metadata)
+18
View File
@@ -24,13 +24,16 @@ type LibraryScanResult struct {
FileModTime int64 `json:"fileModTime,omitempty"` // Unix timestamp in milliseconds
ISRC string `json:"isrc,omitempty"`
TrackNumber int `json:"trackNumber,omitempty"`
TotalTracks int `json:"totalTracks,omitempty"`
DiscNumber int `json:"discNumber,omitempty"`
TotalDiscs int `json:"totalDiscs,omitempty"`
Duration int `json:"duration,omitempty"`
ReleaseDate string `json:"releaseDate,omitempty"`
BitDepth int `json:"bitDepth,omitempty"`
SampleRate int `json:"sampleRate,omitempty"`
Bitrate int `json:"bitrate,omitempty"` // kbps, for lossy formats (MP3, Opus, Vorbis)
Genre string `json:"genre,omitempty"`
Composer string `json:"composer,omitempty"`
Label string `json:"label,omitempty"`
Copyright string `json:"copyright,omitempty"`
Format string `json:"format,omitempty"`
@@ -367,9 +370,12 @@ func scanFLACFile(filePath string, result *LibraryScanResult, displayNameHint st
result.AlbumArtist = metadata.AlbumArtist
result.ISRC = metadata.ISRC
result.TrackNumber = metadata.TrackNumber
result.TotalTracks = metadata.TotalTracks
result.DiscNumber = metadata.DiscNumber
result.TotalDiscs = metadata.TotalDiscs
result.ReleaseDate = metadata.Date
result.Genre = metadata.Genre
result.Composer = metadata.Composer
result.Label = metadata.Label
result.Copyright = metadata.Copyright
@@ -401,12 +407,15 @@ func scanM4AFile(filePath string, result *LibraryScanResult, displayNameHint str
result.AlbumArtist = metadata.AlbumArtist
result.ISRC = metadata.ISRC
result.TrackNumber = metadata.TrackNumber
result.TotalTracks = metadata.TotalTracks
result.DiscNumber = metadata.DiscNumber
result.TotalDiscs = metadata.TotalDiscs
result.ReleaseDate = metadata.Date
if result.ReleaseDate == "" {
result.ReleaseDate = metadata.Year
}
result.Genre = metadata.Genre
result.Composer = metadata.Composer
result.Label = metadata.Label
result.Copyright = metadata.Copyright
}
@@ -433,7 +442,9 @@ func scanMP3File(filePath string, result *LibraryScanResult, displayNameHint str
result.AlbumName = metadata.Album
result.AlbumArtist = metadata.AlbumArtist
result.TrackNumber = metadata.TrackNumber
result.TotalTracks = metadata.TotalTracks
result.DiscNumber = metadata.DiscNumber
result.TotalDiscs = metadata.TotalDiscs
result.Genre = metadata.Genre
if metadata.Date != "" {
result.ReleaseDate = metadata.Date
@@ -441,6 +452,7 @@ func scanMP3File(filePath string, result *LibraryScanResult, displayNameHint str
result.ReleaseDate = metadata.Year
}
result.ISRC = metadata.ISRC
result.Composer = metadata.Composer
result.Label = metadata.Label
result.Copyright = metadata.Copyright
@@ -472,9 +484,12 @@ func scanOggFile(filePath string, result *LibraryScanResult, displayNameHint str
result.AlbumArtist = metadata.AlbumArtist
result.ISRC = metadata.ISRC
result.TrackNumber = metadata.TrackNumber
result.TotalTracks = metadata.TotalTracks
result.DiscNumber = metadata.DiscNumber
result.TotalDiscs = metadata.TotalDiscs
result.Genre = metadata.Genre
result.ReleaseDate = metadata.Date
result.Composer = metadata.Composer
result.Label = metadata.Label
result.Copyright = metadata.Copyright
@@ -511,13 +526,16 @@ func scanAPEFile(filePath string, result *LibraryScanResult, displayNameHint str
result.AlbumArtist = metadata.AlbumArtist
result.ISRC = metadata.ISRC
result.TrackNumber = metadata.TrackNumber
result.TotalTracks = metadata.TotalTracks
result.DiscNumber = metadata.DiscNumber
result.TotalDiscs = metadata.TotalDiscs
result.Genre = metadata.Genre
if metadata.Date != "" {
result.ReleaseDate = metadata.Date
} else {
result.ReleaseDate = metadata.Year
}
result.Composer = metadata.Composer
result.Label = metadata.Label
result.Copyright = metadata.Copyright
+70 -25
View File
@@ -110,6 +110,7 @@ type Metadata struct {
TrackNumber int
TotalTracks int
DiscNumber int
TotalDiscs int
ISRC string
Description string
Lyrics string
@@ -273,23 +274,23 @@ func ReadMetadata(filePath string) (*Metadata, error) {
trackNum := getComment(cmt, "TRACKNUMBER")
if trackNum != "" {
fmt.Sscanf(trackNum, "%d", &metadata.TrackNumber)
metadata.TrackNumber, metadata.TotalTracks = parseIndexPair(trackNum)
}
if metadata.TrackNumber == 0 {
trackNum = getComment(cmt, "TRACK")
if trackNum != "" {
fmt.Sscanf(trackNum, "%d", &metadata.TrackNumber)
metadata.TrackNumber, metadata.TotalTracks = parseIndexPair(trackNum)
}
}
discNum := getComment(cmt, "DISCNUMBER")
if discNum != "" {
fmt.Sscanf(discNum, "%d", &metadata.DiscNumber)
metadata.DiscNumber, metadata.TotalDiscs = parseIndexPair(discNum)
}
if metadata.DiscNumber == 0 {
discNum = getComment(cmt, "DISC")
if discNum != "" {
fmt.Sscanf(discNum, "%d", &metadata.DiscNumber)
metadata.DiscNumber, metadata.TotalDiscs = parseIndexPair(discNum)
}
}
@@ -403,26 +404,39 @@ func EditFlacFields(filePath string, fields map[string]string) error {
removeCommentKey(cmt, "ALBUM_ARTIST")
}
// Track/disc numbers: present + empty → clear; present + "0" → clear.
if v, ok := fields["track_number"]; ok {
trackNum := 0
if v != "" {
fmt.Sscanf(v, "%d", &trackNum)
// Track/disc numbers: present + empty → clear; when only totals are edited,
// preserve the current index number and rewrite the combined value.
if _, ok := fields["track_number"]; ok || fields["track_total"] != "" || hasMapKey(fields, "track_total") {
currentTrackNum, currentTotalTracks := parseIndexPair(getComment(cmt, "TRACKNUMBER"))
if currentTrackNum == 0 && currentTotalTracks == 0 {
currentTrackNum, currentTotalTracks = parseIndexPair(getComment(cmt, "TRACK"))
}
if trackNum > 0 {
setOrClearComment(cmt, "TRACKNUMBER", strconv.Itoa(trackNum))
if v, ok := fields["track_number"]; ok {
currentTrackNum = parsePositiveInt(v)
}
if v, ok := fields["track_total"]; ok {
currentTotalTracks = parsePositiveInt(v)
}
if currentTrackNum > 0 {
setOrClearComment(cmt, "TRACKNUMBER", formatIndexValue(currentTrackNum, currentTotalTracks))
} else {
removeCommentKey(cmt, "TRACKNUMBER")
}
removeCommentKey(cmt, "TRACK") // alias
}
if v, ok := fields["disc_number"]; ok {
discNum := 0
if v != "" {
fmt.Sscanf(v, "%d", &discNum)
if _, ok := fields["disc_number"]; ok || fields["disc_total"] != "" || hasMapKey(fields, "disc_total") {
currentDiscNum, currentTotalDiscs := parseIndexPair(getComment(cmt, "DISCNUMBER"))
if currentDiscNum == 0 && currentTotalDiscs == 0 {
currentDiscNum, currentTotalDiscs = parseIndexPair(getComment(cmt, "DISC"))
}
if discNum > 0 {
setOrClearComment(cmt, "DISCNUMBER", strconv.Itoa(discNum))
if v, ok := fields["disc_number"]; ok {
currentDiscNum = parsePositiveInt(v)
}
if v, ok := fields["disc_total"]; ok {
currentTotalDiscs = parsePositiveInt(v)
}
if currentDiscNum > 0 {
setOrClearComment(cmt, "DISCNUMBER", formatIndexValue(currentDiscNum, currentTotalDiscs))
} else {
removeCommentKey(cmt, "DISCNUMBER")
}
@@ -478,15 +492,11 @@ func writeVorbisMetadata(cmt *flacvorbis.MetaDataBlockVorbisComment, metadata Me
setComment(cmt, "DATE", metadata.Date)
if metadata.TrackNumber > 0 {
if metadata.TotalTracks > 0 {
setComment(cmt, "TRACKNUMBER", fmt.Sprintf("%d/%d", metadata.TrackNumber, metadata.TotalTracks))
} else {
setComment(cmt, "TRACKNUMBER", strconv.Itoa(metadata.TrackNumber))
}
setComment(cmt, "TRACKNUMBER", formatIndexValue(metadata.TrackNumber, metadata.TotalTracks))
}
if metadata.DiscNumber > 0 {
setComment(cmt, "DISCNUMBER", strconv.Itoa(metadata.DiscNumber))
setComment(cmt, "DISCNUMBER", formatIndexValue(metadata.DiscNumber, metadata.TotalDiscs))
}
if metadata.ISRC != "" {
@@ -953,9 +963,9 @@ func ReadM4ATags(filePath string) (*AudioMetadata, error) {
case "\xa9lyr":
metadata.Lyrics, _ = readM4ATextValue(f, header, fi.Size())
case "trkn":
metadata.TrackNumber, _ = readM4AIndexValue(f, header, fi.Size())
metadata.TrackNumber, metadata.TotalTracks, _ = readM4AIndexPair(f, header, fi.Size())
case "disk":
metadata.DiscNumber, _ = readM4AIndexValue(f, header, fi.Size())
metadata.DiscNumber, metadata.TotalDiscs, _ = readM4AIndexPair(f, header, fi.Size())
case "----":
name, value, freeformErr := readM4AFreeformValue(f, header, fi.Size())
if freeformErr == nil {
@@ -1150,6 +1160,41 @@ func readM4AIndexValue(f *os.File, parent atomHeader, fileSize int64) (int, erro
return int(binary.BigEndian.Uint16(payload[2:4])), nil
}
func readM4AIndexPair(f *os.File, parent atomHeader, fileSize int64) (int, int, error) {
payload, err := readM4ADataPayload(f, parent, fileSize)
if err != nil {
return 0, 0, err
}
if len(payload) < 6 {
return 0, 0, fmt.Errorf("index payload too short in %s", parent.typ)
}
return int(binary.BigEndian.Uint16(payload[2:4])), int(binary.BigEndian.Uint16(payload[4:6])), nil
}
func parsePositiveInt(value string) int {
value = strings.TrimSpace(value)
if value == "" {
return 0
}
n, _ := strconv.Atoi(value)
return n
}
func formatIndexValue(number, total int) string {
if number <= 0 {
return ""
}
if total > 0 {
return fmt.Sprintf("%d/%d", number, total)
}
return strconv.Itoa(number)
}
func hasMapKey(fields map[string]string, key string) bool {
_, ok := fields[key]
return ok
}
func readM4AFreeformValue(f *os.File, parent atomHeader, fileSize int64) (string, string, error) {
start := parent.offset + parent.headerSize
end := parent.offset + parent.size
+4
View File
@@ -23,11 +23,13 @@ type TrackMetadata struct {
TrackNumber int `json:"track_number"`
TotalTracks int `json:"total_tracks,omitempty"`
DiscNumber int `json:"disc_number,omitempty"`
TotalDiscs int `json:"total_discs,omitempty"`
ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"`
AlbumID string `json:"album_id,omitempty"`
ArtistID string `json:"artist_id,omitempty"`
AlbumType string `json:"album_type,omitempty"`
Composer string `json:"composer,omitempty"`
}
type AlbumTrackMetadata struct {
@@ -42,11 +44,13 @@ type AlbumTrackMetadata struct {
TrackNumber int `json:"track_number"`
TotalTracks int `json:"total_tracks,omitempty"`
DiscNumber int `json:"disc_number,omitempty"`
TotalDiscs int `json:"total_discs,omitempty"`
ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"`
AlbumID string `json:"album_id,omitempty"`
AlbumURL string `json:"album_url,omitempty"`
AlbumType string `json:"album_type,omitempty"`
Composer string `json:"composer,omitempty"`
}
type AlbumInfoMetadata struct {
+9
View File
@@ -1030,6 +1030,7 @@ func (q *QobuzDownloader) GetAlbumMetadata(resourceID string) (*AlbumResponsePay
}
tracks := make([]AlbumTrackMetadata, 0, len(album.Tracks.Items))
totalDiscs := 0
for i := range album.Tracks.Items {
track := &album.Tracks.Items[i]
track.Album.ID = album.ID
@@ -1041,8 +1042,14 @@ func (q *QobuzDownloader) GetAlbumMetadata(resourceID string) (*AlbumResponsePay
Large: album.Image.Large,
}
track.Album.TracksCount = album.TracksCount
if track.MediaNumber > totalDiscs {
totalDiscs = track.MediaNumber
}
tracks = append(tracks, qobuzTrackToAlbumTrackMetadata(track))
}
for i := range tracks {
tracks[i].TotalDiscs = totalDiscs
}
return &AlbumResponsePayload{
AlbumInfo: qobuzAlbumToAlbumInfo(album),
@@ -2793,10 +2800,12 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) {
TrackNumber: actualTrackNumber,
TotalTracks: req.TotalTracks,
DiscNumber: req.DiscNumber,
TotalDiscs: req.TotalDiscs,
ISRC: track.ISRC,
Genre: req.Genre,
Label: req.Label,
Copyright: req.Copyright,
Composer: req.Composer,
}
var coverData []byte
+9
View File
@@ -1012,6 +1012,7 @@ func (t *TidalDownloader) GetAlbumMetadata(resourceID string) (*AlbumResponsePay
}
tracks := make([]AlbumTrackMetadata, 0, len(itemsModule.PagedList.Items))
totalDiscs := 0
for _, item := range itemsModule.PagedList.Items {
track := item.Item
track.Album.ID = headerModule.Album.ID
@@ -1019,8 +1020,14 @@ func (t *TidalDownloader) GetAlbumMetadata(resourceID string) (*AlbumResponsePay
track.Album.Cover = headerModule.Album.Cover
track.Album.ReleaseDate = headerModule.Album.ReleaseDate
track.Album.URL = headerModule.Album.URL
if track.VolumeNumber > totalDiscs {
totalDiscs = track.VolumeNumber
}
tracks = append(tracks, tidalTrackToAlbumTrackMetadata(&track))
}
for i := range tracks {
tracks[i].TotalDiscs = totalDiscs
}
return &AlbumResponsePayload{
AlbumInfo: tidalAlbumToAlbumInfo(&headerModule.Album),
@@ -2360,10 +2367,12 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) {
TrackNumber: actualTrackNumber,
TotalTracks: req.TotalTracks,
DiscNumber: actualDiscNumber,
TotalDiscs: req.TotalDiscs,
ISRC: track.ISRC,
Genre: req.Genre,
Label: req.Label,
Copyright: req.Copyright,
Composer: req.Composer,
}
var coverData []byte
+4
View File
@@ -16,12 +16,14 @@ class Track {
final int duration;
final int? trackNumber;
final int? discNumber;
final int? totalDiscs;
final String? releaseDate;
final String? deezerId;
final ServiceAvailability? availability;
final String? source;
final String? albumType;
final int? totalTracks;
final String? composer;
final String? itemType;
const Track({
@@ -37,12 +39,14 @@ class Track {
required this.duration,
this.trackNumber,
this.discNumber,
this.totalDiscs,
this.releaseDate,
this.deezerId,
this.availability,
this.source,
this.albumType,
this.totalTracks,
this.composer,
this.itemType,
});
+4
View File
@@ -19,6 +19,7 @@ Track _$TrackFromJson(Map<String, dynamic> json) => Track(
duration: (json['duration'] as num).toInt(),
trackNumber: (json['trackNumber'] as num?)?.toInt(),
discNumber: (json['discNumber'] as num?)?.toInt(),
totalDiscs: (json['totalDiscs'] as num?)?.toInt(),
releaseDate: json['releaseDate'] as String?,
deezerId: json['deezerId'] as String?,
availability: json['availability'] == null
@@ -29,6 +30,7 @@ Track _$TrackFromJson(Map<String, dynamic> json) => Track(
source: json['source'] as String?,
albumType: json['albumType'] as String?,
totalTracks: (json['totalTracks'] as num?)?.toInt(),
composer: json['composer'] as String?,
itemType: json['itemType'] as String?,
);
@@ -45,12 +47,14 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
'duration': instance.duration,
'trackNumber': instance.trackNumber,
'discNumber': instance.discNumber,
'totalDiscs': instance.totalDiscs,
'releaseDate': instance.releaseDate,
'deezerId': instance.deezerId,
'availability': instance.availability,
'source': instance.source,
'albumType': instance.albumType,
'totalTracks': instance.totalTracks,
'composer': instance.composer,
'itemType': instance.itemType,
};
+73 -8
View File
@@ -3251,11 +3251,13 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
isrc: backendIsrc ?? baseTrack.isrc,
trackNumber: backendTrackNum ?? baseTrack.trackNumber,
discNumber: backendDiscNum ?? baseTrack.discNumber,
totalDiscs: baseTrack.totalDiscs,
releaseDate: backendYear ?? baseTrack.releaseDate,
deezerId: baseTrack.deezerId,
availability: baseTrack.availability,
albumType: baseTrack.albumType,
totalTracks: baseTrack.totalTracks,
composer: baseTrack.composer,
source: baseTrack.source,
);
}
@@ -3329,17 +3331,25 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
'ARTIST': track.artistName,
'ALBUM': track.albumName,
};
String formatIndexTag(int number, int? total) {
if (total != null && total > 0) {
return '$number/$total';
}
return number.toString();
}
final albumArtist = _resolveAlbumArtistForMetadata(track, settings);
metadata['ALBUMARTIST'] = albumArtist;
if (track.trackNumber != null && track.trackNumber! > 0) {
metadata['TRACKNUMBER'] = track.trackNumber.toString();
if (isFlac || isMp3) metadata['TRACK'] = track.trackNumber.toString();
final trackTag = formatIndexTag(track.trackNumber!, track.totalTracks);
metadata['TRACKNUMBER'] = trackTag;
if (isFlac || isMp3) metadata['TRACK'] = trackTag;
}
if (track.discNumber != null && track.discNumber! > 0) {
metadata['DISCNUMBER'] = track.discNumber.toString();
if (isFlac || isMp3) metadata['DISC'] = track.discNumber.toString();
final discTag = formatIndexTag(track.discNumber!, track.totalDiscs);
metadata['DISCNUMBER'] = discTag;
if (isFlac || isMp3) metadata['DISC'] = discTag;
}
if (track.releaseDate != null) {
metadata['DATE'] = track.releaseDate!;
@@ -3353,6 +3363,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (copyright != null && copyright.isNotEmpty) {
metadata['COPYRIGHT'] = copyright;
}
if (track.composer != null && track.composer!.isNotEmpty) {
metadata['COMPOSER'] = track.composer!;
}
// Lyrics
final lyricsMode = settings.lyricsMode;
@@ -3875,7 +3888,11 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
(trackToDownload.isrc == null ||
trackToDownload.isrc!.isEmpty ||
trackToDownload.trackNumber == null ||
trackToDownload.trackNumber == 0);
trackToDownload.trackNumber == 0 ||
trackToDownload.totalTracks == null ||
trackToDownload.totalTracks == 0 ||
(trackToDownload.composer == null ||
trackToDownload.composer!.isEmpty));
if (needsEnrichment) {
try {
@@ -3923,6 +3940,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
isrc: (data['isrc'] as String?) ?? trackToDownload.isrc,
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs:
data['total_discs'] as int? ?? trackToDownload.totalDiscs,
releaseDate: data['release_date'] as String?,
deezerId: rawId,
availability: trackToDownload.availability,
@@ -3931,6 +3950,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
trackToDownload.albumType,
totalTracks:
data['total_tracks'] as int? ?? trackToDownload.totalTracks,
composer:
data['composer']?.toString() ?? trackToDownload.composer,
source: trackToDownload.source,
);
_log.d(
@@ -4101,7 +4122,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
trackData['release_date'] as String?,
);
final provTrackNum = trackData['track_number'] as int?;
final provTotalTracks = trackData['total_tracks'] as int?;
final provDiscNum = trackData['disc_number'] as int?;
final provTotalDiscs = trackData['total_discs'] as int?;
final provComposer = normalizeOptionalString(
trackData['composer'] as String?,
);
trackToDownload = Track(
id: trackToDownload.id,
@@ -4124,11 +4150,21 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
trackToDownload.discNumber! > 0)
? trackToDownload.discNumber
: provDiscNum,
totalDiscs:
(trackToDownload.totalDiscs != null &&
trackToDownload.totalDiscs! > 0)
? trackToDownload.totalDiscs
: provTotalDiscs,
releaseDate: trackToDownload.releaseDate ?? provReleaseDate,
deezerId: trackToDownload.deezerId,
availability: trackToDownload.availability,
albumType: trackToDownload.albumType,
totalTracks: trackToDownload.totalTracks,
totalTracks:
(trackToDownload.totalTracks != null &&
trackToDownload.totalTracks! > 0)
? trackToDownload.totalTracks
: provTotalTracks,
composer: trackToDownload.composer ?? provComposer,
source: trackToDownload.source,
);
@@ -4198,7 +4234,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
trackData['isrc'] as String?,
);
final deezerTrackNum = trackData['track_number'] as int?;
final deezerTotalTracks = trackData['total_tracks'] as int?;
final deezerDiscNum = trackData['disc_number'] as int?;
final deezerTotalDiscs = trackData['total_discs'] as int?;
final deezerComposer = normalizeOptionalString(
trackData['composer'] as String?,
);
final needsEnrich =
(trackToDownload.releaseDate == null &&
@@ -4210,10 +4251,21 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
trackToDownload.trackNumber! <= 0) &&
deezerTrackNum != null &&
deezerTrackNum > 0) ||
((trackToDownload.totalTracks == null ||
trackToDownload.totalTracks! <= 0) &&
deezerTotalTracks != null &&
deezerTotalTracks > 0) ||
((trackToDownload.discNumber == null ||
trackToDownload.discNumber! <= 0) &&
deezerDiscNum != null &&
deezerDiscNum > 0);
deezerDiscNum > 0) ||
((trackToDownload.totalDiscs == null ||
trackToDownload.totalDiscs! <= 0) &&
deezerTotalDiscs != null &&
deezerTotalDiscs > 0) ||
((trackToDownload.composer == null ||
trackToDownload.composer!.isEmpty) &&
deezerComposer != null);
if (needsEnrich) {
trackToDownload = Track(
@@ -4239,11 +4291,21 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
trackToDownload.discNumber! > 0)
? trackToDownload.discNumber
: deezerDiscNum,
totalDiscs:
(trackToDownload.totalDiscs != null &&
trackToDownload.totalDiscs! > 0)
? trackToDownload.totalDiscs
: deezerTotalDiscs,
releaseDate: trackToDownload.releaseDate ?? deezerReleaseDate,
deezerId: deezerTrackId,
availability: trackToDownload.availability,
albumType: trackToDownload.albumType,
totalTracks: trackToDownload.totalTracks,
totalTracks:
(trackToDownload.totalTracks != null &&
trackToDownload.totalTracks! > 0)
? trackToDownload.totalTracks
: deezerTotalTracks,
composer: trackToDownload.composer ?? deezerComposer,
source: trackToDownload.source,
);
_log.d(
@@ -4390,6 +4452,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
metadataEmbeddingEnabled && settings.maxQualityCover,
trackNumber: normalizedTrackNumber,
discNumber: normalizedDiscNumber,
totalTracks: trackToDownload.totalTracks ?? 0,
totalDiscs: trackToDownload.totalDiscs ?? 0,
releaseDate: trackToDownload.releaseDate ?? '',
itemId: item.id,
durationMs: trackToDownload.duration,
@@ -4397,6 +4461,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
genre: genre ?? '',
label: label ?? '',
copyright: copyright ?? '',
composer: trackToDownload.composer ?? '',
qobuzId: payloadQobuzId,
tidalId: payloadTidalId,
deezerId: deezerTrackId ?? '',
+4
View File
@@ -906,9 +906,11 @@ class TrackNotifier extends Notifier<TrackState> {
duration: (durationMs / 1000).round(),
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: data['release_date'] as String?,
albumType: data['album_type'] as String?,
totalTracks: data['total_tracks'] as int?,
composer: data['composer']?.toString(),
);
}
@@ -939,10 +941,12 @@ class TrackNotifier extends Notifier<TrackState> {
duration: (durationMs / 1000).round(),
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: data['release_date']?.toString(),
totalTracks: data['total_tracks'] as int?,
source: effectiveSource,
albumType: data['album_type']?.toString(),
composer: data['composer']?.toString(),
itemType: itemType,
);
}
+2
View File
@@ -299,9 +299,11 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
duration: ((data['duration_ms'] as int? ?? 0) / 1000).round(),
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: data['release_date'] as String?,
albumType: data['album_type'] as String?,
totalTracks: data['total_tracks'] as int?,
composer: data['composer']?.toString(),
);
}
+4
View File
@@ -410,9 +410,11 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
duration: (durationMs / 1000).round(),
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: data['release_date']?.toString(),
albumType: data['album_type']?.toString() ?? album?.albumType,
totalTracks: data['total_tracks'] as int? ?? album?.totalTracks,
composer: data['composer']?.toString(),
source: data['provider_id']?.toString() ?? widget.extensionId,
);
}
@@ -1129,9 +1131,11 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
trackNumber:
data['track_position'] as int? ?? data['track_number'] as int?,
discNumber: data['disk_number'] as int? ?? data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: album.releaseDate,
albumType: album.albumType,
totalTracks: album.totalTracks,
composer: data['composer']?.toString(),
);
}
+10
View File
@@ -1840,6 +1840,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
duration: item.durationMs ~/ 1000,
trackNumber: null,
discNumber: null,
totalDiscs: null,
isrc: null,
releaseDate: item.releaseDate,
coverUrl: item.coverUrl,
@@ -4403,7 +4404,10 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
duration: (durationMs / 1000).round(),
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: data['release_date']?.toString(),
totalTracks: data['total_tracks'] as int?,
composer: data['composer']?.toString(),
source: widget.extensionId,
);
}
@@ -4562,7 +4566,10 @@ class _ExtensionPlaylistScreenState
duration: (durationMs / 1000).round(),
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: data['release_date']?.toString(),
totalTracks: data['total_tracks'] as int?,
composer: data['composer']?.toString(),
source: widget.extensionId,
);
}
@@ -4739,7 +4746,10 @@ class _ExtensionArtistScreenState extends ConsumerState<ExtensionArtistScreen> {
duration: (durationMs / 1000).round(),
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: data['release_date']?.toString(),
totalTracks: data['total_tracks'] as int?,
composer: data['composer']?.toString(),
source: (data['provider_id'] ?? widget.extensionId).toString(),
);
}
+3
View File
@@ -164,7 +164,10 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
duration: (durationMs / 1000).round(),
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: data['release_date']?.toString(),
totalTracks: data['total_tracks'] as int?,
composer: data['composer']?.toString(),
);
}
+136 -8
View File
@@ -327,8 +327,23 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
// Resolve label/copyright from file when the model doesn't carry them
// (e.g. local library items, or download history items without these fields).
final resolvedTotalTracks = _readPositiveInt(metadata['total_tracks']);
final resolvedTotalDiscs = _readPositiveInt(metadata['total_discs']);
final resolvedComposer = metadata['composer']?.toString();
final resolvedLabel = metadata['label']?.toString();
final resolvedCopyright = metadata['copyright']?.toString();
final needsTotalTracks =
resolvedTotalTracks != null &&
resolvedTotalTracks > 0 &&
totalTracks == null;
final needsTotalDiscs =
resolvedTotalDiscs != null &&
resolvedTotalDiscs > 0 &&
totalDiscs == null;
final needsComposer =
resolvedComposer != null &&
resolvedComposer.isNotEmpty &&
(composer == null || composer!.isEmpty);
final needsLabel =
resolvedLabel != null &&
resolvedLabel.isNotEmpty &&
@@ -348,6 +363,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
resolvedSampleRate != null ||
needsAlbum ||
needsDuration ||
needsTotalTracks ||
needsTotalDiscs ||
needsComposer ||
needsLabel ||
needsCopyright ||
isPlaceholderQualityLabel(_quality)) &&
@@ -361,6 +379,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
if (resolvedSampleRate != null) 'sample_rate': resolvedSampleRate,
if (needsAlbum) 'album': resolvedAlbum,
if (needsDuration) 'duration': resolvedDuration,
if (needsTotalTracks) 'total_tracks': resolvedTotalTracks,
if (needsTotalDiscs) 'total_discs': resolvedTotalDiscs,
if (needsComposer) 'composer': resolvedComposer,
if (needsLabel) 'label': resolvedLabel,
if (needsCopyright) 'copyright': resolvedCopyright,
};
@@ -482,6 +503,10 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
: _downloadItem!.trackNumber;
}
int? get totalTracks =>
_readPositiveInt(_editedMetadata?['total_tracks']) ??
(_isLocalItem ? _localLibraryItem!.totalTracks : null);
int? get discNumber {
final edited = _editedMetadata?['disc_number'];
if (edited != null) {
@@ -493,6 +518,10 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
: _downloadItem!.discNumber;
}
int? get totalDiscs =>
_readPositiveInt(_editedMetadata?['total_discs']) ??
(_isLocalItem ? _localLibraryItem!.totalDiscs : null);
String? get releaseDate =>
_editedMetadata?['date']?.toString() ??
(_isLocalItem
@@ -523,6 +552,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
String? get copyright =>
_editedMetadata?['copyright']?.toString() ??
(_isLocalItem ? _localLibraryItem!.copyright : _downloadItem!.copyright);
String? get composer =>
_editedMetadata?['composer']?.toString() ??
(_isLocalItem ? _localLibraryItem!.composer : null);
int? get duration =>
_readPositiveInt(_editedMetadata?['duration']) ??
(_isLocalItem ? _localLibraryItem!.duration : _downloadItem!.duration);
@@ -1257,8 +1289,12 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
_MetadataItem(context.l10n.trackAlbum, albumName),
if (trackNumber != null && trackNumber! > 0)
_MetadataItem(context.l10n.trackTrackNumber, trackNumber.toString()),
if (totalTracks != null && totalTracks! > 0)
_MetadataItem('Track Total', totalTracks.toString()),
if (discNumber != null && discNumber! > 0)
_MetadataItem(context.l10n.trackDiscNumber, discNumber.toString()),
if (totalDiscs != null && totalDiscs! > 0)
_MetadataItem('Disc Total', totalDiscs.toString()),
if (duration != null)
_MetadataItem(context.l10n.trackDuration, _formatDuration(duration!)),
if (audioQualityStr != null)
@@ -1271,6 +1307,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
_MetadataItem(context.l10n.trackLabel, label!),
if (copyright != null && copyright!.isNotEmpty)
_MetadataItem(context.l10n.trackCopyright, copyright!),
if (composer != null && composer!.isNotEmpty)
_MetadataItem('Composer', composer!),
if (isrc != null && isrc!.isNotEmpty) _MetadataItem('ISRC', isrc!),
];
@@ -2527,12 +2565,15 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
'album_name': albumName,
'album_artist': albumArtist ?? artistName,
'track_number': trackNumber ?? 0,
'total_tracks': totalTracks ?? 0,
'disc_number': discNumber ?? 0,
'total_discs': totalDiscs ?? 0,
'release_date': releaseDate ?? '',
'isrc': isrc ?? '',
'genre': genre ?? '',
'label': label ?? '',
'copyright': copyright ?? '',
'composer': composer ?? '',
'duration_ms': durationMs,
'search_online': true,
};
@@ -2550,11 +2591,14 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
'album_artist': enriched['album_artist'] ?? albumArtist,
'date': enriched['release_date'] ?? releaseDate,
'track_number': enriched['track_number'] ?? trackNumber,
'total_tracks': enriched['total_tracks'] ?? totalTracks,
'disc_number': enriched['disc_number'] ?? discNumber,
'total_discs': enriched['total_discs'] ?? totalDiscs,
'isrc': enriched['isrc'] ?? isrc,
'genre': enriched['genre'] ?? genre,
'label': enriched['label'] ?? label,
'copyright': enriched['copyright'] ?? copyright,
'composer': enriched['composer'] ?? composer,
};
});
}
@@ -2991,19 +3035,29 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
}
Map<String, String> _buildFallbackMetadata() {
String formatIndexTag(int number, int? total) {
if (total != null && total > 0) {
return '$number/$total';
}
return number.toString();
}
return {
'TITLE': trackName,
'ARTIST': artistName,
'ALBUM': albumName,
if (albumArtist != null && albumArtist!.isNotEmpty)
'ALBUMARTIST': albumArtist!,
if (trackNumber != null) 'TRACKNUMBER': trackNumber.toString(),
if (discNumber != null) 'DISCNUMBER': discNumber.toString(),
if (trackNumber != null)
'TRACKNUMBER': formatIndexTag(trackNumber!, totalTracks),
if (discNumber != null)
'DISCNUMBER': formatIndexTag(discNumber!, totalDiscs),
if (releaseDate != null && releaseDate!.isNotEmpty) 'DATE': releaseDate!,
if (isrc != null && isrc!.isNotEmpty) 'ISRC': isrc!,
if (genre != null && genre!.isNotEmpty) 'GENRE': genre!,
if (label != null && label!.isNotEmpty) 'LABEL': label!,
if (copyright != null && copyright!.isNotEmpty) 'COPYRIGHT': copyright!,
if (composer != null && composer!.isNotEmpty) 'COMPOSER': composer!,
};
}
@@ -3031,12 +3085,26 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
put('UNSYNCEDLYRICS', source['lyrics']);
final trackNumber = source['track_number'];
final totalTracks = source['total_tracks'];
if (trackNumber != null && trackNumber.toString() != '0') {
put('TRACKNUMBER', trackNumber);
final trackTag =
totalTracks != null &&
totalTracks.toString().isNotEmpty &&
totalTracks.toString() != '0'
? '${trackNumber.toString()}/${totalTracks.toString()}'
: trackNumber;
put('TRACKNUMBER', trackTag);
}
final discNumber = source['disc_number'];
final totalDiscs = source['total_discs'];
if (discNumber != null && discNumber.toString() != '0') {
put('DISCNUMBER', discNumber);
final discTag =
totalDiscs != null &&
totalDiscs.toString().isNotEmpty &&
totalDiscs.toString() != '0'
? '${discNumber.toString()}/${totalDiscs.toString()}'
: discNumber;
put('DISCNUMBER', discTag);
}
return mapped;
@@ -4023,13 +4091,17 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
'date': val('date', releaseDate),
'track_number': (fileMetadata?['track_number'] ?? trackNumber ?? '')
.toString(),
'total_tracks': (fileMetadata?['total_tracks'] ?? totalTracks ?? '')
.toString(),
'disc_number': (fileMetadata?['disc_number'] ?? discNumber ?? '')
.toString(),
'total_discs': (fileMetadata?['total_discs'] ?? totalDiscs ?? '')
.toString(),
'genre': val('genre', genre),
'isrc': val('isrc', isrc),
'label': val('label', label),
'copyright': val('copyright', copyright),
'composer': fileMetadata?['composer']?.toString() ?? '',
'composer': val('composer', composer),
'comment': fileMetadata?['comment']?.toString() ?? '',
};
@@ -4316,11 +4388,14 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
'album_artist': 'album_artist',
'date': 'date',
'track_number': 'track_number',
'total_tracks': 'total_tracks',
'disc_number': 'disc_number',
'total_discs': 'total_discs',
'genre': 'genre',
'isrc': 'isrc',
'label': 'label',
'copyright': 'copyright',
'composer': 'composer',
'cover': 'cover',
};
@@ -4330,7 +4405,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
late final TextEditingController _albumArtistCtrl;
late final TextEditingController _dateCtrl;
late final TextEditingController _trackNumCtrl;
late final TextEditingController _trackTotalCtrl;
late final TextEditingController _discNumCtrl;
late final TextEditingController _discTotalCtrl;
late final TextEditingController _genreCtrl;
late final TextEditingController _isrcCtrl;
late final TextEditingController _labelCtrl;
@@ -4518,8 +4595,12 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
return l10n.editMetadataFieldDate;
case 'track_number':
return l10n.editMetadataFieldTrackNum;
case 'total_tracks':
return 'Track Total';
case 'disc_number':
return l10n.editMetadataFieldDiscNum;
case 'total_discs':
return 'Disc Total';
case 'genre':
return l10n.editMetadataFieldGenre;
case 'isrc':
@@ -4528,6 +4609,8 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
return l10n.editMetadataFieldLabel;
case 'copyright':
return l10n.editMetadataFieldCopyright;
case 'composer':
return 'Composer';
case 'cover':
return l10n.editMetadataFieldCover;
default:
@@ -4549,8 +4632,12 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
return _dateCtrl;
case 'track_number':
return _trackNumCtrl;
case 'total_tracks':
return _trackTotalCtrl;
case 'disc_number':
return _discNumCtrl;
case 'total_discs':
return _discTotalCtrl;
case 'genre':
return _genreCtrl;
case 'isrc':
@@ -4559,6 +4646,8 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
return _labelCtrl;
case 'copyright':
return _copyrightCtrl;
case 'composer':
return _composerCtrl;
default:
return null;
}
@@ -4724,11 +4813,14 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
put('album_artist', track['album_artist']);
put('date', track['release_date']);
put('track_number', track['track_number']);
put('total_tracks', track['total_tracks']);
put('disc_number', track['disc_number']);
put('total_discs', track['total_discs']);
put('isrc', track['isrc']);
put('genre', track['genre']);
put('label', track['label']);
put('copyright', track['copyright']);
put('composer', track['composer']);
}
Future<_ResolvedAutoFillTrack?> _resolveAutoFillTrackFromIdentifiers(
@@ -4929,8 +5021,11 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
'album_artist': (selectedBest['album_artist'] ?? '').toString(),
'date': (selectedBest['release_date'] ?? '').toString(),
'track_number': (selectedBest['track_number'] ?? '').toString(),
'total_tracks': (selectedBest['total_tracks'] ?? '').toString(),
'disc_number': (selectedBest['disc_number'] ?? '').toString(),
'total_discs': (selectedBest['total_discs'] ?? '').toString(),
'isrc': (selectedBest['isrc'] ?? '').toString(),
'composer': (selectedBest['composer'] ?? '').toString(),
};
_mergeOnlineTrackData(enriched, selectedBest);
@@ -4939,7 +5034,8 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
final needsExtended =
_autoFillFields.contains('genre') ||
_autoFillFields.contains('label') ||
_autoFillFields.contains('copyright');
_autoFillFields.contains('copyright') ||
_autoFillFields.contains('composer');
final rawSpotifyId = _extractRawSpotifyTrackId(selectedBest);
@@ -5101,7 +5197,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
_albumArtistCtrl = TextEditingController(text: v['album_artist'] ?? '');
_dateCtrl = TextEditingController(text: v['date'] ?? '');
_trackNumCtrl = TextEditingController(text: v['track_number'] ?? '');
_trackTotalCtrl = TextEditingController(text: v['total_tracks'] ?? '');
_discNumCtrl = TextEditingController(text: v['disc_number'] ?? '');
_discTotalCtrl = TextEditingController(text: v['total_discs'] ?? '');
_genreCtrl = TextEditingController(text: v['genre'] ?? '');
_isrcCtrl = TextEditingController(text: v['isrc'] ?? '');
_labelCtrl = TextEditingController(text: v['label'] ?? '');
@@ -5121,7 +5219,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
_albumArtistCtrl.dispose();
_dateCtrl.dispose();
_trackNumCtrl.dispose();
_trackTotalCtrl.dispose();
_discNumCtrl.dispose();
_discTotalCtrl.dispose();
_genreCtrl.dispose();
_isrcCtrl.dispose();
_labelCtrl.dispose();
@@ -5141,7 +5241,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
'album_artist': _albumArtistCtrl.text,
'date': _dateCtrl.text,
'track_number': _trackNumCtrl.text,
'track_total': _trackTotalCtrl.text,
'disc_number': _discNumCtrl.text,
'disc_total': _discTotalCtrl.text,
'genre': _genreCtrl.text,
'isrc': _isrcCtrl.text,
'label': _labelCtrl.text,
@@ -5193,12 +5295,18 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
'TRACKNUMBER':
(metadata['track_number']?.isNotEmpty == true &&
metadata['track_number'] != '0')
? metadata['track_number']!
? (metadata['track_total']?.isNotEmpty == true &&
metadata['track_total'] != '0'
? '${metadata['track_number']}/${metadata['track_total']}'
: metadata['track_number']!)
: '',
'DISCNUMBER':
(metadata['disc_number']?.isNotEmpty == true &&
metadata['disc_number'] != '0')
? metadata['disc_number']!
? (metadata['disc_total']?.isNotEmpty == true &&
metadata['disc_total'] != '0'
? '${metadata['disc_number']}/${metadata['disc_total']}'
: metadata['disc_number']!)
: '',
'GENRE': metadata['genre'] ?? '',
'ISRC': metadata['isrc'] ?? '',
@@ -5411,6 +5519,18 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
),
),
const SizedBox(width: 12),
Expanded(
child: _field(
'Track Total',
_trackTotalCtrl,
keyboard: TextInputType.number,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _field(
'Disc #',
@@ -5418,6 +5538,14 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
keyboard: TextInputType.number,
),
),
const SizedBox(width: 12),
Expanded(
child: _field(
'Disc Total',
_discTotalCtrl,
keyboard: TextInputType.number,
),
),
],
),
_field('Genre', _genreCtrl),
@@ -17,6 +17,7 @@ class DownloadRequestPayload {
final int trackNumber;
final int discNumber;
final int totalTracks;
final int totalDiscs;
final String releaseDate;
final String itemId;
final int durationMs;
@@ -24,6 +25,7 @@ class DownloadRequestPayload {
final String genre;
final String label;
final String copyright;
final String composer;
final String tidalId;
final String qobuzId;
final String deezerId;
@@ -56,6 +58,7 @@ class DownloadRequestPayload {
this.trackNumber = 0,
this.discNumber = 0,
this.totalTracks = 1,
this.totalDiscs = 0,
this.releaseDate = '',
this.itemId = '',
this.durationMs = 0,
@@ -63,6 +66,7 @@ class DownloadRequestPayload {
this.genre = '',
this.label = '',
this.copyright = '',
this.composer = '',
this.tidalId = '',
this.qobuzId = '',
this.deezerId = '',
@@ -97,6 +101,7 @@ class DownloadRequestPayload {
'track_number': trackNumber,
'disc_number': discNumber,
'total_tracks': totalTracks,
'total_discs': totalDiscs,
'release_date': releaseDate,
'item_id': itemId,
'duration_ms': durationMs,
@@ -104,6 +109,7 @@ class DownloadRequestPayload {
'genre': genre,
'label': label,
'copyright': copyright,
'composer': composer,
'tidal_id': tidalId,
'qobuz_id': qobuzId,
'deezer_id': deezerId,
@@ -142,6 +148,7 @@ class DownloadRequestPayload {
trackNumber: trackNumber,
discNumber: discNumber,
totalTracks: totalTracks,
totalDiscs: totalDiscs,
releaseDate: releaseDate,
itemId: itemId,
durationMs: durationMs,
@@ -149,6 +156,7 @@ class DownloadRequestPayload {
genre: genre,
label: label,
copyright: copyright,
composer: composer,
tidalId: tidalId,
qobuzId: qobuzId,
deezerId: deezerId,
+34 -6
View File
@@ -20,13 +20,16 @@ class LocalLibraryItem {
final int? fileModTime;
final String? isrc;
final int? trackNumber;
final int? totalTracks;
final int? discNumber;
final int? totalDiscs;
final int? duration;
final String? releaseDate;
final int? bitDepth;
final int? sampleRate;
final int? bitrate; // kbps, for lossy formats (mp3, opus, ogg)
final String? genre;
final String? composer;
final String? label;
final String? copyright;
final String? format; // flac, mp3, opus, m4a
@@ -43,13 +46,16 @@ class LocalLibraryItem {
this.fileModTime,
this.isrc,
this.trackNumber,
this.totalTracks,
this.discNumber,
this.totalDiscs,
this.duration,
this.releaseDate,
this.bitDepth,
this.sampleRate,
this.bitrate,
this.genre,
this.composer,
this.label,
this.copyright,
this.format,
@@ -67,13 +73,16 @@ class LocalLibraryItem {
'fileModTime': fileModTime,
'isrc': isrc,
'trackNumber': trackNumber,
'totalTracks': totalTracks,
'discNumber': discNumber,
'totalDiscs': totalDiscs,
'duration': duration,
'releaseDate': releaseDate,
'bitDepth': bitDepth,
'sampleRate': sampleRate,
'bitrate': bitrate,
'genre': genre,
'composer': composer,
'label': label,
'copyright': copyright,
'format': format,
@@ -91,14 +100,17 @@ class LocalLibraryItem {
scannedAt: DateTime.parse(json['scannedAt'] as String),
fileModTime: (json['fileModTime'] as num?)?.toInt(),
isrc: json['isrc'] as String?,
trackNumber: json['trackNumber'] as int?,
discNumber: json['discNumber'] as int?,
duration: json['duration'] as int?,
trackNumber: (json['trackNumber'] as num?)?.toInt(),
totalTracks: (json['totalTracks'] as num?)?.toInt(),
discNumber: (json['discNumber'] as num?)?.toInt(),
totalDiscs: (json['totalDiscs'] as num?)?.toInt(),
duration: (json['duration'] as num?)?.toInt(),
releaseDate: json['releaseDate'] as String?,
bitDepth: json['bitDepth'] as int?,
sampleRate: json['sampleRate'] as int?,
bitDepth: (json['bitDepth'] as num?)?.toInt(),
sampleRate: (json['sampleRate'] as num?)?.toInt(),
bitrate: (json['bitrate'] as num?)?.toInt(),
genre: json['genre'] as String?,
composer: json['composer'] as String?,
label: json['label'] as String?,
copyright: json['copyright'] as String?,
format: json['format'] as String?,
@@ -130,7 +142,7 @@ class LibraryDatabase {
return await openDatabase(
path,
version: 5,
version: 6,
onConfigure: (db) async {
await db.rawQuery('PRAGMA journal_mode = WAL');
await db.execute('PRAGMA synchronous = NORMAL');
@@ -156,13 +168,16 @@ class LibraryDatabase {
file_mod_time INTEGER,
isrc TEXT,
track_number INTEGER,
total_tracks INTEGER,
disc_number INTEGER,
total_discs INTEGER,
duration INTEGER,
release_date TEXT,
bit_depth INTEGER,
sample_rate INTEGER,
bitrate INTEGER,
genre TEXT,
composer TEXT,
label TEXT,
copyright TEXT,
format TEXT
@@ -206,6 +221,13 @@ class LibraryDatabase {
await db.execute('ALTER TABLE library ADD COLUMN copyright TEXT');
_log.i('Added label/copyright columns');
}
if (oldVersion < 6) {
await db.execute('ALTER TABLE library ADD COLUMN total_tracks INTEGER');
await db.execute('ALTER TABLE library ADD COLUMN total_discs INTEGER');
await db.execute('ALTER TABLE library ADD COLUMN composer TEXT');
_log.i('Added total_tracks/total_discs/composer columns');
}
}
Map<String, dynamic> _jsonToDbRow(Map<String, dynamic> json) {
@@ -221,13 +243,16 @@ class LibraryDatabase {
'file_mod_time': json['fileModTime'],
'isrc': json['isrc'],
'track_number': json['trackNumber'],
'total_tracks': json['totalTracks'],
'disc_number': json['discNumber'],
'total_discs': json['totalDiscs'],
'duration': json['duration'],
'release_date': json['releaseDate'],
'bit_depth': json['bitDepth'],
'sample_rate': json['sampleRate'],
'bitrate': json['bitrate'],
'genre': json['genre'],
'composer': json['composer'],
'label': json['label'],
'copyright': json['copyright'],
'format': json['format'],
@@ -247,13 +272,16 @@ class LibraryDatabase {
'fileModTime': row['file_mod_time'],
'isrc': row['isrc'],
'trackNumber': row['track_number'],
'totalTracks': row['total_tracks'],
'discNumber': row['disc_number'],
'totalDiscs': row['total_discs'],
'duration': row['duration'],
'releaseDate': row['release_date'],
'bitDepth': row['bit_depth'],
'sampleRate': row['sample_rate'],
'bitrate': row['bitrate'],
'genre': row['genre'],
'composer': row['composer'],
'label': row['label'],
'copyright': row['copyright'],
'format': row['format'],
@@ -142,8 +142,10 @@ class LocalTrackRedownloadService {
duration: (durationMs / 1000).round(),
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: data['total_discs'] as int?,
releaseDate: data['release_date']?.toString(),
totalTracks: data['total_tracks'] as int?,
composer: data['composer']?.toString(),
source: data['source']?.toString() ?? data['provider_id']?.toString(),
albumType: data['album_type']?.toString(),
itemType: itemType,
+18 -2
View File
@@ -100,12 +100,28 @@ void mergePlatformMetadataForTagEmbed({
put('UNSYNCEDLYRICS', source['lyrics']);
final trackNumber = source['track_number'];
final totalTracks = source['total_tracks'];
if (trackNumber != null && trackNumber.toString() != '0') {
put('TRACKNUMBER', trackNumber);
put(
'TRACKNUMBER',
totalTracks != null &&
totalTracks.toString().isNotEmpty &&
totalTracks.toString() != '0'
? '${trackNumber.toString()}/${totalTracks.toString()}'
: trackNumber,
);
}
final discNumber = source['disc_number'];
final totalDiscs = source['total_discs'];
if (discNumber != null && discNumber.toString() != '0') {
put('DISCNUMBER', discNumber);
put(
'DISCNUMBER',
totalDiscs != null &&
totalDiscs.toString().isNotEmpty &&
totalDiscs.toString() != '0'
? '${discNumber.toString()}/${totalDiscs.toString()}'
: discNumber,
);
}
}