wip episode actions
This commit is contained in:
parent
7ce41cd034
commit
3e79bec974
16 changed files with 319 additions and 13 deletions
|
|
@ -3,6 +3,7 @@ mod repository;
|
|||
mod schema;
|
||||
|
||||
pub use models::device::{Device, DeviceType, NewDevice};
|
||||
pub use models::episode_action::{ActionType, EpisodeAction, NewEpisodeAction};
|
||||
pub use models::session::Session;
|
||||
pub use models::subscription::{NewSubscription, Subscription};
|
||||
pub use models::user::{NewUser, User};
|
||||
|
|
|
|||
113
src/db/models/episode_action.rs
Normal file
113
src/db/models/episode_action.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use std::{fmt, str::FromStr};
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{
|
||||
deserialize::{FromSql, FromSqlRow},
|
||||
expression::AsExpression,
|
||||
prelude::{Insertable, Queryable},
|
||||
serialize::ToSql,
|
||||
sql_types::Text,
|
||||
sqlite::{Sqlite, SqliteValue},
|
||||
Selectable,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::{schema::*, DbPool, DbResult};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
|
||||
#[diesel(table_name = episode_actions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct EpisodeAction {
|
||||
id: i64,
|
||||
subscription_id: i64,
|
||||
device_id: Option<i64>,
|
||||
episode: String,
|
||||
timestamp: Option<i64>,
|
||||
action: ActionType,
|
||||
started: Option<i32>,
|
||||
position: Option<i32>,
|
||||
total: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Insertable)]
|
||||
#[diesel(table_name = episode_actions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct NewEpisodeAction {
|
||||
subscription_id: i64,
|
||||
device_id: Option<i64>,
|
||||
episode: String,
|
||||
timestamp: Option<i64>,
|
||||
action: ActionType,
|
||||
started: Option<i32>,
|
||||
position: Option<i32>,
|
||||
total: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)]
|
||||
#[diesel(sql_type = Text)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ActionType {
|
||||
New,
|
||||
Download,
|
||||
Play,
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl fmt::Display for ActionType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::New => "new",
|
||||
Self::Download => "download",
|
||||
Self::Play => "play",
|
||||
Self::Delete => "delete",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ActionTypeParseErr(String);
|
||||
|
||||
impl fmt::Display for ActionTypeParseErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "invalid action type '{}'", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ActionTypeParseErr {}
|
||||
|
||||
impl FromStr for ActionType {
|
||||
type Err = ActionTypeParseErr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"new" => Ok(Self::New),
|
||||
"download" => Ok(Self::Download),
|
||||
"delete" => Ok(Self::Delete),
|
||||
"play" => Ok(Self::Play),
|
||||
_ => Err(ActionTypeParseErr(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql<Text, Sqlite> for ActionType {
|
||||
fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result<Self> {
|
||||
let s = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
|
||||
|
||||
Ok(s.as_str().parse()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<Text, Sqlite> for ActionType {
|
||||
fn to_sql<'b>(
|
||||
&'b self,
|
||||
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
|
||||
) -> diesel::serialize::Result {
|
||||
out.set_value(self.to_string());
|
||||
|
||||
Ok(diesel::serialize::IsNull::No)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod device;
|
||||
pub mod episode_action;
|
||||
pub mod session;
|
||||
pub mod subscription;
|
||||
pub mod user;
|
||||
|
|
|
|||
23
src/db/repository/episode_action.rs
Normal file
23
src/db/repository/episode_action.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use diesel::prelude::*;
|
||||
|
||||
use super::SqliteRepository;
|
||||
use crate::{
|
||||
db::{self, schema::*},
|
||||
gpodder,
|
||||
};
|
||||
|
||||
impl gpodder::EpisodeActionRepository for SqliteRepository {
|
||||
fn add_episode_actions(
|
||||
&self,
|
||||
user: &gpodder::User,
|
||||
actions: Vec<gpodder::EpisodeAction>,
|
||||
) -> Result<i64, gpodder::AuthErr> {
|
||||
self.pool.get()?.transaction(|conn| {
|
||||
for action in actions {}
|
||||
|
||||
Ok::<_, diesel::result::Error>(())
|
||||
})?;
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
mod auth;
|
||||
mod device;
|
||||
mod episode_action;
|
||||
mod subscription;
|
||||
|
||||
use super::DbPool;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,20 @@ diesel::table! {
|
|||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
episode_actions (id) {
|
||||
id -> BigInt,
|
||||
subscription_id -> BigInt,
|
||||
device_id -> Nullable<BigInt>,
|
||||
episode -> Text,
|
||||
timestamp -> Nullable<BigInt>,
|
||||
action -> Text,
|
||||
started -> Nullable<Integer>,
|
||||
position -> Nullable<Integer>,
|
||||
total -> Nullable<Integer>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
sessions (id) {
|
||||
id -> BigInt,
|
||||
|
|
@ -37,11 +51,14 @@ diesel::table! {
|
|||
}
|
||||
|
||||
diesel::joinable!(devices -> users (user_id));
|
||||
diesel::joinable!(episode_actions -> devices (device_id));
|
||||
diesel::joinable!(episode_actions -> subscriptions (subscription_id));
|
||||
diesel::joinable!(sessions -> users (user_id));
|
||||
diesel::joinable!(subscriptions -> devices (device_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
devices,
|
||||
episode_actions,
|
||||
sessions,
|
||||
subscriptions,
|
||||
users,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ pub enum AuthErr {
|
|||
Other(Box<dyn std::error::Error + Sync + Send>),
|
||||
}
|
||||
|
||||
pub trait AuthRepository: Send + Sync {
|
||||
pub trait AuthRepository {
|
||||
/// Validate the given session ID and return its user.
|
||||
fn validate_session(&self, session_id: i64) -> Result<models::User, AuthErr>;
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ pub trait AuthRepository: Send + Sync {
|
|||
fn remove_session(&self, username: &str, session_id: i64) -> Result<(), AuthErr>;
|
||||
}
|
||||
|
||||
pub trait DeviceRepository: Send + Sync {
|
||||
pub trait DeviceRepository {
|
||||
/// Return all devices associated with the user
|
||||
fn devices_for_user(&self, user: &User) -> Result<Vec<Device>, AuthErr>;
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ pub trait DeviceRepository: Send + Sync {
|
|||
) -> Result<(), AuthErr>;
|
||||
}
|
||||
|
||||
pub trait SubscriptionRepository: Send + Sync {
|
||||
pub trait SubscriptionRepository {
|
||||
/// Return the subscriptions for the given device
|
||||
fn subscriptions_for_device(
|
||||
&self,
|
||||
|
|
@ -73,3 +73,9 @@ pub trait SubscriptionRepository: Send + Sync {
|
|||
since: i64,
|
||||
) -> Result<(i64, Vec<String>, Vec<String>), AuthErr>;
|
||||
}
|
||||
|
||||
pub trait EpisodeActionRepository {
|
||||
/// Insert the given episode actions into the datastore.
|
||||
fn add_episode_actions(&self, user: &User, actions: Vec<EpisodeAction>)
|
||||
-> Result<i64, AuthErr>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -29,3 +30,30 @@ pub struct DevicePatch {
|
|||
pub caption: Option<String>,
|
||||
pub r#type: Option<DeviceType>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[serde(tag = "action")]
|
||||
pub enum EpisodeActionType {
|
||||
Download,
|
||||
Play {
|
||||
#[serde(default)]
|
||||
started: Option<i32>,
|
||||
position: i32,
|
||||
#[serde(default)]
|
||||
total: Option<i32>,
|
||||
},
|
||||
Delete,
|
||||
New,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct EpisodeAction {
|
||||
podcast: String,
|
||||
episode: String,
|
||||
timestamp: Option<NaiveDateTime>,
|
||||
#[serde(default)]
|
||||
device: Option<String>,
|
||||
#[serde(flatten)]
|
||||
action: EpisodeActionType,
|
||||
}
|
||||
|
|
|
|||
44
src/server/gpodder/advanced/episodes.rs
Normal file
44
src/server/gpodder/advanced/episodes.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use axum::{
|
||||
extract::{Path, State},
|
||||
middleware,
|
||||
routing::post,
|
||||
Extension, Json, Router,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
db,
|
||||
server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models::{EpisodeAction, UpdatedUrlsResponse},
|
||||
},
|
||||
Context,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn router(ctx: Context) -> Router<Context> {
|
||||
Router::new()
|
||||
.route("/{username}", post(post_episode_actions))
|
||||
.layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
|
||||
}
|
||||
|
||||
async fn post_episode_actions(
|
||||
State(ctx): State<Context>,
|
||||
Path(username): Path<StringWithFormat>,
|
||||
Extension(user): Extension<db::User>,
|
||||
Json(actions): Json<Vec<EpisodeAction>>,
|
||||
) -> AppResult<Json<UpdatedUrlsResponse>> {
|
||||
if username.format != Format::Json {
|
||||
return Err(AppError::NotFound);
|
||||
}
|
||||
|
||||
if *username != user.username {
|
||||
return Err(AppError::BadRequest);
|
||||
}
|
||||
|
||||
tracing::debug!("{:?}", actions);
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
mod auth;
|
||||
mod devices;
|
||||
mod episodes;
|
||||
mod subscriptions;
|
||||
|
||||
use axum::Router;
|
||||
|
|
@ -11,4 +12,5 @@ pub fn router(ctx: Context) -> Router<Context> {
|
|||
.nest("/auth", auth::router())
|
||||
.nest("/devices", devices::router(ctx.clone()))
|
||||
.nest("/subscriptions", subscriptions::router(ctx.clone()))
|
||||
.nest("/episodes", episodes::router(ctx.clone()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ use crate::{
|
|||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models::{SubscriptionChangeResponse, SubscriptionDelta, SubscriptionDeltaResponse},
|
||||
models::{
|
||||
DeviceType, SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse,
|
||||
},
|
||||
},
|
||||
Context,
|
||||
},
|
||||
|
|
@ -33,7 +35,7 @@ pub async fn post_subscription_changes(
|
|||
Path((username, id)): Path<(String, StringWithFormat)>,
|
||||
Extension(user): Extension<gpodder::User>,
|
||||
Json(delta): Json<SubscriptionDelta>,
|
||||
) -> AppResult<Json<SubscriptionChangeResponse>> {
|
||||
) -> AppResult<Json<UpdatedUrlsResponse>> {
|
||||
if id.format != Format::Json {
|
||||
return Err(AppError::NotFound);
|
||||
}
|
||||
|
|
@ -49,7 +51,7 @@ pub async fn post_subscription_changes(
|
|||
.await
|
||||
.unwrap()
|
||||
.map(|timestamp| {
|
||||
Json(SubscriptionChangeResponse {
|
||||
Json(UpdatedUrlsResponse {
|
||||
timestamp,
|
||||
update_urls: Vec::new(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use chrono::{NaiveDateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db;
|
||||
|
|
@ -64,7 +65,34 @@ pub struct SubscriptionDeltaResponse {
|
|||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SubscriptionChangeResponse {
|
||||
pub struct UpdatedUrlsResponse {
|
||||
pub timestamp: i64,
|
||||
pub update_urls: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[serde(tag = "action")]
|
||||
pub enum EpisodeActionType {
|
||||
Download,
|
||||
Play {
|
||||
#[serde(default)]
|
||||
started: Option<i32>,
|
||||
position: i32,
|
||||
#[serde(default)]
|
||||
total: Option<i32>,
|
||||
},
|
||||
Delete,
|
||||
New,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct EpisodeAction {
|
||||
podcast: String,
|
||||
episode: String,
|
||||
timestamp: Option<NaiveDateTime>,
|
||||
#[serde(default)]
|
||||
device: Option<String>,
|
||||
#[serde(flatten)]
|
||||
action: EpisodeActionType,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue