Skip to content

Commit a8d3135

Browse files
authored
Add support for MarkPlayed event from Jellyfin (#1288)
* refactor(integration): rename yank_progress to sink_progress across providers - Update method names from `yank_progress` to `sink_progress` in all integration sink modules - Modify integration service to use the new method names - Consistent naming across Kodi, Emby, Jellyfin, Plex, and Generic JSON providers * refactor(integration): update Jellyfin webhook payload model * refactor(integration): enhance Jellyfin progress tracking logic * feat(integration): add MarkPlayed event handling for Jellyfin progress tracking * refactor(integration): simplify Jellyfin progress tracking implementation * docs(integration): update Jellyfin webhook events documentation - Add `MarkPlayed` to the list of supported webhook events - Clarify event configuration for Jellyfin integration
1 parent 4cce9ec commit a8d3135

File tree

7 files changed

+40
-32
lines changed

7 files changed

+40
-32
lines changed

crates/services/integration/src/lib.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,14 @@ impl IntegrationService {
165165
return Err(Error::new("Integration is disabled".to_owned()));
166166
}
167167
let maybe_progress_update = match integration.provider {
168-
IntegrationProvider::Kodi => sink::kodi::yank_progress(payload).await,
169-
IntegrationProvider::Emby => sink::emby::yank_progress(payload, &self.0.db).await,
170-
IntegrationProvider::JellyfinSink => sink::jellyfin::yank_progress(payload).await,
168+
IntegrationProvider::Kodi => sink::kodi::sink_progress(payload).await,
169+
IntegrationProvider::Emby => sink::emby::sink_progress(payload, &self.0.db).await,
170+
IntegrationProvider::JellyfinSink => sink::jellyfin::sink_progress(payload).await,
171171
IntegrationProvider::PlexSink => {
172172
let specifics = integration.clone().provider_specifics.unwrap();
173-
sink::plex::yank_progress(payload, &self.0.db, specifics.plex_sink_username).await
173+
sink::plex::sink_progress(payload, &self.0.db, specifics.plex_sink_username).await
174174
}
175-
IntegrationProvider::GenericJson => sink::generic_json::yank_progress(payload).await,
175+
IntegrationProvider::GenericJson => sink::generic_json::sink_progress(payload).await,
176176
_ => return Err(Error::new("Unsupported integration source".to_owned())),
177177
};
178178
match maybe_progress_update {

crates/services/integration/src/sink/emby.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ mod models {
4747
}
4848
}
4949

50-
pub async fn yank_progress(payload: String, db: &DatabaseConnection) -> Result<ImportResult> {
50+
pub async fn sink_progress(payload: String, db: &DatabaseConnection) -> Result<ImportResult> {
5151
let payload: models::EmbyWebhookPayload = serde_json::from_str(&payload)?;
5252
let runtime = payload
5353
.item

crates/services/integration/src/sink/generic_json.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::{bail, Result};
22
use dependent_models::{CompleteExport, ImportCompletedItem, ImportResult};
33

4-
pub async fn yank_progress(payload: String) -> Result<ImportResult> {
4+
pub async fn sink_progress(payload: String) -> Result<ImportResult> {
55
let payload = match serde_json::from_str::<CompleteExport>(&payload) {
66
Ok(val) => val,
77
Err(err) => bail!(err),

crates/services/integration/src/sink/jellyfin.rs

+30-22
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,26 @@ mod models {
2727
#[derive(Serialize, Deserialize, Debug, Clone)]
2828
#[serde(rename_all = "PascalCase")]
2929
pub struct JellyfinWebhookItemPayload {
30-
pub run_time_ticks: Option<Decimal>,
3130
#[serde(rename = "Type")]
3231
pub item_type: String,
33-
pub provider_ids: JellyfinWebhookItemProviderIdsPayload,
3432
#[serde(rename = "ParentIndexNumber")]
3533
pub season_number: Option<i32>,
3634
#[serde(rename = "IndexNumber")]
3735
pub episode_number: Option<i32>,
36+
pub run_time_ticks: Option<Decimal>,
37+
pub provider_ids: JellyfinWebhookItemProviderIdsPayload,
3838
}
3939
#[derive(Serialize, Deserialize, Debug, Clone)]
4040
#[serde(rename_all = "PascalCase")]
4141
pub struct JellyfinWebhookPayload {
4242
pub event: Option<String>,
4343
pub item: JellyfinWebhookItemPayload,
4444
pub series: Option<JellyfinWebhookItemPayload>,
45-
pub session: JellyfinWebhookSessionPayload,
45+
pub session: Option<JellyfinWebhookSessionPayload>,
4646
}
4747
}
4848

49-
pub async fn yank_progress(payload: String) -> Result<ImportResult> {
49+
pub async fn sink_progress(payload: String) -> Result<ImportResult> {
5050
let payload = serde_json::from_str::<models::JellyfinWebhookPayload>(&payload)?;
5151
let identifier = payload
5252
.item
@@ -62,35 +62,43 @@ pub async fn yank_progress(payload: String) -> Result<ImportResult> {
6262
.ok_or_else(|| anyhow::anyhow!("No TMDb ID associated with this media"))?
6363
.clone();
6464

65-
let runtime = payload
66-
.item
67-
.run_time_ticks
68-
.ok_or_else(|| anyhow::anyhow!("No run time associated with this media"))?;
69-
70-
let position = payload
71-
.session
72-
.play_state
73-
.position_ticks
74-
.ok_or_else(|| anyhow::anyhow!("No position associated with this media"))?;
75-
7665
let lot = match payload.item.item_type.as_str() {
7766
"Episode" => MediaLot::Show,
7867
"Movie" => MediaLot::Movie,
7968
_ => bail!("Only movies and shows supported"),
8069
};
8170

71+
let mut seen_item = ImportOrExportMetadataItemSeen {
72+
show_season_number: payload.item.season_number,
73+
show_episode_number: payload.item.episode_number,
74+
provider_watched_on: Some("Jellyfin".to_string()),
75+
..Default::default()
76+
};
77+
78+
match payload.event.unwrap_or_default().as_str() {
79+
"MarkPlayed" => {}
80+
_ => {
81+
let runtime = payload
82+
.item
83+
.run_time_ticks
84+
.ok_or_else(|| anyhow::anyhow!("No run time associated with this media"))?;
85+
86+
let position = payload
87+
.session
88+
.as_ref()
89+
.and_then(|s| s.play_state.position_ticks.as_ref())
90+
.ok_or_else(|| anyhow::anyhow!("No position associated with this media"))?;
91+
92+
seen_item.progress = Some(position / runtime * dec!(100));
93+
}
94+
}
95+
8296
Ok(ImportResult {
8397
completed: vec![ImportCompletedItem::Metadata(ImportOrExportMetadataItem {
8498
lot,
8599
identifier,
86100
source: MediaSource::Tmdb,
87-
seen_history: vec![ImportOrExportMetadataItemSeen {
88-
progress: Some(position / runtime * dec!(100)),
89-
show_season_number: payload.item.season_number,
90-
show_episode_number: payload.item.episode_number,
91-
provider_watched_on: Some("Jellyfin".to_string()),
92-
..Default::default()
93-
}],
101+
seen_history: vec![seen_item],
94102
..Default::default()
95103
})],
96104
..Default::default()

crates/services/integration/src/sink/kodi.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ struct IntegrationMediaSeen {
1414
show_episode_number: Option<i32>,
1515
}
1616

17-
pub async fn yank_progress(payload: String) -> Result<ImportResult> {
17+
pub async fn sink_progress(payload: String) -> Result<ImportResult> {
1818
let payload = match serde_json::from_str::<IntegrationMediaSeen>(&payload) {
1919
Ok(val) => val,
2020
Err(err) => bail!(err),

crates/services/integration/src/sink/plex.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ fn calculate_progress(payload: &models::PlexWebhookPayload) -> Result<Decimal> {
8989
}
9090
}
9191

92-
pub async fn yank_progress(
92+
pub async fn sink_progress(
9393
payload: String,
9494
db: &DatabaseConnection,
9595
plex_user: Option<String>,

docs/content/integrations.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ will work for all the media that have a valid TMDb ID attached to their metadata
4242
- Webhook Url => `<paste_url_copied>`
4343
- Payload format => `Default`
4444
- Listen to events only for => Choose your user
45-
- Events => `Play`, `Pause`, `Resume`, `Stop` and `Progress`
45+
- Events => `Play`, `Pause`, `Resume`, `Stop`, `Progress` and `MarkPlayed`
4646

4747
### Emby
4848

0 commit comments

Comments
 (0)