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 {
|
pub enum TrsSubCommand {
|
||||||
AddChannel(AddChannelArgs),
|
AddChannel(AddChannelArgs),
|
||||||
ListChannels(ListChannelArgs),
|
ListChannels(ListChannelArgs),
|
||||||
|
GetArticles(GetArticlesArgs),
|
||||||
RemoveChannel(RemoveChannelArgs),
|
RemoveChannel(RemoveChannelArgs),
|
||||||
|
MarkRead(MarkReadArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new RSS channel
|
/// Add a new RSS channel
|
||||||
@ -33,6 +35,32 @@ pub struct ListChannelArgs {
|
|||||||
pub limit: Option<u32>,
|
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
|
/// Delete an RSS channel
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
#[argh(subcommand, name = "remove")]
|
#[argh(subcommand, name = "remove")]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
args::{AddChannelArgs, ListChannelArgs, RemoveChannelArgs},
|
args::{self, AddChannelArgs, ListChannelArgs, RemoveChannelArgs},
|
||||||
error::TrsError,
|
error::TrsError,
|
||||||
parser,
|
parser,
|
||||||
persistence::{Db, RssChannelD},
|
persistence::{Db, RssChannelD},
|
||||||
@ -51,3 +51,34 @@ pub fn list_channels(
|
|||||||
pub fn remove_channel(ctx: &mut TrsEnv, args: &RemoveChannelArgs) -> Result<(), TrsError> {
|
pub fn remove_channel(ctx: &mut TrsEnv, args: &RemoveChannelArgs) -> Result<(), TrsError> {
|
||||||
ctx.db.remove_channel(args.id).map(|_| ())
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
TrsSubCommand::RemoveChannel(args) => commands::remove_channel(&mut ctx, &args),
|
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::Result;
|
||||||
use crate::error::TrsError;
|
use crate::error::TrsError;
|
||||||
|
use crate::parser::RssArticle;
|
||||||
use crate::parser::RssChannel;
|
use crate::parser::RssChannel;
|
||||||
|
|
||||||
const SCHEMA_CHANNELS: &'static str = "CREATE TABLE IF NOT EXISTS Channels ( \
|
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, \
|
title TEXT NOT NULL, \
|
||||||
description TEXT, \
|
description TEXT, \
|
||||||
link TEXT NOT NULL UNIQUE, \
|
link TEXT NOT NULL UNIQUE, \
|
||||||
pub TIMESTAMP, \
|
pub_date TIMESTAMP, \
|
||||||
last_update TIMESTAMP DEFAULT CURRENT_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 \
|
FOREIGN KEY(channel_id) REFERENCES Channels(id) ON DELETE CASCADE \
|
||||||
)";
|
)";
|
||||||
|
|
||||||
@ -36,21 +37,23 @@ const LIST_CHANNELS: &'static str =
|
|||||||
const GET_CHANNEL: &'static str =
|
const GET_CHANNEL: &'static str =
|
||||||
"SELECT id, name, link, description, last_update FROM Channels WHERE link = ?1";
|
"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) \
|
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 =
|
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 =
|
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 =
|
const MARK_ARTICLE_READ: &'static str = "UPDATE Articles SET unread = FALSE WHERE id = ?1";
|
||||||
"UPDATE Articles SET unread = FALSE WHERE id = ?1";
|
|
||||||
|
|
||||||
const MARK_ARTICLE_UNREAD: &'static str =
|
const MARK_ARTICLE_UNREAD: &'static str = "UPDATE Articles SET unread = TRUE WHERE id = ?1";
|
||||||
"UPDATE Articles SET unread = TRUE WHERE id = ?1";
|
|
||||||
|
|
||||||
pub struct Db {
|
pub struct Db {
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
@ -62,6 +65,18 @@ pub struct RssChannelD {
|
|||||||
pub link: String,
|
pub link: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub last_update: OffsetDateTime,
|
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 {
|
macro_rules! schema_sql {
|
||||||
@ -100,11 +115,15 @@ impl Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_channel(&self, link: &str) -> Result<RssChannelD> {
|
pub fn get_channel(&self, link: &str) -> Result<RssChannelD> {
|
||||||
self.connection
|
let mut channel = self
|
||||||
|
.connection
|
||||||
.query_row(GET_CHANNEL, (link,), Db::map_rsschanneld)
|
.query_row(GET_CHANNEL, (link,), Db::map_rsschanneld)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
TrsError::SqlError(e, format!("Failed to retrieve channel with link {}", link))
|
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> {
|
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()))?;
|
.map_err(|e| TrsError::SqlError(e, "Failed to add channel".to_string()))?;
|
||||||
|
|
||||||
let inserted_channel = self.get_channel(&channel.link)
|
let mut inserted_channel = self.get_channel(&channel.link).map_err(|e| {
|
||||||
.map_err(|e| TrsError::Error(format!("Failed to retrieve channel after adding: {}", e)))?;
|
TrsError::Error(format!("Failed to retrieve channel after adding: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut articles = Vec::new();
|
||||||
for article in &channel.articles {
|
for article in &channel.articles {
|
||||||
self.connection
|
let article = self.add_article(inserted_channel.id, article)?;
|
||||||
.execute(
|
articles.push(article);
|
||||||
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()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inserted_channel.articles = articles;
|
||||||
Ok(inserted_channel)
|
Ok(inserted_channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +156,7 @@ impl Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_channels(&self, limit: u32) -> Result<Vec<RssChannelD>> {
|
pub fn list_channels(&self, limit: u32) -> Result<Vec<RssChannelD>> {
|
||||||
let channels = self
|
let mut channels = self
|
||||||
.connection
|
.connection
|
||||||
.prepare(LIST_CHANNELS)
|
.prepare(LIST_CHANNELS)
|
||||||
.map_err(|e| TrsError::SqlError(e, "Failed to prepare query".to_string()))?
|
.map_err(|e| TrsError::SqlError(e, "Failed to prepare query".to_string()))?
|
||||||
@ -153,9 +165,86 @@ impl Db {
|
|||||||
.map(|r| r.unwrap())
|
.map(|r| r.unwrap())
|
||||||
.collect::<Vec<RssChannelD>>();
|
.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)
|
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> {
|
fn map_rsschanneld(row: &rusqlite::Row) -> std::result::Result<RssChannelD, rusqlite::Error> {
|
||||||
Ok(RssChannelD::new(
|
Ok(RssChannelD::new(
|
||||||
row.get(0)?,
|
row.get(0)?,
|
||||||
@ -163,8 +252,22 @@ impl Db {
|
|||||||
row.get(2)?,
|
row.get(2)?,
|
||||||
row.get(3)?,
|
row.get(3)?,
|
||||||
row.get(4)?,
|
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 {
|
impl RssChannelD {
|
||||||
@ -174,6 +277,7 @@ impl RssChannelD {
|
|||||||
link: String,
|
link: String,
|
||||||
description: String,
|
description: String,
|
||||||
last_update: OffsetDateTime,
|
last_update: OffsetDateTime,
|
||||||
|
articles: Vec<RssArticleD>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
RssChannelD {
|
RssChannelD {
|
||||||
id,
|
id,
|
||||||
@ -181,6 +285,22 @@ impl RssChannelD {
|
|||||||
link,
|
link,
|
||||||
description,
|
description,
|
||||||
last_update,
|
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