mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
improve search menu preview and artist release details
This commit is contained in:
@@ -19,6 +19,8 @@ type searchResult struct {
|
||||
Title string
|
||||
Artist string
|
||||
Album string
|
||||
Date string
|
||||
Releases int
|
||||
TrackCount int
|
||||
Explicit bool
|
||||
}
|
||||
@@ -191,31 +193,40 @@ func promptSearchSelectionMenu(source, mediaType, query string, results []search
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
prevTemplate := survey.MultiSelectQuestionTemplate
|
||||
survey.MultiSelectQuestionTemplate = streamripLikeSearchTemplate
|
||||
defer func() {
|
||||
survey.MultiSelectQuestionTemplate = prevTemplate
|
||||
}()
|
||||
|
||||
labels := make([]string, 0, len(results))
|
||||
labelToIndex := map[string]int{}
|
||||
for i, r := range results {
|
||||
artist := strings.TrimSpace(r.Artist)
|
||||
if artist == "" {
|
||||
artist = "Unknown Artist"
|
||||
}
|
||||
label := fmt.Sprintf("%2d. %s - %s", i+1, artist, r.Title)
|
||||
label := formatSearchOptionLabel(i+1, mediaType, r)
|
||||
labels = append(labels, label)
|
||||
labelToIndex[label] = i
|
||||
}
|
||||
pageSize := len(labels)
|
||||
if pageSize < 15 {
|
||||
pageSize = 15
|
||||
}
|
||||
if pageSize > 30 {
|
||||
pageSize = 30
|
||||
}
|
||||
|
||||
selected := []string{}
|
||||
prompt := &survey.MultiSelect{
|
||||
Message: fmt.Sprintf("Results for %s '%s' from %s", mediaType, query, jsonutil.TitleCase(source)),
|
||||
Help: "SPACE: select ENTER: download /: filter ESC: cancel",
|
||||
Help: "SPACE - select, ENTER - download, ESC - exit",
|
||||
Options: labels,
|
||||
Description: func(value string, index int) string {
|
||||
resultIndex, ok := labelToIndex[value]
|
||||
if !ok || resultIndex < 0 || resultIndex >= len(results) {
|
||||
return ""
|
||||
}
|
||||
return formatSearchDetails(results[resultIndex])
|
||||
return formatSearchDetails(mediaType, results[resultIndex])
|
||||
},
|
||||
PageSize: 15,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &selected); err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "interrupt") {
|
||||
@@ -241,6 +252,22 @@ func promptSearchSelectionMenu(source, mediaType, query string, results []search
|
||||
return out, nil
|
||||
}
|
||||
|
||||
const streamripLikeSearchTemplate = `
|
||||
{{ color "default+hb"}}{{ .Message }}{{color "reset"}}
|
||||
{{- if .Help }}
|
||||
{{ .Help }}
|
||||
{{- end }}
|
||||
{{- range $ix, $option := .PageEntries }}
|
||||
{{ if eq $.SelectedIndex $ix }}>{{ else }} {{ end }} [{{ if index $.Checked $option.Index }}x{{ else }} {{ end }}] {{ $option.Value }}
|
||||
{{- end }}
|
||||
|
||||
preview
|
||||
{{- if gt (len .PageEntries) 0 }}
|
||||
{{ $current := index .PageEntries .SelectedIndex }}
|
||||
{{ $.GetDescription $current }}
|
||||
{{- end }}
|
||||
`
|
||||
|
||||
func writeSearchResultsToFile(source, mediaType string, results []searchResult, path string) error {
|
||||
type outItem struct {
|
||||
Source string `json:"source"`
|
||||
@@ -391,7 +418,23 @@ func normalizeSearchResults(source, mediaType string, pages []map[string]any) []
|
||||
trackCount = searchInt(itm["track_count"])
|
||||
}
|
||||
explicit := searchBool(itm["parental_warning"])
|
||||
appendUnique(searchResult{ID: id, Title: title, Artist: artist, Album: album, TrackCount: trackCount, Explicit: explicit})
|
||||
date := firstNonEmpty(
|
||||
asString(itm["release_date_original"]),
|
||||
asString(itm["release_date"]),
|
||||
asString(itm["releaseDate"]),
|
||||
asString(itm["streamStartDate"]),
|
||||
nestedSearchString(itm, "album", "release_date_original"),
|
||||
nestedSearchString(itm, "album", "release_date"),
|
||||
)
|
||||
releases := 0
|
||||
if mediaType == "artist" {
|
||||
releases = firstPositiveInt(
|
||||
searchInt(itm["albums_count"]),
|
||||
searchInt(itm["albumsCount"]),
|
||||
nestedSearchInt(itm, "albums", "total"),
|
||||
)
|
||||
}
|
||||
appendUnique(searchResult{ID: id, Title: title, Artist: artist, Album: album, Date: date, Releases: releases, TrackCount: trackCount, Explicit: explicit})
|
||||
}
|
||||
case "tidal":
|
||||
items, ok := page["items"].([]any)
|
||||
@@ -425,7 +468,23 @@ func normalizeSearchResults(source, mediaType string, pages []map[string]any) []
|
||||
trackCount = searchInt(itm["tracks_count"])
|
||||
}
|
||||
explicit := searchBool(itm["explicit"])
|
||||
appendUnique(searchResult{ID: id, Title: title, Artist: artist, Album: album, TrackCount: trackCount, Explicit: explicit})
|
||||
date := firstNonEmpty(
|
||||
asString(itm["releaseDate"]),
|
||||
asString(itm["streamStartDate"]),
|
||||
asString(itm["release_date"]),
|
||||
nestedSearchString(itm, "album", "releaseDate"),
|
||||
nestedSearchString(itm, "album", "release_date"),
|
||||
)
|
||||
releases := 0
|
||||
if mediaType == "artist" {
|
||||
releases = firstPositiveInt(
|
||||
searchInt(itm["numberOfAlbums"]),
|
||||
searchInt(itm["albums_count"]),
|
||||
searchInt(itm["albumsCount"]),
|
||||
nestedSearchInt(itm, "albums", "total"),
|
||||
)
|
||||
}
|
||||
appendUnique(searchResult{ID: id, Title: title, Artist: artist, Album: album, Date: date, Releases: releases, TrackCount: trackCount, Explicit: explicit})
|
||||
}
|
||||
case "deezer":
|
||||
key := mediaType + "s"
|
||||
@@ -451,7 +510,19 @@ func normalizeSearchResults(source, mediaType string, pages []map[string]any) []
|
||||
album := nestedSearchString(itm, "album", "title")
|
||||
trackCount := searchInt(itm["nb_tracks"])
|
||||
explicit := searchBool(itm["explicit_lyrics"])
|
||||
appendUnique(searchResult{ID: id, Title: title, Artist: artist, Album: album, TrackCount: trackCount, Explicit: explicit})
|
||||
date := firstNonEmpty(
|
||||
asString(itm["release_date"]),
|
||||
nestedSearchString(itm, "album", "release_date"),
|
||||
)
|
||||
releases := 0
|
||||
if mediaType == "artist" {
|
||||
releases = firstPositiveInt(
|
||||
searchInt(itm["nb_album"]),
|
||||
searchInt(itm["albums_count"]),
|
||||
searchInt(itm["numberOfAlbums"]),
|
||||
)
|
||||
}
|
||||
appendUnique(searchResult{ID: id, Title: title, Artist: artist, Album: album, Date: date, Releases: releases, TrackCount: trackCount, Explicit: explicit})
|
||||
}
|
||||
case "soundcloud":
|
||||
items, ok := page["items"].([]any)
|
||||
@@ -467,32 +538,80 @@ func normalizeSearchResults(source, mediaType string, pages []map[string]any) []
|
||||
title := asString(itm["title"])
|
||||
artist := nestedSearchString(itm, "artist", "name")
|
||||
trackCount := searchInt(itm["tracks_count"])
|
||||
appendUnique(searchResult{ID: id, Title: title, Artist: artist, TrackCount: trackCount})
|
||||
date := firstNonEmpty(
|
||||
asString(itm["release_date"]),
|
||||
asString(itm["display_date"]),
|
||||
)
|
||||
appendUnique(searchResult{ID: id, Title: title, Artist: artist, Date: date, TrackCount: trackCount})
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func formatSearchDetails(r searchResult) string {
|
||||
lines := []string{"Selected item", ""}
|
||||
lines = append(lines, fmt.Sprintf("Title : %s", r.Title))
|
||||
func formatSearchDetails(mediaType string, r searchResult) string {
|
||||
date := strings.TrimSpace(r.Date)
|
||||
if date == "" {
|
||||
date = "Unknown"
|
||||
}
|
||||
lines := []string{}
|
||||
switch mediaType {
|
||||
case "album":
|
||||
lines = append(lines, "Date released:", date)
|
||||
if r.TrackCount > 0 {
|
||||
lines = append(lines, "", fmt.Sprintf("%d Tracks", r.TrackCount))
|
||||
}
|
||||
case "artist":
|
||||
if strings.TrimSpace(r.Title) != "" {
|
||||
lines = append(lines, r.Title)
|
||||
}
|
||||
if r.Releases > 0 {
|
||||
lines = append(lines, "", fmt.Sprintf("%d Releases", r.Releases))
|
||||
}
|
||||
case "track":
|
||||
lines = append(lines, "Released on:", date)
|
||||
case "playlist":
|
||||
if r.TrackCount > 0 {
|
||||
lines = append(lines, fmt.Sprintf("%d Tracks", r.TrackCount))
|
||||
}
|
||||
default:
|
||||
if strings.TrimSpace(r.Title) != "" {
|
||||
lines = append(lines, r.Title)
|
||||
}
|
||||
if strings.TrimSpace(r.Artist) != "" {
|
||||
lines = append(lines, fmt.Sprintf("Artist : %s", r.Artist))
|
||||
lines = append(lines, strings.TrimSpace(r.Artist))
|
||||
}
|
||||
if strings.TrimSpace(r.Album) != "" {
|
||||
lines = append(lines, fmt.Sprintf("Album : %s", r.Album))
|
||||
lines = append(lines, strings.TrimSpace(r.Album))
|
||||
}
|
||||
if r.TrackCount > 0 {
|
||||
lines = append(lines, fmt.Sprintf("Tracks : %d", r.TrackCount))
|
||||
lines = append(lines, fmt.Sprintf("%d Tracks", r.TrackCount))
|
||||
}
|
||||
if r.Explicit {
|
||||
lines = append(lines, "Explicit: yes")
|
||||
lines = append(lines, "Explicit")
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("ID : %s", r.ID))
|
||||
}
|
||||
lines = append(lines, "", "ID: "+r.ID)
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func formatSearchOptionLabel(index int, mediaType string, r searchResult) string {
|
||||
title := strings.TrimSpace(r.Title)
|
||||
if title == "" {
|
||||
title = "Unknown"
|
||||
}
|
||||
artist := strings.TrimSpace(r.Artist)
|
||||
switch mediaType {
|
||||
case "artist", "label":
|
||||
return fmt.Sprintf("%d. %s", index, title)
|
||||
default:
|
||||
if artist == "" {
|
||||
return fmt.Sprintf("%d. %s", index, title)
|
||||
}
|
||||
return fmt.Sprintf("%d. %s by %s", index, title, artist)
|
||||
}
|
||||
}
|
||||
|
||||
func nestedSearchString(v map[string]any, keys ...string) string {
|
||||
cur := any(v)
|
||||
for _, key := range keys {
|
||||
@@ -505,6 +624,27 @@ func nestedSearchString(v map[string]any, keys ...string) string {
|
||||
return asString(cur)
|
||||
}
|
||||
|
||||
func nestedSearchInt(v map[string]any, keys ...string) int {
|
||||
cur := any(v)
|
||||
for _, key := range keys {
|
||||
m, ok := cur.(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
cur = m[key]
|
||||
}
|
||||
return searchInt(cur)
|
||||
}
|
||||
|
||||
func firstPositiveInt(values ...int) int {
|
||||
for _, v := range values {
|
||||
if v > 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func searchInt(v any) int {
|
||||
switch t := v.(type) {
|
||||
case int:
|
||||
|
||||
87
cmd/rip/search_test.go
Normal file
87
cmd/rip/search_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFormatSearchDetailsAlbum(t *testing.T) {
|
||||
r := searchResult{ID: "9399700017915", Date: "1994-02-01", TrackCount: 18}
|
||||
got := formatSearchDetails("album", r)
|
||||
want := "Date released:\n1994-02-01\n\n18 Tracks\n\nID: 9399700017915"
|
||||
if got != want {
|
||||
t.Fatalf("unexpected album details:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSearchDetailsTrackDefaultsDate(t *testing.T) {
|
||||
r := searchResult{ID: "3083287"}
|
||||
got := formatSearchDetails("track", r)
|
||||
want := "Released on:\nUnknown\n\nID: 3083287"
|
||||
if got != want {
|
||||
t.Fatalf("unexpected track details:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSearchDetailsArtistWithReleases(t *testing.T) {
|
||||
r := searchResult{ID: "1863897", Title: "Lost Frequencies", Releases: 23}
|
||||
got := formatSearchDetails("artist", r)
|
||||
want := "Lost Frequencies\n\n23 Releases\n\nID: 1863897"
|
||||
if got != want {
|
||||
t.Fatalf("unexpected artist details:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSearchResultsQobuzExtractsDate(t *testing.T) {
|
||||
pages := []map[string]any{
|
||||
{"albums": map[string]any{"items": []any{
|
||||
map[string]any{
|
||||
"id": "1",
|
||||
"title": "Master of Reality",
|
||||
"release_date_original": "1971-07-21",
|
||||
"artist": map[string]any{"name": "Black Sabbath"},
|
||||
},
|
||||
}}},
|
||||
}
|
||||
results := normalizeSearchResults("qobuz", "album", pages)
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("len(results)=%d want 1", len(results))
|
||||
}
|
||||
if results[0].Date != "1971-07-21" {
|
||||
t.Fatalf("date=%q want 1971-07-21", results[0].Date)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSearchOptionLabelArtistNoUnknownArtistSuffix(t *testing.T) {
|
||||
r := searchResult{Title: "Lost Frequencies"}
|
||||
got := formatSearchOptionLabel(1, "artist", r)
|
||||
want := "1. Lost Frequencies"
|
||||
if got != want {
|
||||
t.Fatalf("label=%q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSearchOptionLabelTrackWithArtist(t *testing.T) {
|
||||
r := searchResult{Title: "Dreams", Artist: "Fleetwood Mac"}
|
||||
got := formatSearchOptionLabel(2, "track", r)
|
||||
want := "2. Dreams by Fleetwood Mac"
|
||||
if got != want {
|
||||
t.Fatalf("label=%q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSearchResultsQobuzArtistExtractsReleases(t *testing.T) {
|
||||
pages := []map[string]any{
|
||||
{"artists": map[string]any{"items": []any{
|
||||
map[string]any{
|
||||
"id": "1863897",
|
||||
"name": "Lost Frequencies",
|
||||
"albums_count": 23,
|
||||
},
|
||||
}}},
|
||||
}
|
||||
results := normalizeSearchResults("qobuz", "artist", pages)
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("len(results)=%d want 1", len(results))
|
||||
}
|
||||
if results[0].Releases != 23 {
|
||||
t.Fatalf("releases=%d want 23", results[0].Releases)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user