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
|
Title string
|
||||||
Artist string
|
Artist string
|
||||||
Album string
|
Album string
|
||||||
|
Date string
|
||||||
|
Releases int
|
||||||
TrackCount int
|
TrackCount int
|
||||||
Explicit bool
|
Explicit bool
|
||||||
}
|
}
|
||||||
@@ -191,31 +193,40 @@ func promptSearchSelectionMenu(source, mediaType, query string, results []search
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevTemplate := survey.MultiSelectQuestionTemplate
|
||||||
|
survey.MultiSelectQuestionTemplate = streamripLikeSearchTemplate
|
||||||
|
defer func() {
|
||||||
|
survey.MultiSelectQuestionTemplate = prevTemplate
|
||||||
|
}()
|
||||||
|
|
||||||
labels := make([]string, 0, len(results))
|
labels := make([]string, 0, len(results))
|
||||||
labelToIndex := map[string]int{}
|
labelToIndex := map[string]int{}
|
||||||
for i, r := range results {
|
for i, r := range results {
|
||||||
artist := strings.TrimSpace(r.Artist)
|
label := formatSearchOptionLabel(i+1, mediaType, r)
|
||||||
if artist == "" {
|
|
||||||
artist = "Unknown Artist"
|
|
||||||
}
|
|
||||||
label := fmt.Sprintf("%2d. %s - %s", i+1, artist, r.Title)
|
|
||||||
labels = append(labels, label)
|
labels = append(labels, label)
|
||||||
labelToIndex[label] = i
|
labelToIndex[label] = i
|
||||||
}
|
}
|
||||||
|
pageSize := len(labels)
|
||||||
|
if pageSize < 15 {
|
||||||
|
pageSize = 15
|
||||||
|
}
|
||||||
|
if pageSize > 30 {
|
||||||
|
pageSize = 30
|
||||||
|
}
|
||||||
|
|
||||||
selected := []string{}
|
selected := []string{}
|
||||||
prompt := &survey.MultiSelect{
|
prompt := &survey.MultiSelect{
|
||||||
Message: fmt.Sprintf("Results for %s '%s' from %s", mediaType, query, jsonutil.TitleCase(source)),
|
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,
|
Options: labels,
|
||||||
Description: func(value string, index int) string {
|
Description: func(value string, index int) string {
|
||||||
resultIndex, ok := labelToIndex[value]
|
resultIndex, ok := labelToIndex[value]
|
||||||
if !ok || resultIndex < 0 || resultIndex >= len(results) {
|
if !ok || resultIndex < 0 || resultIndex >= len(results) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return formatSearchDetails(results[resultIndex])
|
return formatSearchDetails(mediaType, results[resultIndex])
|
||||||
},
|
},
|
||||||
PageSize: 15,
|
PageSize: pageSize,
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(prompt, &selected); err != nil {
|
if err := survey.AskOne(prompt, &selected); err != nil {
|
||||||
if strings.Contains(strings.ToLower(err.Error()), "interrupt") {
|
if strings.Contains(strings.ToLower(err.Error()), "interrupt") {
|
||||||
@@ -241,6 +252,22 @@ func promptSearchSelectionMenu(source, mediaType, query string, results []search
|
|||||||
return out, nil
|
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 {
|
func writeSearchResultsToFile(source, mediaType string, results []searchResult, path string) error {
|
||||||
type outItem struct {
|
type outItem struct {
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
@@ -391,7 +418,23 @@ func normalizeSearchResults(source, mediaType string, pages []map[string]any) []
|
|||||||
trackCount = searchInt(itm["track_count"])
|
trackCount = searchInt(itm["track_count"])
|
||||||
}
|
}
|
||||||
explicit := searchBool(itm["parental_warning"])
|
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":
|
case "tidal":
|
||||||
items, ok := page["items"].([]any)
|
items, ok := page["items"].([]any)
|
||||||
@@ -425,7 +468,23 @@ func normalizeSearchResults(source, mediaType string, pages []map[string]any) []
|
|||||||
trackCount = searchInt(itm["tracks_count"])
|
trackCount = searchInt(itm["tracks_count"])
|
||||||
}
|
}
|
||||||
explicit := searchBool(itm["explicit"])
|
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":
|
case "deezer":
|
||||||
key := mediaType + "s"
|
key := mediaType + "s"
|
||||||
@@ -451,7 +510,19 @@ func normalizeSearchResults(source, mediaType string, pages []map[string]any) []
|
|||||||
album := nestedSearchString(itm, "album", "title")
|
album := nestedSearchString(itm, "album", "title")
|
||||||
trackCount := searchInt(itm["nb_tracks"])
|
trackCount := searchInt(itm["nb_tracks"])
|
||||||
explicit := searchBool(itm["explicit_lyrics"])
|
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":
|
case "soundcloud":
|
||||||
items, ok := page["items"].([]any)
|
items, ok := page["items"].([]any)
|
||||||
@@ -467,32 +538,80 @@ func normalizeSearchResults(source, mediaType string, pages []map[string]any) []
|
|||||||
title := asString(itm["title"])
|
title := asString(itm["title"])
|
||||||
artist := nestedSearchString(itm, "artist", "name")
|
artist := nestedSearchString(itm, "artist", "name")
|
||||||
trackCount := searchInt(itm["tracks_count"])
|
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
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatSearchDetails(r searchResult) string {
|
func formatSearchDetails(mediaType string, r searchResult) string {
|
||||||
lines := []string{"Selected item", ""}
|
date := strings.TrimSpace(r.Date)
|
||||||
lines = append(lines, fmt.Sprintf("Title : %s", r.Title))
|
if date == "" {
|
||||||
if strings.TrimSpace(r.Artist) != "" {
|
date = "Unknown"
|
||||||
lines = append(lines, fmt.Sprintf("Artist : %s", r.Artist))
|
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(r.Album) != "" {
|
lines := []string{}
|
||||||
lines = append(lines, fmt.Sprintf("Album : %s", r.Album))
|
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, strings.TrimSpace(r.Artist))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(r.Album) != "" {
|
||||||
|
lines = append(lines, strings.TrimSpace(r.Album))
|
||||||
|
}
|
||||||
|
if r.TrackCount > 0 {
|
||||||
|
lines = append(lines, fmt.Sprintf("%d Tracks", r.TrackCount))
|
||||||
|
}
|
||||||
|
if r.Explicit {
|
||||||
|
lines = append(lines, "Explicit")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if r.TrackCount > 0 {
|
lines = append(lines, "", "ID: "+r.ID)
|
||||||
lines = append(lines, fmt.Sprintf("Tracks : %d", r.TrackCount))
|
|
||||||
}
|
|
||||||
if r.Explicit {
|
|
||||||
lines = append(lines, "Explicit: yes")
|
|
||||||
}
|
|
||||||
lines = append(lines, fmt.Sprintf("ID : %s", r.ID))
|
|
||||||
return strings.Join(lines, "\n")
|
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 {
|
func nestedSearchString(v map[string]any, keys ...string) string {
|
||||||
cur := any(v)
|
cur := any(v)
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
@@ -505,6 +624,27 @@ func nestedSearchString(v map[string]any, keys ...string) string {
|
|||||||
return asString(cur)
|
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 {
|
func searchInt(v any) int {
|
||||||
switch t := v.(type) {
|
switch t := v.(type) {
|
||||||
case int:
|
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