Initial commit
This commit is contained in:
199
src/main.rs
Normal file
199
src/main.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use actix_web::{get, post, web, http, App, HttpResponse, HttpServer, Responder};
|
||||
use askama_actix::{Template, TemplateIntoResponse};
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use simple_logger::SimpleLogger;
|
||||
|
||||
const DATA_DIR: &'static str = "/home/manuel/wolke/Projects/tinypod/data";
|
||||
const DB_DIR: &'static str = "data";
|
||||
const DB_FILE_EXT: &'static str = ".json";
|
||||
|
||||
mod date_format {
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde::{Serializer, Deserialize, Deserializer};
|
||||
|
||||
pub fn serialize<S>(date: &DateTime<FixedOffset>, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
|
||||
serializer.serialize_str(&date.to_rfc3339())
|
||||
}
|
||||
|
||||
|
||||
pub fn deserialize<'de, D> (deserializer: D) -> Result<DateTime<FixedOffset>, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let mut res = DateTime::parse_from_rfc2822(&s).map_err(serde::de::Error::custom);
|
||||
if res.is_err() {
|
||||
res = DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Episode {
|
||||
title: String,
|
||||
video_url: String,
|
||||
guid: String,
|
||||
#[serde(with="date_format")]
|
||||
pub_date: DateTime<FixedOffset>
|
||||
}
|
||||
|
||||
impl Episode {
|
||||
fn new() -> Episode {
|
||||
Episode{title: String::new(), video_url: String::new(), guid: String::new(), pub_date: chrono::Utc::now().into()}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Podcast {
|
||||
title: String,
|
||||
logo_url: Option<String>,
|
||||
homepage: String,
|
||||
episodes: Vec<Episode>
|
||||
}
|
||||
|
||||
impl Podcast {
|
||||
fn build_path<S: AsRef<str>>(name: S) -> PathBuf {
|
||||
let filename = String::from(name.as_ref()) + DB_FILE_EXT;
|
||||
let mut path = PathBuf::from(DATA_DIR);
|
||||
path.push(DB_DIR);
|
||||
path.push(filename);
|
||||
path
|
||||
}
|
||||
|
||||
fn db_load<S: AsRef<str>>(name: S) -> Result<Podcast, std::io::Error> {
|
||||
let file = BufReader::new(File::open(Self::build_path(name))?);
|
||||
let podcast = Podcast::deserialize(serde_json::from_reader::<_, serde_json::Value>(file)?)?;
|
||||
Ok(podcast)
|
||||
}
|
||||
|
||||
fn db_store<S: AsRef<str>>(&self, name: S) -> Result<(), std::io::Error> {
|
||||
let file = BufWriter::new(File::create(Self::build_path(name))?);
|
||||
serde_json::to_writer_pretty(file, &self)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "feed.xml", escape="xml")]
|
||||
// TODO Change ext to rss
|
||||
struct FeedTemplate<'a> {
|
||||
podcast: &'a Podcast
|
||||
}
|
||||
|
||||
#[get("/feed/{name}")]
|
||||
async fn feed(web::Path(name): web::Path<String>) -> impl Responder {
|
||||
let podcast = match Podcast::db_load(&name) {
|
||||
Ok(podcast) => podcast,
|
||||
Err(e) => {
|
||||
// TODO Log error
|
||||
return HttpResponse::InternalServerError().body(e.to_string());
|
||||
}
|
||||
};
|
||||
FeedTemplate{podcast: &podcast}.into_response().unwrap()
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "edit_podcast.html")]
|
||||
struct EditPodcastTemplate<'a> {
|
||||
podcast: &'a Podcast
|
||||
}
|
||||
|
||||
#[get("/edit/{name}/")]
|
||||
async fn edit_podcast(web::Path(name): web::Path<String>) -> impl Responder {
|
||||
let podcast = match Podcast::db_load(&name) {
|
||||
Ok(podcast) => podcast,
|
||||
Err(e) => {
|
||||
// TODO Log error
|
||||
return HttpResponse::InternalServerError().body(e.to_string());
|
||||
}
|
||||
};
|
||||
EditPodcastTemplate{podcast: &podcast}.into_response().unwrap()
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "edit_episode.html")]
|
||||
struct EditEpisodeTemplate<'a> {
|
||||
episode: &'a Episode
|
||||
}
|
||||
|
||||
#[get("/edit/{podcast_name}/edit/{episode_number}")]
|
||||
async fn edit_episode(web::Path((podcast_name, episode_number)): web::Path<(String, usize)>) -> impl Responder {
|
||||
let podcast = match Podcast::db_load(&podcast_name) {
|
||||
Ok(podcast) => podcast,
|
||||
Err(e) => {
|
||||
// TODO Log error
|
||||
return HttpResponse::InternalServerError().body(e.to_string());
|
||||
}
|
||||
};
|
||||
// TODO Check episode_number out of bounds
|
||||
EditEpisodeTemplate{episode: &podcast.episodes[episode_number]}.into_response().unwrap()
|
||||
}
|
||||
|
||||
#[post("/edit/{podcast_name}/edit/{episode_number}")]
|
||||
async fn post_edit_episode(web::Path((podcast_name, episode_number)): web::Path<(String, usize)>, form: web::Form<Episode>) -> impl Responder {
|
||||
let mut podcast = match Podcast::db_load(&podcast_name) {
|
||||
Ok(podcast) => podcast,
|
||||
Err(e) => {
|
||||
// TODO Log error
|
||||
return HttpResponse::InternalServerError().body(e.to_string());
|
||||
}
|
||||
};
|
||||
podcast.episodes[episode_number] = form.into_inner();
|
||||
match podcast.db_store(&podcast_name) {
|
||||
Ok(_) => {},
|
||||
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
|
||||
}
|
||||
HttpResponse::Found().header(http::header::LOCATION, format!("/edit/{}/", &podcast_name)).finish()
|
||||
}
|
||||
|
||||
#[get("/edit/{podcast_name}/new")]
|
||||
async fn new_episode(web::Path(podcast_name): web::Path<String>) -> impl Responder {
|
||||
// TODO This checks if the podcast exists. Build a slimmer test
|
||||
match Podcast::db_load(&podcast_name) {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
// TODO Log error
|
||||
return HttpResponse::InternalServerError().body(e.to_string());
|
||||
}
|
||||
};
|
||||
// TODO Check episode_number out of bounds
|
||||
EditEpisodeTemplate{episode: &Episode::new()}.into_response().unwrap()
|
||||
}
|
||||
|
||||
#[post("/edit/{podcast_name}/new")]
|
||||
async fn post_new_episode(web::Path(podcast_name): web::Path<String>, form: web::Form<Episode>) -> impl Responder {
|
||||
let mut podcast = match Podcast::db_load(&podcast_name) {
|
||||
Ok(podcast) => podcast,
|
||||
Err(e) => {
|
||||
// TODO Log error
|
||||
return HttpResponse::InternalServerError().body(e.to_string());
|
||||
}
|
||||
};
|
||||
podcast.episodes.insert(0, form.into_inner());
|
||||
match podcast.db_store(&podcast_name) {
|
||||
Ok(_) => {},
|
||||
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
|
||||
}
|
||||
HttpResponse::Found().header(http::header::LOCATION, format!("/edit/{}/", &podcast_name)).finish()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
SimpleLogger::new().init().unwrap();
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(feed)
|
||||
.service(edit_podcast)
|
||||
.service(edit_episode)
|
||||
.service(post_edit_episode)
|
||||
.service(new_episode)
|
||||
.service(post_new_episode)
|
||||
})
|
||||
.bind("0.0.0.0:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
Reference in New Issue
Block a user