mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
235 lines
5.9 KiB
Go
235 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"streamrip-go/internal/app"
|
|
"streamrip-go/internal/urlparse"
|
|
)
|
|
|
|
type fileIDItem struct {
|
|
Source string
|
|
MediaType string
|
|
ID string
|
|
}
|
|
|
|
type failedRow struct {
|
|
Source string
|
|
MediaType string
|
|
ID string
|
|
}
|
|
|
|
type lastFMOptions struct {
|
|
Source string
|
|
FallbackSource string
|
|
PlaylistURL string
|
|
}
|
|
|
|
type lastFMTrack struct {
|
|
Title string
|
|
Artist string
|
|
}
|
|
|
|
type resolvedLastFMTrack struct {
|
|
Source string
|
|
ID string
|
|
Query string
|
|
}
|
|
|
|
var (
|
|
lastFMTitleTagsRe = regexp.MustCompile(`<a\b[^>]*\btitle=(?:"([^"]+)"|'([^']+)')`)
|
|
lastFMDataTrackArtistRe = regexp.MustCompile(`data-track-name=(?:"([^"]+)"|'([^']+)')[^>]*data-artist-name=(?:"([^"]+)"|'([^']+)')`)
|
|
lastFMTotalTracksRe = regexp.MustCompile(`data-playlisting-entry-count="(\d+)"`)
|
|
lastFMPlaylistTitleRe = regexp.MustCompile(`<h1[^>]*class="[^"]*playlisting-playlist-header-title[^"]*"[^>]*>([^<]+)</h1>`)
|
|
lastFMMirrorTitleRe = regexp.MustCompile(`^Title:\s*(.+?)\s+\|`)
|
|
lastFMMirrorLinkTextRe = regexp.MustCompile(`\[([^\]]+)\]\(`)
|
|
errLastFMInvalidSource = "unsupported source"
|
|
)
|
|
|
|
func addURLToQueue(ctx context.Context, mainApp *app.Main, raw string) bool {
|
|
parsed := urlparse.Parse(raw)
|
|
if parsed == nil {
|
|
fmt.Printf("invalid: %s\n", raw)
|
|
return false
|
|
}
|
|
if parsed.Kind != urlparse.KindGeneric && parsed.Kind != urlparse.KindSoundcloud {
|
|
fmt.Printf("not yet supported: %s (kind=%s)\n", raw, parsed.Kind)
|
|
return false
|
|
}
|
|
if parsed.Source != "qobuz" && parsed.Source != "tidal" && parsed.Source != "deezer" && parsed.Source != "yandex" && parsed.Source != "soundcloud" {
|
|
fmt.Printf("provider not yet implemented: source=%s url=%s\n", parsed.Source, raw)
|
|
return false
|
|
}
|
|
if err := mainApp.AddByID(ctx, parsed.Source, parsed.MediaType, parsed.ID); err != nil {
|
|
fmt.Printf("add failed: source=%s type=%s id=%s err=%v\n", parsed.Source, parsed.MediaType, parsed.ID, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func parseFileInput(content []byte) ([]fileIDItem, []string, int, bool, error) {
|
|
trimmed := strings.TrimSpace(string(content))
|
|
if trimmed == "" {
|
|
return nil, nil, 0, false, nil
|
|
}
|
|
|
|
var parsed any
|
|
if err := json.Unmarshal([]byte(trimmed), &parsed); err == nil {
|
|
arr, ok := parsed.([]any)
|
|
if !ok {
|
|
return nil, nil, 0, true, fmt.Errorf("json input must be an array of objects")
|
|
}
|
|
items := make([]fileIDItem, 0, len(arr))
|
|
for i, raw := range arr {
|
|
entry, ok := raw.(map[string]any)
|
|
if !ok {
|
|
return nil, nil, 0, true, fmt.Errorf("json item %d must be an object", i+1)
|
|
}
|
|
source := strings.ToLower(strings.TrimSpace(asString(entry["source"])))
|
|
mediaType := strings.ToLower(strings.TrimSpace(asString(entry["media_type"])))
|
|
if mediaType == "" {
|
|
mediaType = strings.ToLower(strings.TrimSpace(asString(entry["mediaType"])))
|
|
}
|
|
id := strings.TrimSpace(asString(entry["id"]))
|
|
if source == "" || mediaType == "" || id == "" {
|
|
return nil, nil, 0, true, fmt.Errorf("json item %d missing source/media_type/id", i+1)
|
|
}
|
|
items = append(items, fileIDItem{Source: source, MediaType: mediaType, ID: id})
|
|
}
|
|
return items, nil, 0, true, nil
|
|
}
|
|
|
|
parts := strings.Fields(trimmed)
|
|
if len(parts) == 0 {
|
|
return nil, nil, 0, false, nil
|
|
}
|
|
seen := make(map[string]struct{}, len(parts))
|
|
urls := make([]string, 0, len(parts))
|
|
repeated := 0
|
|
for _, raw := range parts {
|
|
if _, ok := seen[raw]; ok {
|
|
repeated++
|
|
continue
|
|
}
|
|
seen[raw] = struct{}{}
|
|
urls = append(urls, raw)
|
|
}
|
|
return nil, urls, repeated, false, nil
|
|
}
|
|
|
|
func promptYesNo(prompt string) (bool, error) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
fmt.Print(prompt)
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
line = strings.ToLower(strings.TrimSpace(line))
|
|
return line == "y" || line == "yes", nil
|
|
}
|
|
|
|
func openConfigInEditor(path string, vim bool) error {
|
|
launch := func(name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
if vim {
|
|
if p, err := exec.LookPath("nvim"); err == nil {
|
|
return launch(p, path)
|
|
}
|
|
if p, err := exec.LookPath("vim"); err == nil {
|
|
return launch(p, path)
|
|
}
|
|
}
|
|
|
|
if editor := strings.TrimSpace(os.Getenv("EDITOR")); editor != "" {
|
|
parts := strings.Fields(editor)
|
|
if len(parts) > 0 {
|
|
return launch(parts[0], append(parts[1:], path)...)
|
|
}
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return launch("open", path)
|
|
case "windows":
|
|
return launch("cmd", "/c", "start", "", path)
|
|
default:
|
|
if p, err := exec.LookPath("xdg-open"); err == nil {
|
|
return launch(p, path)
|
|
}
|
|
return fmt.Errorf("could not find an editor (set $EDITOR or install xdg-open)")
|
|
}
|
|
}
|
|
|
|
func listDownloadsRows(path string) ([]string, error) {
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = db.Close() }()
|
|
|
|
rows, err := db.Query(`SELECT id FROM downloads ORDER BY rowid`)
|
|
if err != nil {
|
|
if isNoSuchTableErr(err) {
|
|
return []string{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
out := []string{}
|
|
for rows.Next() {
|
|
var id string
|
|
if err = rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, id)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func listFailedRows(path string) ([]failedRow, error) {
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = db.Close() }()
|
|
|
|
rows, err := db.Query(`SELECT source, media_type, id FROM failed_downloads ORDER BY rowid`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
out := []failedRow{}
|
|
for rows.Next() {
|
|
var r failedRow
|
|
if err = rows.Scan(&r.Source, &r.MediaType, &r.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func isNoSuchTableErr(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
return strings.Contains(strings.ToLower(err.Error()), "no such table")
|
|
}
|