1use crate::{error::Result, metadata::EpisodeMetadata};
2use rsubs_lib::{
3 srt::{SRTFile, SRTLine},
4 ssa::{SSAEvent, SSAFile, SSAStyle},
5 util::{
6 color::{Alignment, Color, ColorType},
7 time::Time,
8 },
9 vtt::{VTTFile, VTTLine, VTTStyle},
10 Subtitle,
11};
12use serde::{Deserialize, Serialize};
13use std::{collections::HashMap, fs, path::Path};
14
15macro_rules! new_ssa_subtitile {
16 ($value: expr) => {{
17 let mut ass_info = HashMap::new();
18 ass_info.insert("Title".into(), "Bilibili Subtitle".into());
19 ass_info.insert("ScriptType".into(), "v4.00+".into());
20 ass_info.insert("WrapStyle".into(), "0".into());
21 ass_info.insert("ScaledBorderAndShadow".into(), "yes".into());
22 ass_info.insert("YCbCr Matrix".into(), "TV.601".into());
23 ass_info.insert("PlayResX".into(), "1920".into());
24 ass_info.insert("PlayResY".into(), "1080".into());
25
26 let ass_styles = SSAStyle {
27 name: "Default".into(),
28 fontsize: 70.,
29 bold: false, borderstyle: 1,
32 outline: 5.,
33 alignment: Alignment::BottomCenter,
34 vmargin: 30,
35 ..Default::default()
36 };
37
38 let mut ass_event = vec![];
39
40 $value.body.iter().for_each(|b| {
41 ass_event.push(SSAEvent {
42 style: "Default".into(),
43 line_start: Time {
44 ms: (b.from * 1000.) as u32,
45 ..Default::default()
46 },
47 line_end: Time {
48 ms: (b.to * 1000.) as u32,
49 ..Default::default()
50 },
51 line_text: b.content.clone().replace("\n", "\\N"),
52 ..Default::default()
53 })
54 });
55
56 SSAFile {
57 events: ass_event,
58 styles: vec![ass_styles],
59 info: ass_info,
60 format: ".ass".into(),
61 }
62 }};
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
83pub struct JsonSubtitle {
84 pub body: Vec<JsonSubtitleBody>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
89pub struct JsonSubtitleBody {
90 pub from: f32,
91 pub to: f32,
92 pub content: String,
93}
94
95#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
97pub enum SubtitleType {
98 Hard,
99 #[default]
100 Soft,
101}
102
103#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
105pub enum SubtitleFormat {
106 #[default]
107 Json,
108 Ssa,
109 Srt,
110 Vtt,
111}
112
113impl JsonSubtitle {
114 pub fn new_from_episode(episode: &EpisodeMetadata, subtitle_language: &str) -> Result<Self> {
116 let subtitle_path = episode
117 .path
118 .join(subtitle_language)
119 .read_dir()?
120 .next()
121 .ok_or("Subtitle directory is empty.")??
122 .path();
123
124 Self::new_from_path(subtitle_path)
125 }
126
127 pub fn new_from_path(path: impl AsRef<Path>) -> Result<Self> {
129 let json_string = fs::read_to_string(path)?;
130
131 Ok(serde_json::from_str(&json_string)?)
132 }
133
134 pub fn to_subtitle(self) -> Subtitle {
136 self.into()
137 }
138
139 pub fn to_ssa(self) -> SSAFile {
141 self.into()
142 }
143
144 pub fn to_srt(self) -> SRTFile {
146 self.into()
147 }
148
149 pub fn to_vtt(self) -> VTTFile {
151 self.into()
152 }
153}
154
155impl From<JsonSubtitle> for Subtitle {
156 fn from(value: JsonSubtitle) -> Self {
157 let ssa_subtitle = new_ssa_subtitile!(value);
158
159 Subtitle::SSA(Some(ssa_subtitle))
160 }
161}
162
163impl From<JsonSubtitle> for SSAFile {
164 fn from(value: JsonSubtitle) -> Self {
165 new_ssa_subtitile!(value)
166 }
167}
168
169impl From<JsonSubtitle> for SRTFile {
170 fn from(value: JsonSubtitle) -> Self {
171 let mut srt_line = vec![];
172
173 for (i, b) in value.body.iter().enumerate() {
174 srt_line.push(SRTLine {
175 line_number: (i + 1) as i32,
176 line_text: b.content.clone(),
177 line_start: Time {
178 ms: (b.from * 1000.) as u32,
179 ..Default::default()
180 },
181 line_end: Time {
182 ms: (b.to * 1000.) as u32,
183 ..Default::default()
184 },
185 });
186 }
187
188 SRTFile { lines: srt_line }
189 }
190}
191
192impl From<JsonSubtitle> for VTTFile {
193 fn from(value: JsonSubtitle) -> Self {
194 let vtt_style = VTTStyle {
195 name: Some("Default".into()),
196 font_family: "Noto Sans".into(),
197 font_size: "100".into(),
198 color: ColorType::VTTColor(Color {
199 r: 255,
200 g: 255,
201 b: 255,
202 a: 0,
203 }),
204 background_color: ColorType::VTTColor(Color {
205 r: 0,
206 g: 0,
207 b: 0,
208 a: 127,
209 }),
210 ..Default::default()
211 };
212
213 let mut vtt_lines = vec![];
214
215 for (i, b) in value.body.iter().enumerate() {
216 vtt_lines.push(VTTLine {
217 line_number: i.to_string(),
218 style: Some("Default".into()),
219 line_start: Time {
220 ms: (b.from * 1000.) as u32,
221 ..Default::default()
222 },
223 line_end: Time {
224 ms: (b.to * 1000.) as u32,
225 ..Default::default()
226 },
227 position: None,
228 line_text: b.content.clone(),
229 })
230 }
231
232 VTTFile {
233 styles: vec![vtt_style],
234 lines: vtt_lines,
235 }
236 }
237}
238
239impl SubtitleFormat {
240 pub fn get_episode_subtitle_type(
242 episode: &EpisodeMetadata,
243 subtitle_language: &str,
244 ) -> Result<Self> {
245 let subtitle_path = episode
246 .path
247 .join(subtitle_language)
248 .read_dir()?
249 .next()
250 .ok_or("Subtitle directory is empty")??
251 .path();
252 let extension = subtitle_path.extension().ok_or(format!(
253 "Subtitle {} has no extension.",
254 subtitle_path.display()
255 ))?;
256
257 match extension
258 .to_str()
259 .ok_or("OsStr doesn't yeild valid Unicode.")?
260 {
261 "json" => Ok(Self::Json),
262 "ass" | "ssa" => Ok(Self::Ssa),
263 "srt" => Ok(Self::Srt),
264 "vtt" => Ok(Self::Vtt),
265 _ => Err(format!("Invalid extension: {}", extension.to_string_lossy()).into()),
266 }
267 }
268}