From 0f0b9cdef5307c1a48f36fcef449ae92ed22664d Mon Sep 17 00:00:00 2001 From: Josh Brickner <15388+leftouterjoins@users.noreply.github.com> Date: Sun, 22 Jun 2025 10:55:06 -0600 Subject: [PATCH] Add segment and clip review interface --- sql/tables.sql | 17 ++++ src/Brickner/Podsumer/State.php | 60 +++++++++++++- .../Podsumer/TStateSchemaMigrations.php | 29 ++++++- templates/clips.html.php | 23 ++++++ templates/segments.html.php | 23 ++++++ tests/Brickner/Podsumer/SegmentsTest.php | 51 ++++++++++++ www/index.php | 79 +++++++++++++++++++ 7 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 templates/clips.html.php create mode 100644 templates/segments.html.php create mode 100644 tests/Brickner/Podsumer/SegmentsTest.php diff --git a/sql/tables.sql b/sql/tables.sql index b86161d..0a82a38 100755 --- a/sql/tables.sql +++ b/sql/tables.sql @@ -43,3 +43,20 @@ CREATE TABLE IF NOT EXISTS `items` ( CREATE TABLE IF NOT EXISTS `versions` ( version INTEGER PRIMARY KEY AUTOINCREMENT ); +CREATE TABLE IF NOT EXISTS `segments` ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE, + file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, + start REAL NOT NULL, + end REAL NOT NULL, + has_sponsor INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS `clips` ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + segment_id INTEGER NOT NULL REFERENCES segments(id) ON DELETE CASCADE, + file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, + has_sponsor INTEGER NOT NULL DEFAULT 0, + spectrogram_file INTEGER, + FOREIGN KEY (spectrogram_file) REFERENCES files(id) ON DELETE SET NULL +); diff --git a/src/Brickner/Podsumer/State.php b/src/Brickner/Podsumer/State.php index ff18500..b94186d 100755 --- a/src/Brickner/Podsumer/State.php +++ b/src/Brickner/Podsumer/State.php @@ -13,7 +13,7 @@ class State { use TStateSchemaMigrations; - CONST VERSION = 6; # The version of the schema for this commit. + CONST VERSION = 8; # The version of the schema for this commit. protected Main $main; protected $state_file_path; @@ -457,6 +457,64 @@ public function getItemAdSections(int $item_id): array return is_array($sections) ? $sections : []; } + public function addSegment(int $item_id, int $file_id, float $start, float $end, bool $has_sponsor = false): int + { + $sql = 'INSERT INTO segments (item_id, file_id, start, end, has_sponsor) VALUES (:item_id, :file_id, :start, :end, :has_sponsor)'; + $this->query($sql, [ + 'item_id' => $item_id, + 'file_id' => $file_id, + 'start' => $start, + 'end' => $end, + 'has_sponsor' => $has_sponsor ? 1 : 0 + ]); + return intval($this->pdo->lastInsertId()); + } + + public function getSegmentsForItem(int $item_id): array + { + $sql = 'SELECT * FROM segments WHERE item_id = :item_id ORDER BY start'; + $result = $this->query($sql, ['item_id' => $item_id]); + return $result && is_array($result) ? $result : []; + } + + public function getSegment(int $segment_id): array + { + $sql = 'SELECT * FROM segments WHERE id = :id'; + $result = $this->query($sql, ['id' => $segment_id]); + return $result && isset($result[0]) ? $result[0] : []; + } + + public function deleteSegment(int $segment_id): void + { + $sql = 'DELETE FROM segments WHERE id = :id'; + $this->query($sql, ['id' => $segment_id]); + } + + public function addClip(int $segment_id, int $file_id, bool $has_sponsor = false, ?int $spectrogram_file = null): int + { + $sql = 'INSERT INTO clips (segment_id, file_id, has_sponsor, spectrogram_file) VALUES (:segment_id, :file_id, :has_sponsor, :spectrogram_file)'; + $this->query($sql, [ + 'segment_id' => $segment_id, + 'file_id' => $file_id, + 'has_sponsor' => $has_sponsor ? 1 : 0, + 'spectrogram_file' => $spectrogram_file + ]); + return intval($this->pdo->lastInsertId()); + } + + public function getClipsForSegment(int $segment_id): array + { + $sql = 'SELECT * FROM clips WHERE segment_id = :segment_id'; + $result = $this->query($sql, ['segment_id' => $segment_id]); + return $result && is_array($result) ? $result : []; + } + + public function deleteClip(int $clip_id): void + { + $sql = 'DELETE FROM clips WHERE id = :id'; + $this->query($sql, ['id' => $clip_id]); + } + protected function loadFile(string $filename): string { $contents = false; diff --git a/src/Brickner/Podsumer/TStateSchemaMigrations.php b/src/Brickner/Podsumer/TStateSchemaMigrations.php index 5f7732c..a3948c9 100644 --- a/src/Brickner/Podsumer/TStateSchemaMigrations.php +++ b/src/Brickner/Podsumer/TStateSchemaMigrations.php @@ -16,7 +16,8 @@ trait TStateSchemaMigrations 'addJobsTable', 'updateJobsTableConstraints', 'addJobsLogColumn', - 'removeJobsProgressColumn' + 'removeJobsProgressColumn', + 'addSegmentsAndClipsTables' ]; protected function checkDBVersion() @@ -188,5 +189,31 @@ public function removeJobsProgressColumn(): bool { return $dropTable !== false && $createJobsTable !== false && $createJobsIndex !== false && $createJobsTypeIndex !== false; } + + public function addSegmentsAndClipsTables(): bool { + $createSegments = $this->query( + "CREATE TABLE IF NOT EXISTS segments (\n" . + " id INTEGER PRIMARY KEY AUTOINCREMENT,\n" . + " item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,\n" . + " file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n" . + " start REAL NOT NULL,\n" . + " end REAL NOT NULL,\n" . + " has_sponsor INTEGER NOT NULL DEFAULT 0\n" . + ")" + ); + + $createClips = $this->query( + "CREATE TABLE IF NOT EXISTS clips (\n" . + " id INTEGER PRIMARY KEY AUTOINCREMENT,\n" . + " segment_id INTEGER NOT NULL REFERENCES segments(id) ON DELETE CASCADE,\n" . + " file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n" . + " has_sponsor INTEGER NOT NULL DEFAULT 0,\n" . + " spectrogram_file INTEGER,\n" . + " FOREIGN KEY (spectrogram_file) REFERENCES files(id) ON DELETE SET NULL\n" . + ")" + ); + + return $createSegments !== false && $createClips !== false; + } } diff --git a/templates/clips.html.php b/templates/clips.html.php new file mode 100644 index 0000000..a1e2f44 --- /dev/null +++ b/templates/clips.html.php @@ -0,0 +1,23 @@ +
+ +
+

No Clips

+
+ + +
+ + + Sponsor + + No Sponsor + + + + +   + Delete +
+ + +
diff --git a/templates/segments.html.php b/templates/segments.html.php new file mode 100644 index 0000000..f5d1eb5 --- /dev/null +++ b/templates/segments.html.php @@ -0,0 +1,23 @@ +
+ +
+

No Segments

+
+ +

+ +
+ + + Sponsor + + No Sponsor + +   + Clips +   + Delete +
+ + +
diff --git a/tests/Brickner/Podsumer/SegmentsTest.php b/tests/Brickner/Podsumer/SegmentsTest.php new file mode 100644 index 0000000..052e73c --- /dev/null +++ b/tests/Brickner/Podsumer/SegmentsTest.php @@ -0,0 +1,51 @@ + 'http', + 'HTTP_HOST' => 'example.com', + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'GET', + 'REMOTE_ADDR' => '127.0.0.1', + ]; + + $tmp_main = new Main($this->root, $env, [], [], true); + @unlink($tmp_main->getStateFilePath()); + + $this->main = new Main($this->root, $env, [], [], true); + $this->state = new State($this->main); + + $feed = new Feed(self::TEST_FEED_URL); + $this->state->addFeed($feed); + } + + public function testAddSegmentAndClip(): void + { + $item = $this->state->getFeedItem(1); + $feed = $this->state->getFeed($item['feed_id']); + + $file_id = $this->state->addFile('dummy.mp3', 'abc', $feed); + $segment_id = $this->state->addSegment($item['id'], $file_id, 0.0, 1.0, false); + $segments = $this->state->getSegmentsForItem($item['id']); + $this->assertGreaterThan(0, count($segments)); + + $clip_file_id = $this->state->addFile('clip.mp3', 'xyz', $feed); + $clip_id = $this->state->addClip($segment_id, $clip_file_id, false, null); + $clips = $this->state->getClipsForSegment($segment_id); + $this->assertGreaterThan(0, count($clips)); + } +} diff --git a/www/index.php b/www/index.php index 0c4fb84..f9f1048 100755 --- a/www/index.php +++ b/www/index.php @@ -1022,3 +1022,82 @@ function reprocess_ads(array $args): void } } +#[Route('/segments', 'GET', true)] +function segments(array $args): void +{ + global $main; + + if (empty($args['item_id'])) { + $main->setResponseCode(404); + return; + } + + $item_id = intval($args['item_id']); + $segments = $main->getState()->getSegmentsForItem($item_id); + $item = $main->getState()->getFeedItem($item_id); + + $vars = [ + 'segments' => $segments, + 'item' => $item + ]; + + Template::render($main, 'segments', $vars); +} + +#[Route('/clips', 'GET', true)] +function clips(array $args): void +{ + global $main; + + if (empty($args['segment_id'])) { + $main->setResponseCode(404); + return; + } + + $segment_id = intval($args['segment_id']); + $clips = $main->getState()->getClipsForSegment($segment_id); + $segment = $main->getState()->getSegment($segment_id); + + $vars = [ + 'clips' => $clips, + 'segment' => $segment + ]; + + Template::render($main, 'clips', $vars); +} + +#[Route('/delete_segment', 'GET', true)] +function delete_segment(array $args): void +{ + global $main; + + if (empty($args['segment_id'])) { + $main->setResponseCode(404); + return; + } + + $segment_id = intval($args['segment_id']); + $segment = $main->getState()->getSegment($segment_id); + if (!empty($segment)) { + $main->getState()->deleteSegment($segment_id); + $main->redirect('/segments?item_id=' . $segment['item_id']); + } else { + $main->setResponseCode(404); + } +} + +#[Route('/delete_clip', 'GET', true)] +function delete_clip(array $args): void +{ + global $main; + + if (empty($args['clip_id'])) { + $main->setResponseCode(404); + return; + } + + $clip_id = intval($args['clip_id']); + $main->getState()->deleteClip($clip_id); + $main->redirect($_SERVER['HTTP_REFERER'] ?? '/'); +} +