aboutsummaryrefslogtreecommitdiff
path: root/remuxer/src/metadata.rs
blob: ddcf4c09b994da7b3f4b29e43fd13a69c931b669 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/*
    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 <metamuffin.org>
*/
use anyhow::{Context, Result};
use bincode::{Decode, Encode};
use ebml_struct::{
    ids::*,
    matroska::*,
    read::{EbmlReadExt, TagRead},
};
use jellycache::{cache_file, cache_memory, CachePath};
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<Info>,
    pub tracks: Option<Tracks>,
    pub cover: Option<CachePath>,
    pub chapters: Option<Chapters>,
    pub tags: Option<Tags>,
    pub infojson: Option<Vec<u8>>,
}
pub fn checked_matroska_metadata(path: &Path) -> Result<Arc<Option<MatroskaMetadata>>> {
    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<Arc<MatroskaMetadata>> {
    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(cache_file("att-cover", path, move |mut file| {
                                    file.write_all(&f.data)?;
                                    Ok(())
                                })?)
                            }
                            _ => (),
                        }
                    }
                }
                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,
        })
    })
}