41f07b6ad8
Browser-based feed selection layered on top of config/feeds.php: - manage.php (password + CSRF gated, session based): add subreddits (name / r/name / URL) and YouTube channels (@handle / UC id / URL, handle resolved to channel_id once, youtube.com only); enable/disable toggles; remove manual feeds; config feeds are hidden, not deleted. - New feeds are validated with one immediate fetch (bypassing the per-refresh host cap): HTTP 404 rejects the entry, 429 keeps it for a later refresh. YouTube labels adopt the real channel title. - feeds table gains origin (config|manual) and enabled columns, with a guarded ALTER migration for existing databases. - Only enabled feeds are refreshed and served: getItems/countItems filter by feed_url in enabled feeds, so deselected feeds' cached stories are hidden, not deleted; re-adding a URL re-enables it. - Dashboard header gets a Manage link; manage page styled with the existing design tokens; README documents the feature.
84 lines
2.7 KiB
PHP
84 lines
2.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
use PDO;
|
|
|
|
final class Database
|
|
{
|
|
public static function connect(string $path): PDO
|
|
{
|
|
$dir = dirname($path);
|
|
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
|
|
throw new \RuntimeException(sprintf('Unable to create database directory "%s".', $dir));
|
|
}
|
|
|
|
$pdo = new PDO('sqlite:' . $path, null, null, [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]);
|
|
|
|
$pdo->exec('PRAGMA journal_mode = WAL');
|
|
$pdo->exec('PRAGMA busy_timeout = 5000');
|
|
$pdo->exec('PRAGMA foreign_keys = ON');
|
|
|
|
self::migrate($pdo);
|
|
|
|
return $pdo;
|
|
}
|
|
|
|
private static function migrate(PDO $pdo): void
|
|
{
|
|
$pdo->exec(<<<'SQL'
|
|
CREATE TABLE IF NOT EXISTS items (
|
|
id TEXT PRIMARY KEY,
|
|
source TEXT NOT NULL,
|
|
feed_label TEXT,
|
|
feed_url TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
link TEXT NOT NULL,
|
|
author TEXT,
|
|
published_at TEXT,
|
|
description TEXT,
|
|
thumbnail TEXT,
|
|
fetched_at TEXT NOT NULL
|
|
)
|
|
SQL);
|
|
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_items_published ON items (published_at DESC)');
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_items_source ON items (source, published_at DESC)');
|
|
|
|
$pdo->exec(<<<'SQL'
|
|
CREATE TABLE IF NOT EXISTS feeds (
|
|
url TEXT PRIMARY KEY,
|
|
source TEXT NOT NULL,
|
|
label TEXT,
|
|
last_attempt_at TEXT,
|
|
last_success_at TEXT,
|
|
last_error TEXT,
|
|
origin TEXT NOT NULL DEFAULT 'config',
|
|
enabled INTEGER NOT NULL DEFAULT 1
|
|
)
|
|
SQL);
|
|
|
|
// SQLite has no "ADD COLUMN IF NOT EXISTS": migrate pre-existing tables.
|
|
self::addColumn($pdo, 'feeds', 'origin', "ALTER TABLE feeds ADD COLUMN origin TEXT NOT NULL DEFAULT 'config'");
|
|
self::addColumn($pdo, 'feeds', 'enabled', 'ALTER TABLE feeds ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1');
|
|
}
|
|
|
|
private static function addColumn(PDO $pdo, string $table, string $column, string $sql): void
|
|
{
|
|
$existing = [];
|
|
foreach ($pdo->query(sprintf('PRAGMA table_info(%s)', $table)) as $row) {
|
|
$existing[strtolower((string) $row['name'])] = true;
|
|
}
|
|
|
|
if (!isset($existing[strtolower($column)])) {
|
|
$pdo->exec($sql);
|
|
}
|
|
}
|
|
}
|