/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ use anyhow::{Context, Result}; use bincode::{Decode, Encode}; use ebml_struct::{ ids::*, matroska::*, read::{EbmlReadExt, TagRead}, }; use jellybase::{ assetfed::AssetInner, cache::{cache_file, cache_memory}, common::Asset, }; use log::{info, warn}; use std::{ fs::File, io::{BufReader, ErrorKind, Read, Write}, path::Path, sync::Arc, }; pub use ebml_struct::matroska::TrackEntry as MatroskaTrackEntry; #[derive(Debug, Encode, Decode, Clone)] pub struct MatroskaMetadata { pub info: Option, pub tracks: Option, pub cover: Option, pub chapters: Option, pub tags: Option, pub infojson: Option>, } pub fn checked_matroska_metadata(path: &Path) -> Result>> { cache_memory("mkmeta-check-v1", path, || { let mut magic = [0; 4]; File::open(path)?.read_exact(&mut magic).ok(); if !matches!(magic, [0x1A, 0x45, 0xDF, 0xA3]) { return Ok(None); } Ok(Some((*matroska_metadata(path)?).clone())) }) } pub fn matroska_metadata(path: &Path) -> Result> { cache_memory("mkmeta-v3", path, || { info!("reading {path:?}"); let mut file = BufReader::new(File::open(path)?); let mut file = file.by_ref().take(u64::MAX); let (x, mut ebml) = file.read_tag()?; assert_eq!(x, EL_EBML); let ebml = Ebml::read(&mut ebml).unwrap(); assert!(ebml.doc_type == "matroska" || ebml.doc_type == "webm"); let (x, mut segment) = file.read_tag()?; assert_eq!(x, EL_SEGMENT); let mut info = None; let mut infojson = None; let mut tracks = None; let mut cover = None; let mut chapters = None; let mut tags = None; loop { let (x, mut seg) = match segment.read_tag() { Ok(o) => o, Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, Err(e) => return Err(e.into()), }; match x { EL_INFO => info = Some(Info::read(&mut seg).context("info")?), EL_TRACKS => tracks = Some(Tracks::read(&mut seg).context("tracks")?), EL_CHAPTERS => chapters = Some(Chapters::read(&mut seg).context("chapters")?), EL_TAGS => tags = Some(Tags::read(&mut seg).context("tags")?), EL_ATTACHMENTS => { let attachments = Attachments::read(&mut seg).context("attachments")?; for f in attachments.files { match f.name.as_str() { "info.json" => { infojson = Some(f.data); } "cover.webp" | "cover.png" | "cover.jpg" | "cover.jpeg" | "cover.avif" => { cover = Some( AssetInner::Cache(cache_file( "att-cover", path, move |mut file| { file.write_all(&f.data)?; Ok(()) }, )?) .ser(), ) } _ => (), } } } EL_VOID | EL_CRC32 | EL_CUES | EL_SEEKHEAD => { seg.consume()?; } EL_CLUSTER => { break; } id => { warn!("unknown top-level element {id:x}"); seg.consume()?; } } } Ok(MatroskaMetadata { chapters, cover, info, infojson, tags, tracks, }) }) }