flix_model/text.rs
1//! This module contains helper functions for normalizing media titles
2
3use core::iter::Peekable;
4
5use itertools::Itertools;
6
7/// # Panics
8///
9/// Panics if `input` is not ASCII.
10fn split_normalized_words(input: &str) -> impl Iterator<Item = String> {
11 if !input.is_ascii() {
12 panic!("Input is not ASCII: {input}");
13 }
14
15 input.split_ascii_whitespace().map(|s| {
16 let chars = s
17 .chars()
18 .filter(|c| c.is_ascii_alphanumeric() || *c == '-')
19 .map(|c| c.to_ascii_lowercase());
20
21 if s.len() > 4
22 && s.len().is_multiple_of(2)
23 && chars.clone().tuples().all(|(l, r)| l != '.' && r == '.')
24 {
25 // Collapse acronym
26 chars.tuples().map(|(l, _)| l).collect()
27 } else {
28 chars.collect()
29 }
30 })
31}
32
33fn split_leading_article<I: Iterator<Item = String>>(iter: I) -> (Option<String>, Peekable<I>) {
34 let mut iter = iter.peekable();
35 match iter.peek().map(String::as_str) {
36 Some("a" | "an" | "the") => (iter.next(), iter),
37 _ => (None, iter),
38 }
39}
40
41/// Convert a media title to be sortable and searchable
42///
43/// use flix_model::text::make_sortable_title;
44///
45/// assert_eq!(make_sortable_title("The Matrix"), "matrix, the");
46/// assert_eq!(make_sortable_title("Marvel's Agents of S.H.I.E.L.D."), "marvels agents of shield");
47/// assert_eq!(make_sortable_title("Avatar: The Last Airbender"), "avatar the last airbender");
48///
49/// # Panics
50///
51/// Panics if `input` is not ASCII.
52pub fn make_sortable_title(title: &str) -> String {
53 let words = split_normalized_words(title);
54 let (article, words) = split_leading_article(words);
55
56 let output = Itertools::intersperse(words, " ".to_string());
57 if let Some(article) = article {
58 output.chain([", ".to_string(), article]).collect()
59 } else {
60 output.collect()
61 }
62}
63
64/// Convert a media title to a folder name representable on filesystems
65///
66/// use flix_model::text::make_fs_slug;
67///
68/// assert_eq!(make_fs_slug("The Matrix"), "matrix");
69/// assert_eq!(make_fs_slug("Marvel's Agents of S.H.I.E.L.D."), "marvels agents of shield");
70/// assert_eq!(make_fs_slug("Avatar: The Last Airbender"), "avatar the last airbender");
71///
72/// # Panics
73///
74/// Panics if `input` is not ASCII.
75pub fn make_fs_slug(title: &str) -> String {
76 let words = split_normalized_words(title);
77 let (_, words) = split_leading_article(words);
78
79 Itertools::intersperse(words, " ".to_string()).collect()
80}
81
82/// Convert a media title and year to a folder name representable on filesystems
83///
84/// use flix_model::text::make_fs_slug_year;
85///
86/// assert_eq!(make_fs_slug_year("The Matrix", 1999), "matrix (1999)");
87/// assert_eq!(make_fs_slug_year("Marvel's Agents of S.H.I.E.L.D.", 2013), "marvels agents of shield (2013)");
88/// assert_eq!(make_fs_slug_year("Avatar: The Last Airbender", 2005), "avatar the last airbender (2005)");
89///
90/// # Panics
91///
92/// Panics if `input` is not ASCII.
93pub fn make_fs_slug_year(title: &str, year: i32) -> String {
94 let words = split_normalized_words(title);
95 let (_, words) = split_leading_article(words);
96
97 Itertools::intersperse(words, " ".to_string())
98 .chain([format!(" ({year})")])
99 .collect()
100}
101
102/// Normalize a filesystem name
103///
104/// use flix_model::text::normalize_fs_name;
105///
106/// assert_eq!(normalize_fs_name("Matrix (1999)"), "matrix (1999)");
107/// assert_eq!(normalize_fs_name("Marvel's Agents of SHIELD (2013)"), "marvels agents of shield (2013)");
108/// assert_eq!(normalize_fs_name("Avatar The Last Airbender (2005)"), "avatar the last airbender (2005)");
109pub fn normalize_fs_name(input: &str) -> String {
110 let chars = input.split_ascii_whitespace().map(|s| {
111 let chars = s
112 .chars()
113 .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '(' || *c == ')')
114 .map(|c| c.to_ascii_lowercase());
115
116 if s.len() > 4
117 && s.len().is_multiple_of(2)
118 && chars.clone().tuples().all(|(l, r)| l != '.' && r == '.')
119 {
120 // Collapse acronym
121 chars.tuples().map(|(l, _)| l).collect()
122 } else {
123 chars.collect()
124 }
125 });
126 Itertools::intersperse(chars, " ".to_string()).collect()
127}
128
129/// Convert a media title to a url compatible string
130///
131/// use flix_model::text::make_web_slug;
132///
133/// assert_eq!(make_web_slug("The Matrix"), "matrix");
134/// assert_eq!(make_web_slug("Marvel's Agents of S.H.I.E.L.D."), "marvels-agents-of-shield");
135/// assert_eq!(make_web_slug("Avatar: The Last Airbender"), "avatar-the-last-airbender");
136///
137/// # Panics
138///
139/// Panics if `input` is not ASCII.
140pub fn make_web_slug(title: &str) -> String {
141 let words = split_normalized_words(title);
142 let (_, words) = split_leading_article(words);
143
144 Itertools::intersperse(words, "-".to_string()).collect()
145}
146
147/// Convert a media title and year to a url compatible string
148///
149/// use flix_model::text::make_web_slug_year;
150///
151/// assert_eq!(make_web_slug_year("The Matrix", 1999), "matrix-1999");
152/// assert_eq!(make_web_slug_year("Marvel's Agents of S.H.I.E.L.D.", 2013), "marvels-agents-of-shield-2013");
153/// assert_eq!(make_web_slug_year("Avatar: The Last Airbender", 2005), "avatar-the-last-airbender-2005");
154///
155/// # Panics
156///
157/// Panics if `input` is not ASCII.
158pub fn make_web_slug_year(title: &str, year: i32) -> String {
159 let words = split_normalized_words(title);
160 let (_, words) = split_leading_article(words);
161
162 Itertools::intersperse(words, "-".to_string())
163 .chain([format!("-{year}")])
164 .collect()
165}