72 lines
6.8 KiB
PHP
72 lines
6.8 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
final class Store
|
|
{
|
|
public ?PDO $pdo = null;
|
|
public function __construct(string $path, bool $initializeSchema = true)
|
|
{
|
|
if (!extension_loaded('pdo_sqlite')) throw new RuntimeException('pdo_sqlite is required');
|
|
@mkdir(dirname($path), 0775, true);
|
|
$this->pdo = new PDO('sqlite:' . $path, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
|
$this->pdo->exec('PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;');
|
|
if (!$initializeSchema) return;
|
|
$this->pdo->exec('PRAGMA journal_mode=WAL;');
|
|
$this->pdo->exec(<<<'SQL'
|
|
CREATE TABLE IF NOT EXISTS users (fingerprint TEXT PRIMARY KEY,last_version TEXT,last_seen INTEGER NOT NULL DEFAULT(unixepoch()));
|
|
CREATE TABLE IF NOT EXISTS playlists (fingerprint TEXT PRIMARY KEY,data TEXT NOT NULL DEFAULT '[]',updated_at INTEGER NOT NULL DEFAULT(unixepoch()));
|
|
CREATE TABLE IF NOT EXISTS video_history (fingerprint TEXT NOT NULL,video_id TEXT NOT NULL,title TEXT,channel TEXT,thumbnail TEXT,duration REAL DEFAULT 0,accessed_at INTEGER NOT NULL DEFAULT(unixepoch()),PRIMARY KEY(fingerprint,video_id));
|
|
CREATE INDEX IF NOT EXISTS idx_vh_fp_time ON video_history(fingerprint,accessed_at DESC);
|
|
CREATE TABLE IF NOT EXISTS profiles (name TEXT PRIMARY KEY,data TEXT NOT NULL DEFAULT '{}',created_at INTEGER NOT NULL DEFAULT(unixepoch()),updated_at INTEGER NOT NULL DEFAULT(unixepoch()),secret_hash TEXT,secret_kind TEXT);
|
|
CREATE TABLE IF NOT EXISTS media_cache (video_id TEXT PRIMARY KEY,status TEXT NOT NULL DEFAULT 'queued',gen INTEGER NOT NULL DEFAULT 0,size INTEGER NOT NULL DEFAULT 0,height INTEGER NOT NULL DEFAULT 0,vcodec TEXT,acodec TEXT,duration REAL NOT NULL DEFAULT 0,optimized INTEGER NOT NULL DEFAULT 0,meta TEXT NOT NULL DEFAULT '{}',priority INTEGER NOT NULL DEFAULT 1,auto INTEGER NOT NULL DEFAULT 0,attempts INTEGER NOT NULL DEFAULT 0,error TEXT,retry_at INTEGER NOT NULL DEFAULT 0,created_at INTEGER NOT NULL DEFAULT 0,updated_at INTEGER NOT NULL DEFAULT 0,last_access INTEGER NOT NULL DEFAULT 0,hits INTEGER NOT NULL DEFAULT 0,sha256 TEXT);
|
|
CREATE INDEX IF NOT EXISTS idx_media_lru ON media_cache(status,last_access);
|
|
CREATE TABLE IF NOT EXISTS search_cache (q TEXT PRIMARY KEY,results TEXT NOT NULL,size INTEGER NOT NULL DEFAULT 0,fetched_at INTEGER NOT NULL DEFAULT 0,last_access INTEGER NOT NULL DEFAULT 0);
|
|
CREATE INDEX IF NOT EXISTS idx_search_cache_lru ON search_cache(last_access);
|
|
CREATE TABLE IF NOT EXISTS video_meta (id TEXT PRIMARY KEY,card TEXT NOT NULL,hay TEXT NOT NULL,seen INTEGER NOT NULL DEFAULT 1,updated_at INTEGER NOT NULL DEFAULT 0);
|
|
CREATE INDEX IF NOT EXISTS idx_video_meta_upd ON video_meta(updated_at);
|
|
SQL);
|
|
}
|
|
public function close(): void { $this->pdo = null; }
|
|
public function one(string $sql, array $args = []): ?array { $s = $this->pdo->prepare($sql); $s->execute($args); $r = $s->fetch(); return $r === false ? null : $r; }
|
|
public function all(string $sql, array $args = []): array { $s = $this->pdo->prepare($sql); $s->execute($args); return $s->fetchAll(); }
|
|
public function run(string $sql, array $args = []): int { $s = $this->pdo->prepare($sql); $s->execute($args); return $s->rowCount(); }
|
|
|
|
public function syncUser(array $body): void
|
|
{
|
|
$fp = trim((string)($body['fingerprint'] ?? ''));
|
|
$this->run('INSERT INTO users(fingerprint,last_version,last_seen) VALUES(?,?,unixepoch()) ON CONFLICT(fingerprint) DO UPDATE SET last_version=excluded.last_version,last_seen=excluded.last_seen', [$fp, $body['appVersion'] ?? null]);
|
|
if (is_array($body['playlists'] ?? null)) $this->run('INSERT INTO playlists(fingerprint,data,updated_at) VALUES(?,?,unixepoch()) ON CONFLICT(fingerprint) DO UPDATE SET data=excluded.data,updated_at=excluded.updated_at', [$fp, json_encode($body['playlists'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)]);
|
|
if (is_array($body['recentVideo'] ?? null) && !empty($body['recentVideo']['id'])) $this->recordVideo($fp, $body['recentVideo']);
|
|
}
|
|
public function recordVideo(string $fp, array $v): void
|
|
{
|
|
$this->run('INSERT INTO video_history(fingerprint,video_id,title,channel,thumbnail,duration,accessed_at) VALUES(?,?,?,?,?,?,unixepoch()) ON CONFLICT(fingerprint,video_id) DO UPDATE SET title=excluded.title,channel=excluded.channel,thumbnail=excluded.thumbnail,duration=excluded.duration,accessed_at=excluded.accessed_at', [$fp, (string)$v['id'], $v['title'] ?? null, $v['channel'] ?? null, $v['thumbnail'] ?? null, (float)($v['duration'] ?? 0)]);
|
|
$this->run('DELETE FROM video_history WHERE fingerprint=? AND video_id NOT IN (SELECT video_id FROM video_history WHERE fingerprint=? ORDER BY accessed_at DESC LIMIT 200)', [$fp, $fp]);
|
|
}
|
|
public function userData(string $fp): array
|
|
{
|
|
$user = $this->one('SELECT last_version FROM users WHERE fingerprint=?', [$fp]);
|
|
$playlist = $this->one('SELECT data FROM playlists WHERE fingerprint=?', [$fp]);
|
|
$history = $this->all('SELECT video_id AS id,title,channel,thumbnail,duration FROM video_history WHERE fingerprint=? ORDER BY accessed_at DESC LIMIT 50', [$fp]);
|
|
return ['lastVersion' => $user['last_version'] ?? null, 'playlists' => $playlist ? json_decode($playlist['data'], true) ?: [] : [], 'history' => array_map(static fn($r) => ['id'=>$r['id'],'title'=>$r['title'],'channel'=>$r['channel'],'thumbnail'=>$r['thumbnail'],'duration'=>(float)$r['duration']], $history)];
|
|
}
|
|
public function rememberCards(array $cards): void
|
|
{
|
|
$now = time(); $this->pdo->beginTransaction();
|
|
try { foreach ($cards as $card) if (!empty($card['id'])) {
|
|
$json = json_encode($card, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
$hay = mb_strtolower(($card['title'] ?? '') . ' ' . ($card['channel'] ?? ''));
|
|
$this->run('INSERT INTO video_meta(id,card,hay,seen,updated_at) VALUES(?,?,?,1,?) ON CONFLICT(id) DO UPDATE SET card=excluded.card,hay=excluded.hay,seen=video_meta.seen+1,updated_at=excluded.updated_at', [$card['id'],$json,$hay,$now]);
|
|
} $this->pdo->commit(); } catch (Throwable $e) { $this->pdo->rollBack(); throw $e; }
|
|
}
|
|
public function localSearch(string $query, int $limit = 60): array
|
|
{
|
|
$words = array_slice(preg_split('/\s+/u', mb_strtolower(trim($query))) ?: [], 0, 8); $words = array_filter($words, fn($w) => $w !== '');
|
|
if (!$words) return [];
|
|
$where = implode(' AND ', array_fill(0, count($words), "hay LIKE ? ESCAPE '!'"));
|
|
$args = array_map(fn($w) => '%' . str_replace(['!','%','_'], ['!!','!%','!_'], $w) . '%', $words); $args[] = $limit;
|
|
$rows = $this->all("SELECT card FROM video_meta WHERE $where ORDER BY seen DESC,updated_at DESC LIMIT ?", $args);
|
|
return array_values(array_filter(array_map(fn($r) => json_decode($r['card'], true), $rows)));
|
|
}
|
|
}
|