Improve title handling and rework catalog builder

This commit is contained in:
2026-08-25 20:16:42 -06:00
parent d5a05f532f
commit 1b6f7c3bf0
143 changed files with 1188 additions and 1186 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "flix-mux"
version = "0.0.19"
version = "0.0.20"
license-file.workspace = true
description = "CLI for bulk media muxing"
+26 -4
View File
@@ -5,6 +5,13 @@ use anyhow::{Context as _, Result};
use crate::model::MediaFile;
use crate::parser::{Matcher, Selector, StreamFlag, StreamType};
#[derive(Default)]
struct OutputIndex {
video: usize,
audio: usize,
subtitle: usize,
}
pub fn mux_files<T>(
dry_run: bool,
files: &[MediaFile],
@@ -40,14 +47,16 @@ fn mux(dry_run: bool, file: &MediaFile, selectors: &[Selector]) -> Result<()> {
}
command = command.args(["-c:v", "copy", "-c:a", "copy", "-c:s", "mov_text"]);
command = command.args(["-strict", "-2"]);
command = command.args(["-movflags", "faststart+disable_chpl+write_colr"]);
command = command.args(["-map_chapters", "-1"]);
command = command.args(["-map_metadata", "-1"]);
command = command.args(["-metadata:g", "encoding_tool=Skrundzflix"]);
let mut ouput_index = OutputIndex::default();
for selector in selectors {
command = command.args(
make_metadata_args(file, selector)
make_metadata_args(file, selector, &mut ouput_index)
.with_context(|| format!("Failed to mux {:?}", file.path))?,
);
}
@@ -91,9 +100,16 @@ fn make_map_args(file: &MediaFile, selector: &Selector) -> Result<Vec<String>> {
])
}
fn make_metadata_args(file: &MediaFile, selector: &Selector) -> Result<Vec<String>> {
fn make_metadata_args(
file: &MediaFile,
selector: &Selector,
index: &mut OutputIndex,
) -> Result<Vec<String>> {
let stream_type = selector.stream_type.as_ref();
let Some(stream_index) = find_stream_index(file, selector) else {
let stream_language = selector.language();
// Bail early if the stream is not selected
let Some(_) = find_stream_index(file, selector) else {
if selector.optional {
return Ok(vec![]);
} else {
@@ -101,7 +117,13 @@ fn make_metadata_args(file: &MediaFile, selector: &Selector) -> Result<Vec<Strin
}
};
let stream_language = selector.language();
let counter = match selector.stream_type {
StreamType::Video => &mut index.video,
StreamType::Audio => &mut index.audio,
StreamType::Subtitle => &mut index.subtitle,
};
let stream_index = *counter;
*counter = counter.saturating_add(1);
let mut args = vec![
format!("-metadata:s:{}:{}", stream_type, stream_index),
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "flix-cli"
version = "0.0.19"
version = "0.0.20"
license-file.workspace = true
description = "CLI for interacting with a flix database"
+10 -1
View File
@@ -50,10 +50,19 @@ pub async fn add(
let fs_slug = overrides
.fs_slug
.unwrap_or_else(|| text::make_fs_slug(&title));
let web_slug = overrides
let mut web_slug = overrides
.web_slug
.unwrap_or_else(|| text::make_web_slug(&title));
const COLLECTION_SUFFIX_TO_REMOVE: &str = "-collection";
if web_slug.ends_with(COLLECTION_SUFFIX_TO_REMOVE) {
web_slug.truncate(
web_slug
.len()
.saturating_sub(COLLECTION_SUFFIX_TO_REMOVE.len()),
);
}
let result: Result<CollectionId, TransactionError<DbErr>> = db
.transaction(|txn| {
let title = title.clone();
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "flix-db"
version = "0.0.19"
version = "0.0.20"
license-file.workspace = true
description = "Types for storing persistent data about media"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "flix"
version = "0.0.19"
version = "0.0.20"
license-file.workspace = true
description = "Mechanisms for interacting with flix media"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "flix-fs"
version = "0.0.19"
version = "0.0.20"
license-file.workspace = true
description = "Filesystem scanner for flix media"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "flix-model"
version = "0.0.19"
version = "0.0.20"
license-file.workspace = true
description = "Core types for flix data"
+15 -1
View File
@@ -15,6 +15,10 @@ fn split_normalized_words(input: &str) -> impl Iterator<Item = String> {
input
.split_ascii_whitespace()
.map(|s| {
if s == "&" {
return "and".to_string();
}
let chars = s
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
@@ -30,7 +34,7 @@ fn split_normalized_words(input: &str) -> impl Iterator<Item = String> {
chars.collect()
}
})
.filter(|part: &String| !part.is_empty())
.filter(|part: &String| !part.is_empty() && part != "-")
}
fn split_leading_article<I: Iterator<Item = String>>(iter: I) -> (Option<String>, Peekable<I>) {
@@ -48,6 +52,7 @@ fn split_leading_article<I: Iterator<Item = String>>(iter: I) -> (Option<String>
/// assert_eq!(make_sortable_title("The Matrix"), "matrix, the");
/// assert_eq!(make_sortable_title("Marvel's Agents of S.H.I.E.L.D."), "marvels agents of shield");
/// assert_eq!(make_sortable_title("Avatar: The Last Airbender"), "avatar the last airbender");
/// assert_eq!(make_sortable_title("Cloak & Dagger"), "cloak and dagger");
///
/// # Panics
///
@@ -71,6 +76,7 @@ pub fn make_sortable_title(title: &str) -> String {
/// assert_eq!(make_fs_slug("The Matrix"), "matrix");
/// assert_eq!(make_fs_slug("Marvel's Agents of S.H.I.E.L.D."), "marvels agents of shield");
/// assert_eq!(make_fs_slug("Avatar: The Last Airbender"), "avatar the last airbender");
/// assert_eq!(make_fs_slug("Cloak & Dagger"), "cloak and dagger");
///
/// # Panics
///
@@ -89,6 +95,7 @@ pub fn make_fs_slug(title: &str) -> String {
/// assert_eq!(make_fs_slug_year("The Matrix", 1999), "matrix (1999)");
/// assert_eq!(make_fs_slug_year("Marvel's Agents of S.H.I.E.L.D.", 2013), "marvels agents of shield (2013)");
/// assert_eq!(make_fs_slug_year("Avatar: The Last Airbender", 2005), "avatar the last airbender (2005)");
/// assert_eq!(make_fs_slug_year("Cloak & Dagger", 2018), "cloak and dagger (2018)");
///
/// # Panics
///
@@ -109,8 +116,13 @@ pub fn make_fs_slug_year(title: &str, year: i32) -> String {
/// assert_eq!(normalize_fs_name("Matrix (1999)"), "matrix (1999)");
/// 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)");
pub fn normalize_fs_name(input: &str) -> String {
let chars = input.split_ascii_whitespace().map(|s| {
if s == "&" {
return "and".to_string();
}
let chars = s
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '(' || *c == ')')
@@ -136,6 +148,7 @@ pub fn normalize_fs_name(input: &str) -> String {
/// assert_eq!(make_web_slug("The Matrix"), "matrix");
/// assert_eq!(make_web_slug("Marvel's Agents of S.H.I.E.L.D."), "marvels-agents-of-shield");
/// assert_eq!(make_web_slug("Avatar: The Last Airbender"), "avatar-the-last-airbender");
/// assert_eq!(make_web_slug("Cloak & Dagger"), "cloak-and-dagger");
///
/// # Panics
///
@@ -154,6 +167,7 @@ pub fn make_web_slug(title: &str) -> String {
/// assert_eq!(make_web_slug_year("The Matrix", 1999), "matrix-1999");
/// assert_eq!(make_web_slug_year("Marvel's Agents of S.H.I.E.L.D.", 2013), "marvels-agents-of-shield-2013");
/// assert_eq!(make_web_slug_year("Avatar: The Last Airbender", 2005), "avatar-the-last-airbender-2005");
/// assert_eq!(make_web_slug_year("Cloak & Dagger", 2018), "cloak-and-dagger-2018");
///
/// # Panics
///
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "flix-tmdb"
version = "0.0.19"
version = "0.0.20"
license-file.workspace = true
description = "Clients and models for fetching data from TMDB"