package ui import ( "bufio" "fmt" "os" "sort" "strconv" "strings" "qtransfer/internal/model" ) func ResolveSelection(lib model.Library, allPlaylists, includeLiked, nonInteractive bool, names []string) ([]model.Playlist, bool, error) { if allPlaylists || len(names) > 0 || nonInteractive { selected, liked, err := selectByFlags(lib, allPlaylists, includeLiked, names) if err != nil { return nil, false, err } if nonInteractive && len(selected) == 0 && !liked { return nil, false, fmt.Errorf("no selection provided in non-interactive mode (use --all, --playlist, or --liked)") } if len(selected) == 0 && !liked && !nonInteractive { return nil, false, fmt.Errorf("no playlists selected") } return selected, liked, nil } return interactiveSelection(lib) } func selectByFlags(lib model.Library, allPlaylists, includeLiked bool, names []string) ([]model.Playlist, bool, error) { if allPlaylists { return lib.Playlists, includeLiked, nil } if len(names) == 0 { return nil, includeLiked, nil } lookup := map[string]model.Playlist{} for _, p := range lib.Playlists { lookup[strings.ToLower(strings.TrimSpace(p.Name))] = p } selected := make([]model.Playlist, 0, len(names)) missing := []string{} seen := map[string]struct{}{} for _, n := range names { k := strings.ToLower(strings.TrimSpace(n)) p, ok := lookup[k] if !ok { missing = append(missing, n) continue } if _, exists := seen[p.SourceID]; exists { continue } seen[p.SourceID] = struct{}{} selected = append(selected, p) } if len(missing) > 0 { return nil, false, fmt.Errorf("playlist(s) not found: %s", strings.Join(missing, ", ")) } return selected, includeLiked, nil } func interactiveSelection(lib model.Library) ([]model.Playlist, bool, error) { fmt.Println("Fetched Spotify data:") for i, p := range lib.Playlists { fmt.Printf(" %2d) %s (%d tracks)\n", i+1, p.Name, len(p.Tracks)) } fmt.Printf(" L) %s (%d tracks)\n", lib.LikedName, len(lib.LikedSongs)) fmt.Println("\nSelect playlists to transfer. Examples: 1,2,5-8,L or A for all playlists + liked songs") scanner := bufio.NewScanner(os.Stdin) for { fmt.Print("Selection: ") if !scanner.Scan() { if scanner.Err() != nil { return nil, false, scanner.Err() } return nil, false, fmt.Errorf("input closed") } line := strings.TrimSpace(scanner.Text()) if line == "" { continue } idxs, liked, all, err := parseSelection(line, len(lib.Playlists)) if err != nil { fmt.Printf("Invalid selection: %v\n", err) continue } if all { return lib.Playlists, true, nil } selected := make([]model.Playlist, 0, len(idxs)) for _, idx := range idxs { selected = append(selected, lib.Playlists[idx-1]) } if len(selected) == 0 && !liked { fmt.Println("No playlists selected. Please choose at least one playlist or L.") continue } return selected, liked, nil } } func parseSelection(s string, max int) ([]int, bool, bool, error) { parts := strings.Split(strings.ToUpper(s), ",") set := map[int]struct{}{} liked := false all := false for _, p := range parts { p = strings.TrimSpace(p) if p == "" { continue } switch p { case "A", "ALL": all = true liked = true continue case "L", "LIKED": liked = true continue } if strings.Contains(p, "-") { r := strings.SplitN(p, "-", 2) if len(r) != 2 { return nil, false, false, fmt.Errorf("invalid range %q", p) } start, err := strconv.Atoi(strings.TrimSpace(r[0])) if err != nil { return nil, false, false, fmt.Errorf("invalid range start in %q", p) } end, err := strconv.Atoi(strings.TrimSpace(r[1])) if err != nil { return nil, false, false, fmt.Errorf("invalid range end in %q", p) } if start < 1 || end < 1 || start > max || end > max || end < start { return nil, false, false, fmt.Errorf("range out of bounds: %q", p) } for i := start; i <= end; i++ { set[i] = struct{}{} } continue } n, err := strconv.Atoi(p) if err != nil { return nil, false, false, fmt.Errorf("invalid token %q", p) } if n < 1 || n > max { return nil, false, false, fmt.Errorf("playlist index out of bounds: %d", n) } set[n] = struct{}{} } idxs := make([]int, 0, len(set)) for i := range set { idxs = append(idxs, i) } sort.Ints(idxs) return idxs, liked, all, nil }