diff --git a/sample.env b/sample.env index 360232b..68511ba 100644 --- a/sample.env +++ b/sample.env @@ -8,6 +8,8 @@ # DISCOVERY_SERVICE=listenbrainz # Your ListenBrainz username LISTENBRAINZ_USER= +# User token from ListenBrainz +LISTENBRAINZ_USER_TOKEN= # 'playlist' to fetch weekly playlist (50 songs), 'api' for fewer songs (good for testing) (default: playlist) # LISTENBRAINZ_DISCOVERY=playlist # Size of the cover art downloaded from coverartarchive (250 or 500) (default: 250) diff --git a/src/config/config.go b/src/config/config.go index 8eabc58..512c6a9 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -159,6 +159,7 @@ type DiscoveryConfig struct { type Listenbrainz struct { Discovery string `env:"LISTENBRAINZ_DISCOVERY" env-default:"playlist"` User string `env:"LISTENBRAINZ_USER"` + UserToken string `env:"LISTENBRAINZ_USER_TOKEN"` ImportPlaylist string SingleArtist bool `env:"SINGLE_ARTIST" env-default:"true"` CoverArtSize string `env:"COVER_ART_SIZE" env-default:"250"` @@ -261,6 +262,10 @@ func (cfg *Config) HandleDeprecation() { // slog.Warn("--persist flag now only handles playlist deletion, use toggle in UI or --clean-downloads to delete tracks") } + if cfg.DiscoveryCfg.Listenbrainz.UserToken == "" { + slog.Warn("Provide a ListenBrainz user token via Wizard or env (LISTENBRAINZ_USER_TOKEN)") + } + if cfg.Flags.CleanDownloads && !cfg.DownloadCfg.UseSubDir { slog.Warn("Deleting tracks requires 'USE_SUBDIRECTORY' to be true") } diff --git a/src/discovery/listenbrainz.go b/src/discovery/listenbrainz.go index f164806..c7f3509 100644 --- a/src/discovery/listenbrainz.go +++ b/src/discovery/listenbrainz.go @@ -182,13 +182,20 @@ type MBRecording struct { type ListenBrainz struct { HttpClient *util.HttpClient + Headers map[string]string cfg cfg.Listenbrainz Separator string } func NewListenBrainz(cfg cfg.DiscoveryConfig, httpClient *util.HttpClient) *ListenBrainz { + authHeader := make(map[string]string) + if cfg.Listenbrainz.UserToken != "" { + authHeader["Authorization"] = fmt.Sprintf("Token %s", cfg.Listenbrainz.UserToken) + } + return &ListenBrainz{ cfg: cfg.Listenbrainz, + Headers: authHeader, HttpClient: httpClient, } } @@ -725,7 +732,7 @@ func (c *ListenBrainz) parsePlaylist(identifier string, singleArtist bool) (stri func (c *ListenBrainz) lbRequest(path string) ([]byte, error) { reqURL := fmt.Sprintf("https://api.listenbrainz.org/1/%s", path) - body, err := c.HttpClient.MakeRequest("GET", reqURL, nil, nil) + body, err := c.HttpClient.MakeRequest("GET", reqURL, nil, c.Headers) if err != nil { return nil, fmt.Errorf("failed to make request to ListenBrainz API: %s", err) } diff --git a/src/web/backend/defs/defs.go b/src/web/backend/defs/defs.go index 84fe591..5b4756d 100644 --- a/src/web/backend/defs/defs.go +++ b/src/web/backend/defs/defs.go @@ -181,5 +181,5 @@ var AllConfigKeys = []string{ "DOWNLOAD_DIR", "USE_SUBDIRECTORY", "PATH_TEMPLATE", "ENRICH_TRACK_METADATA", "DOWNLOAD_SERVICES", "YOUTUBE_API_KEY", "TRACK_EXTENSION", "FILTER_LIST", "SLSKD_URL", "SLSKD_API_KEY", - "WIZARD_COMPLETE", "MIGRATE_DOWNLOADS", "EXTENSIONS", + "WIZARD_COMPLETE", "MIGRATE_DOWNLOADS", "EXTENSIONS", "LISTENBRAINZ_USER_TOKEN", } \ No newline at end of file diff --git a/src/web/backend/settings/handlers.go b/src/web/backend/settings/handlers.go index 3225673..6ca56f9 100644 --- a/src/web/backend/settings/handlers.go +++ b/src/web/backend/settings/handlers.go @@ -195,6 +195,7 @@ func (s *Settings) HandleSaveEnrichMetadata(w http.ResponseWriter, r *http.Reque func (s *Settings) HandleWizardStep1(w http.ResponseWriter, r *http.Request) { var body struct { User string `json:"user"` + UserToken string `json:"userToken"` Playlists []string `json:"playlists"` DiscoveryMode string `json:"discovery_mode"` } @@ -214,6 +215,7 @@ func (s *Settings) HandleWizardStep1(w http.ResponseWriter, r *http.Request) { updates := map[string]string{ "LISTENBRAINZ_USER": body.User, + "LISTENBRAINZ_USER_TOKEN": body.UserToken, "LISTENBRAINZ_DISCOVERY": body.DiscoveryMode, } for name, def := range defs.PlaylistDefs { diff --git a/src/web/frontend/src/components/Wizard.jsx b/src/web/frontend/src/components/Wizard.jsx index ab9fe82..2744d8c 100644 --- a/src/web/frontend/src/components/Wizard.jsx +++ b/src/web/frontend/src/components/Wizard.jsx @@ -46,9 +46,9 @@ const PLAYLISTS = [ ]; function Step1({ fields, setField, envSources, onNext, saving }) { - const { user, discoveryMode, checked } = fields + const { user, userToken, discoveryMode, checked } = fields const isLocked = key => envSources[key] === 'env' - const valid = user.trim() !== '' && (discoveryMode !== 'playlist' || Object.values(checked).some(Boolean)) + const valid = user.trim() !== '' && userToken.trim() !== '' && (discoveryMode !== 'playlist' || Object.values(checked).some(Boolean)) return (
@@ -65,6 +65,13 @@ function Step1({ fields, setField, envSources, onNext, saving }) { disabled={isLocked('LISTENBRAINZ_USER')} /> + Copy it from ListenBrainz{' '}settings page}> + setField('userToken', e.target.value)} + disabled={isLocked('LISTENBRAINZ_USER_TOKEN')} /> + +
@@ -627,6 +634,7 @@ export default function Wizard({ return { // Step 1 user: config.LISTENBRAINZ_USER || "", + userToken: config.LISTENBRAINZ_USER_TOKEN || "", discoveryMode: config.LISTENBRAINZ_DISCOVERY || "playlist", checked: { "weekly-exploration": !!config.WEEKLY_EXPLORATION_SCHEDULE, @@ -679,7 +687,7 @@ export default function Wizard({ const playlists = Object.entries(fields.checked) .filter(([, v]) => v) .map(([k]) => k); - await wizardStep1(fields.user.trim(), playlists, fields.discoveryMode); + await wizardStep1(fields.user.trim(), fields.userToken.trim(), playlists, fields.discoveryMode); if (playlists.length > 0) { prefetchPlaylists(fields.user.trim(), playlists, { source: "wizard", diff --git a/src/web/frontend/src/lib/api.js b/src/web/frontend/src/lib/api.js index 7b53bcc..de148f4 100644 --- a/src/web/frontend/src/lib/api.js +++ b/src/web/frontend/src/lib/api.js @@ -82,11 +82,11 @@ export async function saveSchedule(name, enabled, day, hour, minute) { if (!res.ok) throw new Error(await res.text()) } -export async function wizardStep1(user, playlists, discovery_mode) { +export async function wizardStep1(user, userToken, playlists, discovery_mode) { const res = await apiFetch('/api/ui/wizard/step1', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ user, playlists, discovery_mode }), + body: JSON.stringify({ user, userToken, playlists, discovery_mode }), }) if (!res.ok) throw new Error(await res.text()) } diff --git a/src/web/sample.env b/src/web/sample.env index 360232b..68511ba 100644 --- a/src/web/sample.env +++ b/src/web/sample.env @@ -8,6 +8,8 @@ # DISCOVERY_SERVICE=listenbrainz # Your ListenBrainz username LISTENBRAINZ_USER= +# User token from ListenBrainz +LISTENBRAINZ_USER_TOKEN= # 'playlist' to fetch weekly playlist (50 songs), 'api' for fewer songs (good for testing) (default: playlist) # LISTENBRAINZ_DISCOVERY=playlist # Size of the cover art downloaded from coverartarchive (250 or 500) (default: 250)