wip
This commit is contained in:
parent
c15745b156
commit
cdf1322a92
54
Cargo.lock
generated
54
Cargo.lock
generated
@ -253,6 +253,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
@ -920,6 +929,12 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
@ -1041,6 +1056,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
@ -1163,6 +1184,7 @@ dependencies = [
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1499,6 +1521,37 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
@ -1630,6 +1683,7 @@ dependencies = [
|
||||
"ratatui",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"time",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
|
@ -8,5 +8,6 @@ argh = "0.1.13"
|
||||
crossterm = "0.29.0"
|
||||
ratatui = "0.29.0"
|
||||
reqwest = { version = "0.12.20", features = ["blocking"] }
|
||||
rusqlite = { version = "0.36.0", features = ["bundled"] }
|
||||
rusqlite = { version = "0.36.0", features = ["bundled", "time"] }
|
||||
time = { version = "0.3.41", features = ["parsing"] }
|
||||
xml-rs = "0.8.26"
|
||||
|
10
justfile
Normal file
10
justfile
Normal file
@ -0,0 +1,10 @@
|
||||
set quiet := true
|
||||
|
||||
build:
|
||||
cargo build
|
||||
|
||||
test:
|
||||
cargo test
|
||||
|
||||
run +args='list':
|
||||
cargo run -- {{args}}
|
@ -1,31 +1,28 @@
|
||||
use crate::{
|
||||
args::{AddChannelArgs, ListChannelArgs, RemoveChannelArgs, TrsArgs, TrsSubCommand},
|
||||
args::{AddChannelArgs, ListChannelArgs, RemoveChannelArgs},
|
||||
error::TrsError,
|
||||
parser,
|
||||
persistence::Db,
|
||||
persistence::{Db, RssChannelD},
|
||||
};
|
||||
|
||||
pub struct RssChannelD {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub link: String,
|
||||
pub description: String,
|
||||
pub struct TrsEnv {
|
||||
db: Db,
|
||||
http_client: reqwest::blocking::Client,
|
||||
}
|
||||
|
||||
impl RssChannelD {
|
||||
fn new(id: i64, title: String, link: String, description: String) -> Self {
|
||||
RssChannelD {
|
||||
id,
|
||||
title,
|
||||
link,
|
||||
description,
|
||||
}
|
||||
impl TrsEnv {
|
||||
pub fn new(instance_name: &str) -> Result<Self, TrsError> {
|
||||
let db = Db::create(instance_name)?;
|
||||
let http_client = reqwest::blocking::Client::builder()
|
||||
.user_agent("cool-mist/trs")
|
||||
.build()
|
||||
.map_err(|e| TrsError::ReqwestError(e, "Failed to create HTTP client".to_string()))?;
|
||||
Ok(TrsEnv { db, http_client })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_channel(db: &mut Db, args: &AddChannelArgs) -> Result<(), TrsError> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let rss = client.get(&args.link).send().map_err(|e| {
|
||||
pub fn add_channel(ctx: &mut TrsEnv, args: &AddChannelArgs) -> Result<RssChannelD, TrsError> {
|
||||
let rss = ctx.http_client.get(&args.link).send().map_err(|e| {
|
||||
TrsError::ReqwestError(
|
||||
e,
|
||||
"Unable to download provided RSS channel link".to_string(),
|
||||
@ -41,49 +38,16 @@ pub fn add_channel(db: &mut Db, args: &AddChannelArgs) -> Result<(), TrsError> {
|
||||
.ignore_invalid_encoding_declarations(true)
|
||||
.create_reader(&bytes[..]);
|
||||
let channel = parser::parse_rss_channel(xml_source_stream)?;
|
||||
db.add_channel
|
||||
.execute((channel.title, &args.link, channel.description))
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to insert channel into database".to_string()))?;
|
||||
|
||||
Ok(())
|
||||
ctx.db.add_channel(&channel)
|
||||
}
|
||||
|
||||
pub fn list_channels(conn: &mut Db, args: &ListChannelArgs) -> Result<Vec<RssChannelD>, TrsError> {
|
||||
let channels_iter =
|
||||
conn.list_channels
|
||||
.query_map([args.limit.unwrap_or_else(|| 999)], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut channels = Vec::new();
|
||||
|
||||
for row in channels_iter {
|
||||
let (id, name, link, description) = row?;
|
||||
let channel = RssChannelD::new(id, name, link, description);
|
||||
channels.push(channel);
|
||||
}
|
||||
|
||||
Ok(channels)
|
||||
pub fn list_channels(
|
||||
ctx: &mut TrsEnv,
|
||||
args: &ListChannelArgs,
|
||||
) -> Result<Vec<RssChannelD>, TrsError> {
|
||||
ctx.db.list_channels(args.limit.unwrap_or(u32::MAX))
|
||||
}
|
||||
|
||||
pub fn remove_channel(db: &mut Db, args: &RemoveChannelArgs) -> Result<(), TrsError> {
|
||||
let rows_affected = db
|
||||
.remove_channel
|
||||
.execute([args.id])
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to delete channel from database".to_string()))?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(TrsError::Error(format!(
|
||||
"No channel found with ID: {}",
|
||||
args.id
|
||||
)));
|
||||
}
|
||||
|
||||
println!("Channel with ID {} deleted successfully.", args.id);
|
||||
Ok(())
|
||||
pub fn remove_channel(ctx: &mut TrsEnv, args: &RemoveChannelArgs) -> Result<(), TrsError> {
|
||||
ctx.db.remove_channel(args.id).map(|_| ())
|
||||
}
|
||||
|
22
src/main.rs
22
src/main.rs
@ -1,4 +1,5 @@
|
||||
use args::{TrsArgs, TrsSubCommand};
|
||||
use commands::TrsEnv;
|
||||
use error::Result;
|
||||
pub mod args;
|
||||
pub mod commands;
|
||||
@ -10,26 +11,27 @@ pub mod ui;
|
||||
fn main() -> Result<()> {
|
||||
if std::env::args().len() < 2 {
|
||||
let terminal = ratatui::init();
|
||||
let conn = persistence::init_connection()?;
|
||||
let db = persistence::init_db(&conn)?;
|
||||
ui::ui(db, terminal)?;
|
||||
let ctx = TrsEnv::new("test")?;
|
||||
ui::ui(ctx, terminal)?;
|
||||
ratatui::restore();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let args = argh::from_env::<TrsArgs>();
|
||||
let conn = persistence::init_connection()?;
|
||||
let mut db = persistence::init_db(&conn)?;
|
||||
let mut ctx = TrsEnv::new("test")?;
|
||||
match args.sub_command {
|
||||
TrsSubCommand::AddChannel(args) => commands::add_channel(&mut db, &args),
|
||||
TrsSubCommand::AddChannel(args) => {
|
||||
commands::add_channel(&mut ctx, &args)?;
|
||||
Ok(())
|
||||
}
|
||||
TrsSubCommand::ListChannels(args) => {
|
||||
let channels = commands::list_channels(&mut db, &args)?;
|
||||
let channels = commands::list_channels(&mut ctx, &args)?;
|
||||
for channel in channels {
|
||||
println!("{}: {} ({})", channel.id, channel.title, channel.link);
|
||||
println!("{}: {} ({}) updated on {}", channel.id, channel.title, channel.link, channel.last_update);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
TrsSubCommand::RemoveChannel(args) => commands::remove_channel(&mut db, &args),
|
||||
TrsSubCommand::RemoveChannel(args) => commands::remove_channel(&mut ctx, &args),
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,11 @@
|
||||
use std::io::Read;
|
||||
|
||||
use time::format_description;
|
||||
use time::format_description::well_known::Iso8601;
|
||||
use time::format_description::well_known::Rfc2822;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::OffsetDateTime;
|
||||
use time::PrimitiveDateTime;
|
||||
use xml::{reader::XmlEvent, EventReader};
|
||||
|
||||
use crate::error::Result;
|
||||
@ -12,6 +18,13 @@ pub struct RssChannel {
|
||||
pub articles: Vec<RssArticle>,
|
||||
}
|
||||
|
||||
pub struct RssArticle {
|
||||
pub title: String,
|
||||
pub link: String,
|
||||
pub description: String,
|
||||
pub date: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl RssChannel {
|
||||
fn new() -> Self {
|
||||
RssChannel {
|
||||
@ -32,41 +45,64 @@ impl RssChannel {
|
||||
};
|
||||
|
||||
match field.field {
|
||||
XmlField::ArticleTitle => self.title = value,
|
||||
XmlField::ArticleLink => self.link = value,
|
||||
XmlField::ArticleDescription => self.description = value,
|
||||
XmlField::ItemTitle => last_article.ok_or_else(no_item_error)?.title = value,
|
||||
XmlField::ItemLink => last_article.ok_or_else(no_item_error)?.link = value,
|
||||
XmlField::ItemPubDate => last_article.ok_or_else(no_item_error)?.date = value,
|
||||
XmlField::ChannelTitle => self.title = value,
|
||||
XmlField::ChannelLink => self.link = value,
|
||||
XmlField::ChannelDescription => self.description = value,
|
||||
XmlField::ArticleTitle => last_article.ok_or_else(no_item_error)?.title = value,
|
||||
XmlField::ArticleLink => last_article.ok_or_else(no_item_error)?.link = value,
|
||||
XmlField::ArticleDescription => {
|
||||
last_article.ok_or_else(no_item_error)?.description = value
|
||||
}
|
||||
XmlField::ArticlePubDate => {
|
||||
last_article.ok_or_else(no_item_error)?.date = Some(RssArticle::parse_date(&value)?)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RssArticle {
|
||||
pub title: String,
|
||||
pub link: String,
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
impl RssArticle {
|
||||
fn new() -> Self {
|
||||
RssArticle {
|
||||
title: String::new(),
|
||||
link: String::new(),
|
||||
date: String::new(),
|
||||
description: String::new(),
|
||||
date: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_date(value: &str) -> Result<OffsetDateTime> {
|
||||
let weird_format = format_description::parse(
|
||||
"[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] UTC",
|
||||
)
|
||||
.unwrap();
|
||||
let parsed = OffsetDateTime::parse(value, &Rfc2822)
|
||||
.or(OffsetDateTime::parse(value, &Rfc3339))
|
||||
.or(OffsetDateTime::parse(value, &Iso8601::DEFAULT));
|
||||
|
||||
match parsed {
|
||||
Ok(date) => Ok(date),
|
||||
Err(_) => {
|
||||
// Try parsing with the weird format
|
||||
PrimitiveDateTime::parse(value, &weird_format)
|
||||
.map(|dt| dt.assume_utc())
|
||||
.map_err(|e| {
|
||||
TrsError::Error(format!("Failed to parse date '{}': {}", value, e))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum XmlField {
|
||||
ItemTitle,
|
||||
ItemLink,
|
||||
ItemPubDate,
|
||||
ArticleTitle,
|
||||
ArticleLink,
|
||||
ArticlePubDate,
|
||||
ArticleDescription,
|
||||
ChannelTitle,
|
||||
ChannelLink,
|
||||
ChannelDescription,
|
||||
}
|
||||
|
||||
struct XmlTagField {
|
||||
@ -95,13 +131,18 @@ impl XmlTagField {
|
||||
}
|
||||
}
|
||||
|
||||
const FIELD_TAG_MAPPINGS: [XmlTagField; 6] = [
|
||||
XmlTagField::mapping("title", "title", XmlField::ArticleTitle),
|
||||
XmlTagField::mapping("link", "link", XmlField::ArticleLink),
|
||||
XmlTagField::mapping("description", "description", XmlField::ArticleDescription),
|
||||
XmlTagField::mapping("item > title", "title", XmlField::ItemTitle),
|
||||
XmlTagField::mapping("item > link", "link", XmlField::ItemLink),
|
||||
XmlTagField::mapping("item > pubDate", "pubDate", XmlField::ItemPubDate),
|
||||
const FIELD_TAG_MAPPINGS: [XmlTagField; 7] = [
|
||||
XmlTagField::mapping("title", "title", XmlField::ChannelTitle),
|
||||
XmlTagField::mapping("link", "link", XmlField::ChannelLink),
|
||||
XmlTagField::mapping("description", "description", XmlField::ChannelDescription),
|
||||
XmlTagField::mapping("item > title", "title", XmlField::ArticleTitle),
|
||||
XmlTagField::mapping("item > link", "link", XmlField::ArticleLink),
|
||||
XmlTagField::mapping(
|
||||
"item > description",
|
||||
"description",
|
||||
XmlField::ArticleDescription,
|
||||
),
|
||||
XmlTagField::mapping("item > pubDate", "pubDate", XmlField::ArticlePubDate),
|
||||
];
|
||||
|
||||
pub fn parse_rss_channel<R: Read>(xml_source_stream: EventReader<R>) -> Result<RssChannel> {
|
||||
@ -201,7 +242,8 @@ mod tests {
|
||||
for article in &rss_channel.articles {
|
||||
assert!(!article.title.is_empty());
|
||||
assert!(!article.link.is_empty());
|
||||
assert!(!article.date.is_empty());
|
||||
assert!(!article.description.is_empty());
|
||||
assert!(article.date.is_some());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,78 +1,186 @@
|
||||
use std::env;
|
||||
|
||||
use rusqlite::{Connection, Statement};
|
||||
use rusqlite::Connection;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::error::TrsError;
|
||||
use crate::parser::RssChannel;
|
||||
|
||||
const CREATE_TABLE: &'static str = "CREATE TABLE IF NOT EXISTS Channels ( \
|
||||
const SCHEMA_CHANNELS: &'static str = "CREATE TABLE IF NOT EXISTS Channels ( \
|
||||
id INTEGER PRIMARY KEY, \
|
||||
name TEXT NOT NULL, \
|
||||
link TEXT NOT NULL UNIQUE, \
|
||||
description TEXT \
|
||||
description TEXT, \
|
||||
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP \
|
||||
)";
|
||||
|
||||
const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, description) \
|
||||
VALUES (?1, ?2, ?3)\
|
||||
ON CONFLICT(link) DO UPDATE SET name=?1, description=?3";
|
||||
const REMOVE_CHANNEL: &'static str = "DELETE FROM Channels WHERE id = ?1";
|
||||
const LIST_CHANNELS: &'static str = "SELECT id, name, link, description FROM Channels LIMIT ?1";
|
||||
const SCHEMA_ARTICLES: &'static str = "CREATE TABLE IF NOT EXISTS Articles ( \
|
||||
id INTEGER PRIMARY KEY, \
|
||||
channel_id INTEGER NOT NULL, \
|
||||
title TEXT NOT NULL, \
|
||||
description TEXT, \
|
||||
link TEXT NOT NULL UNIQUE, \
|
||||
pub TIMESTAMP, \
|
||||
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP, \
|
||||
unread BOOLEAN DEFAULT TRUE, \
|
||||
FOREIGN KEY(channel_id) REFERENCES Channels(id) ON DELETE CASCADE \
|
||||
)";
|
||||
|
||||
pub struct Db<'a> {
|
||||
pub add_channel: Statement<'a>,
|
||||
pub remove_channel: Statement<'a>,
|
||||
pub list_channels: Statement<'a>,
|
||||
const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, description, last_update) \
|
||||
VALUES (?1, ?2, ?3, CURRENT_TIMESTAMP)\
|
||||
ON CONFLICT(link) DO UPDATE SET name=?1, description=?3, last_update=CURRENT_TIMESTAMP";
|
||||
const REMOVE_CHANNEL: &'static str = "DELETE FROM Channels WHERE id = ?1";
|
||||
const LIST_CHANNELS: &'static str =
|
||||
"SELECT id, name, link, description, last_update FROM Channels LIMIT ?1";
|
||||
const GET_CHANNEL: &'static str =
|
||||
"SELECT id, name, link, description, last_update FROM Channels WHERE link = ?1";
|
||||
|
||||
const ADD_ARTICLE: &'static str = "INSERT INTO Articles (channel_id, title, description, link, pub, last_update) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP) \
|
||||
ON CONFLICT(link) DO UPDATE SET pub=?5, last_update=CURRENT_TIMESTAMP";
|
||||
|
||||
const GET_ARTICLES_BY_CHANNEL: &'static str =
|
||||
"SELECT id, channel_id, title, description, link, pub, last_update, unread FROM Articles WHERE channel_id = ?1";
|
||||
|
||||
const LIST_ARTICLES: &'static str =
|
||||
"SELECT id, channel_id, title, description, link, pub, last_update, unread FROM Articles ORDER BY pub DESC LIMIT ?2";
|
||||
|
||||
const MARK_ARTICLE_READ: &'static str =
|
||||
"UPDATE Articles SET unread = FALSE WHERE id = ?1";
|
||||
|
||||
const MARK_ARTICLE_UNREAD: &'static str =
|
||||
"UPDATE Articles SET unread = TRUE WHERE id = ?1";
|
||||
|
||||
pub struct Db {
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
macro_rules! prepare_sql {
|
||||
pub struct RssChannelD {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub link: String,
|
||||
pub description: String,
|
||||
pub last_update: OffsetDateTime,
|
||||
}
|
||||
|
||||
macro_rules! schema_sql {
|
||||
($conn:expr, $sql:expr) => {
|
||||
$conn.prepare($sql).map_err(|e| {
|
||||
TrsError::SqlError(e, format!("Failed to prepare SQL statement: {}", $sql))
|
||||
})
|
||||
$conn
|
||||
.execute($sql, ())
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to create Channels table".to_string()))?;
|
||||
};
|
||||
}
|
||||
|
||||
impl<'a> Db<'a> {
|
||||
fn create(conn: &'a Connection) -> Result<Self> {
|
||||
let add_channel = prepare_sql!(conn, ADD_CHANNEL)?;
|
||||
let remove_channel = prepare_sql!(conn, REMOVE_CHANNEL)?;
|
||||
let list_channels = prepare_sql!(conn, LIST_CHANNELS)?;
|
||||
Ok(Db {
|
||||
add_channel,
|
||||
remove_channel,
|
||||
list_channels,
|
||||
})
|
||||
impl Db {
|
||||
pub fn create(instance_name: &str) -> Result<Self> {
|
||||
let home_dir = env::home_dir();
|
||||
let db_dir = home_dir
|
||||
.map(|dir| dir.join(".config").join("trs"))
|
||||
.ok_or(TrsError::Error(
|
||||
"Unable to determine home directory".to_string(),
|
||||
))?;
|
||||
|
||||
match std::fs::create_dir_all(&db_dir) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return Err(TrsError::Error(format!(
|
||||
"Failed to create database directory: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let db_file = db_dir.join(format!("{}.db", instance_name));
|
||||
let connection = Connection::open(db_file)?;
|
||||
|
||||
schema_sql!(connection, SCHEMA_CHANNELS);
|
||||
schema_sql!(connection, SCHEMA_ARTICLES);
|
||||
Ok(Db { connection })
|
||||
}
|
||||
|
||||
pub fn get_channel(&self, link: &str) -> Result<RssChannelD> {
|
||||
self.connection
|
||||
.query_row(GET_CHANNEL, (link,), Db::map_rsschanneld)
|
||||
.map_err(|e| {
|
||||
TrsError::SqlError(e, format!("Failed to retrieve channel with link {}", link))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_channel(&self, channel: &RssChannel) -> Result<RssChannelD> {
|
||||
self.connection
|
||||
.execute(
|
||||
ADD_CHANNEL,
|
||||
(&channel.title, &channel.link, &channel.description),
|
||||
)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to add channel".to_string()))?;
|
||||
|
||||
let inserted_channel = self.get_channel(&channel.link)
|
||||
.map_err(|e| TrsError::Error(format!("Failed to retrieve channel after adding: {}", e)))?;
|
||||
|
||||
for article in &channel.articles {
|
||||
self.connection
|
||||
.execute(
|
||||
ADD_ARTICLE,
|
||||
(
|
||||
&inserted_channel.id,
|
||||
&article.title,
|
||||
&article.description,
|
||||
&article.link,
|
||||
article.date.map(|d| d.to_string()),
|
||||
),
|
||||
)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to add article".to_string()))?;
|
||||
}
|
||||
|
||||
Ok(inserted_channel)
|
||||
}
|
||||
|
||||
pub fn remove_channel(&self, id: u32) -> Result<usize> {
|
||||
self.connection
|
||||
.execute(REMOVE_CHANNEL, (id,))
|
||||
.map(|rows| rows as usize)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to remove channel".to_string()))
|
||||
}
|
||||
|
||||
pub fn list_channels(&self, limit: u32) -> Result<Vec<RssChannelD>> {
|
||||
let channels = self
|
||||
.connection
|
||||
.prepare(LIST_CHANNELS)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to prepare query".to_string()))?
|
||||
.query_map((limit as i64,), Db::map_rsschanneld)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to list channels".to_string()))?
|
||||
.map(|r| r.unwrap())
|
||||
.collect::<Vec<RssChannelD>>();
|
||||
|
||||
Ok(channels)
|
||||
}
|
||||
|
||||
fn map_rsschanneld(row: &rusqlite::Row) -> std::result::Result<RssChannelD, rusqlite::Error> {
|
||||
Ok(RssChannelD::new(
|
||||
row.get(0)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_db(conn: &Connection) -> Result<Db> {
|
||||
conn.execute(CREATE_TABLE, ())
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to create Channels table".to_string()))?;
|
||||
Db::create(conn)
|
||||
}
|
||||
|
||||
pub fn init_connection() -> Result<Connection> {
|
||||
let home_dir = env::home_dir();
|
||||
let db_dir = home_dir
|
||||
.map(|dir| dir.join(".config").join("trs"))
|
||||
.ok_or(TrsError::Error(
|
||||
"Unable to determine home directory".to_string(),
|
||||
))?;
|
||||
|
||||
match std::fs::create_dir_all(&db_dir) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return Err(TrsError::Error(format!(
|
||||
"Failed to create database directory: {}",
|
||||
e
|
||||
)));
|
||||
impl RssChannelD {
|
||||
fn new(
|
||||
id: i64,
|
||||
title: String,
|
||||
link: String,
|
||||
description: String,
|
||||
last_update: OffsetDateTime,
|
||||
) -> Self {
|
||||
RssChannelD {
|
||||
id,
|
||||
title,
|
||||
link,
|
||||
description,
|
||||
last_update,
|
||||
}
|
||||
}
|
||||
|
||||
let db_file = db_dir.join("test.db");
|
||||
let conn = Connection::open(db_file)?;
|
||||
conn.execute(CREATE_TABLE, ())
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to create Channels table".to_string()))?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ use std::io::Stdout;
|
||||
|
||||
use crate::{
|
||||
args::ListChannelArgs,
|
||||
commands::{self, RssChannelD},
|
||||
commands::{self, TrsEnv},
|
||||
error::{Result, TrsError},
|
||||
persistence::Db,
|
||||
persistence::RssChannelD,
|
||||
};
|
||||
use crossterm::event::{self, Event, KeyEventKind};
|
||||
use ratatui::{
|
||||
@ -72,13 +72,13 @@ struct AppState {
|
||||
channels: Vec<RssChannelD>,
|
||||
}
|
||||
|
||||
pub fn ui(mut db: Db, mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
pub fn ui(mut ctx: TrsEnv, mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
let mut app_state = AppState {
|
||||
channels: Vec::new(),
|
||||
exit: false,
|
||||
};
|
||||
|
||||
let channels = commands::list_channels(&mut db, &ListChannelArgs { limit: None })?;
|
||||
let channels = commands::list_channels(&mut ctx, &ListChannelArgs { limit: None })?;
|
||||
app_state.channels = channels;
|
||||
|
||||
loop {
|
||||
|
Loading…
x
Reference in New Issue
Block a user