Fix: Refactor playlist loading and sharing logic

Resolves an issue where Tidal playlists with UUIDs were incorrectly identified as missing user playlists.

Introduces explicit source (api/user) parameter to renderPlaylistPage for reliable routing.

Restricts the Share button to user playlists only and fixes npm deprecation warnings.

# Conflicts:
#	package.json
This commit is contained in:
Julien Maille 2026-01-08 13:00:48 +01:00
parent c9f639ba6c
commit ff166b27ca
5 changed files with 63 additions and 106 deletions

View file

@ -378,7 +378,7 @@ document.addEventListener('DOMContentLoaded', async () => {
ui.renderLibraryPage();
// Also update current page if we are on it
if (window.location.hash === `#userplaylist/${editingId}`) {
ui.renderPlaylistPage(editingId);
ui.renderPlaylistPage(editingId, 'user');
}
modal.style.display = 'none';
delete modal.dataset.editingId;
@ -553,7 +553,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const trackId = playlist.tracks[index].id;
const updatedPlaylist = await db.removeTrackFromPlaylist(playlistId, trackId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
ui.renderPlaylistPage(playlistId);
ui.renderPlaylistPage(playlistId, 'user');
}
});
}

View file

@ -17,10 +17,10 @@ export function createRouter(ui) {
ui.renderArtistPage(param);
break;
case 'playlist':
ui.renderPlaylistPage(param);
ui.renderPlaylistPage(param, 'api');
break;
case 'userplaylist':
ui.renderPlaylistPage(param);
ui.renderPlaylistPage(param, 'user');
break;
case 'mix':
ui.renderMixPage(param);

View file

@ -1103,7 +1103,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
}
}
async renderPlaylistPage(playlistId) {
async renderPlaylistPage(playlistId, source = null) {
this.showPage('playlist');
const imageEl = document.getElementById('playlist-detail-image');
const titleEl = document.getElementById('playlist-detail-title');
@ -1133,15 +1133,25 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
// Check if it's a user playlist (UUID format)
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(playlistId);
const ownedPlaylist = await db.getPlaylist(playlistId);
let playlistData = ownedPlaylist;
// If not in local DB, check if it's a public Firebase playlist
if (!playlistData) {
try {
playlistData = await syncManager.getPublicPlaylist(playlistId);
} catch (e) {
console.warn('Failed to check public Firebase playlists:', e);
let playlistData = null;
let ownedPlaylist = null;
// Priority:
// 1. If source is 'user', check DB/Sync.
// 2. If source is 'api', check API.
// 3. If no source, check DB if UUID, then API.
if (source === 'user' || (!source && isUUID)) {
ownedPlaylist = await db.getPlaylist(playlistId);
playlistData = ownedPlaylist;
// If not in local DB, check if it's a public Firebase playlist
if (!playlistData) {
try {
playlistData = await syncManager.getPublicPlaylist(playlistId);
} catch (e) {
console.warn('Failed to check public Firebase playlists:', e);
}
}
}
@ -1221,9 +1231,9 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
});
document.title = `${playlistData.name || playlistData.title} - Monochrome`;
} else {
// If it is a UUID, we know it won't be in the API.
if (isUUID) {
throw new Error('Playlist not found. If this is a custom playlist, make sure it is set to Public.');
// If source was explicitly 'user' and we didn't find it, fail.
if (source === 'user') {
throw new Error('Playlist not found. If this is a custom playlist, make sure it is set to Public.');
}
// Render API playlist
@ -1282,7 +1292,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
}
// Render Actions (Shuffle + Share)
this.updatePlaylistHeaderActions(playlist, false, tracks, true);
this.updatePlaylistHeaderActions(playlist, false, tracks, false);
recentActivityManager.addPlaylist(playlist);
document.title = playlist.title || 'Artist Mix';
@ -1608,28 +1618,15 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
fragment.appendChild(deleteBtn);
}
// Share
if (showShare || playlist.isPublic) {
// Share (User Playlists Only)
if (showShare || (isOwned && playlist.isPublic)) {
const shareBtn = document.createElement('button');
shareBtn.id = 'share-playlist-btn';
shareBtn.className = 'btn-secondary';
shareBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg><span>Share</span>';
// Determine URL based on type (User Playlist or API Playlist)
// User Playlists use #userplaylist/ID, API use #playlist/UUID
// But we don't have isOwned strictly here for that decision?
// Actually, if it's owned it's #userplaylist.
// If it's public firebase, it's also #userplaylist.
// If it's API, it's #playlist.
// Heuristic: If it has `isPublic` property (even if false), it's likely a User/Firebase playlist structure.
// API playlists don't usually have `isPublic`.
const isUserType = 'isPublic' in playlist || isOwned;
const prefix = isUserType ? 'userplaylist' : 'playlist';
shareBtn.onclick = () => {
const url = `${window.location.origin}${window.location.pathname}#${prefix}/${playlist.id || playlist.uuid}`;
const url = `${window.location.origin}${window.location.pathname}#userplaylist/${playlist.id || playlist.uuid}`;
navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!'));
};
fragment.appendChild(shareBtn);

100
package-lock.json generated
View file

@ -62,6 +62,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@ -2542,6 +2543,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@ -2750,6 +2752,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -4296,13 +4299,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
"dev": true,
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -4323,6 +4319,14 @@
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/magic-string/node_modules/sourcemap-codec": {
"name": "@jridgewell/sourcemap-codec",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -4577,16 +4581,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -5053,20 +5047,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
"integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==",
"deprecated": "The work that was done in this beta branch won't be included in future versions",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"whatwg-url": "^7.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -5089,23 +5069,15 @@
}
},
"node_modules/source-map-support/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
"node": ">= 12"
}
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
"dev": true,
"license": "MIT"
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@ -5384,6 +5356,7 @@
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@ -5414,16 +5387,6 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tr46": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
"integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/type-fest": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
@ -5649,6 +5612,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@ -5749,25 +5713,6 @@
}
}
},
"node_modules/webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
"integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -6045,6 +5990,7 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@ -6055,6 +6001,16 @@
"fsevents": "~2.3.2"
}
},
"node_modules/workbox-build/node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 12"
}
},
"node_modules/workbox-cacheable-response": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz",

View file

@ -24,5 +24,9 @@
"devDependencies": {
"vite": "^7.3.0",
"vite-plugin-pwa": "^1.2.0"
},
"overrides": {
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.4.14",
"source-map": "^0.7.4"
}
}