mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
refactor(core,player): move the language table into pstr-core
The crawl needs it to read a subtitle file's language from its name (en, eng, English), and pstr-player re-exports it unchanged.
This commit is contained in:
1 parent
c44b04cf4a
commit
51454b5a6e
3 files changed
+128
-82
No files matched your search
@@ -0,0 +1,126 @@
|
||||
//! Language tags: what a container or a file name calls a language, and what a
|
||||
//! viewer calls it.
|
||||
|
||||
/// The languages offered by name where a preference is chosen ahead of any
|
||||
/// file, as the three-letter tag a Matroska file carries. The same list
|
||||
/// [`language_name`] knows, and the phones' `LANGUAGES`; mpv treats the two-
|
||||
/// and three-letter forms as one.
|
||||
pub const LANGUAGES: [&str; 29] = [
|
||||
"jpn", "eng", "ger", "fre", "spa", "ita", "por", "dut", "rus", "pol", "swe", "dan", "nor",
|
||||
"fin", "cze", "hun", "tur", "ara", "heb", "hin", "kor", "chi", "tha", "vie", "ukr", "rum",
|
||||
"gre", "ind", "may",
|
||||
];
|
||||
|
||||
/// Each language's name and the tags it goes by, the [`LANGUAGES`] form first.
|
||||
///
|
||||
/// Deliberately a short list rather than a full ISO 639 table: it covers what
|
||||
/// the shares this app is pointed at actually contain, and an unrecognised tag
|
||||
/// still shows as something a viewer can pick between — `"por-BR"` reads fine
|
||||
/// on its own.
|
||||
const TABLE: [(&str, &[&str]); 29] = [
|
||||
("English", &["eng", "en"]),
|
||||
("Japanese", &["jpn", "ja"]),
|
||||
("German", &["ger", "de", "deu"]),
|
||||
("French", &["fre", "fr", "fra"]),
|
||||
("Spanish", &["spa", "es"]),
|
||||
("Italian", &["ita", "it"]),
|
||||
("Portuguese", &["por", "pt"]),
|
||||
("Dutch", &["dut", "nl", "nld"]),
|
||||
("Russian", &["rus", "ru"]),
|
||||
("Polish", &["pol", "pl"]),
|
||||
("Swedish", &["swe", "sv"]),
|
||||
("Danish", &["dan", "da"]),
|
||||
("Norwegian", &["nor", "no"]),
|
||||
("Finnish", &["fin", "fi"]),
|
||||
("Czech", &["cze", "cs", "ces"]),
|
||||
("Hungarian", &["hun", "hu"]),
|
||||
("Turkish", &["tur", "tr"]),
|
||||
("Arabic", &["ara", "ar"]),
|
||||
("Hebrew", &["heb", "he"]),
|
||||
("Hindi", &["hin", "hi"]),
|
||||
("Korean", &["kor", "ko"]),
|
||||
("Chinese", &["chi", "zh", "zho"]),
|
||||
("Thai", &["tha", "th"]),
|
||||
("Vietnamese", &["vie", "vi"]),
|
||||
("Ukrainian", &["ukr", "uk"]),
|
||||
("Romanian", &["rum", "ro", "ron"]),
|
||||
("Greek", &["gre", "el", "ell"]),
|
||||
("Indonesian", &["ind", "id"]),
|
||||
("Malay", &["may", "ms", "msa"]),
|
||||
];
|
||||
|
||||
/// A readable name for a language tag, falling back to the tag itself.
|
||||
pub fn language_name(tag: &str) -> String {
|
||||
// Container tags come in both the two- and three-letter forms, and often
|
||||
// with a region suffix: "en", "eng", "en-US", "pt-BR".
|
||||
let base = tag.split(['-', '_']).next().unwrap_or(tag);
|
||||
let Some((name, _)) = TABLE
|
||||
.iter()
|
||||
.find(|(_, tags)| tags.iter().any(|known| known.eq_ignore_ascii_case(base)))
|
||||
else {
|
||||
return tag.to_string();
|
||||
};
|
||||
|
||||
// The region matters when there is one — Brazilian Portuguese is not
|
||||
// Portuguese to anyone choosing between the two.
|
||||
match tag.split_once(['-', '_']) {
|
||||
Some((_, region)) if !region.is_empty() => {
|
||||
format!("{name} ({})", region.to_ascii_uppercase())
|
||||
}
|
||||
_ => (*name).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether two container tags name the same language: "en" and "eng" do.
|
||||
pub fn same_language(a: &str, b: &str) -> bool {
|
||||
a.eq_ignore_ascii_case(b) || language_name(a) == language_name(b)
|
||||
}
|
||||
|
||||
/// The [`LANGUAGES`] tag for one word of a file name, if it names a language:
|
||||
/// "en", "eng" and "English" are all `"eng"`.
|
||||
///
|
||||
/// For the words around a subtitle file's name — `Episode 01.en.srt`,
|
||||
/// `2_English.srt` — where a two-letter word is far more often a language than
|
||||
/// anything else.
|
||||
pub fn language_tag(word: &str) -> Option<&'static str> {
|
||||
TABLE
|
||||
.iter()
|
||||
.find(|(name, tags)| {
|
||||
name.eq_ignore_ascii_case(word) || tags.iter().any(|tag| tag.eq_ignore_ascii_case(word))
|
||||
})
|
||||
.map(|(_, tags)| tags[0])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn language_tags_resolve_in_both_iso_forms_and_keep_their_region() {
|
||||
assert_eq!(language_name("jpn"), "Japanese");
|
||||
assert_eq!(language_name("ja"), "Japanese");
|
||||
assert_eq!(language_name("pt-BR"), "Portuguese (BR)");
|
||||
// An unknown tag is shown as it stands rather than as "Unknown", which
|
||||
// would make two of them indistinguishable.
|
||||
assert_eq!(language_name("mis"), "mis");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_language_named_by_either_tag_or_by_its_name_has_the_tag_a_preference_uses() {
|
||||
assert_eq!(language_tag("en"), Some("eng"));
|
||||
assert_eq!(language_tag("ENG"), Some("eng"));
|
||||
assert_eq!(language_tag("German"), Some("ger"));
|
||||
assert_eq!(language_tag("deu"), Some("ger"));
|
||||
assert_eq!(language_tag("forced"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_language_offered_by_name_is_one_the_table_knows() {
|
||||
let firsts: Vec<&str> = TABLE.iter().map(|(_, tags)| tags[0]).collect();
|
||||
let mut offered = LANGUAGES.to_vec();
|
||||
let mut known = firsts.clone();
|
||||
offered.sort_unstable();
|
||||
known.sort_unstable();
|
||||
assert_eq!(offered, known);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ pub mod chapters;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod franchise;
|
||||
pub mod language;
|
||||
pub mod library;
|
||||
pub mod metadata;
|
||||
pub mod naming;
|
||||
|
||||
@@ -152,11 +152,6 @@ pub fn pick_track(
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether two container tags name the same language: "en" and "eng" do.
|
||||
fn same_language(a: &str, b: &str) -> bool {
|
||||
a.eq_ignore_ascii_case(b) || language_name(a) == language_name(b)
|
||||
}
|
||||
|
||||
fn read_one(mpv: &Mpv, index: i64) -> Option<Track> {
|
||||
let kind = TrackKind::from_mpv(&text(mpv, index, "type")?)?;
|
||||
let id: i64 = mpv
|
||||
@@ -190,73 +185,7 @@ fn flag(mpv: &Mpv, index: i64, field: &str) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The languages offered by name where a preference is chosen ahead of any
|
||||
/// file, as the three-letter tag a Matroska file carries. The same list
|
||||
/// [`language_name`] knows, and the phones' `LANGUAGES`; mpv treats the two-
|
||||
/// and three-letter forms as one.
|
||||
pub const LANGUAGES: [&str; 29] = [
|
||||
"jpn", "eng", "ger", "fre", "spa", "ita", "por", "dut", "rus", "pol", "swe", "dan", "nor",
|
||||
"fin", "cze", "hun", "tur", "ara", "heb", "hin", "kor", "chi", "tha", "vie", "ukr", "rum",
|
||||
"gre", "ind", "may",
|
||||
];
|
||||
|
||||
/// A readable name for a language tag, falling back to the tag itself.
|
||||
///
|
||||
/// Deliberately a short list rather than a full ISO 639 table: it covers what
|
||||
/// the shares this app is pointed at actually contain, and an unrecognised tag
|
||||
/// still shows as something a viewer can pick between — `"por-BR"` reads fine
|
||||
/// on its own.
|
||||
pub fn language_name(tag: &str) -> String {
|
||||
// Container tags come in both the two- and three-letter forms, and often
|
||||
// with a region suffix: "en", "eng", "en-US", "pt-BR".
|
||||
let base = tag
|
||||
.split(['-', '_'])
|
||||
.next()
|
||||
.unwrap_or(tag)
|
||||
.to_ascii_lowercase();
|
||||
|
||||
let name = match base.as_str() {
|
||||
"en" | "eng" => "English",
|
||||
"ja" | "jpn" => "Japanese",
|
||||
"de" | "ger" | "deu" => "German",
|
||||
"fr" | "fre" | "fra" => "French",
|
||||
"es" | "spa" => "Spanish",
|
||||
"it" | "ita" => "Italian",
|
||||
"pt" | "por" => "Portuguese",
|
||||
"nl" | "dut" | "nld" => "Dutch",
|
||||
"ru" | "rus" => "Russian",
|
||||
"pl" | "pol" => "Polish",
|
||||
"sv" | "swe" => "Swedish",
|
||||
"da" | "dan" => "Danish",
|
||||
"no" | "nor" => "Norwegian",
|
||||
"fi" | "fin" => "Finnish",
|
||||
"cs" | "cze" | "ces" => "Czech",
|
||||
"hu" | "hun" => "Hungarian",
|
||||
"tr" | "tur" => "Turkish",
|
||||
"ar" | "ara" => "Arabic",
|
||||
"he" | "heb" => "Hebrew",
|
||||
"hi" | "hin" => "Hindi",
|
||||
"ko" | "kor" => "Korean",
|
||||
"zh" | "chi" | "zho" => "Chinese",
|
||||
"th" | "tha" => "Thai",
|
||||
"vi" | "vie" => "Vietnamese",
|
||||
"uk" | "ukr" => "Ukrainian",
|
||||
"ro" | "rum" | "ron" => "Romanian",
|
||||
"el" | "gre" | "ell" => "Greek",
|
||||
"id" | "ind" => "Indonesian",
|
||||
"ms" | "may" | "msa" => "Malay",
|
||||
_ => return tag.to_string(),
|
||||
};
|
||||
|
||||
// The region matters when there is one — Brazilian Portuguese is not
|
||||
// Portuguese to anyone choosing between the two.
|
||||
match tag.split_once(['-', '_']) {
|
||||
Some((_, region)) if !region.is_empty() => {
|
||||
format!("{name} ({})", region.to_ascii_uppercase())
|
||||
}
|
||||
_ => name.to_string(),
|
||||
}
|
||||
}
|
||||
pub use pstr_core::language::{LANGUAGES, language_name, same_language};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -292,16 +221,6 @@ mod tests {
|
||||
assert_eq!(sub.label(), "English — Signs & Songs [ass]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_tags_resolve_in_both_iso_forms_and_keep_their_region() {
|
||||
assert_eq!(language_name("jpn"), "Japanese");
|
||||
assert_eq!(language_name("ja"), "Japanese");
|
||||
assert_eq!(language_name("pt-BR"), "Portuguese (BR)");
|
||||
// An unknown tag is shown as it stands rather than as "Unknown", which
|
||||
// would make two of them indistinguishable.
|
||||
assert_eq!(language_name("mis"), "mis");
|
||||
}
|
||||
|
||||
fn named(kind: TrackKind, id: i64, language: Option<&str>, title: Option<&str>) -> Track {
|
||||
Track {
|
||||
language: language.map(Into::into),
|
||||
|
||||
Reference in new issue
Block a user