Skip to content

Commit 0f0b9cd

Browse files
Add segment and clip review interface
1 parent 5529a49 commit 0f0b9cd

File tree

7 files changed

+280
-2
lines changed

7 files changed

+280
-2
lines changed

sql/tables.sql

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,20 @@ CREATE TABLE IF NOT EXISTS `items` (
4343
CREATE TABLE IF NOT EXISTS `versions` (
4444
version INTEGER PRIMARY KEY AUTOINCREMENT
4545
);
46+
CREATE TABLE IF NOT EXISTS `segments` (
47+
id INTEGER PRIMARY KEY AUTOINCREMENT,
48+
item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
49+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
50+
start REAL NOT NULL,
51+
end REAL NOT NULL,
52+
has_sponsor INTEGER NOT NULL DEFAULT 0
53+
);
54+
55+
CREATE TABLE IF NOT EXISTS `clips` (
56+
id INTEGER PRIMARY KEY AUTOINCREMENT,
57+
segment_id INTEGER NOT NULL REFERENCES segments(id) ON DELETE CASCADE,
58+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
59+
has_sponsor INTEGER NOT NULL DEFAULT 0,
60+
spectrogram_file INTEGER,
61+
FOREIGN KEY (spectrogram_file) REFERENCES files(id) ON DELETE SET NULL
62+
);

src/Brickner/Podsumer/State.php

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class State
1313
{
1414
use TStateSchemaMigrations;
1515

16-
CONST VERSION = 6; # The version of the schema for this commit.
16+
CONST VERSION = 8; # The version of the schema for this commit.
1717

1818
protected Main $main;
1919
protected $state_file_path;
@@ -457,6 +457,64 @@ public function getItemAdSections(int $item_id): array
457457
return is_array($sections) ? $sections : [];
458458
}
459459

460+
public function addSegment(int $item_id, int $file_id, float $start, float $end, bool $has_sponsor = false): int
461+
{
462+
$sql = 'INSERT INTO segments (item_id, file_id, start, end, has_sponsor) VALUES (:item_id, :file_id, :start, :end, :has_sponsor)';
463+
$this->query($sql, [
464+
'item_id' => $item_id,
465+
'file_id' => $file_id,
466+
'start' => $start,
467+
'end' => $end,
468+
'has_sponsor' => $has_sponsor ? 1 : 0
469+
]);
470+
return intval($this->pdo->lastInsertId());
471+
}
472+
473+
public function getSegmentsForItem(int $item_id): array
474+
{
475+
$sql = 'SELECT * FROM segments WHERE item_id = :item_id ORDER BY start';
476+
$result = $this->query($sql, ['item_id' => $item_id]);
477+
return $result && is_array($result) ? $result : [];
478+
}
479+
480+
public function getSegment(int $segment_id): array
481+
{
482+
$sql = 'SELECT * FROM segments WHERE id = :id';
483+
$result = $this->query($sql, ['id' => $segment_id]);
484+
return $result && isset($result[0]) ? $result[0] : [];
485+
}
486+
487+
public function deleteSegment(int $segment_id): void
488+
{
489+
$sql = 'DELETE FROM segments WHERE id = :id';
490+
$this->query($sql, ['id' => $segment_id]);
491+
}
492+
493+
public function addClip(int $segment_id, int $file_id, bool $has_sponsor = false, ?int $spectrogram_file = null): int
494+
{
495+
$sql = 'INSERT INTO clips (segment_id, file_id, has_sponsor, spectrogram_file) VALUES (:segment_id, :file_id, :has_sponsor, :spectrogram_file)';
496+
$this->query($sql, [
497+
'segment_id' => $segment_id,
498+
'file_id' => $file_id,
499+
'has_sponsor' => $has_sponsor ? 1 : 0,
500+
'spectrogram_file' => $spectrogram_file
501+
]);
502+
return intval($this->pdo->lastInsertId());
503+
}
504+
505+
public function getClipsForSegment(int $segment_id): array
506+
{
507+
$sql = 'SELECT * FROM clips WHERE segment_id = :segment_id';
508+
$result = $this->query($sql, ['segment_id' => $segment_id]);
509+
return $result && is_array($result) ? $result : [];
510+
}
511+
512+
public function deleteClip(int $clip_id): void
513+
{
514+
$sql = 'DELETE FROM clips WHERE id = :id';
515+
$this->query($sql, ['id' => $clip_id]);
516+
}
517+
460518
protected function loadFile(string $filename): string
461519
{
462520
$contents = false;

src/Brickner/Podsumer/TStateSchemaMigrations.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ trait TStateSchemaMigrations
1616
'addJobsTable',
1717
'updateJobsTableConstraints',
1818
'addJobsLogColumn',
19-
'removeJobsProgressColumn'
19+
'removeJobsProgressColumn',
20+
'addSegmentsAndClipsTables'
2021
];
2122

2223
protected function checkDBVersion()
@@ -188,5 +189,31 @@ public function removeJobsProgressColumn(): bool {
188189

189190
return $dropTable !== false && $createJobsTable !== false && $createJobsIndex !== false && $createJobsTypeIndex !== false;
190191
}
192+
193+
public function addSegmentsAndClipsTables(): bool {
194+
$createSegments = $this->query(
195+
"CREATE TABLE IF NOT EXISTS segments (\n" .
196+
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n" .
197+
" item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,\n" .
198+
" file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n" .
199+
" start REAL NOT NULL,\n" .
200+
" end REAL NOT NULL,\n" .
201+
" has_sponsor INTEGER NOT NULL DEFAULT 0\n" .
202+
")"
203+
);
204+
205+
$createClips = $this->query(
206+
"CREATE TABLE IF NOT EXISTS clips (\n" .
207+
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n" .
208+
" segment_id INTEGER NOT NULL REFERENCES segments(id) ON DELETE CASCADE,\n" .
209+
" file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n" .
210+
" has_sponsor INTEGER NOT NULL DEFAULT 0,\n" .
211+
" spectrogram_file INTEGER,\n" .
212+
" FOREIGN KEY (spectrogram_file) REFERENCES files(id) ON DELETE SET NULL\n" .
213+
")"
214+
);
215+
216+
return $createSegments !== false && $createClips !== false;
217+
}
191218
}
192219

templates/clips.html.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div class="container py-10">
2+
<? if (empty($clips)): ?>
3+
<div class="py-10">
4+
<h1 class="text-2xl">No Clips</h1>
5+
</div>
6+
<? else: ?>
7+
<? foreach ($clips as $clip): ?>
8+
<div class="py-4">
9+
<audio controls src="/file?file_id=<?= $clip['file_id'] ?>"></audio>
10+
<? if ($clip['has_sponsor']): ?>
11+
<span class="text-amber-500">Sponsor</span>
12+
<? else: ?>
13+
<span class="text-green-600">No Sponsor</span>
14+
<? endif ?>
15+
<? if (!empty($clip['spectrogram_file'])): ?>
16+
<img src="/file?file_id=<?= $clip['spectrogram_file'] ?>" class="w-64">
17+
<? endif ?>
18+
&nbsp;
19+
<a href="/delete_clip?clip_id=<?= $clip['id'] ?>" class="text-red-600 underline">Delete</a>
20+
</div>
21+
<? endforeach ?>
22+
<? endif ?>
23+
</div>

templates/segments.html.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div class="container py-10">
2+
<? if (empty($segments)): ?>
3+
<div class="py-10">
4+
<h1 class="text-2xl">No Segments</h1>
5+
</div>
6+
<? else: ?>
7+
<h1 class="text-2xl pb-4"><?= htmlspecialchars($item['name'] ?? '') ?></h1>
8+
<? foreach ($segments as $seg): ?>
9+
<div class="py-4">
10+
<audio controls src="/file?file_id=<?= $seg['file_id'] ?>"></audio>
11+
<? if ($seg['has_sponsor']): ?>
12+
<span class="text-amber-500">Sponsor</span>
13+
<? else: ?>
14+
<span class="text-green-600">No Sponsor</span>
15+
<? endif ?>
16+
&nbsp;
17+
<a href="/clips?segment_id=<?= $seg['id'] ?>" class="underline">Clips</a>
18+
&nbsp;
19+
<a href="/delete_segment?segment_id=<?= $seg['id'] ?>" class="text-red-600 underline">Delete</a>
20+
</div>
21+
<? endforeach ?>
22+
<? endif ?>
23+
</div>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types=1);
2+
use PHPUnit\Framework\TestCase;
3+
4+
use Brickner\Podsumer\Main;
5+
use Brickner\Podsumer\State;
6+
use Brickner\Podsumer\Feed;
7+
8+
final class SegmentsTest extends TestCase
9+
{
10+
const TEST_FEED_URL = 'https://feeds.npr.org/500005/podcast.xml';
11+
public string $root = __DIR__ . DIRECTORY_SEPARATOR . '../../..' . DIRECTORY_SEPARATOR;
12+
13+
private Main $main;
14+
private State $state;
15+
16+
protected function setUp(): void
17+
{
18+
$env = [
19+
'REQUEST_SCHEME' => 'http',
20+
'HTTP_HOST' => 'example.com',
21+
'REQUEST_URI' => '/',
22+
'REQUEST_METHOD' => 'GET',
23+
'REMOTE_ADDR' => '127.0.0.1',
24+
];
25+
26+
$tmp_main = new Main($this->root, $env, [], [], true);
27+
@unlink($tmp_main->getStateFilePath());
28+
29+
$this->main = new Main($this->root, $env, [], [], true);
30+
$this->state = new State($this->main);
31+
32+
$feed = new Feed(self::TEST_FEED_URL);
33+
$this->state->addFeed($feed);
34+
}
35+
36+
public function testAddSegmentAndClip(): void
37+
{
38+
$item = $this->state->getFeedItem(1);
39+
$feed = $this->state->getFeed($item['feed_id']);
40+
41+
$file_id = $this->state->addFile('dummy.mp3', 'abc', $feed);
42+
$segment_id = $this->state->addSegment($item['id'], $file_id, 0.0, 1.0, false);
43+
$segments = $this->state->getSegmentsForItem($item['id']);
44+
$this->assertGreaterThan(0, count($segments));
45+
46+
$clip_file_id = $this->state->addFile('clip.mp3', 'xyz', $feed);
47+
$clip_id = $this->state->addClip($segment_id, $clip_file_id, false, null);
48+
$clips = $this->state->getClipsForSegment($segment_id);
49+
$this->assertGreaterThan(0, count($clips));
50+
}
51+
}

www/index.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,3 +1022,82 @@ function reprocess_ads(array $args): void
10221022
}
10231023
}
10241024

1025+
#[Route('/segments', 'GET', true)]
1026+
function segments(array $args): void
1027+
{
1028+
global $main;
1029+
1030+
if (empty($args['item_id'])) {
1031+
$main->setResponseCode(404);
1032+
return;
1033+
}
1034+
1035+
$item_id = intval($args['item_id']);
1036+
$segments = $main->getState()->getSegmentsForItem($item_id);
1037+
$item = $main->getState()->getFeedItem($item_id);
1038+
1039+
$vars = [
1040+
'segments' => $segments,
1041+
'item' => $item
1042+
];
1043+
1044+
Template::render($main, 'segments', $vars);
1045+
}
1046+
1047+
#[Route('/clips', 'GET', true)]
1048+
function clips(array $args): void
1049+
{
1050+
global $main;
1051+
1052+
if (empty($args['segment_id'])) {
1053+
$main->setResponseCode(404);
1054+
return;
1055+
}
1056+
1057+
$segment_id = intval($args['segment_id']);
1058+
$clips = $main->getState()->getClipsForSegment($segment_id);
1059+
$segment = $main->getState()->getSegment($segment_id);
1060+
1061+
$vars = [
1062+
'clips' => $clips,
1063+
'segment' => $segment
1064+
];
1065+
1066+
Template::render($main, 'clips', $vars);
1067+
}
1068+
1069+
#[Route('/delete_segment', 'GET', true)]
1070+
function delete_segment(array $args): void
1071+
{
1072+
global $main;
1073+
1074+
if (empty($args['segment_id'])) {
1075+
$main->setResponseCode(404);
1076+
return;
1077+
}
1078+
1079+
$segment_id = intval($args['segment_id']);
1080+
$segment = $main->getState()->getSegment($segment_id);
1081+
if (!empty($segment)) {
1082+
$main->getState()->deleteSegment($segment_id);
1083+
$main->redirect('/segments?item_id=' . $segment['item_id']);
1084+
} else {
1085+
$main->setResponseCode(404);
1086+
}
1087+
}
1088+
1089+
#[Route('/delete_clip', 'GET', true)]
1090+
function delete_clip(array $args): void
1091+
{
1092+
global $main;
1093+
1094+
if (empty($args['clip_id'])) {
1095+
$main->setResponseCode(404);
1096+
return;
1097+
}
1098+
1099+
$clip_id = intval($args['clip_id']);
1100+
$main->getState()->deleteClip($clip_id);
1101+
$main->redirect($_SERVER['HTTP_REFERER'] ?? '/');
1102+
}
1103+

0 commit comments

Comments
 (0)