This commit is contained in:
peterino2 2026-02-07 00:12:18 -08:00
parent cd4237dcbe
commit 3f7bd2ec1c
3 changed files with 161 additions and 17 deletions

119
db.ts
View File

@ -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 {
const token = crypto.randomUUID().slice(0, 12);
db.query("UPDATE playlists SET share_token = ? WHERE id = ?").run(token, playlistId);

31
init.ts
View File

@ -161,19 +161,26 @@ export async function init(): Promise<void> {
const trackTitle = normalizeForMatch(track.title || "");
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}"`);
for (const [pendingTitle, pending] of pendingPlaylistTracks) {
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)
// Require exact match or very high overlap
const matches =
(trackTitle && trackTitle === normalizedPending) ||
(trackFilename && trackFilename === normalizedPending) ||
(trackTitle && normalizedPending && trackTitle.includes(normalizedPending)) ||
(trackTitle && normalizedPending && normalizedPending.includes(trackTitle)) ||
(trackFilename && normalizedPending && trackFilename.includes(normalizedPending)) ||
(trackFilename && normalizedPending && normalizedPending.includes(trackFilename));
(trackTitle && normalizedPending && trackTitle.includes(normalizedPending) && normalizedPending.length >= trackTitle.length * 0.8) ||
(trackTitle && normalizedPending && normalizedPending.includes(trackTitle) && trackTitle.length >= normalizedPending.length * 0.8) ||
(trackFilename && normalizedPending && trackFilename.includes(normalizedPending) && normalizedPending.length >= trackFilename.length * 0.8) ||
(trackFilename && normalizedPending && normalizedPending.includes(trackFilename) && trackFilename.length >= normalizedPending.length * 0.8);
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) {
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
for (const track of tracks) {
const trackTitle = normalizeForMatch(track.title || "");
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 =
(trackTitle && trackTitle === itemTitle) ||
(trackFilename && trackFilename === itemTitle) ||
(trackTitle && itemTitle && trackTitle.includes(itemTitle)) ||
(trackTitle && itemTitle && itemTitle.includes(trackTitle)) ||
(trackFilename && itemTitle && trackFilename.includes(itemTitle)) ||
(trackFilename && itemTitle && itemTitle.includes(trackFilename));
// Only allow includes if the shorter string is at least 80% of the longer
(trackTitle && itemTitle && trackTitle.includes(itemTitle) && itemTitle.length >= trackTitle.length * 0.8) ||
(trackTitle && itemTitle && itemTitle.includes(trackTitle) && trackTitle.length >= itemTitle.length * 0.8) ||
(trackFilename && itemTitle && trackFilename.includes(itemTitle) && itemTitle.length >= trackFilename.length * 0.8) ||
(trackFilename && itemTitle && itemTitle.includes(trackFilename) && trackFilename.length >= itemTitle.length * 0.8);
if (matches) {
console.log(`[ytdlp] Prescan: "${item.title}" already exists as "${track.title || track.filename}"`);

View File

@ -9,6 +9,9 @@ import {
setPlaylistTracks,
addTracksToPlaylist,
removeTrackFromPlaylist,
removeTracksFromPlaylist,
movePlaylistTracks,
insertTracksToPlaylistAt,
generatePlaylistShareToken,
removePlaylistShareToken,
findUserById,
@ -154,7 +157,7 @@ export async function handleModifyPlaylistTracks(req: Request, server: any, play
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 {
body = await req.json();
} catch {
@ -167,18 +170,25 @@ export async function handleModifyPlaylistTracks(req: Request, server: any, play
return Response.json({ ok: true });
}
// Remove tracks by position (do removes first, in reverse order)
if (body.remove?.length) {
const positions = [...body.remove].sort((a, b) => b - a);
for (const pos of positions) {
removeTrackFromPlaylist(playlistId, pos);
}
// Move/reorder tracks
if (body.move?.length && typeof body.to === "number") {
movePlaylistTracks(playlistId, body.move, body.to);
return Response.json({ ok: true });
}
// 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 (typeof body.insertAt === "number") {
insertTracksToPlaylistAt(playlistId, body.add, body.insertAt);
} else {
addTracksToPlaylist(playlistId, body.add);
}
}
return Response.json({ ok: true });
}