aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2023-12-17 20:58:10 +0100
committermetamuffin <metamuffin@disroot.org>2023-12-17 20:58:10 +0100
commit2a26a6030e25127a3967dd5bccba1fb05d485ab1 (patch)
treedb2ecba63bde78804277ec66915dc3b4512a4991
parente7155ec05458ed00035ce8bc5c087306cfeedbc1 (diff)
downloadjellything-2a26a6030e25127a3967dd5bccba1fb05d485ab1.tar
jellything-2a26a6030e25127a3967dd5bccba1fb05d485ab1.tar.bz2
jellything-2a26a6030e25127a3967dd5bccba1fb05d485ab1.tar.zst
chapters
-rw-r--r--common/src/lib.rs9
-rw-r--r--remuxer/src/import/mod.rs82
-rw-r--r--server/src/routes/ui/node.rs19
-rw-r--r--tool/src/import/mod.rs11
-rw-r--r--tool/src/main.rs3
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,