mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-11 09:27:03 +01:00
fix(core,meta,app,android,cli): number episodes the way releases do
AniList's per-season lookup searched for "<title> 2nd Season" and the scorer picked the base entry again, so Attack on Titan seasons 2-4 and Jigokuraku season 2 were captioned with season one's names. Episode names came from streamingEpisodes, which is partial and numbered however a streaming site counted. Bleach S17 and Mushishi S02/S03 had none. AniList now walks SEQUEL relations from the match and takes each entry's episodes from ani.zip, which carries TVDB's season, number and absolute number. An episode row keeps all three plus its place in the AniList entry. EpisodeGuide::get matches whichever numbering a file uses and never answers from another season. Episodes and fanart are stored as one enrichment. The episode list is replaced even when empty, and the enrichment is recorded as asked, so a film is not re-asked every run. Storing a different match drops the old entry's episode rows. AniList's 4.75:1 bannerImage is no longer stored as the 16:9 backdrop; ani.zip's 1920x1080 fanart is used instead. Schema v9 adds the columns and deletes the AniList episode rows and banners. Matches are kept. Desktop, Android and the CLI now share service::plan and MetadataService::work. The settings text names ani.zip. Audit and roadmap in docs/METADATA.md; bug entry B61.
This commit is contained in:
1 parent
52d27ec996
commit
4c80076762
17 files changed
+1680
-618
No files matched your search
+71
-31
@@ -483,18 +483,18 @@ async fn metadata(dirs: &AppDirs, command: MetadataCommand) -> Result<()> {
|
||||
)
|
||||
.context("build the metadata provider")?;
|
||||
|
||||
let catalog = Catalog::open(&dirs.catalog_db()).context("open catalog")?;
|
||||
let mut catalog = Catalog::open(&dirs.catalog_db()).context("open catalog")?;
|
||||
let library = Library::build(catalog.all_files()?, &catalog.all_watch_states()?);
|
||||
let stored = catalog.all_metadata()?;
|
||||
let enriched = catalog.enrichment_ages()?;
|
||||
|
||||
let mut pending: Vec<_> = library
|
||||
.titles
|
||||
.iter()
|
||||
.filter(|title| {
|
||||
force
|
||||
|| !pstr_meta::service::is_usable(stored.get(&title.key), settings.provider)
|
||||
})
|
||||
.collect();
|
||||
let mut pending = pstr_meta::service::plan(
|
||||
library.titles,
|
||||
&stored,
|
||||
&enriched,
|
||||
settings.provider,
|
||||
force,
|
||||
);
|
||||
if let Some(limit) = limit {
|
||||
pending.truncate(limit);
|
||||
}
|
||||
@@ -504,7 +504,7 @@ async fn metadata(dirs: &AppDirs, command: MetadataCommand) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
println!(
|
||||
"matching {} titles against {}…",
|
||||
"looking up {} titles against {}…",
|
||||
pending.len(),
|
||||
settings.provider.label()
|
||||
);
|
||||
@@ -513,32 +513,65 @@ async fn metadata(dirs: &AppDirs, command: MetadataCommand) -> Result<()> {
|
||||
// and the GUI already has the parallel path. What this command is
|
||||
// for is reading the output.
|
||||
let (mut matched, mut failed) = (0usize, 0usize);
|
||||
for title in pending {
|
||||
match service.record(title).await {
|
||||
Ok(record) => {
|
||||
match &record.metadata {
|
||||
Some(found) => {
|
||||
matched += 1;
|
||||
println!(
|
||||
" {:<40} → {} ({})",
|
||||
title.name,
|
||||
found.name,
|
||||
found
|
||||
.year
|
||||
.map(|year| year.to_string())
|
||||
.unwrap_or_else(|| "-".into())
|
||||
);
|
||||
}
|
||||
None => println!(" {:<40} → no match", title.name),
|
||||
}
|
||||
catalog.set_metadata(&record)?;
|
||||
}
|
||||
for work in pending {
|
||||
let name = &work.title().name;
|
||||
let outcome = match service.work(&work).await {
|
||||
Ok(outcome) => outcome,
|
||||
Err(error) => {
|
||||
failed += 1;
|
||||
// Not stored: a failure to ask is not an answer, and the
|
||||
// title has to stay askable. See `pstr_meta::service`.
|
||||
println!(" {:<40} ! {error}", title.name);
|
||||
println!(" {name:<40} ! {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(record) = &outcome.record {
|
||||
match &record.metadata {
|
||||
Some(found) => {
|
||||
matched += 1;
|
||||
println!(
|
||||
" {name:<40} → {} ({})",
|
||||
found.name,
|
||||
found
|
||||
.year
|
||||
.map(|year| year.to_string())
|
||||
.unwrap_or_else(|| "-".into())
|
||||
);
|
||||
}
|
||||
None => println!(" {name:<40} → no match"),
|
||||
}
|
||||
catalog.set_metadata(record)?;
|
||||
}
|
||||
match &outcome.enrichment {
|
||||
Some(enrichment) => {
|
||||
println!(
|
||||
" {:<40} {} episodes{}",
|
||||
"",
|
||||
enrichment.episodes.len(),
|
||||
if enrichment.backdrop_url.is_some() {
|
||||
", fanart"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
catalog.set_enrichment(
|
||||
&outcome.title_key,
|
||||
settings.provider,
|
||||
unix_now(),
|
||||
enrichment,
|
||||
)?;
|
||||
}
|
||||
// Matched, but the episodes could not be asked for; the
|
||||
// next run asks again.
|
||||
None if outcome
|
||||
.record
|
||||
.as_ref()
|
||||
.is_some_and(|r| r.metadata.is_some()) =>
|
||||
{
|
||||
println!(" {:<40} ! episodes not fetched", "");
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
println!("{matched} matched, {failed} could not be looked up");
|
||||
@@ -926,3 +959,10 @@ async fn play(
|
||||
drop(opened.library);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unix seconds, for the "when was this asked" columns.
|
||||
fn unix_now() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |since| since.as_secs() as i64)
|
||||
}
|
||||
Reference in new issue
Block a user