You've already forked flix
Update dependencies and lints
This commit is contained in:
+29
-6
@@ -1,26 +1,49 @@
|
||||
//! This module contains types relating to flix media IDs
|
||||
//! This module contains types relating to flix media IDs.
|
||||
|
||||
#![expect(clippy::module_name_repetitions, reason = "ID types end with Id")]
|
||||
|
||||
use seamantic::model::id::Id;
|
||||
|
||||
/// Type alias for the raw ID representation
|
||||
/// Type alias for the raw ID representation.
|
||||
pub use seamantic::model::id::SeaOrmRepr as RawId;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Library {}
|
||||
/// Type alias for a library ID
|
||||
/// Type alias for a library ID.
|
||||
pub type LibraryId = Id<Library>;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Collection {}
|
||||
/// Type alias for a collection ID
|
||||
/// Type alias for a collection ID.
|
||||
pub type CollectionId = Id<Collection>;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Movie {}
|
||||
/// Type alias for a movie ID
|
||||
/// Type alias for a movie ID.
|
||||
pub type MovieId = Id<Movie>;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Show {}
|
||||
/// Type alias for a show ID
|
||||
/// Type alias for a show ID.
|
||||
pub type ShowId = Id<Show>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CollectionId, LibraryId, MovieId, ShowId};
|
||||
|
||||
#[test]
|
||||
fn use_types() {
|
||||
_ = CollectionId::from_raw(0);
|
||||
_ = LibraryId::from_raw(0);
|
||||
_ = MovieId::from_raw(0);
|
||||
_ = ShowId::from_raw(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! flix-model provides core types for flix data
|
||||
//! flix-model provides core types for flix data.
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
|
||||
+60
-26
@@ -1,4 +1,4 @@
|
||||
//! This module contains season and episode numbers and related errors
|
||||
//! This module contains season and episode numbers and related errors.
|
||||
|
||||
use core::fmt;
|
||||
use core::ops::RangeInclusive;
|
||||
@@ -7,7 +7,7 @@ use std::collections::HashSet;
|
||||
|
||||
use seamantic::sea_orm;
|
||||
|
||||
/// Newtype for representing season numbers
|
||||
/// Newtype for representing season numbers.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sea_orm::DeriveValueType,
|
||||
)]
|
||||
@@ -17,13 +17,16 @@ use seamantic::sea_orm;
|
||||
pub struct SeasonNumber(u32);
|
||||
|
||||
impl SeasonNumber {
|
||||
/// Create a `SeasonNumber` from an integer
|
||||
pub fn new(value: u32) -> Self {
|
||||
/// Create a [`SeasonNumber`] from an integer.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn new(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SeasonNumber {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
@@ -32,12 +35,13 @@ impl fmt::Display for SeasonNumber {
|
||||
impl FromStr for SeasonNumber {
|
||||
type Err = <u32 as FromStr>::Err;
|
||||
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
u32::from_str(s).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Newtype for representing episode numbers
|
||||
/// Newtype for representing episode numbers.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sea_orm::DeriveValueType,
|
||||
)]
|
||||
@@ -47,18 +51,23 @@ impl FromStr for SeasonNumber {
|
||||
pub struct EpisodeNumber(u32);
|
||||
|
||||
impl EpisodeNumber {
|
||||
/// Create an `EpisodeNumber` from an integer
|
||||
pub fn new(value: u32) -> Self {
|
||||
/// Create an [`EpisodeNumber`] from an integer.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn new(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Get the underlying value
|
||||
pub fn into_inner(self) -> u32 {
|
||||
/// Get the underlying value.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn into_inner(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EpisodeNumber {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
@@ -67,33 +76,40 @@ impl fmt::Display for EpisodeNumber {
|
||||
impl FromStr for EpisodeNumber {
|
||||
type Err = <u32 as FromStr>::Err;
|
||||
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
u32::from_str(s).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Potential errors when building EpisodeNumbers
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// There are no episodes
|
||||
/// Potential errors when building [`EpisodeNumbers`].
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[expect(clippy::exhaustive_enums, reason = "unlikely to add new variants")]
|
||||
pub enum EpisodeNumbersError {
|
||||
/// There are no episodes.
|
||||
#[error("zero episodes")]
|
||||
Zero,
|
||||
/// There are gaps in the episodes
|
||||
/// There are gaps in the episodes.
|
||||
#[error("noncontiguous episodes")]
|
||||
Noncontiguous,
|
||||
}
|
||||
|
||||
/// A wrapper for handling single and multi-episode entries
|
||||
/// A wrapper for handling single and multi-episode entries.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[expect(
|
||||
clippy::module_name_repetitions,
|
||||
reason = "Episodes is not a good name"
|
||||
)]
|
||||
pub struct EpisodeNumbers(RangeInclusive<EpisodeNumber>);
|
||||
|
||||
impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
|
||||
type Error = Error;
|
||||
type Error = EpisodeNumbersError;
|
||||
|
||||
#[inline]
|
||||
fn try_from(value: &[EpisodeNumber]) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
[] => Err(Error::Zero),
|
||||
[] => Err(EpisodeNumbersError::Zero),
|
||||
[n] => Ok(Self(*n..=*n)),
|
||||
_ => {
|
||||
// min and max will always exist
|
||||
@@ -102,12 +118,12 @@ impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
|
||||
let len = value.len();
|
||||
|
||||
if usize::try_from(max.0.saturating_sub(min.0).saturating_add(1)) != Ok(len) {
|
||||
return Err(Error::Noncontiguous);
|
||||
return Err(EpisodeNumbersError::Noncontiguous);
|
||||
}
|
||||
|
||||
let set: HashSet<_> = value.iter().copied().collect();
|
||||
if set.len() != len {
|
||||
return Err(Error::Noncontiguous);
|
||||
return Err(EpisodeNumbersError::Noncontiguous);
|
||||
}
|
||||
|
||||
Ok(Self(min..=max))
|
||||
@@ -117,27 +133,45 @@ impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
|
||||
}
|
||||
|
||||
impl EpisodeNumbers {
|
||||
/// Create an [EpisodeNumbers] from a starting number and a count.
|
||||
/// Create an [`EpisodeNumbers`] from a starting number and a count.
|
||||
/// `count` should be zero for single episodes.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn new(start: EpisodeNumber, count: u8) -> Self {
|
||||
Self(start..=EpisodeNumber(start.0.saturating_add(count.into())))
|
||||
}
|
||||
|
||||
/// Get the range of episodes
|
||||
pub fn as_range(&self) -> &RangeInclusive<EpisodeNumber> {
|
||||
/// Get the range of episodes.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn as_range(&self) -> &RangeInclusive<EpisodeNumber> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Render this [EpisodeNumbers] as a range. If only one episode is
|
||||
/// is present it renders as `01`, if multiple it renders as `01-02`
|
||||
/// Render this [`EpisodeNumbers`] as a range. If only one episode is
|
||||
/// is present it renders as `01`, if multiple it renders as `01-02`.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn range_string(&self) -> String {
|
||||
let start = self.0.start();
|
||||
let end = self.0.end();
|
||||
|
||||
if start == end {
|
||||
format!("{:02}", start)
|
||||
format!("{start:02}")
|
||||
} else {
|
||||
format!("{:02}-{:02}", start, end)
|
||||
format!("{start:02}-{end:02}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{EpisodeNumber, EpisodeNumbers, SeasonNumber};
|
||||
|
||||
#[test]
|
||||
fn use_fn() {
|
||||
_ = SeasonNumber::new(0);
|
||||
let e = EpisodeNumber::new(0);
|
||||
_ = EpisodeNumbers::new(e, 1);
|
||||
}
|
||||
}
|
||||
|
||||
+48
-13
@@ -1,22 +1,26 @@
|
||||
//! This module contains helper functions for normalizing media titles
|
||||
//! This module contains helper functions for normalizing media titles.
|
||||
|
||||
use core::iter::Peekable;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
/// Return an iterator over the normalized words of a string.
|
||||
/// - Alphanumeric
|
||||
/// - Lowercase
|
||||
/// - Replace `&` with `and`
|
||||
/// - Collapse acronyms
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `input` is not ASCII.
|
||||
fn split_normalized_words(input: &str) -> impl Iterator<Item = String> {
|
||||
if !input.is_ascii() {
|
||||
panic!("Input is not ASCII: {input}");
|
||||
}
|
||||
assert!(input.is_ascii(), "input is not ASCII: {input}");
|
||||
|
||||
input
|
||||
.split_ascii_whitespace()
|
||||
.map(|s| {
|
||||
if s == "&" {
|
||||
return "and".to_string();
|
||||
return "and".to_owned();
|
||||
}
|
||||
|
||||
let chars = s
|
||||
@@ -37,6 +41,7 @@ fn split_normalized_words(input: &str) -> impl Iterator<Item = String> {
|
||||
.filter(|part: &String| !part.is_empty() && part != "-")
|
||||
}
|
||||
|
||||
/// Split out `a`, `an`, and `the` from the start of the string if it exists.
|
||||
fn split_leading_article<I: Iterator<Item = String>>(iter: I) -> (Option<String>, Peekable<I>) {
|
||||
let mut iter = iter.peekable();
|
||||
match iter.peek().map(String::as_str) {
|
||||
@@ -57,13 +62,15 @@ fn split_leading_article<I: Iterator<Item = String>>(iter: I) -> (Option<String>
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `input` is not ASCII.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn make_sortable_title(title: &str) -> String {
|
||||
let words = split_normalized_words(title);
|
||||
let (article, words) = split_leading_article(words);
|
||||
|
||||
let output = Itertools::intersperse(words, " ".to_string());
|
||||
let output = Itertools::intersperse(words, " ".to_owned());
|
||||
if let Some(article) = article {
|
||||
output.chain([", ".to_string(), article]).collect()
|
||||
output.chain([", ".to_owned(), article]).collect()
|
||||
} else {
|
||||
output.collect()
|
||||
}
|
||||
@@ -81,11 +88,13 @@ pub fn make_sortable_title(title: &str) -> String {
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `input` is not ASCII.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn make_fs_slug(title: &str) -> String {
|
||||
let words = split_normalized_words(title);
|
||||
let (_, words) = split_leading_article(words);
|
||||
|
||||
Itertools::intersperse(words, " ".to_string()).collect()
|
||||
Itertools::intersperse(words, " ".to_owned()).collect()
|
||||
}
|
||||
|
||||
/// Convert a media title and year to a folder name representable on filesystems
|
||||
@@ -100,11 +109,13 @@ pub fn make_fs_slug(title: &str) -> String {
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `input` is not ASCII.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn make_fs_slug_year(title: &str, year: i32) -> String {
|
||||
let words = split_normalized_words(title);
|
||||
let (_, words) = split_leading_article(words);
|
||||
|
||||
Itertools::intersperse(words, " ".to_string())
|
||||
Itertools::intersperse(words, " ".to_owned())
|
||||
.chain([format!(" ({year})")])
|
||||
.collect()
|
||||
}
|
||||
@@ -117,10 +128,12 @@ pub fn make_fs_slug_year(title: &str, year: i32) -> String {
|
||||
/// assert_eq!(normalize_fs_name("Marvel's Agents of SHIELD (2013)"), "marvels agents of shield (2013)");
|
||||
/// assert_eq!(normalize_fs_name("Avatar The Last Airbender (2005)"), "avatar the last airbender (2005)");
|
||||
/// assert_eq!(normalize_fs_name("Cloak & Dagger (2018)"), "cloak and dagger (2018)");
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn normalize_fs_name(input: &str) -> String {
|
||||
let chars = input.split_ascii_whitespace().map(|s| {
|
||||
if s == "&" {
|
||||
return "and".to_string();
|
||||
return "and".to_owned();
|
||||
}
|
||||
|
||||
let chars = s
|
||||
@@ -138,7 +151,7 @@ pub fn normalize_fs_name(input: &str) -> String {
|
||||
chars.collect()
|
||||
}
|
||||
});
|
||||
Itertools::intersperse(chars, " ".to_string()).collect()
|
||||
Itertools::intersperse(chars, " ".to_owned()).collect()
|
||||
}
|
||||
|
||||
/// Convert a media title to a url compatible string
|
||||
@@ -153,11 +166,13 @@ pub fn normalize_fs_name(input: &str) -> String {
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `input` is not ASCII.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn make_web_slug(title: &str) -> String {
|
||||
let words = split_normalized_words(title);
|
||||
let (_, words) = split_leading_article(words);
|
||||
|
||||
Itertools::intersperse(words, "-".to_string()).collect()
|
||||
Itertools::intersperse(words, "-".to_owned()).collect()
|
||||
}
|
||||
|
||||
/// Convert a media title and year to a url compatible string
|
||||
@@ -172,11 +187,31 @@ pub fn make_web_slug(title: &str) -> String {
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `input` is not ASCII.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn make_web_slug_year(title: &str, year: i32) -> String {
|
||||
let words = split_normalized_words(title);
|
||||
let (_, words) = split_leading_article(words);
|
||||
|
||||
Itertools::intersperse(words, "-".to_string())
|
||||
Itertools::intersperse(words, "-".to_owned())
|
||||
.chain([format!("-{year}")])
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
make_fs_slug, make_fs_slug_year, make_sortable_title, make_web_slug, make_web_slug_year,
|
||||
normalize_fs_name,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn use_fn() {
|
||||
drop(make_sortable_title(""));
|
||||
drop(make_fs_slug(""));
|
||||
drop(make_fs_slug_year("", 0));
|
||||
drop(normalize_fs_name(""));
|
||||
drop(make_web_slug(""));
|
||||
drop(make_web_slug_year("", 0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user