1use crate::{
2 error::{Error, Result},
3 metadata::{EpisodeMetadata, NormalEpisodeMetadata, SpecialEpisodeMetadata},
4};
5use rsubs_lib::{
6 srt::{SRTFile, SRTLine},
7 ssa::{SSAEvent, SSAFile, SSAStyle},
8 util::{
9 color::{
10 Alignment, Color,
11 ColorType::{self, SSAColor},
12 },
13 time::Time,
14 },
15 vtt::{VTTFile, VTTLine, VTTStyle},
16 Subtitle,
17};
18use serde::{Deserialize, Serialize};
19use std::{collections::HashMap, fs, path::Path};
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
39pub struct JsonSubtitle {
40 pub body: Vec<JsonSubtitleBody>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
45pub struct JsonSubtitleBody {
46 pub from: f32,
47 pub to: f32,
48 pub content: String,
49}
50
51#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
53pub enum SubtitleType {
54 Hard,
55 #[default]
56 Soft,
57}
58
59#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
61pub enum SubtitleFormat {
62 #[default]
63 Json,
64 Ssa,
65 Srt,
66 Vtt,
67}
68
69impl JsonSubtitle {
70 pub fn new_from_episode(
72 episode: &EpisodeMetadata<impl AsRef<Path>>,
73 subtitle_language: &str,
74 ) -> Result<Self> {
75 match episode {
76 EpisodeMetadata::Normal(e) => Self::new_from_normal_episode(e, subtitle_language),
77 EpisodeMetadata::Special(e) => Self::new_from_special_episode(e, subtitle_language),
78 }
79 }
80
81 pub fn new_from_normal_episode(
83 episode: &NormalEpisodeMetadata<impl AsRef<Path>>,
84 subtitle_language: &str,
85 ) -> Result<Self> {
86 let subtitle_path = episode
87 .path
88 .as_ref()
89 .ok_or(format!(
90 "Episode {} of {} doesn't have a path.",
91 episode.episode, episode.title
92 ))?
93 .as_ref()
94 .join(subtitle_language)
95 .read_dir()?
96 .next()
97 .ok_or("Subtitle directory is empty.")??
98 .path();
99
100 Self::new_from_path(subtitle_path)
101 }
102
103 pub fn new_from_special_episode(
105 episode: &SpecialEpisodeMetadata<impl AsRef<Path>>,
106 subtitle_language: &str,
107 ) -> Result<Self> {
108 let subtitle_path = episode
109 .path
110 .as_ref()
111 .ok_or(format!(
112 "{} of {} doesn't have a path.",
113 episode.episode_name, episode.title
114 ))?
115 .as_ref()
116 .join(subtitle_language)
117 .read_dir()?
118 .next()
119 .ok_or("Subtitle directory is empty.")??
120 .path();
121
122 Self::new_from_path(subtitle_path)
123 }
124
125 pub fn new_from_path(path: impl AsRef<Path>) -> Result<Self> {
127 let json_string = fs::read_to_string(path)?;
128
129 Ok(serde_json::from_str(&json_string)?)
130 }
131
132 pub fn to_subtitle(self) -> Subtitle {
134 self.into()
135 }
136
137 pub fn to_ssa(self) -> SSAFile {
139 self.into()
140 }
141
142 pub fn to_srt(self) -> SRTFile {
144 self.into()
145 }
146
147 pub fn to_vtt(self) -> VTTFile {
149 self.into()
150 }
151}
152
153impl From<JsonSubtitle> for Subtitle {
154 fn from(value: JsonSubtitle) -> Self {
155 let mut ass_info = HashMap::new();
156 ass_info.insert("Title".into(), "Bilibili Subtitle".into());
157 ass_info.insert("ScriptType".into(), "v4.00+".into());
158 ass_info.insert("WrapStyle".into(), "0".into());
159 ass_info.insert("ScaledBorderAndShadow".into(), "yes".into());
160 ass_info.insert("YCbCr Matrix".into(), "TV.601".into());
161 ass_info.insert("PlayResX".into(), "1920".into());
162 ass_info.insert("PlayResY".into(), "1080".into());
163
164 let ass_styles = SSAStyle {
165 name: "Default".into(),
166 fontname: "Noto Sans".into(),
167 fontsize: 100.,
168 firstcolor: SSAColor(Color {
169 a: 0,
170 r: 255,
171 g: 255,
172 b: 255,
173 }),
174 secondcolor: SSAColor(Color {
175 r: 0,
176 g: 255,
177 b: 255,
178 a: 0,
179 }),
180 outlinecolor: SSAColor(Color {
181 r: 0,
182 g: 0,
183 b: 0,
184 a: 0,
185 }),
186 backgroundcolor: SSAColor(Color {
187 r: 0,
188 g: 0,
189 b: 0,
190 a: 0,
191 }),
192 bold: false,
193 italic: true,
194 unerline: true,
195 strikeout: true,
196 scalex: 100.,
197 scaley: 100.,
198 spacing: 0.,
199 angle: 0.,
200 borderstyle: 1,
201 outline: 3.,
202 shadow: 0.,
203 alignment: Alignment::BottomCenter,
204 lmargin: 96,
205 rmargin: 96,
206 vmargin: 65,
207 encoding: 1,
208 ..Default::default()
209 };
210
211 let mut ass_event = vec![];
212
213 value.body.iter().for_each(|b| {
214 ass_event.push(SSAEvent {
215 style: "Default".into(),
216 line_start: Time {
217 ms: (b.from * 1000.) as u32,
218 ..Default::default()
219 },
220 line_end: Time {
221 ms: (b.to * 1000.) as u32,
222 ..Default::default()
223 },
224 line_text: b.content.clone(),
225 ..Default::default()
226 })
227 });
228
229 Subtitle::SSA(Some(SSAFile {
230 events: ass_event,
231 styles: vec![ass_styles],
232 info: ass_info,
233 format: ".ass".into(),
234 }))
235 }
236}
237
238impl From<JsonSubtitle> for SSAFile {
239 fn from(value: JsonSubtitle) -> Self {
240 let mut ass_info = HashMap::new();
241 ass_info.insert("Title".into(), "Bilibili Subtitle".into());
242 ass_info.insert("ScriptType".into(), "v4.00+".into());
243 ass_info.insert("WrapStyle".into(), "0".into());
244 ass_info.insert("ScaledBorderAndShadow".into(), "yes".into());
245 ass_info.insert("YCbCr Matrix".into(), "TV.601".into());
246 ass_info.insert("PlayResX".into(), "1920".into());
247 ass_info.insert("PlayResY".into(), "1080".into());
248
249 let ass_styles = SSAStyle {
250 name: "Default".into(),
251 fontname: "Noto Sans".into(),
252 fontsize: 100.,
253 firstcolor: SSAColor(Color {
254 a: 0,
255 r: 255,
256 g: 255,
257 b: 255,
258 }),
259 secondcolor: SSAColor(Color {
260 r: 0,
261 g: 255,
262 b: 255,
263 a: 0,
264 }),
265 outlinecolor: SSAColor(Color {
266 r: 0,
267 g: 0,
268 b: 0,
269 a: 0,
270 }),
271 backgroundcolor: SSAColor(Color {
272 r: 0,
273 g: 0,
274 b: 0,
275 a: 0,
276 }),
277 bold: false,
278 italic: true,
279 unerline: true,
280 strikeout: true,
281 scalex: 100.,
282 scaley: 100.,
283 spacing: 0.,
284 angle: 0.,
285 borderstyle: 1,
286 outline: 3.,
287 shadow: 0.,
288 alignment: Alignment::BottomCenter,
289 lmargin: 96,
290 rmargin: 96,
291 vmargin: 65,
292 encoding: 1,
293 ..Default::default()
294 };
295
296 let mut ass_event = vec![];
297
298 value.body.iter().for_each(|b| {
299 ass_event.push(SSAEvent {
300 style: "Default".into(),
301 line_start: Time {
302 ms: (b.from * 1000.) as u32,
303 ..Default::default()
304 },
305 line_end: Time {
306 ms: (b.to * 1000.) as u32,
307 ..Default::default()
308 },
309 line_text: b.content.clone(),
310 ..Default::default()
311 })
312 });
313
314 SSAFile {
315 events: ass_event,
316 styles: vec![ass_styles],
317 info: ass_info,
318 format: ".ass".into(),
319 }
320 }
321}
322
323impl From<JsonSubtitle> for SRTFile {
324 fn from(value: JsonSubtitle) -> Self {
325 let mut srt_line = vec![];
326
327 for (i, b) in value.body.iter().enumerate() {
328 srt_line.push(SRTLine {
329 line_number: (i + 1) as i32,
330 line_text: b.content.clone(),
331 line_start: Time {
332 ms: (b.from * 1000.) as u32,
333 ..Default::default()
334 },
335 line_end: Time {
336 ms: (b.to * 1000.) as u32,
337 ..Default::default()
338 },
339 });
340 }
341
342 SRTFile { lines: srt_line }
343 }
344}
345
346impl From<JsonSubtitle> for VTTFile {
347 fn from(value: JsonSubtitle) -> Self {
348 let vtt_style = VTTStyle {
349 name: Some("Default".into()),
350 font_family: "Noto Sans".into(),
351 font_size: "100".into(),
352 color: ColorType::VTTColor(Color {
353 r: 255,
354 g: 255,
355 b: 255,
356 a: 0,
357 }),
358 background_color: ColorType::VTTColor(Color {
359 r: 0,
360 g: 0,
361 b: 0,
362 a: 127,
363 }),
364 ..Default::default()
365 };
366
367 let mut vtt_lines = vec![];
368
369 for (i, b) in value.body.iter().enumerate() {
370 vtt_lines.push(VTTLine {
371 line_number: i.to_string(),
372 style: Some("Default".into()),
373 line_start: Time {
374 ms: (b.from * 1000.) as u32,
375 ..Default::default()
376 },
377 line_end: Time {
378 ms: (b.to * 1000.) as u32,
379 ..Default::default()
380 },
381 position: None,
382 line_text: b.content.clone(),
383 })
384 }
385
386 VTTFile {
387 styles: vec![vtt_style],
388 lines: vtt_lines,
389 }
390 }
391}
392
393impl SubtitleFormat {
394 pub fn get_episode_subtitle_type(
396 episode: &EpisodeMetadata<impl AsRef<Path>>,
397 subtitle_language: &str,
398 ) -> Result<Self> {
399 match episode {
400 EpisodeMetadata::Normal(e) => {
401 Self::get_normal_episode_subtitle_type(e, subtitle_language)
402 }
403 EpisodeMetadata::Special(e) => {
404 Self::get_special_episode_subtitle_type(e, subtitle_language)
405 }
406 }
407 }
408
409 pub fn get_normal_episode_subtitle_type(
411 episode: &NormalEpisodeMetadata<impl AsRef<Path>>,
412 subtitle_language: &str,
413 ) -> Result<Self> {
414 let subtitle_path = episode
415 .path
416 .as_ref()
417 .ok_or(format!(
418 "Episode {} of {} doesn't have a path.",
419 episode.episode, episode.title
420 ))?
421 .as_ref()
422 .join(subtitle_language)
423 .read_dir()?
424 .next()
425 .ok_or("Subtitle directory is empty")??
426 .path();
427 let extension = subtitle_path.extension().ok_or(format!(
428 "Subtitle {} has no extension.",
429 subtitle_path.display()
430 ))?;
431
432 match extension
433 .to_str()
434 .ok_or("OsStr doesn't yeild valid Unicode.")?
435 {
436 "json" => Ok(Self::Json),
437 "ass" | "ssa" => Ok(Self::Ssa),
438 "srt" => Ok(Self::Srt),
439 "vtt" => Ok(Self::Vtt),
440 _ => Err(Error::FromString(format!(
441 "Invalid extension: {}",
442 subtitle_path.display()
443 ))),
444 }
445 }
446
447 pub fn get_special_episode_subtitle_type(
449 episode: &SpecialEpisodeMetadata<impl AsRef<Path>>,
450 subtitle_language: &str,
451 ) -> Result<Self> {
452 let subtitle_path = episode
453 .path
454 .as_ref()
455 .ok_or(format!(
456 "{} of {} doesn't have a path.",
457 episode.episode_name, episode.title
458 ))?
459 .as_ref()
460 .join(subtitle_language)
461 .read_dir()?
462 .next()
463 .ok_or("Subtitle directory is empty.")??
464 .path();
465 let extension = subtitle_path.extension().ok_or(format!(
466 "Subtitle {} has no extension.",
467 subtitle_path.display()
468 ))?;
469
470 match extension
471 .to_str()
472 .ok_or("OsStr doesn't yeild valid Unicode.")?
473 {
474 "json" => Ok(Self::Json),
475 "ass" | "ssa" => Ok(Self::Ssa),
476 "srt" => Ok(Self::Srt),
477 "vtt" => Ok(Self::Vtt),
478 _ => Err(Error::FromString(format!(
479 "Invalid extension: {}",
480 subtitle_path.display()
481 ))),
482 }
483 }
484}