175 lines
4.3 KiB
Go
175 lines
4.3 KiB
Go
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
|
|
}
|