Remove LocalTracksPosition UI, auto-init cookie date tracking

- Removed LocalTracksPosition from playlist table (uses Spotify order now)
- Removed LocalTracksPosition from add playlist modal and JS
- Added /api/admin/config/init-cookie-date endpoint to auto-set date
- Cookie date auto-initializes when cookie exists but date is unknown
- Improved user display message when profile scope unavailable
- TOTP tokens work for playlists but don't have user-read-private scope
This commit is contained in:
2026-02-03 15:00:12 -05:00
parent b7379e2fd4
commit 4036c739a3
2 changed files with 65 additions and 20 deletions

View File

@@ -76,7 +76,7 @@ public class AdminController : ControllerBase
var token = await _spotifyClient.GetWebAccessTokenAsync(); var token = await _spotifyClient.GetWebAccessTokenAsync();
if (!string.IsNullOrEmpty(token)) if (!string.IsNullOrEmpty(token))
{ {
// Try to get user info (may fail even with valid token) // Try to get user info (may fail even with valid token due to scope limitations)
var (success, userId, displayName) = await _spotifyClient.GetCurrentUserAsync(); var (success, userId, displayName) = await _spotifyClient.GetCurrentUserAsync();
if (success) if (success)
{ {
@@ -85,10 +85,10 @@ public class AdminController : ControllerBase
} }
else else
{ {
// Token is valid but /me endpoint failed - still consider it authenticated // Token is valid but /me endpoint failed - this is normal for TOTP tokens
// (the token being non-anonymous is the real indicator) // which don't have user-read-private scope. Playlists still work fine.
spotifyAuthStatus = "authenticated"; spotifyAuthStatus = "authenticated";
spotifyUser = "(profile not accessible)"; spotifyUser = "(working - profile scope not available)";
} }
} }
else else
@@ -498,6 +498,36 @@ public class AdminController : ControllerBase
return Ok(new { message = "Cache cleared", filesDeleted = clearedFiles }); return Ok(new { message = "Cache cleared", filesDeleted = clearedFiles });
} }
/// <summary>
/// Initialize cookie date to current date if cookie exists but date is not set
/// </summary>
[HttpPost("config/init-cookie-date")]
public async Task<IActionResult> InitCookieDate()
{
// Only init if cookie exists but date is not set
if (string.IsNullOrEmpty(_spotifyApiSettings.SessionCookie))
{
return BadRequest(new { error = "No cookie set" });
}
if (!string.IsNullOrEmpty(_spotifyApiSettings.SessionCookieSetDate))
{
return Ok(new { message = "Cookie date already set", date = _spotifyApiSettings.SessionCookieSetDate });
}
_logger.LogInformation("Initializing cookie date to current date (cookie existed without date tracking)");
var updateRequest = new ConfigUpdateRequest
{
Updates = new Dictionary<string, string>
{
["SPOTIFY_API_SESSION_COOKIE_SET_DATE"] = DateTime.UtcNow.ToString("o")
}
};
return await UpdateConfig(updateRequest);
}
private static string MaskValue(string? value, int showLast = 0) private static string MaskValue(string? value, int showLast = 0)
{ {
if (string.IsNullOrEmpty(value)) return "(not set)"; if (string.IsNullOrEmpty(value)) return "(not set)";

View File

@@ -532,7 +532,6 @@
<th>Name</th> <th>Name</th>
<th>Spotify ID</th> <th>Spotify ID</th>
<th>Tracks</th> <th>Tracks</th>
<th>Position</th>
<th>Cache Age</th> <th>Cache Age</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
@@ -685,13 +684,6 @@
<label>Spotify Playlist ID</label> <label>Spotify Playlist ID</label>
<input type="text" id="new-playlist-id" placeholder="Get from Spotify Import plugin"> <input type="text" id="new-playlist-id" placeholder="Get from Spotify Import plugin">
</div> </div>
<div class="form-group">
<label>Local Tracks Position</label>
<select id="new-playlist-position">
<option value="first">First (local tracks shown before Spotify tracks)</option>
<option value="last">Last (Spotify tracks shown first)</option>
</select>
</div>
<div class="modal-actions"> <div class="modal-actions">
<button onclick="closeModal('add-playlist-modal')">Cancel</button> <button onclick="closeModal('add-playlist-modal')">Cancel</button>
<button class="primary" onclick="addPlaylist()">Add Playlist</button> <button class="primary" onclick="addPlaylist()">Add Playlist</button>
@@ -774,8 +766,13 @@
}); });
// Format cookie age with color coding // Format cookie age with color coding
function formatCookieAge(setDateStr) { function formatCookieAge(setDateStr, hasCookie = false) {
if (!setDateStr) return { text: 'Unknown age', class: 'warning', detail: 'Set the cookie to start tracking its age' }; if (!setDateStr) {
if (hasCookie) {
return { text: 'Unknown age', class: 'warning', detail: 'Initializing tracking...', needsInit: true };
}
return { text: 'No cookie', class: '', detail: '' };
}
const setDate = new Date(setDateStr); const setDate = new Date(setDateStr);
const now = new Date(); const now = new Date();
@@ -804,6 +801,20 @@
return { text, class: status, detail }; return { text, class: status, detail };
} }
// Initialize cookie date if cookie exists but date is not set
async function initCookieDate() {
try {
const res = await fetch('/api/admin/config/init-cookie-date', { method: 'POST' });
if (res.ok) {
// Refresh status after initialization
fetchStatus();
fetchConfig();
}
} catch (error) {
console.error('Failed to init cookie date:', error);
}
}
// API calls // API calls
async function fetchStatus() { async function fetchStatus() {
try { try {
@@ -847,8 +858,14 @@
// Update cookie age display // Update cookie age display
if (cookieAgeEl) { if (cookieAgeEl) {
const age = formatCookieAge(data.spotify.cookieSetDate); const hasCookie = data.spotify.hasCookie;
const age = formatCookieAge(data.spotify.cookieSetDate, hasCookie);
cookieAgeEl.innerHTML = `<span class="${age.class}">${age.text}</span><br><small style="color:var(--text-secondary)">${age.detail}</small>`; cookieAgeEl.innerHTML = `<span class="${age.class}">${age.text}</span><br><small style="color:var(--text-secondary)">${age.detail}</small>`;
// Auto-init cookie date if cookie exists but date is not set
if (age.needsInit) {
initCookieDate();
}
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch status:', error); console.error('Failed to fetch status:', error);
@@ -873,7 +890,6 @@
<td><strong>${escapeHtml(p.name)}</strong></td> <td><strong>${escapeHtml(p.name)}</strong></td>
<td style="font-family:monospace;font-size:0.85rem;color:var(--text-secondary);">${p.id || '-'}</td> <td style="font-family:monospace;font-size:0.85rem;color:var(--text-secondary);">${p.id || '-'}</td>
<td class="track-count">${p.trackCount || 0}</td> <td class="track-count">${p.trackCount || 0}</td>
<td>${p.localTracksPosition}</td>
<td class="cache-age">${p.cacheAge || '-'}</td> <td class="cache-age">${p.cacheAge || '-'}</td>
<td> <td>
<button onclick="viewTracks('${escapeHtml(p.name)}')">View</button> <button onclick="viewTracks('${escapeHtml(p.name)}')">View</button>
@@ -901,7 +917,8 @@
// Cookie age in config tab // Cookie age in config tab
const configCookieAge = document.getElementById('config-cookie-age'); const configCookieAge = document.getElementById('config-cookie-age');
if (configCookieAge) { if (configCookieAge) {
const age = formatCookieAge(data.spotifyApi.sessionCookieSetDate); const hasCookie = data.spotifyApi.sessionCookie && data.spotifyApi.sessionCookie !== '(not set)';
const age = formatCookieAge(data.spotifyApi.sessionCookieSetDate, hasCookie);
configCookieAge.innerHTML = `<span class="${age.class}">${age.text}</span> - ${age.detail}`; configCookieAge.innerHTML = `<span class="${age.class}">${age.text}</span> - ${age.detail}`;
} }
@@ -963,14 +980,12 @@
function openAddPlaylist() { function openAddPlaylist() {
document.getElementById('new-playlist-name').value = ''; document.getElementById('new-playlist-name').value = '';
document.getElementById('new-playlist-id').value = ''; document.getElementById('new-playlist-id').value = '';
document.getElementById('new-playlist-position').value = 'first';
openModal('add-playlist-modal'); openModal('add-playlist-modal');
} }
async function addPlaylist() { async function addPlaylist() {
const name = document.getElementById('new-playlist-name').value.trim(); const name = document.getElementById('new-playlist-name').value.trim();
const id = document.getElementById('new-playlist-id').value.trim(); const id = document.getElementById('new-playlist-id').value.trim();
const position = document.getElementById('new-playlist-position').value;
if (!name || !id) { if (!name || !id) {
showToast('Name and ID are required', 'error'); showToast('Name and ID are required', 'error');
@@ -981,7 +996,7 @@
const res = await fetch('/api/admin/playlists', { const res = await fetch('/api/admin/playlists', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, spotifyId: id, localTracksPosition: position }) body: JSON.stringify({ name, spotifyId: id })
}); });
const data = await res.json(); const data = await res.json();