Skip to content

Commit 17cf9ad

Browse files
authored
Jellyfin fixes (#864)
* refactor(backend): extract to variable * feat(backend): get jellyfin media directly Do not go through each library. * fix(backend): do not append to history multiple times * feat(backend): get seen started_at and ended_at as naive dates * build(backend): bump version
1 parent 87c5d6d commit 17cf9ad

File tree

12 files changed

+102
-146
lines changed

12 files changed

+102
-146
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/backend/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ryot"
3-
version = "6.1.0"
3+
version = "6.1.1"
44
edition = "2021"
55
repository = "https://github.com/IgnisDa/ryot"
66
license = "GPL-3.0"

apps/backend/src/importer/goodreads.rs

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use async_graphql::Result;
2-
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
2+
use chrono::NaiveDate;
33
use convert_case::{Case, Casing};
44
use csv::Reader;
55
use database::{ImportSource, MediaLot, MediaSource};
@@ -87,11 +87,7 @@ pub async fn import(
8787
];
8888
if let Some(w) = record.date_read {
8989
let w = NaiveDate::parse_from_str(&w, "%Y/%m/%d").unwrap();
90-
let read_at = Some(DateTime::from_naive_utc_and_offset(
91-
NaiveDateTime::new(w, NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
92-
Utc,
93-
));
94-
seen_history.first_mut().unwrap().ended_on = read_at;
90+
seen_history.first_mut().unwrap().ended_on = Some(w);
9591
}
9692
let mut collections = vec![];
9793
if !record.bookshelf.is_empty() {

apps/backend/src/importer/jellyfin.rs

+83-119
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
use async_graphql::Result;
22
use database::{MediaLot, MediaSource};
33
use enum_meta::HashMap;
4-
use itertools::Itertools;
54
use sea_orm::prelude::DateTimeUtc;
65
use serde::{Deserialize, Serialize};
76
use serde_json::json;
87
use surf::{
9-
http::headers::{ACCEPT, USER_AGENT},
8+
http::headers::{ACCEPT, AUTHORIZATION, USER_AGENT},
109
Client, Config, Url,
1110
};
1211

@@ -20,14 +19,8 @@ use crate::{
2019
utils::USER_AGENT_STR,
2120
};
2221

23-
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
24-
#[serde(rename_all = "lowercase")]
25-
enum CollectionType {
26-
Movies,
27-
Tvshows,
28-
#[serde(untagged)]
29-
Unknown(String),
30-
}
22+
static EMBY_HEADER_VALUE: &str =
23+
r#"MediaBrowser , Client="other", Device="script", DeviceId="script", Version="0.0.0""#;
3124

3225
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
3326
enum MediaType {
@@ -47,7 +40,6 @@ struct ItemProviderIdsPayload {
4740
#[derive(Serialize, Deserialize, Debug, Clone)]
4841
#[serde(rename_all = "PascalCase")]
4942
struct ItemUserData {
50-
play_count: Option<i32>,
5143
last_played_date: Option<DateTimeUtc>,
5244
is_favorite: Option<bool>,
5345
}
@@ -64,7 +56,6 @@ struct ItemResponse {
6456
series_name: Option<String>,
6557
user_data: Option<ItemUserData>,
6658
parent_index_number: Option<i32>,
67-
collection_type: Option<CollectionType>,
6859
provider_ids: Option<ItemProviderIdsPayload>,
6960
}
7061

@@ -82,19 +73,18 @@ struct AuthenticateResponse {
8273
}
8374

8475
pub async fn import(input: DeployUrlAndKeyAndUsernameImportInput) -> Result<ImportResult> {
85-
let authenticate: AuthenticateResponse = surf::post(format!(
86-
"{}/Users/AuthenticateByName",
87-
input.api_url
88-
))
89-
.header(
90-
"X-Emby-Authorization",
91-
r#"MediaBrowser , Client="other", Device="script", DeviceId="script", Version="0.0.0""#,
92-
)
93-
.body_json(&serde_json::json!({ "Username": input.username, "Pw": input.password }))
94-
.unwrap()
95-
.await?
96-
.body_json()
97-
.await?;
76+
let uri = format!("{}/Users/AuthenticateByName", input.api_url);
77+
let authenticate: AuthenticateResponse = surf::post(uri)
78+
.header(AUTHORIZATION, EMBY_HEADER_VALUE)
79+
.body_json(&serde_json::json!({ "Username": input.username, "Pw": input.password }))
80+
.unwrap()
81+
.await
82+
.unwrap()
83+
.body_json()
84+
.await
85+
.unwrap();
86+
tracing::debug!("Authenticated with token: {}", authenticate.access_token);
87+
9888
let client: Client = Config::new()
9989
.add_header(USER_AGENT, USER_AGENT_STR)
10090
.unwrap()
@@ -106,12 +96,16 @@ pub async fn import(input: DeployUrlAndKeyAndUsernameImportInput) -> Result<Impo
10696
.try_into()
10797
.unwrap();
10898
let user_id = authenticate.user.id;
99+
tracing::debug!("Authenticated as user id: {}", user_id);
109100

110101
let mut to_handle_media = vec![];
111102
let mut failed_items = vec![];
112103

113-
let views_data: ItemsResponse = client
114-
.get(&format!("Users/{}/Views", user_id))
104+
let query = json!({ "recursive": true, "IsPlayed": true, "fields": "ProviderIds" });
105+
let library_data: ItemsResponse = client
106+
.get(&format!("Users/{}/Items", user_id))
107+
.query(&query)
108+
.unwrap()
115109
.await
116110
.unwrap()
117111
.body_json()
@@ -120,107 +114,77 @@ pub async fn import(input: DeployUrlAndKeyAndUsernameImportInput) -> Result<Impo
120114

121115
let mut series_id_to_tmdb_id: HashMap<String, Option<String>> = HashMap::new();
122116

123-
for library in views_data.items {
124-
let collection_type = library.collection_type.unwrap();
125-
if matches!(collection_type, CollectionType::Unknown(_)) {
126-
failed_items.push(ImportFailedItem {
127-
step: ImportFailStep::ItemDetailsFromSource,
128-
identifier: library.name,
129-
error: Some(format!("Unknown collection type: {:?}", collection_type)),
130-
lot: None,
131-
});
132-
continue;
133-
}
134-
let query = json!({
135-
"parentId": library.id, "recursive": true,
136-
"IsPlayed": true, "fields": "ProviderIds"
137-
});
138-
let library_data: ItemsResponse = client
139-
.get(&format!("Users/{}/Items", user_id))
140-
.query(&query)
141-
.unwrap()
142-
.await
143-
.unwrap()
144-
.body_json()
145-
.await
146-
.unwrap();
147-
for item in library_data.items {
148-
let typ = item.typ.clone().unwrap();
149-
tracing::debug!("Processing item: {:?} ({:?})", item.name, typ);
150-
let (lot, tmdb_id, ssn, sen) = match typ.clone() {
151-
MediaType::Movie => (MediaLot::Movie, item.provider_ids.unwrap().tmdb, None, None),
152-
MediaType::Series | MediaType::Episode => {
153-
if let Some(series_id) = item.series_id {
154-
let mut tmdb_id = series_id_to_tmdb_id.get(&series_id).cloned().flatten();
155-
if tmdb_id.is_none() {
156-
let details: ItemResponse = client
157-
.get(&format!("Items/{}", series_id))
158-
.await
159-
.unwrap()
160-
.body_json()
161-
.await
162-
.unwrap();
163-
let insert_id = details.provider_ids.unwrap().tmdb;
164-
series_id_to_tmdb_id.insert(series_id.clone(), insert_id.clone());
165-
tmdb_id = insert_id;
166-
}
167-
(
168-
MediaLot::Show,
169-
tmdb_id,
170-
item.parent_index_number,
171-
item.index_number,
172-
)
173-
} else {
174-
continue;
117+
for item in library_data.items {
118+
let typ = item.typ.clone().unwrap();
119+
tracing::debug!("Processing item: {:?} ({:?})", item.name, typ);
120+
let (lot, tmdb_id, ssn, sen) = match typ.clone() {
121+
MediaType::Movie => (MediaLot::Movie, item.provider_ids.unwrap().tmdb, None, None),
122+
MediaType::Series | MediaType::Episode => {
123+
if let Some(series_id) = item.series_id {
124+
let mut tmdb_id = series_id_to_tmdb_id.get(&series_id).cloned().flatten();
125+
if tmdb_id.is_none() {
126+
let details: ItemResponse = client
127+
.get(&format!("Items/{}", series_id))
128+
.await
129+
.unwrap()
130+
.body_json()
131+
.await
132+
.unwrap();
133+
let insert_id = details.provider_ids.unwrap().tmdb;
134+
series_id_to_tmdb_id.insert(series_id.clone(), insert_id.clone());
135+
tmdb_id = insert_id;
175136
}
176-
}
177-
_ => {
178-
failed_items.push(ImportFailedItem {
179-
step: ImportFailStep::ItemDetailsFromSource,
180-
identifier: item.name,
181-
error: Some(format!("Unknown media type: {:?}", typ)),
182-
lot: None,
183-
});
137+
(
138+
MediaLot::Show,
139+
tmdb_id,
140+
item.parent_index_number,
141+
item.index_number,
142+
)
143+
} else {
184144
continue;
185145
}
186-
};
187-
if let Some(tmdb_id) = tmdb_id {
188-
let item_user_data = item.user_data.unwrap();
189-
let num_times_seen = item_user_data.play_count.unwrap_or(0);
190-
let mut seen_history = (0..num_times_seen)
191-
.map(|_| ImportOrExportMediaItemSeen {
192-
show_season_number: ssn,
193-
show_episode_number: sen,
194-
..Default::default()
195-
})
196-
.collect_vec();
197-
if let Some(last) = seen_history.last_mut() {
198-
last.ended_on = item_user_data.last_played_date;
199-
};
200-
let mut collections = vec![];
201-
if let Some(true) = item_user_data.is_favorite {
202-
collections.push("Favorites".to_string());
203-
}
204-
to_handle_media.push(ImportOrExportMediaItem {
205-
lot,
206-
source_id: item.series_name.unwrap_or(item.name),
207-
source: MediaSource::Tmdb,
208-
internal_identifier: Some(ImportOrExportItemIdentifier::NeedsDetails(
209-
tmdb_id.clone(),
210-
)),
211-
seen_history,
212-
identifier: tmdb_id,
213-
reviews: vec![],
214-
collections,
215-
});
216-
} else {
146+
}
147+
_ => {
217148
failed_items.push(ImportFailedItem {
218149
step: ImportFailStep::ItemDetailsFromSource,
219150
identifier: item.name,
220-
error: Some("No tmdb id found".to_string()),
151+
error: Some(format!("Unknown media type: {:?}", typ)),
221152
lot: None,
222153
});
154+
continue;
223155
}
156+
};
157+
if let Some(tmdb_id) = tmdb_id {
158+
let item_user_data = item.user_data.unwrap();
159+
let seen = ImportOrExportMediaItemSeen {
160+
show_season_number: ssn,
161+
show_episode_number: sen,
162+
ended_on: item_user_data.last_played_date.map(|d| d.date_naive()),
163+
..Default::default()
164+
};
165+
let mut collections = vec![];
166+
if let Some(true) = item_user_data.is_favorite {
167+
collections.push("Favorites".to_string());
168+
}
169+
to_handle_media.push(ImportOrExportMediaItem {
170+
lot,
171+
source_id: item.series_name.unwrap_or(item.name),
172+
source: MediaSource::Tmdb,
173+
internal_identifier: Some(ImportOrExportItemIdentifier::NeedsDetails(
174+
tmdb_id.clone(),
175+
)),
176+
seen_history: vec![seen],
177+
identifier: tmdb_id,
178+
reviews: vec![],
179+
collections,
180+
});
181+
} else {
182+
failed_items.push(ImportFailedItem {
183+
step: ImportFailStep::ItemDetailsFromSource,
184+
identifier: item.name,
185+
error: Some("No tmdb id found".to_string()),
186+
lot: None,
187+
});
224188
}
225189
}
226190

apps/backend/src/importer/mal.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ use std::{
44
};
55

66
use async_graphql::Result;
7+
use chrono::NaiveDate;
78
use database::{ImportSource, MediaLot, MediaSource};
89
use flate2::bufread::GzDecoder;
910
use itertools::Itertools;
10-
use rs_utils::{convert_naive_to_utc, convert_string_to_date};
11+
use rs_utils::convert_string_to_date;
1112
use rust_decimal::{prelude::FromPrimitive, Decimal};
1213
use rust_decimal_macros::dec;
13-
use sea_orm::prelude::DateTimeUtc;
1414
use serde::{de::DeserializeOwned, Deserialize, Serialize};
1515

1616
use crate::{
@@ -55,11 +55,11 @@ where
5555
Ok(deserialized)
5656
}
5757

58-
fn get_date(date: String) -> Option<DateTimeUtc> {
58+
fn get_date(date: String) -> Option<NaiveDate> {
5959
if date.starts_with("0000") {
6060
None
6161
} else {
62-
convert_string_to_date(&date).map(convert_naive_to_utc)
62+
convert_string_to_date(&date)
6363
}
6464
}
6565

apps/backend/src/importer/media_tracker.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ pub async fn import(input: DeployUrlAndKeyImportInput) -> Result<ImportResult> {
348348
(None, None)
349349
};
350350
ImportOrExportMediaItemSeen {
351-
ended_on: s.date,
351+
ended_on: s.date.map(|d| d.date_naive()),
352352
show_season_number: season_number,
353353
show_episode_number: episode_number,
354354
provider_watched_on: Some(ImportSource::MediaTracker.to_string()),

apps/backend/src/importer/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ impl ImporterService {
372372
ProgressUpdateInput {
373373
metadata_id: metadata.id.clone(),
374374
progress,
375-
date: seen.ended_on.map(|d| d.date_naive()),
375+
date: seen.ended_on,
376376
show_season_number: seen.show_season_number,
377377
show_episode_number: seen.show_episode_number,
378378
podcast_episode_number: seen.podcast_episode_number,

apps/backend/src/importer/movary.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ pub async fn import(input: DeployMovaryImportInput) -> Result<ImportResult> {
122122
let watched_at = Some(convert_naive_to_utc(record.watched_at));
123123
let seen_item = ImportOrExportMediaItemSeen {
124124
started_on: None,
125-
ended_on: watched_at,
125+
ended_on: watched_at.map(|d| d.date_naive()),
126126
provider_watched_on: Some(ImportSource::Movary.to_string()),
127127
..Default::default()
128128
};

apps/backend/src/importer/story_graph.rs

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use async_graphql::Result;
2-
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
2+
use chrono::NaiveDate;
33
use convert_case::{Case, Casing};
44
use csv::Reader;
55
use database::{ImportSource, MediaLot, MediaSource};
@@ -94,11 +94,7 @@ pub async fn import(
9494
];
9595
if let Some(w) = record.last_date_read {
9696
let w = NaiveDate::parse_from_str(&w, "%Y/%m/%d").unwrap();
97-
let read_at = Some(DateTime::from_naive_utc_and_offset(
98-
NaiveDateTime::new(w, NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
99-
Utc,
100-
));
101-
seen_history.first_mut().unwrap().ended_on = read_at;
97+
seen_history.first_mut().unwrap().ended_on = Some(w);
10298
}
10399
let mut collections = vec![];
104100
collections.push(match record.read_status {

apps/backend/src/importer/trakt.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ pub async fn import(input: DeployTraktImportInput) -> Result<ImportResult> {
202202
continue;
203203
}
204204
d.seen_history.push(ImportOrExportMediaItemSeen {
205-
ended_on: item.watched_at,
205+
ended_on: item.watched_at.map(|d| d.date_naive()),
206206
show_season_number,
207207
provider_watched_on: Some(ImportSource::Trakt.to_string()),
208208
show_episode_number,

0 commit comments

Comments
 (0)