Skip to content

Commit

Permalink
Merge pull request #70 from SamTV12345/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
SamTV12345 authored Apr 20, 2023
2 parents 5b38efa + 710399c commit 13b26b9
Show file tree
Hide file tree
Showing 17 changed files with 231 additions and 28 deletions.
36 changes: 35 additions & 1 deletion src/controllers/podcast_episode_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ use crate::db::DB;
use crate::service::mapping_service::MappingService;
use crate::service::podcast_episode_service::PodcastEpisodeService;
use actix_web::web::{Data, Query};
use actix_web::{get, put};
use actix_web::{get, HttpRequest, put};
use actix_web::{web, HttpResponse, Responder};
use serde_json::from_str;
use std::sync::Mutex;
use std::thread;
use crate::controllers::watch_time_controller::get_username;
use crate::DbPool;
use crate::models::itunes_models::{Podcast, PodcastEpisode};
use crate::mutex::LockResultExt;

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -45,6 +47,38 @@ pub async fn find_all_podcast_episodes_of_podcast(
HttpResponse::Ok().json(mapped_podcasts)
}

#[derive(Serialize, Deserialize)]
pub struct TimeLinePodcastEpisode {
podcast_episode: PodcastEpisode,
podcast: Podcast,
}

#[get("/podcasts/timeline")]
pub async fn get_timeline(conn: Data<DbPool>, req: HttpRequest, mapping_service:
Data<Mutex<MappingService>>) ->
impl
Responder {
let mapping_service = mapping_service.lock().ignore_poison();
let username = get_username(req);
if username.is_err(){
return HttpResponse::BadRequest().json("Username not found");
}


let res = DB::get_timeline(username.unwrap(),&mut conn.get().unwrap());

let mapped_timeline = res.iter().map(|podcast_episode| {
let (podcast_episode, podcast) = podcast_episode;
let mapped_podcast_episode = mapping_service.map_podcastepisode_to_dto(podcast_episode);

TimeLinePodcastEpisode{
podcast_episode: mapped_podcast_episode,
podcast: podcast.clone()
}
}).collect::<Vec<TimeLinePodcastEpisode>>();
HttpResponse::Ok().json(mapped_timeline)
}

/**
* id is the episode id (uuid)
*/
Expand Down
19 changes: 18 additions & 1 deletion src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ impl DB {
-> Result<Vec<PodcastDto>, String> {
use crate::schema::podcasts::dsl::podcasts;
use crate::schema::favorites::dsl::favorites as f_db;
use crate::schema::favorites::dsl::podcast_id as f_id;
use crate::schema::podcasts::id as p_id;
use crate::schema::favorites::dsl::username;
let result = podcasts
.left_join(f_db.on(username.eq(u)))
.left_join(f_db.on(username.eq(u).and(f_id.eq(p_id))))
.load::<(Podcast, Option<Favorite>)>(conn)
.expect("Error loading podcasts");

Expand Down Expand Up @@ -741,4 +743,19 @@ impl DB {
.load::<PodcastEpisode>(&mut self.conn)
.expect("Error loading podcast episode by id")
}


pub fn get_timeline(username_to_search: String, conn: &mut SqliteConnection) -> Vec<
(PodcastEpisode, Podcast)> {
let podcast_timeline = sql_query("SELECT * FROM podcast_episodes,podcasts, \
favorites \
WHERE podcasts.id = podcast_episodes.podcast_id AND podcasts.id = favorites.podcast_id \
AND favorites.username=? AND favored=1 ORDER BY podcast_episodes.date_of_recording DESC \
LIMIT 20");

let res = podcast_timeline.bind::<Text, _>(&username_to_search);


res.load::<(PodcastEpisode, Podcast)>(conn).expect("Error loading podcast episode by id")
}
}
5 changes: 2 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ 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,
};
use crate::controllers::podcast_episode_controller::{
download_podcast_episodes_of_podcast, find_all_podcast_episodes_of_podcast,
};
use crate::controllers::podcast_episode_controller::{download_podcast_episodes_of_podcast, find_all_podcast_episodes_of_podcast, get_timeline};
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};
Expand Down Expand Up @@ -405,6 +403,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(get_timeline)
.configure(config_secure_user_management)
.service(find_podcast)
.service(add_podcast)
Expand Down
26 changes: 23 additions & 3 deletions src/models/itunes_models.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::schema::*;
use chrono::NaiveDateTime;
use diesel::prelude::{Insertable, Queryable, Identifiable, Selectable};
use diesel::prelude::{Queryable, Identifiable, Selectable, QueryableByName};
use utoipa::ToSchema;
use diesel::sql_types::{Integer, Text, Nullable, Bool, Timestamp};

#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
Expand Down Expand Up @@ -49,7 +50,8 @@ pub struct ResponseModel {
pub results: Vec<ItunesModel>,
}

#[derive(Queryable, Identifiable, Selectable, Debug, PartialEq, Clone, ToSchema,Serialize, Deserialize)]
#[derive(Queryable, Identifiable,QueryableByName, Selectable, Debug, PartialEq, Clone, ToSchema,
Serialize, Deserialize)]
pub struct Podcast {
#[diesel(sql_type = Integer)]
pub(crate) id: i32,
Expand All @@ -73,8 +75,12 @@ pub struct Podcast {
pub last_build_date: Option<String>,
#[diesel(sql_type = Nullable<Text>)]
pub author: Option<String>,
#[diesel(sql_type = Bool)]
pub active: bool,
#[diesel(sql_type = Text)]
pub original_image_url: String,
#[diesel(sql_type = Text)]

pub directory_name:String
}

Expand All @@ -97,19 +103,33 @@ pub struct PodcastDto {
}


#[derive(Serialize, Deserialize, Queryable, Insertable, Clone, Debug, ToSchema)]
#[derive(Queryable, Identifiable,QueryableByName, Selectable, Debug, PartialEq, Clone, ToSchema,
Serialize, Deserialize)]
pub struct PodcastEpisode {
#[diesel(sql_type = Integer)]
pub(crate) id: i32,
#[diesel(sql_type = Integer)]
pub(crate) podcast_id: i32,
#[diesel(sql_type = Text)]
pub(crate) episode_id: String,
#[diesel(sql_type = Text)]
pub(crate) name: String,
#[diesel(sql_type = Text)]
pub(crate) url: String,
#[diesel(sql_type = Text)]
pub(crate) date_of_recording: String,
#[diesel(sql_type = Text)]
pub image_url: String,
#[diesel(sql_type = Integer)]
pub total_time: i32,
#[diesel(sql_type = Text)]
pub(crate) local_url: String,
#[diesel(sql_type = Text)]
pub(crate) local_image_url: String,
#[diesel(sql_type = Text)]
pub(crate) description: String,
#[diesel(sql_type = Text)]
pub(crate) status: String,
#[diesel(sql_type = Nullable<Timestamp>)]
pub(crate) download_time: Option<NaiveDateTime>
}
3 changes: 2 additions & 1 deletion src/service/file_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,6 @@ impl FileService {

pub fn prepare_podcast_title_to_directory(title: &str) ->String {
let re = Regex::new(r"[^a-zA-Z0-9_./]").unwrap();
re.replace_all(title, "").to_string()
let res = re.replace_all(title, "").to_string();
res.replace("..","")
}
4 changes: 3 additions & 1 deletion ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ import {
PodcastDetailViewLazyLoad,
PodcastInfoViewLazyLoad,
PodcastViewLazyLoad,
SettingsViewLazyLoad
SettingsViewLazyLoad, TimeLineViewLazyLoad
} from "./utils/LazyLoading";
import {apiURL, configWSUrl, isJsonString} from "./utils/Utilities";
import {LoginComponent} from "./components/LoginComponent";
import {enqueueSnackbar} from "notistack";
import {useTranslation} from "react-i18next";
import {InviteComponent} from "./components/InviteComponent";
import {Timeline} from "./pages/Timeline";


export const router = createBrowserRouter(createRoutesFromElements(
Expand All @@ -33,6 +34,7 @@ export const router = createBrowserRouter(createRoutesFromElements(
<Route path={":id/episodes"} element={<Suspense><PodcastDetailViewLazyLoad/></Suspense>}/>
<Route path={":id/episodes/:podcastid"} element={<Suspense><PodcastDetailViewLazyLoad/></Suspense>}/>
</Route>
<Route path="timeline" element={<Suspense><TimeLineViewLazyLoad/></Suspense>}/>
<Route path={"favorites"}>
<Route element={<PodcastViewLazyLoad onlyFavorites={true}/>} index/>
<Route path={":id/episodes"} element={<Suspense><PodcastDetailViewLazyLoad/></Suspense>}/>
Expand Down
44 changes: 44 additions & 0 deletions ui/src/components/PodcastEpisodeTimeLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {PlayIcon} from "./PlayIcon";
import axios, {AxiosResponse} from "axios";
import {apiURL, prepareOnlinePodcastEpisode, preparePodcastEpisode} from "../utils/Utilities";
import {PodcastWatchedModel} from "../models/PodcastWatchedModel";
import {store} from "../store/store";
import {setCurrentPodcast, setCurrentPodcastEpisode, setPlaying} from "../store/AudioPlayerSlice";
import {FC} from "react";
import {selectPodcastImage} from "../pages/Homepage";
import {TimeLineModel} from "../models/TimeLineModel";
import {useAppDispatch} from "../store/hooks";


type PodcastEpisodeTimeLineProps = {
podcastEpisode: TimeLineModel
}

export const PodcastEpisodeTimeLine:FC<PodcastEpisodeTimeLineProps> = ({podcastEpisode}) => {
const dispatch = useAppDispatch()

return <div key={podcastEpisode.podcast_episode.episode_id+"dv"}
className="max-w-sm rounded-lg shadow bg-gray-800 border-gray-700">
<div className="relative" key={podcastEpisode.podcast_episode.episode_id}>
<img src={selectPodcastImage(podcastEpisode.podcast_episode)} alt="" className=""/>
<div className="absolute left-0 top-0 w-full h-full hover:bg-gray-500 opacity-80 z-10 grid place-items-center play-button-background">
<PlayIcon key={podcastEpisode.podcast_episode.episode_id+"icon"} podcast={podcastEpisode.podcast_episode} className="w-20 h-20 opacity-0" onClick={()=>{
axios.get(apiURL+"/podcast/episode/"+podcastEpisode.podcast_episode.episode_id)
.then((response: AxiosResponse<PodcastWatchedModel>)=>{
if (podcastEpisode.podcast_episode.local_image_url.trim().length>1){
store.dispatch(setCurrentPodcastEpisode(preparePodcastEpisode(podcastEpisode.podcast_episode, response.data)))
}
else{
store.dispatch(setCurrentPodcastEpisode(prepareOnlinePodcastEpisode(podcastEpisode.podcast_episode, response.data)))
}
dispatch(setCurrentPodcast(podcastEpisode.podcast))
dispatch(setPlaying(true))
})
}}/>
</div>
</div>
<div className="p-5">
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white break-words">{podcastEpisode.podcast_episode.name}</h5>
</div>
</div>
}
1 change: 1 addition & 0 deletions ui/src/components/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const SideBar = ()=>{
<SideBarItem highlightPath={'./'} translationkey={t('homepage')} icon={<i className="fa-solid fa-house fa-xl"></i>}/>
<SideBarItem highlightPath={'podcasts'} translationkey={t('podcasts')} icon={<i className="fa-solid fa-guitar fa-xl"></i>}/>
<SideBarItem highlightPath={"favorites"} translationkey={t('favorites')} icon={<i className="fa-solid fa-star"></i>}/>
<SideBarItem highlightPath={"timeline"} translationkey={t('timeline')} icon={<i className="fa-solid fa-timeline fa-xl"/> }/>
<SideBarItem highlightPath={"info"} translationkey={t('info')} icon={<i className="fa-solid fa-info-circle fa-xl"></i>}/>
<SideBarItem highlightPath={"settings"} translationkey={t('settings')} icon={<i className="fa-solid fa-wrench fa-xl"/> }/>
{(config?.oidcConfig|| config?.basicAuth)&&<SideBarItem highlightPath={"administration"} translationkey={t('administration')} icon={<i className="fa-solid fa-gavel fa-xl"/> }/>}
Expand Down
3 changes: 2 additions & 1 deletion ui/src/language/json/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,6 @@
"password-too-weak": "Passwort zu schwach. Ein sicheres Passwort hat eine Länge 8 und enthält mindestens eine Zahl, einen Großbuchstaben und einen Kleinbuchstaben.",
"not-admin": "Sie sind kein Administrator",
"not-admin-or-uploader": "Sie sind weder Administrator noch Uploader",
"stream-podcast-episode": "Podcast-Episode streamen"
"stream-podcast-episode": "Podcast-Episode streamen",
"timeline": "Zeitleiste"
}
3 changes: 2 additions & 1 deletion ui/src/language/json/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,6 @@
"password-too-weak": "Password too weak. A secure password contains at least 8 characters, a number, a lowercase and an uppercase letter.",
"not-admin": "You are not an admin",
"not-admin-or-uploader": "You are not an admin or an uploader",
"stream-podcast-episode": "Stream podcast episode"
"stream-podcast-episode": "Stream podcast episode",
"timeline": "Timeline"
}
3 changes: 2 additions & 1 deletion ui/src/language/json/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,6 @@
"password-too-weak": "Le mot de passe est trop faible. Il doit contenir au moins 8 caractères, dont au moins une lettre majuscule, une lettre minuscule et un chiffre.",
"not-admin": "Vous n'êtes pas administrateur",
"not-admin-or-uploader": "Vous n'êtes pas administrateur ou uploader",
"stream-podcast-episode": "Écouter l'épisode"
"stream-podcast-episode": "Écouter l'épisode",
"timeline": "Chronologie"
}
6 changes: 6 additions & 0 deletions ui/src/models/TimeLineModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {Podcast, PodcastEpisode} from "../store/CommonSlice";

export type TimeLineModel = {
podcast: Podcast,
podcast_episode: PodcastEpisode
}
34 changes: 26 additions & 8 deletions ui/src/pages/Homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,32 @@ import {store} from "../store/store";
import {setCurrentPodcast, setCurrentPodcastEpisode, setPlaying} from "../store/AudioPlayerSlice";
import {useAppDispatch} from "../store/hooks";
import {useTranslation} from "react-i18next";
import {PodcastEpisode} from "../store/CommonSlice";

const isPodcastWatchedEpisodeModel = (podcast: PodcastWatchedEpisodeModel|PodcastEpisode): podcast is PodcastWatchedEpisodeModel => {
return (podcast as PodcastWatchedEpisodeModel).watchedTime !== undefined;
}

export const selectPodcastImage = (podcast: PodcastWatchedEpisodeModel|PodcastEpisode) => {
if (isPodcastWatchedEpisodeModel(podcast)){
if(podcast.podcastEpisode.local_image_url.length>1){
return preparePath(podcast.podcastEpisode.local_image_url)
}
else{
return podcast.podcastEpisode.image_url
}
}
else{
console.log(podcast.image_url)
if(podcast.local_image_url.trim().length>1){
return preparePath(podcast.local_image_url)
}
else{
return podcast.image_url
}
}

}
export const Homepage = () => {
const [podcastWatched, setPodcastWatched] = useState<PodcastWatchedEpisodeModel[]>([])
const dispatch = useAppDispatch()
Expand All @@ -24,14 +49,7 @@ export const Homepage = () => {

}, [])

const selectPodcastImage = (podcast: PodcastWatchedEpisodeModel) => {
if(podcast.podcastEpisode.local_image_url.length>1){
return preparePath(podcast.podcastEpisode.local_image_url)
}
else{
return podcast.podcastEpisode.image_url
}
}


return <div className="p-3">
<h1 className="font-bold text-2xl">{t('last-listened')}</h1>
Expand Down
52 changes: 52 additions & 0 deletions ui/src/pages/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {useTranslation} from "react-i18next";
import {useAppDispatch, useAppSelector} from "../store/hooks";
import {useEffect, useMemo} from "react";
import axios, {AxiosResponse} from "axios";
import {apiURL, formatTime} from "../utils/Utilities";
import {PodcastEpisode, setTimeLineEpisodes} from "../store/CommonSlice";
import {PodcastEpisodeTimeLine} from "../components/PodcastEpisodeTimeLine";
import {TimeLineModel} from "../models/TimeLineModel";

const convertToTimeLineEpisodes = (podcastEpisodes: TimeLineModel[]) => {
return podcastEpisodes.reduce((groups, game) => {
const date = game.podcast_episode.date_of_recording.split('T')[0];
// @ts-ignore
if (!groups[date]) {
// @ts-ignore
groups[date] = [];
}
// @ts-ignore
groups[date].push(game);
return groups;
}, {});
}

export const Timeline = ()=>{
const {t} = useTranslation()
const dispatch = useAppDispatch()
const timeLineEpisodes = useAppSelector(state=>state.common.timeLineEpisodes)
const mappedEpisodes = useMemo(()=>convertToTimeLineEpisodes(timeLineEpisodes),[timeLineEpisodes])

useEffect(()=>{
axios.get(apiURL+"/podcasts/timeline")
.then((c:AxiosResponse<TimeLineModel[]>)=>{
dispatch(setTimeLineEpisodes(c.data))
})
},
[])

console.log(convertToTimeLineEpisodes(timeLineEpisodes))

return <div className="p-3">
<h1 className="font-bold text-3xl">{t('timeline')}</h1>
{
Object.keys(mappedEpisodes).map((e)=> {
// @ts-ignore
let episodesOnDate = mappedEpisodes[e] as TimeLineModel[]
return <div key={e}><h2 className="text-xl">{formatTime(e)}</h2><div className="flex gap-4">{episodesOnDate.map((v)=><PodcastEpisodeTimeLine podcastEpisode={v} key={v.podcast_episode.episode_id+"Parent"}/>)}</div></div>
}
)

}
</div>
}
Loading

0 comments on commit 13b26b9

Please sign in to comment.