mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Migration: probe video durations with ffprobe during assignment-to-playlist conversion
Videos were getting the default 10s from the assignments table. Now ffprobe runs for each video content item during migration, backfills the content table, and uses the real duration in playlist_items. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aca1558702
commit
aee6766c4c
|
|
@ -115,6 +115,30 @@ function migrateAssignmentsToPlaylists() {
|
||||||
|
|
||||||
console.log(`Migrating ${devicesWithAssignments.length} device(s) from assignments to playlists...`);
|
console.log(`Migrating ${devicesWithAssignments.length} device(s) from assignments to playlists...`);
|
||||||
|
|
||||||
|
// Probe video duration with ffprobe (synchronous — fine for one-time migration)
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
function probeVideoDuration(content) {
|
||||||
|
if (!content || !content.mime_type || !content.mime_type.startsWith('video/')) return null;
|
||||||
|
if (content.duration_sec) return Math.ceil(content.duration_sec);
|
||||||
|
if (!content.filepath) return null;
|
||||||
|
try {
|
||||||
|
const fullPath = path.join(config.contentDir, content.filepath);
|
||||||
|
const stdout = execFileSync('ffprobe', [
|
||||||
|
'-v', 'quiet', '-print_format', 'json', '-show_format', fullPath
|
||||||
|
], { timeout: 15000 });
|
||||||
|
const info = JSON.parse(stdout);
|
||||||
|
if (info.format?.duration) {
|
||||||
|
const dur = parseFloat(info.format.duration);
|
||||||
|
// Backfill the content table too
|
||||||
|
db.prepare('UPDATE content SET duration_sec = ? WHERE id = ?').run(dur, content.id);
|
||||||
|
return Math.ceil(dur);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(` ffprobe failed for ${content.id}:`, e.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const insertPlaylist = db.prepare(`
|
const insertPlaylist = db.prepare(`
|
||||||
INSERT INTO playlists (id, user_id, name, description, is_auto_generated)
|
INSERT INTO playlists (id, user_id, name, description, is_auto_generated)
|
||||||
VALUES (?, ?, ?, ?, 1)
|
VALUES (?, ?, ?, ?, 1)
|
||||||
|
|
@ -125,22 +149,37 @@ function migrateAssignmentsToPlaylists() {
|
||||||
`);
|
`);
|
||||||
const setDevicePlaylist = db.prepare('UPDATE devices SET playlist_id = ? WHERE id = ?');
|
const setDevicePlaylist = db.prepare('UPDATE devices SET playlist_id = ? WHERE id = ?');
|
||||||
const getAssignments = db.prepare(`
|
const getAssignments = db.prepare(`
|
||||||
SELECT content_id, widget_id, sort_order, duration_sec
|
SELECT a.content_id, a.widget_id, a.sort_order, a.duration_sec,
|
||||||
FROM assignments
|
c.mime_type, c.filepath, c.duration_sec as content_duration
|
||||||
WHERE device_id = ? AND enabled = 1
|
FROM assignments a
|
||||||
ORDER BY sort_order ASC
|
LEFT JOIN content c ON a.content_id = c.id
|
||||||
|
WHERE a.device_id = ? AND a.enabled = 1
|
||||||
|
ORDER BY a.sort_order ASC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const migrate = db.transaction(() => {
|
// Probe durations outside the transaction (ffprobe can't run inside SQLite transaction)
|
||||||
for (const device of devicesWithAssignments) {
|
const devicePlaylists = [];
|
||||||
const playlistId = uuidv4();
|
for (const device of devicesWithAssignments) {
|
||||||
insertPlaylist.run(playlistId, device.user_id, `${device.name} (migrated)`, 'Auto-generated from previous assignments');
|
const playlistId = uuidv4();
|
||||||
|
const assignments = getAssignments.all(device.id);
|
||||||
const assignments = getAssignments.all(device.id);
|
const items = assignments.map(a => {
|
||||||
for (const a of assignments) {
|
let duration = a.duration_sec;
|
||||||
insertItem.run(playlistId, a.content_id || null, a.widget_id || null, a.sort_order, a.duration_sec);
|
if (a.content_id && a.mime_type?.startsWith('video/')) {
|
||||||
|
const probed = probeVideoDuration({ id: a.content_id, mime_type: a.mime_type, filepath: a.filepath, duration_sec: a.content_duration });
|
||||||
|
if (probed) duration = probed;
|
||||||
}
|
}
|
||||||
|
return { content_id: a.content_id, widget_id: a.widget_id, sort_order: a.sort_order, duration_sec: duration };
|
||||||
|
});
|
||||||
|
devicePlaylists.push({ device, playlistId, items });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert everything in a single transaction
|
||||||
|
const migrate = db.transaction(() => {
|
||||||
|
for (const { device, playlistId, items } of devicePlaylists) {
|
||||||
|
insertPlaylist.run(playlistId, device.user_id, `${device.name} (migrated)`, 'Auto-generated from previous assignments');
|
||||||
|
for (const item of items) {
|
||||||
|
insertItem.run(playlistId, item.content_id || null, item.widget_id || null, item.sort_order, item.duration_sec);
|
||||||
|
}
|
||||||
setDevicePlaylist.run(playlistId, device.id);
|
setDevicePlaylist.run(playlistId, device.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue