Skip to content

Commit

Permalink
Merge pull request #61 from SamTV12345/develop
Browse files Browse the repository at this point in the history
Added opml local and online export.
  • Loading branch information
SamTV12345 authored Apr 11, 2023
2 parents 565a024 + a384093 commit 476b57c
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 71 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
81 changes: 79 additions & 2 deletions src/controllers/settings_controller.rs
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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<DbPool>, type_of: Path<Mode>, env_service: Data<Mutex<EnvironmentService>>) ->
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<u8> = 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<Podcast>, env_service: MutexGuard<EnvironmentService>, 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
}
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -377,6 +377,7 @@ fn get_private_api() -> Scope<impl ServiceFactory<ServiceRequest, Config = (), R
.service(run_cleanup)
.service(add_podcast_from_podindex)
.service(delete_podcast)
.service(get_opml)
}


Expand Down
51 changes: 51 additions & 0 deletions ui/src/components/OPMLExport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {useState} from "react";
import {useTranslation} from "react-i18next";
import axios from "axios";
import {apiURL} from "../utils/Utilities";

export const OPMLExport = () => {
const [exportType, setExportType] = useState<string>('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 <div className="bg-slate-900 rounded p-5 text-white">
<h1 className="text-2xl text-center">{t('opml-export')}</h1>
<div className="mt-2 flex">
<div className="mt-2 mr-2">{t('i-want-to-export')}</div>
<select id="countries" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg
focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600
dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
onChange={(v)=>{setExportType(v.target.value)}}
value={exportType}>
<option value="local">{t('local')}</option>
<option value="online">{t('online')}</option>
</select>
<div className="mt-2 ml-2">{t('export')}</div>
</div>
<div className="flex">
<div className="flex-1"></div>
<button className="bg-blue-600 rounded p-2" onClick={()=>downloadOPML()}>{t('download')}</button>
</div>
</div>
}
75 changes: 75 additions & 0 deletions ui/src/components/PodcastDelete.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className="bg-slate-900 rounded p-5 text-white">
<h1 className="text-2xl text-center">{t('manage-podcasts')}</h1>
<div className="mt-2">
{
podcasts.map(p=>
<div className="border-2 border-b-indigo-100 p-4">
<h2>{p.name}</h2>
<div className="grid grid-cols-2 gap-5">
<button className="bg-red-500" onClick={()=>{
dispatch(setConfirmModalData({
headerText: t('delete-podcast-with-files'),
onAccept:()=>{
deletePodcast(true, p.id)
},
onReject: ()=>{
dispatch(setModalOpen(false))
},
acceptText: t('delete-podcast'),
rejectText: t('cancel'),
bodyText: t('delete-podcast-with-files-body', {name: p.name})
}))
dispatch(setModalOpen(true))
}}>{t('delete-podcasts-without-files')}</button>
<button className="bg-red-500" onClick={()=>{
dispatch(setConfirmModalData({
headerText: t('delete-podcast-without-files'),
onAccept:()=>{
deletePodcast(false, p.id)
},
onReject: ()=>{
dispatch(setModalOpen(false))
},
acceptText: t('delete-podcast'),
rejectText: t('cancel'),
bodyText: t('delete-podcast-without-files-body', {name: p.name})
}))
dispatch(setModalOpen(true))
}}>{t('delete-podcasts-without-files')}</button>
</div>
</div>
)
}
</div>
</div>
}
11 changes: 10 additions & 1 deletion ui/src/language/json/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
11 changes: 10 additions & 1 deletion ui/src/language/json/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
11 changes: 10 additions & 1 deletion ui/src/language/json/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Loading

0 comments on commit 476b57c

Please sign in to comment.