Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions sql/tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
60 changes: 59 additions & 1 deletion src/Brickner/Podsumer/State.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
29 changes: 28 additions & 1 deletion src/Brickner/Podsumer/TStateSchemaMigrations.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ trait TStateSchemaMigrations
'addJobsTable',
'updateJobsTableConstraints',
'addJobsLogColumn',
'removeJobsProgressColumn'
'removeJobsProgressColumn',
'addSegmentsAndClipsTables'
];

protected function checkDBVersion()
Expand Down Expand Up @@ -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;
}
}

23 changes: 23 additions & 0 deletions templates/clips.html.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="container py-10">
<? if (empty($clips)): ?>
<div class="py-10">
<h1 class="text-2xl">No Clips</h1>
</div>
<? else: ?>
<? foreach ($clips as $clip): ?>
<div class="py-4">
<audio controls src="/file?file_id=<?= $clip['file_id'] ?>"></audio>
<? if ($clip['has_sponsor']): ?>
<span class="text-amber-500">Sponsor</span>
<? else: ?>
<span class="text-green-600">No Sponsor</span>
<? endif ?>
<? if (!empty($clip['spectrogram_file'])): ?>
<img src="/file?file_id=<?= $clip['spectrogram_file'] ?>" class="w-64">
<? endif ?>
&nbsp;
<a href="/delete_clip?clip_id=<?= $clip['id'] ?>" class="text-red-600 underline">Delete</a>
</div>
<? endforeach ?>
<? endif ?>
</div>
23 changes: 23 additions & 0 deletions templates/segments.html.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="container py-10">
<? if (empty($segments)): ?>
<div class="py-10">
<h1 class="text-2xl">No Segments</h1>
</div>
<? else: ?>
<h1 class="text-2xl pb-4"><?= htmlspecialchars($item['name'] ?? '') ?></h1>
<? foreach ($segments as $seg): ?>
<div class="py-4">
<audio controls src="/file?file_id=<?= $seg['file_id'] ?>"></audio>
<? if ($seg['has_sponsor']): ?>
<span class="text-amber-500">Sponsor</span>
<? else: ?>
<span class="text-green-600">No Sponsor</span>
<? endif ?>
&nbsp;
<a href="/clips?segment_id=<?= $seg['id'] ?>" class="underline">Clips</a>
&nbsp;
<a href="/delete_segment?segment_id=<?= $seg['id'] ?>" class="text-red-600 underline">Delete</a>
</div>
<? endforeach ?>
<? endif ?>
</div>
51 changes: 51 additions & 0 deletions tests/Brickner/Podsumer/SegmentsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

use Brickner\Podsumer\Main;
use Brickner\Podsumer\State;
use Brickner\Podsumer\Feed;

final class SegmentsTest extends TestCase
{
const TEST_FEED_URL = 'https://feeds.npr.org/500005/podcast.xml';
public string $root = __DIR__ . DIRECTORY_SEPARATOR . '../../..' . DIRECTORY_SEPARATOR;

private Main $main;
private State $state;

protected function setUp(): void
{
$env = [
'REQUEST_SCHEME' => '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));
}
}
79 changes: 79 additions & 0 deletions www/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] ?? '/');
}