use std::path::{Path, PathBuf};
use crate::Downloader;
use crate::download::Fetcher;
use crate::error::Error;
use crate::model::Video;
use crate::model::caption::Extension as CaptionExtension;
impl Downloader {
pub async fn download_subtitle(
&self,
video: &Video,
language_code: impl AsRef<str>,
output: impl AsRef<str>,
fallback_to_automatic: bool,
) -> crate::error::Result<PathBuf> {
let language_code = language_code.as_ref();
tracing::debug!(video_id = video.id, language = language_code, "💬 Downloading subtitle");
let output_path = self.output_dir.join(output.as_ref());
#[cfg(cache)]
if let Some(cache) = &self.cache
&& let Ok(Some((_, cached_path))) = cache.downloads.get_subtitle_by_language(&video.id, language_code).await
{
tracing::debug!(
video_id = video.id,
language = language_code,
"🔍 Using cached subtitle"
);
if tokio::fs::hard_link(&cached_path, &output_path).await.is_err() {
tokio::fs::copy(&cached_path, &output_path).await?;
}
return Ok(output_path);
}
let owned_fallback: Vec<crate::model::caption::Subtitle>;
let subtitles: &[crate::model::caption::Subtitle] = if let Some(subs) = video.subtitles.get(language_code) {
subs.as_slice()
} else if fallback_to_automatic {
if let Some(captions) = video.automatic_captions.get(language_code) {
owned_fallback = captions
.iter()
.map(|c| crate::model::caption::Subtitle::from_automatic_caption(c, language_code.to_string()))
.collect();
owned_fallback.as_slice()
} else {
return Err(Error::SubtitleNotAvailable {
video_id: video.id.clone(),
language: language_code.to_string(),
});
}
} else {
return Err(Error::SubtitleNotAvailable {
video_id: video.id.clone(),
language: language_code.to_string(),
});
};
let subtitle = subtitles
.iter()
.find(|s| s.is_format(&CaptionExtension::Srt))
.or_else(|| subtitles.iter().find(|s| s.is_format(&CaptionExtension::Vtt)))
.or_else(|| subtitles.first())
.ok_or_else(|| Error::SubtitleNotAvailable {
video_id: video.id.clone(),
language: language_code.to_string(),
})?;
tracing::debug!(url = subtitle.url, path = ?output_path, "💬 Downloading subtitle file");
let fetcher = Fetcher::new(&subtitle.url, self.proxy.as_ref(), None)?;
fetcher.fetch_asset(&output_path).await?;
#[cfg(cache)]
if let Some(cache) = &self.cache {
tracing::debug!(video_id = video.id, language = language_code, "🔍 Caching subtitle");
if let Err(_e) = cache
.downloads
.put_subtitle_file(
&output_path,
output.as_ref(),
video.id.clone(),
language_code.to_string(),
)
.await
{
tracing::warn!(error = %_e, "Failed to cache subtitle");
}
}
tracing::info!(language = language_code, path = ?output_path, "✅ Subtitle downloaded");
Ok(output_path)
}
pub async fn download_all_subtitles(
&self,
video: &Video,
output_dir: impl AsRef<Path>,
fallback_to_automatic: bool,
) -> crate::error::Result<Vec<PathBuf>> {
tracing::debug!(
video_id = %video.id,
subtitle_langs = video.subtitles.len(),
caption_langs = video.automatic_captions.len(),
"💬 Downloading all subtitles and automatic captions"
);
let output_dir = output_dir.as_ref();
let mut downloaded_files = Vec::new();
let mut all_languages: std::collections::HashMap<&str, Vec<crate::model::caption::Subtitle>> =
std::collections::HashMap::new();
if fallback_to_automatic {
for (lang, captions) in &video.automatic_captions {
let subs: Vec<_> = captions
.iter()
.map(|c| crate::model::caption::Subtitle::from_automatic_caption(c, lang.clone()))
.collect();
all_languages.entry(lang.as_str()).or_insert(subs);
}
}
for (lang, subs) in &video.subtitles {
all_languages.insert(lang.as_str(), subs.clone());
}
for (language_code, subtitles) in &all_languages {
let Some(subtitle) = subtitles
.iter()
.find(|s| s.is_format(&CaptionExtension::Srt))
.or_else(|| subtitles.iter().find(|s| s.is_format(&CaptionExtension::Vtt)))
.or_else(|| subtitles.first())
else {
continue;
};
let filename = format!("{}.{}.{}", video.id, language_code, subtitle.file_extension());
let output_path = output_dir.join(&filename);
tracing::debug!(
video_id = %video.id,
language_code = language_code,
url = %subtitle.url,
"💬 Downloading subtitle/caption"
);
let fetcher = Fetcher::new(&subtitle.url, self.proxy.as_ref(), None)?;
fetcher.fetch_asset(&output_path).await?;
downloaded_files.push(output_path);
}
tracing::info!(
video_id = %video.id,
count = downloaded_files.len(),
"✅ Subtitle/caption files downloaded"
);
Ok(downloaded_files)
}
}