Add movie support to Fetch Data: local title/year guessing plus TMDB movie lookup
- renamr-core::matching: guess_movie() extracts a movie title (and release year, if
present) from a filename using the same year/quality-token boundary heuristics real
release names follow, as a fallback when guess_episode() finds no season/episode
marker.
- renamr-core::web::tmdb: search_movie()/lookup_movie() mirror the TV client, reusing
the same Sorensen-Dice title-disambiguation as lookup_episode(), narrowed further by
TMDB's own primary_release_year filter when a year was locally guessed. Also exposed
search_tv_best_match() and fixed a latent gap where the real Fetch Data command path
(metadata.rs::fetch_season) bypassed the fuzzy show-name disambiguation entirely,
doing its own raw first-result selection instead of reusing it.
- src-tauri: new guess_movie command and fetch_movie_metadata command/types
(MovieFetchQuery/Outcome/Result), registered alongside the existing TV commands.
- Frontend: Item now tracks mediaKind ("tv"/"movie"/null); Match tries guess_episode
then falls back to guess_movie; Fetch Data resolves TV and movie targets via their
own commands concurrently and merges results. A single batch can mix TV and movie
files - each only fills in the fields that apply to it. Movie fields ({movie name},
{year}, reusing {airdate} for release date, matching FileBot's own unified binding)
are documented in the naming-scheme field reference.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
93b5ac5e56
commit
45a9f5d523
+12
-1
@@ -4,7 +4,7 @@ mod metadata;
|
||||
mod settings;
|
||||
|
||||
use renamr_core::format::ExpressionFormat;
|
||||
use renamr_core::matching::{self, EpisodeGuess};
|
||||
use renamr_core::matching::{self, EpisodeGuess, MovieGuess};
|
||||
use renamr_core::rename::{self, RenamePlanItem, RenameResult};
|
||||
use renamr_core::string_bindings;
|
||||
use std::collections::HashMap;
|
||||
@@ -30,6 +30,15 @@ fn guess_episode(filename: String) -> Option<EpisodeGuess> {
|
||||
matching::guess_episode(&filename)
|
||||
}
|
||||
|
||||
/// Guesses a movie title (and release year, if present) from a filename using local
|
||||
/// regex/heuristic patterns only - no network calls. Callers should try
|
||||
/// `guess_episode` first and only fall back to this when it returns `None`; see
|
||||
/// `matching::guess_movie` for why.
|
||||
#[tauri::command]
|
||||
fn guess_movie(filename: String) -> Option<MovieGuess> {
|
||||
matching::guess_movie(&filename)
|
||||
}
|
||||
|
||||
/// Executes a batch of planned on-disk renames (optionally moving files into
|
||||
/// subfolders - see `renamr_core::rename`; conflicting targets are always skipped,
|
||||
/// never overwritten). The UI is expected to have already shown the user the full
|
||||
@@ -54,8 +63,10 @@ pub fn run() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
render_expression,
|
||||
guess_episode,
|
||||
guess_movie,
|
||||
execute_rename,
|
||||
metadata::fetch_metadata,
|
||||
metadata::fetch_movie_metadata,
|
||||
settings::get_tmdb_token,
|
||||
settings::save_tmdb_token,
|
||||
history::list_rename_history,
|
||||
|
||||
@@ -104,10 +104,10 @@ async fn run_fetch(token: String, items: Vec<FetchQuery>) -> Vec<FetchResult> {
|
||||
}
|
||||
|
||||
async fn fetch_season(client: &TmdbClient, series_query: &str, season: i32) -> SeasonLookup {
|
||||
let search_results = client.search_tv(series_query).await.map_err(|e| e.to_string())?;
|
||||
let best = search_results
|
||||
.into_iter()
|
||||
.next()
|
||||
let best = client
|
||||
.search_tv_best_match(series_query)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("no TMDB match for \"{series_query}\""))?;
|
||||
let episodes = client
|
||||
.season_episodes(best.id, season)
|
||||
@@ -115,3 +115,79 @@ async fn fetch_season(client: &TmdbClient, series_query: &str, season: i32) -> S
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok((best.name, episodes))
|
||||
}
|
||||
|
||||
/// One file's local movie-title guess, sent up from the UI to be resolved against
|
||||
/// TMDB. Mirrors `FetchQuery`, but for the movie side of "Fetch Data".
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MovieFetchQuery {
|
||||
/// Echoed back in the result so the frontend can map it back to the right item.
|
||||
pub id: String,
|
||||
pub title_query: String,
|
||||
/// A release year, if the local filename guess happened to carry one - used to
|
||||
/// narrow the TMDB search server-side before fuzzy title matching even runs.
|
||||
pub year: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
pub enum MovieFetchOutcome {
|
||||
Found {
|
||||
title: String,
|
||||
release_date: Option<String>,
|
||||
},
|
||||
/// Nothing on TMDB was a plausible match for the title (and year, if given).
|
||||
NotFound,
|
||||
Failed {
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MovieFetchResult {
|
||||
pub id: String,
|
||||
pub outcome: MovieFetchOutcome,
|
||||
}
|
||||
|
||||
/// Resolves a batch of local movie-title guesses against TMDB. Mirrors
|
||||
/// `fetch_metadata`, but each query is independent (no per-season caching the way TV
|
||||
/// episodes benefit from) since a movie search result already carries everything
|
||||
/// needed in one request.
|
||||
#[tauri::command]
|
||||
pub async fn fetch_movie_metadata(app: AppHandle, items: Vec<MovieFetchQuery>) -> Vec<MovieFetchResult> {
|
||||
let Some(token) = settings::tmdb_token(&app) else {
|
||||
return items
|
||||
.into_iter()
|
||||
.map(|item| MovieFetchResult {
|
||||
id: item.id,
|
||||
outcome: MovieFetchOutcome::Failed {
|
||||
error: "No TMDB API token configured — open Settings to add one.".to_string(),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
};
|
||||
|
||||
tauri::async_runtime::spawn(run_movie_fetch(token, items))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn run_movie_fetch(token: String, items: Vec<MovieFetchQuery>) -> Vec<MovieFetchResult> {
|
||||
let client = TmdbClient::new(token);
|
||||
let mut results = Vec::with_capacity(items.len());
|
||||
|
||||
for item in items {
|
||||
let outcome = match client.lookup_movie(&item.title_query, item.year).await {
|
||||
Ok(info) => MovieFetchOutcome::Found {
|
||||
title: info.title,
|
||||
release_date: info.release_date,
|
||||
},
|
||||
Err(renamr_core::web::tmdb::TmdbError::NotFound(_)) => MovieFetchOutcome::NotFound,
|
||||
Err(error) => MovieFetchOutcome::Failed { error: error.to_string() },
|
||||
};
|
||||
|
||||
results.push(MovieFetchResult { id: item.id, outcome });
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user