Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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")
}
Expand Down
9 changes: 8 additions & 1 deletion src/discovery/listenbrainz.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion src/web/backend/defs/defs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
2 changes: 2 additions & 0 deletions src/web/backend/settings/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand All @@ -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 {
Expand Down
14 changes: 11 additions & 3 deletions src/web/frontend/src/components/Wizard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
Expand All @@ -65,6 +65,13 @@ function Step1({ fields, setField, envSources, onNext, saving }) {
disabled={isLocked('LISTENBRAINZ_USER')} />
</TextField>

<TextField label="ListenBrainz user token" labelFor="lb-user-token"
hint={<>Copy it from ListenBrainz{' '}<a href="https://listenbrainz.org/settings/" target="_blank" rel="noreferrer" className="text-accent">settings page</a></>}>
<input id="lb-user-token" type="text" className={inputCls}
autoComplete="off" spellCheck={false} value={userToken} onChange={e => setField('userToken', e.target.value)}
disabled={isLocked('LISTENBRAINZ_USER_TOKEN')} />
</TextField>

<div className="flex flex-col gap-2">
<label className="text-[13px] font-medium text-muted">Discovery mode</label>
<div className="grid grid-cols-2 gap-2">
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/web/frontend/src/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
2 changes: 2 additions & 0 deletions src/web/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading