Refactor: comprehensive cleanup and modularization

- Extracted common JSON parsing helpers into internal/jsonutil
- Removed duplicated helper functions from provider packages
- Removed dead code in internal/app/app.go and downloader.go
- Replaced deprecated strings.Title with jsonutil.TitleCase
- Added graceful shutdown with signal handling in main.go
- Split monolithic cmd/rip/main.go into args.go, helpers.go, lastfm.go, search.go
This commit is contained in:
2026-04-21 23:38:41 +02:00
parent d65dc182f8
commit 6bc4b3b319
15 changed files with 1763 additions and 1853 deletions

View File

@@ -18,6 +18,7 @@ import (
"streamrip-go/internal/config"
"streamrip-go/internal/domain/media"
"streamrip-go/internal/download"
"streamrip-go/internal/jsonutil"
"streamrip-go/internal/naming"
"streamrip-go/internal/provider"
deezerprovider "streamrip-go/internal/provider/deezer"
@@ -259,7 +260,7 @@ func (m *Main) AddMixedPlaylistByTrackRefs(ctx context.Context, playlistID, play
func (m *Main) ripCollection(ctx context.Context, p provider.Client, source, kind, id string, meta map[string]any) error {
name := titleFromMetadata(meta, id)
if n := stringFromAny(meta["name"]); n != "" {
if n := jsonutil.StringFromAny(meta["name"]); n != "" {
name = n
}
@@ -327,18 +328,18 @@ func (m *Main) ripVideo(ctx context.Context, p provider.Client, source, videoID
}
func buildCollectionAlbum(id string, meta map[string]any) collectionAlbum {
trackCount := intFromAny(meta["tracks_count"])
trackCount := jsonutil.IntFromAny(meta["tracks_count"])
if trackCount == 0 {
trackCount = intFromAny(meta["numberOfTracks"])
trackCount = jsonutil.IntFromAny(meta["numberOfTracks"])
}
return collectionAlbum{
ID: id,
Meta: meta,
Title: titleFromMetadata(meta, id),
AlbumArtist: nestedString(meta, "artist", "name"),
BitDepth: intFromAny(meta["maximum_bit_depth"]),
Sampling: floatFromAny(meta["maximum_sampling_rate"]),
Explicit: boolFromAny(meta["parental_warning"]),
AlbumArtist: jsonutil.NestedString(meta, "artist", "name"),
BitDepth: jsonutil.IntFromAny(meta["maximum_bit_depth"]),
Sampling: jsonutil.FloatFromAny(meta["maximum_sampling_rate"]),
Explicit: jsonutil.BoolFromAny(meta["parental_warning"]),
TrackCount: trackCount,
}
}
@@ -459,10 +460,10 @@ func extractAlbumIDs(meta map[string]any) []string {
if !ok {
continue
}
id := stringFromAny(itm["id"])
id := jsonutil.StringFromAny(itm["id"])
if id == "" {
if nested, ok := itm["album"].(map[string]any); ok {
id = stringFromAny(nested["id"])
id = jsonutil.StringFromAny(nested["id"])
}
}
if id == "" {
@@ -517,23 +518,23 @@ func (m *Main) ripAlbum(ctx context.Context, p provider.Client, source, albumID
}
albumTitle := titleFromMetadata(albumMeta, albumID)
albumArtist := nestedString(albumMeta, "artist", "name")
albumArtist := jsonutil.NestedString(albumMeta, "artist", "name")
if albumArtist == "" {
albumArtist = "Unknown"
}
releaseDate := stringFromAny(albumMeta["release_date_original"])
releaseDate := jsonutil.StringFromAny(albumMeta["release_date_original"])
if releaseDate == "" {
releaseDate = stringFromAny(albumMeta["release_date"])
releaseDate = jsonutil.StringFromAny(albumMeta["release_date"])
}
if releaseDate == "" {
releaseDate = stringFromAny(albumMeta["releaseDate"])
releaseDate = jsonutil.StringFromAny(albumMeta["releaseDate"])
}
if releaseDate == "" {
releaseDate = stringFromAny(albumMeta["streamStartDate"])
releaseDate = jsonutil.StringFromAny(albumMeta["streamStartDate"])
}
year := naming.YearFromDate(releaseDate)
bitDepth := intFromAny(albumMeta["maximum_bit_depth"])
sampling := stringFromAny(albumMeta["maximum_sampling_rate"])
bitDepth := jsonutil.IntFromAny(albumMeta["maximum_bit_depth"])
sampling := jsonutil.StringFromAny(albumMeta["maximum_sampling_rate"])
if bitDepth == 0 || sampling == "" {
fallbackBitDepth, fallbackSampling := m.qualityProfileForSource(source)
if bitDepth == 0 {
@@ -564,7 +565,7 @@ func (m *Main) ripAlbum(ctx context.Context, p provider.Client, source, albumID
if !ok {
continue
}
id := stringFromAny(itm["id"])
id := jsonutil.StringFromAny(itm["id"])
if id != "" {
trackIDs = append(trackIDs, id)
}
@@ -573,9 +574,9 @@ func (m *Main) ripAlbum(ctx context.Context, p provider.Client, source, albumID
folder := m.albumFolderPath(source, albumID, albumTitle, albumArtist, year, bitDepth, sampling)
artRes, _ := artwork.Prepare(ctx, m.DL, folder, albumMeta, m.Config.Session.Artwork, false)
total := len(trackIDs)
discTotal := intFromAny(albumMeta["media_count"])
discTotal := jsonutil.IntFromAny(albumMeta["media_count"])
if discTotal == 0 {
discTotal = intFromAny(albumMeta["numberOfVolumes"])
discTotal = jsonutil.IntFromAny(albumMeta["numberOfVolumes"])
}
m.logf("Album: %s (%d tracks)\n", albumTitle, total)
failures := 0
@@ -631,12 +632,12 @@ func (m *Main) ripPlaylist(ctx context.Context, p provider.Client, source, playl
}
name := titleFromMetadata(playlistMeta, playlistID)
if n := stringFromAny(playlistMeta["name"]); n != "" {
if n := jsonutil.StringFromAny(playlistMeta["name"]); n != "" {
name = n
}
base := m.Config.Session.Downloads.Folder
if m.Config.Session.Downloads.SourceSubdirectories {
base = filepath.Join(base, strings.Title(source))
base = filepath.Join(base, jsonutil.TitleCase(source))
}
folder := filepath.Join(base, naming.CleanName(name, naming.Config{
RestrictCharacters: m.Config.Session.Filepaths.RestrictCharacters,
@@ -665,9 +666,9 @@ func (m *Main) ripPlaylist(ctx context.Context, p provider.Client, source, playl
if !ok {
continue
}
id := stringFromAny(itm["id"])
id := jsonutil.StringFromAny(itm["id"])
if id == "" {
id = stringFromAny(itm["track_id"])
id = jsonutil.StringFromAny(itm["track_id"])
}
if id != "" {
ids = append(ids, id)
@@ -806,11 +807,9 @@ func (m *Main) requireSourceDownloadAuth(source string) error {
}
func (m *Main) ripTrack(ctx context.Context, p provider.Client, source, id, fallbackTitle string, opts ripTrackOptions) error {
alreadyDownloaded, err := m.Store.IsDownloaded(ctx, source, id)
if err == nil && alreadyDownloaded {
if m.IgnoreDB {
alreadyDownloaded = false
} else {
if !m.IgnoreDB {
alreadyDownloaded, err := m.Store.IsDownloaded(ctx, source, id)
if err == nil && alreadyDownloaded {
if opts.total > 0 {
m.logf("[%d/%d] skip (already downloaded) id=%s\n", opts.index, opts.total, id)
} else {
@@ -820,19 +819,6 @@ func (m *Main) ripTrack(ctx context.Context, p provider.Client, source, id, fall
}
}
if m.IgnoreDB {
alreadyDownloaded = false
}
if alreadyDownloaded {
if opts.total > 0 {
m.logf("[%d/%d] skip (already downloaded) id=%s\n", opts.index, opts.total, id)
} else {
m.logf("skip (already downloaded) id=%s\n", id)
}
return nil
}
meta, err := p.GetMetadata(ctx, id, "track")
if err != nil {
_ = m.Store.MarkFailed(ctx, source, "track", id)
@@ -970,7 +956,7 @@ func (m *Main) qualityProfileForSource(source string) (int, string) {
func (m *Main) albumFolderPath(source, albumID, albumTitle, albumArtist, year string, bitDepth int, samplingRate string) string {
base := m.Config.Session.Downloads.Folder
if m.Config.Session.Downloads.SourceSubdirectories {
base = filepath.Join(base, strings.Title(source))
base = filepath.Join(base, jsonutil.TitleCase(source))
}
vals := map[string]string{
@@ -995,34 +981,34 @@ func (m *Main) albumFolderPath(source, albumID, albumTitle, albumArtist, year st
func (m *Main) trackOutputPath(source, id, title, ext string, trackMeta map[string]any, albumFolder string, albumDiscTotal int) string {
base := m.Config.Session.Downloads.Folder
if m.Config.Session.Downloads.SourceSubdirectories {
base = filepath.Join(base, strings.Title(source))
base = filepath.Join(base, jsonutil.TitleCase(source))
}
if albumFolder == "" && m.Config.Session.Filepaths.AddSinglesToFolder {
albumTitle := nestedString(trackMeta, "album", "title")
albumID := nestedString(trackMeta, "album", "id")
albumTitle := jsonutil.NestedString(trackMeta, "album", "title")
albumID := jsonutil.NestedString(trackMeta, "album", "id")
if albumID == "" {
albumID = id
}
albumArtist := nestedString(trackMeta, "album", "artist", "name")
albumArtist := jsonutil.NestedString(trackMeta, "album", "artist", "name")
if albumArtist == "" {
albumArtist = nestedString(trackMeta, "performer", "name")
albumArtist = jsonutil.NestedString(trackMeta, "performer", "name")
}
albumYear := naming.YearFromDate(stringFromAny(trackMeta["release_date_original"]))
albumYear := naming.YearFromDate(jsonutil.StringFromAny(trackMeta["release_date_original"]))
if albumYear == "Unknown" {
albumYear = naming.YearFromDate(stringFromAny(trackMeta["release_date"]))
albumYear = naming.YearFromDate(jsonutil.StringFromAny(trackMeta["release_date"]))
}
albumFolder = m.albumFolderPath(source, albumID, albumTitle, albumArtist, albumYear, intFromAny(trackMeta["maximum_bit_depth"]), stringFromAny(trackMeta["maximum_sampling_rate"]))
albumFolder = m.albumFolderPath(source, albumID, albumTitle, albumArtist, albumYear, jsonutil.IntFromAny(trackMeta["maximum_bit_depth"]), jsonutil.StringFromAny(trackMeta["maximum_sampling_rate"]))
}
if albumFolder != "" {
base = albumFolder
if m.Config.Session.Downloads.DiscSubdirectories && albumDiscTotal > 1 {
discNumber := intFromAny(trackMeta["media_number"])
discNumber := jsonutil.IntFromAny(trackMeta["media_number"])
if discNumber == 0 {
discNumber = intFromAny(trackMeta["volumeNumber"])
discNumber = jsonutil.IntFromAny(trackMeta["volumeNumber"])
}
if discNumber == 0 {
discNumber = intFromAny(trackMeta["disk_number"])
discNumber = jsonutil.IntFromAny(trackMeta["disk_number"])
}
if discNumber == 0 {
discNumber = 1
@@ -1033,19 +1019,19 @@ func (m *Main) trackOutputPath(source, id, title, ext string, trackMeta map[stri
}
}
trackNumber := intFromAny(trackMeta["track_number"])
trackNumber := jsonutil.IntFromAny(trackMeta["track_number"])
if trackNumber == 0 {
trackNumber = intFromAny(trackMeta["trackNumber"])
trackNumber = jsonutil.IntFromAny(trackMeta["trackNumber"])
}
explicit := ""
if boolFromAny(trackMeta["parental_warning"]) || boolFromAny(trackMeta["explicit"]) {
if jsonutil.BoolFromAny(trackMeta["parental_warning"]) || jsonutil.BoolFromAny(trackMeta["explicit"]) {
explicit = " (Explicit)"
}
artist := nestedString(trackMeta, "performer", "name")
artist := jsonutil.NestedString(trackMeta, "performer", "name")
if artist == "" {
artist = nestedString(trackMeta, "artist", "name")
artist = jsonutil.NestedString(trackMeta, "artist", "name")
}
albumArtist := nestedString(trackMeta, "album", "artist", "name")
albumArtist := jsonutil.NestedString(trackMeta, "album", "artist", "name")
if albumArtist == "" {
albumArtist = artist
}
@@ -1073,7 +1059,7 @@ func (m *Main) videoOutputPath(source, id, title, ext string) string {
}
base := m.Config.Session.Downloads.Folder
if m.Config.Session.Downloads.SourceSubdirectories {
base = filepath.Join(base, strings.Title(source))
base = filepath.Join(base, jsonutil.TitleCase(source))
}
fileName := naming.CleanName(title, naming.Config{
RestrictCharacters: m.Config.Session.Filepaths.RestrictCharacters,
@@ -1088,7 +1074,7 @@ func (m *Main) videoOutputPath(source, id, title, ext string) string {
func titleFromMetadata(meta map[string]any, fallback string) string {
if title, ok := meta["title"].(string); ok {
title = strings.TrimSpace(title)
version := strings.TrimSpace(stringFromAny(meta["version"]))
version := strings.TrimSpace(jsonutil.StringFromAny(meta["version"]))
if version != "" {
return title + " (" + version + ")"
}
@@ -1099,70 +1085,8 @@ func titleFromMetadata(meta map[string]any, fallback string) string {
return fallback
}
func nestedString(v map[string]any, keys ...string) string {
return stringFromAny(nestedAny(v, keys...))
}
func nestedAny(v map[string]any, keys ...string) any {
cur := any(v)
for _, key := range keys {
m, ok := cur.(map[string]any)
if !ok {
return nil
}
cur = m[key]
}
return cur
}
func stringFromAny(v any) string {
switch t := v.(type) {
case string:
return t
case float64:
return strconv.FormatFloat(t, 'f', -1, 64)
case int64:
return strconv.FormatInt(t, 10)
case int:
return strconv.Itoa(t)
default:
return ""
}
}
func intFromAny(v any) int {
switch t := v.(type) {
case int:
return t
case int64:
return int(t)
case float64:
return int(t)
default:
return 0
}
}
func floatFromAny(v any) float64 {
switch t := v.(type) {
case float64:
return t
case int:
return float64(t)
case int64:
return float64(t)
default:
return 0
}
}
func boolFromAny(v any) bool {
b, _ := v.(bool)
return b
}
func replaygainGainFromAny(v any) string {
s := strings.TrimSpace(stringFromAny(v))
s := strings.TrimSpace(jsonutil.StringFromAny(v))
if s == "" {
return ""
}
@@ -1183,7 +1107,7 @@ func replaygainGainFromAny(v any) string {
}
func replaygainPeakFromAny(v any) string {
return strings.TrimSpace(stringFromAny(v))
return strings.TrimSpace(jsonutil.StringFromAny(v))
}
func trackMetaAlbum(trackMeta map[string]any) map[string]any {
@@ -1195,53 +1119,53 @@ func trackMetaAlbum(trackMeta map[string]any) map[string]any {
}
func buildTagMetadata(trackMeta map[string]any, title, source, trackID string, opts ripTrackOptions) tag.Metadata {
artist := nestedString(trackMeta, "performer", "name")
artist := jsonutil.NestedString(trackMeta, "performer", "name")
if artist == "" {
artist = nestedString(trackMeta, "artist", "name")
artist = jsonutil.NestedString(trackMeta, "artist", "name")
}
albumArtist := nestedString(trackMeta, "album", "artist", "name")
albumArtist := jsonutil.NestedString(trackMeta, "album", "artist", "name")
if albumArtist == "" {
albumArtist = artist
}
if strings.TrimSpace(opts.albumArtist) != "" {
albumArtist = strings.TrimSpace(opts.albumArtist)
}
trackNumber := intFromAny(trackMeta["track_number"])
trackNumber := jsonutil.IntFromAny(trackMeta["track_number"])
if trackNumber == 0 {
trackNumber = intFromAny(trackMeta["trackNumber"])
trackNumber = jsonutil.IntFromAny(trackMeta["trackNumber"])
}
discNumber := intFromAny(trackMeta["media_number"])
discNumber := jsonutil.IntFromAny(trackMeta["media_number"])
if discNumber == 0 {
discNumber = intFromAny(trackMeta["volumeNumber"])
discNumber = jsonutil.IntFromAny(trackMeta["volumeNumber"])
}
if discNumber == 0 {
discNumber = intFromAny(trackMeta["disk_number"])
discNumber = jsonutil.IntFromAny(trackMeta["disk_number"])
}
date := stringFromAny(trackMeta["release_date_original"])
date := jsonutil.StringFromAny(trackMeta["release_date_original"])
if date == "" {
date = stringFromAny(trackMeta["release_date"])
date = jsonutil.StringFromAny(trackMeta["release_date"])
}
if date == "" {
date = stringFromAny(trackMeta["streamStartDate"])
date = jsonutil.StringFromAny(trackMeta["streamStartDate"])
}
album := nestedString(trackMeta, "album", "title")
album := jsonutil.NestedString(trackMeta, "album", "title")
if album == "" {
album = stringFromAny(trackMeta["title"])
album = jsonutil.StringFromAny(trackMeta["title"])
}
trackTotal := intFromAny(trackMeta["tracks_count"])
trackTotal := jsonutil.IntFromAny(trackMeta["tracks_count"])
if trackTotal == 0 {
trackTotal = intFromAny(trackMeta["numberOfTracks"])
trackTotal = jsonutil.IntFromAny(trackMeta["numberOfTracks"])
}
if trackTotal == 0 {
trackTotal = intFromAny(trackMeta["track_total"])
trackTotal = jsonutil.IntFromAny(trackMeta["track_total"])
}
if opts.forPlaylist && opts.total > 0 {
trackTotal = opts.total
}
discTotal := intFromAny(trackMeta["media_count"])
discTotal := jsonutil.IntFromAny(trackMeta["media_count"])
if discTotal == 0 {
discTotal = intFromAny(trackMeta["numberOfVolumes"])
discTotal = jsonutil.IntFromAny(trackMeta["numberOfVolumes"])
}
if discTotal == 0 && opts.albumDiscTotal > 0 {
discTotal = opts.albumDiscTotal
@@ -1253,15 +1177,15 @@ func buildTagMetadata(trackMeta map[string]any, title, source, trackID string, o
discNumber = 1
}
genre := nestedString(trackMeta, "genre", "name")
genre := jsonutil.NestedString(trackMeta, "genre", "name")
if genre == "" {
genre = stringFromAny(trackMeta["genre"])
genre = jsonutil.StringFromAny(trackMeta["genre"])
}
comment := stringFromAny(trackMeta["comment"])
description := stringFromAny(trackMeta["description"])
lyrics := stringFromAny(trackMeta["lyrics"])
if lrc := stringFromAny(trackMeta["lyrics_synced"]); lrc != "" {
comment := jsonutil.StringFromAny(trackMeta["comment"])
description := jsonutil.StringFromAny(trackMeta["description"])
lyrics := jsonutil.StringFromAny(trackMeta["lyrics"])
if lrc := jsonutil.StringFromAny(trackMeta["lyrics_synced"]); lrc != "" {
lyrics = lrc
}
trackGain := replaygainGainFromAny(trackMeta["replaygain_track_gain"])
@@ -1273,7 +1197,7 @@ func buildTagMetadata(trackMeta map[string]any, title, source, trackID string, o
}
albumGain := replaygainGainFromAny(trackMeta["replaygain_album_gain"])
if albumGain == "" {
albumGain = replaygainGainFromAny(nestedAny(trackMeta, "album", "replaygain_album_gain"))
albumGain = replaygainGainFromAny(jsonutil.NestedAny(trackMeta, "album", "replaygain_album_gain"))
}
trackPeak := replaygainPeakFromAny(trackMeta["replaygain_track_peak"])
if trackPeak == "" {
@@ -1281,22 +1205,22 @@ func buildTagMetadata(trackMeta map[string]any, title, source, trackID string, o
}
albumPeak := replaygainPeakFromAny(trackMeta["replaygain_album_peak"])
if albumPeak == "" {
albumPeak = replaygainPeakFromAny(nestedAny(trackMeta, "album", "replaygain_album_peak"))
albumPeak = replaygainPeakFromAny(jsonutil.NestedAny(trackMeta, "album", "replaygain_album_peak"))
}
sourceAlbumID := nestedString(trackMeta, "album", "id")
sourceAlbumID := jsonutil.NestedString(trackMeta, "album", "id")
if sourceAlbumID == "" {
sourceAlbumID = stringFromAny(trackMeta["source_album_id"])
sourceAlbumID = jsonutil.StringFromAny(trackMeta["source_album_id"])
}
sourceArtistID := nestedString(trackMeta, "artist", "id")
sourceArtistID := jsonutil.NestedString(trackMeta, "artist", "id")
if sourceArtistID == "" {
sourceArtistID = nestedString(trackMeta, "performer", "id")
sourceArtistID = jsonutil.NestedString(trackMeta, "performer", "id")
}
if sourceArtistID == "" {
sourceArtistID = stringFromAny(trackMeta["source_artist_id"])
sourceArtistID = jsonutil.StringFromAny(trackMeta["source_artist_id"])
}
sourceTrackID := trackID
if v := stringFromAny(trackMeta["source_track_id"]); v != "" {
if v := jsonutil.StringFromAny(trackMeta["source_track_id"]); v != "" {
sourceTrackID = v
}
@@ -1314,8 +1238,8 @@ func buildTagMetadata(trackMeta map[string]any, title, source, trackID string, o
Comment: comment,
Description: description,
Lyrics: lyrics,
Copyright: stringFromAny(trackMeta["copyright"]),
ISRC: stringFromAny(trackMeta["isrc"]),
Copyright: jsonutil.StringFromAny(trackMeta["copyright"]),
ISRC: jsonutil.StringFromAny(trackMeta["isrc"]),
ReplaygainTrackGain: trackGain,
ReplaygainAlbumGain: albumGain,
ReplaygainTrackPeak: trackPeak,