diff options
Diffstat (limited to 'src/download')
-rw-r--r-- | src/download/download_options.rs | 118 | ||||
-rw-r--r-- | src/download/mod.rs | 140 |
2 files changed, 258 insertions, 0 deletions
diff --git a/src/download/download_options.rs b/src/download/download_options.rs new file mode 100644 index 0000000..17cf66c --- /dev/null +++ b/src/download/download_options.rs @@ -0,0 +1,118 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use serde_json::{json, Value}; + +use crate::{constants, storage::video_database::YtDlpOptions}; + +// { +// "ratelimit": conf.ratelimit if conf.ratelimit > 0 else None, +// "retries": conf.retries, +// "merge_output_format": conf.merge_output_format, +// "restrictfilenames": conf.restrict_filenames, +// "ignoreerrors": False, +// "postprocessors": [{"key": "FFmpegMetadata"}], +// "logger": _ytdl_logger +// } + +pub fn download_opts(additional_opts: YtDlpOptions) -> serde_json::Map<String, serde_json::Value> { + match json!({ + "extract_flat": false, + "extractor_args": { + "youtube": { + "comment_sort": [ + "top" + ], + "max_comments": [ + "150", + "all", + "100" + ] + } + }, + "ffmpeg_location": env!("FFMPEG_LOCATION"), + "format": "bestvideo[height<=?1080]+bestaudio/best", + "fragment_retries": 10, + "getcomments": true, + "ignoreerrors": false, + "retries": 10, + + "writeinfojson": true, + "writeannotations": true, + "writesubtitles": true, + "writeautomaticsub": true, + + "outtmpl": { + "default": constants::download_dir().join("%(channel)s/%(title)s.%(ext)s"), + "chapter": "%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s" + }, + "compat_opts": {}, + "forceprint": {}, + "print_to_file": {}, + "windowsfilenames": false, + "restrictfilenames": false, + "trim_file_names": false, + "postprocessors": [ + { + "api": "https://sponsor.ajay.app", + "categories": [ + "interaction", + "intro", + "music_offtopic", + "sponsor", + "outro", + "poi_highlight", + "preview", + "selfpromo", + "filler", + "chapter" + ], + "key": "SponsorBlock", + "when": "after_filter" + }, + { + "force_keyframes": false, + "key": "ModifyChapters", + "remove_chapters_patterns": [], + "remove_ranges": [], + "remove_sponsor_segments": [ + "sponsor" + ], + "sponsorblock_chapter_title": "[SponsorBlock]: %(category_names)l" + }, + { + "add_chapters": true, + "add_infojson": null, + "add_metadata": false, + "key": "FFmpegMetadata" + }, + { + "key": "FFmpegConcat", + "only_multi_video": true, + "when": "playlist" + } + ] + }) { + serde_json::Value::Object(mut obj) => { + obj.insert( + "subtitleslangs".to_owned(), + serde_json::Value::Array( + additional_opts + .subtitle_langs + .split(',') + .map(|val| Value::String(val.to_owned())) + .collect::<Vec<_>>(), + ), + ); + obj + } + _ => unreachable!("This is an object"), + } +} diff --git a/src/download/mod.rs b/src/download/mod.rs new file mode 100644 index 0000000..62fae84 --- /dev/null +++ b/src/download/mod.rs @@ -0,0 +1,140 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::time::Duration; + +use crate::{ + app::App, + download::download_options::download_opts, + storage::video_database::{ + downloader::{get_next_uncached_video, set_video_cache_path}, extractor_hash::ExtractorHash, getters::get_video_yt_dlp_opts, Video + }, +}; + +use anyhow::{Context, Result}; +use log::{debug, info}; +use tokio::{task::JoinHandle, time}; + +mod download_options; + +#[derive(Debug)] +pub struct CurrentDownload { + task_handle: JoinHandle<Result<()>>, + extractor_hash: ExtractorHash, +} + +impl CurrentDownload { + fn new_from_video(video: Video) -> Self { + let extractor_hash = video.extractor_hash.clone(); + + let task_handle = tokio::spawn(async move { + // FIXME: Remove this app reconstruction <2024-07-29> + let new_app = App::new().await?; + + Downloader::actually_cache_video(&new_app, &video) + .await + .with_context(|| format!("Failed to cache video: '{}'", video.title))?; + Ok(()) + }); + + Self { + task_handle, + extractor_hash, + } + } +} + +pub struct Downloader { + current_download: Option<CurrentDownload>, +} + +impl Downloader { + pub fn new() -> Self { + Self { + current_download: None, + } + } + + /// The entry point to the Downloader. + /// This Downloader will periodically check if the database has changed, and then also + /// change which videos it downloads. + /// This will run, until the database doesn't contain any watchable videos + pub async fn consume(&mut self, app: &App) -> Result<()> { + while let Some(next_video) = get_next_uncached_video(app).await? { + if let Some(_) = &self.current_download { + let current_download = self.current_download.take().expect("Is Some"); + + if current_download.task_handle.is_finished() { + current_download.task_handle.await??; + continue; + } + + if next_video.extractor_hash != current_download.extractor_hash { + info!( + "Noticed, that the next video is not the video being downloaded, replacing it ('{}' vs. '{}')!", + next_video.extractor_hash.into_short_hash(app).await?, current_download.extractor_hash.into_short_hash(app).await? + ); + + // Replace the currently downloading video + current_download.task_handle.abort(); + + let new_current_download = CurrentDownload::new_from_video(next_video); + + self.current_download = Some(new_current_download); + } else { + debug!( + "Currently downloading '{}'", + current_download.extractor_hash.into_short_hash(app).await? + ); + // Reset the taken value + self.current_download = Some(current_download); + time::sleep(Duration::new(1, 0)).await; + } + } else { + info!( + "No video is being downloaded right now, setting it to '{}'", + next_video.title + ); + let new_current_download = CurrentDownload::new_from_video(next_video); + self.current_download = Some(new_current_download); + } + + // if get_allocated_cache().await? < CONCURRENT { + // .cache_video(next_video).await?; + // } + } + + info!("Finished downloading!"); + Ok(()) + } + + async fn actually_cache_video(app: &App, video: &Video) -> Result<()> { + debug!("Download started: {}", &video.title); + + let addional_opts = get_video_yt_dlp_opts(&app, &video.extractor_hash).await?; + + let result = yt_dlp::download(&[video.url.clone()], &download_opts(addional_opts)) + .await + .with_context(|| format!("Failed to download video: '{}'", video.title))?; + + assert_eq!(result.len(), 1); + let result = &result[0]; + + set_video_cache_path(app, &video.extractor_hash, Some(&result)).await?; + + info!( + "Video '{}' was downlaoded to path: {}", + video.title, + result.display() + ); + + Ok(()) + } +} |