Skip to content

Commit

Permalink
add matcher impl for tv show scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
vgarleanu committed Sep 20, 2022
1 parent 3dc6b69 commit 3bdc563
Show file tree
Hide file tree
Showing 15 changed files with 526 additions and 162 deletions.
71 changes: 69 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion dim/Cargo.toml
Expand Up @@ -33,7 +33,7 @@ rand = { version = "0.7.3", features = ["small_rng"] }
chrono = { version = "0.4.19", features = ["serde"] }
rust-embed = "^5.9.0"
torrent-name-parser = "0.6.3"
reqwest = { version = "0.11.0", features = ["json", "rustls-tls"], default-features = false }
reqwest = { version = "0.11.0", features = ["json", "rustls-tls", "brotli"], default-features = false }
notify = "4.0.17"
cfg-if = "1.0.0"
once_cell = "1.8.0"
Expand Down Expand Up @@ -72,6 +72,8 @@ itertools = "0.10.3"
# FIXME: Remove when we get rid of xtra_proc
new_xtra = { package = "xtra", git = "https://github.com/Restioson/xtra", features = ["with-tokio-1"] }
url = "2.2.2"
retry-block = "1.0.0"
hyper = "0.14.20"

[build-dependencies]
fs_extra = "1.1.0"
Expand Down
4 changes: 2 additions & 2 deletions dim/src/errors.rs
Expand Up @@ -103,8 +103,8 @@ impl warp::reject::Reject for DimError {}

impl warp::Reply for DimError {
fn into_response(self) -> warp::reply::Response {
//| Self::ScannerError(_)
//| Self::TmdbIdSearchError(_) => StatusCode::NOT_FOUND,
//| Self::ScannerError(_)
//| Self::TmdbIdSearchError(_) => StatusCode::NOT_FOUND,
let status = match self {
Self::LibraryNotFound
| Self::NoneError
Expand Down
34 changes: 33 additions & 1 deletion dim/src/external/filename.rs
Expand Up @@ -38,12 +38,44 @@ impl FilenameMetadata for Anitomy {
year: metadata
.get(ElementCategory::AnimeYear)
.and_then(|x| x.parse().ok()),
// If season isnt specified we assume season 1 here.
season: metadata
.get(ElementCategory::AnimeSeason)
.and_then(|x| x.parse().ok()),
.and_then(|x| x.parse().ok())
.or(Some(1)),
episode: metadata
.get(ElementCategory::EpisodeNumber)
.and_then(|x| x.parse().ok()),
})
}
}

/// A special filename metadata extractor that combines torrent_name_parser and anitomy which in
/// some cases is necessary. TNP is really good at extracting show titles but not season and
/// episode numbers. Anitomy excels at this. Here we combine the title extracted by TPN and the
/// season and episode number extracted by Anitomy.
pub struct CombinedExtractor;

impl FilenameMetadata for CombinedExtractor {
fn from_str(s: &str) -> Option<Metadata> {
let metadata_tnp = TorrentMetadata::from(s).ok()?;
let metadata_anitomy = match Anitomy::new().parse(s) {
Ok(v) | Err(v) => v,
};

Some(Metadata {
name: metadata_tnp.title().to_owned(),
year: metadata_tnp.year().map(|x| x as i64),
// If season isnt specified we assume season 1 here as some releases only have a
// episode number and no season number.
season: metadata_anitomy
.get(ElementCategory::AnimeSeason)
.and_then(|x| x.parse().ok())
.or(Some(1)),
episode: metadata_anitomy
.get(ElementCategory::EpisodeNumber)
.and_then(|x| x.parse().ok()),
})

}
}
3 changes: 3 additions & 0 deletions dim/src/external/mock.rs
@@ -1,6 +1,7 @@
use super::ExternalActor;
use super::ExternalMedia;
use super::ExternalQuery;
use super::IntoQueryShow;
use super::Result;

#[derive(Debug, Clone, Copy)]
Expand All @@ -23,3 +24,5 @@ impl ExternalQuery for MockProvider {
unimplemented!()
}
}

impl IntoQueryShow for MockProvider {}
9 changes: 8 additions & 1 deletion dim/src/external/mod.rs
Expand Up @@ -129,7 +129,7 @@ impl std::fmt::Display for MediaSearchType {
/// Trait that must be implemented by external metadata agents which allows the scanners to query
/// for data.
#[async_trait]
pub trait ExternalQuery: Debug + Send + Sync {
pub trait ExternalQuery: IntoQueryShow + Debug + Send + Sync {
/// Search by title and year. This must return a Vec of `ExternalMedia` sorted by the search
/// score.
async fn search(&self, title: &str, year: Option<i32>) -> Result<Vec<ExternalMedia>>;
Expand All @@ -138,11 +138,18 @@ pub trait ExternalQuery: Debug + Send + Sync {
async fn search_by_id(&self, external_id: &str) -> Result<ExternalMedia>;
/// Get all actors for a media by external id. Actors must be ordered in order of importance.
async fn cast(&self, external_id: &str) -> Result<Vec<ExternalActor>>;
}

pub trait IntoQueryShow {
/// Upcast `self` into `ExternalQueryShow`. It is important that providers that can query for
/// tv shows, implements this to return `Some(self)`.
fn as_query_show<'a>(&'a self) -> Option<&'a dyn ExternalQueryShow> {
None
}

fn into_query_show(self: Arc<Self>) -> Option<Arc<dyn ExternalQueryShow>> {
None
}
}

/// Trait must be implemented by all external metadata agents which support querying for tv shows.
Expand Down
76 changes: 50 additions & 26 deletions dim/src/external/tmdb/metadata_provider.rs
Expand Up @@ -10,6 +10,7 @@ use std::time::Instant;
use async_trait::async_trait;

use tokio::sync::broadcast;
use tracing::instrument;

use crate::external::{Result as QueryResult, *};
use core::result::Result;
Expand All @@ -28,7 +29,7 @@ use super::*;
/// How long items should be cached for. Defaults to 12 hours.
const CACHED_ITEM_TTL: Duration = Duration::from_secs(60 * 60 * 12);
/// How many requests we can send per second.
const REQ_QUOTA: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(200) };
const REQ_QUOTA: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(128) };

type Governor = RateLimiter<NotKeyed, InMemoryState, DefaultClock, NoOpMiddleware>;

Expand Down Expand Up @@ -62,6 +63,10 @@ impl TMDBMetadataProvider {
pub fn new(api_key: &str) -> Self {
let http_client = reqwest::ClientBuilder::new()
.user_agent(APP_USER_AGENT)
.brotli(true)
.tcp_keepalive(Some(Duration::from_millis(16_000)))
.tcp_nodelay(true)
.http1_only()
.build()
.expect("building this client should never fail.");

Expand Down Expand Up @@ -273,14 +278,22 @@ impl TMDBMetadataProvider {
..
}) = media
{
let genre_vec = genres.insert(vec![]);

for genre_id in ids.iter().cloned() {
if let Some(genre) = genre_id_cache.get(&genre_id) {
genres.push(genre.name.clone());
genre_vec.push(Genre {
id: genre_id,
name: genre.name.clone(),
});
} else if let Some(genre) =
genre_list.genres.iter().find(|x| x.id == genre_id)
{
genre_id_cache.insert(genre_id, genre.clone());
genres.push(genre.name.clone());
genre_vec.push(Genre {
id: genre_id,
name: genre.name.clone(),
});
}
}
}
Expand Down Expand Up @@ -321,31 +334,14 @@ impl TMDBMetadataProvider {
)
.await?;

match media_type {
MediaSearchType::Movie => {
let movie_details =
serde_json::from_str::<MovieDetails>(&response_body).map_err(|err| {
crate::external::Error::DeserializationError {
body: response_body,
error: format!("{err}"),
}
})?;

Ok(movie_details.into())
let details = serde_json::from_str::<TMDBMediaObject>(&response_body).map_err(|err| {
crate::external::Error::DeserializationError {
body: response_body,
error: format!("{err}"),
}
})?;

MediaSearchType::Tv => {
let tv_details =
serde_json::from_str::<TvDetails>(&response_body).map_err(|err| {
crate::external::Error::DeserializationError {
body: response_body,
error: format!("{err}"),
}
})?;

Ok(tv_details.into())
}
}
Ok(details.into())
}

async fn cast(
Expand Down Expand Up @@ -497,28 +493,56 @@ impl<K> ExternalQuery for MetadataProviderOf<K>
where
K: sealed::AssocMediaTypeConst + Send + Sync + 'static,
{
#[instrument]
async fn search(&self, title: &str, year: Option<i32>) -> QueryResult<Vec<ExternalMedia>> {
self.provider.search(title, year, K::MEDIA_TYPE).await
}

#[instrument]
async fn search_by_id(&self, external_id: &str) -> QueryResult<ExternalMedia> {
self.provider.search_by_id(external_id, K::MEDIA_TYPE).await
}

#[instrument]
async fn cast(&self, external_id: &str) -> QueryResult<Vec<ExternalActor>> {
self.provider.cast(external_id, K::MEDIA_TYPE).await
}
}

impl<K> IntoQueryShow for MetadataProviderOf<K>
where
K: sealed::AssocMediaTypeConst + Send + Sync + 'static,
{
default fn as_query_show<'a>(&'a self) -> Option<&'a dyn ExternalQueryShow> {
None
}

default fn into_query_show(self: Arc<Self>) -> Option<Arc<dyn ExternalQueryShow>> {
None
}
}

impl IntoQueryShow for MetadataProviderOf<TvShows> {
fn as_query_show(&self) -> Option<&dyn ExternalQueryShow> {
Some(self)
}

fn into_query_show(self: Arc<Self>) -> Option<Arc<dyn ExternalQueryShow>> {
Some(self)
}
}

#[async_trait]
impl ExternalQueryShow for MetadataProviderOf<TvShows> {
#[instrument]
async fn seasons_for_id(&self, external_id: &str) -> QueryResult<Vec<ExternalSeason>> {
let mut seasons = self.provider.seasons_by_id(external_id).await?;
seasons.sort_by(|a, b| a.season_number.cmp(&b.season_number));

Ok(seasons)
}

#[instrument]
async fn episodes_for_season(
&self,
external_id: &str,
Expand Down

0 comments on commit 3bdc563

Please sign in to comment.