Skip to content

Commit

Permalink
Merge pull request #99 from SamTV12345/96-add-feed-by-url
Browse files Browse the repository at this point in the history
96 add feed by url
  • Loading branch information
SamTV12345 authored May 2, 2023
2 parents 6d8b478 + 761a750 commit 3f5757e
Show file tree
Hide file tree
Showing 13 changed files with 400 additions and 226 deletions.
45 changes: 45 additions & 0 deletions src/controllers/podcast_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::service::file_service::FileService;
use awc::Client as AwcClient;
use crate::models::itunes_models::Podcast;
use crate::models::messages::BroadcastMessage;
use crate::models::podcast_rssadd_model::PodcastRSSAddModel;

#[utoipa::path(
context_path="/api/v1",
Expand Down Expand Up @@ -189,6 +190,50 @@ pub async fn add_podcast(
}.into();
}

#[post("/podcast/feed")]
pub async fn add_podcast_by_feed(
rss_feed: web::Json<PodcastRSSAddModel>,
lobby: Data<Addr<Lobby>>,
podcast_service: Data<Mutex<PodcastService>>,
conn: Data<DbPool>, rq: HttpRequest) -> impl Responder {
let mut podcast_service = podcast_service
.lock()
.ignore_poison();
return match User::get_username_from_req_header(&rq) {
Ok(username) => {
match User::check_if_admin_or_uploader(&username, &mut conn.get().unwrap()) {
Some(err) => {
return err;
}
None => {
let client = AsyncClientBuilder::new().build().unwrap();
let result = client.get(rss_feed.clone().rss_feed_url).send().await.unwrap();
let bytes = result.bytes().await.unwrap();
let channel = Channel::read_from(&*bytes).unwrap();
let num = rand::thread_rng().gen_range(100..10000000);

let res = podcast_service.handle_insert_of_podcast(
&mut conn.get().unwrap(),
PodcastInsertModel {
feed_url: rss_feed.clone().rss_feed_url.clone(),
title: channel.title.clone(),
id: num,
image_url: channel.image.map(|i| i.url).unwrap_or("".to_string()),
},
MappingService::new(),
lobby,
).await.expect("Error handling insert of podcast");

HttpResponse::Ok().json(res)
}
}
}
Err(e) => {
return HttpResponse::BadRequest().json(e.to_string()).into();
}
}.into()
}

#[utoipa::path(
context_path="/api/v1",
request_body=OpmlModel,
Expand Down
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use crate::controllers::api_doc::ApiDoc;
use crate::controllers::notification_controller::{
dismiss_notifications, get_unread_notifications,
};
use crate::controllers::podcast_controller::{add_podcast, delete_podcast, find_all_podcasts, find_podcast, find_podcast_by_id, proxy_podcast, refresh_all_podcasts};
use crate::controllers::podcast_controller::{add_podcast, add_podcast_by_feed, delete_podcast, find_all_podcasts, find_podcast, find_podcast_by_id, proxy_podcast, refresh_all_podcasts};
use crate::controllers::podcast_controller::{
add_podcast_from_podindex, download_podcast, favorite_podcast, get_favored_podcasts,
import_podcasts_from_opml, query_for_podcast, update_active_podcast,
Expand Down Expand Up @@ -424,6 +424,7 @@ fn get_private_api(db: Pool<ConnectionManager<SqliteConnection>>) -> Scope<impl
web::scope("")
.wrap(Condition::new(enable_basic_auth, auth))
.wrap(Condition::new(enable_oidc_auth, oidc_auth))
.service(add_podcast_by_feed)
.service(refresh_all_podcasts)
.service(get_info)
.service(get_timeline)
Expand Down
1 change: 1 addition & 0 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ pub mod subscription;
pub mod subscription_changes_from_client;
pub mod device_subscription;
pub mod episode;
pub mod podcast_rssadd_model;
5 changes: 5 additions & 0 deletions src/models/podcast_rssadd_model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[derive(Debug, Serialize, Deserialize,Clone)]
pub struct PodcastRSSAddModel{
#[serde(rename = "rssFeedUrl")]
pub rss_feed_url: String,
}
31 changes: 31 additions & 0 deletions ui/src/components/AddHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {FC} from "react";
import {useTranslation} from "react-i18next";
import {ConfigModel} from "../models/SysInfo";
import {AddTypes} from "../models/AddTypes";

type AddHeaderProps = {
selectedSearchType: AddTypes;
setSelectedSearchType: (type: AddTypes) => void,
configModel: ConfigModel|undefined
}

export const AddHeader:FC<AddHeaderProps> = ({selectedSearchType,setSelectedSearchType, configModel})=>{
const {t} = useTranslation()
return <ul id="podcast-add-decider" className="flex flex-wrap text-sm font-medium text-center border-b border-gray-700 text-gray-400">
<li className="mr-2">
<div className={`cursor-pointer inline-block p-4 rounded-t-lg ${selectedSearchType=== "itunes"&& 'active'}`} onClick={()=>setSelectedSearchType(AddTypes.ITUNES)}>iTunes</div>
</li>
{configModel?.podindexConfigured&&<li className="mr-2">
<div
className={`cursor-pointer inline-block p-4 rounded-t-lg hover:bg-gray-800 hover:text-gray-300 ${selectedSearchType === "podindex" && 'active'}`} onClick={()=>setSelectedSearchType(AddTypes.PODINDEX)}>PodIndex</div>
</li>}
<li>
<div
className={`cursor-pointer inline-block p-4 rounded-t-lg hover:bg-gray-800 hover:text-gray-300 ${selectedSearchType === "opml" && 'active'}`} onClick={()=>setSelectedSearchType(AddTypes.OPML)}>{t('opml-file')}</div>
</li>
<li>
<div
className={`cursor-pointer inline-block p-4 rounded-t-lg hover:bg-gray-800 hover:text-gray-300 ${selectedSearchType === "feed" && 'active'}`} onClick={()=>setSelectedSearchType(AddTypes.FEED)}>{t('rss-feed-url')}</div>
</li>
</ul>
}
241 changes: 19 additions & 222 deletions ui/src/components/AddPodcast.tsx
Original file line number Diff line number Diff line change
@@ -1,235 +1,32 @@
import {Modal} from "./Modal";
import {useEffect, useRef, useState} from "react";
import {useDebounce} from "../utils/useDebounce";
import {apiURL} from "../utils/Utilities";
import axios, {AxiosResponse} from "axios";
import {AgnosticPodcastDataModel, GeneralModel, PodIndexModel} from "../models/PodcastAddModel";
import {useAppDispatch, useAppSelector} from "../store/hooks";
import {setSearchedPodcasts} from "../store/CommonSlice";
import {setModalOpen} from "../store/ModalSlice";
import {useState} from "react";
import {useAppSelector} from "../store/hooks";
import {useTranslation} from "react-i18next";
import {FileItem, readFile} from "../utils/FileUtils";
import {Spinner} from "./Spinner";
import {enqueueSnackbar} from "notistack";
import {setInProgress} from "../store/opmlImportSlice";
import {AddHeader} from "./AddHeader";
import {AddTypes} from "../models/AddTypes";
import {OpmlAdd} from "./OpmlAdd";
import {ProviderImportComponent} from "./ProviderImportComponent";
import {FeedURLComponent} from "./FeedURLComponent";

export const AddPodcast = ()=>{
const [searchText, setSearchText] = useState<string>("")
const dispatch = useAppDispatch()
const searchedPodcasts = useAppSelector(state=>state.common.searchedPodcasts)
const {t} = useTranslation()
const [selectedSearchType, setSelectedSearchType] = useState<"itunes"|"podindex"|"opml">("itunes")
const configModel = useAppSelector(state=>state.common.configModel)
const [dragState, setDragState] = useState<DragState>("none")
type DragState = "none" | "allowed" | "invalid"
const fileInputRef = useRef<HTMLInputElement>(null)
const [files, setFiles] = useState<FileItem[]>([])
const [loading, setLoading] = useState<boolean>()
const opmlUploading = useAppSelector(state=>state.opmlImport.inProgress)
const progress = useAppSelector(state => state.opmlImport.progress)
const messages = useAppSelector(state=>state.opmlImport.messages)
const [podcastsToUpload, setPodcastsToUpload] = useState<number>(0)

useEffect(()=>{
if (progress.length===podcastsToUpload){
dispatch(setInProgress(false))
}
},[progress])

type AddPostPostModel = {
trackId: number,
userId: number
}

const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.dataTransfer.dropEffect = "copy"
}
const handleDropColor =()=> {
switch (dragState) {
case "none":
return "border-double"
case "allowed":
return "border-dashed"
case "invalid":
return "border-solid border-red-500"
}
}

const uploadOpml = ()=>{
let content = files[0].content
const count = (content.match(/type="rss"/g) || []).length;
setPodcastsToUpload(count)
axios.post(apiURL+"/podcast/opml", {
content: files[0].content
})
.then((v)=>{
console.log(v)
})
.catch((e)=>{
console.log(e)
})
}

const handleDrop = (e: React.DragEvent) => {
e.preventDefault()

const fileList: Promise<FileItem>[] = []
for (const f of e.dataTransfer.files) {
fileList.push(readFile(f))
}
Promise.all(fileList).then(e => {
setFiles(e)
})
setDragState("none")
}

useDebounce(()=>{
setLoading(true)
selectedSearchType === "itunes"?
axios.get(apiURL+"/podcasts/0/"+searchText+"/search")
.then((v:AxiosResponse<GeneralModel>)=>{
setLoading(false)
const agnosticModel:AgnosticPodcastDataModel[] = v.data.results.map((podcast)=>{
return {
title: podcast.collectionName,
artist: podcast.artistName,
id: podcast.trackId,
imageUrl: podcast.artworkUrl600
}
})

dispatch(setSearchedPodcasts(agnosticModel))
}):axios.get(apiURL+"/podcasts/1/"+searchText+"/search")
.then((v:AxiosResponse<PodIndexModel>)=>{
setLoading(false)
let agnosticModel: AgnosticPodcastDataModel[] = v.data.feeds.map((podcast)=>{
return {
title: podcast.title,
artist: podcast.author,
id: podcast.id,
imageUrl: podcast.artwork
}
})
dispatch(setSearchedPodcasts(agnosticModel))
})
},
2000,[searchText])

const addPodcast = (podcast:AddPostPostModel)=>{
axios.post(apiURL+"/podcast/"+selectedSearchType,podcast).then(()=>{
dispatch(setModalOpen(false))
}).catch(()=>enqueueSnackbar(t('not-admin-or-uploader'),{variant: "error"}))
}

const handleClick = () => {
fileInputRef.current?.click()
}

const handleInputChanged = (e: any) => {
uploadFiles(e.target.files[0])
}


const uploadFiles = (files: File) => {
const fileList: Promise<FileItem>[] = []
fileList.push(readFile(files))

Promise.all(fileList).then(e => {
setFiles(e)
})
}
export const AddPodcast = ()=>{
const {t} = useTranslation()
const [selectedSearchType, setSelectedSearchType] = useState<AddTypes>(AddTypes.ITUNES)
const configModel = useAppSelector(state=>state.common.configModel)

console.log("Länge: "+podcastsToUpload)
console.log("Progress: "+progress.length)
console.log((progress.length/podcastsToUpload)*100)
return <Modal onCancel={()=>{}} onAccept={()=>{}} headerText={t('add-podcast')!} onDelete={()=>{}} cancelText={"Abbrechen"} acceptText={"Hinzufügen"} >
<div>
<ul id="podcast-add-decider" className="flex flex-wrap text-sm font-medium text-center border-b border-gray-700 text-gray-400">
<li className="mr-2">
<div className={`cursor-pointer inline-block p-4 rounded-t-lg ${selectedSearchType=== "itunes"&& 'active'}`} onClick={()=>setSelectedSearchType("itunes")}>iTunes</div>
</li>
{configModel?.podindexConfigured&&<li className="mr-2">
<div
className={`cursor-pointer inline-block p-4 rounded-t-lg hover:bg-gray-800 hover:text-gray-300 ${selectedSearchType === "podindex" && 'active'}`} onClick={()=>setSelectedSearchType("podindex")}>PodIndex</div>
</li>}
<li>
<div
className={`cursor-pointer inline-block p-4 rounded-t-lg hover:bg-gray-800 hover:text-gray-300 ${selectedSearchType === "opml" && 'active'}`} onClick={()=>setSelectedSearchType("opml")}>OPML-File</div>
</li>
</ul>
{selectedSearchType!=="opml"&&<div className="flex flex-col gap-4">
<input value={searchText} placeholder={t('search-podcast')!}
className={"border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"}
onChange={(v)=>setSearchText(v.target.value)}/>
<div className="border-2 border-gray-600 rounded p-5 max-h-80 overflow-y-scroll">
{loading?<div className="grid place-items-center"><Spinner className="w-12 h-12"/></div>:
searchedPodcasts&& searchedPodcasts.map((podcast, index)=>{
return <div key={index}>
<div className="flex">
<div className="flex-1 grid grid-rows-2">
<div>{podcast.title}</div>
<div className="text-sm">{podcast.artist}</div>
</div>
<div>
<button className="fa fa-plus bg-blue-900 text-white p-3" onClick={()=>{
addPodcast({
trackId: podcast.id,
userId:1
})
}}></button>
</div>
</div>
<hr className="border-gray-500"/>
</div>
})
}
</div>
</div>}
<AddHeader selectedSearchType={selectedSearchType} setSelectedSearchType={setSelectedSearchType} configModel={configModel}/>
{selectedSearchType!==AddTypes.OPML&& selectedSearchType!==AddTypes.FEED&&
<ProviderImportComponent selectedSearchType={selectedSearchType}/>
}
{
selectedSearchType==="opml"&&<div className="flex flex-col gap-4">
{files.length===0&&<><div className={`p-4 border-4 ${handleDropColor()} border-dashed border-gray-500 text-center w-full h-40 grid place-items-center cursor-pointer`}
onDragEnter={() => setDragState("allowed")}
onDragLeave={() => setDragState("none")}
onDragOver={handleDragOver} onDrop={handleDrop}
onClick={handleClick}>
{t('drag-here')}
</div>
<input type={"file"} ref={fileInputRef} accept="application/xml, .opml" hidden onChange={(e)=>{
handleInputChanged(e)}
} /></>}
{
files.length > 0&& !opmlUploading && files.length===0 && <div>
{t('following-file-uploaded')}
<div className="ml-4" onClick={()=>{setFiles([])}}>{files[0].name}<i className="ml-5 fa-solid cursor-pointer active:scale-90 fa-x text-red-700"></i></div>
</div>
}
{
opmlUploading&& <>
<div className="mt-4">
{t('progress')}: {progress.length}/{podcastsToUpload}
</div>{ podcastsToUpload>0 && progress.length>0&&<div className="mt-2 w-full rounded-full h-2.5 bg-gray-700">

<div className="bg-blue-600 h-2.5 rounded-full" style={{width:`${(progress.length/podcastsToUpload)*100}%`}}></div>
{
!opmlUploading && <div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} className="w-6 h-6 text-slate-800">
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
</div>
}
</div>
}
</>
}

<div className="flex">
<div className="flex-1"/>
<button className="bg-blue-800 p-2 disabled:bg-gray-800" disabled={files.length==0} onClick={()=>{
dispatch(setInProgress(true))
uploadOpml()}}>Upload OPML</button>
</div>
</div>
selectedSearchType===AddTypes.OPML&&<OpmlAdd selectedSearchType={selectedSearchType}/>
}
{
selectedSearchType === AddTypes.FEED&&<FeedURLComponent/>
}
</div>
</Modal>
Expand Down
Loading

0 comments on commit 3f5757e

Please sign in to comment.