fix add to playlist modal not checking songs, making playlist causes big issues
This commit is contained in:
parent
0b6c1a4230
commit
de86337e3e
4 changed files with 138 additions and 38 deletions
|
|
@ -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', () => {
|
||||
|
|
|
|||
55
js/db.js
55
js/db.js
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue