move podcasts to within nested podcasts folder (significant breaking change)
This commit is contained in:
@@ -7,8 +7,10 @@ use std::collections::{HashMap, BTreeMap, HashSet};
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use sanitise_file_name::sanitise;
|
use sanitise_file_name::sanitise;
|
||||||
|
|
||||||
|
use crate::folders;
|
||||||
use crate::rss;
|
use crate::rss;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||||
pub(crate) struct Specification<'a> {
|
pub(crate) struct Specification<'a> {
|
||||||
files: HashMap<Cow<'a, str>, Cow<'a, path::Path>>,
|
files: HashMap<Cow<'a, str>, Cow<'a, path::Path>>,
|
||||||
@@ -98,10 +100,10 @@ pub(crate) fn update_podcast(
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
|
||||||
// Create output directory
|
// Create output directory
|
||||||
let output = root.join(sanitise(&alias));
|
let output = folders::podcast_folder(root, alias);
|
||||||
if !output.exists() {
|
if !output.exists() {
|
||||||
fs::create_dir(&output)
|
fs::create_dir_all(&output)
|
||||||
.with_context(|| format!("failed to create output directory for podcast {}", alias))?;
|
.context(format!("failed to create output directory for podcast {}", alias))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(r#"[info] scanning feed for "{}""#, alias);
|
println!(r#"[info] scanning feed for "{}""#, alias);
|
||||||
@@ -115,7 +117,7 @@ pub(crate) fn update_podcast(
|
|||||||
.with_header("User-Agent", "podcast-downloader")
|
.with_header("User-Agent", "podcast-downloader")
|
||||||
.with_header("Accept", "*/*")
|
.with_header("Accept", "*/*")
|
||||||
.send()
|
.send()
|
||||||
.with_context(|| format!(r#"error when requesting feed url "{}" for {}"#, feed_url, alias))?;
|
.context(format!(r#"error when requesting feed url "{}" for {}"#, feed_url, alias))?;
|
||||||
|
|
||||||
if response.status_code != 200 {
|
if response.status_code != 200 {
|
||||||
eprintln!(r#"[error] feed "{}" for alias {} responded with non-200 ({}) status code"#, feed_url, alias, response.status_code);
|
eprintln!(r#"[error] feed "{}" for alias {} responded with non-200 ({}) status code"#, feed_url, alias, response.status_code);
|
||||||
|
|||||||
12
src/folders.rs
Normal file
12
src/folders.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use std::path;
|
||||||
|
|
||||||
|
use sanitise_file_name::sanitise;
|
||||||
|
|
||||||
|
pub(crate) const PODCASTS_DIR: &str = "Podcasts";
|
||||||
|
|
||||||
|
pub(crate) fn podcast_folder(
|
||||||
|
root: &path::Path,
|
||||||
|
alias: &str,
|
||||||
|
) -> path::PathBuf {
|
||||||
|
root.join(PODCASTS_DIR).join(sanitise(alias))
|
||||||
|
}
|
||||||
26
src/input.rs
26
src/input.rs
@@ -2,58 +2,58 @@ use std::path;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
pub (crate) struct Args {
|
pub(crate) struct Args {
|
||||||
/// Path to the configuration file listing podcast RSS feeds.
|
/// Path to the configuration file listing podcast RSS feeds.
|
||||||
#[arg(default_value = "./podcasts.toml")]
|
#[arg(default_value = "./podcasts.toml")]
|
||||||
pub (crate) config : path::PathBuf,
|
pub(crate) config: path::PathBuf,
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub (crate) command : Command,
|
pub(crate) command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::ValueEnum, Copy, Clone, Debug,PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(clap::ValueEnum, Copy, Clone, Debug,PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub (crate) enum ListenStatus {
|
pub(crate) enum ListenStatus {
|
||||||
Listened,
|
Listened,
|
||||||
Unlistened,
|
Unlistened,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Subcommand)]
|
#[derive(clap::Subcommand)]
|
||||||
pub (crate) enum Command {
|
pub(crate) enum Command {
|
||||||
/// Updates feed and downloads latest episodes.
|
/// Updates feed and downloads latest episodes.
|
||||||
Download {
|
Download {
|
||||||
/// The podcast to update. Updates all in configuration file if unspecified.
|
/// The podcast to update. Updates all in configuration file if unspecified.
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
podcast : Option<String>,
|
podcast: Option<String>,
|
||||||
},
|
},
|
||||||
/// Lists the episodes for a given podcast, filtered based on if it has been listened or not.
|
/// Lists the episodes for a given podcast, filtered based on if it has been listened or not.
|
||||||
List {
|
List {
|
||||||
/// Filter for if episodes have been listened to or not.
|
/// Filter for if episodes have been listened to or not.
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
status : Option<ListenStatus>,
|
status: Option<ListenStatus>,
|
||||||
/// The podcast to list episodes for.
|
/// The podcast to list episodes for.
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
podcast : String,
|
podcast: String,
|
||||||
},
|
},
|
||||||
/// Marks an entire podcast's history of episodes as played or unplayed.
|
/// Marks an entire podcast's history of episodes as played or unplayed.
|
||||||
Mark {
|
Mark {
|
||||||
/// The new listen status for the episodes.
|
/// The new listen status for the episodes.
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
status : ListenStatus,
|
status: ListenStatus,
|
||||||
/// The podcast to change the listen status of.
|
/// The podcast to change the listen status of.
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
podcast : String,
|
podcast: String,
|
||||||
},
|
},
|
||||||
/// Tags files and generates playlists ready for use with an iPod.
|
/// Tags files and generates playlists ready for use with an iPod.
|
||||||
Tag {
|
Tag {
|
||||||
/// The podcast to tag and generate playlists for.
|
/// The podcast to tag and generate playlists for.
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
podcast : Option<String>,
|
podcast: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Struct modelling configuration file format.
|
/// Struct modelling configuration file format.
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub (crate) struct Config {
|
pub(crate) struct Config {
|
||||||
/// Map from podcast alias to RSS feed either as a url (prefix: http) or file path.
|
/// Map from podcast alias to RSS feed either as a url (prefix: http) or file path.
|
||||||
pub (crate) podcasts : HashMap<String, String>,
|
pub(crate) podcasts: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod rss;
|
mod rss;
|
||||||
mod input;
|
mod input;
|
||||||
mod tagging;
|
mod tagging;
|
||||||
|
mod folders;
|
||||||
mod download;
|
mod download;
|
||||||
|
|
||||||
use input::{Command, ListenStatus};
|
use input::{Command, ListenStatus};
|
||||||
@@ -8,7 +9,6 @@ use input::{Command, ListenStatus};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use sanitise_file_name::sanitise;
|
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
input::Args::parse()
|
input::Args::parse()
|
||||||
};
|
};
|
||||||
|
|
||||||
let config : input::Config = {
|
let config: input::Config = {
|
||||||
let config = fs::read_to_string(&args.config)
|
let config = fs::read_to_string(&args.config)
|
||||||
.context("failed to read in podcast configuration file")?;
|
.context("failed to read in podcast configuration file")?;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Command::List { status, podcast } => {
|
Command::List { status, podcast } => {
|
||||||
let output = root.join(sanitise(&podcast));
|
let output = folders::podcast_folder(root, &podcast);
|
||||||
let spec_file = output.join("spec.toml");
|
let spec_file = output.join("spec.toml");
|
||||||
|
|
||||||
let spec = download::Specification::read_from(&spec_file)?;
|
let spec = download::Specification::read_from(&spec_file)?;
|
||||||
@@ -64,7 +64,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Command::Mark { status, podcast } => {
|
Command::Mark { status, podcast } => {
|
||||||
let output = root.join(sanitise(&podcast));
|
let output = folders::podcast_folder(root, &podcast);
|
||||||
let spec_file = output.join("spec.toml");
|
let spec_file = output.join("spec.toml");
|
||||||
|
|
||||||
let mut spec = download::Specification::read_from(&spec_file)?;
|
let mut spec = download::Specification::read_from(&spec_file)?;
|
||||||
|
|||||||
54
src/rss.rs
54
src/rss.rs
@@ -8,55 +8,55 @@ use std::borrow::Cow;
|
|||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct Feed<'a> {
|
pub struct Feed<'a> {
|
||||||
pub (crate) rss : Rss<'a>,
|
pub(crate) rss: Rss<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct Rss<'a> {
|
pub struct Rss<'a> {
|
||||||
pub (crate) channel : Channel<'a>,
|
pub(crate) channel: Channel<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct Channel<'a> {
|
pub struct Channel<'a> {
|
||||||
#[serde(rename = "item", default)]
|
#[serde(rename = "item", default)]
|
||||||
pub (crate) items : Vec<Item<'a>>,
|
pub(crate) items: Vec<Item<'a>>,
|
||||||
pub (crate) link : Option<Cow<'a, str>>,
|
pub(crate) link: Option<Cow<'a, str>>,
|
||||||
pub (crate) title : Cow<'a, str>,
|
pub(crate) title: Cow<'a, str>,
|
||||||
pub (crate) description : Option<Cow<'a, str>>,
|
pub(crate) description: Option<Cow<'a, str>>,
|
||||||
#[serde(rename = "{http://www.itunes.com/dtds/podcast-1.0.dtd}itunes:author")]
|
#[serde(rename = "{http://www.itunes.com/dtds/podcast-1.0.dtd}itunes:author")]
|
||||||
pub (crate) author : Option<Cow<'a, str>>,
|
pub(crate) author: Option<Cow<'a, str>>,
|
||||||
#[serde(rename = "{http://www.itunes.com/dtds/podcast-1.0.dtd}itunes:summary")]
|
#[serde(rename = "{http://www.itunes.com/dtds/podcast-1.0.dtd}itunes:summary")]
|
||||||
pub (crate) summary : Option<Cow<'a, str>>,
|
pub(crate) summary: Option<Cow<'a, str>>,
|
||||||
#[serde(rename = "{http://www.itunes.com/dtds/podcast-1.0.dtd}itunes:image")]
|
#[serde(rename = "{http://www.itunes.com/dtds/podcast-1.0.dtd}itunes:image")]
|
||||||
pub (crate) itunes_image : Option<ItunesImage<'a>>,
|
pub(crate) itunes_image: Option<ItunesImage<'a>>,
|
||||||
pub (crate) image : Option<Image<'a>>,
|
pub(crate) image: Option<Image<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct Image<'a> {
|
pub struct Image<'a> {
|
||||||
pub (crate) link : Option<Cow<'a, str>>,
|
pub(crate) link: Option<Cow<'a, str>>,
|
||||||
pub (crate) title : Cow<'a, str>,
|
pub(crate) title: Cow<'a, str>,
|
||||||
pub (crate) url : Cow<'a, str>,
|
pub(crate) url: Cow<'a, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct ItunesImage<'a> {
|
pub struct ItunesImage<'a> {
|
||||||
#[serde(rename = "$attr:href")]
|
#[serde(rename = "$attr:href")]
|
||||||
pub (crate) href : Cow<'a, str>,
|
pub(crate) href: Cow<'a, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_publish_date<'de, D : serde::de::Deserializer<'de>> (
|
fn deserialize_publish_date<'de, D: serde::de::Deserializer<'de>> (
|
||||||
deserializer : D
|
deserializer: D
|
||||||
) -> Result<chrono::NaiveDateTime, D::Error> {
|
) -> Result<chrono::NaiveDateTime, D::Error> {
|
||||||
struct Visitor;
|
struct Visitor;
|
||||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||||
type Value = chrono::NaiveDateTime;
|
type Value = chrono::NaiveDateTime;
|
||||||
|
|
||||||
fn expecting(&self, formatter : &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
formatter.write_str("a string containing json data")
|
formatter.write_str("a string containing json data")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_str<E : serde::de::Error>(self, input : &str) -> Result<Self::Value, E> {
|
fn visit_str<E: serde::de::Error>(self, input: &str) -> Result<Self::Value, E> {
|
||||||
chrono::NaiveDateTime::parse_from_str(
|
chrono::NaiveDateTime::parse_from_str(
|
||||||
input,
|
input,
|
||||||
"%a, %d %b %Y %H:%M:%S %Z"
|
"%a, %d %b %Y %H:%M:%S %Z"
|
||||||
@@ -69,23 +69,23 @@ fn deserialize_publish_date<'de, D : serde::de::Deserializer<'de>> (
|
|||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct Item<'a> {
|
pub struct Item<'a> {
|
||||||
pub (crate) title : Cow<'a, str>,
|
pub(crate) title: Cow<'a, str>,
|
||||||
pub (crate) enclosure : Option<Enclosure<'a>>,
|
pub(crate) enclosure: Option<Enclosure<'a>>,
|
||||||
pub (crate) description : Option<Cow<'a, str>>,
|
pub(crate) description: Option<Cow<'a, str>>,
|
||||||
#[serde(rename = "{http://www.itunes.com/dtds/podcast-1.0.dtd}itunes:summary")]
|
#[serde(rename = "{http://www.itunes.com/dtds/podcast-1.0.dtd}itunes:summary")]
|
||||||
pub (crate) summary : Option<Cow<'a, str>>,
|
pub(crate) summary: Option<Cow<'a, str>>,
|
||||||
#[serde(rename = "pubDate", deserialize_with = "deserialize_publish_date")]
|
#[serde(rename = "pubDate", deserialize_with = "deserialize_publish_date")]
|
||||||
pub (crate) published : chrono::NaiveDateTime,
|
pub(crate) published: chrono::NaiveDateTime,
|
||||||
pub (crate) guid : Option<Cow<'a, str>>,
|
pub(crate) guid: Option<Cow<'a, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct Enclosure<'a> {
|
pub struct Enclosure<'a> {
|
||||||
#[serde(rename = "$attr:url")]
|
#[serde(rename = "$attr:url")]
|
||||||
pub (crate) url : Cow<'a, str>,
|
pub(crate) url: Cow<'a, str>,
|
||||||
#[serde(rename = "$attr:type")]
|
#[serde(rename = "$attr:type")]
|
||||||
pub (crate) mime_type : Option<Cow<'a, str>>,
|
pub(crate) mime_type: Option<Cow<'a, str>>,
|
||||||
//#[serde(rename = "$attr:length")]
|
//#[serde(rename = "$attr:length")]
|
||||||
//pub (crate) length : Option<u64>,
|
//pub(crate) length: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{fs, path};
|
use std::{fs, path};
|
||||||
|
|
||||||
use crate::download;
|
use crate::{folders, download};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ pub(crate) fn generate_m3u(
|
|||||||
alias: &str,
|
alias: &str,
|
||||||
root: &path::Path,
|
root: &path::Path,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let output = root.join(sanitise(&alias));
|
let output = folders::podcast_folder(root, alias);
|
||||||
let spec_file = output.join("spec.toml");
|
let spec_file = output.join("spec.toml");
|
||||||
|
|
||||||
let spec = download::Specification::read_from(&spec_file)?;
|
let spec = download::Specification::read_from(&spec_file)?;
|
||||||
@@ -54,7 +54,7 @@ pub(crate) fn strip_tags(
|
|||||||
alias: &str,
|
alias: &str,
|
||||||
root: &path::Path,
|
root: &path::Path,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let output = root.join(sanitise(&alias));
|
let output = folders::podcast_folder(root, alias);
|
||||||
let spec_file = output.join("spec.toml");
|
let spec_file = output.join("spec.toml");
|
||||||
|
|
||||||
let spec = download::Specification::read_from(&spec_file)?;
|
let spec = download::Specification::read_from(&spec_file)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user