mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-07-27 23:42:28 +02:00
feat: add beatport provider
This commit is contained in:
+173
-2
@@ -21,6 +21,7 @@ import (
|
||||
"streamrip-go/internal/jsonutil"
|
||||
"streamrip-go/internal/naming"
|
||||
"streamrip-go/internal/provider"
|
||||
beatportprovider "streamrip-go/internal/provider/beatport"
|
||||
deezerprovider "streamrip-go/internal/provider/deezer"
|
||||
qobuzprovider "streamrip-go/internal/provider/qobuz"
|
||||
soundcloudprovider "streamrip-go/internal/provider/soundcloud"
|
||||
@@ -110,6 +111,7 @@ func New(cfg *config.Config) (*Main, error) {
|
||||
}
|
||||
|
||||
providers := map[string]provider.Client{
|
||||
"beatport": beatportprovider.New(cfg),
|
||||
"qobuz": qobuzprovider.New(cfg),
|
||||
"tidal": tidalprovider.New(cfg),
|
||||
"deezer": deezerprovider.New(cfg),
|
||||
@@ -203,7 +205,12 @@ func (m *Main) AddByID(ctx context.Context, source, mediaType, id string) error
|
||||
case "artist":
|
||||
return m.ripCollection(ctx, p, source, "Artist", id, meta)
|
||||
case "label":
|
||||
if source == "beatport" {
|
||||
return m.ripTrackCollection(ctx, p, source, "Label", id, meta, false)
|
||||
}
|
||||
return m.ripCollection(ctx, p, source, "Label", id, meta)
|
||||
case "chart":
|
||||
return m.ripTrackCollection(ctx, p, source, "Chart", id, meta, true)
|
||||
case "video":
|
||||
return m.ripVideo(ctx, p, source, id, meta)
|
||||
default:
|
||||
@@ -327,6 +334,98 @@ func (m *Main) ripCollection(ctx context.Context, p provider.Client, source, kin
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Main) ripTrackCollection(ctx context.Context, p provider.Client, source, kind, id string, meta map[string]any, playlistLike bool) error {
|
||||
if err := m.requireSourceDownloadAuth(source); err != nil {
|
||||
return err
|
||||
}
|
||||
name := titleFromMetadata(meta, id)
|
||||
if n := jsonutil.StringFromAny(meta["name"]); n != "" {
|
||||
name = n
|
||||
}
|
||||
base := m.Config.Session.Downloads.Folder
|
||||
if m.Config.Session.Downloads.SourceSubdirectories {
|
||||
base = filepath.Join(base, jsonutil.TitleCase(source))
|
||||
}
|
||||
folder := filepath.Join(base, naming.CleanName(name, naming.Config{RestrictCharacters: m.Config.Session.Filepaths.RestrictCharacters, TruncateTo: m.Config.Session.Filepaths.TruncateTo}))
|
||||
tracksMap, ok := meta["tracks"].(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s missing tracks data", strings.ToLower(kind))
|
||||
}
|
||||
rawItems := make([]any, 0)
|
||||
switch items := tracksMap["items"].(type) {
|
||||
case []any:
|
||||
rawItems = items
|
||||
case []map[string]any:
|
||||
for _, item := range items {
|
||||
rawItems = append(rawItems, item)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%s tracks missing items", strings.ToLower(kind))
|
||||
}
|
||||
ids := make([]string, 0, len(rawItems))
|
||||
for _, raw := range rawItems {
|
||||
itm, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if id := jsonutil.StringFromAny(itm["id"]); id != "" {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
m.logf("%s: %s (%d tracks)\n", kind, name, len(ids))
|
||||
failures := 0
|
||||
runOne := func(i int, trackID string) {
|
||||
opts := ripTrackOptions{albumFolder: folder, index: i, total: len(ids)}
|
||||
if playlistLike {
|
||||
opts.forPlaylist = true
|
||||
opts.playlistName = name
|
||||
opts.playlistPos = i
|
||||
}
|
||||
if err := m.ripTrack(ctx, p, source, trackID, "", opts); err != nil {
|
||||
failures++
|
||||
m.logf("track failed: id=%s reason=%v\n", trackID, err)
|
||||
}
|
||||
}
|
||||
if !m.Config.Session.Downloads.Concurrency || m.Config.Session.Downloads.MaxConnections == 1 {
|
||||
for i, trackID := range ids {
|
||||
runOne(i+1, trackID)
|
||||
}
|
||||
} else {
|
||||
maxWorkers := m.Config.Session.Downloads.MaxConnections
|
||||
if maxWorkers <= 0 {
|
||||
maxWorkers = 6
|
||||
}
|
||||
sem := make(chan struct{}, maxWorkers)
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
for i, trackID := range ids {
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(pos int, tid string) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
opts := ripTrackOptions{albumFolder: folder, index: pos, total: len(ids)}
|
||||
if playlistLike {
|
||||
opts.forPlaylist = true
|
||||
opts.playlistName = name
|
||||
opts.playlistPos = pos
|
||||
}
|
||||
if err := m.ripTrack(ctx, p, source, tid, "", opts); err != nil {
|
||||
mu.Lock()
|
||||
failures++
|
||||
m.logf("track failed: id=%s reason=%v\n", tid, err)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(i+1, trackID)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
if failures > 0 {
|
||||
m.logf("%s done with %d failed track(s)\n", kind, failures)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Main) ripVideo(ctx context.Context, p provider.Client, source, videoID string, meta map[string]any) error {
|
||||
alreadyDownloaded, err := m.Store.IsDownloaded(ctx, source, videoID)
|
||||
if err == nil && alreadyDownloaded && !m.IgnoreDB {
|
||||
@@ -924,8 +1023,11 @@ downloaded:
|
||||
embedCoverPath = res.EmbedPath
|
||||
}
|
||||
}
|
||||
} else if opts.albumFolder == "" {
|
||||
parent := filepath.Dir(outPath)
|
||||
} else if embedCoverPath == "" {
|
||||
parent := opts.albumFolder
|
||||
if parent == "" {
|
||||
parent = filepath.Dir(outPath)
|
||||
}
|
||||
if res, prepErr := artwork.Prepare(ctx, m.DL, parent, trackMetaAlbum(meta), m.Config.Session.Artwork, false); prepErr == nil {
|
||||
if res.EmbedPath != "" {
|
||||
embedCoverPath = res.EmbedPath
|
||||
@@ -971,6 +1073,8 @@ func (m *Main) qualityForSource(source string) int {
|
||||
return m.Config.Session.Deezer.Quality
|
||||
case "yandex":
|
||||
return m.Config.Session.Yandex.Quality
|
||||
case "beatport":
|
||||
return m.Config.Session.Beatport.Quality
|
||||
case "soundcloud":
|
||||
return m.Config.Session.Soundcloud.Quality
|
||||
default:
|
||||
@@ -1313,6 +1417,11 @@ func buildTagMetadata(trackMeta map[string]any, title, source, trackID string, o
|
||||
if genre == "" {
|
||||
genre = jsonutil.StringFromAny(trackMeta["genre"])
|
||||
}
|
||||
initialKey := jsonutil.FirstNonEmpty(
|
||||
jsonutil.StringFromAny(trackMeta["key"]),
|
||||
jsonutil.StringFromAny(trackMeta["initialkey"]),
|
||||
)
|
||||
initialKey = normalizeInitialKey(initialKey)
|
||||
|
||||
comment := jsonutil.StringFromAny(trackMeta["comment"])
|
||||
description := jsonutil.StringFromAny(trackMeta["description"])
|
||||
@@ -1368,6 +1477,7 @@ func buildTagMetadata(trackMeta map[string]any, title, source, trackID string, o
|
||||
DiscTotal: discTotal,
|
||||
Date: date,
|
||||
Genre: genre,
|
||||
InitialKey: initialKey,
|
||||
Comment: comment,
|
||||
Description: description,
|
||||
Lyrics: lyrics,
|
||||
@@ -1384,6 +1494,67 @@ func buildTagMetadata(trackMeta map[string]any, title, source, trackID string, o
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeInitialKey(in string) string {
|
||||
s := strings.TrimSpace(in)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Fields(s)
|
||||
if len(parts) >= 2 {
|
||||
switch strings.ToLower(parts[1]) {
|
||||
case "major", "maj":
|
||||
if root := normalizeMajorKeyRoot(parts[0]); root != "" {
|
||||
return root
|
||||
}
|
||||
case "minor", "min":
|
||||
if root := normalizeMinorKeyRoot(parts[0]); root != "" {
|
||||
return root + "m"
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(s, "m") && len(s) > 1 {
|
||||
root := normalizeMinorKeyRoot(strings.TrimSuffix(s, "m"))
|
||||
if root != "" {
|
||||
return root + "m"
|
||||
}
|
||||
}
|
||||
if root := normalizeMajorKeyRoot(s); root != "" {
|
||||
return root
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func normalizeMajorKeyRoot(root string) string {
|
||||
s := normalizeKeyRootCase(root)
|
||||
switch s {
|
||||
case "C", "Db", "D", "Eb", "E", "F", "F#", "Gb", "G", "Ab", "A", "Bb", "B":
|
||||
return s
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeMinorKeyRoot(root string) string {
|
||||
s := normalizeKeyRootCase(root)
|
||||
switch s {
|
||||
case "C", "C#", "D", "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B":
|
||||
return s
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeKeyRootCase(root string) string {
|
||||
s := strings.TrimSpace(root)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
if len(s) >= 1 {
|
||||
s = strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func applyPlaylistMetadataOverrides(meta map[string]any, cfg config.MetadataConfig, playlistName string, position int) {
|
||||
if cfg.RenumberPlaylistTracks && position > 0 {
|
||||
meta["track_number"] = position
|
||||
|
||||
Reference in New Issue
Block a user