saving
This commit is contained in:
parent
cd4237dcbe
commit
3f7bd2ec1c
119
db.ts
119
db.ts
|
|
@ -685,6 +685,125 @@ export function removeTrackFromPlaylist(playlistId: string, position: number): v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeTracksFromPlaylist(playlistId: string, positions: number[]): void {
|
||||||
|
if (positions.length === 0) return;
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
// Sort descending to remove from end first (preserves indices)
|
||||||
|
const sorted = [...positions].sort((a, b) => b - a);
|
||||||
|
|
||||||
|
db.query("BEGIN").run();
|
||||||
|
try {
|
||||||
|
for (const pos of sorted) {
|
||||||
|
db.query("DELETE FROM playlist_tracks WHERE playlist_id = ? AND position = ?").run(playlistId, pos);
|
||||||
|
// Reorder remaining tracks
|
||||||
|
db.query(`
|
||||||
|
UPDATE playlist_tracks
|
||||||
|
SET position = position - 1
|
||||||
|
WHERE playlist_id = ? AND position > ?
|
||||||
|
`).run(playlistId, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.query("UPDATE playlists SET updated_at = ? WHERE id = ?").run(now, playlistId);
|
||||||
|
db.query("COMMIT").run();
|
||||||
|
} catch (e) {
|
||||||
|
db.query("ROLLBACK").run();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function movePlaylistTracks(playlistId: string, fromPositions: number[], toPosition: number): void {
|
||||||
|
if (fromPositions.length === 0) return;
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const sorted = [...fromPositions].sort((a, b) => a - b);
|
||||||
|
|
||||||
|
db.query("BEGIN").run();
|
||||||
|
try {
|
||||||
|
// Get tracks to move
|
||||||
|
const tracksToMove: string[] = [];
|
||||||
|
for (const pos of sorted) {
|
||||||
|
const row = db.query(
|
||||||
|
"SELECT track_id FROM playlist_tracks WHERE playlist_id = ? AND position = ?"
|
||||||
|
).get(playlistId, pos) as { track_id: string } | null;
|
||||||
|
if (row) tracksToMove.push(row.track_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracksToMove.length === 0) {
|
||||||
|
db.query("ROLLBACK").run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove tracks from current positions (from end to preserve indices)
|
||||||
|
for (let i = sorted.length - 1; i >= 0; i--) {
|
||||||
|
const pos = sorted[i];
|
||||||
|
db.query("DELETE FROM playlist_tracks WHERE playlist_id = ? AND position = ?").run(playlistId, pos);
|
||||||
|
db.query(`
|
||||||
|
UPDATE playlist_tracks
|
||||||
|
SET position = position - 1
|
||||||
|
WHERE playlist_id = ? AND position > ?
|
||||||
|
`).run(playlistId, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust target for removed items
|
||||||
|
let adjustedTarget = toPosition;
|
||||||
|
for (const pos of sorted) {
|
||||||
|
if (pos < toPosition) adjustedTarget--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make room at target position
|
||||||
|
db.query(`
|
||||||
|
UPDATE playlist_tracks
|
||||||
|
SET position = position + ?
|
||||||
|
WHERE playlist_id = ? AND position >= ?
|
||||||
|
`).run(tracksToMove.length, playlistId, adjustedTarget);
|
||||||
|
|
||||||
|
// Insert tracks at new position
|
||||||
|
const insert = db.query(
|
||||||
|
"INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES (?, ?, ?)"
|
||||||
|
);
|
||||||
|
for (let i = 0; i < tracksToMove.length; i++) {
|
||||||
|
insert.run(playlistId, tracksToMove[i], adjustedTarget + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.query("UPDATE playlists SET updated_at = ? WHERE id = ?").run(now, playlistId);
|
||||||
|
db.query("COMMIT").run();
|
||||||
|
} catch (e) {
|
||||||
|
db.query("ROLLBACK").run();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertTracksToPlaylistAt(playlistId: string, trackIds: string[], position: number): void {
|
||||||
|
if (trackIds.length === 0) return;
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
db.query("BEGIN").run();
|
||||||
|
try {
|
||||||
|
// Make room at position
|
||||||
|
db.query(`
|
||||||
|
UPDATE playlist_tracks
|
||||||
|
SET position = position + ?
|
||||||
|
WHERE playlist_id = ? AND position >= ?
|
||||||
|
`).run(trackIds.length, playlistId, position);
|
||||||
|
|
||||||
|
// Insert tracks
|
||||||
|
const insert = db.query(
|
||||||
|
"INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES (?, ?, ?)"
|
||||||
|
);
|
||||||
|
for (let i = 0; i < trackIds.length; i++) {
|
||||||
|
insert.run(playlistId, trackIds[i], position + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.query("UPDATE playlists SET updated_at = ? WHERE id = ?").run(now, playlistId);
|
||||||
|
db.query("COMMIT").run();
|
||||||
|
} catch (e) {
|
||||||
|
db.query("ROLLBACK").run();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function generatePlaylistShareToken(playlistId: string): string {
|
export function generatePlaylistShareToken(playlistId: string): string {
|
||||||
const token = crypto.randomUUID().slice(0, 12);
|
const token = crypto.randomUUID().slice(0, 12);
|
||||||
db.query("UPDATE playlists SET share_token = ? WHERE id = ?").run(token, playlistId);
|
db.query("UPDATE playlists SET share_token = ? WHERE id = ?").run(token, playlistId);
|
||||||
|
|
|
||||||
31
init.ts
31
init.ts
|
|
@ -161,19 +161,26 @@ export async function init(): Promise<void> {
|
||||||
const trackTitle = normalizeForMatch(track.title || "");
|
const trackTitle = normalizeForMatch(track.title || "");
|
||||||
const trackFilename = normalizeForMatch((track.filename || "").replace(/\.[^.]+$/, "")); // Remove extension
|
const trackFilename = normalizeForMatch((track.filename || "").replace(/\.[^.]+$/, "")); // Remove extension
|
||||||
|
|
||||||
|
// Skip if both title and filename are too short
|
||||||
|
if ((!trackTitle || trackTitle.length < 5) && (!trackFilename || trackFilename.length < 5)) return;
|
||||||
|
|
||||||
console.log(`[ytdlp] Checking track against ${pendingPlaylistTracks.size} pending: title="${trackTitle}" filename="${trackFilename}"`);
|
console.log(`[ytdlp] Checking track against ${pendingPlaylistTracks.size} pending: title="${trackTitle}" filename="${trackFilename}"`);
|
||||||
|
|
||||||
for (const [pendingTitle, pending] of pendingPlaylistTracks) {
|
for (const [pendingTitle, pending] of pendingPlaylistTracks) {
|
||||||
const normalizedPending = normalizeForMatch(pendingTitle);
|
const normalizedPending = normalizeForMatch(pendingTitle);
|
||||||
|
|
||||||
|
// Skip if pending title is too short
|
||||||
|
if (!normalizedPending || normalizedPending.length < 5) continue;
|
||||||
|
|
||||||
// Match by title or filename (yt-dlp uses title as filename)
|
// Match by title or filename (yt-dlp uses title as filename)
|
||||||
|
// Require exact match or very high overlap
|
||||||
const matches =
|
const matches =
|
||||||
(trackTitle && trackTitle === normalizedPending) ||
|
(trackTitle && trackTitle === normalizedPending) ||
|
||||||
(trackFilename && trackFilename === normalizedPending) ||
|
(trackFilename && trackFilename === normalizedPending) ||
|
||||||
(trackTitle && normalizedPending && trackTitle.includes(normalizedPending)) ||
|
(trackTitle && normalizedPending && trackTitle.includes(normalizedPending) && normalizedPending.length >= trackTitle.length * 0.8) ||
|
||||||
(trackTitle && normalizedPending && normalizedPending.includes(trackTitle)) ||
|
(trackTitle && normalizedPending && normalizedPending.includes(trackTitle) && trackTitle.length >= normalizedPending.length * 0.8) ||
|
||||||
(trackFilename && normalizedPending && trackFilename.includes(normalizedPending)) ||
|
(trackFilename && normalizedPending && trackFilename.includes(normalizedPending) && normalizedPending.length >= trackFilename.length * 0.8) ||
|
||||||
(trackFilename && normalizedPending && normalizedPending.includes(trackFilename));
|
(trackFilename && normalizedPending && normalizedPending.includes(trackFilename) && trackFilename.length >= normalizedPending.length * 0.8);
|
||||||
|
|
||||||
console.log(`[ytdlp] vs pending="${normalizedPending}" → ${matches ? "MATCH" : "no match"}`);
|
console.log(`[ytdlp] vs pending="${normalizedPending}" → ${matches ? "MATCH" : "no match"}`);
|
||||||
|
|
||||||
|
|
@ -219,18 +226,26 @@ export async function init(): Promise<void> {
|
||||||
for (const item of queuedItems) {
|
for (const item of queuedItems) {
|
||||||
const itemTitle = normalizeForMatch(item.title);
|
const itemTitle = normalizeForMatch(item.title);
|
||||||
|
|
||||||
|
// Skip if title is too short (avoid false matches)
|
||||||
|
if (!itemTitle || itemTitle.length < 5) continue;
|
||||||
|
|
||||||
// Check if any library track matches
|
// Check if any library track matches
|
||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
const trackTitle = normalizeForMatch(track.title || "");
|
const trackTitle = normalizeForMatch(track.title || "");
|
||||||
const trackFilename = normalizeForMatch((track.filename || "").replace(/\.[^.]+$/, ""));
|
const trackFilename = normalizeForMatch((track.filename || "").replace(/\.[^.]+$/, ""));
|
||||||
|
|
||||||
|
// Skip if both track title and filename are too short
|
||||||
|
if ((!trackTitle || trackTitle.length < 5) && (!trackFilename || trackFilename.length < 5)) continue;
|
||||||
|
|
||||||
|
// Require exact match or very high overlap (not just substring)
|
||||||
const matches =
|
const matches =
|
||||||
(trackTitle && trackTitle === itemTitle) ||
|
(trackTitle && trackTitle === itemTitle) ||
|
||||||
(trackFilename && trackFilename === itemTitle) ||
|
(trackFilename && trackFilename === itemTitle) ||
|
||||||
(trackTitle && itemTitle && trackTitle.includes(itemTitle)) ||
|
// Only allow includes if the shorter string is at least 80% of the longer
|
||||||
(trackTitle && itemTitle && itemTitle.includes(trackTitle)) ||
|
(trackTitle && itemTitle && trackTitle.includes(itemTitle) && itemTitle.length >= trackTitle.length * 0.8) ||
|
||||||
(trackFilename && itemTitle && trackFilename.includes(itemTitle)) ||
|
(trackTitle && itemTitle && itemTitle.includes(trackTitle) && trackTitle.length >= itemTitle.length * 0.8) ||
|
||||||
(trackFilename && itemTitle && itemTitle.includes(trackFilename));
|
(trackFilename && itemTitle && trackFilename.includes(itemTitle) && itemTitle.length >= trackFilename.length * 0.8) ||
|
||||||
|
(trackFilename && itemTitle && itemTitle.includes(trackFilename) && trackFilename.length >= itemTitle.length * 0.8);
|
||||||
|
|
||||||
if (matches) {
|
if (matches) {
|
||||||
console.log(`[ytdlp] Prescan: "${item.title}" already exists as "${track.title || track.filename}"`);
|
console.log(`[ytdlp] Prescan: "${item.title}" already exists as "${track.title || track.filename}"`);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import {
|
||||||
setPlaylistTracks,
|
setPlaylistTracks,
|
||||||
addTracksToPlaylist,
|
addTracksToPlaylist,
|
||||||
removeTrackFromPlaylist,
|
removeTrackFromPlaylist,
|
||||||
|
removeTracksFromPlaylist,
|
||||||
|
movePlaylistTracks,
|
||||||
|
insertTracksToPlaylistAt,
|
||||||
generatePlaylistShareToken,
|
generatePlaylistShareToken,
|
||||||
removePlaylistShareToken,
|
removePlaylistShareToken,
|
||||||
findUserById,
|
findUserById,
|
||||||
|
|
@ -154,7 +157,7 @@ export async function handleModifyPlaylistTracks(req: Request, server: any, play
|
||||||
return Response.json({ error: "Not your playlist" }, { status: 403 });
|
return Response.json({ error: "Not your playlist" }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let body: { add?: string[]; remove?: number[]; set?: string[] };
|
let body: { add?: string[]; remove?: number[]; set?: string[]; move?: number[]; to?: number; insertAt?: number };
|
||||||
try {
|
try {
|
||||||
body = await req.json();
|
body = await req.json();
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -167,18 +170,25 @@ export async function handleModifyPlaylistTracks(req: Request, server: any, play
|
||||||
return Response.json({ ok: true });
|
return Response.json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove tracks by position (do removes first, in reverse order)
|
// Move/reorder tracks
|
||||||
if (body.remove?.length) {
|
if (body.move?.length && typeof body.to === "number") {
|
||||||
const positions = [...body.remove].sort((a, b) => b - a);
|
movePlaylistTracks(playlistId, body.move, body.to);
|
||||||
for (const pos of positions) {
|
return Response.json({ ok: true });
|
||||||
removeTrackFromPlaylist(playlistId, pos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tracks
|
// Remove tracks by position (bulk operation)
|
||||||
|
if (body.remove?.length) {
|
||||||
|
removeTracksFromPlaylist(playlistId, body.remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tracks (at specific position or at end)
|
||||||
if (body.add?.length) {
|
if (body.add?.length) {
|
||||||
|
if (typeof body.insertAt === "number") {
|
||||||
|
insertTracksToPlaylistAt(playlistId, body.add, body.insertAt);
|
||||||
|
} else {
|
||||||
addTracksToPlaylist(playlistId, body.add);
|
addTracksToPlaylist(playlistId, body.add);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Response.json({ ok: true });
|
return Response.json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue