Skip to content

Commit

Permalink
814 add list of gpodder synchronized podcasts (#906)
Browse files Browse the repository at this point in the history
* Added gpodder list in UI for syncing podcasts across devices

* Fixed clippy

---------

Co-authored-by: SamTV12345 <[email protected]>
  • Loading branch information
SamTV12345 and SamTV12345 authored Aug 29, 2024
1 parent 9392229 commit a7ea8a6
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 11 deletions.
15 changes: 15 additions & 0 deletions src/controllers/podcast_episode_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ pub struct TimeLinePodcastEpisode {
favorite: Option<Favorite>,
}

#[get("/podcast/available/gpodder")]
pub async fn get_available_podcasts_not_in_webview(
requester: Option<web::ReqData<User>>,
conn: Data<DbPool>,
) -> Result<HttpResponse, CustomError> {
if !requester.unwrap().is_privileged_user() {
return Err(CustomError::Forbidden)
}
let mut retrieved_conn = conn.get().map_err(map_r2d2_error)?;
let found_episodes = Episode::
find_episodes_not_in_webview(&mut retrieved_conn)?;

Ok(HttpResponse::Ok().json(found_episodes))
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeLinePodcastItem {
Expand Down
6 changes: 2 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +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::{
delete_podcast_episode_locally, download_podcast_episodes_of_podcast,
find_all_podcast_episodes_of_podcast, get_timeline, retrieve_episode_sample_format,
};
use crate::controllers::podcast_episode_controller::{delete_podcast_episode_locally, download_podcast_episodes_of_podcast, find_all_podcast_episodes_of_podcast, get_available_podcasts_not_in_webview, get_timeline, retrieve_episode_sample_format};
use crate::controllers::settings_controller::{
get_opml, get_settings, run_cleanup, update_name, update_settings,
};
Expand Down Expand Up @@ -427,6 +424,7 @@ fn get_private_api() -> Scope<
.service(refresh_all_podcasts)
.service(get_info)
.service(get_timeline)
.service(get_available_podcasts_not_in_webview)
.configure(config_secure_user_management)
.service(find_podcast)
.service(add_podcast)
Expand Down
35 changes: 33 additions & 2 deletions src/models/episode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::dbconfig::schema::episodes::dsl::episodes as episodes_dsl;
use crate::DBType as DbConnection;
use chrono::{NaiveDateTime, Utc};
use diesel::sql_types::{Integer, Nullable, Text, Timestamp};
use diesel::ExpressionMethods;
use diesel::{debug_query, ExpressionMethods, JoinOnDsl};
use diesel::{
BoolExpressionMethods, Insertable, NullableExpressionMethods, OptionalExtension, QueryDsl,
QueryId, Queryable, QueryableByName, RunQueryDsl, Selectable,
Expand All @@ -13,8 +13,10 @@ use reqwest::Url;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::io::Error;
use diesel::query_dsl::methods::DistinctDsl;
use diesel::sqlite::Sqlite;
use utoipa::ToSchema;

use crate::models::gpodder_available_podcasts::GPodderAvailablePodcasts;
use crate::models::misc_models::{
PodcastWatchedEpisodeModelWithPodcastEpisode, PodcastWatchedPostModel,
};
Expand Down Expand Up @@ -267,6 +269,35 @@ impl Episode {
Ok(())
}


pub fn find_episodes_not_in_webview(conn: &mut DbConnection) -> Result<Vec<GPodderAvailablePodcasts>, CustomError> {
use crate::dbconfig::schema::episodes::dsl::episodes;
use crate::dbconfig::schema::episodes::device;
use crate::dbconfig::schema::episodes::podcast;
use crate::dbconfig::schema::podcasts::dsl::podcasts;
use crate::dbconfig::schema::podcasts::dsl::rssfeed;

let binding = DistinctDsl::distinct(episodes
.left_join(podcasts.on(podcast.eq(rssfeed)))
.select((device, podcast))
.filter(rssfeed.is_null()))
.filter(device.ne("webview"));
let sql = debug_query::<Sqlite, _>(&binding);


println!("SQL ist {}", sql);
let result = DistinctDsl::distinct(episodes
.left_join(podcasts.on(podcast.eq(rssfeed)))
.select((device, podcast))
.filter(rssfeed.is_null()))
.filter(device.ne("webview"))
.load::<GPodderAvailablePodcasts>(conn)
.map_err(map_db_error)?;


Ok(result)
}

pub fn delete_watchtime(conn: &mut DbConnection, podcast_id: i32) -> Result<(), CustomError> {
use crate::dbconfig::schema::episodes::dsl as ep_dsl;
use crate::dbconfig::schema::episodes::table as ep_table;
Expand Down
11 changes: 11 additions & 0 deletions src/models/gpodder_available_podcasts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use diesel::sql_types::Text;
use diesel::{Queryable, QueryableByName};

#[derive(Serialize, QueryableByName, Queryable)]
#[serde(rename_all = "camelCase")]
pub struct GPodderAvailablePodcasts {
#[diesel(sql_type = Text)]
pub device: String,
#[diesel(sql_type = Text)]
pub podcast: String
}
1 change: 1 addition & 0 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ pub mod subscription_changes_from_client;
pub mod user;
pub mod web_socket_message;
pub mod podcast_settings;
pub mod gpodder_available_podcasts;
2 changes: 2 additions & 0 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {UserAdminUsers} from "./components/UserAdminUsers";
import {UserAdminInvites} from "./components/UserAdminInvites";
import {UserManagementPage} from "./pages/UserManagement";
import {configWSUrl} from "./utils/navigationUtils";
import {GPodderIntegration} from "./pages/GPodderIntegration";

export const router = createBrowserRouter(createRoutesFromElements(
<>
Expand Down Expand Up @@ -73,6 +74,7 @@ export const router = createBrowserRouter(createRoutesFromElements(
<Route path="opml" element={<SettingsOPMLExport/>}/>
<Route path="naming" element={<SettingsNaming/>}/>
<Route path="podcasts" element={<SettingsPodcastDelete/>}/>
<Route path="gpodder" element={<GPodderIntegration/>}/>
</Route>
<Route path={"administration"} element={<Suspense><UserAdminViewLazyLoad /></Suspense>}>
<Route index element={<Navigate to="users"/>}/>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/FeedURLComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const FeedURLComponent: FC = () => {
})} placeholder={t('rss-feed-url')!}
className={"bg-[--input-bg-color] w-full px-4 py-2 rounded-lg text-sm text-[--input-fg-color] placeholder:text-[--input-fg-color-disabled]"} />

<CustomButtonPrimary disabled={feedUrlWatched.trim().length === 0} type="submit">Add</CustomButtonPrimary>
<CustomButtonPrimary disabled={feedUrlWatched.trim().length === 0} type="submit">{t('add')}</CustomButtonPrimary>
</form>
)
}
5 changes: 4 additions & 1 deletion ui/src/language/json/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,5 +183,8 @@
"user-settings-updated": "Benutzereinstellungen aktualisiert",
"notification.episode-now-available": "Episode {{episode}} ist jetzt verfügbar",
"episode-numbering": "Episodennummerierung",
"activated": "Aktiviert"
"activated": "Aktiviert",
"manage-gpodder-podcasts": "GPodder Podcasts verwalten",
"device": "Gerät",
"add": "Hinzufügen"
}
5 changes: 4 additions & 1 deletion ui/src/language/json/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,5 +183,8 @@
"user-settings-updated": "User settings updated",
"notification.episode-now-available": "Episode {{episode}} now available",
"episode-numbering": "Episode numbering",
"activated": "Activated"
"activated": "Activated",
"manage-gpodder-podcasts": "Manage GPodder podcasts",
"device": "Device",
"add": "Add"
}
66 changes: 66 additions & 0 deletions ui/src/pages/GPodderIntegration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import axios, {AxiosResponse} from "axios";
import {Setting} from "../models/Setting";
import {useState} from "react";
import {useTranslation} from "react-i18next";
import {Podcast} from "../store/CommonSlice";
import {handleAddPodcast} from "../utils/ErrorSnackBarResponses";
import {CustomButtonPrimary} from "../components/CustomButtonPrimary";

type GPodderIntegrationItem = {
device: string,
podcast: string
}


export const GPodderIntegration = ()=> {
const [gpodderOnlyPodcasts, setGPodderOnlyPodcasts] = useState<GPodderIntegrationItem[]>([])
const {t} = useTranslation()

axios.get('/podcast/available/gpodder')
.then((res: AxiosResponse<GPodderIntegrationItem[]>) => {
console.log("Res is",res)
setGPodderOnlyPodcasts(res.data)
})

const addPodcast = (feedUrl: string)=>{
setGPodderOnlyPodcasts(gpodderOnlyPodcasts.filter(p=>p.podcast!=feedUrl))
axios.post( '/podcast/feed', {
rssFeedUrl: feedUrl
}).then((v: AxiosResponse<Podcast>) => {
handleAddPodcast(v.status, v.data.name, t)
})
}


return <table className="text-left text-sm text-stone-900 w-full overflow-y-auto text-[--fg-color]">
<thead>
<tr className="border-b border-stone-300">
<th scope="col" className="pr-2 py-3 text-[--fg-color]">
#
</th>
<th scope="col" className="px-2 py-3 text-[--fg-color]">
{t('device')}
</th>
<th scope="col" className="px-2 py-3 text-[--fg-color]">
{t('podcasts')}
</th>
<th scope="col" className="px-2 py-3 text-[--fg-color]">
{t('actions')}
</th>
</tr>
</thead>
<tbody className="">
{
gpodderOnlyPodcasts?.map((int, index)=>{
return <tr key={index}>
<td className="px-2 py-4 text-[--fg-color]">{index}</td>
<td className="px-2 py-4 text-[--fg-color]">{int.device}</td>
<td className="px-2 py-4 text-[--fg-color]">{int.podcast}</td>
<td><CustomButtonPrimary onClick={()=>addPodcast(int.podcast)}>{t('add')}</CustomButtonPrimary></td>
</tr>
}
)
}
</tbody>
</table>
}
9 changes: 7 additions & 2 deletions ui/src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,27 @@ export const SettingsPage = () => {
</li>
<li className={`cursor-pointer inline-block px-2 py-4`}>
<NavLink to="naming">
{t('podcast-naming')}
{t('podcast-naming')}
</NavLink>
</li>
<li className={`cursor-pointer inline-block px-2 py-4`}>
<NavLink to="podcasts">
{t('manage-podcasts')}
</NavLink>
</li>
<li className={`cursor-pointer inline-block px-2 py-4`}>
<NavLink to="gpodder">
{t('manage-gpodder-podcasts')}
</NavLink>
</li>
</ul>
</div>

<div className="max-w-screen-md">
<Outlet/>
</div>

<InfoModal />
<InfoModal/>
</div>
)
}

0 comments on commit a7ea8a6

Please sign in to comment.