Files
Video-Cutter/backend/app/db.py
T
PickleRick 4a7d1e6663 1. fix bulk edit and export Now I can only edit one video at the time overwise the timestamp get overwritten. Save timestamps for each file uploaded
2. in export bulk make clear wich files will get elaborated and whick is being elaborated also add multple progress bar one for bulk one for the actual video and one for actual process (use Material You expresssive and make them clear)
3. remove save marker button; autosave then save edit
4. add paste timestamp button on hover other timestamp input
5. make unlink button not global but add a link icon colored by couple to let understand which timestamps are linked and make the single link removed (link should work that if I edit a timestamp the next/previuos based on which are linked get automatically setted to the next/previous frame timestamp)
6. I'm unable to pit whichever timestamp I want in the timestamp table for example 00:07:23.109 (the one in the frame visualizer) get automatically changed to 00:07:23.110 making the colored border not working correctly sometimes fix both
2026-06-03 00:54:41 +02:00

388 lines
13 KiB
Python

import sqlite3
import threading
import uuid
from datetime import datetime
from .settings import DB_PATH
_DB_LOCK = threading.Lock()
DEFAULT_FFMPEG_PASS1_TEMPLATE = (
'ffmpeg -hwaccel auto -y -ss {start} -t {duration} -i {input} '
'-map_metadata -1 -map_chapters -1 -an -sn -b:v 2M '
'-x265-params no-slow-firstpass=1:pass=1:open-gop=0:keyint=60:min-keyint=60:'
'scenecut=0:vbv-maxrate=4000:vbv-bufsize=8000:stats={stats}:pools=+ '
'-tune animation -c:v libx265 -f null {null}'
)
DEFAULT_FFMPEG_PASS2_TEMPLATE = (
'ffmpeg -hwaccel auto -y -ss {start} -t {duration} -i {input} '
'-map_metadata -1 -map_chapters -1 -map 0:v:0 -map 0:a? '
'-disposition:v:0 default -b:v 2M '
'-x265-params pass=2:open-gop=0:keyint=60:min-keyint=60:scenecut=0:'
'vbv-maxrate=4000:vbv-bufsize=8000:stats={stats}:pools=+ '
'-tune animation -b:a 128k -ac:a 2 -c:v libx265 -c:a libopus -c:s subrip {output}'
)
def _now() -> str:
return datetime.utcnow().isoformat(timespec="seconds") + "Z"
def _row_to_dict(row):
if row is None:
return None
return {key: row[key] for key in row.keys()}
def get_conn() -> sqlite3.Connection:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db() -> None:
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.executescript(
"""
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
segments_count INTEGER NOT NULL,
intro_seconds REAL NOT NULL,
outro_seconds REAL NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS videos (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
filename TEXT NOT NULL,
file_path TEXT NOT NULL,
duration_seconds REAL NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS markers (
id TEXT PRIMARY KEY,
video_id TEXT NOT NULL,
position_seconds REAL NOT NULL,
source TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS segment_edits (
id TEXT PRIMARY KEY,
video_id TEXT NOT NULL,
segment_key TEXT NOT NULL,
start_seconds REAL NOT NULL,
end_seconds REAL NOT NULL,
modified_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_markers_video_id ON markers(video_id);
CREATE INDEX IF NOT EXISTS idx_segment_edits_video_id ON segment_edits(video_id);
"""
)
cur.execute("PRAGMA table_info(projects)")
columns = {row[1] for row in cur.fetchall()}
if "reencode_enabled" not in columns:
cur.execute("ALTER TABLE projects ADD COLUMN reencode_enabled INTEGER NOT NULL DEFAULT 0")
if "ffmpeg_pass1_template" not in columns:
cur.execute("ALTER TABLE projects ADD COLUMN ffmpeg_pass1_template TEXT")
if "ffmpeg_pass2_template" not in columns:
cur.execute("ALTER TABLE projects ADD COLUMN ffmpeg_pass2_template TEXT")
cur.execute("PRAGMA table_info(segment_edits)")
segment_edit_columns = {row[1] for row in cur.fetchall()}
if "modified_at" not in segment_edit_columns:
cur.execute("ALTER TABLE segment_edits ADD COLUMN modified_at TEXT")
cur.execute("UPDATE segment_edits SET modified_at = ?", (_now(),))
cur.execute(
"UPDATE projects SET ffmpeg_pass1_template = ? WHERE ffmpeg_pass1_template IS NULL",
(DEFAULT_FFMPEG_PASS1_TEMPLATE,),
)
cur.execute(
"UPDATE projects SET ffmpeg_pass2_template = ? WHERE ffmpeg_pass2_template IS NULL",
(DEFAULT_FFMPEG_PASS2_TEMPLATE,),
)
conn.commit()
conn.close()
def _normalize_project(project: dict | None) -> dict | None:
if not project:
return None
project["reencode_enabled"] = bool(project.get("reencode_enabled", 0))
project["ffmpeg_pass1_template"] = project.get("ffmpeg_pass1_template") or DEFAULT_FFMPEG_PASS1_TEMPLATE
project["ffmpeg_pass2_template"] = project.get("ffmpeg_pass2_template") or DEFAULT_FFMPEG_PASS2_TEMPLATE
return project
def create_project(
name: str,
segments_count: int,
intro_seconds: float,
outro_seconds: float,
reencode_enabled: bool = False,
ffmpeg_pass1_template: str | None = None,
ffmpeg_pass2_template: str | None = None,
) -> dict:
project_id = uuid.uuid4().hex
now = _now()
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute(
"""
INSERT INTO projects (
id, name, segments_count, intro_seconds, outro_seconds,
reencode_enabled, ffmpeg_pass1_template, ffmpeg_pass2_template,
created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
project_id,
name,
segments_count,
intro_seconds,
outro_seconds,
1 if reencode_enabled else 0,
ffmpeg_pass1_template or DEFAULT_FFMPEG_PASS1_TEMPLATE,
ffmpeg_pass2_template or DEFAULT_FFMPEG_PASS2_TEMPLATE,
now,
now,
),
)
cur.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('current_project_id', ?)",
(project_id,),
)
conn.commit()
cur.execute("SELECT * FROM projects WHERE id = ?", (project_id,))
project = _normalize_project(_row_to_dict(cur.fetchone()))
conn.close()
return project
def get_project(project_id: str) -> dict | None:
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT * FROM projects WHERE id = ?", (project_id,))
project = _normalize_project(_row_to_dict(cur.fetchone()))
conn.close()
return project
def get_current_project() -> dict | None:
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT value FROM settings WHERE key = 'current_project_id'")
row = cur.fetchone()
if not row:
conn.close()
return None
project_id = row[0]
cur.execute("SELECT * FROM projects WHERE id = ?", (project_id,))
project = _normalize_project(_row_to_dict(cur.fetchone()))
conn.close()
return project
def update_current_project(
name: str | None = None,
segments_count: int | None = None,
intro_seconds: float | None = None,
outro_seconds: float | None = None,
reencode_enabled: bool | None = None,
ffmpeg_pass1_template: str | None = None,
ffmpeg_pass2_template: str | None = None,
) -> dict | None:
project = get_current_project()
if not project:
return None
fields = []
params = []
if name is not None:
fields.append("name = ?")
params.append(name)
if segments_count is not None:
fields.append("segments_count = ?")
params.append(segments_count)
if intro_seconds is not None:
fields.append("intro_seconds = ?")
params.append(intro_seconds)
if outro_seconds is not None:
fields.append("outro_seconds = ?")
params.append(outro_seconds)
if reencode_enabled is not None:
fields.append("reencode_enabled = ?")
params.append(1 if reencode_enabled else 0)
if ffmpeg_pass1_template is not None:
fields.append("ffmpeg_pass1_template = ?")
params.append(ffmpeg_pass1_template or DEFAULT_FFMPEG_PASS1_TEMPLATE)
if ffmpeg_pass2_template is not None:
fields.append("ffmpeg_pass2_template = ?")
params.append(ffmpeg_pass2_template or DEFAULT_FFMPEG_PASS2_TEMPLATE)
if not fields:
return project
fields.append("updated_at = ?")
params.append(_now())
params.append(project["id"])
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute(f"UPDATE projects SET {', '.join(fields)} WHERE id = ?", params)
conn.commit()
cur.execute("SELECT * FROM projects WHERE id = ?", (project["id"],))
updated = _normalize_project(_row_to_dict(cur.fetchone()))
conn.close()
return updated
def create_video(project_id: str, filename: str, file_path: str, duration_seconds: float) -> dict:
video_id = uuid.uuid4().hex
now = _now()
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute(
"""
INSERT INTO videos (id, project_id, filename, file_path, duration_seconds, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(video_id, project_id, filename, file_path, duration_seconds, now),
)
conn.commit()
cur.execute("SELECT * FROM videos WHERE id = ?", (video_id,))
video = _row_to_dict(cur.fetchone())
conn.close()
return video
def get_video(video_id: str) -> dict | None:
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT * FROM videos WHERE id = ?", (video_id,))
video = _row_to_dict(cur.fetchone())
conn.close()
return video
def list_videos(project_id: str) -> list[dict]:
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute(
"SELECT * FROM videos WHERE project_id = ? ORDER BY created_at DESC",
(project_id,),
)
rows = cur.fetchall()
conn.close()
return [_row_to_dict(row) for row in rows]
def list_markers(video_id: str) -> list[float]:
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute(
"SELECT position_seconds FROM markers WHERE video_id = ? ORDER BY position_seconds",
(video_id,),
)
rows = cur.fetchall()
conn.close()
return [row[0] for row in rows]
def replace_markers(video_id: str, markers: list[float], source: str) -> list[float]:
now = _now()
markers = [float(m) for m in markers]
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute("DELETE FROM markers WHERE video_id = ?", (video_id,))
for position in markers:
cur.execute(
"""
INSERT INTO markers (id, video_id, position_seconds, source, created_at)
VALUES (?, ?, ?, ?, ?)
""",
(uuid.uuid4().hex, video_id, position, source, now),
)
conn.commit()
conn.close()
return sorted(markers)
def list_segment_edits(video_id: str) -> list[dict]:
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute(
"SELECT * FROM segment_edits WHERE video_id = ? ORDER BY segment_key",
(video_id,),
)
rows = cur.fetchall()
conn.close()
return [_row_to_dict(row) for row in rows]
def replace_segment_edits(video_id: str, segments: list[dict]) -> list[dict]:
now = _now()
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute("DELETE FROM segment_edits WHERE video_id = ?", (video_id,))
for segment in segments:
cur.execute(
"""
INSERT INTO segment_edits (
id, video_id, segment_key, start_seconds, end_seconds, modified_at
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
uuid.uuid4().hex,
video_id,
segment["segment_key"],
float(segment["start_seconds"]),
float(segment["end_seconds"]),
now,
),
)
conn.commit()
cur.execute(
"""
SELECT segment_key, start_seconds, end_seconds, modified_at
FROM segment_edits
WHERE video_id = ?
ORDER BY rowid
""",
(video_id,),
)
rows = cur.fetchall()
conn.close()
return [_row_to_dict(row) for row in rows]
def delete_video(video_id: str) -> dict | None:
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT * FROM videos WHERE id = ?", (video_id,))
video = _row_to_dict(cur.fetchone())
if not video:
conn.close()
return None
cur.execute("DELETE FROM markers WHERE video_id = ?", (video_id,))
cur.execute("DELETE FROM segment_edits WHERE video_id = ?", (video_id,))
cur.execute("DELETE FROM videos WHERE id = ?", (video_id,))
conn.commit()
conn.close()
return video