about summary refs log blame commit diff stats
path: root/src/download/mod.rs
blob: 4431d3efb2164e7997b5559da45088a55aa436e5 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16














                                                                          


                                                                    

























































































































                                                                                                                                          
// 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(())
    }
}