fix add to playlist modal not checking songs, making playlist causes big issues

This commit is contained in:
Samidy 2026-01-12 14:30:49 +03:00
parent 0b6c1a4230
commit de86337e3e
4 changed files with 138 additions and 38 deletions

View file

@ -928,6 +928,13 @@ document.addEventListener('DOMContentLoaded', async () => {
ui.renderLibraryPage(); ui.renderLibraryPage();
} else if (hash === '#home' || hash === '') { } else if (hash === '#home' || hash === '') {
ui.renderHomePage(); ui.renderHomePage();
} else if (hash.startsWith('#userplaylist/')) {
const playlistId = hash.split('/')[1];
const content = document.querySelector('.main-content');
const scroll = content ? content.scrollTop : 0;
ui.renderPlaylistPage(playlistId, 'user').then(() => {
if (content) content.scrollTop = scroll;
});
} }
}); });
window.addEventListener('history-changed', () => { window.addEventListener('history-changed', () => {

View file

@ -168,27 +168,27 @@ export class MusicDatabase {
if (type === 'track') { if (type === 'track') {
return { return {
...base, ...base,
title: item.title, title: item.title || null,
duration: item.duration, duration: item.duration || null,
explicit: item.explicit, explicit: item.explicit || false,
// Keep minimal artist info // Keep minimal artist info
artist: item.artist || (item.artists && item.artists.length > 0 ? item.artists[0] : null), artist: item.artist || (item.artists && item.artists.length > 0 ? item.artists[0] : null) || null,
artists: item.artists?.map((a) => ({ id: a.id, name: a.name })) || [], artists: item.artists?.map((a) => ({ id: a.id, name: a.name || null })) || [],
// Keep minimal album info // Keep minimal album info
album: item.album album: item.album
? { ? {
id: item.album.id, id: item.album.id,
title: item.album.title, title: item.album.title || null,
cover: item.album.cover, cover: item.album.cover || null,
releaseDate: item.album.releaseDate || null, releaseDate: item.album.releaseDate || null,
vibrantColor: item.album.vibrantColor || null, vibrantColor: item.album.vibrantColor || null,
artist: item.album.artist, artist: item.album.artist || null,
numberOfTracks: item.album.numberOfTracks, numberOfTracks: item.album.numberOfTracks || null,
} }
: null, : null,
copyright: item.copyright, copyright: item.copyright || null,
isrc: item.isrc, isrc: item.isrc || null,
trackNumber: item.trackNumber, trackNumber: item.trackNumber || null,
// Fallback date // Fallback date
streamStartDate: item.streamStartDate || null, streamStartDate: item.streamStartDate || null,
// Keep version if exists // Keep version if exists
@ -201,26 +201,26 @@ export class MusicDatabase {
if (type === 'album') { if (type === 'album') {
return { return {
...base, ...base,
title: item.title, title: item.title || null,
cover: item.cover, cover: item.cover || null,
releaseDate: item.releaseDate || null, releaseDate: item.releaseDate || null,
explicit: item.explicit, explicit: item.explicit || false,
// UI uses singular 'artist' // UI uses singular 'artist'
artist: item.artist artist: item.artist
? { name: item.artist.name, id: item.artist.id } ? { name: item.artist.name || null, id: item.artist.id }
: item.artists?.[0] : item.artists?.[0]
? { name: item.artists[0].name, id: item.artists[0].id } ? { name: item.artists[0].name || null, id: item.artists[0].id }
: null, : null,
// Keep type and track count for UI labels // Keep type and track count for UI labels
type: item.type || null, type: item.type || null,
numberOfTracks: item.numberOfTracks, numberOfTracks: item.numberOfTracks || null,
}; };
} }
if (type === 'artist') { if (type === 'artist') {
return { return {
...base, ...base,
name: item.name, name: item.name || null,
picture: item.picture || item.image || null, // Handle both just in case picture: item.picture || item.image || null, // Handle both just in case
}; };
} }
@ -229,11 +229,11 @@ export class MusicDatabase {
return { return {
uuid: item.uuid || item.id, uuid: item.uuid || item.id,
addedAt: item.addedAt || item.createdAt || null, addedAt: item.addedAt || item.createdAt || null,
title: item.title || item.name, title: item.title || item.name || null,
// UI checks squareImage || image || uuid // UI checks squareImage || image || uuid
image: item.image || item.squareImage || item.cover || null, image: item.image || item.squareImage || item.cover || null,
numberOfTracks: item.numberOfTracks || (item.tracks ? item.tracks.length : 0), numberOfTracks: item.numberOfTracks || (item.tracks ? item.tracks.length : 0),
user: item.user ? { name: item.user.name } : null, user: item.user ? { name: item.user.name || null } : null,
}; };
} }
@ -260,7 +260,7 @@ export class MusicDatabase {
const mixes = await this.getFavorites('mix'); const mixes = await this.getFavorites('mix');
const history = await this.getHistory(); const history = await this.getHistory();
const userPlaylists = await this.getPlaylists(); const userPlaylists = await this.getPlaylists(true);
const data = { const data = {
favorites_tracks: tracks.map((t) => this._minifyItem('track', t)), favorites_tracks: tracks.map((t) => this._minifyItem('track', t)),
favorites_albums: albums.map((a) => this._minifyItem('album', a)), favorites_albums: albums.map((a) => this._minifyItem('album', a)),
@ -339,6 +339,7 @@ export class MusicDatabase {
tracks: tracks.map((t) => this._minifyItem('track', t)), tracks: tracks.map((t) => this._minifyItem('track', t)),
cover: cover, cover: cover,
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(),
}; };
this._updatePlaylistMetadata(playlist); this._updatePlaylistMetadata(playlist);
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
@ -352,6 +353,7 @@ export class MusicDatabase {
const minifiedTrack = this._minifyItem('track', track); const minifiedTrack = this._minifyItem('track', track);
if (playlist.tracks.some((t) => t.id === track.id)) return; if (playlist.tracks.some((t) => t.id === track.id)) return;
playlist.tracks.push(minifiedTrack); playlist.tracks.push(minifiedTrack);
playlist.updatedAt = Date.now();
this._updatePlaylistMetadata(playlist); this._updatePlaylistMetadata(playlist);
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
return playlist; return playlist;
@ -362,6 +364,7 @@ export class MusicDatabase {
if (!playlist) throw new Error('Playlist not found'); if (!playlist) throw new Error('Playlist not found');
playlist.tracks = playlist.tracks || []; playlist.tracks = playlist.tracks || [];
playlist.tracks = playlist.tracks.filter((t) => t.id !== trackId); playlist.tracks = playlist.tracks.filter((t) => t.id !== trackId);
playlist.updatedAt = Date.now();
this._updatePlaylistMetadata(playlist); this._updatePlaylistMetadata(playlist);
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
return playlist; return playlist;
@ -375,7 +378,7 @@ export class MusicDatabase {
return await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId)); return await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId));
} }
async getPlaylists() { async getPlaylists(includeTracks = false) {
const db = await this.open(); const db = await this.open();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction('user_playlists', 'readwrite'); // Changed to readwrite for lazy migration const transaction = db.transaction('user_playlists', 'readwrite'); // Changed to readwrite for lazy migration
@ -408,6 +411,10 @@ export class MusicDatabase {
} }
} }
if (includeTracks) {
return playlist;
}
// Return lightweight copy without tracks // Return lightweight copy without tracks
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const { tracks, ...minified } = playlist; const { tracks, ...minified } = playlist;
@ -423,6 +430,7 @@ export class MusicDatabase {
const playlist = await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId)); const playlist = await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId));
if (!playlist) throw new Error('Playlist not found'); if (!playlist) throw new Error('Playlist not found');
playlist.name = newName; playlist.name = newName;
playlist.updatedAt = Date.now();
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
return playlist; return playlist;
} }
@ -441,6 +449,7 @@ export class MusicDatabase {
return; return;
} }
playlist.tracks = tracks; playlist.tracks = tracks;
playlist.updatedAt = Date.now();
this._updatePlaylistMetadata(playlist); this._updatePlaylistMetadata(playlist);
const putRequest = store.put(playlist); const putRequest = store.put(playlist);
putRequest.onsuccess = () => { putRequest.onsuccess = () => {

View file

@ -659,7 +659,7 @@ export async function handleTrackAction(
} }
} }
} else if (action === 'add-to-playlist') { } else if (action === 'add-to-playlist') {
const playlists = await db.getPlaylists(); const playlists = await db.getPlaylists(true);
if (playlists.length === 0) { if (playlists.length === 0) {
showNotification('No playlists yet. Create one first.'); showNotification('No playlists yet. Create one first.');
return; return;
@ -675,7 +675,7 @@ export async function handleTrackAction(
const playlistsWithTrack = new Set(); const playlistsWithTrack = new Set();
for (const playlist of playlists) { for (const playlist of playlists) {
if (playlist.tracks && playlist.tracks.some((track) => track.id === trackId)) { if (playlist.tracks && playlist.tracks.some((track) => track.id == trackId)) {
playlistsWithTrack.add(playlist.id); playlistsWithTrack.add(playlist.id);
} }
} }

View file

@ -10,6 +10,9 @@ import {
child, child,
remove, remove,
runTransaction, runTransaction,
onChildAdded,
onChildChanged,
onChildRemoved,
} from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-database.js'; } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-database.js';
import { db } from '../db.js'; import { db } from '../db.js';
@ -19,6 +22,7 @@ export class SyncManager {
this.userRef = null; this.userRef = null;
this.unsubscribeFunctions = []; this.unsubscribeFunctions = [];
this.isSyncing = false; this.isSyncing = false;
this.listenersSetup = false;
} }
initialize(user) { initialize(user) {
@ -38,6 +42,7 @@ export class SyncManager {
} }
this.user = null; this.user = null;
this.userRef = null; this.userRef = null;
this.listenersSetup = false;
console.log('SyncManager disconnected'); console.log('SyncManager disconnected');
} }
@ -83,12 +88,10 @@ export class SyncManager {
await db.importData(importData, true); await db.importData(importData, true);
console.log('Initial sync complete.'); console.log('Initial sync complete.');
// 6. Setup Listeners for future changes
this.setupListeners();
} catch (error) { } catch (error) {
console.error('Initial sync failed:', error); console.error('Initial sync failed:', error);
} finally { } finally {
this.setupListeners();
this.isSyncing = false; this.isSyncing = false;
} }
} }
@ -110,13 +113,48 @@ export class SyncManager {
// Add/Overwrite with cloud items (Union Strategy) // Add/Overwrite with cloud items (Union Strategy)
if (cloudItems) { if (cloudItems) {
const processItem = (item, key) => {
if (!item || typeof item !== 'object') return;
if (item.tracks && typeof item.tracks === 'object' && !Array.isArray(item.tracks)) {
item.tracks = Object.values(item.tracks);
}
const id = item[idKey] || key;
const localItem = map.get(id);
if (localItem) {
const localTime = localItem.updatedAt || 0;
const cloudTime = item.updatedAt || 0;
if (cloudTime > localTime) {
const localTracks = Array.isArray(localItem.tracks) ? localItem.tracks.length : 0;
const cloudTracks = Array.isArray(item.tracks) ? item.tracks.length : 0;
if (localTracks > 0 && cloudTracks === 0) {
} else {
map.set(id, item);
}
} else if (cloudTime === localTime) {
const localTracks = Array.isArray(localItem.tracks) ? localItem.tracks.length : 0;
const cloudTracks = Array.isArray(item.tracks) ? item.tracks.length : 0;
if (cloudTracks >= localTracks) {
map.set(id, item);
}
}
} else {
map.set(id, item);
}
};
if (Array.isArray(cloudItems)) { if (Array.isArray(cloudItems)) {
cloudItems.forEach((item) => map.set(item[idKey], item)); cloudItems.forEach((item) => processItem(item));
} else { } else {
Object.keys(cloudItems).forEach((key) => { Object.keys(cloudItems).forEach((key) => {
const val = cloudItems[key]; const val = cloudItems[key];
if (typeof val === 'object') { if (typeof val === 'object') {
map.set(val[idKey] || key, val); processItem(val, key);
} }
}); });
} }
@ -162,6 +200,9 @@ export class SyncManager {
} }
setupListeners() { setupListeners() {
if (!this.userRef || this.listenersSetup) return;
this.listenersSetup = true;
// Listen for changes in library // Listen for changes in library
const libraryRef = child(this.userRef, 'library'); const libraryRef = child(this.userRef, 'library');
@ -208,22 +249,42 @@ export class SyncManager {
// Listen for changes in user playlists // Listen for changes in user playlists
const userPlaylistsRef = child(this.userRef, 'user_playlists'); const userPlaylistsRef = child(this.userRef, 'user_playlists');
const unsubUserPlaylists = onValue(userPlaylistsRef, (snapshot) => { const handlePlaylistUpdate = (snapshot) => {
if (this.isSyncing) return; if (this.isSyncing) return;
const val = snapshot.val(); const val = snapshot.val();
if (val) { if (val) {
if (val.tracks && typeof val.tracks === 'object' && !Array.isArray(val.tracks)) {
val.tracks = Object.values(val.tracks);
}
const importData = { const importData = {
user_playlists: Object.values(val), user_playlists: [val],
}; };
db.importData(importData, true).then(() => { db.importData(importData, false).then(() => {
// Notify UI to refresh library // Notify UI to refresh library
window.dispatchEvent(new Event('library-changed')); window.dispatchEvent(new Event('library-changed'));
}); });
} }
};
const unsubChildAdded = onChildAdded(userPlaylistsRef, handlePlaylistUpdate);
const unsubChildChanged = onChildChanged(userPlaylistsRef, handlePlaylistUpdate);
const unsubChildRemoved = onChildRemoved(userPlaylistsRef, (snapshot) => {
if (this.isSyncing) return;
const key = snapshot.key;
if (key) {
db.deletePlaylist(key).then(() => {
window.dispatchEvent(new Event('library-changed'));
});
}
}); });
this.unsubscribeFunctions.push(() => off(userPlaylistsRef, 'value', unsubUserPlaylists)); this.unsubscribeFunctions.push(() => {
off(userPlaylistsRef, 'child_added', unsubChildAdded);
off(userPlaylistsRef, 'child_changed', unsubChildChanged);
off(userPlaylistsRef, 'child_removed', unsubChildRemoved);
});
} }
// --- Public API for Broadcasters --- // --- Public API for Broadcasters ---
@ -258,7 +319,7 @@ export class SyncManager {
...minified, ...minified,
addedAt: item.addedAt || minified.addedAt || Date.now(), addedAt: item.addedAt || minified.addedAt || Date.now(),
}; };
await set(itemRef, entry); await set(itemRef, this.sanitizeForFirebase(entry));
} else { } else {
await remove(itemRef); await remove(itemRef);
} }
@ -269,7 +330,7 @@ export class SyncManager {
const itemRef = child(this.userRef, `history/recentTracks/${track.timestamp}`); const itemRef = child(this.userRef, `history/recentTracks/${track.timestamp}`);
try { try {
await set(itemRef, track); await set(itemRef, this.sanitizeForFirebase(track));
} catch (error) { } catch (error) {
console.error('Failed to sync history item:', error); console.error('Failed to sync history item:', error);
} }
@ -283,7 +344,11 @@ export class SyncManager {
const itemRef = child(this.userRef, path); const itemRef = child(this.userRef, path);
if (action === 'create' || action === 'update') { if (action === 'create' || action === 'update') {
await set(itemRef, playlist); const dataToSync = {
...playlist,
updatedAt: Date.now(),
};
await set(itemRef, this.sanitizeForFirebase(dataToSync));
// Ensure it's not in deleted_playlists (just in case) // Ensure it's not in deleted_playlists (just in case)
const deletedRef = child(this.userRef, `deleted_playlists/${id}`); const deletedRef = child(this.userRef, `deleted_playlists/${id}`);
await remove(deletedRef); await remove(deletedRef);
@ -323,7 +388,7 @@ export class SyncManager {
// Use a global 'public_playlists' node // Use a global 'public_playlists' node
const publicRef = ref(database, `public_playlists/${playlistId}`); const publicRef = ref(database, `public_playlists/${playlistId}`);
await set(publicRef, publicData); await set(publicRef, this.sanitizeForFirebase(publicData));
} }
async unpublishPlaylist(playlistId) { async unpublishPlaylist(playlistId) {
@ -352,6 +417,25 @@ export class SyncManager {
return null; return null;
} }
} }
sanitizeForFirebase(obj) {
if (obj === undefined) return null;
if (obj === null) return null;
if (typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
return obj.map((v) => this.sanitizeForFirebase(v));
}
const newObj = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const val = this.sanitizeForFirebase(obj[key]);
if (val !== undefined) {
newObj[key] = val;
}
}
}
return newObj;
}
} }
export const syncManager = new SyncManager(); export const syncManager = new SyncManager();