feat: Takeout CSV, Atom feed, yt-dlp progress, and SQLite layers

37 unit tests covering the three parsers and the database, including
that a feed refresh preserves download state and that a truncated
feed response fails rather than storing partial results.
This commit is contained in:
vincent
2026-08-29 02:26:17 +02:00
parent 6fdad60190
commit b724e9d56f
11 changed files with 7139 additions and 6 deletions
+104
View File
@@ -0,0 +1,104 @@
use serde::{Deserialize, Serialize};
/// A YouTube channel the user is subscribed to, as imported from Takeout.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Channel {
pub id: String,
pub title: String,
pub url: String,
}
/// A video as described by a channel's public Atom feed.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Video {
pub id: String,
pub channel_id: String,
pub title: String,
pub description: String,
/// Unix seconds, so feed ordering is a plain indexed sort.
pub published: i64,
pub thumb_url: String,
pub views: i64,
pub is_short: bool,
}
/// A channel plus how many of its videos we currently know about.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelWithCount {
pub id: String,
pub title: String,
pub url: String,
pub video_count: i64,
pub downloaded_count: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DownloadState {
Queued,
Running,
Done,
Failed,
Cancelled,
}
impl DownloadState {
pub fn as_str(&self) -> &'static str {
match self {
DownloadState::Queued => "queued",
DownloadState::Running => "running",
DownloadState::Done => "done",
DownloadState::Failed => "failed",
DownloadState::Cancelled => "cancelled",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"queued" => Some(DownloadState::Queued),
"running" => Some(DownloadState::Running),
"done" => Some(DownloadState::Done),
"failed" => Some(DownloadState::Failed),
"cancelled" => Some(DownloadState::Cancelled),
_ => None,
}
}
}
/// A feed row: video metadata joined with whatever we know about its download.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedItem {
pub id: String,
pub channel_id: String,
pub channel_title: String,
pub title: String,
pub description: String,
pub published: i64,
pub thumb_url: String,
pub thumb_path: Option<String>,
pub views: i64,
pub is_short: bool,
pub state: Option<DownloadState>,
pub path: Option<String>,
pub pct: Option<f64>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FeedFilter {
pub channel_id: Option<String>,
pub search: Option<String>,
#[serde(default)]
pub downloaded_only: bool,
#[serde(default)]
pub hide_shorts: bool,
pub limit: Option<i64>,
}
/// Result of checking that the external binaries we shell out to exist.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Prereqs {
pub yt_dlp: Option<String>,
pub ffmpeg: Option<String>,
pub library_path: String,
}