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();
} else if (hash === '#home' || hash === '') {
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', () => {

View file

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

View file

@ -659,7 +659,7 @@ export async function handleTrackAction(
}
}
} else if (action === 'add-to-playlist') {
const playlists = await db.getPlaylists();
const playlists = await db.getPlaylists(true);
if (playlists.length === 0) {
showNotification('No playlists yet. Create one first.');
return;
@ -675,7 +675,7 @@ export async function handleTrackAction(
const playlistsWithTrack = new Set();
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);
}
}

View file

@ -10,6 +10,9 @@ import {
child,
remove,
runTransaction,
onChildAdded,
onChildChanged,
onChildRemoved,
} from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-database.js';
import { db } from '../db.js';
@ -19,6 +22,7 @@ export class SyncManager {
this.userRef = null;
this.unsubscribeFunctions = [];
this.isSyncing = false;
this.listenersSetup = false;
}
initialize(user) {
@ -38,6 +42,7 @@ export class SyncManager {
}
this.user = null;
this.userRef = null;
this.listenersSetup = false;
console.log('SyncManager disconnected');
}
@ -83,12 +88,10 @@ export class SyncManager {
await db.importData(importData, true);
console.log('Initial sync complete.');
// 6. Setup Listeners for future changes
this.setupListeners();
} catch (error) {
console.error('Initial sync failed:', error);
} finally {
this.setupListeners();
this.isSyncing = false;
}
}
@ -110,13 +113,48 @@ export class SyncManager {
// Add/Overwrite with cloud items (Union Strategy)
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)) {
cloudItems.forEach((item) => map.set(item[idKey], item));
cloudItems.forEach((item) => processItem(item));
} else {
Object.keys(cloudItems).forEach((key) => {
const val = cloudItems[key];
if (typeof val === 'object') {
map.set(val[idKey] || key, val);
processItem(val, key);
}
});
}
@ -162,6 +200,9 @@ export class SyncManager {
}
setupListeners() {
if (!this.userRef || this.listenersSetup) return;
this.listenersSetup = true;
// Listen for changes in library
const libraryRef = child(this.userRef, 'library');
@ -208,22 +249,42 @@ export class SyncManager {
// Listen for changes in user playlists
const userPlaylistsRef = child(this.userRef, 'user_playlists');
const unsubUserPlaylists = onValue(userPlaylistsRef, (snapshot) => {
const handlePlaylistUpdate = (snapshot) => {
if (this.isSyncing) return;
const val = snapshot.val();
if (val) {
if (val.tracks && typeof val.tracks === 'object' && !Array.isArray(val.tracks)) {
val.tracks = Object.values(val.tracks);
}
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
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 ---
@ -258,7 +319,7 @@ export class SyncManager {
...minified,
addedAt: item.addedAt || minified.addedAt || Date.now(),
};
await set(itemRef, entry);
await set(itemRef, this.sanitizeForFirebase(entry));
} else {
await remove(itemRef);
}
@ -269,7 +330,7 @@ export class SyncManager {
const itemRef = child(this.userRef, `history/recentTracks/${track.timestamp}`);
try {
await set(itemRef, track);
await set(itemRef, this.sanitizeForFirebase(track));
} catch (error) {
console.error('Failed to sync history item:', error);
}
@ -283,7 +344,11 @@ export class SyncManager {
const itemRef = child(this.userRef, path);
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)
const deletedRef = child(this.userRef, `deleted_playlists/${id}`);
await remove(deletedRef);
@ -323,7 +388,7 @@ export class SyncManager {
// Use a global 'public_playlists' node
const publicRef = ref(database, `public_playlists/${playlistId}`);
await set(publicRef, publicData);
await set(publicRef, this.sanitizeForFirebase(publicData));
}
async unpublishPlaylist(playlistId) {
@ -352,6 +417,25 @@ export class SyncManager {
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();