Initial split into workspaces in order to be more flexible.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "hammond-data"
|
||||
version = "0.1.0"
|
||||
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
rfc822_sanitizer = "0.3.0"
|
||||
rayon = "0.8.2"
|
||||
regex = "0.2"
|
||||
error-chain = "0.11.0"
|
||||
structopt = "0.1.0"
|
||||
structopt-derive = "0.1.0"
|
||||
log = "0.3.8"
|
||||
loggerv = "0.3.0"
|
||||
reqwest = "0.7.3"
|
||||
hyper = "0.11.2"
|
||||
diesel = { version = "0.16.0", features = ["sqlite", "deprecated-time", "chrono"] }
|
||||
diesel_codegen = { version = "0.16.0", features = ["sqlite"] }
|
||||
time = "0.1.38"
|
||||
xdg = "2.1.0"
|
||||
lazy_static = "0.2.8"
|
||||
chrono = "0.4.0"
|
||||
rss = { version = "1.1.0", features = ["from_url"]}
|
||||
# overide diesel's dependancy that would otherwise turn a dotenv feature of
|
||||
# that rss depends upon
|
||||
dotenv = "*"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Drop Table episode;
|
||||
Drop Table podcast;
|
||||
Drop Table source;
|
||||
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE `source` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
`uri` TEXT NOT NULL UNIQUE,
|
||||
`last_modified` TEXT,
|
||||
`http_etag` TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE `episode` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
`title` TEXT,
|
||||
`uri` TEXT UNIQUE,
|
||||
`local_uri` TEXT,
|
||||
`description` TEXT,
|
||||
`published_date` TEXT ,
|
||||
`epoch` INTEGER NOT NULL DEFAULT 0,
|
||||
`length` INTEGER,
|
||||
`guid` TEXT,
|
||||
`podcast_id` INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `podcast` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
`title` TEXT NOT NULL UNIQUE,
|
||||
`link` TEXT NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`image_uri` TEXT,
|
||||
`source_id` INTEGER NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,91 @@
|
||||
use diesel::prelude::*;
|
||||
use models::{Episode, Podcast, Source};
|
||||
|
||||
pub fn get_sources(con: &SqliteConnection) -> QueryResult<Vec<Source>> {
|
||||
use schema::source::dsl::*;
|
||||
|
||||
let s = source.load::<Source>(con);
|
||||
s
|
||||
}
|
||||
|
||||
pub fn get_podcasts(con: &SqliteConnection) -> QueryResult<Vec<Podcast>> {
|
||||
use schema::podcast::dsl::*;
|
||||
|
||||
let pds = podcast.load::<Podcast>(con);
|
||||
pds
|
||||
}
|
||||
|
||||
// Maybe later.
|
||||
// pub fn get_podcasts_ids(con: &SqliteConnection) -> QueryResult<Vec<i32>> {
|
||||
// use schema::podcast::dsl::*;
|
||||
|
||||
// let pds = podcast.select(id).load::<i32>(con);
|
||||
// pds
|
||||
// }
|
||||
|
||||
pub fn get_episodes(con: &SqliteConnection) -> QueryResult<Vec<Episode>> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
let eps = episode.order(epoch.desc()).load::<Episode>(con);
|
||||
eps
|
||||
}
|
||||
|
||||
pub fn get_episodes_with_limit(con: &SqliteConnection, limit: u32) -> QueryResult<Vec<Episode>> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
let eps = episode
|
||||
.order(epoch.desc())
|
||||
.limit(limit as i64)
|
||||
.load::<Episode>(con);
|
||||
eps
|
||||
}
|
||||
|
||||
pub fn get_podcast(con: &SqliteConnection, parent: &Source) -> QueryResult<Vec<Podcast>> {
|
||||
let pd = Podcast::belonging_to(parent).load::<Podcast>(con);
|
||||
// debug!("Returned Podcasts:\n{:?}", pds);
|
||||
pd
|
||||
}
|
||||
|
||||
pub fn get_pd_episodes(con: &SqliteConnection, parent: &Podcast) -> QueryResult<Vec<Episode>> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
let eps = Episode::belonging_to(parent)
|
||||
.order(epoch.desc())
|
||||
.load::<Episode>(con);
|
||||
eps
|
||||
}
|
||||
|
||||
pub fn get_pd_episodes_limit(
|
||||
con: &SqliteConnection,
|
||||
parent: &Podcast,
|
||||
limit: u32,
|
||||
) -> QueryResult<Vec<Episode>> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
let eps = Episode::belonging_to(parent)
|
||||
.order(epoch.desc())
|
||||
.limit(limit as i64)
|
||||
.load::<Episode>(con);
|
||||
eps
|
||||
}
|
||||
|
||||
pub fn load_source(con: &SqliteConnection, uri_: &str) -> QueryResult<Source> {
|
||||
use schema::source::dsl::*;
|
||||
|
||||
let s = source.filter(uri.eq(uri_)).get_result::<Source>(con);
|
||||
s
|
||||
}
|
||||
|
||||
pub fn load_podcast(con: &SqliteConnection, title_: &str) -> QueryResult<Podcast> {
|
||||
use schema::podcast::dsl::*;
|
||||
|
||||
let pd = podcast.filter(title.eq(title_)).get_result::<Podcast>(con);
|
||||
pd
|
||||
}
|
||||
|
||||
pub fn load_episode(con: &SqliteConnection, uri_: &str) -> QueryResult<Episode> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
let ep = episode.filter(uri.eq(uri_)).get_result::<Episode>(con);
|
||||
ep
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#![recursion_limit = "1024"]
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate loggerv;
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_codegen;
|
||||
|
||||
extern crate chrono;
|
||||
extern crate hyper;
|
||||
extern crate rayon;
|
||||
extern crate regex;
|
||||
extern crate reqwest;
|
||||
extern crate rfc822_sanitizer;
|
||||
extern crate rss;
|
||||
extern crate time;
|
||||
extern crate xdg;
|
||||
|
||||
pub mod dbqueries;
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
|
||||
pub mod errors {
|
||||
|
||||
use reqwest;
|
||||
use rss;
|
||||
use chrono;
|
||||
use hyper;
|
||||
use time;
|
||||
use diesel::migrations::RunMigrationsError;
|
||||
use diesel::result;
|
||||
use regex;
|
||||
|
||||
use std::io;
|
||||
// use std::option;
|
||||
// use std::sync;
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
ReqError(reqwest::Error);
|
||||
IoError(io::Error);
|
||||
Log(::log::SetLoggerError);
|
||||
MigrationError(RunMigrationsError);
|
||||
RSSError(rss::Error);
|
||||
DieselResultError(result::Error);
|
||||
ChronoError(chrono::ParseError);
|
||||
DurationError(time::OutOfRangeError);
|
||||
HyperError(hyper::error::Error);
|
||||
RegexError(regex::Error);
|
||||
// NoneError(option::NoneError);
|
||||
// MutexPoison(sync::PoisonError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use errors::*;
|
||||
use diesel::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
embed_migrations!("migrations/");
|
||||
|
||||
lazy_static!{
|
||||
|
||||
static ref HAMMOND_XDG: xdg::BaseDirectories = {
|
||||
xdg::BaseDirectories::with_prefix("Hammond").unwrap()
|
||||
};
|
||||
|
||||
static ref HAMMOND_DATA: PathBuf = {
|
||||
HAMMOND_XDG.create_data_directory(HAMMOND_XDG.get_data_home()).unwrap()
|
||||
};
|
||||
|
||||
static ref _HAMMOND_CONFIG: PathBuf = {
|
||||
HAMMOND_XDG.create_config_directory(HAMMOND_XDG.get_config_home()).unwrap()
|
||||
};
|
||||
|
||||
static ref _HAMMOND_CACHE: PathBuf = {
|
||||
HAMMOND_XDG.create_cache_directory(HAMMOND_XDG.get_cache_home()).unwrap()
|
||||
};
|
||||
|
||||
static ref DB_PATH: PathBuf = {
|
||||
// Ensure that xdg_data is created.
|
||||
&HAMMOND_DATA;
|
||||
|
||||
HAMMOND_XDG.place_data_file("hammond.db").unwrap()
|
||||
};
|
||||
|
||||
pub static ref DL_DIR: PathBuf = {
|
||||
&HAMMOND_DATA;
|
||||
HAMMOND_XDG.create_data_directory("Downloads").unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: REFACTOR
|
||||
pub fn init() -> Result<()> {
|
||||
let conn = establish_connection();
|
||||
// embedded_migrations::run(&conn)?;
|
||||
embedded_migrations::run_with_output(&conn, &mut std::io::stdout())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_migration_on(connection: &SqliteConnection) -> Result<()> {
|
||||
embedded_migrations::run_with_output(connection, &mut std::io::stdout())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn establish_connection() -> SqliteConnection {
|
||||
let database_url = DB_PATH.to_str().unwrap();
|
||||
// let database_url = &String::from(".random/foo.db");
|
||||
SqliteConnection::establish(database_url)
|
||||
.expect(&format!("Error connecting to {}", database_url))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
use reqwest;
|
||||
use rss::Channel;
|
||||
use diesel::SaveChangesDsl;
|
||||
use SqliteConnection;
|
||||
use reqwest::header::{ETag, LastModified};
|
||||
|
||||
use schema::{episode, podcast, source};
|
||||
use errors::*;
|
||||
|
||||
#[derive(Queryable, Identifiable, AsChangeset, Associations)]
|
||||
#[table_name = "episode"]
|
||||
#[belongs_to(Podcast, foreign_key = "podcast_id")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Episode {
|
||||
id: i32,
|
||||
title: Option<String>,
|
||||
uri: String,
|
||||
local_uri: Option<String>,
|
||||
description: Option<String>,
|
||||
published_date: Option<String>,
|
||||
epoch: i32,
|
||||
length: Option<i32>,
|
||||
guid: Option<String>,
|
||||
podcast_id: i32,
|
||||
}
|
||||
|
||||
impl Episode {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn title(&self) -> Option<&str> {
|
||||
self.title.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, value: Option<&str>) {
|
||||
self.title = value.map(|x| x.to_string());
|
||||
}
|
||||
|
||||
/// uri is guaranted to exist based on the db rules
|
||||
pub fn uri(&self) -> &str {
|
||||
self.uri.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_uri(&mut self, value: &str) {
|
||||
self.uri = value.to_string();
|
||||
}
|
||||
|
||||
pub fn local_uri(&self) -> Option<&str> {
|
||||
self.local_uri.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn set_local_uri(&mut self, value: Option<&str>) {
|
||||
self.local_uri = value.map(|x| x.to_string());
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
self.description.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn set_description(&mut self, value: Option<&str>) {
|
||||
self.description = value.map(|x| x.to_string());
|
||||
}
|
||||
|
||||
pub fn published_date(&self) -> Option<&str> {
|
||||
self.published_date.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
// FIXME: make the setter accept &str again
|
||||
pub fn set_published_date(&mut self, value: Option<String>) {
|
||||
// self.published_date = value.map(|x| x.to_string());
|
||||
self.published_date = value;
|
||||
}
|
||||
|
||||
pub fn guid(&self) -> Option<&str> {
|
||||
self.guid.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn set_guid(&mut self, value: Option<&str>) {
|
||||
self.guid = value.map(|x| x.to_string());
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> i32 {
|
||||
self.epoch
|
||||
}
|
||||
|
||||
pub fn set_epoch(&mut self, value: i32) {
|
||||
self.epoch = value;
|
||||
}
|
||||
|
||||
pub fn length(&self) -> Option<i32> {
|
||||
self.length
|
||||
}
|
||||
|
||||
pub fn set_length(&mut self, value: Option<i32>) {
|
||||
self.length = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Queryable, Identifiable, AsChangeset, Associations)]
|
||||
#[belongs_to(Source, foreign_key = "source_id")]
|
||||
#[table_name = "podcast"]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Podcast {
|
||||
id: i32,
|
||||
title: String,
|
||||
link: String,
|
||||
description: String,
|
||||
image_uri: Option<String>,
|
||||
source_id: i32,
|
||||
}
|
||||
|
||||
impl Podcast {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn link(&self) -> &str {
|
||||
&self.link
|
||||
}
|
||||
|
||||
pub fn set_link(&mut self, value: &str) {
|
||||
self.link = value.to_string();
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn set_description(&mut self, value: &str) {
|
||||
self.description = value.to_string();
|
||||
}
|
||||
|
||||
pub fn image_uri(&self) -> Option<&str> {
|
||||
self.image_uri.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn set_image_uri(&mut self, value: Option<&str>) {
|
||||
self.image_uri = value.map(|x| x.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Queryable, Identifiable, AsChangeset)]
|
||||
#[table_name = "source"]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Source {
|
||||
id: i32,
|
||||
uri: String,
|
||||
last_modified: Option<String>,
|
||||
http_etag: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> Source {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> &str {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
pub fn last_modified(&self) -> Option<&str> {
|
||||
self.last_modified.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn set_last_modified(&mut self, value: Option<&str>) {
|
||||
self.last_modified = value.map(|x| x.to_string());
|
||||
}
|
||||
|
||||
pub fn http_etag(&self) -> Option<&str> {
|
||||
self.http_etag.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn set_http_etag(&mut self, value: Option<&str>) {
|
||||
self.http_etag = value.map(|x| x.to_string());
|
||||
}
|
||||
|
||||
/// Extract Etag and LastModifier from req, and update self and the
|
||||
/// corresponding db row.
|
||||
pub fn update_etag(&mut self, con: &SqliteConnection, req: &reqwest::Response) -> Result<()> {
|
||||
let headers = req.headers();
|
||||
|
||||
// let etag = headers.get_raw("ETag").unwrap();
|
||||
let etag = headers.get::<ETag>();
|
||||
let lmod = headers.get::<LastModified>();
|
||||
|
||||
// FIXME: This dsnt work most of the time apparently
|
||||
if self.http_etag() != etag.map(|x| x.tag())
|
||||
|| self.last_modified != lmod.map(|x| format!("{}", x))
|
||||
{
|
||||
self.http_etag = etag.map(|x| x.tag().to_string().to_owned());
|
||||
self.last_modified = lmod.map(|x| format!("{}", x));
|
||||
self.save_changes::<Source>(con)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove pub fields and add setters.
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "source"]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewSource<'a> {
|
||||
pub uri: &'a str,
|
||||
pub last_modified: Option<&'a str>,
|
||||
pub http_etag: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> NewSource<'a> {
|
||||
pub fn new_with_uri(uri: &'a str) -> NewSource {
|
||||
NewSource {
|
||||
uri,
|
||||
last_modified: None,
|
||||
http_etag: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "episode"]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewEpisode<'a> {
|
||||
pub title: Option<&'a str>,
|
||||
pub uri: Option<&'a str>,
|
||||
pub local_uri: Option<&'a str>,
|
||||
pub description: Option<&'a str>,
|
||||
// FIXME: make it &str again
|
||||
pub published_date: Option<String>,
|
||||
pub length: Option<i32>,
|
||||
pub guid: Option<&'a str>,
|
||||
pub epoch: i32,
|
||||
pub podcast_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "podcast"]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewPodcast {
|
||||
pub title: String,
|
||||
pub link: String,
|
||||
pub description: String,
|
||||
pub image_uri: Option<String>,
|
||||
pub source_id: i32,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
table! {
|
||||
episode (id) {
|
||||
id -> Integer,
|
||||
title -> Nullable<Text>,
|
||||
uri -> Text,
|
||||
local_uri -> Nullable<Text>,
|
||||
description -> Nullable<Text>,
|
||||
published_date -> Nullable<Text>,
|
||||
epoch -> Integer,
|
||||
length -> Nullable<Integer>,
|
||||
guid -> Nullable<Text>,
|
||||
podcast_id -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
podcast (id) {
|
||||
id -> Integer,
|
||||
title -> Text,
|
||||
link -> Text,
|
||||
description -> Text,
|
||||
image_uri -> Nullable<Text>,
|
||||
source_id -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
source (id) {
|
||||
id -> Integer,
|
||||
uri -> Text,
|
||||
last_modified -> Nullable<Text>,
|
||||
http_etag -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user