mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-13 17:37:03 +01:00
feat(core): episode titles from dotted release names
Scene releases name the episode in dotted words before their tags: Game.of.Thrones.S01E01.Winter.Is.Coming.2160p... Read the words up to the first quality, source or codec tag; a dotted run that no tag ends is not taken.
This commit is contained in:
1 parent
3ca431feca
commit
70952fa7ca
2 files changed
+65
-10
No files matched your search
@@ -22,8 +22,9 @@ pub struct ParsedName {
|
|||||||
/// Release year, from a parenthesised or bracketed four-digit group.
|
/// Release year, from a parenthesised or bracketed four-digit group.
|
||||||
pub year: Option<u32>,
|
pub year: Option<u32>,
|
||||||
/// The episode's own name, when the filename states one after the
|
/// The episode's own name, when the filename states one after the
|
||||||
/// numbering (`S01E01-Mother and Children`). Never the quality and codec
|
/// numbering (`S01E01-Mother and Children`, `S01E01.Winter.Is.Coming.2160p`).
|
||||||
/// tags that follow numbering in scene releases — see [`episode_title`].
|
/// Never the quality and codec tags that follow numbering in scene
|
||||||
|
/// releases — see [`episode_title`].
|
||||||
pub episode_title: Option<String>,
|
pub episode_title: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,17 +99,21 @@ struct NumberingMatch {
|
|||||||
|
|
||||||
/// The episode's own name out of what followed the numbering.
|
/// The episode's own name out of what followed the numbering.
|
||||||
///
|
///
|
||||||
/// Only taken when a dash introduces it. Scene releases separate their quality,
|
/// Taken when a dash introduces it, or from a scene release's dotted words up
|
||||||
/// source and codec tags with the same character as their title words (`.`), so
|
/// to its first release tag. Scene releases separate their quality, source and
|
||||||
/// without that requirement `Show.Name.S02E05.1080p.WEB-DL.x265` would yield an
|
/// codec tags with the same character as their title words (`.`), so the tag
|
||||||
/// "episode title" of `1080p WEB-DL x265`.
|
/// is what ends the name: `Show.S01E01.Winter.Is.Coming.2160p.BluRay` names
|
||||||
|
/// its episode, `Show.Name.S02E05.1080p.WEB-DL.x265` does not, and a dotted
|
||||||
|
/// run that no tag ends is not trusted to be a name at all.
|
||||||
fn episode_title(tail: &str) -> Option<String> {
|
fn episode_title(tail: &str) -> Option<String> {
|
||||||
let rest = tail.trim_start();
|
let rest = tail.trim_start();
|
||||||
let rest = rest
|
let cleaned = match rest
|
||||||
.strip_prefix('-')
|
.strip_prefix('-')
|
||||||
.or_else(|| rest.strip_prefix('\u{2013}'))?;
|
.or_else(|| rest.strip_prefix('\u{2013}'))
|
||||||
|
{
|
||||||
let cleaned = clean_title(rest, None);
|
Some(rest) => clean_title(rest, None),
|
||||||
|
None => dotted_title(rest.strip_prefix('.')?)?,
|
||||||
|
};
|
||||||
let cleaned = cleaned.trim().to_string();
|
let cleaned = cleaned.trim().to_string();
|
||||||
if cleaned.is_empty() {
|
if cleaned.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -117,6 +122,29 @@ fn episode_title(tail: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Winter.Is.Coming.2160p.UHD…` → `Winter Is Coming`.
|
||||||
|
fn dotted_title(rest: &str) -> Option<String> {
|
||||||
|
let words: Vec<&str> = rest.split('.').collect();
|
||||||
|
let end = words.iter().position(|word| is_release_tag(word))?;
|
||||||
|
Some(words[..end].join(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a dotted word is a release's quality, source or codec rather than
|
||||||
|
/// part of a name.
|
||||||
|
fn is_release_tag(word: &str) -> bool {
|
||||||
|
const TAGS: &[&str] = &[
|
||||||
|
"4k", "uhd", "bluray", "blu-ray", "bdrip", "brrip", "bdremux", "remux", "web", "web-dl",
|
||||||
|
"webdl", "webrip", "hdtv", "dvdrip", "hdrip", "x264", "x265", "h264", "h265", "hevc",
|
||||||
|
"avc", "hdr", "hdr10", "dv", "10bit", "10-bit", "proper", "repack", "amzn", "nf", "dsnp",
|
||||||
|
"hmax", "atvp",
|
||||||
|
];
|
||||||
|
let word = word.to_ascii_lowercase();
|
||||||
|
let resolution = word.strip_suffix('p').is_some_and(|digits| {
|
||||||
|
(3..=4).contains(&digits.len()) && digits.bytes().all(|b| b.is_ascii_digit())
|
||||||
|
});
|
||||||
|
resolution || TAGS.contains(&word.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
/// Drop a trailing extension, but only one that looks like an extension.
|
/// Drop a trailing extension, but only one that looks like an extension.
|
||||||
///
|
///
|
||||||
/// A bare `rsplit('.')` would eat the tail of `Serial.Experiments.Lain`, so the
|
/// A bare `rsplit('.')` would eat the tail of `Serial.Experiments.Lain`, so the
|
||||||
@@ -1112,6 +1140,28 @@ mod tests {
|
|||||||
assert_eq!(p.title, "Show Name");
|
assert_eq!(p.title, "Show Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A scene release names its episode in dotted words before its tags, and
|
||||||
|
/// the first tag is where the name ends.
|
||||||
|
#[test]
|
||||||
|
fn a_dotted_episode_name_before_the_release_tags_is_captured() {
|
||||||
|
let p = parse(
|
||||||
|
"Game.of.Thrones.S01E01.Winter.Is.Coming.2160p.UHD.BluRay.10bit.HDR.x265-GRP.mkv",
|
||||||
|
);
|
||||||
|
assert_eq!(p.title, "Game of Thrones");
|
||||||
|
assert_eq!((p.season, p.episode), (Some(1), Some(1)));
|
||||||
|
assert_eq!(p.episode_title.as_deref(), Some("Winter Is Coming"));
|
||||||
|
|
||||||
|
let p = parse("Game.of.Thrones.S06E05.The.Door.1080p.WEB-DL.mkv");
|
||||||
|
assert_eq!(p.episode_title.as_deref(), Some("The Door"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Without a tag to end it, a dotted run could be a group's name or
|
||||||
|
/// anything else, and is not taken.
|
||||||
|
#[test]
|
||||||
|
fn a_dotted_run_no_tag_ends_is_not_an_episode_name() {
|
||||||
|
assert_eq!(parse("Show.S01E01.Pilot.mkv").episode_title, None);
|
||||||
|
}
|
||||||
|
|
||||||
/// A library that names its files with nothing but the episode marker.
|
/// A library that names its files with nothing but the episode marker.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_bare_episode_marker_is_read_as_an_episode() {
|
fn a_bare_episode_marker_is_read_as_an_episode() {
|
||||||
|
|||||||
@@ -220,6 +220,11 @@ premiere date, runtime, rating — so a match carries its details with no second
|
|||||||
request. Enrichment is one more: `/shows/{id}` with the episodes and images
|
request. Enrichment is one more: `/shows/{id}` with the episodes and images
|
||||||
embedded. Specials, which TVmaze numbers nowhere a file would, are left out.
|
embedded. Specials, which TVmaze numbers nowhere a file would, are left out.
|
||||||
|
|
||||||
|
**Episode names from scene releases (done).** `Game.of.Thrones.S01E01.Winter.Is.Coming.2160p…`
|
||||||
|
names its episode in dotted words, and `naming::episode_title` now reads them,
|
||||||
|
stopping at the first release tag. A dotted run that no tag ends is not taken.
|
||||||
|
This is the fallback for a title no provider has.
|
||||||
|
|
||||||
Still to do:
|
Still to do:
|
||||||
|
|
||||||
- **Local matching** against the offline database, so only ids leave the
|
- **Local matching** against the offline database, so only ids leave the
|
||||||
|
|||||||
Reference in new issue
Block a user