diff options
-rw-r--r-- | common/src/lib.rs | 9 | ||||
-rw-r--r-- | remuxer/src/import/mod.rs | 82 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 19 | ||||
-rw-r--r-- | tool/src/import/mod.rs | 11 | ||||
-rw-r--r-- | tool/src/main.rs | 3 |
5 files changed, 113 insertions, 11 deletions
diff --git a/common/src/lib.rs b/common/src/lib.rs index ea19205..aedcb07 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -110,6 +110,15 @@ pub struct LocalTrack { pub struct MediaInfo { pub duration: f64, // in seconds pub tracks: Vec<SourceTrack>, + #[serde(default)] + pub chapters: Vec<Chapter>, +} + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct Chapter { + pub time_start: Option<f64>, + pub time_end: Option<f64>, + pub labels: Vec<(String, String)>, } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/remuxer/src/import/mod.rs b/remuxer/src/import/mod.rs index d2e0f3c..cf37b78 100644 --- a/remuxer/src/import/mod.rs +++ b/remuxer/src/import/mod.rs @@ -4,7 +4,7 @@ Copyright (C) 2023 metamuffin <metamuffin.org> */ use anyhow::{anyhow, bail, Context, Result}; -use jellycommon::{LocalTrack, SourceTrack, SourceTrackKind}; +use jellycommon::{Chapter, LocalTrack, SourceTrack, SourceTrackKind}; use jellymatroska::{ matroska::MatroskaTag, read::EbmlReader, @@ -13,7 +13,7 @@ use jellymatroska::{ use log::{debug, error, info, warn}; use std::{path::PathBuf, time::Instant}; -#[derive(Default)] +#[derive(Default, Debug)] pub struct MatroskaMetadata { pub title: Option<String>, pub description: Option<String>, @@ -22,6 +22,7 @@ pub struct MatroskaMetadata { pub track_sources: Vec<LocalTrack>, pub cover: Option<(String, Vec<u8>)>, pub infojson: Option<String>, + pub chapters: Vec<Chapter>, pub duration: f64, } @@ -71,8 +72,13 @@ fn import_read_segment(segment: &mut Unflatten) -> Result<MatroskaMetadata> { let (mut timestamp_scale, mut duration) = (None, None); let mut m = MatroskaMetadata::default(); - let (mut info_found, mut tags_found, mut attachments_found, mut tracks_found) = - (false, false, false, false); + let ( + mut info_found, + mut tags_found, + mut attachments_found, + mut tracks_found, + mut found_chapters, + ) = (false, false, false, false, false); while let Some(Ok(Unflat { children, item, .. })) = segment.n() { match item { @@ -175,7 +181,71 @@ fn import_read_segment(segment: &mut Unflatten) -> Result<MatroskaMetadata> { } } MatroskaTag::Cues(_) => {} - MatroskaTag::Chapters(_) => {} + MatroskaTag::Chapters(_) => { + found_chapters = true; + let mut children = children.unwrap(); + while let Some(Ok(Unflat { children, item, .. })) = children.n() { + match item { + MatroskaTag::EditionEntry(_) => { + let mut children = children.unwrap(); + while let Some(Ok(Unflat { children, item, .. })) = children.n() { + match item { + MatroskaTag::EditionUID(_) + | MatroskaTag::EditionFlagHidden(_) + | MatroskaTag::EditionFlagDefault(_) => {} + MatroskaTag::ChapterAtom(_) => { + let mut children = children.unwrap(); + let mut chap = Chapter::default(); + while let Some(Ok(Unflat { children, item, .. })) = + children.n() + { + match item { + MatroskaTag::ChapterFlagEnabled(_) + | MatroskaTag::ChapterFlagHidden(_) + | MatroskaTag::ChapterUID(_) => (), + MatroskaTag::ChapterTimeStart(t) => { + chap.time_start = Some(t as f64 * 1e-9) + } + MatroskaTag::ChapterTimeEnd(t) => { + chap.time_end = Some(t as f64 * 1e-9) + } + MatroskaTag::ChapterDisplay(_) => { + let mut string = String::new(); + let mut lang = String::new(); + let mut children = children.unwrap(); + while let Some(Ok(Unflat { item, .. })) = + children.n() + { + match item { + MatroskaTag::ChapString(s) => { + string = s + } + MatroskaTag::ChapLanguage(l) => { + lang = l + } + _ => warn!( + "(rscead) tag ignored: {item:?}" + ), + } + } + chap.labels.push((lang, string)) + } + _ => warn!("(rscea) tag ignored: {item:?}"), + } + } + m.chapters.push(chap); + } + _ => warn!("(rsce) tag ignored: {item:?}"), + } + } + if !m.chapters.is_empty() { + info!("{} chapters added", m.chapters.len()); + } + } + _ => warn!("(rsc) tag ignored: {item:?}"), + } + } + } MatroskaTag::Tracks(_) => { tracks_found = true; let mut children = children.unwrap(); @@ -276,7 +346,7 @@ fn import_read_segment(segment: &mut Unflatten) -> Result<MatroskaMetadata> { _ => warn!("(rs) tag ignored: {item:?}"), }; - if info_found && tracks_found && attachments_found && tags_found { + if info_found && tracks_found && attachments_found && tags_found && found_chapters { debug!("we found all we need, stopping read early"); break; } diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 6ce82c2..fbbf012 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -26,7 +26,7 @@ use anyhow::{anyhow, Context, Result}; use jellybase::permission::NodePermissionExt; use jellycommon::{ user::{NodeUserData, WatchedState}, - MediaInfo, NodeKind, NodePublic, Rating, SourceTrackKind, + Chapter, MediaInfo, NodeKind, NodePublic, Rating, SourceTrackKind, }; use rocket::{get, serde::json::Json, Either, State}; @@ -142,6 +142,14 @@ markup::define! { li { @format!("{track}") } }} } + @if !media.chapters.is_empty() { + details { + summary { "Chapters" } + ol { @for chap in &media.chapters { + li { @format_chapter(chap) } + }} + } + } } } @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) { @@ -280,3 +288,12 @@ fn format_count(n: impl Into<usize>) -> String { format!("{n}") } } + +fn format_chapter(c: &Chapter) -> String { + format!( + "{}-{}: {}", + c.time_start.map(format_duration).unwrap_or_default(), + c.time_end.map(format_duration).unwrap_or_default(), + c.labels.first().map(|l| l.1.clone()).unwrap_or_default() + ) +} diff --git a/tool/src/import/mod.rs b/tool/src/import/mod.rs index b4e03d6..973629c 100644 --- a/tool/src/import/mod.rs +++ b/tool/src/import/mod.rs @@ -37,6 +37,7 @@ pub(crate) fn import(action: Action, dry: bool) -> anyhow::Result<()> { video, ignore_metadata, r#move, + title, skip_existing, } => { if std::env::current_dir().unwrap().file_name().unwrap() != "library" { @@ -131,10 +132,11 @@ pub(crate) fn import(action: Action, dry: bool) -> anyhow::Result<()> { }; } - let title = tmdb_details - .as_ref() - .map(|d| d.title.clone().or(d.name.clone())) - .flatten() + let title = title + .or(tmdb_details + .as_ref() + .map(|d| d.title.clone().or(d.name.clone())) + .flatten()) .or(file_meta.as_ref().map(|m| m.title.clone()).flatten()) .expect("no title detected"); @@ -272,6 +274,7 @@ pub(crate) fn import(action: Action, dry: bool) -> anyhow::Result<()> { kind, children: Vec::new(), media: file_meta.as_ref().map(|m| MediaInfo { + chapters: m.chapters.clone(), duration: m.duration, tracks: m.tracks.clone(), }), diff --git a/tool/src/main.rs b/tool/src/main.rs index 3744560..31e63b7 100644 --- a/tool/src/main.rs +++ b/tool/src/main.rs @@ -74,6 +74,9 @@ enum Action { /// Skip any action that appears to be run already. #[arg(long)] skip_existing: bool, + /// Sets the title + #[arg(long)] + title: Option<String>, }, Migrate { database: PathBuf, |