From a384093369f86762043b411fa06a6cde7754ac0c Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Tue, 11 Apr 2023 21:17:06 +0200 Subject: [PATCH] Added opml local and online export. --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/controllers/settings_controller.rs | 81 +++++++++++++++++++++++++- src/main.rs | 3 +- ui/src/components/OPMLExport.tsx | 51 ++++++++++++++++ ui/src/components/PodcastDelete.tsx | 75 ++++++++++++++++++++++++ ui/src/language/json/de.json | 11 +++- ui/src/language/json/en.json | 11 +++- ui/src/language/json/fr.json | 11 +++- ui/src/pages/SettingsPage.tsx | 72 +++-------------------- 10 files changed, 252 insertions(+), 71 deletions(-) create mode 100644 ui/src/components/OPMLExport.tsx create mode 100644 ui/src/components/PodcastDelete.tsx diff --git a/Cargo.lock b/Cargo.lock index e70b96f7..92fb5215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1748,6 +1748,7 @@ dependencies = [ "utoipa", "utoipa-swagger-ui", "uuid", + "xml-builder", ] [[package]] @@ -2879,6 +2880,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "xml-builder" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efc4f1a86af7800dfc4056c7833648ea4515ae21502060b5c98114d828f5333b" + [[package]] name = "xmlparser" version = "0.13.5" diff --git a/Cargo.toml b/Cargo.toml index 737aec83..86be15fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ actix-cors="0.6.4" rss = "2.0.2" frankenstein = "0.24.1" regex = "1.7.1" +xml-builder = "0.5.2" diesel = { version = "2.0.3", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono", "r2d2"] } r2d2 = "0.8.10" utoipa = { version = "3", features = ["actix_extras"] } diff --git a/src/controllers/settings_controller.rs b/src/controllers/settings_controller.rs index 1c2b6b3c..bfc7d411 100644 --- a/src/controllers/settings_controller.rs +++ b/src/controllers/settings_controller.rs @@ -1,11 +1,19 @@ +use std::fmt::format; use crate::models::settings::Setting; use crate::service::podcast_episode_service::PodcastEpisodeService; -use actix_web::web::Data; +use actix_web::web::{Data, Path}; use actix_web::{get, put}; use actix_web::{web, HttpResponse, Responder}; -use std::sync::Mutex; +use std::sync::{Mutex, MutexGuard}; +use chrono::Local; +use fs_extra::dir::DirEntryValue::SystemTime; +use opml::OPML; +use xml_builder::{XMLBuilder, XMLElement, XMLVersion}; +use crate::db::DB; use crate::DbPool; +use crate::models::itunes_models::Podcast; use crate::mutex::LockResultExt; +use crate::service::environment_service::EnvironmentService; use crate::service::settings_service::SettingsService; #[utoipa::path( @@ -67,3 +75,72 @@ pub async fn run_cleanup( } } } + +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Mode{ + LOCAL,ONLINE +} + +#[get("/settings/opml/{type_of}")] +pub async fn get_opml(conn: Data, type_of: Path, env_service: Data>) -> + impl +Responder { + let env_service = env_service.lock().ignore_poison(); + let podcasts_found = DB::get_podcasts(&mut conn.get().unwrap()).unwrap(); + + let mut xml = XMLBuilder::new().version(XMLVersion::XML1_1) + .encoding("UTF-8".into()) + .build(); + let mut opml = XMLElement::new("opml"); + opml.add_attribute("version", "2.0"); + opml.add_child(add_header()).expect("TODO: panic message"); + opml.add_child(add_podcasts(podcasts_found, env_service,type_of.into_inner() )).expect("TODO: panic \ + message"); + + xml.set_root_element(opml); + + + let mut writer: Vec = Vec::new(); + xml.generate(&mut writer).unwrap(); + HttpResponse::Ok().body(writer) +} + + +fn add_header()->XMLElement { + let mut head = XMLElement::new("head"); + let mut title = XMLElement::new("title"); + title.add_text("PodFetch Feed Export".to_string()).expect("Error creating title"); + head.add_child(title).expect("TODO: panic message"); + let mut date_created = XMLElement::new("dateCreated"); + date_created.add_text(Local::now().to_rfc3339()).expect("Error creating dateCreated"); + + head.add_child(date_created).expect("TODO: panic message"); + head +} + + +fn add_body()->XMLElement { + let mut body = XMLElement::new("body"); + body +} + + +fn add_podcasts(podcasts_found: Vec, env_service: MutexGuard, type_of: Mode) -> XMLElement { + let mut body = add_body(); + for podcast in podcasts_found { + let mut outline = XMLElement::new("outline"); + if podcast.summary.is_some(){ + outline.add_attribute("text", &*podcast.summary.unwrap()); + } + outline.add_attribute("title", &*podcast.name); + outline.add_attribute("type", "rss"); + match type_of { + Mode::LOCAL => outline.add_attribute("xmlUrl", &*format!("{}rss/{}", &*env_service + .get_server_url(), podcast.id)), + Mode::ONLINE => outline.add_attribute("xmlUrl", &*podcast.rssfeed), + } + body.add_child(outline).expect("TODO: panic message"); + } + body +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 754dc4df..597f04b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,7 @@ use crate::controllers::podcast_controller::{ use crate::controllers::podcast_episode_controller::{ download_podcast_episodes_of_podcast, find_all_podcast_episodes_of_podcast, }; -use crate::controllers::settings_controller::{get_settings, run_cleanup, update_settings}; +use crate::controllers::settings_controller::{get_opml, get_settings, run_cleanup, update_settings}; use crate::controllers::sys_info_controller::{get_public_config, get_sys_info, login}; use crate::controllers::watch_time_controller::{get_last_watched, get_watchtime, log_watchtime}; use crate::controllers::websocket_controller::{ @@ -377,6 +377,7 @@ fn get_private_api() -> Scope { + const [exportType, setExportType] = useState('local') + const {t} = useTranslation() + + const downloadOPML = ()=>{ + axios({ + url: apiURL+"/settings/opml/"+exportType, //your url + method: 'GET', + responseType: "blob" + }).then((response) => { + // create file link in browser's memory + const href = URL.createObjectURL(response.data); + + // create "a" HTML element with href to file & click + const link = document.createElement('a'); + link.href = href; + link.setAttribute('download', 'podcast_'+exportType+".opml"); //or any other extension + document.body.appendChild(link); + link.click(); + + // clean up "a" element & remove ObjectURL + document.body.removeChild(link); + URL.revokeObjectURL(href); + }); + } + + return
+

{t('opml-export')}

+
+
{t('i-want-to-export')}
+ +
{t('export')}
+
+
+
+ +
+
+} diff --git a/ui/src/components/PodcastDelete.tsx b/ui/src/components/PodcastDelete.tsx new file mode 100644 index 00000000..e5931456 --- /dev/null +++ b/ui/src/components/PodcastDelete.tsx @@ -0,0 +1,75 @@ +import {setConfirmModalData, setPodcasts} from "../store/CommonSlice"; +import {setModalOpen} from "../store/ModalSlice"; +import {useAppDispatch, useAppSelector} from "../store/hooks"; +import {useTranslation} from "react-i18next"; +import axios from "axios"; +import {apiURL} from "../utils/Utilities"; +import {enqueueSnackbar} from "notistack"; +import {useEffect} from "react"; + +export const PodcastDelete = () => { + const podcasts = useAppSelector(state=>state.common.podcasts) + const dispatch = useAppDispatch() + const {t} = useTranslation() + + useEffect(()=>{ + if(podcasts.length===0){ + axios.get(apiURL+"/podcasts") + .then((v)=>{ + dispatch(setPodcasts(v.data)) + }) + } + },[]) + + const deletePodcast = (withFiles:boolean, podcast_id: number)=>{ + axios.delete(apiURL+"/podcast/"+podcast_id,{data: {delete_files: withFiles}}) + .then(()=>{ + enqueueSnackbar(t('podcast-deleted'),{variant: "success"}) + }) + } + + return
+

{t('manage-podcasts')}

+
+ { + podcasts.map(p=> +
+

{p.name}

+
+ + +
+
+ ) + } +
+
+} diff --git a/ui/src/language/json/de.json b/ui/src/language/json/de.json index 538fe67b..b6fcfad7 100644 --- a/ui/src/language/json/de.json +++ b/ui/src/language/json/de.json @@ -57,6 +57,15 @@ "cancel": "Abbrechen", "delete-podcast-with-files-body": "Der Podcast {{name}} und alle lokalen Podcastfolgen werden gelöscht.", "podcast-deleted": "Podcast {{name}} gelöscht", + "delete-podcasts-with-files": "Podcasts mit Dateien löschen", + "delete-podcasts-without-files": "Podcasts ohne Dateien löschen", "delete-podcast-without-files": "Podcast ohne Dateien löschen", - "delete-podcast-without-files-body": "Der Podcast {{name}} wird gelöscht." + "delete-podcast-without-files-body": "Der Podcast {{name}} wird gelöscht.", + "manage-podcasts": "Podcasts verwalten", + "opml-export": "OPML-Export", + "local": "Lokal", + "online": "Online", + "i-want-to-export": "Ich möchte die Podcasts mit der Konfiguration", + "export": "exportieren", + "download": "Herunterladen" } diff --git a/ui/src/language/json/en.json b/ui/src/language/json/en.json index 396fe94d..f87834d1 100644 --- a/ui/src/language/json/en.json +++ b/ui/src/language/json/en.json @@ -56,5 +56,14 @@ "delete-podcast": "Delete podcast", "cancel": "Cancel", "delete-podcast-with-files-body": "Are you sure you want to delete the podcast {{name}} and all downloaded files?", - "podcast-deleted": "Podcast deleted" + "podcast-deleted": "Podcast deleted", + "manage-podcasts": "Manage podcasts", + "delete-podcasts-without-files": "Delete podcasts without files", + "delete-podcasts-with-files": "Delete podcasts with files", + "opml-export": "OPML export", + "local": "Local", + "online": "Online", + "i-want-to-export": "I want to export the podcasts with", + "export": "export", + "download": "Download" } diff --git a/ui/src/language/json/fr.json b/ui/src/language/json/fr.json index 5f468396..2901259e 100644 --- a/ui/src/language/json/fr.json +++ b/ui/src/language/json/fr.json @@ -56,5 +56,14 @@ "delete-podcast": "Delete podcast", "cancel": "Annuler", "delete-podcast-with-files-body": "Êtes-vous sûr de vouloir supprimer le podcast {{name}} et tous les fichiers associés ?", - "podcast-deleted": "Podcast supprimé" + "podcast-deleted": "Podcast supprimé", + "manage-podcasts": "Gérer les podcasts", + "delete-podcasts-without-files": "Supprimer les podcasts sans fichiers", + "delete-podcasts-with-files": "Supprimer les podcasts et les fichiers", + "opml-export": "Exporter OPML", + "local": "Local", + "online": "En ligne", + "i-want-to-export": "Je veux exporter mes podcasts en mode", + "export": "exporter", + "download": "Télécharger" } diff --git a/ui/src/pages/SettingsPage.tsx b/ui/src/pages/SettingsPage.tsx index 0f2059a6..f6ba1189 100644 --- a/ui/src/pages/SettingsPage.tsx +++ b/ui/src/pages/SettingsPage.tsx @@ -6,17 +6,15 @@ import axios, {AxiosResponse} from "axios"; import {Setting} from "../models/Setting"; import {useSnackbar} from "notistack"; import {Loading} from "../components/Loading"; -import {useAppDispatch, useAppSelector} from "../store/hooks"; -import {setConfirmModalData, setPodcasts} from "../store/CommonSlice"; -import {setModalOpen} from "../store/ModalSlice"; import {ConfirmModal} from "../components/ConfirmModal"; +import {PodcastDelete} from "../components/PodcastDelete"; +import {OPMLExport} from "../components/OPMLExport"; export const SettingsPage = () => { const {t} = useTranslation() - const dispatch = useAppDispatch() const [settings, setSettings] = useState() const {enqueueSnackbar} = useSnackbar() - const podcasts = useAppSelector(state=>state.common.podcasts) + useEffect(()=>{ axios.get(apiURL+"/settings").then((res:AxiosResponse)=>{ @@ -24,27 +22,12 @@ export const SettingsPage = () => { }) },[]) - useEffect(()=>{ - if(podcasts.length===0){ - axios.get(apiURL+"/podcasts") - .then((v)=>{ - dispatch(setPodcasts(v.data)) - }) - } - },[]) - - const deletePodcast = (withFiles:boolean, podcast_id: number)=>{ - axios.delete(apiURL+"/podcast/"+podcast_id,{data: {delete_files: withFiles}}) - .then(()=>{ - enqueueSnackbar(t('podcast-deleted'),{variant: "success"}) - }) - } - if(settings===undefined){ return } + return (
@@ -94,50 +77,9 @@ export const SettingsPage = () => {
-
-

Podcasts verwalten

-
- { - podcasts.map(p=> -
-

{p.name}

-
- - -
-
- ) - } -
-
+ + + )