Initial commit: ym-dl with yandex-music-api-go library migration
Migrate ym-dl from custom oapi-codegen Yandex Music API client to external gitea.mrixs.me/Mrixs/yandex-music-api-go library with token-only authentication. Key changes: - Replace custom Yandex API client with yandexmusic library wrapper - Token-based OAuth authentication only (no username/password fallback) - SQLite storage index with JSON index migration path - Extended ID3 tag support: genre, comment, lyrics, publisher, ISRC, copyright - High-resolution album covers (1000x1000) - Fixed album download check (requires all tracks, not just one)
This commit is contained in:
commit
fe62910311
16 files changed
+6688
No files matched your search
@@ -0,0 +1,92 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config holds the application configuration
|
||||
type Config struct {
|
||||
Yandex YandexConfig `yaml:"yandex"`
|
||||
Artists []Artist `yaml:"artists"`
|
||||
Storage StorageConfig `yaml:"storage"`
|
||||
Monitor MonitorConfig `yaml:"monitor"`
|
||||
Debug bool `yaml:"debug"`
|
||||
}
|
||||
|
||||
// YandexConfig holds Yandex Music API credentials
|
||||
type YandexConfig struct {
|
||||
Token string `yaml:"token"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
}
|
||||
|
||||
// Artist represents an artist to monitor
|
||||
type Artist struct {
|
||||
ID int `yaml:"id"`
|
||||
}
|
||||
|
||||
// StorageConfig holds storage settings
|
||||
type StorageConfig struct {
|
||||
Path string `yaml:"path"`
|
||||
Organize bool `yaml:"organize"`
|
||||
}
|
||||
|
||||
// MonitorConfig holds monitor settings
|
||||
type MonitorConfig struct {
|
||||
Interval string `yaml:"interval"`
|
||||
}
|
||||
|
||||
// Load loads configuration from a YAML file
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// Validate validates the configuration
|
||||
func (c *Config) Validate() error {
|
||||
if len(c.Artists) == 0 {
|
||||
return errors.New("at least one artist must be configured")
|
||||
}
|
||||
|
||||
for _, a := range c.Artists {
|
||||
if a.ID <= 0 {
|
||||
return errors.New("artist ID must be positive")
|
||||
}
|
||||
}
|
||||
|
||||
if c.Storage.Path == "" {
|
||||
return errors.New("storage path is required")
|
||||
}
|
||||
|
||||
if c.Yandex.Token == "" && (c.Yandex.Username == "" || c.Yandex.Password == "") {
|
||||
return errors.New("either yandex.token or yandex.username/password must be provided")
|
||||
}
|
||||
|
||||
if _, err := time.ParseDuration(c.Monitor.Interval); err != nil {
|
||||
return errors.New("monitor.interval must be a valid duration (e.g., 24h, 1h, 7d)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMonitorInterval returns the parsed monitor interval
|
||||
func (c *Config) GetMonitorInterval() (time.Duration, error) {
|
||||
return time.ParseDuration(c.Monitor.Interval)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadValidConfig(t *testing.T) {
|
||||
content := `
|
||||
yandex:
|
||||
token: "test-token"
|
||||
artists:
|
||||
- id: 12345
|
||||
storage:
|
||||
path: "/data/music"
|
||||
organize: true
|
||||
monitor:
|
||||
interval: "24h"
|
||||
`
|
||||
tmpfile, err := os.CreateTemp("", "config-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
|
||||
cfg, err := Load(tmpfile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Load failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Yandex.Token != "test-token" {
|
||||
t.Errorf("expected token 'test-token', got '%s'", cfg.Yandex.Token)
|
||||
}
|
||||
if len(cfg.Artists) != 1 || cfg.Artists[0].ID != 12345 {
|
||||
t.Errorf("expected artist ID 12345, got %v", cfg.Artists)
|
||||
}
|
||||
if cfg.Storage.Path != "/data/music" {
|
||||
t.Errorf("expected path '/data/music', got '%s'", cfg.Storage.Path)
|
||||
}
|
||||
if !cfg.Storage.Organize {
|
||||
t.Error("expected organize=true")
|
||||
}
|
||||
interval, err := cfg.GetMonitorInterval()
|
||||
if err != nil {
|
||||
t.Fatalf("GetMonitorInterval failed: %v", err)
|
||||
}
|
||||
if interval != 24*time.Hour {
|
||||
t.Errorf("expected 24h, got %v", interval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigWithUsernamePassword(t *testing.T) {
|
||||
content := `
|
||||
yandex:
|
||||
username: "user"
|
||||
password: "pass"
|
||||
artists:
|
||||
- id: 1
|
||||
storage:
|
||||
path: "/data"
|
||||
monitor:
|
||||
interval: "1h"
|
||||
`
|
||||
tmpfile, err := os.CreateTemp("", "config-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
|
||||
cfg, err := Load(tmpfile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Load failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Yandex.Username != "user" || cfg.Yandex.Password != "pass" {
|
||||
t.Errorf("expected username/password, got %v", cfg.Yandex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigMissingAuth(t *testing.T) {
|
||||
content := `
|
||||
yandex: {}
|
||||
artists:
|
||||
- id: 1
|
||||
storage:
|
||||
path: "/data"
|
||||
monitor:
|
||||
interval: "1h"
|
||||
`
|
||||
tmpfile, err := os.CreateTemp("", "config-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
|
||||
_, err = Load(tmpfile.Name())
|
||||
if err == nil {
|
||||
t.Error("expected error for missing auth")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigNoArtists(t *testing.T) {
|
||||
content := `
|
||||
yandex:
|
||||
token: "test"
|
||||
artists: []
|
||||
storage:
|
||||
path: "/data"
|
||||
monitor:
|
||||
interval: "1h"
|
||||
`
|
||||
tmpfile, err := os.CreateTemp("", "config-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
|
||||
_, err = Load(tmpfile.Name())
|
||||
if err == nil {
|
||||
t.Error("expected error for no artists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigInvalidInterval(t *testing.T) {
|
||||
content := `
|
||||
yandex:
|
||||
token: "test"
|
||||
artists:
|
||||
- id: 1
|
||||
storage:
|
||||
path: "/data"
|
||||
monitor:
|
||||
interval: "invalid"
|
||||
`
|
||||
tmpfile, err := os.CreateTemp("", "config-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
|
||||
_, err = Load(tmpfile.Name())
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid interval")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigNegativeArtistID(t *testing.T) {
|
||||
content := `
|
||||
yandex:
|
||||
token: "test"
|
||||
artists:
|
||||
- id: -1
|
||||
storage:
|
||||
path: "/data"
|
||||
monitor:
|
||||
interval: "1h"
|
||||
`
|
||||
tmpfile, err := os.CreateTemp("", "config-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
|
||||
_, err = Load(tmpfile.Name())
|
||||
if err == nil {
|
||||
t.Error("expected error for negative artist ID")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user