Skip to content

Commit

Permalink
Feature/tags (#464)
Browse files Browse the repository at this point in the history
* Added backend implementation.

* Added backend implementation.

* Added backend implementation.

* Added delete and create podcasts.

* Added api doc.

* Added manifest.json in backend.

* Added tagging system in backend

* Added tagging.

* Added tagging system

* Fixed clippy

---------

Co-authored-by: SamTV12345 <noreply+samtv1235@github.com>
  • Loading branch information
SamTV12345 and SamTV12345 authored Aug 30, 2024
1 parent d259137 commit a397ece
Show file tree
Hide file tree
Showing 44 changed files with 1,036 additions and 3,560 deletions.
3 changes: 3 additions & 0 deletions migrations/postgres/2024-11-29-083801_tags/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE tags;
DROP TABLE
33 changes: 33 additions & 0 deletions migrations/postgres/2024-11-29-083801_tags/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- Your SQL goes here

CREATE TABLE tags (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
username TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP NOT NULL,
color TEXT NOT NULL,
UNIQUE (name, username)
);

CREATE TABLE tags_podcasts
(
tag_id TEXT NOT NULL,
podcast_id INTEGER NOT NULL,
FOREIGN KEY (tag_id) REFERENCES tags (id),
FOREIGN KEY (podcast_id) REFERENCES podcasts (id),
PRIMARY KEY (tag_id, podcast_id)
);

-- INDEXES
CREATE INDEX idx_tags_name ON tags (name);
CREATE INDEX idx_tags_username ON tags (username);
CREATE INDEX idx_devices ON devices(name);
CREATE INDEX idx_episodes_podcast ON episodes(podcast);
CREATE INDEX idx_episodes_episode ON episodes(episode);
CREATE INDEX idx_podcast_episodes ON podcast_episodes(podcast_id);
CREATE INDEX idx_podcast_episodes_url ON podcast_episodes(url);
CREATE INDEX idx_podcasts_name ON podcasts(name);
CREATE INDEX idx_podcasts_rssfeed ON podcasts(rssfeed);
CREATE INDEX idx_subscriptions ON subscriptions(username);
CREATE INDEX idx_subscriptions_device ON subscriptions(device);
3 changes: 3 additions & 0 deletions migrations/sqlite/2024-11-29-083801_tags/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE tags;
DROP TABLE
34 changes: 34 additions & 0 deletions migrations/sqlite/2024-11-29-083801_tags/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- Your SQL goes here

CREATE TABLE tags (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
username TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP NOT NULL,
color TEXT NOT NULL,
UNIQUE (name, username)
);

CREATE TABLE tags_podcasts
(
tag_id TEXT NOT NULL,
podcast_id INTEGER NOT NULL,
FOREIGN KEY (tag_id) REFERENCES tags (id),
FOREIGN KEY (podcast_id) REFERENCES podcasts (id),
PRIMARY KEY (tag_id, podcast_id)
);


-- INDEXES
CREATE INDEX idx_tags_name ON tags (name);
CREATE INDEX idx_tags_username ON tags (username);
CREATE INDEX idx_devices ON devices(name);
CREATE INDEX idx_episodes_podcast ON episodes(podcast);
CREATE INDEX idx_episodes_episode ON episodes(episode);
CREATE INDEX idx_podcast_episodes ON podcast_episodes(podcast_id);
CREATE INDEX idx_podcast_episodes_url ON podcast_episodes(url);
CREATE INDEX idx_podcasts_name ON podcasts(name);
CREATE INDEX idx_podcasts_rssfeed ON podcasts(rssfeed);
CREATE INDEX idx_subscriptions ON subscriptions(username);
CREATE INDEX idx_subscriptions_device ON subscriptions(device);
12 changes: 8 additions & 4 deletions src/controllers/api_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ use utoipa::{
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
Modify, OpenApi,
};

use crate::models::tag::Tag;
use crate::controllers::tags_controller::*;
use crate::controllers::playlist_controller::*;
#[derive(OpenApi)]
#[openapi(
paths(
Expand All @@ -57,13 +59,14 @@ paths(
dismiss_notifications,get_public_config,onboard_user,
get_watchtime,get_timeline,download_podcast_episodes_of_podcast,update_name,get_sys_info,
get_filter,search_podcasts,add_podcast_by_feed,refresh_all_podcasts,update_active_podcast,
delete_podcast,proxy_podcast
add_playlist,update_playlist,get_all_playlists,get_playlist_by_id,delete_playlist_by_id,delete_playlist_item,
delete_podcast,proxy_podcast,insert_tag, get_tags, delete_tag, update_tag, add_podcast_to_tag, delete_podcast_from_tag
),
components(
schemas(Podcast, PodcastEpisode, ItunesModel,PodcastFavorUpdateModel,
PodcastWatchedEpisodeModel, PodcastWatchedPostModel, PodcastAddModel,Notification, Setting,
Invite,
Filter,OpmlModel,DeletePodcast, UpdateNameSettings,SysExtraInfo,UserOnboardingModel,User,InvitePostModel)
Invite,LoginRequest,PlaylistDtoPost,Tag,
Filter,OpmlModel,DeletePodcast, UpdateNameSettings,SysExtraInfo,UserOnboardingModel,User,InvitePostModel, TagCreate,TagWithPodcast)
),
tags(
(name = "podcasts", description = "Podcast management endpoints."),
Expand All @@ -73,6 +76,7 @@ tags(
(name = "settings", description = "Settings management endpoints. Settings are globally scoped."),
(name = "info", description = "Gets multiple information about your installation."),
(name = "playlist", description = "Playlist management endpoints."),
(name = "tags", description = "Tag management endpoints."),
),
modifiers(&SecurityAddon)
)]
Expand Down
50 changes: 50 additions & 0 deletions src/controllers/manifest_controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use actix_web::{get, HttpResponse};
use crate::constants::inner_constants::ENVIRONMENT_SERVICE;
use crate::utils::error::CustomError;

#[derive(Serialize)]
pub struct Icon {
pub src: String,
pub sizes: String,
pub r#type: String
}


#[derive(Serialize)]
pub struct Manifest {
pub name: String,
pub short_name: String,
pub start_url: String,
pub icons: Vec<Icon>,
pub theme_color: String,
pub background_color: String,
pub display: String,
pub orientation: String
}



#[get("manifest.json")]
pub async fn get_manifest() -> Result<HttpResponse, CustomError> {
let env_service = ENVIRONMENT_SERVICE.get().unwrap();
let mut icons = Vec::new();
let icon = Icon{
src: env_service.server_url.to_string()+"ui/logo.png",
sizes: "512x512".to_string(),
r#type: "image/png".to_string()
};
icons.push(icon);


let manifest = Manifest{
name: "PodFetch".to_string(),
short_name: "PodFetch".to_string(),
start_url: env_service.server_url.to_string(),
icons,
orientation: "landscape".to_string(),
theme_color: "#ffffff".to_string(),
display: "fullscreen".to_string(),
background_color: "#ffffff".to_string()
};
Ok(HttpResponse::Ok().json(manifest))
}
2 changes: 2 additions & 0 deletions src/controllers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ pub mod user_controller;
pub mod watch_time_controller;
pub mod web_socket;
pub mod websocket_controller;
pub mod tags_controller;
pub mod server;
pub mod manifest_controller;
35 changes: 30 additions & 5 deletions src/controllers/playlist_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ use crate::DbPool;
use actix_web::web::Data;
use actix_web::{delete, get, post, put, web, HttpResponse};
use std::ops::DerefMut;
use utoipa::ToSchema;

#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, ToSchema)]
pub struct PlaylistDtoPost {
pub name: String,
pub items: Vec<PlaylistItem>,
}

#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone,ToSchema)]
pub struct PlaylistItem {
pub episode: i32,
}

#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, ToSchema)]
pub struct PlaylistDto {
pub id: String,
pub name: String,
Expand All @@ -28,7 +29,7 @@ pub struct PlaylistDto {
#[utoipa::path(
context_path="/api/v1",
responses(
(status = 200, description = "Adds a new playlist for the user",body= Vec<PlaylistDto>)),
(status = 200, description = "Adds a new playlist for the user",body= PlaylistDtoPost)),
tag="playlist"
)]
#[post("/playlist")]
Expand All @@ -52,7 +53,7 @@ pub async fn add_playlist(
#[utoipa::path(
context_path="/api/v1",
responses(
(status = 200, description = "Updates a playlist of the user",body= Vec<PlaylistDto>)),
(status = 200, description = "Updates a playlist of the user",body= PlaylistDtoPost)),
tag="playlist"
)]
#[put("/playlist/{playlist_id}")]
Expand All @@ -75,6 +76,12 @@ pub async fn update_playlist(
Ok(HttpResponse::Ok().json(res))
}

#[utoipa::path(
context_path="/api/v1",
responses(
(status = 200, description = "Gets all playlists of the user")),
tag="playlist"
)]
#[get("/playlist")]
pub async fn get_all_playlists(
requester: Option<web::ReqData<User>>,
Expand All @@ -87,6 +94,12 @@ pub async fn get_all_playlists(
.map(|playlists| HttpResponse::Ok().json(playlists))
}

#[utoipa::path(
context_path="/api/v1",
responses(
(status = 200, description = "Gets a specific playlist of a user")),
tag="playlist"
)]
#[get("/playlist/{playlist_id}")]
pub async fn get_playlist_by_id(
requester: Option<web::ReqData<User>>,
Expand All @@ -108,6 +121,12 @@ pub async fn get_playlist_by_id(
Ok(HttpResponse::Ok().json(playlist))
}

#[utoipa::path(
context_path="/api/v1",
responses(
(status = 200, description = "Deletes a specific playlist of a user")),
tag="playlist"
)]
#[delete("/playlist/{playlist_id}")]
pub async fn delete_playlist_by_id(
requester: Option<web::ReqData<User>>,
Expand All @@ -123,6 +142,12 @@ pub async fn delete_playlist_by_id(
Ok(HttpResponse::Ok().json(()))
}

#[utoipa::path(
context_path="/api/v1",
responses(
(status = 200, description = "Deletes a specific playlist item of a user")),
tag="playlist"
)]
#[delete("/playlist/{playlist_id}/episode/{episode_id}")]
pub async fn delete_playlist_item(
requester: Option<web::ReqData<User>>,
Expand Down
22 changes: 18 additions & 4 deletions src/controllers/podcast_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub struct PodcastSearchModel {
title: Option<String>,
order_option: Option<OrderOption>,
favored_only: bool,
tag: Option<String>
}

#[utoipa::path(
Expand Down Expand Up @@ -87,6 +88,7 @@ pub async fn search_podcasts(
let query = query.into_inner();
let _order = query.order.unwrap_or(OrderCriteria::Asc);
let _latest_pub = query.order_option.unwrap_or(OrderOption::Title);
let tag = query.tag;

let opt_filter = Filter::get_filter_by_username(
requester.clone().unwrap().username.clone(),
Expand Down Expand Up @@ -122,6 +124,7 @@ pub async fn search_podcasts(
_latest_pub.clone(),
conn.get().map_err(map_r2d2_error)?.deref_mut(),
username,
tag
)?;
}
Ok(HttpResponse::Ok().json(podcasts))
Expand All @@ -135,29 +138,35 @@ pub async fn search_podcasts(
_latest_pub.clone(),
&mut conn.get().unwrap(),
username,
tag
)?;
}
Ok(HttpResponse::Ok().json(podcasts))
}
}
}


#[utoipa::path(
context_path="/api/v1",
responses(
(status = 200, description = "Find a podcast by its collection id", body = [Podcast])
(status = 200, description = "Find a podcast by its collection id", body = [(Podcast, Tags)])
),
tag="podcasts"
)]
#[get("/podcast/{id}")]
pub async fn find_podcast_by_id(
id: Path<String>,
conn: Data<DbPool>,
user: Option<web::ReqData<User>>,
) -> Result<HttpResponse, CustomError> {
let id_num = from_str::<i32>(&id).unwrap();
let username = user.unwrap().username.clone();

let podcast =
PodcastService::get_podcast(conn.get().map_err(map_r2d2_error)?.deref_mut(), id_num)?;
let mapped_podcast = MappingService::map_podcast_to_podcast_dto(&podcast);
let tags = Tag::get_tags_of_podcast(conn.get().map_err(map_r2d2_error)?.deref_mut(), id_num, &username)?;
let mapped_podcast = MappingService::map_podcast_to_podcast_dto(&podcast, tags);
Ok(HttpResponse::Ok().json(mapped_podcast))
}

Expand All @@ -177,6 +186,7 @@ pub async fn find_all_podcasts(

let podcasts =
PodcastService::get_podcasts(conn.get().map_err(map_r2d2_error)?.deref_mut(), username)?;

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

Expand Down Expand Up @@ -485,7 +495,7 @@ pub async fn refresh_all_podcasts(
podcast_episode: None,
type_of: PodcastType::RefreshPodcast,
message: format!("Refreshed podcast: {}", podcast.name),
podcast: Option::from(podcast.clone()),
podcast: Option::from(MappingService::map_podcast_to_podcast_dto(&podcast, vec![])),
podcast_episodes: None,
}).unwrap());
}
Expand Down Expand Up @@ -690,7 +700,7 @@ async fn insert_outline(
let _ = lobby.send_broadcast(MAIN_ROOM.parse().unwrap(), serde_json::to_string(&BroadcastMessage {
type_of: PodcastType::OpmlAdded,
message: "Refreshed podcasts".to_string(),
podcast: Option::from(podcast),
podcast: Option::from(MappingService::map_podcast_to_podcast_dto(&podcast, vec![])),
podcast_episodes: None,
podcast_episode: None,
}).unwrap()).await;
Expand Down Expand Up @@ -719,12 +729,14 @@ async fn insert_outline(
}
use crate::models::episode::Episode;
use utoipa::ToSchema;
use crate::models::tag::Tag;

use crate::controllers::podcast_episode_controller::EpisodeFormatDto;
use crate::controllers::server::ChatServerHandle;
use crate::controllers::websocket_controller::RSSAPiKey;
use crate::models::podcast_settings::PodcastSetting;
use crate::models::settings::Setting;
use crate::models::tags_podcast::TagsPodcast;
use crate::utils::environment_variables::is_env_var_present_and_true;

use crate::utils::error::{map_r2d2_error, map_reqwest_error, CustomError};
Expand Down Expand Up @@ -763,6 +775,8 @@ pub async fn delete_podcast(
}
Episode::delete_watchtime(&mut db.get().unwrap(), *id)?;
PodcastEpisode::delete_episodes_of_podcast(&mut db.get().unwrap(), *id)?;
TagsPodcast::delete_tags_by_podcast_id(&mut db.get().unwrap(), *id)?;

Podcast::delete_podcast(&mut db.get().unwrap(), *id)?;
Ok(HttpResponse::Ok().into())
}
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/sys_info_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ pub async fn login(
Err(CustomError::Forbidden)
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct LoginRequest {
pub username: String,
pub password: String,
Expand Down
Loading

0 comments on commit a397ece

Please sign in to comment.