f98e6f8dfa
Adds the full writer/publisher stack: NIP-35 event signing and relay delivery, qBittorrent polling with publish-delay queue, lava_torrent .torrent file parsing, TMDB inline lookup before publish, and kindexr-cli identity/publish subcommands.
106 lines
3.4 KiB
Rust
106 lines
3.4 KiB
Rust
use anyhow::Result;
|
|
use reqwest::Client;
|
|
use serde::Deserialize;
|
|
use sqlx::SqlitePool;
|
|
use tracing::{debug, warn};
|
|
use url::form_urlencoded;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Enricher {
|
|
api_key: String,
|
|
client: Client,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct SearchResponse {
|
|
results: Vec<TmdbHit>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct TmdbHit {
|
|
id: i64,
|
|
}
|
|
|
|
impl Enricher {
|
|
pub fn new(api_key: String) -> Self {
|
|
Enricher { api_key, client: Client::new() }
|
|
}
|
|
|
|
pub fn enabled(&self) -> bool {
|
|
!self.api_key.is_empty()
|
|
}
|
|
|
|
pub async fn enrich(&self, pool: &SqlitePool, event_id: &str, title: &str, year: Option<i32>, newznab_cat: Option<i32>) {
|
|
if !self.enabled() { return; }
|
|
|
|
let (kind, prefix) = match newznab_cat {
|
|
Some(c) if (2000..3000).contains(&c) => ("movie", "movie"),
|
|
Some(c) if (5000..6000).contains(&c) => ("tv", "tv"),
|
|
_ => return,
|
|
};
|
|
|
|
let result = match kind {
|
|
"movie" => self.search("search/movie", title, year, "year").await,
|
|
_ => self.search("search/tv", title, year, "first_air_date_year").await,
|
|
};
|
|
|
|
let tmdb_id = match result {
|
|
Ok(Some(id)) => id,
|
|
Ok(None) => return,
|
|
Err(e) => {
|
|
warn!(event_id, "tmdb lookup error: {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let stored = format!("{prefix}:{tmdb_id}");
|
|
match sqlx::query(
|
|
"UPDATE torrents SET tmdb_id = ? WHERE event_id = ? AND (tmdb_id IS NULL OR tmdb_id = '')",
|
|
)
|
|
.bind(&stored)
|
|
.bind(event_id)
|
|
.execute(pool)
|
|
.await
|
|
{
|
|
Ok(_) => debug!(event_id, tmdb_id = %stored, "tmdb enriched"),
|
|
Err(e) => warn!(event_id, "tmdb db update failed: {e}"),
|
|
}
|
|
}
|
|
|
|
/// Look up a TMDB ID and return it as "movie:ID" or "tv:ID" without writing to DB.
|
|
/// Returns None if the title doesn't match or TMDB is disabled.
|
|
pub async fn lookup(&self, title: &str, year: Option<i32>, newznab_cat: Option<i32>) -> Option<String> {
|
|
if !self.enabled() { return None; }
|
|
let (kind, prefix) = match newznab_cat {
|
|
Some(c) if (2000..3000).contains(&c) => ("movie", "movie"),
|
|
Some(c) if (5000..6000).contains(&c) => ("tv", "tv"),
|
|
_ => return None,
|
|
};
|
|
let result = match kind {
|
|
"movie" => self.search("search/movie", title, year, "year").await,
|
|
_ => self.search("search/tv", title, year, "first_air_date_year").await,
|
|
};
|
|
match result {
|
|
Ok(Some(id)) => Some(format!("{prefix}:{id}")),
|
|
Ok(None) => None,
|
|
Err(e) => {
|
|
tracing::warn!("tmdb lookup error for '{title}': {e}");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn search(&self, path: &str, title: &str, year: Option<i32>, year_param: &str) -> Result<Option<i64>> {
|
|
let encoded: String = form_urlencoded::byte_serialize(title.as_bytes()).collect();
|
|
let mut url = format!(
|
|
"https://api.themoviedb.org/3/{path}?api_key={key}&query={encoded}",
|
|
key = self.api_key,
|
|
);
|
|
if let Some(y) = year {
|
|
url.push_str(&format!("&{year_param}={y}"));
|
|
}
|
|
let res: SearchResponse = self.client.get(&url).send().await?.json().await?;
|
|
Ok(res.results.first().map(|h| h.id))
|
|
}
|
|
}
|