about summary refs log tree commit diff stats
path: root/src/download
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 10:49:23 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 11:28:43 +0200
commit1debeb77f7986de1b659dcfdc442de6415e1d9f5 (patch)
tree4df3e7c3f6a2d1ec116e4088c5ace7f143a8b05f /src/download
downloadyt-1debeb77f7986de1b659dcfdc442de6415e1d9f5.tar.gz
yt-1debeb77f7986de1b659dcfdc442de6415e1d9f5.zip
chore: Initial Commit
This repository was migrated out of my nixos-config.
Diffstat (limited to 'src/download')
-rw-r--r--src/download/download_options.rs118
-rw-r--r--src/download/mod.rs140
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(())
+    }
+}