ttml_processor/generator/
mod.rs1mod 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
19pub 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 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
60fn 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 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 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 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 namespace_attrs.sort_by_key(|&(key, _)| key);
116
117 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}