@@ -7,7 +7,7 @@ use regex::Regex;
7
7
use reqwest:: header:: { HeaderValue , AUTHORIZATION } ;
8
8
use rust_decimal:: Decimal ;
9
9
use rust_decimal_macros:: dec;
10
- use sea_orm:: { ColumnTrait , DatabaseConnection , EntityTrait , QueryFilter } ;
10
+ use sea_orm:: { ColumnTrait , Condition , DatabaseConnection , EntityTrait , QueryFilter } ;
11
11
use sea_query:: { extension:: postgres:: PgExpr , Alias , Expr , Func } ;
12
12
use serde:: { Deserialize , Serialize } ;
13
13
@@ -52,6 +52,38 @@ impl IntegrationService {
52
52
Self { db : db. clone ( ) }
53
53
}
54
54
55
+ // DEV: Fuzzy search for show by episode name and series name.
56
+ async fn get_show_by_episode_identifier (
57
+ & self ,
58
+ series : & str ,
59
+ episode : & str ,
60
+ ) -> Result < metadata:: Model > {
61
+ let db_show = Metadata :: find ( )
62
+ . filter ( metadata:: Column :: Lot . eq ( MediaLot :: Show ) )
63
+ . filter ( metadata:: Column :: Source . eq ( MediaSource :: Tmdb ) )
64
+ . filter (
65
+ Condition :: all ( )
66
+ . add (
67
+ Expr :: expr ( Func :: cast_as (
68
+ Expr :: col ( metadata:: Column :: ShowSpecifics ) ,
69
+ Alias :: new ( "text" ) ,
70
+ ) )
71
+ . ilike ( ilike_sql ( episode) ) ,
72
+ )
73
+ . add ( Expr :: col ( metadata:: Column :: Title ) . ilike ( ilike_sql ( series) ) ) ,
74
+ )
75
+ . one ( & self . db )
76
+ . await ?;
77
+ match db_show {
78
+ Some ( show) => Ok ( show) ,
79
+ None => bail ! (
80
+ "No show found with Series {:#?} and Episode {:#?}" ,
81
+ series,
82
+ episode
83
+ ) ,
84
+ }
85
+ }
86
+
55
87
pub async fn jellyfin_progress ( & self , payload : & str ) -> Result < IntegrationMediaSeen > {
56
88
mod models {
57
89
use super :: * ;
@@ -215,25 +247,11 @@ impl IntegrationService {
215
247
let ( identifier, lot) = match payload. metadata . item_type . as_str ( ) {
216
248
"movie" => ( identifier. to_owned ( ) , MediaLot :: Movie ) ,
217
249
"episode" => {
218
- // DEV: Since Plex and Ryot both use TMDb, we can safely assume that the
219
- // TMDB ID sent by Plex (which is actually the episode ID) is also present
220
- // in the media specifics we have in DB.
221
- let db_show = Metadata :: find ( )
222
- . filter ( metadata:: Column :: Lot . eq ( MediaLot :: Show ) )
223
- . filter ( metadata:: Column :: Source . eq ( MediaSource :: Tmdb ) )
224
- . filter (
225
- Expr :: expr ( Func :: cast_as (
226
- Expr :: col ( metadata:: Column :: ShowSpecifics ) ,
227
- Alias :: new ( "text" ) ,
228
- ) )
229
- . ilike ( ilike_sql ( identifier) ) ,
230
- )
231
- . one ( & self . db )
250
+ let series_name = payload. metadata . show_name . as_ref ( ) . unwrap ( ) ;
251
+ let db_show = self
252
+ . get_show_by_episode_identifier ( series_name, identifier)
232
253
. await ?;
233
- if db_show. is_none ( ) {
234
- bail ! ( "No show found with TMDb ID {}" , identifier) ;
235
- }
236
- ( db_show. unwrap ( ) . identifier , MediaLot :: Show )
254
+ ( db_show. identifier , MediaLot :: Show )
237
255
}
238
256
_ => bail ! ( "Only movies and shows supported" ) ,
239
257
} ;
@@ -257,6 +275,105 @@ impl IntegrationService {
257
275
} )
258
276
}
259
277
278
+ pub async fn emby_progress ( & self , payload : & str ) -> Result < IntegrationMediaSeen > {
279
+ mod models {
280
+ use super :: * ;
281
+
282
+ #[ derive( Serialize , Deserialize , Debug , Clone ) ]
283
+ #[ serde( rename_all = "PascalCase" ) ]
284
+ pub struct EmbyWebhookPlaybackInfoPayload {
285
+ pub position_ticks : Option < Decimal > ,
286
+ }
287
+ #[ derive( Serialize , Deserialize , Debug , Clone ) ]
288
+ #[ serde( rename_all = "PascalCase" ) ]
289
+ pub struct EmbyWebhookItemProviderIdsPayload {
290
+ pub tmdb : Option < String > ,
291
+ }
292
+ #[ derive( Serialize , Deserialize , Debug , Clone ) ]
293
+ #[ serde( rename_all = "PascalCase" ) ]
294
+ pub struct EmbyWebhookItemPayload {
295
+ pub run_time_ticks : Option < Decimal > ,
296
+ #[ serde( rename = "Type" ) ]
297
+ pub item_type : String ,
298
+ pub provider_ids : EmbyWebhookItemProviderIdsPayload ,
299
+ #[ serde( rename = "ParentIndexNumber" ) ]
300
+ pub season_number : Option < i32 > ,
301
+ #[ serde( rename = "IndexNumber" ) ]
302
+ pub episode_number : Option < i32 > ,
303
+ #[ serde( rename = "Name" ) ]
304
+ pub episode_name : Option < String > ,
305
+ pub series_name : Option < String > ,
306
+ }
307
+ #[ derive( Serialize , Deserialize , Debug , Clone ) ]
308
+ #[ serde( rename_all = "PascalCase" ) ]
309
+ pub struct EmbyWebhookPayload {
310
+ pub event : Option < String > ,
311
+ pub item : EmbyWebhookItemPayload ,
312
+ pub series : Option < EmbyWebhookItemPayload > ,
313
+ pub playback_info : EmbyWebhookPlaybackInfoPayload ,
314
+ }
315
+ }
316
+
317
+ let payload = serde_json:: from_str :: < models:: EmbyWebhookPayload > ( payload) ?;
318
+
319
+ let identifier = if let Some ( id) = payload. item . provider_ids . tmdb . as_ref ( ) {
320
+ Some ( id. clone ( ) )
321
+ } else {
322
+ payload
323
+ . series
324
+ . as_ref ( )
325
+ . and_then ( |s| s. provider_ids . tmdb . clone ( ) )
326
+ } ;
327
+
328
+ if payload. item . run_time_ticks . is_none ( ) {
329
+ bail ! ( "No run time associated with this media" )
330
+ }
331
+ if payload. playback_info . position_ticks . is_none ( ) {
332
+ bail ! ( "No position associated with this media" )
333
+ }
334
+
335
+ let runtime = payload. item . run_time_ticks . unwrap ( ) ;
336
+ let position = payload. playback_info . position_ticks . unwrap ( ) ;
337
+
338
+ let ( identifier, lot) = match payload. item . item_type . as_str ( ) {
339
+ "Movie" => {
340
+ if identifier. is_none ( ) {
341
+ bail ! ( "No TMDb ID associated with this media" )
342
+ }
343
+
344
+ ( identifier. unwrap ( ) . to_owned ( ) , MediaLot :: Movie )
345
+ }
346
+ "Episode" => {
347
+ if payload. item . episode_name . is_none ( ) {
348
+ bail ! ( "No episode name associated with this media" )
349
+ }
350
+
351
+ if payload. item . series_name . is_none ( ) {
352
+ bail ! ( "No series name associated with this media" )
353
+ }
354
+
355
+ let series_name = payload. item . series_name . unwrap ( ) ;
356
+ let episode_name = payload. item . episode_name . unwrap ( ) ;
357
+ let db_show = self
358
+ . get_show_by_episode_identifier ( & series_name, & episode_name)
359
+ . await ?;
360
+ ( db_show. identifier , MediaLot :: Show )
361
+ }
362
+ _ => bail ! ( "Only movies and shows supported" ) ,
363
+ } ;
364
+
365
+ Ok ( IntegrationMediaSeen {
366
+ identifier,
367
+ lot,
368
+ source : MediaSource :: Tmdb ,
369
+ progress : position / runtime * dec ! ( 100 ) ,
370
+ show_season_number : payload. item . season_number ,
371
+ show_episode_number : payload. item . episode_number ,
372
+ provider_watched_on : Some ( "Emby" . to_string ( ) ) ,
373
+ ..Default :: default ( )
374
+ } )
375
+ }
376
+
260
377
pub async fn kodi_progress ( & self , payload : & str ) -> Result < IntegrationMediaSeen > {
261
378
let mut payload = match serde_json:: from_str :: < IntegrationMediaSeen > ( payload) {
262
379
Result :: Ok ( val) => val,
0 commit comments