mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-04-21 02:02:31 -04:00
1197 lines
64 KiB
HTML
1197 lines
64 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Allstarr Dashboard</title>
|
||
<link rel="stylesheet" href="styles.css">
|
||
</head>
|
||
|
||
<body>
|
||
<!-- Restart Required Banner -->
|
||
<div class="restart-banner" id="restart-banner">
|
||
⚠️ Configuration changed. Restart required to apply changes.
|
||
<button onclick="restartContainer()">Restart Now</button>
|
||
<button onclick="dismissRestartBanner()"
|
||
style="background: transparent; border: 1px solid var(--bg-primary);">Dismiss</button>
|
||
</div>
|
||
|
||
<div class="auth-gate" id="auth-gate">
|
||
<div class="auth-card">
|
||
<h2>Sign In With Jellyfin</h2>
|
||
<p>Use your Jellyfin account to access the local Allstarr admin UI.</p>
|
||
<form id="auth-login-form" autocomplete="off">
|
||
<label for="auth-username">Username</label>
|
||
<input id="auth-username" type="text" required>
|
||
|
||
<label for="auth-password">Password</label>
|
||
<input id="auth-password" type="password" required>
|
||
|
||
<button class="primary" type="submit">Sign In</button>
|
||
<div class="auth-error" id="auth-error" role="alert"></div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container" id="main-container" style="display:none;">
|
||
<header>
|
||
<h1>
|
||
Allstarr <span class="version" id="version">Loading...</span>
|
||
</h1>
|
||
<div class="header-actions">
|
||
<div class="auth-user" id="auth-user-display" style="display:none;">
|
||
Signed in as <strong id="auth-user-name">-</strong>
|
||
</div>
|
||
<button id="auth-logout-btn" onclick="logoutAdminSession()" style="display:none;">Logout</button>
|
||
<div id="status-indicator">
|
||
<span class="status-badge" id="spotify-status">
|
||
<span class="status-dot"></span>
|
||
<span>Loading...</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" data-tab="dashboard">Dashboard</div>
|
||
<div class="tab" data-tab="jellyfin-playlists">Link Playlists</div>
|
||
<div class="tab" data-tab="playlists">Injected Playlists</div>
|
||
<div class="tab" data-tab="kept">Kept Downloads</div>
|
||
<div class="tab" data-tab="scrobbling">Scrobbling</div>
|
||
<div class="tab" data-tab="config">Configuration</div>
|
||
<div class="tab" data-tab="endpoints">API Analytics</div>
|
||
</div>
|
||
|
||
<!-- Dashboard Tab -->
|
||
<div class="tab-content active" id="tab-dashboard">
|
||
<div class="grid">
|
||
<div class="card">
|
||
<h2>Spotify API</h2>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Status</span>
|
||
<span class="stat-value" id="spotify-auth-status">Loading...</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">User</span>
|
||
<span class="stat-value" id="spotify-user">-</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Cookie Age</span>
|
||
<span class="stat-value" id="spotify-cookie-age">-</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Cache Duration</span>
|
||
<span class="stat-value" id="cache-duration">-</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">ISRC Matching</span>
|
||
<span class="stat-value" id="isrc-matching">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Jellyfin</h2>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Backend</span>
|
||
<span class="stat-value" id="backend-type">-</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">URL</span>
|
||
<span class="stat-value" id="jellyfin-url">-</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Playlists</span>
|
||
<span class="stat-value" id="playlist-count">-</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>
|
||
Quick Actions
|
||
</h2>
|
||
<div id="dashboard-guidance" class="guidance-stack"></div>
|
||
<div class="card-actions-row">
|
||
<button class="primary" onclick="refreshPlaylists()">Refresh All Playlists</button>
|
||
<button onclick="clearCache()">Clear Cache</button>
|
||
<button onclick="openAddPlaylist()">Add Playlist</button>
|
||
<button onclick="window.location.href='/spotify-mappings.html'">View Spotify Mappings</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Link Playlists Tab -->
|
||
<div class="tab-content" id="tab-jellyfin-playlists">
|
||
<div class="card">
|
||
<h2>
|
||
Link Jellyfin Playlists to Spotify
|
||
<div class="actions">
|
||
<button onclick="fetchJellyfinPlaylists()">Refresh</button>
|
||
</div>
|
||
</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Connect your Jellyfin playlists to Spotify playlists. Allstarr will automatically fill in missing
|
||
tracks from Spotify using your preferred music service (SquidWTF/Deezer/Qobuz).
|
||
<br><strong>Tip:</strong> Use the sp_dc cookie method for best results - it's simpler and more
|
||
reliable.
|
||
</p>
|
||
<div id="jellyfin-guidance" class="guidance-stack"></div>
|
||
|
||
<div id="jellyfin-user-filter" style="display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap;">
|
||
<div class="form-group" style="margin: 0; flex: 1; min-width: 200px;">
|
||
<label
|
||
style="display: block; margin-bottom: 4px; color: var(--text-secondary); font-size: 0.85rem;">User</label>
|
||
<select id="jellyfin-user-select" onchange="fetchJellyfinPlaylists()"
|
||
style="width: 100%; padding: 8px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary);">
|
||
<option value="">All Users</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<table class="playlist-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Tracks</th>
|
||
<th>Status</th>
|
||
<th>...</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="jellyfin-playlist-table-body">
|
||
<tr>
|
||
<td colspan="4" class="loading">
|
||
<span class="spinner"></span> Loading Jellyfin playlists...
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Active Playlists Tab -->
|
||
<div class="tab-content" id="tab-playlists">
|
||
<!-- Warning Banner (hidden by default) -->
|
||
<div id="matching-warning-banner" class="guidance-banner warning matching-progress-banner" style="display:none;">
|
||
⚠️ TRACK MATCHING IN PROGRESS - Please wait for matching to complete before making changes to playlists
|
||
or mappings!
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Playlist Injection Settings</h2>
|
||
<div class="guidance-banner info compact">
|
||
ℹ️ Music Library ID is required for playlist injection. Get it from your Jellyfin music library URL.
|
||
</div>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">Music Library ID <span style="color: var(--error);">*</span></span>
|
||
<span class="value" id="config-jellyfin-library-id">-</span>
|
||
<button
|
||
onclick="openEditSetting('JELLYFIN_LIBRARY_ID', 'Music Library ID', 'text', 'Required for playlist injection. Get from Jellyfin music library URL.')">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>
|
||
Injected Spotify Playlists
|
||
<div class="actions">
|
||
<button onclick="matchAllPlaylists()"
|
||
title="Re-match tracks when local library changed (uses cached Spotify data)">Rematch All</button>
|
||
<button onclick="refreshPlaylists()"
|
||
title="Fetch the latest playlist data from Spotify without re-matching tracks">Refresh All</button>
|
||
<button class="primary" onclick="refreshAndMatchAll()"
|
||
title="Rebuild all playlists when Spotify playlists changed (clears cache, fetches fresh data, re-matches)"
|
||
>Rebuild All</button>
|
||
</div>
|
||
</h2>
|
||
|
||
<details class="advanced-section">
|
||
<summary>Advanced: Rematch vs Refresh vs Rebuild</summary>
|
||
<div class="advanced-section-content">
|
||
<div class="advanced-guide-list">
|
||
<div><strong>Rematch</strong>: Use when your <em>local Jellyfin library</em> changed. Fast and uses cached Spotify data.</div>
|
||
<div><strong>Refresh</strong>: Pull fresh Spotify playlist items without re-matching.</div>
|
||
<div><strong>Rebuild</strong>: Full reset when <em>Spotify playlist content</em> changed. Clears cache and re-matches everything.</div>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<p style="color: var(--text-secondary); margin-bottom: 12px;">
|
||
These are the Spotify playlists currently being injected into Jellyfin with tracks from your music
|
||
service.
|
||
</p>
|
||
<div id="playlists-guidance" class="guidance-stack"></div>
|
||
<table class="playlist-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Tracks</th>
|
||
<th>Status</th>
|
||
<th>...</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="playlist-table-body">
|
||
<tr>
|
||
<td colspan="4" class="loading">
|
||
<span class="spinner"></span> Loading playlists...
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Manual Track Mappings Section -->
|
||
<div class="card">
|
||
<h2>
|
||
Manual Track Mappings
|
||
<div class="actions">
|
||
<button onclick="fetchTrackMappings()">Refresh</button>
|
||
</div>
|
||
</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 12px;">
|
||
Manual mappings override automatic matching for external providers (SquidWTF, Deezer, Qobuz). For
|
||
local Jellyfin tracks, use the Spotify Import plugin instead.
|
||
</p>
|
||
<div id="mappings-summary"
|
||
style="display: flex; gap: 20px; margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 6px;">
|
||
<div>
|
||
<span style="color: var(--text-secondary);">Total:</span>
|
||
<span style="font-weight: 600; margin-left: 8px;" id="mappings-total">0</span>
|
||
</div>
|
||
<div>
|
||
<span style="color: var(--text-secondary);">External:</span>
|
||
<span style="font-weight: 600; margin-left: 8px; color: var(--success);"
|
||
id="mappings-external">0</span>
|
||
</div>
|
||
</div>
|
||
<table class="playlist-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Playlist</th>
|
||
<th>Spotify ID</th>
|
||
<th>Type</th>
|
||
<th>Target</th>
|
||
<th>Created</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="mappings-table-body">
|
||
<tr>
|
||
<td colspan="6" class="loading">
|
||
<span class="spinner"></span> Loading mappings...
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Missing Tracks Section -->
|
||
<div class="card">
|
||
<h2>
|
||
Missing Tracks (All Playlists)
|
||
<div class="actions">
|
||
<button onclick="fetchMissingTracks()">Refresh</button>
|
||
</div>
|
||
</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 12px;">
|
||
Tracks that couldn't be matched locally or externally. Map them manually to add them to your
|
||
playlists.
|
||
</p>
|
||
<div id="missing-summary"
|
||
style="display: flex; gap: 20px; margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 6px;">
|
||
<div>
|
||
<span style="color: var(--text-secondary);">Total Missing:</span>
|
||
<span style="font-weight: 600; margin-left: 8px; color: var(--warning);"
|
||
id="missing-total">0</span>
|
||
</div>
|
||
</div>
|
||
<table class="playlist-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Playlist</th>
|
||
<th>Track</th>
|
||
<th>Artist</th>
|
||
<th>Album</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="missing-tracks-table-body">
|
||
<tr>
|
||
<td colspan="5" class="loading">
|
||
<span class="spinner"></span> Loading missing tracks...
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Kept Downloads Tab -->
|
||
<div class="tab-content" id="tab-kept">
|
||
<div class="card">
|
||
<h2>
|
||
Kept Downloads
|
||
<div class="actions">
|
||
<button onclick="downloadAllKept()" style="background:var(--accent);border-color:var(--accent);">Download All</button>
|
||
<button onclick="fetchDownloads()">Refresh</button>
|
||
</div>
|
||
</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 12px;">
|
||
Downloaded files stored permanently. Download individual tracks or download all as a zip archive.
|
||
</p>
|
||
<div id="downloads-summary"
|
||
style="display: flex; gap: 20px; margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 6px;">
|
||
<div>
|
||
<span style="color: var(--text-secondary);">Total Files:</span>
|
||
<span style="font-weight: 600; margin-left: 8px; color: var(--accent);"
|
||
id="downloads-count">0</span>
|
||
</div>
|
||
<div>
|
||
<span style="color: var(--text-secondary);">Total Size:</span>
|
||
<span style="font-weight: 600; margin-left: 8px; color: var(--accent);" id="downloads-size">0
|
||
B</span>
|
||
</div>
|
||
</div>
|
||
<table class="playlist-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Artist</th>
|
||
<th>Album</th>
|
||
<th>File</th>
|
||
<th>Size</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="downloads-table-body">
|
||
<tr>
|
||
<td colspan="5" class="loading">
|
||
<span class="spinner"></span> Loading downloads...
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Scrobbling Tab -->
|
||
<div class="tab-content" id="tab-scrobbling">
|
||
<div class="card">
|
||
<h2>Scrobbling Configuration</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Scrobble your listening history to Last.fm and ListenBrainz. Tracks are scrobbled when you listen to at least half the track or 4 minutes (whichever comes first).
|
||
</p>
|
||
|
||
<div class="config-section" style="margin-bottom: 24px;">
|
||
<div class="config-item">
|
||
<span class="label">Scrobbling Enabled</span>
|
||
<span class="value" id="scrobbling-enabled-value">-</span>
|
||
<button onclick="toggleScrobblingEnabled()">Toggle</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Local Track Scrobbling</span>
|
||
<span class="value" id="local-tracks-enabled-value">-</span>
|
||
<button onclick="toggleLocalTracksEnabled()">Toggle</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Synthetic Local Played Signal</span>
|
||
<span class="value" id="synthetic-local-played-signal-enabled-value">-</span>
|
||
<button onclick="toggleSyntheticLocalPlayedSignalEnabled()">Toggle</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="background: rgba(255, 193, 7, 0.15); border: 1px solid #ffc107; border-radius: 6px; padding: 12px; margin-bottom: 16px; color: var(--text-primary);">
|
||
ℹ️ <strong>Recommended:</strong> Keep local track scrobbling disabled and use native Jellyfin plugins instead:
|
||
<br>• <a href="https://github.com/danielfariati/jellyfin-plugin-lastfm" target="_blank" style="color: var(--accent);">Last.fm Plugin</a>
|
||
<br>• <a href="https://github.com/lyarenei/jellyfin-plugin-listenbrainz" target="_blank" style="color: var(--accent);">ListenBrainz Plugin</a>
|
||
<br>This ensures Allstarr only scrobbles external tracks (Spotify, Deezer, Qobuz).
|
||
<br><strong>Default:</strong> keep Synthetic Local Played Signal disabled to avoid duplicate plugin scrobbles.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Last.fm</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Scrobble to Last.fm. Enter your Last.fm username and password below, then click "Authenticate & Save" to generate a session key.
|
||
</p>
|
||
|
||
<div class="config-section" style="margin-bottom: 24px;">
|
||
<div class="config-item">
|
||
<span class="label">Last.fm Enabled</span>
|
||
<span class="value" id="lastfm-enabled-value">-</span>
|
||
<button onclick="toggleLastFmEnabled()">Toggle</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Username</span>
|
||
<span class="value" id="lastfm-username-value">-</span>
|
||
<button onclick="editLastFmUsername()">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Password</span>
|
||
<span class="value" id="lastfm-password-value">-</span>
|
||
<button onclick="editLastFmPassword()">Edit</button>
|
||
</div>
|
||
<div class="config-item" style="grid-template-columns: 200px 1fr;">
|
||
<span class="label">Session Key</span>
|
||
<span class="value" id="lastfm-session-key-value" style="font-family: monospace; font-size: 0.85rem; word-break: break-all;">-</span>
|
||
</div>
|
||
<div class="config-item" style="grid-template-columns: 200px 1fr;">
|
||
<span class="label">Status</span>
|
||
<span class="value" id="lastfm-status-value">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||
<button class="primary" onclick="authenticateLastFm()">Authenticate & Save</button>
|
||
<button onclick="testLastFmConnection()">Test Connection</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>ListenBrainz</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Scrobble to ListenBrainz. Get your user token from <a href="https://listenbrainz.org/settings/" target="_blank" style="color: var(--accent);">ListenBrainz Settings</a>.
|
||
<br><strong>Note:</strong> Only external tracks (Spotify, Deezer, Qobuz) are scrobbled to ListenBrainz. Local library tracks are not scrobbled.
|
||
</p>
|
||
|
||
<div class="config-section" style="margin-bottom: 24px;">
|
||
<div class="config-item">
|
||
<span class="label">ListenBrainz Enabled</span>
|
||
<span class="value" id="listenbrainz-enabled-value">-</span>
|
||
<button onclick="toggleListenBrainzEnabled()">Toggle</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">User Token</span>
|
||
<span class="value" id="listenbrainz-token-value">-</span>
|
||
<button onclick="editListenBrainzToken()">Edit</button>
|
||
</div>
|
||
<div class="config-item" style="grid-template-columns: 200px 1fr;">
|
||
<span class="label">Status</span>
|
||
<span class="value" id="listenbrainz-status-value">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||
<button class="primary" onclick="validateListenBrainzToken()">Validate & Save Token</button>
|
||
<button onclick="testListenBrainzConnection()">Test Connection</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Configuration Tab -->
|
||
<div class="tab-content" id="tab-config">
|
||
<div class="card">
|
||
<h2>Core Settings</h2>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">Backend Type <span style="color: var(--error);">*</span></span>
|
||
<span class="value" id="config-backend-type">-</span>
|
||
<button
|
||
onclick="openEditSetting('BACKEND_TYPE', 'Backend Type', 'select', 'Choose your media server backend', ['Jellyfin', 'Subsonic'])">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Music Service <span style="color: var(--error);">*</span></span>
|
||
<span class="value" id="config-music-service">-</span>
|
||
<button
|
||
onclick="openEditSetting('MUSIC_SERVICE', 'Music Service', 'select', 'Choose your music download provider', ['SquidWTF', 'Deezer', 'Qobuz'])">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Storage Mode</span>
|
||
<span class="value" id="config-storage-mode">-</span>
|
||
<button
|
||
onclick="openEditSetting('STORAGE_MODE', 'Storage Mode', 'select', 'Permanent keeps files forever, Cache auto-deletes after duration', ['Permanent', 'Cache'])">Edit</button>
|
||
</div>
|
||
<div class="config-item" id="cache-duration-row" style="display: none;">
|
||
<span class="label">Cache Duration (hours)</span>
|
||
<span class="value" id="config-cache-duration-hours">-</span>
|
||
<button
|
||
onclick="openEditSetting('CACHE_DURATION_HOURS', 'Cache Duration (hours)', 'number', 'How long to keep cached files before deletion')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Download Mode</span>
|
||
<span class="value" id="config-download-mode">-</span>
|
||
<button
|
||
onclick="openEditSetting('DOWNLOAD_MODE', 'Download Mode', 'select', 'Download individual tracks or full albums', ['Track', 'Album'])">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Explicit Filter</span>
|
||
<span class="value" id="config-explicit-filter">-</span>
|
||
<button
|
||
onclick="openEditSetting('EXPLICIT_FILTER', 'Explicit Filter', 'select', 'Filter explicit content', ['All', 'ExplicitOnly', 'CleanOnly'])">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Enable External Playlists</span>
|
||
<span class="value" id="config-enable-external-playlists">-</span>
|
||
<button
|
||
onclick="openEditSetting('ENABLE_EXTERNAL_PLAYLISTS', 'Enable External Playlists', 'toggle')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Playlists Directory</span>
|
||
<span class="value" id="config-playlists-directory">-</span>
|
||
<button
|
||
onclick="openEditSetting('PLAYLISTS_DIRECTORY', 'Playlists Directory', 'text', 'Directory path for external playlists')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Redis Enabled</span>
|
||
<span class="value" id="config-redis-enabled">-</span>
|
||
<button onclick="openEditSetting('REDIS_ENABLED', 'Redis Enabled', 'toggle')">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Admin Network Settings</h2>
|
||
<div
|
||
style="background: rgba(245, 158, 11, 0.12); border: 1px solid var(--warning); border-radius: 6px; padding: 12px; margin-bottom: 16px; color: var(--text-secondary); font-size: 0.9rem;">
|
||
Keep admin UI on localhost by default. If you enable LAN bind, restrict access with trusted CIDR
|
||
subnets (for example: <code>192.168.1.0/24,10.0.0.0/8</code>).
|
||
</div>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">Bind Admin UI On LAN</span>
|
||
<span class="value" id="config-admin-bind-any-ip">-</span>
|
||
<button onclick="openEditSetting('ADMIN_BIND_ANY_IP', 'Bind Admin UI On LAN', 'toggle')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Trusted Subnets (CIDR)</span>
|
||
<span class="value" id="config-admin-trusted-subnets">-</span>
|
||
<button onclick="openEditSetting('ADMIN_TRUSTED_SUBNETS', 'Trusted Subnets (CIDR)', 'text', 'Comma-separated CIDRs allowed on admin port 5275. Example: 192.168.1.0/24,10.0.0.0/8')">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<details class="advanced-section">
|
||
<summary>Advanced: Debug Settings</summary>
|
||
<div class="advanced-section-content">
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">Log All Requests</span>
|
||
<span class="value" id="config-debug-log-requests">-</span>
|
||
<button onclick="openEditSetting('DEBUG_LOG_ALL_REQUESTS', 'Log All Requests', 'toggle', 'Enable detailed logging of every HTTP request (useful for debugging client issues)')">Edit</button>
|
||
</div>
|
||
<div class="guidance-banner info compact" style="margin-top: 12px;">
|
||
ℹ️ When enabled, logs every incoming request with method, path, headers, and response status. Auth tokens are automatically masked.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Spotify API Settings</h2>
|
||
<div
|
||
style="background: rgba(248, 81, 73, 0.15); border: 1px solid var(--error); border-radius: 6px; padding: 12px; margin-bottom: 16px; color: var(--text-primary);">
|
||
⚠️ For active playlists and link functionality to work, sp_dc session cookie must be set!
|
||
</div>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">API Enabled</span>
|
||
<span class="value" id="config-spotify-enabled">-</span>
|
||
<button
|
||
onclick="openEditSetting('SPOTIFY_API_ENABLED', 'Spotify API Enabled', 'toggle')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Session Cookie (sp_dc) <span style="color: var(--error);">*</span></span>
|
||
<span class="value" id="config-spotify-cookie">-</span>
|
||
<button
|
||
onclick="openEditSetting('SPOTIFY_API_SESSION_COOKIE', 'Spotify Session Cookie', 'password', 'Get from browser dev tools while logged into Spotify. Cookie typically lasts ~1 year.')">Update</button>
|
||
</div>
|
||
<div class="config-item" style="grid-template-columns: 200px 1fr;">
|
||
<span class="label">Cookie Age</span>
|
||
<span class="value" id="config-cookie-age">-</span>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Cache Duration</span>
|
||
<span class="value" id="config-cache-duration">-</span>
|
||
<button
|
||
onclick="openEditSetting('SPOTIFY_API_CACHE_DURATION_MINUTES', 'Cache Duration (minutes)', 'number')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">ISRC Matching</span>
|
||
<span class="value" id="config-isrc-matching">-</span>
|
||
<button
|
||
onclick="openEditSetting('SPOTIFY_API_PREFER_ISRC_MATCHING', 'Prefer ISRC Matching', 'toggle')">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Deezer Settings</h2>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">ARL Token</span>
|
||
<span class="value" id="config-deezer-arl">-</span>
|
||
<button
|
||
onclick="openEditSetting('DEEZER_ARL', 'Deezer ARL Token', 'password', 'Get from browser cookies while logged into Deezer')">Update</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Quality</span>
|
||
<span class="value" id="config-deezer-quality">-</span>
|
||
<button
|
||
onclick="openEditSetting('DEEZER_QUALITY', 'Deezer Quality', 'select', '', ['FLAC', 'MP3_320', 'MP3_128'])">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>SquidWTF / Tidal Settings</h2>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">Quality</span>
|
||
<span class="value" id="config-squid-quality">-</span>
|
||
<button
|
||
onclick="openEditSetting('SQUIDWTF_QUALITY', 'SquidWTF Quality', 'select', 'HI_RES_LOSSLESS: 24-bit/192kHz FLAC (highest)\\nLOSSLESS: 16-bit/44.1kHz FLAC (default)\\nHIGH: 320kbps AAC\\nLOW: 96kbps AAC', ['HI_RES_LOSSLESS', 'LOSSLESS', 'HIGH', 'LOW'])">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>MusicBrainz Settings</h2>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">Enabled</span>
|
||
<span class="value" id="config-musicbrainz-enabled">-</span>
|
||
<button
|
||
onclick="openEditSetting('MUSICBRAINZ_ENABLED', 'MusicBrainz Enabled', 'toggle')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Username</span>
|
||
<span class="value" id="config-musicbrainz-username">-</span>
|
||
<button
|
||
onclick="openEditSetting('MUSICBRAINZ_USERNAME', 'MusicBrainz Username', 'text', 'Your MusicBrainz username')">Update</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Password</span>
|
||
<span class="value" id="config-musicbrainz-password">-</span>
|
||
<button
|
||
onclick="openEditSetting('MUSICBRAINZ_PASSWORD', 'MusicBrainz Password', 'password', 'Your MusicBrainz password')">Update</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Qobuz Settings</h2>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">User Auth Token</span>
|
||
<span class="value" id="config-qobuz-token">-</span>
|
||
<button
|
||
onclick="openEditSetting('QOBUZ_USER_AUTH_TOKEN', 'Qobuz User Auth Token', 'password', 'Get from browser while logged into Qobuz')">Update</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Quality</span>
|
||
<span class="value" id="config-qobuz-quality">-</span>
|
||
<button
|
||
onclick="openEditSetting('QOBUZ_QUALITY', 'Qobuz Quality', 'select', '', ['FLAC_24_192', 'FLAC_24_96', 'FLAC_16_44', 'MP3_320'])">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Jellyfin Settings</h2>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">URL <span style="color: var(--error);">*</span></span>
|
||
<span class="value" id="config-jellyfin-url">-</span>
|
||
<button onclick="openEditSetting('JELLYFIN_URL', 'Jellyfin URL', 'text')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">API Key <span style="color: var(--error);">*</span></span>
|
||
<span class="value" id="config-jellyfin-api-key">-</span>
|
||
<button
|
||
onclick="openEditSetting('JELLYFIN_API_KEY', 'Jellyfin API Key', 'password')">Update</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">User ID <span style="color: var(--error);">*</span></span>
|
||
<span class="value" id="config-jellyfin-user-id">-</span>
|
||
<button
|
||
onclick="openEditSetting('JELLYFIN_USER_ID', 'Jellyfin User ID', 'text', 'Required for playlist operations. Get from Jellyfin user profile URL: userId=...')">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Library Settings</h2>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">Download Path (Cache)</span>
|
||
<span class="value" id="config-download-path">-</span>
|
||
<button
|
||
onclick="openEditSetting('LIBRARY_DOWNLOAD_PATH', 'Download Path', 'text')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Kept Path (Favorited)</span>
|
||
<span class="value" id="config-kept-path">-</span>
|
||
<button onclick="openEditSetting('LIBRARY_KEPT_PATH', 'Kept Path', 'text')">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Spotify Import Settings</h2>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">Spotify Import Enabled</span>
|
||
<span class="value" id="config-spotify-import-enabled">-</span>
|
||
<button
|
||
onclick="openEditSetting('SPOTIFY_IMPORT_ENABLED', 'Spotify Import Enabled', 'toggle')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Matching Interval (hours)</span>
|
||
<span class="value" id="config-matching-interval">-</span>
|
||
<button
|
||
onclick="openEditSetting('SPOTIFY_IMPORT_MATCHING_INTERVAL_HOURS', 'Matching Interval (hours)', 'number', 'How often to check for playlist updates')">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Cache Settings</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Configure how long different types of data are cached. Longer durations reduce API calls but may
|
||
show stale data.
|
||
</p>
|
||
<div class="config-section">
|
||
<div class="config-item">
|
||
<span class="label">Search Results (minutes)</span>
|
||
<span class="value" id="config-cache-search">-</span>
|
||
<button
|
||
onclick="openEditCacheSetting('CACHE_SEARCH_RESULTS_MINUTES', 'Search Results Cache (minutes)', 'How long to cache search results')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Playlist Images (hours)</span>
|
||
<span class="value" id="config-cache-playlist-images">-</span>
|
||
<button
|
||
onclick="openEditCacheSetting('CACHE_PLAYLIST_IMAGES_HOURS', 'Playlist Images Cache (hours)', 'How long to cache playlist cover images')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Spotify Playlist Items (hours)</span>
|
||
<span class="value" id="config-cache-spotify-items">-</span>
|
||
<button
|
||
onclick="openEditCacheSetting('CACHE_SPOTIFY_PLAYLIST_ITEMS_HOURS', 'Spotify Playlist Items Cache (hours)', 'How long to cache Spotify playlist data')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Spotify Matched Tracks (days)</span>
|
||
<span class="value" id="config-cache-matched-tracks">-</span>
|
||
<button
|
||
onclick="openEditCacheSetting('CACHE_SPOTIFY_MATCHED_TRACKS_DAYS', 'Matched Tracks Cache (days)', 'How long to cache Spotify ID to track mappings')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Lyrics (days)</span>
|
||
<span class="value" id="config-cache-lyrics">-</span>
|
||
<button
|
||
onclick="openEditCacheSetting('CACHE_LYRICS_DAYS', 'Lyrics Cache (days)', 'How long to cache fetched lyrics')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Genre Data (days)</span>
|
||
<span class="value" id="config-cache-genres">-</span>
|
||
<button
|
||
onclick="openEditCacheSetting('CACHE_GENRE_DAYS', 'Genre Cache (days)', 'How long to cache genre information')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">External Metadata (days)</span>
|
||
<span class="value" id="config-cache-metadata">-</span>
|
||
<button
|
||
onclick="openEditCacheSetting('CACHE_METADATA_DAYS', 'Metadata Cache (days)', 'How long to cache SquidWTF/Deezer/Qobuz metadata')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Odesli Lookups (days)</span>
|
||
<span class="value" id="config-cache-odesli">-</span>
|
||
<button
|
||
onclick="openEditCacheSetting('CACHE_ODESLI_LOOKUP_DAYS', 'Odesli Lookup Cache (days)', 'How long to cache Odesli URL conversions')">Edit</button>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="label">Proxy Images (days)</span>
|
||
<span class="value" id="config-cache-proxy-images">-</span>
|
||
<button
|
||
onclick="openEditCacheSetting('CACHE_PROXY_IMAGES_DAYS', 'Proxy Images Cache (days)', 'How long to cache proxied Jellyfin images')">Edit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" id="config-backup-card">
|
||
<h2>Configuration Backup</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;" id="config-backup-description">
|
||
Export your .env configuration for backup or import a previously saved configuration.
|
||
</p>
|
||
<p style="color: var(--warning); margin-bottom: 12px; display: none;" id="export-env-disabled-hint">
|
||
.env export is disabled by default for security.
|
||
</p>
|
||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||
<button id="export-env-btn" onclick="exportEnv()">📥 Export .env</button>
|
||
<button onclick="document.getElementById('import-env-input').click()">📤 Import .env</button>
|
||
<input type="file" id="import-env-input" accept=".env" style="display:none"
|
||
onchange="importEnv(event)">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" style="background: rgba(248, 81, 73, 0.1); border-color: var(--error);">
|
||
<h2 style="color: var(--error);">Danger Zone</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
These actions can affect your data. Use with caution.
|
||
</p>
|
||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||
<button class="danger" onclick="clearCache()">Clear All Cache</button>
|
||
<button class="danger" onclick="restartContainer()">Restart Container</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- API Analytics Tab -->
|
||
<div class="tab-content" id="tab-endpoints">
|
||
<div class="card">
|
||
<h2>
|
||
API Endpoint Usage
|
||
<div class="actions">
|
||
<button onclick="fetchEndpointUsage()">Refresh</button>
|
||
<button class="danger" onclick="clearEndpointUsage()">Clear Data</button>
|
||
</div>
|
||
</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Track which Jellyfin API endpoints are being called most frequently. Useful for debugging and
|
||
understanding client behavior.
|
||
</p>
|
||
|
||
<div id="endpoints-summary"
|
||
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 20px;">
|
||
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 8px;">
|
||
<div style="color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 4px;">Total
|
||
Requests</div>
|
||
<div style="font-size: 1.8rem; font-weight: 600; color: var(--accent);"
|
||
id="endpoints-total-requests">0</div>
|
||
</div>
|
||
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 8px;">
|
||
<div style="color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 4px;">Unique
|
||
Endpoints</div>
|
||
<div style="font-size: 1.8rem; font-weight: 600; color: var(--success);"
|
||
id="endpoints-unique-count">0</div>
|
||
</div>
|
||
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 8px;">
|
||
<div style="color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 4px;">Most Called
|
||
</div>
|
||
<div style="font-size: 1.1rem; font-weight: 600; color: var(--text-primary); word-break: break-all;"
|
||
id="endpoints-most-called">-</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 16px;">
|
||
<label
|
||
style="display: block; margin-bottom: 8px; color: var(--text-secondary); font-size: 0.9rem;">Show
|
||
Top</label>
|
||
<select id="endpoints-top-select" onchange="fetchEndpointUsage()"
|
||
style="padding: 8px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary);">
|
||
<option value="25">Top 25</option>
|
||
<option value="50" selected>Top 50</option>
|
||
<option value="100">Top 100</option>
|
||
<option value="500">Top 500</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="max-height: 600px; overflow-y: auto;">
|
||
<table class="playlist-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 60px;">#</th>
|
||
<th>Endpoint</th>
|
||
<th style="width: 120px; text-align: right;">Requests</th>
|
||
<th style="width: 120px; text-align: right;">% of Total</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="endpoints-table-body">
|
||
<tr>
|
||
<td colspan="4" class="loading">
|
||
<span class="spinner"></span> Loading endpoint usage data...
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>About Endpoint Tracking</h2>
|
||
<p style="color: var(--text-secondary); line-height: 1.6;">
|
||
Allstarr logs every Jellyfin API endpoint call to help you understand how clients interact with your
|
||
server.
|
||
This data is stored in <code
|
||
style="background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px;">/app/cache/endpoint-usage/endpoints.csv</code>
|
||
and persists across restarts.
|
||
<br><br>
|
||
<strong>Common Endpoints:</strong>
|
||
<ul style="margin-top: 8px; margin-left: 20px;">
|
||
<li><code>/Users/{userId}/Items</code> - Browse library items</li>
|
||
<li><code>/Items/{itemId}</code> - Get item details</li>
|
||
<li><code>/Audio/{itemId}/stream</code> - Stream audio</li>
|
||
<li><code>/Sessions/Playing</code> - Report playback status</li>
|
||
<li><code>/Search/Hints</code> - Search functionality</li>
|
||
</ul>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add Playlist Modal -->
|
||
<div class="modal" id="add-playlist-modal">
|
||
<div class="modal-content">
|
||
<h3>Add Playlist</h3>
|
||
<div class="form-group">
|
||
<label>Playlist Name</label>
|
||
<input type="text" id="new-playlist-name" placeholder="e.g., Release Radar">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Spotify Playlist ID</label>
|
||
<input type="text" id="new-playlist-id" placeholder="Get from Spotify Import plugin">
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button onclick="closeModal('add-playlist-modal')">Cancel</button>
|
||
<button class="primary" onclick="addPlaylist()">Add Playlist</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Setting Modal -->
|
||
<div class="modal" id="edit-setting-modal">
|
||
<div class="modal-content">
|
||
<h3 id="edit-setting-title">Edit Setting</h3>
|
||
<p id="edit-setting-help" style="color: var(--text-secondary); margin-bottom: 16px; display: none;"></p>
|
||
<div class="form-group">
|
||
<label id="edit-setting-label">Value</label>
|
||
<div id="edit-setting-input-container">
|
||
<input type="text" id="edit-setting-value" placeholder="Enter value">
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button onclick="closeModal('edit-setting-modal')">Cancel</button>
|
||
<button class="primary" onclick="saveEditSetting()">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Track List Modal -->
|
||
<div class="modal" id="tracks-modal">
|
||
<div class="modal-content" style="max-width: 90%; width: 90%;">
|
||
<h3 id="tracks-modal-title">Playlist Tracks</h3>
|
||
<div class="tracks-list" id="tracks-list">
|
||
<div class="loading">
|
||
<span class="spinner"></span> Loading tracks...
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button onclick="closeModal('tracks-modal')">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Manual Track Mapping Modal -->
|
||
<div class="modal" id="manual-map-modal">
|
||
<div class="modal-content" style="max-width: 760px; width: min(94vw, 760px);">
|
||
<h3>Map Track to External Provider</h3>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Map this track to an external provider (SquidWTF, Deezer, or Qobuz). For local Jellyfin tracks, use the
|
||
Jellyfin mapping modal instead.
|
||
</p>
|
||
|
||
<!-- Track Info -->
|
||
<div class="form-group">
|
||
<label>Spotify Track (Position <span id="map-position"></span>)</label>
|
||
<div style="background: var(--bg-primary); padding: 12px; border-radius: 8px; margin-bottom: 16px;">
|
||
<strong id="map-spotify-title"></strong><br>
|
||
<span style="color: var(--text-secondary);" id="map-spotify-artist"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- External Mapping Section -->
|
||
<div id="external-mapping-section">
|
||
<div class="form-group">
|
||
<label>External Provider</label>
|
||
<select id="map-external-provider" style="width: 100%;">
|
||
<option value="SquidWTF">SquidWTF</option>
|
||
<option value="Deezer">Deezer</option>
|
||
<option value="Qobuz">Qobuz</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Search External Provider</label>
|
||
<input type="text" id="map-external-search" placeholder="Search for track name or artist...">
|
||
<button onclick="searchExternalTracks()" style="margin-top: 8px; width: 100%;">🔍 Search</button>
|
||
</div>
|
||
<div id="map-external-results" style="max-height: 240px; overflow-y: auto; margin-top: 8px; margin-bottom: 12px;">
|
||
<p style="color: var(--text-secondary); text-align: center; padding: 20px;">Enter search terms and click Search</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>External Provider ID</label>
|
||
<input type="text" id="map-external-id" placeholder="Enter the provider-specific track ID..."
|
||
oninput="validateExternalMapping()">
|
||
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
|
||
For SquidWTF: Use a numeric track ID or full track URL<br>
|
||
For Deezer: Use the track ID from Deezer URLs<br>
|
||
For Qobuz: Use the track ID from Qobuz URLs
|
||
</small>
|
||
</div>
|
||
</div>
|
||
|
||
<input type="hidden" id="map-playlist-name">
|
||
<input type="hidden" id="map-spotify-id">
|
||
<div class="modal-actions">
|
||
<button onclick="closeModal('manual-map-modal')">Cancel</button>
|
||
<button class="primary" onclick="saveManualMapping()" id="map-save-btn" disabled>Save Mapping</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Local Jellyfin Track Mapping Modal -->
|
||
<div class="modal" id="local-map-modal">
|
||
<div class="modal-content" style="max-width: 700px;">
|
||
<h3>Map Track to Local Jellyfin Track</h3>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Search your Jellyfin library and select a local track to map to this Spotify track.
|
||
</p>
|
||
|
||
<!-- Track Info -->
|
||
<div class="form-group">
|
||
<label>Spotify Track (Position <span id="local-map-position"></span>)</label>
|
||
<div style="background: var(--bg-primary); padding: 12px; border-radius: 8px; margin-bottom: 16px;">
|
||
<strong id="local-map-spotify-title"></strong><br>
|
||
<span style="color: var(--text-secondary);" id="local-map-spotify-artist"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Search Section -->
|
||
<div class="form-group">
|
||
<label>Search Jellyfin Library</label>
|
||
<input type="text" id="local-map-search" placeholder="Search for track name or artist...">
|
||
<button onclick="searchJellyfinTracks()" style="margin-top: 8px; width: 100%;">🔍 Search</button>
|
||
</div>
|
||
|
||
<!-- Search Results -->
|
||
<div id="local-map-results" style="max-height: 300px; overflow-y: auto; margin-top: 16px;"></div>
|
||
|
||
<input type="hidden" id="local-map-playlist-name">
|
||
<input type="hidden" id="local-map-spotify-id">
|
||
<input type="hidden" id="local-map-jellyfin-id">
|
||
<div class="modal-actions">
|
||
<button onclick="closeModal('local-map-modal')">Cancel</button>
|
||
<button class="primary" onclick="saveLocalMapping()" id="local-map-save-btn" disabled>Save
|
||
Mapping</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Link Playlist Modal -->
|
||
<div class="modal" id="link-playlist-modal">
|
||
<div class="modal-content">
|
||
<h3>Link to Spotify Playlist</h3>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Select a playlist from your Spotify library or enter a playlist ID/URL manually. Allstarr will
|
||
automatically download missing tracks from your configured music service.
|
||
</p>
|
||
<div class="form-group">
|
||
<label>Jellyfin Playlist</label>
|
||
<input type="text" id="link-jellyfin-name" readonly style="background: var(--bg-primary);">
|
||
<input type="hidden" id="link-jellyfin-id">
|
||
</div>
|
||
|
||
<!-- Toggle between select and manual input -->
|
||
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
|
||
<button type="button" id="select-mode-btn" class="primary" onclick="switchLinkMode('select')"
|
||
style="flex: 1;">Select from My Playlists</button>
|
||
<button type="button" id="manual-mode-btn" onclick="switchLinkMode('manual')" style="flex: 1;">Enter
|
||
Manually</button>
|
||
</div>
|
||
|
||
<!-- Select from user playlists -->
|
||
<div class="form-group" id="link-select-group">
|
||
<label>Your Spotify Playlists</label>
|
||
<select id="link-spotify-select" style="width: 100%;">
|
||
<option value="">Loading playlists...</option>
|
||
</select>
|
||
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
|
||
Select a playlist from your Spotify library
|
||
</small>
|
||
</div>
|
||
|
||
<!-- Manual input -->
|
||
<div class="form-group" id="link-manual-group" style="display: none;">
|
||
<label>Spotify Playlist ID or URL</label>
|
||
<input type="text" id="link-spotify-id"
|
||
placeholder="37i9dQZF1DXcBWIGoYBM5M or spotify:playlist:... or full URL">
|
||
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
|
||
Accepts: <code>37i9dQZF1DXcBWIGoYBM5M</code>, <code>spotify:playlist:37i9dQZF1DXcBWIGoYBM5M</code>,
|
||
or full Spotify URL
|
||
</small>
|
||
</div>
|
||
|
||
<!-- Sync Schedule -->
|
||
<div class="form-group">
|
||
<label>Sync Schedule (Cron)</label>
|
||
<input type="text" id="link-sync-schedule" placeholder="0 8 * * *" value="0 8 * * *"
|
||
style="font-family: monospace;">
|
||
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
|
||
Cron format: <code>minute hour day month dayofweek</code><br>
|
||
Default: <code>0 8 * * *</code> = 8 AM every day<br>
|
||
Examples: <code>0 6 * * *</code> = daily at 6 AM, <code>0 20 * * 5</code> = Fridays at 8 PM<br>
|
||
<a href="https://crontab.guru/" target="_blank" style="color: var(--primary);">Use crontab.guru to
|
||
build your schedule</a>
|
||
</small>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button onclick="closeModal('link-playlist-modal')">Cancel</button>
|
||
<button class="primary" onclick="linkPlaylist()">Link Playlist</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lyrics ID Mapping Modal -->
|
||
<div class="modal" id="lyrics-map-modal">
|
||
<div class="modal-content" style="max-width: 600px;">
|
||
<h3>Map Lyrics ID</h3>
|
||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||
Manually map a track to a specific lyrics ID from lrclib.net. You can find lyrics IDs by searching on <a
|
||
href="https://lrclib.net" target="_blank" style="color: var(--accent);">lrclib.net</a>.
|
||
</p>
|
||
|
||
<!-- Track Info -->
|
||
<div class="form-group">
|
||
<label>Track</label>
|
||
<div style="background: var(--bg-primary); padding: 12px; border-radius: 8px; margin-bottom: 16px;">
|
||
<strong id="lyrics-map-title"></strong><br>
|
||
<span style="color: var(--text-secondary);" id="lyrics-map-artist"></span><br>
|
||
<small style="color: var(--text-secondary);" id="lyrics-map-album"></small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lyrics ID Input -->
|
||
<div class="form-group">
|
||
<label>Lyrics ID from lrclib.net</label>
|
||
<input type="number" id="lyrics-map-id" placeholder="Enter lyrics ID (e.g., 5929990)" min="1">
|
||
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
|
||
Search for the track on <a href="https://lrclib.net" target="_blank"
|
||
style="color: var(--accent);">lrclib.net</a> and copy the ID from the URL or API response
|
||
</small>
|
||
</div>
|
||
|
||
<input type="hidden" id="lyrics-map-artist-value">
|
||
<input type="hidden" id="lyrics-map-title-value">
|
||
<input type="hidden" id="lyrics-map-album-value">
|
||
<input type="hidden" id="lyrics-map-duration">
|
||
|
||
<div class="modal-actions">
|
||
<button onclick="closeModal('lyrics-map-modal')">Cancel</button>
|
||
<button class="primary" onclick="saveLyricsMapping()" id="lyrics-map-save-btn">Save Mapping</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Restart Overlay -->
|
||
<div class="restart-overlay" id="restart-overlay">
|
||
<div class="spinner-large"></div>
|
||
<h2>Restarting Container</h2>
|
||
<p id="restart-status">Applying configuration changes...</p>
|
||
</div>
|
||
|
||
<script type="module" src="js/main.js"></script>
|
||
|
||
</html>
|