refactor: decoupled gpodder and server models

Jef Roosens 2025-03-15 20:06:58 +01:00
parent 8a5e625e6f
commit bd51c1c768
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
4 changed files with 185 additions and 31 deletions

View File

@ -1,5 +1,4 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct User {
@ -8,8 +7,6 @@ pub struct User {
pub password_hash: String,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DeviceType {
Desktop,
Laptop,
@ -18,7 +15,6 @@ pub enum DeviceType {
Other,
}
#[derive(Serialize)]
pub struct Device {
pub id: String,
pub caption: String,
@ -26,37 +22,28 @@ pub struct Device {
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<DateTime<Utc>>,
pub time_changed: DateTime<Utc>,
#[serde(default)]
pub device: Option<String>,
#[serde(flatten)]
pub action: EpisodeActionType,
}

View File

@ -12,6 +12,7 @@ use crate::{
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models,
},
Context,
},
@ -28,7 +29,7 @@ async fn get_devices(
State(ctx): State<Context>,
Path(username): Path<StringWithFormat>,
Extension(user): Extension<gpodder::User>,
) -> AppResult<Json<Vec<gpodder::Device>>> {
) -> AppResult<Json<Vec<models::Device>>> {
if username.format != Format::Json {
return Err(AppError::NotFound);
}
@ -41,7 +42,7 @@ async fn get_devices(
tokio::task::spawn_blocking(move || ctx.store.devices_for_user(&user))
.await
.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>,
Path((_username, id)): Path<(String, StringWithFormat)>,
Extension(user): Extension<gpodder::User>,
Json(patch): Json<gpodder::DevicePatch>,
Json(patch): Json<models::DevicePatch>,
) -> AppResult<()> {
if id.format != Format::Json {
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
.unwrap()?;

View File

@ -14,6 +14,7 @@ use crate::{
gpodder::{
auth_middleware,
format::{Format, StringWithFormat},
models,
models::UpdatedUrlsResponse,
},
Context,
@ -33,7 +34,7 @@ async fn post_episode_actions(
State(ctx): State<Context>,
Path(username): Path<StringWithFormat>,
Extension(user): Extension<gpodder::User>,
Json(actions): Json<Vec<gpodder::EpisodeAction>>,
Json(actions): Json<Vec<models::EpisodeAction>>,
) -> AppResult<Json<UpdatedUrlsResponse>> {
if username.format != Format::Json {
return Err(AppError::NotFound);
@ -43,17 +44,18 @@ async fn post_episode_actions(
return Err(AppError::BadRequest);
}
Ok(
tokio::task::spawn_blocking(move || ctx.store.add_episode_actions(&user, actions))
.await
.unwrap()
.map(|time_changed| {
Json(UpdatedUrlsResponse {
timestamp: time_changed.timestamp(),
update_urls: Vec::new(),
})
})?,
)
Ok(tokio::task::spawn_blocking(move || {
ctx.store
.add_episode_actions(&user, actions.into_iter().map(Into::into).collect())
})
.await
.unwrap()
.map(|time_changed| {
Json(UpdatedUrlsResponse {
timestamp: time_changed.timestamp(),
update_urls: Vec::new(),
})
})?)
}
#[derive(Deserialize, Default)]
@ -68,7 +70,7 @@ struct FilterQuery {
#[derive(Serialize)]
struct EpisodeActionsResponse {
timestamp: i64,
actions: Vec<gpodder::EpisodeAction>,
actions: Vec<models::EpisodeAction>,
}
async fn get_episode_actions(
@ -104,7 +106,7 @@ async fn get_episode_actions(
.map(|(ts, actions)| {
Json(EpisodeActionsResponse {
timestamp: ts.timestamp(),
actions,
actions: actions.into_iter().map(Into::into).collect(),
})
})?)
}

View File

@ -1,5 +1,8 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::gpodder;
#[derive(Deserialize, Debug)]
pub struct SubscriptionDelta {
pub add: Vec<String>,
@ -18,3 +21,164 @@ pub struct UpdatedUrlsResponse {
pub timestamp: i64,
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,
}
}
}