diff --git a/Cargo.lock b/Cargo.lock index 2a0f4ca..47f637c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,9 +49,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys", ] @@ -68,22 +68,22 @@ dependencies = [ [[package]] name = "atom_syndication" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571832dcff775e26562e8e6930cd483de5587301d40d3a3b85d532b6383e15a7" +checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30" dependencies = [ "chrono", - "derive_builder 0.12.0", + "derive_builder", "diligent-date-parser", "never", - "quick-xml 0.30.0", + "quick-xml", ] [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bumpalo" @@ -93,9 +93,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cc" -version = "1.0.96" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -136,7 +136,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] @@ -148,7 +148,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.60", + "syn", ] [[package]] @@ -171,81 +171,37 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "darling" -version = "0.14.4" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - -[[package]] -name = "darling" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" -dependencies = [ - "darling_core 0.20.8", - "darling_macro 0.20.8", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.60", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", + "strsim", + "syn", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ - "darling_core 0.20.8", + "darling_core", "quote", - "syn 2.0.60", -] - -[[package]] -name = "derive_builder" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" -dependencies = [ - "derive_builder_macro 0.12.0", + "syn", ] [[package]] @@ -254,19 +210,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" dependencies = [ - "derive_builder_macro 0.20.0", -] - -[[package]] -name = "derive_builder_core" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", + "derive_builder_macro", ] [[package]] @@ -275,31 +219,27 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" dependencies = [ - "darling 0.20.8", + "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn", ] [[package]] name = "derive_builder_macro" -version = "0.12.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ - "derive_builder_core 0.12.0", - "syn 1.0.109", + "derive_builder_core", + "syn", ] [[package]] -name = "derive_builder_macro" -version = "0.20.0" +name = "diff" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" -dependencies = [ - "derive_builder_core 0.20.0", - "syn 2.0.60", -] +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "diligent-date-parser" @@ -325,6 +265,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.5.0" @@ -377,9 +328,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "log" @@ -401,9 +352,9 @@ checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -415,22 +366,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] -name = "proc-macro2" -version = "1.0.81" +name = "pretty_assertions" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ - "unicode-ident", + "diff", + "yansi", ] [[package]] -name = "quick-xml" -version = "0.30.0" +name = "proc-macro2" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ - "encoding_rs", - "memchr", + "unicode-ident", ] [[package]] @@ -439,6 +390,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ + "encoding_rs", "memchr", "serde", ] @@ -454,42 +406,36 @@ dependencies = [ [[package]] name = "rss" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a" +checksum = "2f374fd66bb795938b78c021db1662d43a8ffbc42ec1ac25429fc4833b732751" dependencies = [ "atom_syndication", - "derive_builder 0.12.0", + "derive_builder", "never", - "quick-xml 0.30.0", + "quick-xml", ] [[package]] name = "serde" -version = "1.0.200" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -498,20 +444,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -520,22 +455,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn", ] [[package]] @@ -550,6 +485,34 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", + "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9881bea7cbe687e36c9ab3b778c36cd0487402e270304e8b1296d5085303c1a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -571,7 +534,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn", "wasm-bindgen-shared", ] @@ -593,7 +556,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -697,13 +660,22 @@ dependencies = [ [[package]] name = "xmltv2rss" -version = "0.1.1" +version = "0.1.2" dependencies = [ + "atom_syndication", "chrono", "clap", - "derive_builder 0.20.0", - "quick-xml 0.31.0", + "derive_builder", + "pretty_assertions", + "quick-xml", "rss", "thiserror", + "uuid", "xmltv", ] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml index 4be7513..0c78bbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xmltv2rss" -version = "0.1.1" +version = "0.1.2" authors = ["willemw12 "] edition = "2021" license = "GPL-3.0-or-later" @@ -9,11 +9,9 @@ homepage = "https://github.com/willemw12/xmltv2rss-rs" #documentation = "https://github.com/willemw12/xmltv2rss-rs" repository = "https://github.com/willemw12/xmltv2rss-rs" readme = "README.md" -keywords = ["epg", "rss", "tv", "xmltv"] +keywords = ["atom", "epg", "rss", "tv", "xmltv"] categories = ["command-line-utilities"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - # [[bin]] # name = "xmltv2rss" # required-features = ["build-binary"] @@ -22,11 +20,16 @@ categories = ["command-line-utilities"] # build-binary = ["dep:clap"] [dependencies] -# clap = { version = "4.5.4", features = ["cargo", "derive"], optional = true } -clap = { version = "4.5.4", features = ["cargo", "derive"] } +atom_syndication = "0.12.3" chrono = "0.4.38" +# clap = { version = "...", features = ["cargo", "derive"], optional = true } +clap = { version = "4.5.4", features = ["cargo", "derive"] } derive_builder = "0.20.0" quick-xml = { version = "0.31", features = ["serialize"] } -rss = "2.0.7" -thiserror = "1.0.59" +rss = "2.0.8" +thiserror = "1.0.61" +uuid = { version = "1.8.0", features = ["v4", "macro-diagnostics", "serde"] } xmltv = "0.9.6" + +[dev-dependencies] +pretty_assertions = "1.4.0" diff --git a/README.md b/README.md index 430a0df..6be1979 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ xmltv2rss ========= -Generate an RSS feed from an XMLTV TV listing. +Generate an RSS or Atom feed from an XMLTV TV listing. Installation @@ -11,6 +11,10 @@ The following requires [Rust](https://www.rust-lang.org/). To install xmltv2rss, for example, in folder ~/.local/bin, run: + $ cargo install --git=https://github.com/willemw12/xmltv2rss-rs.git --no-track --root=$HOME/.local + +Or the same, but download separately: + $ git clone https://github.com/willemw12/xmltv2rss-rs.git $ cargo install --no-track --path=./xmltv2rss-rs --root=$HOME/.local @@ -19,7 +23,7 @@ Usage ----- $ xmltv2rss --help - Generate an RSS feed from an XMLTV TV listing. Print the result to standard output. + Generate an RSS or Atom feed from an XMLTV TV listing. Print the result to standard output. For information about date and time format strings ("%Y", "%H", etc.), search for "strftime" on . @@ -31,36 +35,45 @@ Usage Options: -d, --feed-date-format - RSS feed date format. Examples: "%%Y-%%m-%%d", "%%a %%d %%B, %%Y", "%%x" + Output feed date format. Examples: "%%Y-%%m-%%d", "%%a %%d %%B, %%Y", "%%x" [default: "%a %d %B, %Y"] --feed-description - RSS feed description + Output feed description --feed-indent - RSS feed indentation + Output feed indentation [default: 2] --feed-language - RSS feed language + Output feed language --feed-link - RSS feed URL + Output feed URL [default: ] -t, --feed-time-format - RSS feed time format. Examples: "%%H:%%M", "%%I:%%M %%p", "%%X" + Output feed time format. Examples: "%%H:%%M", "%%I:%%M %%p", "%%X" [default: %H:%M] --feed-title - RSS feed title + Output feed title [default: "XMLTV feed"] + --feed-type + Output feed type + + [default: rss] + + Possible values: + - atom + - rss: Rss 2.0 + --xmltv-datetime-format XMLTV date and time format [default fallback: "%Y%m%d%H%M%S"] @@ -79,18 +92,24 @@ Library usage ```rust use std::io; -use xmltv2rss::export::rss::{export, OptionsBuilder}; +use xmltv2rss::error::Result; +use xmltv2rss::export::{rss, OptionsBuilder}; + +fn print() -> Result<()> { + // let options = rss::Options::default(); + let options = OptionsBuilder::default() + // .language(&*string) + // .language(string.as_str()) + .language("en") + .build()?; -// let options = Options::default(); -let options = OptionsBuilder::default() - // .language(string.as_str()) - .language("en") - .build()?; + let channel = rss::export("Title", "https://example.com/", Some("Description"), + &options, Some("./tests/input/simple.xml"))?; -let channel = export("Title", "https://example.com/", Some("Description"), - &options, Some("file.xml"))?; + channel.pretty_write_to(io::stdout(), b' ', 2)?; -channel.pretty_write_to(io::stdout(), b' ', 2)?; + Ok(()) +} ``` diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..d23aa70 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,24 @@ +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Atom(#[from] atom_syndication::Error), + + #[error(transparent)] + De(#[from] quick_xml::DeError), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + OptionsBuilder(#[from] crate::export::OptionsBuilderError), + + #[error(transparent)] + Parse(#[from] chrono::ParseError), + + #[error(transparent)] + Rss(#[from] rss::Error), +} diff --git a/src/export.rs b/src/export.rs index 6a46591..3b9b1aa 100644 --- a/src/export.rs +++ b/src/export.rs @@ -1,10 +1,52 @@ -//! Common export module. - +use derive_builder::Builder; use xmltv::{Programme, Tv}; +pub mod atom; pub mod rss; -use crate::xmltv::Error; +use crate::error::Error; +use crate::xmltv::DEFAULT_XMLTV_DATETIME_FORMAT; + +pub const DEFAULT_FEED_CHANNEL_DESCRIPTION: &str = "Generated by xmltv2rss"; +pub const DEFAULT_FEED_CHANNEL_TITLE: &str = "XMLTV feed"; + +pub const DEFAULT_FEED_DATE_FORMAT: &str = "%a %d %B, %Y"; +pub const DEFAULT_FEED_TIME_FORMAT: &str = "%H:%M"; + +const GUID_DATETIME_FORMAT: &str = "%Y%m%d%H%M%S"; + +/// Feed export options struct. +#[derive(Builder)] +pub struct Options<'a> { + #[builder(default, setter(into, strip_option))] + pub language: Option<&'a str>, + + /// See [`DEFAULT_FEED_DATE_FORMAT`]. + #[builder(default = "DEFAULT_FEED_DATE_FORMAT")] + pub date_format: &'a str, + + /// See [`DEFAULT_FEED_TIME_FORMAT`]. + #[builder(default = "DEFAULT_FEED_TIME_FORMAT")] + pub time_format: &'a str, + + /// See [`DEFAULT_XMLTV_DATETIME_FORMAT`]. + #[builder(default = "DEFAULT_XMLTV_DATETIME_FORMAT")] + pub xmltv_datetime_format: &'a str, +} + +impl Default for Options<'_> { + fn default() -> Self { + Self { + language: None, + date_format: DEFAULT_FEED_DATE_FORMAT, + time_format: DEFAULT_FEED_TIME_FORMAT, + + xmltv_datetime_format: DEFAULT_XMLTV_DATETIME_FORMAT, + } + } +} + +// /// XMLTV export trait. pub trait Visitor { @@ -55,6 +97,7 @@ pub trait Visitor { // + /// Returns the export result. fn result(&self) -> Result; } diff --git a/src/export/atom.rs b/src/export/atom.rs new file mode 100644 index 0000000..ea0bc24 --- /dev/null +++ b/src/export/atom.rs @@ -0,0 +1,360 @@ +use atom_syndication::{ + Entry, EntryBuilder, Feed, FeedBuilder, GeneratorBuilder, LinkBuilder, Text, +}; +use chrono::{DateTime, FixedOffset, Local}; +use quick_xml::de::from_str; +use std::fs; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::io; +use uuid::Uuid; +use xmltv::{Channel, Programme, Tv}; + +use crate::error::Error; +use crate::export::{Options, Visitor, GUID_DATETIME_FORMAT}; +use crate::export::{DEFAULT_FEED_CHANNEL_DESCRIPTION, DEFAULT_FEED_CHANNEL_TITLE}; +use crate::xmltv::{find_name, find_value, first_url, parse_from_str}; +use crate::xmltv::{DEFAULT_XMLTV_DATETIME_FORMAT, DEFAULT_XMLTV_DATETIME_FORMAT_UTC}; + +/// Exports an XMLTV TV listing to an Atom feed. +pub fn export( + title: &str, + link: &str, + subtitle: Option<&str>, + options: &Options, + // reader: &mut impl Read, + file: Option<&str>, +) -> Result { + let last_build_date = Local::now(); + let (xmltv_listing, updated) = match &file { + Some(file) if *file != "-" => ( + fs::read_to_string(file)?, + DateTime::::from(fs::metadata(file)?.modified()?), + ), + _ => (io::read_to_string(io::stdin())?, last_build_date), + }; + let xmltv_listing: Tv = from_str(&xmltv_listing)?; + + let mut visitor = Atom::new( + title, + link, + subtitle, + Some(updated), + options, + &xmltv_listing.channels, + ); + + super::export::(&mut visitor, &xmltv_listing) +} + +// + +/// Atom feed export struct. +pub(crate) struct Atom<'a> { + title: &'a str, + link: &'a str, + subtitle: Option<&'a str>, + updated: Option>, + options: &'a Options<'a>, + + // Visitor state + xmltv_channels: &'a Vec, + feed: FeedBuilder, + entries: Vec, +} + +impl<'a> Atom<'a> { + pub fn new( + title: &'a str, + link: &'a str, + subtitle: Option<&'a str>, + updated: Option>, + options: &'a Options, + + // Input data + xmltv_channels: &'a Vec, + ) -> Self { + let title = if !title.is_empty() { + title + } else { + DEFAULT_FEED_CHANNEL_TITLE + }; + + Self { + title, + link, + subtitle, + updated, + options, + + // Visitor state + xmltv_channels, + feed: FeedBuilder::default(), + entries: vec![], + } + } + + fn entry_summary( + &mut self, + language: Option<&str>, + title: &str, + channel_id: &str, + starttime_dt: DateTime, + stoptime_dt: DateTime, + xmltv_programme: &Programme, + ) -> Result { + let channel = if let Some(channel_callsign) = self + .xmltv_channels + .iter() + .find(|channel| channel.id == *channel_id) + { + let display_name = find_name(&channel_callsign.display_names, language); + format!("{channel_id}-{display_name}") + } else { + channel_id.to_string() + }; + + let airdate = format!("{}", starttime_dt.format(self.options.date_format)); + let airtime = format!( + "{} - {}", + starttime_dt.format(self.options.time_format), + stoptime_dt.format(self.options.time_format) + ); + + let airtime_length_td = stoptime_dt - starttime_dt; + let airtime_length_mins = airtime_length_td.num_seconds() / 60; + let airtime_length = format!( + "{:02}:{:02}:00", + airtime_length_mins / 60, + airtime_length_mins % 60 + ); + + let category = find_name(&xmltv_programme.categories, language); + + let desc = find_value(&xmltv_programme.descriptions, language); + let desc = desc + .trim() + .lines() + .map(|line| line.trim()) + .collect::>() + .join("
"); + + let summary = format!("\ +\ +\ +\ +\ +\ +\ +\ +\ +
Title:{title}
Channel:{channel}
Airdate:{airdate}
Airtime:{airtime}
Length:{airtime_length}
Category:{category}
Description:{desc}
"); + + Ok(summary) + } +} + +impl Visitor for Atom<'_> { + type Output = Feed; + + /// Exports from XMLTV TV listing to Atom feed. + fn visit_tv(&mut self, _xmltv_listing: &Tv) -> Result<(), Error> { + self.feed + .title(self.title) + .link(LinkBuilder::default().href(self.link).build()) + .generator( + GeneratorBuilder::default() + .value(DEFAULT_FEED_CHANNEL_DESCRIPTION) + .build(), + ); + + if let Some(subtitle) = self.subtitle { + self.feed.subtitle(Text::plain(subtitle)); + } + if let Some(language) = self.options.language { + self.feed.lang(language.to_string()); + } + if let Some(updated) = self.updated { + self.feed.updated(updated); + } + + Ok(()) + } + + fn visit_programmes_start(&mut self) -> Result<(), Error> { + self.entries.clear(); + + Ok(()) + } + + /// Exports from XMLTV programme to Atom entry. + fn visit_programme(&mut self, xmltv_programme: &Programme) -> Result<(), Error> { + // let language = self.options.language; + let language = self.options.language.filter(|l| !l.is_empty()); + + let channel_id = &xmltv_programme.channel; + + let starttime = &xmltv_programme.start; + let stoptime = match &xmltv_programme.stop { + Some(time) => time, + None => starttime, + }; + + let starttime_dt = parse_from_str( + starttime, + DEFAULT_XMLTV_DATETIME_FORMAT, + DEFAULT_XMLTV_DATETIME_FORMAT_UTC, + )?; + let stoptime_dt = parse_from_str( + stoptime, + DEFAULT_XMLTV_DATETIME_FORMAT, + DEFAULT_XMLTV_DATETIME_FORMAT_UTC, + )?; + + // + + let title = find_value(&xmltv_programme.titles, language); + + let link = first_url(&xmltv_programme.urls).unwrap_or_default(); + + let summary = self.entry_summary( + language, + title, + channel_id, + starttime_dt, + stoptime_dt, + xmltv_programme, + )?; + + let hash_data = format!("{channel_id}-{}", starttime_dt.format(GUID_DATETIME_FORMAT)); + let uuid = uuid(hash_data.as_bytes()); + + let published = starttime_dt; + + let entry = EntryBuilder::default() + .title(title.to_string()) + .link(LinkBuilder::default().href(link).build()) + .summary(Text::plain(summary)) + .id(format!("urn:uuid:{uuid}")) + .published(published) + .updated(published) + .build(); + + self.entries.push(entry); + + Ok(()) + } + + fn visit_programmes_end(&mut self) -> Result<(), Error> { + self.feed.entries(&*self.entries); + self.entries.clear(); + + Ok(()) + } + + /// Returns the exported Atom feed. + fn result(&self) -> Result { + Ok(self.feed.build()) + } +} + +fn uuid(hash_data: &[u8]) -> Uuid { + let mut hasher = DefaultHasher::new(); + Hash::hash_slice(hash_data, &mut hasher); + let hash = hasher.finish(); + + // Uuid::from_u128(hash as u128) + Uuid::from_u128(hash as u128 * u64::MAX as u128) +} + +// + +#[cfg(test)] +mod tests { + use atom_syndication::WriteConfig; + use pretty_assertions::assert_eq; + use quick_xml::de::from_str; + use std::fs; + + use super::*; + use crate::export; + + const DEFAULT_XML_INDENT: usize = 2; + const UPDATED: &str = "Tue, 30 Apr 2024 12:00:00 +0000"; + + #[derive(Default)] + struct Test<'a> { + input_file: &'a str, + expected_file: &'a str, + language: Option<&'a str>, + } + + #[test] + fn test() { + // const TESTS: [Test; _] = [Test { + const TESTS: [Test; 3] = [ + Test { + input_file: "tests/input/simple.xml", + expected_file: "tests/output/atom/simple.xml", + language: None, + }, + Test { + input_file: "tests/input/simple.xml", + expected_file: "tests/output/atom/simple-language.xml", + language: Some("fr-FR"), + }, + Test { + input_file: "tests/input/timezones.xml", + expected_file: "tests/output/atom/timezones.xml", + language: None, + }, + ]; + + for test in TESTS.iter() { + // Get test values + let expected_file = test.expected_file; + let input_file = test.input_file; + + let expected = fs::read_to_string(expected_file).unwrap(); + // fs::read_to_string() adds a newline character at the end of the string + let expected = expected.trim_end(); + + // Run tests in the expected timezone + // May not work on all platforms and set_var() will be defined as "unsafe" in a future Rust release + std::env::set_var("TZ", "UTC"); + + // Get input arguments + let input = fs::read_to_string(input_file).unwrap(); + let link = ""; + let subtitle = None; + let updated = DateTime::parse_from_rfc2822(UPDATED).unwrap(); + // let options = Options::default(); + let options = Options { + language: test.language, + ..Default::default() + }; + let xmltv_listing: Tv = from_str(&input).unwrap(); + + // Run test + let mut visitor = Atom::new( + DEFAULT_FEED_CHANNEL_TITLE, + link, + subtitle, + Some(updated.into()), + &options, + &xmltv_listing.channels, + ); + let feed = export::export::(&mut visitor, &xmltv_listing).unwrap(); + + let config = WriteConfig { + indent_size: DEFAULT_XML_INDENT.into(), + ..Default::default() + }; + let output = + String::from_utf8(feed.write_with_config(Vec::new(), config).unwrap()).unwrap(); + + // Check result + // assert_eq!(output, expected, "for output file {expected_file} and failed formatted content:\n{output}\n"); + assert_eq!(output, expected, "for output file {expected_file}"); + } + } +} diff --git a/src/export/rss.rs b/src/export/rss.rs index 3921fe6..749a10b 100644 --- a/src/export/rss.rs +++ b/src/export/rss.rs @@ -1,60 +1,24 @@ -//! Export to RSS module. - use chrono::FixedOffset; use chrono::{DateTime, Local}; -use derive_builder::Builder; use quick_xml::de::from_str; use rss::{Channel, ChannelBuilder, Guid, Item, ItemBuilder}; use std::fs; use std::io; use xmltv::{Programme, Tv}; -use super::Visitor; -use crate::xmltv::{find_name, find_value, first_url, parse_from_str, Error}; +use crate::error::Error; +use crate::export::{Options, Visitor, GUID_DATETIME_FORMAT}; +use crate::export::{DEFAULT_FEED_CHANNEL_DESCRIPTION, DEFAULT_FEED_CHANNEL_TITLE}; +use crate::xmltv::{find_name, find_value, first_url, parse_from_str}; use crate::xmltv::{DEFAULT_XMLTV_DATETIME_FORMAT, DEFAULT_XMLTV_DATETIME_FORMAT_UTC}; -pub const DEFAULT_RSS_CHANNEL_DESCRIPTION: &str = "Generated by xmltv2rss"; -pub const DEFAULT_RSS_CHANNEL_TITLE: &str = "XMLTV feed"; - -pub const DEFAULT_RSS_DATE_FORMAT: &str = "%a %d %B, %Y"; -pub const DEFAULT_RSS_TIME_FORMAT: &str = "%H:%M"; - -const GUID_DATETIME_FORMAT: &str = "%Y%m%d%H%M%S"; - -/// Export to RSS options struct. -// Values that could be saved in a configuration file. -#[derive(Builder)] -pub struct Options<'a> { - #[builder(default, setter(into, strip_option))] - pub language: Option<&'a str>, - #[builder(default = "DEFAULT_RSS_DATE_FORMAT")] - pub date_format: &'a str, - #[builder(default = "DEFAULT_RSS_TIME_FORMAT")] - pub time_format: &'a str, - - #[builder(default = "DEFAULT_XMLTV_DATETIME_FORMAT")] - pub xmltv_datetime_format: &'a str, -} - -impl Default for Options<'_> { - fn default() -> Self { - Self { - language: None, - date_format: DEFAULT_RSS_DATE_FORMAT, - time_format: DEFAULT_RSS_TIME_FORMAT, - - xmltv_datetime_format: DEFAULT_XMLTV_DATETIME_FORMAT, - } - } -} - -/// Exports from XMLTV to RSS. +/// Exports an XMLTV TV listing to an RSS channel/feed. pub fn export( title: &str, link: &str, description: Option<&str>, options: &Options, - // mut reader: impl Read, + // reader: &mut impl Read, file: Option<&str>, ) -> Result { let last_build_date = Local::now(); @@ -82,7 +46,7 @@ pub fn export( // -/// Export to RSS struct. +/// RSS channel/feed export struct. pub(crate) struct Rss<'a> { title: &'a str, link: &'a str, @@ -112,7 +76,7 @@ impl<'a> Rss<'a> { let title = if !title.is_empty() { title } else { - DEFAULT_RSS_CHANNEL_TITLE + DEFAULT_FEED_CHANNEL_TITLE }; Self { @@ -193,14 +157,13 @@ impl<'a> Rss<'a> { impl Visitor for Rss<'_> { type Output = Channel; - /// Exports from XMLTV TV listing to RSS channel. + /// Exports from XMLTV TV listing to RSS channel/feed. fn visit_tv(&mut self, _xmltv_listing: &Tv) -> Result<(), Error> { self.channel .title(self.title) .link(self.link) - .description(self.description.unwrap_or(DEFAULT_RSS_CHANNEL_DESCRIPTION)); + .description(self.description.unwrap_or(DEFAULT_FEED_CHANNEL_DESCRIPTION)); - // let mut channel = self.channel; if let Some(language) = self.options.language { self.channel.language(language.to_string()); } @@ -222,7 +185,8 @@ impl Visitor for Rss<'_> { /// Exports from XMLTV programme to RSS item. fn visit_programme(&mut self, xmltv_programme: &Programme) -> Result<(), Error> { - let language = self.options.language; + // let language = self.options.language; + let language = self.options.language.filter(|l| !l.is_empty()); let channel_id = &xmltv_programme.channel; @@ -253,8 +217,8 @@ impl Visitor for Rss<'_> { language, title, channel_id, - starttime_dt.into(), - stoptime_dt.into(), + starttime_dt, + stoptime_dt, xmltv_programme, )?; @@ -287,7 +251,7 @@ impl Visitor for Rss<'_> { Ok(()) } - /// Returns the exported RSS. + /// Returns the exported RSS channel/feed. fn result(&self) -> Result { Ok(self.channel.build()) } @@ -297,6 +261,7 @@ impl Visitor for Rss<'_> { #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use quick_xml::de::from_str; use rss::Channel; use std::fs; @@ -321,17 +286,17 @@ mod tests { const TESTS: [Test; 3] = [ Test { input_file: "tests/input/simple.xml", - expected_file: "tests/output/simple.xml", + expected_file: "tests/output/rss/simple.xml", language: None, }, Test { input_file: "tests/input/simple.xml", - expected_file: "tests/output/simple-language.xml", + expected_file: "tests/output/rss/simple-language.xml", language: Some("fr-FR"), }, Test { input_file: "tests/input/timezones.xml", - expected_file: "tests/output/timezones.xml", + expected_file: "tests/output/rss/timezones.xml", language: None, }, ]; @@ -363,7 +328,7 @@ mod tests { // Run test let mut visitor = Rss::new( - DEFAULT_RSS_CHANNEL_TITLE, + DEFAULT_FEED_CHANNEL_TITLE, link, None, Some(pub_date.into()), @@ -381,6 +346,7 @@ mod tests { .unwrap(); // Check result + // assert_eq!(output, expected, "for output file {expected_file} and failed formatted content:\n{output}\n"); assert_eq!(output, expected, "for output file {expected_file}"); } } diff --git a/src/lib.rs b/src/lib.rs index ce3f0f4..b33cea3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,20 +4,26 @@ //! //! ``` //! use std::io; -//! use xmltv2rss::export::rss::{export, OptionsBuilder}; +//! use xmltv2rss::error::Result; +//! use xmltv2rss::export::{rss, OptionsBuilder}; //! -//! // let options = Options::default(); -//! let options = OptionsBuilder::default() -//! // .language(string.as_str()) -//! .language("en") -//! .build()?; +//! fn print() -> Result<()> { +//! // let options = rss::Options::default(); +//! let options = OptionsBuilder::default() +//! // .language(&*string) +//! // .language(string.as_str()) +//! .language("en") +//! .build()?; //! -//! let channel = export("Title", "https://example.com/", Some("Description"), -//! &options, Some("./tests/input/simple.xml"))?; +//! let channel = rss::export("Title", "https://example.com/", Some("Description"), +//! &options, Some("./tests/input/simple.xml"))?; //! -//! channel.pretty_write_to(io::stdout(), b' ', 2)?; -//! # Ok::<(), xmltv2rss::xmltv::Error>(()) +//! channel.pretty_write_to(io::stdout(), b' ', 2)?; +//! +//! Ok(()) +//! } //! ``` +pub mod error; pub mod export; pub mod xmltv; diff --git a/src/main.rs b/src/main.rs index 3014e6f..015b586 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,59 +1,70 @@ -//! Generate an RSS feed from an XMLTV TV listing. Print the result to standard output. +//! Generate an RSS or Atom feed from an XMLTV TV listing. Print the result to standard output. //! //! [...more][`Args`] -use std::io; - -use clap::{crate_version, Parser}; +use atom_syndication::WriteConfig; +use clap::{crate_version, Parser, ValueEnum}; +use std::io::{self, Write}; +mod error; mod export; mod xmltv; -use export::rss::{export, OptionsBuilder}; -use export::rss::{ - DEFAULT_RSS_CHANNEL_DESCRIPTION, DEFAULT_RSS_CHANNEL_TITLE, DEFAULT_RSS_DATE_FORMAT, - DEFAULT_RSS_TIME_FORMAT, -}; -use xmltv::Error; +use error::Result; +use export::{atom, rss, Options, OptionsBuilder}; +use export::{DEFAULT_FEED_CHANNEL_TITLE, DEFAULT_FEED_DATE_FORMAT, DEFAULT_FEED_TIME_FORMAT}; use xmltv::{DEFAULT_XMLTV_DATETIME_FORMAT, DEFAULT_XMLTV_DATETIME_FORMAT_UTC}; pub const DEFAULT_XML_INDENT: u8 = 2; -/// Generate an RSS feed from an XMLTV TV listing. Print the result to standard output. +#[derive(Clone, Default, Debug, ValueEnum)] +enum FeedType { + Atom, + + /// Rss 2.0 + #[default] + Rss, +} + +/// Generate an RSS or Atom feed from an XMLTV TV listing. Print the result to standard output. /// /// For information about date and time format strings ("%Y", "%H", etc.), /// search for "strftime" on . #[derive(Parser)] #[command(version = crate_version!())] struct Args { - /// RSS feed date format. Examples: "%%Y-%%m-%%d", "%%a %%d %%B, %%Y", "%%x". - #[arg(long, short = 'd', default_value = DEFAULT_RSS_DATE_FORMAT)] + /// Output feed date format. Examples: "%%Y-%%m-%%d", "%%a %%d %%B, %%Y", "%%x". + #[arg(long, short = 'd', default_value = DEFAULT_FEED_DATE_FORMAT)] feed_date_format: String, - /// RSS feed description. + /// Output feed description. #[arg(long)] feed_description: Option, - /// RSS feed indentation. + /// Output feed indentation. #[arg(long, default_value_t = DEFAULT_XML_INDENT)] feed_indent: u8, - /// RSS feed language. + /// Output feed language. #[arg(long)] feed_language: Option, - /// RSS feed URL. + /// Output feed URL. #[arg(long, default_value = "")] feed_link: String, - /// RSS feed time format. Examples: "%%H:%%M", "%%I:%%M %%p", "%%X". - #[arg(long, short = 't', default_value = DEFAULT_RSS_TIME_FORMAT)] + /// Output feed time format. Examples: "%%H:%%M", "%%I:%%M %%p", "%%X". + #[arg(long, short = 't', default_value = DEFAULT_FEED_TIME_FORMAT)] feed_time_format: String, - /// RSS feed title. - #[arg(long, default_value = DEFAULT_RSS_CHANNEL_TITLE)] + /// Output feed title. + #[arg(long, default_value = DEFAULT_FEED_CHANNEL_TITLE)] feed_title: String, + /// Output feed type. + #[arg(long, default_value_t, value_enum)] + feed_type: FeedType, + // #[arg(long, default_value = DEFAULT_XMLTV_DATETIME_FORMAT, // help = concatcp!("XMLTV date and time format\n[default fallback: \"", DEFAULT_XMLTV_DATETIME_FORMAT_UTC, "\"]"))] #[arg(long, default_value = DEFAULT_XMLTV_DATETIME_FORMAT, @@ -64,34 +75,69 @@ struct Args { file: Option, } -fn main() -> Result<(), Error> { +fn main() -> Result<()> { let args = Args::parse(); - run(args) + export(&args) +} + +// + +fn export(args: &Args) -> Result<()> { + let mut options = OptionsBuilder::default(); + // if let Some(language) = &args.feed_language && !language.is_empty() + if let Some(language) = &args.feed_language { + if !language.is_empty() { + options.language(language.as_str()); + } + } + let options = options.build()?; + + let mut writer = io::stdout(); + + match args.feed_type { + FeedType::Atom => export_to_atom(args, &options, &mut writer), + FeedType::Rss => export_to_rss(args, &options, &mut writer), + } } -fn run(args: Args) -> Result<(), Error> { - let feed_language = args.feed_language.unwrap_or_default(); - let options = OptionsBuilder::default() - // .language(feed_language.as_str()) - .language(&*feed_language) - .build()?; +fn export_to_atom(args: &Args, options: &Options, writer: &mut impl Write) -> Result<()> { + let feed = atom::export( + &args.feed_title, + &args.feed_link, + args.feed_description.as_deref(), + options, + args.file.as_deref(), + )?; + + let feed_indent = args.feed_indent; + if feed_indent > 0 { + let config = WriteConfig { + indent_size: Some(feed_indent.into()), + ..Default::default() + }; + feed.write_with_config(writer, config)?; + } else { + feed.write_to(writer)?; + } + + Ok(()) +} - let channel = export( +fn export_to_rss(args: &Args, options: &Options, writer: &mut impl Write) -> Result<()> { + let channel = rss::export( &args.feed_title, &args.feed_link, - args.feed_description - .as_deref() - .or(Some(DEFAULT_RSS_CHANNEL_DESCRIPTION)), - &options, + args.feed_description.as_deref(), + options, args.file.as_deref(), )?; let feed_indent = args.feed_indent; if feed_indent > 0 { - channel.pretty_write_to(io::stdout(), b' ', args.feed_indent.into())?; + channel.pretty_write_to(writer, b' ', feed_indent.into())?; } else { - channel.write_to(io::stdout())?; + channel.write_to(writer)?; } Ok(()) diff --git a/src/xmltv.rs b/src/xmltv.rs index b948d11..bf18309 100644 --- a/src/xmltv.rs +++ b/src/xmltv.rs @@ -1,40 +1,18 @@ -//! Main module. - -use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; -use thiserror::Error; +use chrono::{DateTime, FixedOffset, NaiveDateTime, TimeZone, Utc}; use xmltv::{NameAndLang, Url, ValueAndLang}; -use crate::export::rss::OptionsBuilderError; +use crate::error::Error; -/// Datetime with timezone +/// Datetime with timezone. pub const DEFAULT_XMLTV_DATETIME_FORMAT: &str = "%Y%m%d%H%M%S %z"; -/// Datetime without timezone +/// Datetime without timezone. pub const DEFAULT_XMLTV_DATETIME_FORMAT_UTC: &str = "%Y%m%d%H%M%S"; -/// Common error type. -#[derive(Error, Debug)] -pub enum Error { - #[error(transparent)] - De(#[from] quick_xml::DeError), - - #[error(transparent)] - Io(#[from] std::io::Error), - - #[error(transparent)] - OptionsBuilder(#[from] OptionsBuilderError), - - #[error(transparent)] - Parse(#[from] chrono::ParseError), - - #[error(transparent)] - Rss(#[from] rss::Error), -} - // // XMLTV access functions -/// Returns the name in the specified language or the first value or an empty string. +/// Returns the name in the specified language or else the first name or else an empty string. pub(crate) fn find_name<'a>(elements: &'a [NameAndLang], language: Option<&'a str>) -> &'a str { elements .iter() @@ -43,7 +21,7 @@ pub(crate) fn find_name<'a>(elements: &'a [NameAndLang], language: Option<&'a st .map_or("", |e| &e.name) } -/// Returns the value in the specified language or the first value or an empty string. +/// Returns the value in the specified language or else the first value or else an empty string. pub(crate) fn find_value<'a>(elements: &'a [ValueAndLang], language: Option<&'a str>) -> &'a str { elements .iter() @@ -73,12 +51,13 @@ pub(crate) fn parse_from_str( datetime: &str, datetime_format: &str, naive_datetime_format: &str, -) -> Result, Error> { +) -> Result, Error> { let datetime = DateTime::parse_from_str(datetime, datetime_format) .or_else(|_| { NaiveDateTime::parse_from_str(datetime, naive_datetime_format) .map(|datetime| Utc.from_utc_datetime(&datetime).fixed_offset()) })? + .to_utc() .into(); Ok(datetime) diff --git a/tests/output/atom/simple-language.xml b/tests/output/atom/simple-language.xml new file mode 100644 index 0000000..b3db352 --- /dev/null +++ b/tests/output/atom/simple-language.xml @@ -0,0 +1,24 @@ + + + XMLTV feed + + 2024-04-30T12:00:00+00:00 + Generated by xmltv2rss + + + Le journal + urn:uuid:8e4c6dfa-c3ba-fde3-71b3-92053c45021c + 2001-08-29T00:05:00+00:00 + + 2001-08-29T00:05:00+00:00 + <table><tr><td align="right" valign="top">Title:</td><td>Le journal</td></tr><tr><td align="right" valign="top">Channel:</td><td>bbc2.bbc.co.uk</td></tr><tr><td align="right" valign="top">Airdate:</td><td>Wed 29 August, 2001</td></tr><tr><td align="right" valign="top">Airtime:</td><td>00:05 - 00:05</td></tr><tr><td align="right" valign="top" style="white-space: nowrap">Length:</td><td>00:00:00</td></tr><tr><td align="right" valign="top">Category:</td><td></td></tr><tr><td align="right" valign="top">Description:</td><td>Bilko claims he's had a close encounter with an alien in order<br/>to be given some compassionate leave so he can visit an old<br/>flame in New York.</td></tr></table> + + + Le journal + urn:uuid:3016e146-722c-6ff0-cfe9-1eb98dd3900f + 2001-08-29T09:55:00+00:00 + + 2001-08-29T09:55:00+00:00 + <table><tr><td align="right" valign="top">Title:</td><td>Le journal</td></tr><tr><td align="right" valign="top">Channel:</td><td>channel4.com</td></tr><tr><td align="right" valign="top">Airdate:</td><td>Wed 29 August, 2001</td></tr><tr><td align="right" valign="top">Airtime:</td><td>09:55 - 09:55</td></tr><tr><td align="right" valign="top" style="white-space: nowrap">Length:</td><td>00:00:00</td></tr><tr><td align="right" valign="top">Category:</td><td>animation</td></tr><tr><td align="right" valign="top">Description:</td><td>Bobby tours with a comedy troupe who specialize in<br/>propane-related mirth.</td></tr></table> + + diff --git a/tests/output/atom/simple.xml b/tests/output/atom/simple.xml new file mode 100644 index 0000000..03f5ff7 --- /dev/null +++ b/tests/output/atom/simple.xml @@ -0,0 +1,24 @@ + + + XMLTV feed + + 2024-04-30T12:00:00+00:00 + Generated by xmltv2rss + + + The Phil Silvers Show + urn:uuid:8e4c6dfa-c3ba-fde3-71b3-92053c45021c + 2001-08-29T00:05:00+00:00 + + 2001-08-29T00:05:00+00:00 + <table><tr><td align="right" valign="top">Title:</td><td>The Phil Silvers Show</td></tr><tr><td align="right" valign="top">Channel:</td><td>bbc2.bbc.co.uk</td></tr><tr><td align="right" valign="top">Airdate:</td><td>Wed 29 August, 2001</td></tr><tr><td align="right" valign="top">Airtime:</td><td>00:05 - 00:05</td></tr><tr><td align="right" valign="top" style="white-space: nowrap">Length:</td><td>00:00:00</td></tr><tr><td align="right" valign="top">Category:</td><td></td></tr><tr><td align="right" valign="top">Description:</td><td>Bilko claims he's had a close encounter with an alien in order<br/>to be given some compassionate leave so he can visit an old<br/>flame in New York.</td></tr></table> + + + King of the Hill + urn:uuid:3016e146-722c-6ff0-cfe9-1eb98dd3900f + 2001-08-29T09:55:00+00:00 + + 2001-08-29T09:55:00+00:00 + <table><tr><td align="right" valign="top">Title:</td><td>King of the Hill</td></tr><tr><td align="right" valign="top">Channel:</td><td>channel4.com</td></tr><tr><td align="right" valign="top">Airdate:</td><td>Wed 29 August, 2001</td></tr><tr><td align="right" valign="top">Airtime:</td><td>09:55 - 09:55</td></tr><tr><td align="right" valign="top" style="white-space: nowrap">Length:</td><td>00:00:00</td></tr><tr><td align="right" valign="top">Category:</td><td>animation</td></tr><tr><td align="right" valign="top">Description:</td><td>Bobby tours with a comedy troupe who specialize in<br/>propane-related mirth.</td></tr></table> + + diff --git a/tests/output/atom/timezones.xml b/tests/output/atom/timezones.xml new file mode 100644 index 0000000..07be843 --- /dev/null +++ b/tests/output/atom/timezones.xml @@ -0,0 +1,32 @@ + + + XMLTV feed + + 2024-04-30T12:00:00+00:00 + Generated by xmltv2rss + + + Heart Dance from London, UK + urn:uuid:024bfff8-14b5-8e5f-fdb4-0007eb4a71a0 + 2023-10-28T16:00:00+00:00 + + 2023-10-28T16:00:00+00:00 + <table><tr><td align="right" valign="top">Title:</td><td>Heart Dance from London, UK</td></tr><tr><td align="right" valign="top">Channel:</td><td>niteradio.example.com-Nite Radio</td></tr><tr><td align="right" valign="top">Airdate:</td><td>Sat 28 October, 2023</td></tr><tr><td align="right" valign="top">Airtime:</td><td>16:00 - 22:00</td></tr><tr><td align="right" valign="top" style="white-space: nowrap">Length:</td><td>06:00:00</td></tr><tr><td align="right" valign="top">Category:</td><td>Music</td></tr><tr><td align="right" valign="top">Description:</td><td>Programme within timezone UTC+0200.</td></tr></table> + + + Nuit électronique (requests enabled) + urn:uuid:08eab1a1-3bf1-22f1-f715-4e5ec40edd0e + 2023-10-28T22:00:00+00:00 + + 2023-10-28T22:00:00+00:00 + <table><tr><td align="right" valign="top">Title:</td><td>Nuit électronique (requests enabled)</td></tr><tr><td align="right" valign="top">Channel:</td><td>niteradio.example.com-Nite Radio</td></tr><tr><td align="right" valign="top">Airdate:</td><td>Sat 28 October, 2023</td></tr><tr><td align="right" valign="top">Airtime:</td><td>22:00 - 05:00</td></tr><tr><td align="right" valign="top" style="white-space: nowrap">Length:</td><td>07:00:00</td></tr><tr><td align="right" valign="top">Category:</td><td>Music</td></tr><tr><td align="right" valign="top">Description:</td><td>Programme crossing end of daylight savings time (UTC+0200 to UTC+0100).<br/>This has an actual duration of 7:00:00!</td></tr></table> + + + Pop (requests enabled) + urn:uuid:141d169d-d16a-ed4d-ebe2-e9622e9512b2 + 2023-10-29T05:00:00+00:00 + + 2023-10-29T05:00:00+00:00 + <table><tr><td align="right" valign="top">Title:</td><td>Pop (requests enabled)</td></tr><tr><td align="right" valign="top">Channel:</td><td>niteradio.example.com-Nite Radio</td></tr><tr><td align="right" valign="top">Airdate:</td><td>Sun 29 October, 2023</td></tr><tr><td align="right" valign="top">Airtime:</td><td>05:00 - 11:00</td></tr><tr><td align="right" valign="top" style="white-space: nowrap">Length:</td><td>06:00:00</td></tr><tr><td align="right" valign="top">Category:</td><td>Music</td></tr><tr><td align="right" valign="top">Description:</td><td>Program with no UTC offset given; should assume UTC.</td></tr></table> + + diff --git a/tests/output/simple-language.xml b/tests/output/rss/simple-language.xml similarity index 100% rename from tests/output/simple-language.xml rename to tests/output/rss/simple-language.xml diff --git a/tests/output/simple.xml b/tests/output/rss/simple.xml similarity index 100% rename from tests/output/simple.xml rename to tests/output/rss/simple.xml diff --git a/tests/output/timezones.xml b/tests/output/rss/timezones.xml similarity index 100% rename from tests/output/timezones.xml rename to tests/output/rss/timezones.xml