e5d9c7f043
- Add "Connect Spotify" OAuth flow (/connect/spotify) so newly created Spotify apps can read playlists: Spotify now requires a user token for playlist reads, which client-credentials alone can no longer provide. The stored refresh token is rendered as spotify-refresh into each playlist's sldl .conf -- sldl's own docs confirm supplying it skips its interactive login flow, which is what was causing multi-hour hangs on a stuck sync. Grandfathered apps keep working unchanged if no account is connected. - Fix the connect flow's redirect_uri: request.url_for() reflected the raw connection scheme (http) rather than what the reverse proxy actually served over, which Spotify's exact-match redirect_uri check rejected. Force https rather than trust the unproxied scheme. - Add an AzuraCast base URL credential field alongside the API key, with a keyfile fallback so the scheduled enrich-buy-url.py job can actually reach AzuraCast (previously it had no way to receive a base URL at all when run from the scheduler, so the reprocess call was silently always skipped). - Fix build-fingerprint-index.py exiting non-zero on any single fingerprint failure instead of only when nothing was written. - Guard enrich-buy-url.py's AzuraCast reprocess against an empty --azuracast-base (raised ValueError since PD2 removed the hardcoded base). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
86 lines
4.2 KiB
HTML
86 lines
4.2 KiB
HTML
{% extends "settings/_layout.html" %}
|
|
{% block settings_title %}Credentials{% endblock %}
|
|
{% block settings_content %}
|
|
<h2 style="margin-top:0;">Credentials</h2>
|
|
<p class="muted" style="margin-top:-0.75rem;">Write-only: values are encrypted at rest and never sent back to the browser. Leave a field blank to keep it unchanged.</p>
|
|
|
|
{% if not key_present %}
|
|
<div class="panel" style="border:1px solid #b91c1c;margin-bottom:1.5rem;">
|
|
<strong>No encryption key found.</strong>
|
|
<p class="muted" style="margin:0.5rem 0 0;">Credentials cannot be saved until the master key is in place. Generate one with <code>openssl rand -base64 32</code> and mount it as the <code>alembic_master_key</code> secret, then restart. See the <a href="/setup">setup check</a>.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="cred-grid">
|
|
{% for scope, fields in scope_fields.items() %}
|
|
{% set is_core = scope in core_scopes %}
|
|
{% set is_enabled = enabled.get(scope, True) %}
|
|
<div class="cred-scope {{ 'cred-disabled' if not is_core and not is_enabled else '' }}">
|
|
<div class="panel">
|
|
<div class="cred-scope-header">
|
|
<div>
|
|
<h2>
|
|
{{ scope }}
|
|
{% if is_core %}
|
|
<span class="badge badge-info" title="Needed for the core pipeline to work">Required</span>
|
|
{% elif configured.get(scope) %}
|
|
<span class="badge badge-success">Configured</span>
|
|
{% else %}
|
|
<span class="badge badge-muted">Not configured</span>
|
|
{% endif %}
|
|
</h2>
|
|
<p class="muted cred-desc">{{ scope_descriptions.get(scope, "") }}</p>
|
|
</div>
|
|
{% if not is_core %}
|
|
<form method="post" action="/settings/credentials/{{ scope }}/toggle" class="inline">
|
|
<label class="switch" title="{{ 'Disable' if is_enabled else 'Enable' }} {{ scope }}">
|
|
<input type="checkbox" {{ "checked" if is_enabled else "" }} onchange="this.form.submit()">
|
|
<span class="switch-track"><span class="switch-thumb"></span></span>
|
|
</label>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if not is_core and not is_enabled %}
|
|
<p class="cred-disabled-note">Disabled — its scheduled automation is paused (or, for buy-link sources, its rendered credential file is removed) until you turn this back on. Saved values below are kept.</p>
|
|
{% endif %}
|
|
|
|
{% if scope == "spotify" %}
|
|
<div class="field wide" style="margin-bottom:1rem;">
|
|
<label>Account connection</label>
|
|
<p class="muted" style="margin:0 0 0.5rem;">Newly created Spotify apps can no longer read playlists without this — connecting an account mints a user token that works where the app-only credentials above are blocked. Grandfathered apps keep working without it.</p>
|
|
{% if "refresh_token" in configured.get(scope, []) %}
|
|
<span class="badge badge-success">Connected</span>
|
|
<a href="/connect/spotify" class="btn" style="margin-left:0.5rem;">Reconnect Spotify</a>
|
|
{% else %}
|
|
<a href="/connect/spotify" class="btn">Connect Spotify</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="post" action="/settings/credentials/{{ scope }}">
|
|
{% for field in fields %}
|
|
{% if not (scope == "spotify" and field == "refresh_token") %}
|
|
<div class="field wide">
|
|
<label for="{{ scope }}_{{ field }}">
|
|
{{ field }}
|
|
{% if field in configured.get(scope, []) %}<span class="badge badge-success" style="margin-left:0.5rem;">set</span>{% endif %}
|
|
</label>
|
|
{% if field in ("cookies_txt",) %}
|
|
<textarea id="{{ scope }}_{{ field }}" name="{{ field }}" rows="6" placeholder="leave blank to keep current"></textarea>
|
|
{% elif field in secret_keys %}
|
|
<input type="password" id="{{ scope }}_{{ field }}" name="{{ field }}" placeholder="leave blank to keep current" autocomplete="off">
|
|
{% else %}
|
|
<input type="text" id="{{ scope }}_{{ field }}" name="{{ field }}" placeholder="leave blank to keep current" autocomplete="off" spellcheck="false">
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
<button type="submit" class="btn">Save {{ scope }}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endblock %}
|