aboutsummaryrefslogtreecommitdiff
path: root/import/src
diff options
context:
space:
mode:
Diffstat (limited to 'import/src')
-rw-r--r--import/src/lib.rs102
-rw-r--r--import/src/tmdb.rs32
-rw-r--r--import/src/trakt.rs127
3 files changed, 197 insertions, 64 deletions
diff --git a/import/src/lib.rs b/import/src/lib.rs
index 0d8a10a..0990ba1 100644
--- a/import/src/lib.rs
+++ b/import/src/lib.rs
@@ -18,6 +18,7 @@ use jellyclient::{Appearance, PeopleGroup, TmdbKind, TraktKind, Visibility};
use log::info;
use matroska::matroska_metadata;
use rayon::iter::{ParallelBridge, ParallelIterator};
+use regex::Regex;
use std::{
collections::{BTreeMap, HashMap},
fs::{read_to_string, File},
@@ -42,8 +43,8 @@ pub mod trakt;
static IMPORT_SEM: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1));
pub static IMPORT_ERRORS: RwLock<Vec<String>> = RwLock::const_new(Vec::new());
-// static RE_EPISODE_FILENAME: LazyLock<Regex> =
-// LazyLock::new(|| Regex::new(r#"([sS](\d+))?([eE](\d+))( (.+))?"#).unwrap());
+static RE_EPISODE_FILENAME: LazyLock<Regex> =
+ LazyLock::new(|| Regex::new(r#"([sS](?<season>\d+))?([eE](?<episode>\d+))( (.+))?"#).unwrap());
struct Apis {
trakt: Option<Trakt>,
@@ -296,11 +297,20 @@ fn import_media_file(
})
.unwrap_or_default();
- let filename = path
- .file_name()
- .ok_or(anyhow!("no file stem"))?
- .to_string_lossy()
- .to_string();
+ let filename = path.file_name().unwrap().to_string_lossy().to_string();
+
+ let mut episode_index = None;
+ if let Some(cap) = RE_EPISODE_FILENAME.captures(&filename) {
+ if let Some(episode) = cap.name("episode").map(|m| m.as_str()) {
+ let season = cap.name("season").map(|m| m.as_str());
+ let episode = episode.parse::<usize>().context("parse episode num")?;
+ let season = season
+ .unwrap_or("1")
+ .parse::<usize>()
+ .context("parse season num")?;
+ episode_index = Some((season, episode))
+ }
+ }
let mut filename_toks = filename.split(".");
let filepath_stem = filename_toks.next().unwrap();
@@ -309,17 +319,36 @@ fn import_media_file(
.infojson
.as_ref()
.map(|ij| format!("youtube-{}", ij.id))
- .unwrap_or(make_kebab(&filepath_stem));
+ .unwrap_or_else(|| {
+ if let Some((s, e)) = episode_index {
+ format!(
+ "{}-s{s}e{e}",
+ make_kebab(
+ &path
+ .parent()
+ .unwrap()
+ .file_name()
+ .unwrap_or_default()
+ .to_string_lossy()
+ )
+ )
+ } else {
+ make_kebab(&filepath_stem)
+ }
+ });
let node = NodeID::from_slug(&slug);
db.update_node_init(node, |node| {
node.slug = slug;
- node.title = info.title;
+ node.title = info.title.or(node.title.clone());
node.visibility = visibility;
node.poster = m.cover.clone();
- node.description = tags.remove("DESCRIPTION").or(tags.remove("SYNOPSIS"));
- node.tagline = tags.remove("COMMENT");
+ node.description = tags
+ .remove("DESCRIPTION")
+ .or(tags.remove("SYNOPSIS"))
+ .or(node.description.clone());
+ node.tagline = tags.remove("COMMENT").or(node.tagline.clone());
node.parents.insert(parent);
if let Some(ct) = tags.get("CONTENT_TYPE") {
@@ -440,6 +469,53 @@ fn import_media_file(
Ok(())
})?;
+ if let Some((season, episode)) = episode_index {
+ let mut trakt_id = None;
+ let flagspath = path.parent().unwrap().join("flags");
+ if flagspath.exists() {
+ for flag in read_to_string(flagspath)?.lines() {
+ if let Some(value) = flag.strip_prefix("trakt-").or(flag.strip_prefix("trakt=")) {
+ let (kind, id) = value.split_once(":").unwrap_or(("", value));
+ if kind == "show" {
+ trakt_id = Some(id.parse::<u64>()?);
+ }
+ }
+ }
+ }
+ if let Some(trakt_id) = trakt_id {
+ let trakt = apis.trakt.as_ref().ok_or(anyhow!("trakt required"))?;
+ let seasons = rthandle.block_on(trakt.show_seasons(trakt_id))?;
+ if seasons.iter().any(|x| x.number == season) {
+ let episodes = rthandle.block_on(trakt.show_season_episodes(trakt_id, season))?;
+ let mut poster = None;
+ if let Some(tmdb) = &apis.tmdb {
+ let trakt_details =
+ rthandle.block_on(trakt.lookup(TraktKind::Show, trakt_id))?;
+ if let Some(tmdb_id) = trakt_details.ids.tmdb {
+ let tmdb_details =
+ rthandle.block_on(tmdb.episode_details(tmdb_id, season, episode))?;
+ if let Some(still) = &tmdb_details.still_path {
+ poster = Some(
+ AssetInner::Cache(rthandle.block_on(tmdb.image(&still))?).ser(),
+ )
+ }
+ }
+ }
+ if let Some(episode) = episodes.get(episode.saturating_sub(1)) {
+ db.update_node_init(node, |node| {
+ node.kind = NodeKind::Episode;
+ node.index = Some(episode.number);
+ node.title = Some(episode.title.clone());
+ node.poster = poster.or(node.poster.clone());
+ node.description = episode.overview.clone().or(node.description.clone());
+ node.ratings.insert(Rating::Trakt, episode.rating);
+ Ok(())
+ })?
+ }
+ }
+ }
+ }
+
for tok in filename_toks {
apply_node_flag(db, rthandle, apis, node, tok)?;
}
@@ -510,10 +586,10 @@ fn apply_trakt_tmdb(
let trakt_id: u64 = trakt_id.parse().context("parse trakt id")?;
if let (Some(trakt), Some(tmdb)) = (&apis.trakt, &apis.tmdb) {
let data = rthandle
- .block_on(trakt.lookup(trakt_kind, trakt_id, true))
+ .block_on(trakt.lookup(trakt_kind, trakt_id))
.context("trakt lookup")?;
let people = rthandle
- .block_on(trakt.people(trakt_kind, trakt_id, true))
+ .block_on(trakt.people(trakt_kind, trakt_id))
.context("trakt people lookup")?;
let mut people_map = BTreeMap::<PeopleGroup, Vec<Appearance>>::new();
diff --git a/import/src/tmdb.rs b/import/src/tmdb.rs
index 678ce61..70e95ce 100644
--- a/import/src/tmdb.rs
+++ b/import/src/tmdb.rs
@@ -116,6 +116,26 @@ impl Tmdb {
})
.await
}
+
+ pub async fn episode_details(
+ &self,
+ series_id: u64,
+ season: usize,
+ episode: usize,
+ ) -> anyhow::Result<Arc<TmdbEpisode>> {
+ async_cache_memory(&["api-tmdb-episode-details", &format!("{series_id} {season} {episode}")], || async move {
+ info!("tmdb episode details {series_id} S={season} E={episode}");
+ Ok(self
+ .image_client
+ .get(&format!("https://api.themoviedb.org/3/tv/{series_id}/season/{season}/episode/{episode}?api_key={}", self.key))
+ .send()
+ .await?
+ .error_for_status()?
+ .json()
+ .await?)
+ })
+ .await
+ }
}
pub fn parse_release_date(d: &str) -> anyhow::Result<Option<i64>> {
@@ -143,6 +163,18 @@ pub fn parse_release_date(d: &str) -> anyhow::Result<Option<i64>> {
}
#[derive(Debug, Clone, Deserialize, Encode, Decode)]
+pub struct TmdbEpisode {
+ pub air_date: String,
+ pub overview: String,
+ pub name: String,
+ pub id: u64,
+ pub runtime: f64,
+ pub still_path: Option<String>,
+ pub vote_average: f64,
+ pub vote_count: usize,
+}
+
+#[derive(Debug, Clone, Deserialize, Encode, Decode)]
pub struct TmdbPersonImage {
pub id: u64,
pub profiles: Vec<TmdbPersonImageProfile>,
diff --git a/import/src/trakt.rs b/import/src/trakt.rs
index c7a25ad..1daee77 100644
--- a/import/src/trakt.rs
+++ b/import/src/trakt.rs
@@ -1,3 +1,4 @@
+use anyhow::Context;
/*
This file is part of jellything (https://codeberg.org/metamuffin/jellything)
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
@@ -46,76 +47,115 @@ impl Trakt {
&self,
kinds: &[TraktKind],
query: &str,
- extended: bool,
) -> anyhow::Result<Arc<Vec<TraktSearchResult>>> {
+ async_cache_memory(&["api-trakt-lookup", query], || async move {
+ let url = format!(
+ "https://api.trakt.tv/search/{}?query={}&extended=full",
+ kinds
+ .iter()
+ .map(|t| t.singular())
+ .collect::<Vec<_>>()
+ .join(","),
+ urlencoding::encode(query),
+ );
+ let res = self.client.get(url).send().await?.error_for_status()?;
+ Ok(res.json().await?)
+ })
+ .await
+ .context("trakt search")
+ }
+
+ pub async fn lookup(&self, kind: TraktKind, id: u64) -> anyhow::Result<Arc<TraktMediaObject>> {
async_cache_memory(
- &["api-trakt-lookup", query, if extended { "a" } else { "b" }],
+ &["api-trakt-lookup", &format!("{kind} {id}")],
|| async move {
- let url = format!(
- "https://api.trakt.tv/search/{}?query={}{}",
- kinds
- .iter()
- .map(|t| t.singular())
- .collect::<Vec<_>>()
- .join(","),
- urlencoding::encode(query),
- optext(extended)
- );
+ info!("trakt lookup {kind:?}:{id:?}");
+ let url = format!("https://api.trakt.tv/{}/{id}?extended=full", kind.plural());
let res = self.client.get(url).send().await?.error_for_status()?;
Ok(res.json().await?)
},
)
.await
+ .context("trakt lookup")
}
- pub async fn lookup(
- &self,
- kind: TraktKind,
- id: u64,
- extended: bool,
- ) -> anyhow::Result<Arc<TraktMediaObject>> {
+ pub async fn people(&self, kind: TraktKind, id: u64) -> anyhow::Result<Arc<TraktPeople>> {
async_cache_memory(
- &["api-trakt-lookup", &format!("{id} {extended}")],
+ &["api-trakt-people", &format!("{kind} {id}")],
|| async move {
- info!("trakt lookup {kind:?}:{id:?}");
+ info!("trakt people {kind:?}:{id:?}");
let url = format!(
- "https://api.trakt.tv/{}/{}{}",
- kind.plural(),
- id,
- optext2(extended)
+ "https://api.trakt.tv/{}/{id}/people?extended=full",
+ kind.plural()
);
let res = self.client.get(url).send().await?.error_for_status()?;
Ok(res.json().await?)
},
)
.await
+ .context("trakt people")
+ }
+
+ pub async fn show_seasons(&self, id: u64) -> anyhow::Result<Arc<Vec<TraktSeason>>> {
+ async_cache_memory(&["api-trakt-seasons", &id.to_string()], || async move {
+ info!("trakt seasons {id:?}");
+ let url = format!("https://api.trakt.tv/shows/{id}/seasons?extended=full");
+ let res = self.client.get(url).send().await?.error_for_status()?;
+ Ok(res.json().await?)
+ })
+ .await
+ .context("trakt show seasons")
}
- pub async fn people(
+ pub async fn show_season_episodes(
&self,
- kind: TraktKind,
id: u64,
- extended: bool,
- ) -> anyhow::Result<Arc<TraktPeople>> {
+ season: usize,
+ ) -> anyhow::Result<Arc<Vec<TraktEpisode>>> {
async_cache_memory(
- &["api-trakt-people", &format!("{id} {extended}")],
+ &["api-trakt-episodes", &id.to_string(), &season.to_string()],
|| async move {
- info!("trakt people {kind:?}:{id:?}");
- let url = format!(
- "https://api.trakt.tv/{}/{}/people{}",
- kind.plural(),
- id,
- optext2(extended)
- );
+ info!("trakt episodes {id:?} season={season}");
+ let url = format!("https://api.trakt.tv/shows/{id}/seasons/{season}?extended=full");
let res = self.client.get(url).send().await?.error_for_status()?;
Ok(res.json().await?)
},
)
.await
+ .context("trakt show season episodes")
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, Encode, Decode)]
+pub struct TraktSeason {
+ pub number: usize,
+ pub ids: ObjectIds,
+ pub rating: f64,
+ pub votes: usize,
+ pub episode_count: usize,
+ pub aired_count: Option<usize>,
+ pub title: String,
+ pub overview: Option<String>,
+ pub network: String,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize, Default, Encode, Decode)]
+pub struct TraktEpisode {
+ pub season: Option<usize>,
+ pub number: usize,
+ pub number_abs: Option<usize>,
+ pub ids: ObjectIds,
+ pub rating: f64,
+ pub votes: usize,
+ pub title: String,
+ pub runtime: f64,
+ pub overview: Option<String>,
+ pub available_translations: Vec<String>,
+ pub first_aired: Option<String>,
+ pub episode_type: String,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize, Default, Encode, Decode)]
pub struct TraktPeople {
#[serde(default)]
pub cast: Vec<TraktAppearance>,
@@ -138,21 +178,6 @@ pub struct TraktPerson {
pub ids: ObjectIds,
}
-fn optext(extended: bool) -> &'static str {
- if extended {
- "&extended=full"
- } else {
- ""
- }
-}
-fn optext2(extended: bool) -> &'static str {
- if extended {
- "?extended=full"
- } else {
- ""
- }
-}
-
#[derive(Debug, Serialize, Deserialize, Encode, Decode)]
pub struct TraktSearchResult {
pub r#type: TraktKind,