This commit is contained in:
joren
2026-04-04 20:38:08 +02:00
parent ea32c0baa6
commit c131bf6ab1
4 changed files with 4301 additions and 2192 deletions

View File

@@ -140,6 +140,7 @@ func run() error {
PublicPlaylists: cfg.PublicPlaylists, PublicPlaylists: cfg.PublicPlaylists,
Concurrency: cfg.Concurrency, Concurrency: cfg.Concurrency,
LikedName: cfg.LikedPlaylist, LikedName: cfg.LikedPlaylist,
TargetPlaylistID: cfg.TargetPlaylistID,
Progress: func(msg string) { Progress: func(msg string) {
fmt.Printf("\r%-140s", msg) fmt.Printf("\r%-140s", msg)
}, },
@@ -644,12 +645,12 @@ func buildPlaylistPlans(cfg config.Config, fileEntries []jobconfig.PlaylistEntry
} }
if cfg.TargetPlaylistID > 0 { if cfg.TargetPlaylistID > 0 {
uniqueIDs := map[string]struct{}{} // Apply the global target to all plans that don't already have a per-playlist target.
for id := range plans { for id, plan := range plans {
uniqueIDs[id] = struct{}{} if plan.TargetPlaylistID == 0 {
plan.TargetPlaylistID = cfg.TargetPlaylistID
plans[id] = plan
} }
if len(uniqueIDs) != 1 {
return nil, nil, fmt.Errorf("--target-playlist-id can only be used with exactly one source playlist URL")
} }
} }

View File

@@ -117,7 +117,7 @@ func Load() (Config, error) {
flag.Var(&playlists, "playlist", "Playlist name to transfer (repeatable)") flag.Var(&playlists, "playlist", "Playlist name to transfer (repeatable)")
flag.Var(&playlistURLs, "playlist-url", "Spotify playlist URL/URI/ID to transfer (repeatable)") flag.Var(&playlistURLs, "playlist-url", "Spotify playlist URL/URI/ID to transfer (repeatable)")
if err := flag.CommandLine.Parse(parseArgs); err != nil { if err := flag.CommandLine.Parse(preprocessArgs(parseArgs)); err != nil {
return Config{}, err return Config{}, err
} }
cfg.Command = command cfg.Command = command
@@ -158,9 +158,6 @@ func (c Config) Validate() error {
if c.TargetPlaylistID < 0 { if c.TargetPlaylistID < 0 {
return fmt.Errorf("target-playlist-id must be >= 0") return fmt.Errorf("target-playlist-id must be >= 0")
} }
if c.TargetPlaylistID > 0 && !c.Monitor {
return fmt.Errorf("target-playlist-id is currently supported with --monitor mode")
}
if strings.TrimSpace(c.SessionPath) == "" { if strings.TrimSpace(c.SessionPath) == "" {
return fmt.Errorf("session file path cannot be empty") return fmt.Errorf("session file path cannot be empty")
} }
@@ -190,6 +187,48 @@ func splitComma(s string) []string {
return res return res
} }
type boolFlag interface {
IsBoolFlag() bool
}
// preprocessArgs moves non-flag positional arguments to the end so that flags
// can appear in any order relative to non-flag arguments.
func preprocessArgs(args []string) []string {
boolFlagNames := make(map[string]bool)
flag.CommandLine.VisitAll(func(f *flag.Flag) {
if bf, ok := f.Value.(boolFlag); ok && bf.IsBoolFlag() {
boolFlagNames[f.Name] = true
}
})
var flagArgs []string
var positional []string
i := 0
for i < len(args) {
arg := args[i]
if !strings.HasPrefix(arg, "-") {
positional = append(positional, arg)
i++
continue
}
flagArgs = append(flagArgs, arg)
i++
name := strings.TrimLeft(arg, "-")
if idx := strings.Index(name, "="); idx >= 0 {
continue // value embedded in --flag=value form
}
if boolFlagNames[name] {
continue // bool flags don't consume a following value
}
if i < len(args) && !strings.HasPrefix(args[i], "-") {
flagArgs = append(flagArgs, args[i])
i++
}
}
return append(flagArgs, positional...)
}
func envOr(key, fallback string) string { func envOr(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" { if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v return v

View File

@@ -24,6 +24,7 @@ type Config struct {
PublicPlaylists bool PublicPlaylists bool
Concurrency int Concurrency int
LikedName string LikedName string
TargetPlaylistID int64 // when set, add tracks to this existing playlist instead of creating a new one
Progress ProgressFunc Progress ProgressFunc
} }
@@ -87,14 +88,21 @@ func processPlaylist(ctx context.Context, cfg Config, writer QobuzWriter, matche
return res return res
} }
var playlistID int64
if cfg.TargetPlaylistID > 0 {
playlistID = cfg.TargetPlaylistID
res.TargetID = playlistID
} else {
notify(cfg, fmt.Sprintf("Transfer %d/%d creating playlist: %s", playlistIndex, playlistTotal, shortName(pl.Name))) notify(cfg, fmt.Sprintf("Transfer %d/%d creating playlist: %s", playlistIndex, playlistTotal, shortName(pl.Name)))
playlistID, err := writer.CreatePlaylist(ctx, pl.Name, sanitizeDescription(pl.Description), cfg.PublicPlaylists) created, err := writer.CreatePlaylist(ctx, pl.Name, sanitizeDescription(pl.Description), cfg.PublicPlaylists)
if err != nil { if err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("create playlist: %v", err)) res.Errors = append(res.Errors, fmt.Sprintf("create playlist: %v", err))
notify(cfg, fmt.Sprintf("Transfer %d/%d failed creating playlist: %s", playlistIndex, playlistTotal, shortName(pl.Name))) notify(cfg, fmt.Sprintf("Transfer %d/%d failed creating playlist: %s", playlistIndex, playlistTotal, shortName(pl.Name)))
return res return res
} }
playlistID = created
res.TargetID = playlistID res.TargetID = playlistID
}
ids := uniqueIDs(matched) ids := uniqueIDs(matched)
if len(ids) == 0 { if len(ids) == 0 {

File diff suppressed because it is too large Load Diff