improve CLI error semantics and soundcloud canonicalization

Auto-upgrade outdated configs on startup, add actionable SSL verification hints in rip error paths, and harden SoundCloud search/metadata with canonical URL handling and richer source IDs.
This commit is contained in:
2026-04-20 15:16:59 +02:00
parent 0748d5a325
commit 0ba8faa943
9 changed files with 502 additions and 106 deletions

View File

@@ -36,6 +36,11 @@ type Main struct {
Media []media.Media
}
type PlaylistTrackRef struct {
Source string
ID string
}
type ripTrackOptions struct {
albumFolder string
albumEmbedCover string
@@ -219,6 +224,37 @@ func (m *Main) AddPlaylistByTrackIDs(ctx context.Context, source, playlistID, pl
return nil
}
func (m *Main) AddMixedPlaylistByTrackRefs(ctx context.Context, playlistID, playlistName string, refs []PlaylistTrackRef) error {
if strings.TrimSpace(playlistName) == "" {
playlistName = playlistID
}
valid := make([]PlaylistTrackRef, 0, len(refs))
for _, ref := range refs {
source := strings.TrimSpace(ref.Source)
id := strings.TrimSpace(ref.ID)
if source == "" || id == "" {
continue
}
valid = append(valid, PlaylistTrackRef{Source: source, ID: id})
}
if len(valid) == 0 {
return fmt.Errorf("playlist %q has no track refs", playlistName)
}
pending := media.PendingFunc{
ResolveFn: func(context.Context) (media.Media, error) {
return media.MediaFunc{RipFn: func(ctx context.Context) error {
if m.Config.Session.CLI.TextOutput {
m.logf("Downloading playlist: %s\n", playlistName)
}
return m.ripPlaylistMixed(ctx, playlistID, playlistName, valid)
}}, nil
},
}
m.Pending = append(m.Pending, pending)
return nil
}
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 != "" {
@@ -679,6 +715,56 @@ func (m *Main) ripPlaylist(ctx context.Context, p provider.Client, source, playl
return nil
}
func (m *Main) ripPlaylistMixed(ctx context.Context, playlistID, name string, refs []PlaylistTrackRef) error {
folder := filepath.Join(m.Config.Session.Downloads.Folder, naming.CleanName(name, naming.Config{
RestrictCharacters: m.Config.Session.Filepaths.RestrictCharacters,
TruncateTo: m.Config.Session.Filepaths.TruncateTo,
}))
total := len(refs)
m.logf("Playlist: %s (%d tracks)\n", name, total)
failures := 0
providerCache := map[string]provider.Client{}
getProvider := func(source string) (provider.Client, error) {
if p, ok := providerCache[source]; ok {
return p, nil
}
p, err := m.GetLoggedInProvider(ctx, source)
if err != nil {
return nil, err
}
providerCache[source] = p
return p, nil
}
for i, ref := range refs {
p, err := getProvider(ref.Source)
if err != nil {
failures++
m.logf("track failed: id=%s source=%s reason=%v\n", ref.ID, ref.Source, err)
continue
}
opts := ripTrackOptions{
albumFolder: folder,
index: i + 1,
total: total,
forPlaylist: true,
playlistName: name,
playlistPos: i + 1,
}
if err = m.ripTrack(ctx, p, ref.Source, ref.ID, "", opts); err != nil {
failures++
m.logf("track failed: id=%s source=%s reason=%v\n", ref.ID, ref.Source, err)
}
}
if failures > 0 {
m.logf("Playlist done with %d failed track(s)\n", failures)
}
return nil
}
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 {