wip episode actions
parent
7ce41cd034
commit
3e79bec974
|
@ -107,6 +107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
|
"axum-macros",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -177,6 +178,17 @@ dependencies = [
|
||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-macros"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.74"
|
version = "0.3.74"
|
||||||
|
@ -271,6 +283,7 @@ dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,9 +5,9 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
axum = { version = "0.8.1" }
|
axum = { version = "0.8.1", features = ["macros"] }
|
||||||
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
|
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
|
||||||
chrono = "0.4.39"
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
clap = { version = "4.5.30", features = ["derive", "env"] }
|
clap = { version = "4.5.30", features = ["derive", "env"] }
|
||||||
diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] }
|
diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] }
|
||||||
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
|
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
meta {
|
||||||
|
name: Upload action changes
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: http://localhost:8080/api/2/episodes/:username
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
params:path {
|
||||||
|
username:
|
||||||
|
}
|
|
@ -1,14 +1,17 @@
|
||||||
create table users (
|
create table users (
|
||||||
id integer primary key not null,
|
id integer primary key not null,
|
||||||
|
|
||||||
username text unique not null,
|
username text unique not null,
|
||||||
password_hash text not null
|
password_hash text not null
|
||||||
);
|
);
|
||||||
|
|
||||||
create table sessions (
|
create table sessions (
|
||||||
id bigint primary key not null,
|
id bigint primary key not null,
|
||||||
|
|
||||||
user_id bigint not null
|
user_id bigint not null
|
||||||
references users (id)
|
references users (id)
|
||||||
on delete cascade,
|
on delete cascade,
|
||||||
|
|
||||||
unique (id, user_id)
|
unique (id, user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -28,15 +31,24 @@ create table devices (
|
||||||
|
|
||||||
create table subscriptions (
|
create table subscriptions (
|
||||||
id integer primary key not null,
|
id integer primary key not null,
|
||||||
|
|
||||||
|
url text unique not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table device_subscriptions (
|
||||||
|
id integer primary key not null,
|
||||||
|
|
||||||
device_id bigint not null
|
device_id bigint not null
|
||||||
references devices (id)
|
references devices (id)
|
||||||
on delete cascade,
|
on delete cascade,
|
||||||
url text not null,
|
subscription_id bigint not null
|
||||||
|
references subscriptions (id)
|
||||||
|
on delete cascade
|
||||||
|
|
||||||
time_changed bigint not null default 0,
|
time_changed bigint not null default 0,
|
||||||
deleted boolean not null default false,
|
deleted boolean not null default false,
|
||||||
|
|
||||||
unique (device_id, url)
|
unique (device_id, subscription_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table episode_actions (
|
create table episode_actions (
|
||||||
|
@ -44,14 +56,14 @@ create table episode_actions (
|
||||||
|
|
||||||
subscription_id bigint not null
|
subscription_id bigint not null
|
||||||
references subscriptions (id)
|
references subscriptions (id)
|
||||||
on delete cascade,
|
on delete set null,
|
||||||
|
|
||||||
-- Can be null, as the device is not always provided
|
-- Can be null, as the device is not always provided
|
||||||
device_id bigint
|
device_id bigint
|
||||||
references devices (id)
|
references devices (id)
|
||||||
on delete set null,
|
on delete set null,
|
||||||
|
|
||||||
episode text not null,
|
episode_url text not null,
|
||||||
timestamp bigint,
|
timestamp bigint,
|
||||||
action text not null,
|
action text not null,
|
||||||
started integer,
|
started integer,
|
||||||
|
|
|
@ -3,6 +3,7 @@ mod repository;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|
||||||
pub use models::device::{Device, DeviceType, NewDevice};
|
pub use models::device::{Device, DeviceType, NewDevice};
|
||||||
|
pub use models::episode_action::{ActionType, EpisodeAction, NewEpisodeAction};
|
||||||
pub use models::session::Session;
|
pub use models::session::Session;
|
||||||
pub use models::subscription::{NewSubscription, Subscription};
|
pub use models::subscription::{NewSubscription, Subscription};
|
||||||
pub use models::user::{NewUser, User};
|
pub use models::user::{NewUser, User};
|
||||||
|
|
|
@ -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 device;
|
||||||
|
pub mod episode_action;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
|
@ -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 auth;
|
||||||
mod device;
|
mod device;
|
||||||
|
mod episode_action;
|
||||||
mod subscription;
|
mod subscription;
|
||||||
|
|
||||||
use super::DbPool;
|
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! {
|
diesel::table! {
|
||||||
sessions (id) {
|
sessions (id) {
|
||||||
id -> BigInt,
|
id -> BigInt,
|
||||||
|
@ -37,11 +51,14 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::joinable!(devices -> users (user_id));
|
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!(sessions -> users (user_id));
|
||||||
diesel::joinable!(subscriptions -> devices (device_id));
|
diesel::joinable!(subscriptions -> devices (device_id));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
devices,
|
devices,
|
||||||
|
episode_actions,
|
||||||
sessions,
|
sessions,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
users,
|
users,
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub enum AuthErr {
|
||||||
Other(Box<dyn std::error::Error + Sync + Send>),
|
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.
|
/// Validate the given session ID and return its user.
|
||||||
fn validate_session(&self, session_id: i64) -> Result<models::User, AuthErr>;
|
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>;
|
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
|
/// Return all devices associated with the user
|
||||||
fn devices_for_user(&self, user: &User) -> Result<Vec<Device>, AuthErr>;
|
fn devices_for_user(&self, user: &User) -> Result<Vec<Device>, AuthErr>;
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ pub trait DeviceRepository: Send + Sync {
|
||||||
) -> Result<(), AuthErr>;
|
) -> Result<(), AuthErr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SubscriptionRepository: Send + Sync {
|
pub trait SubscriptionRepository {
|
||||||
/// Return the subscriptions for the given device
|
/// Return the subscriptions for the given device
|
||||||
fn subscriptions_for_device(
|
fn subscriptions_for_device(
|
||||||
&self,
|
&self,
|
||||||
|
@ -73,3 +73,9 @@ pub trait SubscriptionRepository: Send + Sync {
|
||||||
since: i64,
|
since: i64,
|
||||||
) -> Result<(i64, Vec<String>, Vec<String>), AuthErr>;
|
) -> 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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -29,3 +30,30 @@ pub struct DevicePatch {
|
||||||
pub caption: Option<String>,
|
pub caption: Option<String>,
|
||||||
pub r#type: Option<DeviceType>,
|
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,
|
||||||
|
}
|
||||||
|
|
|
@ -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 auth;
|
||||||
mod devices;
|
mod devices;
|
||||||
|
mod episodes;
|
||||||
mod subscriptions;
|
mod subscriptions;
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
@ -11,4 +12,5 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
.nest("/auth", auth::router())
|
.nest("/auth", auth::router())
|
||||||
.nest("/devices", devices::router(ctx.clone()))
|
.nest("/devices", devices::router(ctx.clone()))
|
||||||
.nest("/subscriptions", subscriptions::router(ctx.clone()))
|
.nest("/subscriptions", subscriptions::router(ctx.clone()))
|
||||||
|
.nest("/episodes", episodes::router(ctx.clone()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,9 @@ use crate::{
|
||||||
gpodder::{
|
gpodder::{
|
||||||
auth_middleware,
|
auth_middleware,
|
||||||
format::{Format, StringWithFormat},
|
format::{Format, StringWithFormat},
|
||||||
models::{SubscriptionChangeResponse, SubscriptionDelta, SubscriptionDeltaResponse},
|
models::{
|
||||||
|
DeviceType, SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Context,
|
Context,
|
||||||
},
|
},
|
||||||
|
@ -33,7 +35,7 @@ pub async fn post_subscription_changes(
|
||||||
Path((username, id)): Path<(String, StringWithFormat)>,
|
Path((username, id)): Path<(String, StringWithFormat)>,
|
||||||
Extension(user): Extension<gpodder::User>,
|
Extension(user): Extension<gpodder::User>,
|
||||||
Json(delta): Json<SubscriptionDelta>,
|
Json(delta): Json<SubscriptionDelta>,
|
||||||
) -> AppResult<Json<SubscriptionChangeResponse>> {
|
) -> AppResult<Json<UpdatedUrlsResponse>> {
|
||||||
if id.format != Format::Json {
|
if id.format != Format::Json {
|
||||||
return Err(AppError::NotFound);
|
return Err(AppError::NotFound);
|
||||||
}
|
}
|
||||||
|
@ -49,7 +51,7 @@ pub async fn post_subscription_changes(
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|timestamp| {
|
.map(|timestamp| {
|
||||||
Json(SubscriptionChangeResponse {
|
Json(UpdatedUrlsResponse {
|
||||||
timestamp,
|
timestamp,
|
||||||
update_urls: Vec::new(),
|
update_urls: Vec::new(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
|
@ -64,7 +65,34 @@ pub struct SubscriptionDeltaResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct SubscriptionChangeResponse {
|
pub struct UpdatedUrlsResponse {
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
pub update_urls: Vec<(String, String)>,
|
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…
Reference in New Issue