Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

298 page listing the episodes not yet listened #305

Merged
merged 4 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/controllers/podcast_episode_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub async fn find_all_podcast_episodes_of_podcast(
pub struct TimeLinePodcastEpisode {
podcast_episode: PodcastEpisode,
podcast: Podcast,
history: Option<PodcastHistoryItem>,
favorite: Option<Favorite>,
}

Expand All @@ -90,6 +91,7 @@ pub struct TimeLinePodcastItem {
pub struct TimelineQueryParams {
pub favored_only: bool,
pub last_timestamp: Option<String>,
pub not_listened: bool,
}

#[utoipa::path(
Expand All @@ -109,12 +111,13 @@ Data<Mutex<MappingService>>, favored_only: Query<TimelineQueryParams>) -> Result
favored_only.into_inner())?;

let mapped_timeline = res.data.iter().map(|podcast_episode| {
let (podcast_episode, podcast, favorite) = podcast_episode.clone();
let (podcast_episode, podcast,history, favorite) = podcast_episode.clone();
let mapped_podcast_episode = mapping_service.map_podcastepisode_to_dto(&podcast_episode);

TimeLinePodcastEpisode {
podcast_episode: mapped_podcast_episode,
podcast,
history,
favorite,
}
}).collect::<Vec<TimeLinePodcastEpisode>>();
Expand Down
41 changes: 34 additions & 7 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ use crate::models::podcast_episode::PodcastEpisode;
use crate::models::podcasts::Podcast;
use diesel::prelude::*;
use diesel::{RunQueryDsl};
use diesel::dsl::max;
use crate::controllers::podcast_episode_controller::TimelineQueryParams;
use crate::{DbConnection};
use crate::models::favorites::Favorite;
use crate::models::filter::Filter;
use crate::models::podcast_history_item::PodcastHistoryItem;
use crate::utils::error::{CustomError, map_db_error};

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TimelineItem {
pub data: Vec<(PodcastEpisode, Podcast, Option<Favorite>)>,
pub data: Vec<(PodcastEpisode, Podcast, Option<PodcastHistoryItem>, Option<Favorite>)>,
pub total_elements: i64,
}

Expand All @@ -26,17 +28,34 @@ impl TimelineItem {
use crate::dbconfig::schema::favorites::username as f_username;
use crate::dbconfig::schema::favorites::podcast_id as f_podcast_id;
use crate::dbconfig::schema::podcast_episodes::podcast_id as e_podcast_id;
use crate::dbconfig::schema::podcast_history_items as phi_struct;
use crate::dbconfig::schema::podcast_history_items::username as phi_username;
use crate::dbconfig::schema::podcast_history_items::date as phistory_date;
use crate::dbconfig::schema::podcast_history_items::episode_id as ehid;

Filter::save_decision_for_timeline(username_to_search.clone(), conn, favored_only.favored_only);

let mut query = podcast_episodes.inner_join(podcasts.on(e_podcast_id.eq(pid)))
let (ph1, ph2) = diesel::alias!(phi_struct as ph1, phi_struct as ph2);

let subquery = ph2
.select(max(ph2.field(phistory_date)))
.filter(ph2.field(ehid).eq(episode_id))
.filter(ph2.field(phi_username)
.eq(username_to_search.clone()))
.group_by(ph2.field(ehid));

let part_query = podcast_episodes
.inner_join(podcasts.on(e_podcast_id.eq(pid)))
.left_join(ph1.on(ph1.field(ehid).eq(episode_id)))
.filter(ph1.field(phistory_date).nullable().eq_any(subquery.clone())
.or(ph1.field(phistory_date).is_null()))
.left_join(favorites.on(f_username.eq(username_to_search.clone()).and(f_podcast_id.eq(pid))))
.order(date_of_recording.desc())
.order(date_of_recording.desc());
let mut query = part_query.clone()
.limit(20)
.into_boxed();

let mut total_count = podcast_episodes.inner_join(podcasts.on(e_podcast_id.eq(pid)))
.left_join(favorites.on(f_username.eq(username_to_search.clone()).and(f_podcast_id.eq(pid))))
let mut total_count = part_query
.count()
.into_boxed();

Expand All @@ -58,8 +77,16 @@ impl TimelineItem {

}
}
let results = total_count.get_result::<i64>(conn).expect("Error counting results");
let result = query.load::<(PodcastEpisode, Podcast, Option<Favorite>)>(conn).map_err

if favored_only.not_listened {
query = query.filter(ph1.field(phistory_date).nullable().ne_all(subquery.clone()));
total_count = total_count.filter(ph1.field(phistory_date).nullable().ne_all(subquery));
}
let results = total_count.get_result::<i64>(conn).map_err(map_db_error)?;
let result = query.load::<(PodcastEpisode, Podcast, Option<PodcastHistoryItem>,
Option<Favorite>)>
(conn)
.map_err
(map_db_error)?;

Ok(TimelineItem {
Expand Down
12 changes: 8 additions & 4 deletions ui/src/components/TimelineEpisode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,33 @@ import { addTimelineEpisodes } from '../store/CommonSlice'
import { apiURL } from '../utils/Utilities'
import { TimelineHATEOASModel, TimeLineModel } from '../models/TimeLineModel'
import { EpisodeCard } from './EpisodeCard'
import {PodcastWatchedModel} from "../models/PodcastWatchedModel";

type TimelineEpisodeProps = {
podcastEpisode: TimeLineModel,
index: number,
timelineLength: number,
totalLength: number,
timeLineEpisodes: TimelineHATEOASModel
timeLineEpisodes: TimelineHATEOASModel,
notListened: boolean,
podcastHistoryItem?: PodcastWatchedModel
}

export const TimelineEpisode: FC<TimelineEpisodeProps> = ({ podcastEpisode, index, timelineLength, timeLineEpisodes }) => {
export const TimelineEpisode: FC<TimelineEpisodeProps> = ({ podcastEpisode,podcastHistoryItem, notListened, index, timelineLength, timeLineEpisodes }) => {
const dispatch = useAppDispatch()

return (
<>
<EpisodeCard podcast={podcastEpisode.podcast} podcastEpisode={podcastEpisode.podcast_episode} />
<EpisodeCard watchedTime={podcastHistoryItem?.watchedTime} totalTime={podcastEpisode?.podcast_episode.total_time} podcast={podcastEpisode.podcast} podcastEpisode={podcastEpisode.podcast_episode} />

{/*Infinite scroll */
timeLineEpisodes.data.length === index + 1 &&
<Waypoint key={index + 'waypoint'} onEnter={() => {
axios.get(apiURL + '/podcasts/timeline', {
params:{
lastTimestamp: podcastEpisode.podcast_episode.date_of_recording,
favoredOnly: store.getState().common.filters?.onlyFavored
favoredOnly: store.getState().common.filters?.onlyFavored,
notListened: notListened
}
})
.then((response: AxiosResponse<TimelineHATEOASModel>) => {
Expand Down
4 changes: 2 additions & 2 deletions ui/src/language/json/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
"dash-separated-with-space": "Séparé par des tirets avec espacement",
"remove": "Supprimer",
"standard-podcast-format": "Format standard du podcast",
"onlyFavored": "Seulement favoris podcasts",
"onlyFavored": "Seulement podcasts favoris",
"available-episodes": "Épisodes disponibles",

"days-to-keep-explanation" : "Reflète le nombre de jours pendant lesquels un podcast est conservé, c'est-à-dire stocké sur le serveur. Ce paramètre n'est actif que si le nettoyage automatique est activé ou si l'on clique sur Exécuter le nettoyage maintenant",
Expand All @@ -171,7 +171,7 @@
"episode-name": "Nom de l'épisode",
"clear-all": "Tout effacer",
"use-direct-paths": "Utiliser des chemins directs",
"not-yet-played": "Pas encore joué",
"not-yet-played": "Pas encore écouté",
"podcast-episode-played": "{{percentage}} joué",
"restart-playing": "Recommencer à jouer",
"you-already-listened": "Vous avez déjà écouté l'épisode <name/>. Vous pouvez le réécouter en cliquant sur le bouton ci-dessous"
Expand Down
4 changes: 3 additions & 1 deletion ui/src/models/TimeLineModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Podcast, PodcastEpisode} from "../store/CommonSlice";
import {PodcastWatchedModel} from "./PodcastWatchedModel";

export type TimelineHATEOASModel = {
data: TimeLineModel[],
Expand All @@ -7,5 +8,6 @@ export type TimelineHATEOASModel = {

export type TimeLineModel = {
podcast: Podcast,
podcast_episode: PodcastEpisode
podcast_episode: PodcastEpisode,
history: PodcastWatchedModel
}
3 changes: 2 additions & 1 deletion ui/src/pages/Homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export const Homepage = () => {

axios.get<TimelineHATEOASModel>(apiURL + '/podcasts/timeline', {
params: {
favoredOnly: false
favoredOnly: false,
notListened: false
}
})
.then((response) => {
Expand Down
21 changes: 15 additions & 6 deletions ui/src/pages/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment, useEffect } from 'react'
import {Fragment, useEffect, useState} from 'react'
import { useTranslation } from 'react-i18next'
import axios, { AxiosResponse } from 'axios'
import { apiURL, formatTime, getFiltersDefault } from '../utils/Utilities'
Expand All @@ -16,15 +16,14 @@ export const Timeline = () => {
const timeLineEpisodes = useAppSelector(state => state.common.timeLineEpisodes)
const filter = useAppSelector(state => state.common.filters)
const { t } = useTranslation()

let currentTime = ""
const [notListened, setNotListened] = useState(false)

useEffect(() => {
!filter && axios.get(apiURL + '/podcasts/filter')
.then((filterAxiosResponse: AxiosResponse<Filter>) => {
filterAxiosResponse.data == null && dispatch(setFilters(getFiltersDefault()))

filterAxiosResponse.data &&dispatch(setFilters(filterAxiosResponse.data))
filterAxiosResponse.data && dispatch(setFilters(filterAxiosResponse.data))
})
}, [])

Expand All @@ -34,14 +33,15 @@ export const Timeline = () => {

axios.get(apiURL + '/podcasts/timeline', {
params: {
favoredOnly: favoredOnly === undefined ? false : favoredOnly
favoredOnly: favoredOnly === undefined ? false : favoredOnly,
notListened: notListened
}
})
.then((c: AxiosResponse<TimelineHATEOASModel>) => {
dispatch(setTimeLineEpisodes(c.data))
})
}
}, [filter])
}, [filter,notListened])

if(timeLineEpisodes === undefined){
return <Loading/>
Expand All @@ -53,6 +53,7 @@ export const Timeline = () => {
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-10">
<Heading1>{t('timeline')}</Heading1>

<div className="flex flex-row gap-5">
<div className="flex items-center gap-3">
<span className="text-xs text-[--fg-secondary-color]">{t('onlyFavored')}</span>

Expand All @@ -61,6 +62,12 @@ export const Timeline = () => {
onlyFavored: !filter?.onlyFavored
}))}/>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-[--fg-secondary-color]">{t('not-yet-played')}</span>

<Switcher checked={notListened} setChecked={() => setNotListened(!notListened)}/>
</div>
</div>
</div>

<div className="relative grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-x-8 gap-y-12 pl-6">
Expand All @@ -79,6 +86,8 @@ export const Timeline = () => {
</>) : ''}

<TimelineEpisode
podcastHistoryItem={e.history}
notListened={notListened}
podcastEpisode={e}
key={e.podcast_episode.episode_id+index + "Parent"}
index={index}
Expand Down
Loading