moments update

main
oscar 2 months ago
parent 4596d9168f
commit 0372886d88

@ -0,0 +1,24 @@
# helpers/db_moments.py
from typing import List, Dict
from config import get_local_db_connection
def add_moment(video_uuid: str, ts_seconds: int) -> str:
conn, cursor = get_local_db_connection()
cursor.execute(
"INSERT INTO moments (video_id, timestamp) VALUES (%s, %s)",
(video_uuid, ts_seconds),
)
conn.commit()
new_id = cursor.lastrowid
cursor.close(); conn.close()
return new_id
def list_moments(video_uuid: str):
conn, cursor = get_local_db_connection()
cursor.execute(
"SELECT id, timestamp FROM moments WHERE video_id = %s ORDER BY timestamp ASC",
(video_uuid,),
)
rows = cursor.fetchall()
cursor.close(); conn.close()
return rows

@ -2,6 +2,7 @@ from flask import Blueprint, request, jsonify
import os, subprocess
from helpers.db import db_get_video
from helpers.favorites import db_toggle_fav, db_get_fav_set
from helpers.db_moments import add_moment, list_moments
from config import get_local_db_connection
api = Blueprint("api", __name__)
@ -40,20 +41,17 @@ def api_fav_list():
@api.route('/delete-file', methods=['POST'])
def delete_file():
data = request.json
file_path = data.get("file_path")
data = request.json or {}
file_path = os.path.abspath(data.get("file_path",""))
video_id = data.get("video_id")
if not file_path or not os.path.exists(file_path):
return jsonify({"error": "File not found"}), 404
try:
# delete from filesystem
os.remove(file_path)
# optional: mark video as missing in DB
conn, cur = get_local_db_connection()
cur = conn.cursor()
cur.execute("UPDATE videos SET status = 'missing' WHERE video_id = %s", (video_id,))
conn.commit()
cur.close(); conn.close()
@ -61,3 +59,22 @@ def delete_file():
return jsonify({"success": True})
except Exception as e:
return jsonify({"error": str(e)}), 500
@api.route("/api/moments/<video_id>", methods=["GET"])
def api_list_moments(video_id):
moments = list_moments(video_id)
return jsonify(ok=True, moments=moments)
@api.route("/api/moments/<video_id>", methods=["POST"])
def api_add_moment(video_id):
data = request.get_json(force=True, silent=True) or {}
try:
ts = int(data.get("timestamp", 0))
except (TypeError, ValueError):
return jsonify(ok=False, error="Invalid timestamp"), 400
if ts < 0:
return jsonify(ok=False, error="Negative timestamp"), 400
new_id = add_moment(video_id, ts)
return jsonify(ok=True, id=new_id, timestamp=ts)

@ -60,6 +60,87 @@
}
</style>
<style>
.player {
margin: 20px auto;
width: 80%;
max-width: 1000px;
background: #000;
border: 2px solid #444;
border-radius: 8px;
overflow: hidden;
}
.player video {
width: 100%;
display: block;
}
.controls {
display: flex;
align-items: center;
gap: 10px;
background: #1b1b1b;
padding: 10px;
border-top: 1px solid #333;
}
.controls button,
.controls select,
.controls input[type="range"] {
background: #222;
color: #eee;
border: 1px solid #444;
border-radius: 4px;
padding: 6px 8px;
cursor: pointer;
}
.controls button:hover {
background: #2a2a2a;
}
.time {
font-variant-numeric: tabular-nums;
min-width: 90px;
text-align: right;
}
.range {
flex: 1;
}
.moments {
margin: 20px auto;
text-align: left;
max-width: 800px;
background: #222;
padding: 12px;
border-radius: 6px;
}
.moments h3 {
margin: 0 0 8px 0;
}
.moment-pill {
display: inline-block;
margin: 6px 6px 0 0;
padding: 6px 10px;
background: #0a2740;
color: #cfeaff;
border: 1px solid #0d3b63;
border-radius: 999px;
cursor: pointer;
font-size: 0.95rem;
}
.moment-pill:hover {
background: #103553;
}
</style>
<script>
function openFolder(filePath) {
fetch('/open-folder', {
@ -101,8 +182,6 @@
<script>
// existing openFolder(...) here
document.addEventListener('click', async (e) => {
const btn = e.target.closest('#favBtn');
if (!btn) return;
@ -131,10 +210,37 @@
<div>
<a href="/user/{{ video.username }}">⬅ Back to {{ video.username }}'s Videos</a>
</div>
<video controls muted>
<source src="/video/stream/{{ video.video_id }}" type="video/mp4" poster="{{ video.thumbnail }}">
Your browser does not support the video tag.
</video>
<div class="player">
<video id="player" muted poster="{{ video.thumbnail }}">
<source src="/video/stream/{{ video.video_id }}" type="video/mp4">
Your browser does not support the video tag.
</video>
<div class="controls">
<button id="playPause">▶️</button>
<span class="time" id="currentTime">00:00</span>
<input id="seek" class="range" type="range" min="0" max="0" value="0" step="1">
<span class="time" id="duration">00:00</span>
<input id="volume" type="range" min="0" max="1" step="0.01" value="1" title="Volume">
<select id="speed" title="Speed">
<option value="0.5">0.5×</option>
<option value="0.75">0.75×</option>
<option value="1" selected>1×</option>
<option value="1.25">1.25×</option>
<option value="1.5">1.5×</option>
<option value="2">2×</option>
</select>
<button id="addMoment" title="Add moment"> Add Moment</button>
</div>
</div>
<div class="moments">
<h3>Moments</h3>
<div id="momentsList"></div>
</div>
<div class="meta">
<button onclick="openFolder('{{ video.filepath }}')">Open Folder</button>
@ -158,6 +264,122 @@
<p><strong>Path:</strong> {{ video.filepath }}</p>
<p><strong>Date:</strong> {{ video.created_at }}</p>
</div>
<script>
(function () {
const video = document.getElementById('player');
const playPause = document.getElementById('playPause');
const seek = document.getElementById('seek');
const vol = document.getElementById('volume');
const speed = document.getElementById('speed');
const cur = document.getElementById('currentTime');
const dur = document.getElementById('duration');
const addBtn = document.getElementById('addMoment');
const list = document.getElementById('momentsList');
const videoId = "{{ video.video_id }}";
// format seconds → hh:mm:ss
function fmt(t) {
t = Math.max(0, Math.floor(t || 0));
const h = Math.floor(t / 3600);
const m = Math.floor((t % 3600) / 60);
const s = t % 60;
return (h > 0 ? String(h).padStart(2, '0') + ':' : '') +
String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
// load existing moments
async function loadMoments() {
list.innerHTML = '';
try {
const res = await fetch(`/api/moments/${encodeURIComponent(videoId)}`);
const data = await res.json();
if (!data.ok) throw new Error(data.error || 'Failed');
for (const m of data.moments) {
const pill = document.createElement('button');
pill.className = 'moment-pill';
pill.textContent = fmt(m.timestamp);
pill.dataset.ts = m.timestamp;
pill.addEventListener('click', () => {
video.currentTime = Number(pill.dataset.ts);
video.play();
playPause.textContent = '⏸';
});
list.appendChild(pill);
}
if (list.children.length === 0) {
list.textContent = 'No moments yet.';
}
} catch (e) {
list.textContent = 'Could not load moments.';
console.error(e);
}
}
// add new moment at current time
async function addMoment() {
const ts = Math.floor(video.currentTime || 0);
try {
const res = await fetch(`/api/moments/${encodeURIComponent(videoId)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timestamp: ts })
});
const data = await res.json();
if (!data.ok) throw new Error(data.error || 'Failed');
// append immediately
const pill = document.createElement('button');
pill.className = 'moment-pill';
pill.textContent = fmt(ts);
pill.dataset.ts = ts;
pill.addEventListener('click', () => {
video.currentTime = ts;
video.play();
playPause.textContent = '⏸';
});
if (list.textContent === 'No moments yet.') list.textContent = '';
list.appendChild(pill);
} catch (e) {
alert('Could not add moment: ' + (e?.message || ''));
}
}
// player controls
playPause.addEventListener('click', () => {
if (video.paused) { video.play(); playPause.textContent = '⏸'; }
else { video.pause(); playPause.textContent = '▶️'; }
});
video.addEventListener('play', () => playPause.textContent = '⏸');
video.addEventListener('pause', () => playPause.textContent = '▶️');
video.addEventListener('loadedmetadata', () => {
seek.max = Math.floor(video.duration || 0);
dur.textContent = fmt(video.duration);
});
video.addEventListener('timeupdate', () => {
cur.textContent = fmt(video.currentTime);
if (!seek.matches(':active')) {
seek.value = Math.floor(video.currentTime || 0);
}
});
seek.addEventListener('input', () => {
video.currentTime = Number(seek.value || 0);
});
vol.addEventListener('input', () => video.volume = Number(vol.value));
speed.addEventListener('change', () => video.playbackRate = Number(speed.value));
addBtn.addEventListener('click', addMoment);
// kick off
loadMoments();
})();
</script>
</body>
</html>
Loading…
Cancel
Save