Add watched tracking, resume positions, sort, density slider, channel metadata, mpv IPC, browser selector, auto-rescan

- SQLite DB for watched/position persistence (database.rs)
- Mark watched toggle on cards and detail panel
- mpv IPC socket integration for resume position tracking (unix)
- Sort by title, duration, file size
- Thumbnail density slider in top bar
- Channel metadata banner (subscriber count, uploader, channel URL)
- Cookie browser selector dropdown in settings
- Auto-rescan library when a download job finishes
- Fix yt-dlp --no-progress-bar flag (does not exist; removed)
- Browser field wired through config → downloader

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-05-11 03:47:29 -07:00
parent 95a73b0980
commit 5b3b8fc901
7 changed files with 644 additions and 221 deletions

View file

@ -1,4 +1,5 @@
use rusqlite::{Connection, Result};
use std::collections::{HashMap, HashSet};
use std::path::Path;
pub struct Database {
@ -13,39 +14,69 @@ impl Database {
Ok(db)
}
pub fn open_in_memory() -> Result<Self> {
let conn = Connection::open_in_memory()?;
let db = Database { conn };
db.init_schema()?;
Ok(db)
}
fn init_schema(&self) -> Result<()> {
self.conn.execute_batch(
"CREATE TABLE IF NOT EXISTS videos (
id TEXT PRIMARY KEY,
channel TEXT NOT NULL,
title TEXT NOT NULL,
downloaded_at DATETIME DEFAULT CURRENT_TIMESTAMP
"CREATE TABLE IF NOT EXISTS watched (
video_id TEXT PRIMARY KEY,
watched_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS downloads (
id INTEGER PRIMARY KEY,
url TEXT NOT NULL,
directory TEXT NOT NULL,
status TEXT NOT NULL,
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME
CREATE TABLE IF NOT EXISTS positions (
video_id TEXT PRIMARY KEY,
position_secs REAL NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);",
)?;
Ok(())
}
pub fn record_download(&self, url: &str, dir: &str, status: &str) -> Result<()> {
pub fn set_watched(&self, video_id: &str, watched: bool) -> Result<()> {
if watched {
self.conn.execute(
"INSERT OR REPLACE INTO watched (video_id) VALUES (?1)",
[video_id],
)?;
} else {
self.conn.execute("DELETE FROM watched WHERE video_id = ?1", [video_id])?;
}
Ok(())
}
pub fn get_watched(&self) -> Result<HashSet<String>> {
let mut stmt = self.conn.prepare("SELECT video_id FROM watched")?;
let ids = stmt
.query_map([], |row| row.get(0))?
.filter_map(Result::ok)
.collect();
Ok(ids)
}
pub fn set_position(&self, video_id: &str, position_secs: f64) -> Result<()> {
self.conn.execute(
"INSERT INTO downloads (url, directory, status) VALUES (?1, ?2, ?3)",
[url, dir, status],
"INSERT OR REPLACE INTO positions (video_id, position_secs, updated_at)
VALUES (?1, ?2, CURRENT_TIMESTAMP)",
rusqlite::params![video_id, position_secs],
)?;
Ok(())
}
pub fn update_download_status(&self, id: i64, status: &str) -> Result<()> {
self.conn.execute(
"UPDATE downloads SET status = ?1, completed_at = CURRENT_TIMESTAMP WHERE id = ?2",
[status, &id.to_string()],
)?;
pub fn clear_position(&self, video_id: &str) -> Result<()> {
self.conn.execute("DELETE FROM positions WHERE video_id = ?1", [video_id])?;
Ok(())
}
pub fn get_positions(&self) -> Result<HashMap<String, f64>> {
let mut stmt = self.conn.prepare("SELECT video_id, position_secs FROM positions")?;
let map = stmt
.query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, f64>(1)?)))?
.filter_map(Result::ok)
.collect();
Ok(map)
}
}