Create downloader layer + manage library installation

This commit is contained in:
2026-09-17 03:46:05 -04:00
parent c307f4c5d1
commit 1e13df0d6f
6 changed files with 198 additions and 53 deletions
+21 -33
View File
@@ -1,13 +1,16 @@
use crate::args::ArgsConfig;
use crate::types::{Playlist, Video};
use crate::video_quality::VideoDownloadQuality;
use eframe::egui::{Context, Ui}; use eframe::egui::{Context, Ui};
use eframe::{CreationContext, Frame, Storage}; use eframe::{CreationContext, Frame, Storage};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
use yt_dlp::model::VideoQuality;
pub(crate) struct Application { pub(crate) struct Application {
pub settings: AppSettings, pub(crate) settings: AppSettings,
layers: Vec<Box<dyn crate::layer::Layer>>, layers: Vec<Box<dyn crate::layer::Layer>>,
data: Data, pub(crate) data: Data,
pub(crate) data_dir: String,
} }
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
@@ -15,42 +18,22 @@ pub struct AppSettings {
pub(crate) use_firefox_cookies: bool, pub(crate) use_firefox_cookies: bool,
pub(crate) use_chrome_cookies: bool, pub(crate) use_chrome_cookies: bool,
pub(crate) save_thumbnails: bool, pub(crate) save_thumbnails: bool,
pub(crate) video_download_quality: crate::video_quality::VideoDownloadQuality, pub(crate) video_download_quality: VideoDownloadQuality,
} }
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
struct Data { pub(crate) struct Data {
playlists: Vec<Playlist>, pub(crate) playlists: Vec<Playlist>,
videos: Vec<Video>, pub(crate) videos: Vec<Video>,
root_path: String, pub(crate) root_path: String,
}
#[derive(Serialize, Deserialize, Default)]
struct Video {
name: String,
uuid: String,
original_url: String,
video_path: String,
has_thumbnail: bool,
thumbnail_path: String,
thumbnail_path_low_quality: String,
video_quality: VideoQuality,
}
#[derive(Serialize, Deserialize, Default)]
struct Playlist {
name: String,
uuid: String,
original_url: String,
videos: Vec<Video>,
creator: String,
has_thumbnail: bool,
thumbnail_path: String,
thumbnail_path_low_quality: String,
} }
impl Application { impl Application {
pub fn new(cc: &CreationContext, layers: Vec<Box<dyn crate::layer::Layer>>) -> Self { pub fn new(
cc: &CreationContext,
layers: Vec<Box<dyn crate::layer::Layer>>,
args_config: &ArgsConfig,
) -> Self {
let mut settings = AppSettings::default(); let mut settings = AppSettings::default();
if let Some(storage) = cc.storage { if let Some(storage) = cc.storage {
if let Some(stored_str) = storage.get_string("app_settings") { if let Some(stored_str) = storage.get_string("app_settings") {
@@ -69,10 +52,15 @@ impl Application {
} }
} }
if data.root_path.is_empty() {
data.root_path = args_config.data_dir.clone() + "/data";
}
Self { Self {
settings, settings,
layers, layers,
data, data,
data_dir: args_config.data_dir.clone(),
} }
} }
} }
+120
View File
@@ -0,0 +1,120 @@
use crate::app::Application;
use crate::layer::Layer;
use crate::types::{Playlist, Video};
use eframe::egui::{Context, Ui};
use eframe::{Frame, egui};
use std::path::PathBuf;
use tokio::runtime::Handle;
use tracing::warn;
use yt_dlp::client::LibraryInstaller;
pub(crate) struct Downloader {
video_queue: Vec<Video>,
playlists_queue: Vec<Playlist>,
yt_downloader: Option<yt_dlp::Downloader>,
tokio_handle: Handle,
yt_dlp_installed: bool,
rejected_yt_dlp_install: bool,
ffmpeg_installed: bool,
rejected_ffmpeg_install: bool,
}
impl Downloader {
pub fn new() -> Self {
let yt_downloader = None;
Self {
video_queue: Vec::new(),
playlists_queue: Vec::new(),
yt_downloader,
tokio_handle: Handle::current(),
yt_dlp_installed: false,
rejected_yt_dlp_install: false,
ffmpeg_installed: false,
rejected_ffmpeg_install: false,
}
}
async fn install_yt_dlp(data_dir: String) -> Result<(), String> {
let libraries_dir = PathBuf::from(format!("{}/libs", &data_dir));
let installer = LibraryInstaller::new(libraries_dir);
if let Err(e) = installer.install_youtube(None).await {
warn!("Failed to install yt-dlp: {}", e);
return Err(e.to_string());
}
Ok(())
}
async fn install_ffmpeg(data_dir: String) -> Result<(), String> {
let libraries_dir = PathBuf::from(format!("{}/libs", &data_dir));
let installer = LibraryInstaller::new(libraries_dir);
if let Err(e) = installer.install_ffmpeg(None).await {
warn!("Failed to install yt-dlp: {}", e);
return Err(e.to_string());
}
Ok(())
}
async fn create_new_yt_downloader(application: &Application) -> Option<yt_dlp::Downloader> {
Err("Not implemented".to_string()).expect("Soon")
}
fn check_yt_dlp(data_dir: String) -> bool {
let path = PathBuf::from(format!("{}/libs/yt-dlp", &data_dir));
path.exists()
}
fn check_ffmpeg(data_dir: String) -> bool {
let path = PathBuf::from(format!("{}/libs/ffmpeg", &data_dir));
path.exists()
}
}
impl Layer for Downloader {
fn logic(&mut self, application: &mut Application, ctx: &Context, frame: &mut Frame) {
if !self.yt_dlp_installed {
if Downloader::check_yt_dlp(application.data_dir.clone()) {
self.yt_dlp_installed = true;
}
}
if !self.ffmpeg_installed {
if Downloader::check_ffmpeg(application.data_dir.clone()) {
self.ffmpeg_installed = true;
}
}
}
fn ui(&mut self, application: &mut Application, ui: &mut Ui, frame: &mut Frame) {
if !self.yt_dlp_installed && !self.rejected_yt_dlp_install {
egui::Window::new("Missing yt-dlp").show(ui, |ui| {
ui.label("yt-dlp is not installed. Would you like to install it?");
ui.horizontal(|ui| {
if ui.button("Yes").clicked() {
self.tokio_handle
.spawn(Self::install_yt_dlp(application.data_dir.clone()));
}
if ui.button("No").clicked() {
self.rejected_yt_dlp_install = true;
}
});
});
}
if !self.ffmpeg_installed && !self.rejected_ffmpeg_install {
egui::Window::new("Missing ffmpeg").show(ui, |ui| {
ui.label("ffmpeg is not installed. Would you like to install it?");
ui.horizontal(|ui| {
if ui.button("Yes").clicked() {
self.tokio_handle
.spawn(Self::install_ffmpeg(application.data_dir.clone()));
}
if ui.button("No").clicked() {
self.rejected_ffmpeg_install = true;
}
});
});
}
}
}
+6 -12
View File
@@ -1,19 +1,13 @@
use eframe::{egui, Frame}; use crate::app::Application;
use eframe::{Frame, egui};
use egui::Context; use egui::Context;
use egui::Ui; use egui::Ui;
use crate::app::Application;
#[allow(unused_variables)] #[allow(unused_variables)]
pub trait Layer { pub trait Layer {
fn logic(&mut self, application: &mut Application, ctx: &Context, frame: &mut Frame) { fn logic(&mut self, application: &mut Application, ctx: &Context, frame: &mut Frame) {}
} fn ui(&mut self, application: &mut Application, ui: &mut Ui, frame: &mut Frame) {}
fn ui(&mut self, application: &mut Application, ui: &mut Ui, frame: &mut Frame) { fn on_exit(&mut self, application: &mut Application) {}
}
fn on_exit(&mut self, application: &mut Application) {
}
} }
+6 -2
View File
@@ -6,12 +6,15 @@ use tracing::{debug, error};
mod app; mod app;
mod args; mod args;
mod downloader;
mod layer; mod layer;
mod logging; mod logging;
mod top_bar_layer; mod top_bar_layer;
mod types;
mod video_quality; mod video_quality;
fn main() { #[tokio::main]
async fn main() {
let args = args::parse_args(); let args = args::parse_args();
logging::init(args.verbose); logging::init(args.verbose);
debug!("Using args: \n{:#?}\n", args); debug!("Using args: \n{:#?}\n", args);
@@ -32,6 +35,7 @@ fn main() {
let mut layers: Vec<Box<dyn layer::Layer>> = Vec::new(); let mut layers: Vec<Box<dyn layer::Layer>> = Vec::new();
layers.push(Box::new(TopBarLayer::default())); layers.push(Box::new(TopBarLayer::default()));
layers.push(Box::new(downloader::Downloader::new()));
let mut options = eframe::NativeOptions::default(); let mut options = eframe::NativeOptions::default();
options.persistence_path = Some( options.persistence_path = Some(
@@ -42,7 +46,7 @@ fn main() {
eframe::run_native( eframe::run_native(
"yt-offline", "yt-offline",
options, options,
Box::new(|cc| Ok(Box::new(app::Application::new(cc, layers)))), Box::new(|cc| Ok(Box::new(app::Application::new(cc, layers, &args)))),
) )
.expect("TODO: panic message"); .expect("TODO: panic message");
} }
+14 -6
View File
@@ -67,27 +67,27 @@ impl Layer for TopBarLayer {
ui.radio_value( ui.radio_value(
&mut application.settings.video_download_quality, &mut application.settings.video_download_quality,
VideoDownloadQuality::Best, VideoDownloadQuality::Best,
"Best", "Best (Highest available)",
); );
ui.radio_value( ui.radio_value(
&mut application.settings.video_download_quality, &mut application.settings.video_download_quality,
VideoDownloadQuality::High, VideoDownloadQuality::High,
"High", "High (Target 1080p)",
); );
ui.radio_value( ui.radio_value(
&mut application.settings.video_download_quality, &mut application.settings.video_download_quality,
VideoDownloadQuality::Medium, VideoDownloadQuality::Medium,
"Medium", "Medium (Target 720p)",
); );
ui.radio_value( ui.radio_value(
&mut application.settings.video_download_quality, &mut application.settings.video_download_quality,
VideoDownloadQuality::Low, VideoDownloadQuality::Low,
"Low", "Low (Target 480p)",
); );
ui.radio_value( ui.radio_value(
&mut application.settings.video_download_quality, &mut application.settings.video_download_quality,
VideoDownloadQuality::Worst, VideoDownloadQuality::Worst,
"Worst", "Worst (Lowest available)",
); );
}); });
}); });
@@ -99,7 +99,7 @@ impl Layer for TopBarLayer {
.show(ui, |ui: &mut Ui| { .show(ui, |ui: &mut Ui| {
ui.heading("yt-offline"); ui.heading("yt-offline");
ui.label("A simple unified offline YouTube downloader + viewer"); ui.label("A simple unified offline YouTube downloader + viewer");
ui.label("Created by Lyra in Rust <3"); ui.label("Fully human generated by Lyra in Rust <3");
ui.separator(); ui.separator();
ui.label("Honorable mentions:"); ui.label("Honorable mentions:");
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -110,6 +110,14 @@ impl Layer for TopBarLayer {
ui.label("egui"); ui.label("egui");
ui.hyperlink("https://github.com/emilk/egui"); ui.hyperlink("https://github.com/emilk/egui");
}); });
ui.horizontal(|ui| {
ui.label("egui-sharkplayer");
ui.hyperlink("https://codeberg.org/sharkyshark/egui-sharkplayer");
});
ui.horizontal(|ui| {
ui.label("The yt-dlp crate");
ui.hyperlink("https://github.com/boul2gom/yt-dlp")
});
}); });
} }
} }
+31
View File
@@ -0,0 +1,31 @@
use crate::video_quality::VideoDownloadQuality;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default)]
pub(crate) struct Video {
pub(crate) meta_data: MetaData,
pub(crate) video_path: String,
pub(crate) video_quality: VideoDownloadQuality,
pub(crate) downloaded: bool,
}
#[derive(Serialize, Deserialize, Default)]
pub(crate) struct MetaData {
pub(crate) title: String,
pub(crate) author: String,
pub(crate) description: String,
pub(crate) original_url: String,
pub(crate) has_thumbnail: bool,
pub(crate) thumbnail_path: String,
pub(crate) thumbnail_path_low_quality: String,
pub(crate) downloaded: bool,
pub(crate) date_downloaded: String,
pub(crate) date_published: String,
}
#[derive(Serialize, Deserialize, Default)]
pub(crate) struct Playlist {
pub(crate) meta_data: MetaData,
pub(crate) videos: Vec<Video>,
pub(crate) num_videos: usize,
}