refactor: decoupled gpodder and server models
parent
8a5e625e6f
commit
bd51c1c768
|
@ -1,5 +1,4 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
@ -8,8 +7,6 @@ pub struct User {
|
||||||
pub password_hash: String,
|
pub password_hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum DeviceType {
|
pub enum DeviceType {
|
||||||
Desktop,
|
Desktop,
|
||||||
Laptop,
|
Laptop,
|
||||||
|
@ -18,7 +15,6 @@ pub enum DeviceType {
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Device {
|
pub struct Device {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub caption: String,
|
pub caption: String,
|
||||||
|
@ -26,37 +22,28 @@ pub struct Device {
|
||||||
pub subscriptions: i64,
|
pub subscriptions: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct DevicePatch {
|
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 {
|
pub enum EpisodeActionType {
|
||||||
Download,
|
Download,
|
||||||
Play {
|
Play {
|
||||||
#[serde(default)]
|
|
||||||
started: Option<i32>,
|
started: Option<i32>,
|
||||||
position: i32,
|
position: i32,
|
||||||
#[serde(default)]
|
|
||||||
total: Option<i32>,
|
total: Option<i32>,
|
||||||
},
|
},
|
||||||
Delete,
|
Delete,
|
||||||
New,
|
New,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct EpisodeAction {
|
pub struct EpisodeAction {
|
||||||
pub podcast: String,
|
pub podcast: String,
|
||||||
pub episode: String,
|
pub episode: String,
|
||||||
pub timestamp: Option<DateTime<Utc>>,
|
pub timestamp: Option<DateTime<Utc>>,
|
||||||
pub time_changed: DateTime<Utc>,
|
pub time_changed: DateTime<Utc>,
|
||||||
#[serde(default)]
|
|
||||||
pub device: Option<String>,
|
pub device: Option<String>,
|
||||||
#[serde(flatten)]
|
|
||||||
pub action: EpisodeActionType,
|
pub action: EpisodeActionType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
||||||
gpodder::{
|
gpodder::{
|
||||||
auth_middleware,
|
auth_middleware,
|
||||||
format::{Format, StringWithFormat},
|
format::{Format, StringWithFormat},
|
||||||
|
models,
|
||||||
},
|
},
|
||||||
Context,
|
Context,
|
||||||
},
|
},
|
||||||
|
@ -28,7 +29,7 @@ async fn get_devices(
|
||||||
State(ctx): State<Context>,
|
State(ctx): State<Context>,
|
||||||
Path(username): Path<StringWithFormat>,
|
Path(username): Path<StringWithFormat>,
|
||||||
Extension(user): Extension<gpodder::User>,
|
Extension(user): Extension<gpodder::User>,
|
||||||
) -> AppResult<Json<Vec<gpodder::Device>>> {
|
) -> AppResult<Json<Vec<models::Device>>> {
|
||||||
if username.format != Format::Json {
|
if username.format != Format::Json {
|
||||||
return Err(AppError::NotFound);
|
return Err(AppError::NotFound);
|
||||||
}
|
}
|
||||||
|
@ -41,7 +42,7 @@ async fn get_devices(
|
||||||
tokio::task::spawn_blocking(move || ctx.store.devices_for_user(&user))
|
tokio::task::spawn_blocking(move || ctx.store.devices_for_user(&user))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(Json)?,
|
.map(|devices| Json(devices.into_iter().map(models::Device::from).collect()))?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,13 +50,13 @@ async fn post_device(
|
||||||
State(ctx): State<Context>,
|
State(ctx): State<Context>,
|
||||||
Path((_username, id)): Path<(String, StringWithFormat)>,
|
Path((_username, id)): Path<(String, StringWithFormat)>,
|
||||||
Extension(user): Extension<gpodder::User>,
|
Extension(user): Extension<gpodder::User>,
|
||||||
Json(patch): Json<gpodder::DevicePatch>,
|
Json(patch): Json<models::DevicePatch>,
|
||||||
) -> AppResult<()> {
|
) -> AppResult<()> {
|
||||||
if id.format != Format::Json {
|
if id.format != Format::Json {
|
||||||
return Err(AppError::NotFound);
|
return Err(AppError::NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || ctx.store.update_device_info(&user, &id, patch))
|
tokio::task::spawn_blocking(move || ctx.store.update_device_info(&user, &id, patch.into()))
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ use crate::{
|
||||||
gpodder::{
|
gpodder::{
|
||||||
auth_middleware,
|
auth_middleware,
|
||||||
format::{Format, StringWithFormat},
|
format::{Format, StringWithFormat},
|
||||||
|
models,
|
||||||
models::UpdatedUrlsResponse,
|
models::UpdatedUrlsResponse,
|
||||||
},
|
},
|
||||||
Context,
|
Context,
|
||||||
|
@ -33,7 +34,7 @@ async fn post_episode_actions(
|
||||||
State(ctx): State<Context>,
|
State(ctx): State<Context>,
|
||||||
Path(username): Path<StringWithFormat>,
|
Path(username): Path<StringWithFormat>,
|
||||||
Extension(user): Extension<gpodder::User>,
|
Extension(user): Extension<gpodder::User>,
|
||||||
Json(actions): Json<Vec<gpodder::EpisodeAction>>,
|
Json(actions): Json<Vec<models::EpisodeAction>>,
|
||||||
) -> AppResult<Json<UpdatedUrlsResponse>> {
|
) -> AppResult<Json<UpdatedUrlsResponse>> {
|
||||||
if username.format != Format::Json {
|
if username.format != Format::Json {
|
||||||
return Err(AppError::NotFound);
|
return Err(AppError::NotFound);
|
||||||
|
@ -43,8 +44,10 @@ async fn post_episode_actions(
|
||||||
return Err(AppError::BadRequest);
|
return Err(AppError::BadRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(
|
Ok(tokio::task::spawn_blocking(move || {
|
||||||
tokio::task::spawn_blocking(move || ctx.store.add_episode_actions(&user, actions))
|
ctx.store
|
||||||
|
.add_episode_actions(&user, actions.into_iter().map(Into::into).collect())
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|time_changed| {
|
.map(|time_changed| {
|
||||||
|
@ -52,8 +55,7 @@ async fn post_episode_actions(
|
||||||
timestamp: time_changed.timestamp(),
|
timestamp: time_changed.timestamp(),
|
||||||
update_urls: Vec::new(),
|
update_urls: Vec::new(),
|
||||||
})
|
})
|
||||||
})?,
|
})?)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
|
@ -68,7 +70,7 @@ struct FilterQuery {
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct EpisodeActionsResponse {
|
struct EpisodeActionsResponse {
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
actions: Vec<gpodder::EpisodeAction>,
|
actions: Vec<models::EpisodeAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_episode_actions(
|
async fn get_episode_actions(
|
||||||
|
@ -104,7 +106,7 @@ async fn get_episode_actions(
|
||||||
.map(|(ts, actions)| {
|
.map(|(ts, actions)| {
|
||||||
Json(EpisodeActionsResponse {
|
Json(EpisodeActionsResponse {
|
||||||
timestamp: ts.timestamp(),
|
timestamp: ts.timestamp(),
|
||||||
actions,
|
actions: actions.into_iter().map(Into::into).collect(),
|
||||||
})
|
})
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::gpodder;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct SubscriptionDelta {
|
pub struct SubscriptionDelta {
|
||||||
pub add: Vec<String>,
|
pub add: Vec<String>,
|
||||||
|
@ -18,3 +21,164 @@ pub struct UpdatedUrlsResponse {
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
pub update_urls: Vec<(String, String)>,
|
pub update_urls: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DeviceType {
|
||||||
|
Desktop,
|
||||||
|
Laptop,
|
||||||
|
Mobile,
|
||||||
|
Server,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Device {
|
||||||
|
pub id: String,
|
||||||
|
pub caption: String,
|
||||||
|
pub r#type: DeviceType,
|
||||||
|
pub subscriptions: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
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 {
|
||||||
|
pub podcast: String,
|
||||||
|
pub episode: String,
|
||||||
|
pub timestamp: Option<i64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub device: Option<String>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub action: EpisodeActionType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gpodder::DeviceType> for DeviceType {
|
||||||
|
fn from(value: gpodder::DeviceType) -> Self {
|
||||||
|
match value {
|
||||||
|
gpodder::DeviceType::Other => Self::Other,
|
||||||
|
gpodder::DeviceType::Laptop => Self::Laptop,
|
||||||
|
gpodder::DeviceType::Mobile => Self::Mobile,
|
||||||
|
gpodder::DeviceType::Server => Self::Server,
|
||||||
|
gpodder::DeviceType::Desktop => Self::Desktop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DeviceType> for gpodder::DeviceType {
|
||||||
|
fn from(value: DeviceType) -> Self {
|
||||||
|
match value {
|
||||||
|
DeviceType::Other => gpodder::DeviceType::Other,
|
||||||
|
DeviceType::Laptop => gpodder::DeviceType::Laptop,
|
||||||
|
DeviceType::Mobile => gpodder::DeviceType::Mobile,
|
||||||
|
DeviceType::Server => gpodder::DeviceType::Server,
|
||||||
|
DeviceType::Desktop => gpodder::DeviceType::Desktop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gpodder::Device> for Device {
|
||||||
|
fn from(value: gpodder::Device) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
caption: value.caption,
|
||||||
|
r#type: value.r#type.into(),
|
||||||
|
subscriptions: value.subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DevicePatch> for gpodder::DevicePatch {
|
||||||
|
fn from(value: DevicePatch) -> Self {
|
||||||
|
Self {
|
||||||
|
caption: value.caption,
|
||||||
|
r#type: value.r#type.map(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gpodder::EpisodeActionType> for EpisodeActionType {
|
||||||
|
fn from(value: gpodder::EpisodeActionType) -> Self {
|
||||||
|
match value {
|
||||||
|
gpodder::EpisodeActionType::New => Self::New,
|
||||||
|
gpodder::EpisodeActionType::Delete => Self::Delete,
|
||||||
|
gpodder::EpisodeActionType::Download => Self::Download,
|
||||||
|
gpodder::EpisodeActionType::Play {
|
||||||
|
started,
|
||||||
|
position,
|
||||||
|
total,
|
||||||
|
} => Self::Play {
|
||||||
|
started,
|
||||||
|
position,
|
||||||
|
total,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EpisodeActionType> for gpodder::EpisodeActionType {
|
||||||
|
fn from(value: EpisodeActionType) -> Self {
|
||||||
|
match value {
|
||||||
|
EpisodeActionType::New => gpodder::EpisodeActionType::New,
|
||||||
|
EpisodeActionType::Delete => gpodder::EpisodeActionType::Delete,
|
||||||
|
EpisodeActionType::Download => gpodder::EpisodeActionType::Download,
|
||||||
|
EpisodeActionType::Play {
|
||||||
|
started,
|
||||||
|
position,
|
||||||
|
total,
|
||||||
|
} => gpodder::EpisodeActionType::Play {
|
||||||
|
started,
|
||||||
|
position,
|
||||||
|
total,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gpodder::EpisodeAction> for EpisodeAction {
|
||||||
|
fn from(value: gpodder::EpisodeAction) -> Self {
|
||||||
|
Self {
|
||||||
|
podcast: value.podcast,
|
||||||
|
episode: value.episode,
|
||||||
|
timestamp: value.timestamp.map(|ts| ts.timestamp()),
|
||||||
|
device: value.device,
|
||||||
|
action: value.action.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EpisodeAction> for gpodder::EpisodeAction {
|
||||||
|
fn from(value: EpisodeAction) -> Self {
|
||||||
|
Self {
|
||||||
|
podcast: value.podcast,
|
||||||
|
episode: value.episode,
|
||||||
|
// TODO remove this unwrap
|
||||||
|
timestamp: value
|
||||||
|
.timestamp
|
||||||
|
.map(|ts| DateTime::from_timestamp(ts, 0).unwrap()),
|
||||||
|
device: value.device,
|
||||||
|
action: value.action.into(),
|
||||||
|
time_changed: DateTime::<Utc>::MIN_UTC,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue