mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
fix(core,meta): a folder of films is one title per film
The four Rebuild of Evangelion films in one folder were one series: `- 1.11` read as episode 1, and every file took the folder's name, so the whole folder matched a single film. A dash followed by a decimal is now part of the film's name, and a folder whose files are all unnumbered, outside any grouping folder and each named differently gives each file its own title. AniList needs every query word in a title, and a film's version number is not in it, so a search that finds nothing is retried without it. All four films now match their own entries.
This commit is contained in:
1 parent
65a669e231
commit
238ecd8220
5 files changed
+341
-9
No files matched your search
@@ -1549,7 +1549,9 @@ pub fn from_node(share_id: &str, node: &proton_drive_rs::Node) -> CatalogNode {
|
|||||||
/// - grouping folders (`Movies`, `OVA`, `Specials`) are stepped over, so their
|
/// - grouping folders (`Movies`, `OVA`, `Specials`) are stepped over, so their
|
||||||
/// contents group under the series above them;
|
/// contents group under the series above them;
|
||||||
/// - whatever the filename claimed as a title, when it differs, is kept as the
|
/// - whatever the filename claimed as a title, when it differs, is kept as the
|
||||||
/// episode's own name — with the redundant series prefix stripped.
|
/// episode's own name — with the redundant series prefix stripped;
|
||||||
|
/// - **a folder of films is one title per film**, not one title: see
|
||||||
|
/// [`split_films`].
|
||||||
///
|
///
|
||||||
/// Folders and non-video files are dropped: a share holds `.nfo` and `.jpg`
|
/// Folders and non-video files are dropped: a share holds `.nfo` and `.jpg`
|
||||||
/// alongside the video, and a catalog that offers those as episodes is worse
|
/// alongside the video, and a catalog that offers those as episodes is worse
|
||||||
@@ -1562,12 +1564,18 @@ pub fn build_rows(share_id: &str, nodes: &[proton_drive_rs::Node]) -> Vec<Catalo
|
|||||||
.map(|node| (node.uid.link_id.as_str(), node))
|
.map(|node| (node.uid.link_id.as_str(), node))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
nodes
|
let mut origins = Vec::new();
|
||||||
|
let mut rows: Vec<CatalogNode> = nodes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|node| !node.is_folder() && naming::is_video_file(&node.name))
|
.filter(|node| !node.is_folder() && naming::is_video_file(&node.name))
|
||||||
.map(|node| {
|
.map(|node| {
|
||||||
let mut row = from_node(share_id, node);
|
let mut row = from_node(share_id, node);
|
||||||
let context = ancestry(node, &by_link);
|
let context = ancestry(node, &by_link);
|
||||||
|
origins.push(Origin {
|
||||||
|
own: row.parsed.clone(),
|
||||||
|
folder: context.title_folder.clone(),
|
||||||
|
through_container: context.through_container,
|
||||||
|
});
|
||||||
|
|
||||||
if let Some(title) = context.title {
|
if let Some(title) = context.title {
|
||||||
// The filename's own title, if it had one and it differs, is the
|
// The filename's own title, if it had one and it differs, is the
|
||||||
@@ -1601,7 +1609,75 @@ pub fn build_rows(share_id: &str, nodes: &[proton_drive_rs::Node]) -> Vec<Catalo
|
|||||||
}
|
}
|
||||||
row
|
row
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
|
split_films(&mut rows, &origins);
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a row's title came from, kept for [`split_films`].
|
||||||
|
struct Origin {
|
||||||
|
/// What the filename alone parsed as, before the folder overrode it.
|
||||||
|
own: ParsedName,
|
||||||
|
/// The link id of the folder that named the row.
|
||||||
|
folder: Option<String>,
|
||||||
|
through_container: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Give each film in a folder of films its own title.
|
||||||
|
///
|
||||||
|
/// `Rebuild of Evangelion/` holding `Evangelion - 1.11 You Are (Not)
|
||||||
|
/// Alone.mkv` and three more is four films, each its own entry at every
|
||||||
|
/// provider. Titled by the folder they are one series of four unnumbered
|
||||||
|
/// files, matched as whichever single film the folder's name reads most like —
|
||||||
|
/// and no single film is right.
|
||||||
|
///
|
||||||
|
/// Only when every file in the folder says so, because the same shape with one
|
||||||
|
/// file out of place is a series:
|
||||||
|
///
|
||||||
|
/// - none is numbered, by its name or by a season folder;
|
||||||
|
/// - none is under a grouping folder, because `Neon Genesis Evangelion/Movies/`
|
||||||
|
/// is the series' films, and they belong on its page;
|
||||||
|
/// - each names itself, differently from the others, and not by the folder's
|
||||||
|
/// name — `Mushishi - The Green Seat.mkv` under `Mushishi/` is an episode
|
||||||
|
/// of that show whose number is missing, not a film.
|
||||||
|
fn split_films(rows: &mut [CatalogNode], origins: &[Origin]) {
|
||||||
|
use crate::library::title_key;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
let mut by_folder: HashMap<&str, Vec<usize>> = HashMap::new();
|
||||||
|
for (index, origin) in origins.iter().enumerate() {
|
||||||
|
if let Some(folder) = &origin.folder {
|
||||||
|
by_folder.entry(folder.as_str()).or_default().push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for members in by_folder.into_values() {
|
||||||
|
if members.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut keys = HashSet::new();
|
||||||
|
let films = members.iter().all(|&index| {
|
||||||
|
let (row, origin) = (&rows[index], &origins[index]);
|
||||||
|
let own = &origin.own.title;
|
||||||
|
!origin.through_container
|
||||||
|
&& !origin.own.is_episode()
|
||||||
|
&& origin.own.season.is_none()
|
||||||
|
&& row.parsed.season.is_none()
|
||||||
|
&& !own.is_empty()
|
||||||
|
&& strip_prefix_title(own, &row.parsed.title) == *own
|
||||||
|
&& !own.eq_ignore_ascii_case(&row.parsed.title)
|
||||||
|
&& keys.insert(title_key(own))
|
||||||
|
});
|
||||||
|
if !films {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for index in members {
|
||||||
|
let row = &mut rows[index];
|
||||||
|
let own = &origins[index].own;
|
||||||
|
row.parsed.title = own.title.clone();
|
||||||
|
row.parsed.episode_title = own.episode_title.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pair each subtitle file in a crawl with the video it is for.
|
/// Pair each subtitle file in a crawl with the video it is for.
|
||||||
@@ -1784,6 +1860,10 @@ fn describe_subtitle(rest: &str) -> (Option<String>, Option<String>) {
|
|||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct Ancestry {
|
struct Ancestry {
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
|
/// The link id of the folder [`Ancestry::title`] came from.
|
||||||
|
title_folder: Option<String>,
|
||||||
|
/// Whether a grouping folder sat between the file and that one.
|
||||||
|
through_container: bool,
|
||||||
season: Option<u32>,
|
season: Option<u32>,
|
||||||
year: Option<u32>,
|
year: Option<u32>,
|
||||||
}
|
}
|
||||||
@@ -1822,10 +1902,13 @@ fn ancestry(
|
|||||||
|
|
||||||
match crate::naming::classify_folder(&ancestor.name) {
|
match crate::naming::classify_folder(&ancestor.name) {
|
||||||
FolderRole::Season(season) => found.season = found.season.or(Some(season)),
|
FolderRole::Season(season) => found.season = found.season.or(Some(season)),
|
||||||
FolderRole::Container => {}
|
FolderRole::Container => {
|
||||||
|
found.through_container |= found.title.is_none();
|
||||||
|
}
|
||||||
FolderRole::Title(parsed) => {
|
FolderRole::Title(parsed) => {
|
||||||
if found.title.is_none() && !parsed.title.is_empty() {
|
if found.title.is_none() && !parsed.title.is_empty() {
|
||||||
found.title = Some(parsed.title.clone());
|
found.title = Some(parsed.title.clone());
|
||||||
|
found.title_folder = Some(link_id.clone());
|
||||||
found.year = found.year.or(parsed.year);
|
found.year = found.year.or(parsed.year);
|
||||||
// `Game of Thrones - Season 1` names both; the season it
|
// `Game of Thrones - Season 1` names both; the season it
|
||||||
// states belongs to the files under it.
|
// states belongs to the files under it.
|
||||||
@@ -3081,6 +3164,126 @@ mod tests {
|
|||||||
assert_eq!(row(&film, "film").parsed.title, "Millennium Actress");
|
assert_eq!(row(&film, "film").parsed.title, "Millennium Actress");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **What the Rebuild of Evangelion films were matched as.** A folder of
|
||||||
|
/// four films titled by the folder is one series, matched as a single film
|
||||||
|
/// that none of the four is. Each names itself, so each is its own title.
|
||||||
|
#[test]
|
||||||
|
fn a_folder_of_unnumbered_films_is_one_title_per_film() {
|
||||||
|
let nodes = vec![
|
||||||
|
root(),
|
||||||
|
folder("rebuild", Some("root"), "Rebuild of Evangelion"),
|
||||||
|
video(
|
||||||
|
"one",
|
||||||
|
"rebuild",
|
||||||
|
"Evangelion - 1.11 You Are (Not) Alone.mkv",
|
||||||
|
),
|
||||||
|
video(
|
||||||
|
"two",
|
||||||
|
"rebuild",
|
||||||
|
"Evangelion - 2.22 You Can (Not) Advance..mkv",
|
||||||
|
),
|
||||||
|
video(
|
||||||
|
"three",
|
||||||
|
"rebuild",
|
||||||
|
"Evangelion - 3.33 You Can (Not) Redo..mkv",
|
||||||
|
),
|
||||||
|
video(
|
||||||
|
"four",
|
||||||
|
"rebuild",
|
||||||
|
"Evangelion - 3.0+1.01 Thrice Upon a Time.mkv",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let rows = build_rows("share", &nodes);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
row(&rows, "one").parsed.title,
|
||||||
|
"Evangelion - 1.11 You Are Alone"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
row(&rows, "four").parsed.title,
|
||||||
|
"Evangelion - 3.0+1.01 Thrice Upon a Time"
|
||||||
|
);
|
||||||
|
for film in &rows {
|
||||||
|
assert_eq!(film.parsed.episode, None);
|
||||||
|
assert_eq!(film.parsed.episode_title, None, "a film is not an episode");
|
||||||
|
}
|
||||||
|
|
||||||
|
let library = crate::library::Library::build(rows, &HashMap::new());
|
||||||
|
assert_eq!(library.titles.len(), 4);
|
||||||
|
assert!(
|
||||||
|
library
|
||||||
|
.titles
|
||||||
|
.iter()
|
||||||
|
.all(|title| title.kind == TitleKind::Film)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One numbered file says the folder is a show, and the unnumbered ones are
|
||||||
|
/// its specials.
|
||||||
|
#[test]
|
||||||
|
fn a_folder_with_a_numbered_episode_in_it_is_not_split() {
|
||||||
|
let nodes = vec![
|
||||||
|
root(),
|
||||||
|
folder("show", Some("root"), "Promised Neverland"),
|
||||||
|
video("e1", "show", "[Group] Yakusoku no Neverland - 01.mkv"),
|
||||||
|
video("film", "show", "Some Recap Film.mkv"),
|
||||||
|
video("other", "show", "Another Special.mkv"),
|
||||||
|
];
|
||||||
|
let rows = build_rows("share", &nodes);
|
||||||
|
for file in &rows {
|
||||||
|
assert_eq!(file.parsed.title, "Promised Neverland");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Films under `Movies/` are the series' films, and stay on its page.
|
||||||
|
#[test]
|
||||||
|
fn films_under_a_grouping_folder_stay_with_the_series() {
|
||||||
|
let nodes = vec![
|
||||||
|
root(),
|
||||||
|
folder("eva", Some("root"), "Neon Genesis Evangelion"),
|
||||||
|
folder("movies", Some("eva"), "Movies"),
|
||||||
|
video("a", "movies", "Death and Rebirth.mkv"),
|
||||||
|
video("b", "movies", "The End of Evangelion.mkv"),
|
||||||
|
];
|
||||||
|
let rows = build_rows("share", &nodes);
|
||||||
|
for file in &rows {
|
||||||
|
assert_eq!(file.parsed.title, "Neon Genesis Evangelion");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Files that repeat the folder's name are that show's episodes with their
|
||||||
|
/// numbers missing, not films of their own.
|
||||||
|
#[test]
|
||||||
|
fn unnumbered_files_named_after_their_folder_stay_one_title() {
|
||||||
|
let nodes = vec![
|
||||||
|
root(),
|
||||||
|
folder("mushi", Some("root"), "Mushishi"),
|
||||||
|
video("a", "mushi", "Mushishi - The Green Seat.mkv"),
|
||||||
|
video("b", "mushi", "Mushishi - The Light of the Eyelid.mkv"),
|
||||||
|
];
|
||||||
|
let rows = build_rows("share", &nodes);
|
||||||
|
assert_eq!(row(&rows, "a").parsed.title, "Mushishi");
|
||||||
|
assert_eq!(
|
||||||
|
row(&rows, "a").parsed.episode_title.as_deref(),
|
||||||
|
Some("The Green Seat")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two files that name themselves the same thing are not two films.
|
||||||
|
#[test]
|
||||||
|
fn files_whose_own_names_collide_are_not_split() {
|
||||||
|
let nodes = vec![
|
||||||
|
root(),
|
||||||
|
folder("film", Some("root"), "Akira"),
|
||||||
|
video("a", "film", "[GroupA] Neo Tokyo.mkv"),
|
||||||
|
video("b", "film", "Neo.Tokyo.mkv"),
|
||||||
|
];
|
||||||
|
let rows = build_rows("share", &nodes);
|
||||||
|
for file in &rows {
|
||||||
|
assert_eq!(file.parsed.title, "Akira");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The share root names the *share*, not a series — using it would label
|
/// The share root names the *share*, not a series — using it would label
|
||||||
/// every top-level file identically.
|
/// every top-level file identically.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -539,6 +539,9 @@ fn find_dash_numbering(text: &str) -> Option<NumberingMatch> {
|
|||||||
if is_token_char(tail) && tail != Some(b'v') {
|
if is_token_char(tail) && tail != Some(b'v') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if is_decimal_tail(text, after) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
found = Some(NumberingMatch {
|
found = Some(NumberingMatch {
|
||||||
numbering: Numbering {
|
numbering: Numbering {
|
||||||
season: None,
|
season: None,
|
||||||
@@ -551,6 +554,24 @@ fn find_dash_numbering(text: &str) -> Option<NumberingMatch> {
|
|||||||
found
|
found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the number that ended at `at` goes on as a decimal: `.11` in
|
||||||
|
/// `Evangelion - 1.11 You Are (Not) Alone`, `.0` in `3.0+1.01`.
|
||||||
|
///
|
||||||
|
/// That is a version of a film, not episode 1 of anything, and reading it as
|
||||||
|
/// one turns a folder of four films into a series of episodes 1, 2, 3 and 3.
|
||||||
|
/// One or two digits only: `- 05.1080p` is episode 5 and a resolution.
|
||||||
|
fn is_decimal_tail(text: &str, at: usize) -> bool {
|
||||||
|
let bytes = text.as_bytes();
|
||||||
|
if bytes.get(at) != Some(&b'.') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let digits = bytes[at + 1..]
|
||||||
|
.iter()
|
||||||
|
.take_while(|byte| byte.is_ascii_digit())
|
||||||
|
.count();
|
||||||
|
(1..=2).contains(&digits) && !is_token_char(bytes.get(at + 1 + digits).copied())
|
||||||
|
}
|
||||||
|
|
||||||
/// Step over an `E` / `EP` episode marker and any separator after it.
|
/// Step over an `E` / `EP` episode marker and any separator after it.
|
||||||
///
|
///
|
||||||
/// Returns `at` unchanged when there is none, so a bare number and a marked one
|
/// Returns `at` unchanged when there is none, so a bare number and a marked one
|
||||||
@@ -630,9 +651,18 @@ fn is_token_start(text: &str, at: usize) -> bool {
|
|||||||
/// Normalize the title span: separators to spaces, then trim the debris that
|
/// Normalize the title span: separators to spaces, then trim the debris that
|
||||||
/// sits between the title and whatever followed it.
|
/// sits between the title and whatever followed it.
|
||||||
fn clean_title(raw: &str, year: Option<u32>) -> String {
|
fn clean_title(raw: &str, year: Option<u32>) -> String {
|
||||||
|
// A dot between two digits is a decimal point, not a separator: `1.11`
|
||||||
|
// is the film's name, `1 11` is two numbers.
|
||||||
|
let bytes = raw.as_bytes();
|
||||||
let mut text: String = raw
|
let mut text: String = raw
|
||||||
.chars()
|
.char_indices()
|
||||||
.map(|c| match c {
|
.map(|(at, c)| match c {
|
||||||
|
'.' if at > 0
|
||||||
|
&& bytes[at - 1].is_ascii_digit()
|
||||||
|
&& bytes.get(at + 1).is_some_and(u8::is_ascii_digit) =>
|
||||||
|
{
|
||||||
|
'.'
|
||||||
|
}
|
||||||
'.' | '_' => ' ',
|
'.' | '_' => ' ',
|
||||||
other => other,
|
other => other,
|
||||||
})
|
})
|
||||||
@@ -856,6 +886,46 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **What the four Rebuild of Evangelion films parsed as.** ` - 1.11` read
|
||||||
|
/// as episode 1, so the folder became a series of episodes 1, 2, 3 and 3.
|
||||||
|
/// A decimal is a version of the film, and the name keeps it.
|
||||||
|
#[test]
|
||||||
|
fn a_decimal_after_the_dash_is_part_of_a_films_name_not_an_episode() {
|
||||||
|
assert_eq!(
|
||||||
|
parsed("Evangelion - 1.11 You Are (Not) Alone.mkv"),
|
||||||
|
("Evangelion - 1.11 You Are Alone".into(), None, None, None)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed("Evangelion - 3.0+1.01 Thrice Upon a Time.mkv"),
|
||||||
|
(
|
||||||
|
"Evangelion - 3.0+1.01 Thrice Upon a Time".into(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A recap numbered between two episodes is not a second copy of the one
|
||||||
|
/// before it.
|
||||||
|
#[test]
|
||||||
|
fn a_half_episode_is_not_the_episode_before_it() {
|
||||||
|
assert_eq!(
|
||||||
|
parsed("[Group] Show Name - 12.5 [1080p].mkv"),
|
||||||
|
("Show Name - 12.5".into(), None, None, None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The decimal rule stops at two digits, so a resolution glued on with a
|
||||||
|
/// dot leaves the episode number alone.
|
||||||
|
#[test]
|
||||||
|
fn a_resolution_after_a_dot_does_not_hide_the_episode() {
|
||||||
|
assert_eq!(
|
||||||
|
parsed("Show Name - 05.1080p.mkv"),
|
||||||
|
("Show Name".into(), None, Some(5), None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A version suffix still names the same episode.
|
/// A version suffix still names the same episode.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_versioned_fansub_episode_keeps_its_number() {
|
fn a_versioned_fansub_episode_keeps_its_number() {
|
||||||
|
|||||||
@@ -379,6 +379,12 @@ impl AniList {
|
|||||||
/// tokenisation: by dropping the `s` it was holding on to. It is asked for only
|
/// tokenisation: by dropping the `s` it was holding on to. It is asked for only
|
||||||
/// after the name as written found nothing, which is what keeps it from
|
/// after the name as written found nothing, which is what keeps it from
|
||||||
/// broadening a search that was already working.
|
/// broadening a search that was already working.
|
||||||
|
///
|
||||||
|
/// A film's version number is the same kind of miss. `Evangelion - 1.11 You
|
||||||
|
/// Are (Not) Alone` is the file of a film AniList calls `Evangelion: 1.0 You
|
||||||
|
/// Are (Not) Alone`, and `1.11` is not a word of that name — so the last
|
||||||
|
/// fallback asks without it, and leaves telling the films apart to the scoring,
|
||||||
|
/// which still sees the whole name.
|
||||||
fn search_terms(name: &str) -> Vec<String> {
|
fn search_terms(name: &str) -> Vec<String> {
|
||||||
let mut terms = vec![name.to_string()];
|
let mut terms = vec![name.to_string()];
|
||||||
|
|
||||||
@@ -386,9 +392,31 @@ fn search_terms(name: &str) -> Vec<String> {
|
|||||||
if depossessed != name {
|
if depossessed != name {
|
||||||
terms.push(depossessed);
|
terms.push(depossessed);
|
||||||
}
|
}
|
||||||
|
let unversioned = without_version_numbers(name);
|
||||||
|
if !unversioned.is_empty() && !terms.contains(&unversioned) {
|
||||||
|
terms.push(unversioned);
|
||||||
|
}
|
||||||
terms
|
terms
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Evangelion - 1.11 You Are Alone` → `Evangelion - You Are Alone`.
|
||||||
|
///
|
||||||
|
/// A version is digits with a dot in them (`1.11`, `3.0+1.01`); a bare number
|
||||||
|
/// is left alone, because in `Kaijuu 8-gou` or `86` it is the name.
|
||||||
|
fn without_version_numbers(name: &str) -> String {
|
||||||
|
name.split(' ')
|
||||||
|
.filter(|word| {
|
||||||
|
let versioned = word.contains('.')
|
||||||
|
&& word.chars().any(|c| c.is_ascii_digit())
|
||||||
|
&& word
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_digit() || matches!(c, '.' | '+'));
|
||||||
|
!versioned
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
/// `Heavens Feel` → `Heaven Feel`.
|
/// `Heavens Feel` → `Heaven Feel`.
|
||||||
///
|
///
|
||||||
/// Only words long enough to still mean something without it, and only where
|
/// Only words long enough to still mean something without it, and only where
|
||||||
@@ -836,6 +864,29 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **What three of the four Rebuild films matched as: nothing.** AniList
|
||||||
|
/// requires every word, and a film's version number is not one of its
|
||||||
|
/// title's.
|
||||||
|
#[test]
|
||||||
|
fn a_films_version_number_is_searched_for_again_without_it() {
|
||||||
|
assert_eq!(
|
||||||
|
search_terms("Evangelion - 1.11 You Are Alone"),
|
||||||
|
vec![
|
||||||
|
"Evangelion - 1.11 You Are Alone".to_string(),
|
||||||
|
"Evangelion - You Are Alone".to_string(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
search_terms("Evangelion - 3.0+1.01 Thrice Upon a Time")[1],
|
||||||
|
"Evangelion - Thrice Upon a Time"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
search_terms("Kaijuu 8-gou"),
|
||||||
|
vec!["Kaijuu 8-gou"],
|
||||||
|
"a number that is part of the name stays"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Byte slicing, on names that are not ASCII.
|
/// Byte slicing, on names that are not ASCII.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_multi_byte_name_is_not_cut_inside_a_character() {
|
fn a_multi_byte_name_is_not_cut_inside_a_character() {
|
||||||
|
|||||||
+8
-2
@@ -160,6 +160,14 @@ same facts, tags and franchise row as the desktop's; *Title names* is in
|
|||||||
Settings → Appearance. Android's grid is always covers, so it has no
|
Settings → Appearance. Android's grid is always covers, so it has no
|
||||||
*Library tiles* choice.
|
*Library tiles* choice.
|
||||||
|
|
||||||
|
**Folders of films.** `pstr_core::catalog::build_rows` gives each file in a
|
||||||
|
folder of films its own title, so the four Rebuild films in one folder are
|
||||||
|
four entries, matched one by one, and the franchise folds them back into one
|
||||||
|
tile. Only when no file is numbered, none sits under `Movies`/`OVA`, and each
|
||||||
|
names itself differently from the others and from the folder; anything less
|
||||||
|
is a series with its specials. A film's version number (`1.11`) is not part of its name on
|
||||||
|
AniList, so a search that finds nothing is asked again without it.
|
||||||
|
|
||||||
Not done: TMDB fills no details.
|
Not done: TMDB fills no details.
|
||||||
|
|
||||||
## Phase 3 — reach
|
## Phase 3 — reach
|
||||||
@@ -168,7 +176,5 @@ Not done: TMDB fills no details.
|
|||||||
machine.
|
machine.
|
||||||
- **Routing by title**: AniList first, then TMDB (with a key) or TVmaze for
|
- **Routing by title**: AniList first, then TMDB (with a key) or TVmaze for
|
||||||
whatever is not anime — Game of Thrones in a mostly-anime share.
|
whatever is not anime — Game of Thrones in a mostly-anime share.
|
||||||
- **Folders of films** matched per file: four Rebuild films in one folder are
|
|
||||||
four entries, not one.
|
|
||||||
- **Skip intro** from AniSkip for files without chapters, and filler flags from
|
- **Skip intro** from AniSkip for files without chapters, and filler flags from
|
||||||
Jikan.
|
Jikan.
|
||||||
+3
-1
@@ -46,12 +46,14 @@ The folder decides which show a file belongs to. The file name gives the season
|
|||||||
| `Bocchi the Rock/[Group] Bocchi the Rock - 07 [1080p].mkv` | Bocchi the Rock, episode 7 |
|
| `Bocchi the Rock/[Group] Bocchi the Rock - 07 [1080p].mkv` | Bocchi the Rock, episode 7 |
|
||||||
| `Spy x Family/Season 2 - 06.mkv` | Spy x Family, season 2, episode 6 |
|
| `Spy x Family/Season 2 - 06.mkv` | Spy x Family, season 2, episode 6 |
|
||||||
| `Akira (1988).mkv` | A film, from 1988 |
|
| `Akira (1988).mkv` | A film, from 1988 |
|
||||||
|
| `Rebuild of Evangelion/Evangelion - 1.11 You Are (Not) Alone.mkv` and three more | Four films, one title each |
|
||||||
|
|
||||||
- Season folders can be called `Season 01`, `Season 1`, `S01` or `3rd Season`.
|
- Season folders can be called `Season 01`, `Season 1`, `S01` or `3rd Season`.
|
||||||
- Folders such as `Specials`, `Extras`, `OVA`, `NCOP` or `Subs` are not taken as a show's name;
|
- Folders such as `Specials`, `Extras`, `OVA`, `NCOP` or `Subs` are not taken as a show's name;
|
||||||
the folder above them is.
|
the folder above them is.
|
||||||
- An episode title is read only after a dash, as in `S01E01 - Name`.
|
- An episode title is read only after a dash, as in `S01E01 - Name`.
|
||||||
- A file with no episode number is a film.
|
- A file with no episode number is a film. A folder of such files, each with a different name, is
|
||||||
|
one title per film.
|
||||||
|
|
||||||
The app plays `mkv`, `mp4`, `m4v`, `avi`, `mov`, `webm`, `ts`, `m2ts` and most other common video
|
The app plays `mkv`, `mp4`, `m4v`, `avi`, `mov`, `webm`, `ts`, `m2ts` and most other common video
|
||||||
containers.
|
containers.
|
||||||
|
|||||||
Reference in new issue
Block a user