Skip to main content

ttml_processor/generator/
mod.rs

1//! # Timed Text Markup Language 歌词格式生成器
2//!
3//! 注意:该模块设计上仅用于生成 Apple Music 和 AMLL 使用的 TTML 歌词文件,
4//! 无法用于生成通用的 TTML 字幕文件。
5
6mod body;
7mod head;
8mod track;
9mod utils;
10
11use std::io::Cursor;
12
13use lyrics_helper_core::{
14    AgentStore, CanonicalMetadataKey, ConvertError, LyricLine, MetadataStore,
15    TtmlGenerationOptions, TtmlTimingMode,
16};
17use quick_xml::Writer;
18
19/// TTML 生成的主入口函数。
20///
21/// # 参数
22/// * `lines` - 歌词行数据切片。
23/// * `metadata_store` - 规范化后的元数据存储。
24/// * `agent_store` - 代理信息存储,用于生成歌手标识。
25/// * `options` - TTML 生成选项,控制输出格式和规则。
26///
27/// # 返回
28///
29/// * `Ok(String)` - 成功生成的 TTML 字符串。
30///
31/// # Errors
32///
33/// 如果在生成 XML 或将结果转换为字符串时发生错误(例如 I/O 错误或 UTF-8 编码问题),
34/// 则会返回 `ConvertError`。
35pub fn generate_ttml(
36    lines: &[LyricLine],
37    metadata_store: &MetadataStore,
38    agent_store: &AgentStore,
39    options: &TtmlGenerationOptions,
40) -> Result<String, ConvertError> {
41    let mut buffer = Vec::new();
42    let indent_char = b' ';
43    let indent_size = 2;
44
45    // 决定是否输出格式化的 TTML
46    let result = if options.format {
47        let mut writer =
48            Writer::new_with_indent(Cursor::new(&mut buffer), indent_char, indent_size);
49        generate_ttml_inner(&mut writer, lines, metadata_store, agent_store, options)
50    } else {
51        let mut writer = Writer::new(Cursor::new(&mut buffer));
52        generate_ttml_inner(&mut writer, lines, metadata_store, agent_store, options)
53    };
54
55    result?;
56
57    String::from_utf8(buffer).map_err(ConvertError::FromUtf8)
58}
59
60/// TTML 生成的核心内部逻辑。
61fn generate_ttml_inner<W: std::io::Write>(
62    writer: &mut Writer<W>,
63    lines: &[LyricLine],
64    metadata_store: &MetadataStore,
65    agent_store: &AgentStore,
66    options: &TtmlGenerationOptions,
67) -> Result<(), ConvertError> {
68    // 准备根元素的属性
69    let mut namespace_attrs: Vec<(&str, String)> = Vec::new();
70    namespace_attrs.push(("xmlns", "http://www.w3.org/ns/ttml".to_string()));
71    namespace_attrs.push((
72        "xmlns:ttm",
73        "http://www.w3.org/ns/ttml#metadata".to_string(),
74    ));
75    namespace_attrs.push((
76        "xmlns:itunes",
77        "http://music.apple.com/lyric-ttml-internal".to_string(),
78    ));
79
80    let amll_keys_to_check_for_namespace = [
81        CanonicalMetadataKey::Title,
82        CanonicalMetadataKey::Artist,
83        CanonicalMetadataKey::Album,
84        CanonicalMetadataKey::Isrc,
85        CanonicalMetadataKey::AppleMusicId,
86        CanonicalMetadataKey::NcmMusicId,
87        CanonicalMetadataKey::QqMusicId,
88        CanonicalMetadataKey::SpotifyId,
89        CanonicalMetadataKey::TtmlAuthorGithub,
90        CanonicalMetadataKey::TtmlAuthorGithubLogin,
91    ];
92    if amll_keys_to_check_for_namespace
93        .iter()
94        .any(|key| metadata_store.get_multiple_values(key).is_some())
95    {
96        namespace_attrs.push(("xmlns:amll", "http://www.example.com/ns/amll".to_string()));
97    }
98
99    // 设置主语言属性
100    let lang_attr = options
101        .main_language
102        .as_ref()
103        .or_else(|| metadata_store.get_single_value(&CanonicalMetadataKey::Language))
104        .filter(|s| !s.is_empty())
105        .map(|lang| ("xml:lang", lang.clone()));
106
107    // 设置 itunes:timing 属性
108    let timing_mode_str = match options.timing_mode {
109        TtmlTimingMode::Word => "Word",
110        TtmlTimingMode::Line => "Line",
111    };
112    let timing_attr = ("itunes:timing", timing_mode_str.to_string());
113
114    // 属性排序以保证输出稳定
115    namespace_attrs.sort_by_key(|&(key, _)| key);
116
117    // 写入 <tt> 根元素
118    let mut element_writer = writer.create_element("tt");
119
120    for (i, (key, value)) in namespace_attrs.iter().enumerate() {
121        if i > 0 {
122            element_writer = element_writer.new_line();
123        }
124        element_writer = element_writer.with_attribute((*key, value.as_str()));
125    }
126
127    element_writer = element_writer
128        .new_line()
129        .with_attribute((timing_attr.0, timing_attr.1.as_str()));
130
131    if let Some((key, value)) = &lang_attr {
132        element_writer = element_writer
133            .new_line()
134            .with_attribute((*key, value.as_str()));
135    }
136
137    element_writer.write_inner_content(|writer| {
138        head::write_ttml_head(writer, metadata_store, lines, agent_store, options)?;
139        body::write_ttml_body(writer, lines, options)?;
140        Ok(())
141    })?;
142
143    Ok(())
144}