cli func
This commit is contained in:
parent
cdf1322a92
commit
3c01effdf1
28
src/args.rs
28
src/args.rs
@ -12,7 +12,9 @@ pub struct TrsArgs {
|
||||
pub enum TrsSubCommand {
|
||||
AddChannel(AddChannelArgs),
|
||||
ListChannels(ListChannelArgs),
|
||||
GetArticles(GetArticlesArgs),
|
||||
RemoveChannel(RemoveChannelArgs),
|
||||
MarkRead(MarkReadArgs),
|
||||
}
|
||||
|
||||
/// Add a new RSS channel
|
||||
@ -33,6 +35,32 @@ pub struct ListChannelArgs {
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
/// Get articles
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argh(subcommand, name = "articles")]
|
||||
pub struct GetArticlesArgs {
|
||||
/// id of the channel to get articles from
|
||||
#[argh(option, short = 'c')]
|
||||
pub channel_id: Option<u32>,
|
||||
|
||||
/// only get unread articles
|
||||
#[argh(switch)]
|
||||
pub unread: bool,
|
||||
}
|
||||
|
||||
/// Mark article as read/unread
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argh(subcommand, name = "read")]
|
||||
pub struct MarkReadArgs {
|
||||
/// id of the article to mark read/unread
|
||||
#[argh(option)]
|
||||
pub id: u32,
|
||||
|
||||
/// mark the article as unread
|
||||
#[argh(switch)]
|
||||
pub unread: bool,
|
||||
}
|
||||
|
||||
/// Delete an RSS channel
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argh(subcommand, name = "remove")]
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
args::{AddChannelArgs, ListChannelArgs, RemoveChannelArgs},
|
||||
args::{self, AddChannelArgs, ListChannelArgs, RemoveChannelArgs},
|
||||
error::TrsError,
|
||||
parser,
|
||||
persistence::{Db, RssChannelD},
|
||||
@ -51,3 +51,34 @@ pub fn list_channels(
|
||||
pub fn remove_channel(ctx: &mut TrsEnv, args: &RemoveChannelArgs) -> Result<(), TrsError> {
|
||||
ctx.db.remove_channel(args.id).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn mark_read(ctx: &mut TrsEnv, args: &args::MarkReadArgs) -> Result<(), TrsError> {
|
||||
match args.unread {
|
||||
true => ctx.db.mark_article_unread(args.id as i64)?,
|
||||
false => ctx.db.mark_article_read(args.id as i64)?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_articles_by_channel(
|
||||
ctx: &mut TrsEnv,
|
||||
args: &args::GetArticlesArgs,
|
||||
) -> Result<Vec<RssChannelD>, TrsError> {
|
||||
let mut channels = ctx.db.list_channels(u32::MAX)?;
|
||||
channels.retain(|channel| {
|
||||
if let Some(channel_id) = args.channel_id {
|
||||
channel_id as i64 == channel.id
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if args.unread {
|
||||
for channel in &mut channels {
|
||||
channel.articles.retain(|article| article.unread);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(channels)
|
||||
}
|
||||
|
17
src/main.rs
17
src/main.rs
@ -33,5 +33,22 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
TrsSubCommand::RemoveChannel(args) => commands::remove_channel(&mut ctx, &args),
|
||||
TrsSubCommand::GetArticles(args) => {
|
||||
let channels = commands::get_articles_by_channel(&mut ctx, &args)?;
|
||||
for channel in channels {
|
||||
println!("Channel #{}: {} ({})", channel.id, channel.title, channel.link);
|
||||
for article in channel.articles {
|
||||
println!(
|
||||
" #{} - {} ({}) [{}]",
|
||||
article.id,
|
||||
article.title,
|
||||
article.link,
|
||||
article.pub_date.map_or("No date".to_string(), |d| d.to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
TrsSubCommand::MarkRead(args) => commands::mark_read(&mut ctx, &args),
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use time::OffsetDateTime;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::error::TrsError;
|
||||
use crate::parser::RssArticle;
|
||||
use crate::parser::RssChannel;
|
||||
|
||||
const SCHEMA_CHANNELS: &'static str = "CREATE TABLE IF NOT EXISTS Channels ( \
|
||||
@ -21,9 +22,9 @@ const SCHEMA_ARTICLES: &'static str = "CREATE TABLE IF NOT EXISTS Articles ( \
|
||||
title TEXT NOT NULL, \
|
||||
description TEXT, \
|
||||
link TEXT NOT NULL UNIQUE, \
|
||||
pub TIMESTAMP, \
|
||||
pub_date TIMESTAMP, \
|
||||
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP, \
|
||||
unread BOOLEAN DEFAULT TRUE, \
|
||||
unread BOOLEAN DEFAULT FALSE, \
|
||||
FOREIGN KEY(channel_id) REFERENCES Channels(id) ON DELETE CASCADE \
|
||||
)";
|
||||
|
||||
@ -36,21 +37,23 @@ const LIST_CHANNELS: &'static str =
|
||||
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) \
|
||||
const ADD_ARTICLE: &'static str =
|
||||
"INSERT INTO Articles (channel_id, title, description, link, pub_date, last_update) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP) \
|
||||
ON CONFLICT(link) DO UPDATE SET pub=?5, last_update=CURRENT_TIMESTAMP";
|
||||
ON CONFLICT(link) DO UPDATE SET pub_date=?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";
|
||||
"SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles WHERE channel_id = ?1";
|
||||
|
||||
const GET_ARTICLE: &'static str =
|
||||
"SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles WHERE link = ?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";
|
||||
"SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles";
|
||||
|
||||
const MARK_ARTICLE_READ: &'static str =
|
||||
"UPDATE Articles SET unread = FALSE WHERE id = ?1";
|
||||
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";
|
||||
const MARK_ARTICLE_UNREAD: &'static str = "UPDATE Articles SET unread = TRUE WHERE id = ?1";
|
||||
|
||||
pub struct Db {
|
||||
connection: Connection,
|
||||
@ -62,6 +65,18 @@ pub struct RssChannelD {
|
||||
pub link: String,
|
||||
pub description: String,
|
||||
pub last_update: OffsetDateTime,
|
||||
pub articles: Vec<RssArticleD>,
|
||||
}
|
||||
|
||||
pub struct RssArticleD {
|
||||
pub id: i64,
|
||||
pub channel_id: i64,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub link: String,
|
||||
pub pub_date: Option<OffsetDateTime>,
|
||||
pub last_update: Option<OffsetDateTime>,
|
||||
pub unread: bool,
|
||||
}
|
||||
|
||||
macro_rules! schema_sql {
|
||||
@ -100,11 +115,15 @@ impl Db {
|
||||
}
|
||||
|
||||
pub fn get_channel(&self, link: &str) -> Result<RssChannelD> {
|
||||
self.connection
|
||||
let mut channel = self
|
||||
.connection
|
||||
.query_row(GET_CHANNEL, (link,), Db::map_rsschanneld)
|
||||
.map_err(|e| {
|
||||
TrsError::SqlError(e, format!("Failed to retrieve channel with link {}", link))
|
||||
})
|
||||
})?;
|
||||
|
||||
channel.articles = self.list_articles_by_channel(channel.id)?;
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
pub fn add_channel(&self, channel: &RssChannel) -> Result<RssChannelD> {
|
||||
@ -115,24 +134,17 @@ impl Db {
|
||||
)
|
||||
.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)))?;
|
||||
let mut inserted_channel = self.get_channel(&channel.link).map_err(|e| {
|
||||
TrsError::Error(format!("Failed to retrieve channel after adding: {}", e))
|
||||
})?;
|
||||
|
||||
let mut articles = Vec::new();
|
||||
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()))?;
|
||||
let article = self.add_article(inserted_channel.id, article)?;
|
||||
articles.push(article);
|
||||
}
|
||||
|
||||
inserted_channel.articles = articles;
|
||||
Ok(inserted_channel)
|
||||
}
|
||||
|
||||
@ -144,7 +156,7 @@ impl Db {
|
||||
}
|
||||
|
||||
pub fn list_channels(&self, limit: u32) -> Result<Vec<RssChannelD>> {
|
||||
let channels = self
|
||||
let mut channels = self
|
||||
.connection
|
||||
.prepare(LIST_CHANNELS)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to prepare query".to_string()))?
|
||||
@ -153,9 +165,86 @@ impl Db {
|
||||
.map(|r| r.unwrap())
|
||||
.collect::<Vec<RssChannelD>>();
|
||||
|
||||
let mut articles = self.list_articles()?;
|
||||
|
||||
for channel in &mut channels {
|
||||
for article in &mut articles {
|
||||
if article.channel_id == channel.id {
|
||||
let copied = std::mem::replace(article, RssArticleD::dummy());
|
||||
channel.articles.push(copied);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(channels)
|
||||
}
|
||||
|
||||
pub fn mark_article_read(&self, id: i64) -> Result<usize> {
|
||||
self.connection
|
||||
.execute(MARK_ARTICLE_READ, (id,))
|
||||
.map(|rows| rows as usize)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to mark article as read".to_string()))
|
||||
}
|
||||
|
||||
pub fn mark_article_unread(&self, id: i64) -> Result<usize> {
|
||||
self.connection
|
||||
.execute(MARK_ARTICLE_UNREAD, (id,))
|
||||
.map(|rows| rows as usize)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to mark article as unread".to_string()))
|
||||
}
|
||||
|
||||
fn add_article(&self, channel_id: i64, article: &RssArticle) -> Result<RssArticleD> {
|
||||
self.connection
|
||||
.execute(
|
||||
ADD_ARTICLE,
|
||||
(
|
||||
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()))?;
|
||||
|
||||
self.get_article(&article.link)
|
||||
.map_err(|e| TrsError::Error(format!("Failed to retrieve article after adding: {}", e)))
|
||||
}
|
||||
|
||||
fn get_article(&self, link: &str) -> Result<RssArticleD> {
|
||||
self.connection
|
||||
.query_row(GET_ARTICLE, (link,), Db::map_rssarticled)
|
||||
.map_err(|e| {
|
||||
TrsError::SqlError(e, format!("Failed to retrieve article with link {}", link))
|
||||
})
|
||||
}
|
||||
|
||||
fn list_articles_by_channel(&self, channel_id: i64) -> Result<Vec<RssArticleD>> {
|
||||
let articles = self
|
||||
.connection
|
||||
.prepare(GET_ARTICLES_BY_CHANNEL)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to prepare query".to_string()))?
|
||||
.query_map((channel_id,), Db::map_rssarticled)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to list articles".to_string()))?
|
||||
.map(|r| r.unwrap())
|
||||
.collect::<Vec<RssArticleD>>();
|
||||
|
||||
Ok(articles)
|
||||
}
|
||||
|
||||
fn list_articles(&self) -> Result<Vec<RssArticleD>> {
|
||||
let articles = self
|
||||
.connection
|
||||
.prepare(LIST_ARTICLES)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to prepare query".to_string()))?
|
||||
.query_map([], Db::map_rssarticled)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to list articles".to_string()))?
|
||||
.map(|r| r.unwrap())
|
||||
.collect::<Vec<RssArticleD>>();
|
||||
|
||||
Ok(articles)
|
||||
}
|
||||
|
||||
fn map_rsschanneld(row: &rusqlite::Row) -> std::result::Result<RssChannelD, rusqlite::Error> {
|
||||
Ok(RssChannelD::new(
|
||||
row.get(0)?,
|
||||
@ -163,8 +252,22 @@ impl Db {
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
Vec::new(),
|
||||
))
|
||||
}
|
||||
|
||||
fn map_rssarticled(row: &rusqlite::Row) -> std::result::Result<RssArticleD, rusqlite::Error> {
|
||||
Ok(RssArticleD {
|
||||
id: row.get(0)?,
|
||||
channel_id: row.get(1)?,
|
||||
title: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
link: row.get(4)?,
|
||||
pub_date: row.get(5).ok(),
|
||||
last_update: row.get(6).ok(),
|
||||
unread: row.get(7)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RssChannelD {
|
||||
@ -174,6 +277,7 @@ impl RssChannelD {
|
||||
link: String,
|
||||
description: String,
|
||||
last_update: OffsetDateTime,
|
||||
articles: Vec<RssArticleD>,
|
||||
) -> Self {
|
||||
RssChannelD {
|
||||
id,
|
||||
@ -181,6 +285,22 @@ impl RssChannelD {
|
||||
link,
|
||||
description,
|
||||
last_update,
|
||||
articles,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RssArticleD {
|
||||
fn dummy() -> Self {
|
||||
RssArticleD {
|
||||
id: -1,
|
||||
channel_id: 0,
|
||||
title: String::new(),
|
||||
description: String::new(),
|
||||
link: String::new(),
|
||||
pub_date: None,
|
||||
last_update: None,
|
||||
unread: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user