Compare commits
12
Commits
1b4b4d39c4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a643aea7bf | ||
|
|
5be6e317ad | ||
|
|
0009db0ea9 | ||
|
|
1e13df0d6f | ||
|
|
c307f4c5d1 | ||
|
|
1f67b5010c | ||
|
|
88005f9a0d | ||
|
|
866575d7a0 | ||
|
|
ea2244d6b5 | ||
|
|
1719e47684 | ||
|
|
0abe9a8206 | ||
|
|
5fc42bd0fb |
@@ -0,0 +1,40 @@
|
|||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Release Binary (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
binary_path: target/release/yt-offline
|
||||||
|
- os: windows-latest
|
||||||
|
binary_path: target/release/yt-offline.exe
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Install Rust Toolchain
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v2
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Build release binary
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Upload binary artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: yt-offline-${{ matrix.os }}
|
||||||
|
path: ${{ matrix.binary_path }}
|
||||||
Generated
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JsonSchemaMappingsProjectConfiguration">
|
||||||
|
<state>
|
||||||
|
<map>
|
||||||
|
<entry key="GitHub Workflow">
|
||||||
|
<value>
|
||||||
|
<SchemaInfo>
|
||||||
|
<option name="name" value="GitHub Workflow" />
|
||||||
|
<option name="relativePathToSchema" value="https://www.schemastore.org/github-workflow.json" />
|
||||||
|
<option name="applicationDefined" value="true" />
|
||||||
|
<option name="patterns">
|
||||||
|
<list>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".gitea/workflows/build.yml" />
|
||||||
|
</Item>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</SchemaInfo>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+1
@@ -6338,6 +6338,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"webbrowser",
|
||||||
"yt-dlp",
|
"yt-dlp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -6,13 +6,14 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.6.7", features = ["derive"] }
|
clap = { version = "4.6.7", features = ["derive"] }
|
||||||
dirs = "7.0.0"
|
dirs = "7.0.0"
|
||||||
eframe = {version = "0.36.2", features = ["persistence"]}
|
eframe = { version = "0.36.2", features = ["persistence"] }
|
||||||
egui-sharkplayer = "0.5.1"
|
egui-sharkplayer = "0.5.1"
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.151"
|
serde_json = "1.0.151"
|
||||||
tokio = {version = "1.53.1", features = ["macros", "rt-multi-thread"]}
|
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
||||||
tracing = "0.1.44"
|
tracing = { version = "0.1.44", features = ["log"] }
|
||||||
tracing-subscriber = {version = "0.3.23", features = ["env-filter"]}
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
|
webbrowser = "1.2.4"
|
||||||
yt-dlp = "2.8.3"
|
yt-dlp = "2.8.3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
@@ -24,4 +25,4 @@ debug-assertions = false
|
|||||||
incremental = false
|
incremental = false
|
||||||
rpath = false
|
rpath = false
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
+60
-16
@@ -1,23 +1,42 @@
|
|||||||
use std::time::Duration;
|
use crate::args::ArgsConfig;
|
||||||
use eframe::{Frame, Storage, CreationContext};
|
use crate::types::{Playlist, Queues, Video, VideoDownloadQuality};
|
||||||
use eframe::egui::{Context, Ui};
|
use eframe::egui::{Context, Ui};
|
||||||
|
use eframe::{CreationContext, Frame, Storage};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
pub(crate) struct Application {
|
pub(crate) struct Application {
|
||||||
settings: AppSettings,
|
pub(crate) settings: AppSettings,
|
||||||
layers: Vec<Box<dyn crate::layer::Layer>>,
|
layers: Vec<Box<dyn crate::layer::Layer>>,
|
||||||
|
pub(crate) data: Data,
|
||||||
|
pub(crate) data_dir: String,
|
||||||
|
pub(crate) progress_percent: f32,
|
||||||
|
pub(crate) progress_info: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
struct AppSettings {
|
pub struct AppSettings {
|
||||||
use_cookies: bool,
|
pub(crate) use_firefox_cookies: bool,
|
||||||
use_firefox_cookies: bool,
|
pub(crate) use_chrome_cookies: bool,
|
||||||
use_chrome_cookies: bool,
|
pub(crate) save_thumbnails: bool,
|
||||||
save_thumbnails: bool,
|
pub(crate) reencode_videos: bool,
|
||||||
|
pub(crate) video_download_quality: VideoDownloadQuality,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub(crate) struct Data {
|
||||||
|
pub(crate) queues: Queues,
|
||||||
|
pub(crate) playlists: Vec<Playlist>,
|
||||||
|
pub(crate) videos: Vec<Video>,
|
||||||
|
pub(crate) root_path: 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") {
|
||||||
@@ -27,12 +46,31 @@ impl Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self { settings, layers }
|
let mut data = Data::default();
|
||||||
|
if let Some(storage) = cc.storage {
|
||||||
|
if let Some(stored_str) = storage.get_string("app_data") {
|
||||||
|
if let Ok(parsed) = serde_json::from_str(&stored_str) {
|
||||||
|
data = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.root_path.is_empty() {
|
||||||
|
data.root_path = args_config.data_dir.clone() + "/data";
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
settings,
|
||||||
|
layers,
|
||||||
|
data,
|
||||||
|
data_dir: args_config.data_dir.clone(),
|
||||||
|
progress_percent: 0.0,
|
||||||
|
progress_info: "".to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for Application {
|
impl eframe::App for Application {
|
||||||
|
|
||||||
fn logic(&mut self, ctx: &Context, frame: &mut Frame) {
|
fn logic(&mut self, ctx: &Context, frame: &mut Frame) {
|
||||||
let mut layers = std::mem::take(&mut self.layers);
|
let mut layers = std::mem::take(&mut self.layers);
|
||||||
for layer in &mut layers {
|
for layer in &mut layers {
|
||||||
@@ -53,15 +91,21 @@ impl eframe::App for Application {
|
|||||||
if let Ok(serialized) = serde_json::to_string(&self.settings) {
|
if let Ok(serialized) = serde_json::to_string(&self.settings) {
|
||||||
_storage.set_string("app_settings", serialized);
|
_storage.set_string("app_settings", serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Ok(serialized) = serde_json::to_string(&self.data) {
|
||||||
|
_storage.set_string("app_data", serialized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_exit(&mut self) {
|
fn on_exit(&mut self) {
|
||||||
|
let mut layers = std::mem::take(&mut self.layers);
|
||||||
|
for layer in &mut layers {
|
||||||
|
layer.on_exit(self);
|
||||||
|
}
|
||||||
|
self.layers = layers;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auto_save_interval(&self) -> Duration {
|
fn auto_save_interval(&self) -> Duration {
|
||||||
Duration::from_secs(5)
|
Duration::from_secs(5)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// TODO: Remove once downloader is done
|
||||||
|
#![allow(dead_code)]
|
||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Create a queue manager
|
||||||
|
// TODO: Create task to download videos
|
||||||
|
// TODO: Create task to process playlists
|
||||||
|
// TODO: Create task to process added urls
|
||||||
|
// TODO: Create function to recreate the yt_dlp::Downloader
|
||||||
|
|
||||||
|
impl Downloader {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let yt_downloader = None;
|
||||||
|
Self {
|
||||||
|
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
@@ -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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
use crate::app::Application;
|
||||||
|
use crate::layer::Layer;
|
||||||
|
use eframe::egui::Ui;
|
||||||
|
use eframe::emath::{Pos2, Rect};
|
||||||
|
use eframe::{Frame, egui};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct LeftPanel {
|
||||||
|
show_panel: bool,
|
||||||
|
url_bar: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layer for LeftPanel {
|
||||||
|
fn ui(&mut self, application: &mut Application, ui: &mut Ui, _frame: &mut Frame) {
|
||||||
|
let window_size = ui.input(|i| i.viewport().outer_rect).unwrap_or_else(|| {
|
||||||
|
warn!("Failed to get window size");
|
||||||
|
Rect {
|
||||||
|
min: Pos2 { x: 0., y: 0. },
|
||||||
|
max: Pos2 { x: 1., y: 1. },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
egui::panel::Panel::left("left_panel")
|
||||||
|
.min_size(window_size.width() / 5.)
|
||||||
|
.max_size((window_size.width() / 5.) + 2.)
|
||||||
|
.show_collapsible(ui, &mut self.show_panel, |ui| {
|
||||||
|
// TODO: Display a list of videos and playlists
|
||||||
|
|
||||||
|
// Progress bar and status info
|
||||||
|
egui::Panel::bottom("bottom_panel").show(ui, |ui| {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.text_edit_singleline(&mut self.url_bar);
|
||||||
|
if ui.button("Download").clicked() {
|
||||||
|
// TODO: Add check for if link is valid
|
||||||
|
application
|
||||||
|
.data
|
||||||
|
.queues
|
||||||
|
.added_queue
|
||||||
|
.push(self.url_bar.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
ui.label(format!("{}", application.progress_info));
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(format!("{:.1}%", application.progress_percent * 100.));
|
||||||
|
ui.add(egui::ProgressBar::new(application.progress_percent));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-13
@@ -1,22 +1,29 @@
|
|||||||
|
use crate::left_panel::LeftPanel;
|
||||||
|
use crate::top_bar_layer::TopBarLayer;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
use crate::test_layer::TestLayer;
|
|
||||||
|
|
||||||
mod logging;
|
|
||||||
mod args;
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod args;
|
||||||
|
mod downloader;
|
||||||
mod layer;
|
mod layer;
|
||||||
mod test_layer;
|
mod logging;
|
||||||
|
mod top_bar_layer;
|
||||||
|
mod types;
|
||||||
|
mod left_panel;
|
||||||
|
|
||||||
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);
|
||||||
let data_dir_path = PathBuf::from(&args.data_dir);
|
let data_dir_path = PathBuf::from(&args.data_dir);
|
||||||
if data_dir_path.is_file() {
|
if data_dir_path.is_file() {
|
||||||
error!("data_dir is a file... If you have set the data_dir manually, please pass in a directory. If you didn't pass in a path at all, please report this.");
|
error!(
|
||||||
|
"data_dir is a file... If you have set the data_dir manually, please pass in a directory. If you didn't pass in a path at all, please report this."
|
||||||
|
);
|
||||||
error!("{:#?}", args);
|
error!("{:#?}", args);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -28,15 +35,20 @@ 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(TestLayer::default()));
|
layers.push(Box::new(TopBarLayer::default()));
|
||||||
|
layers.push(Box::new(LeftPanel::default()));
|
||||||
|
layers.push(Box::new(downloader::Downloader::new()));
|
||||||
|
|
||||||
let mut options = eframe::NativeOptions::default();
|
let mut options = eframe::NativeOptions::default();
|
||||||
options.persistence_path = Some(args.state.parse().expect("Failed to parse state config file path into path"));
|
options.persistence_path = Some(
|
||||||
|
args.state
|
||||||
|
.parse()
|
||||||
|
.expect("Failed to parse state config file path into path"),
|
||||||
|
);
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"yt-offline",
|
"yt-offline",
|
||||||
options,
|
options,
|
||||||
Box::new(|cc| {
|
Box::new(|cc| Ok(Box::new(app::Application::new(cc, layers, &args)))),
|
||||||
Ok(Box::new(app::Application::new(cc, layers)))
|
)
|
||||||
})).expect("TODO: panic message");
|
.expect("TODO: panic message");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
use eframe::egui::{Context, Ui};
|
|
||||||
use eframe::{egui, Frame};
|
|
||||||
use crate::app;
|
|
||||||
use crate::layer::Layer;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct TestLayer {
|
|
||||||
test_string: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layer for TestLayer {
|
|
||||||
fn logic(&mut self, application: &mut app::Application, ctx: &Context, frame: &mut Frame) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui(&mut self, application: &mut app::Application, ui: &mut Ui, frame: &mut Frame) {
|
|
||||||
egui::CentralPanel::default().show(ui, |ui| {
|
|
||||||
ui.heading("Hello from the test_layer!");
|
|
||||||
ui.text_edit_singleline(&mut self.test_string);
|
|
||||||
if ui.button("Print test_string in layer").clicked() {
|
|
||||||
println!("{}", self.test_string);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
use crate::app::Application;
|
||||||
|
use crate::layer::Layer;
|
||||||
|
use crate::types::VideoDownloadQuality;
|
||||||
|
use eframe::egui::Ui;
|
||||||
|
use eframe::{Frame, egui};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TopBarLayer {
|
||||||
|
show_settings_window: bool,
|
||||||
|
show_about_window: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layer for TopBarLayer {
|
||||||
|
fn ui(&mut self, application: &mut Application, ui: &mut Ui, _frame: &mut Frame) {
|
||||||
|
egui::MenuBar::new().ui(ui, |ui: &mut Ui| {
|
||||||
|
ui.menu_button("File", |ui: &mut Ui| {
|
||||||
|
if ui.button("Settings").clicked() {
|
||||||
|
self.show_settings_window = true;
|
||||||
|
}
|
||||||
|
if ui.button("Exit").clicked() {
|
||||||
|
ui.send_viewport_cmd(eframe::egui::ViewportCommand::Close);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.menu_button("Help", |ui: &mut Ui| {
|
||||||
|
if ui.button("About").clicked() {
|
||||||
|
self.show_about_window = true;
|
||||||
|
}
|
||||||
|
if ui.button("Git").clicked() {
|
||||||
|
if let Err(e) =
|
||||||
|
webbrowser::open("https://git.catgirls.fish/firewire/yt-offline")
|
||||||
|
{
|
||||||
|
warn!("Failed to open web browser: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.show_settings_window {
|
||||||
|
egui::Window::new("Settings")
|
||||||
|
.open(&mut self.show_settings_window)
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.show(ui, |ui: &mut Ui| {
|
||||||
|
if ui
|
||||||
|
.checkbox(
|
||||||
|
&mut application.settings.use_firefox_cookies,
|
||||||
|
"Use cookies from Firefox",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if application.settings.use_chrome_cookies {
|
||||||
|
application.settings.use_chrome_cookies = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if ui
|
||||||
|
.checkbox(
|
||||||
|
&mut application.settings.use_chrome_cookies,
|
||||||
|
"Use cookies from Chrome",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if application.settings.use_firefox_cookies {
|
||||||
|
application.settings.use_firefox_cookies = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ui.checkbox(&mut application.settings.save_thumbnails, "Save thumbnails");
|
||||||
|
ui.checkbox(
|
||||||
|
&mut application.settings.reencode_videos,
|
||||||
|
"Re-encode videos",
|
||||||
|
);
|
||||||
|
ui.menu_button("Video download quality", |ui: &mut Ui| {
|
||||||
|
ui.radio_value(
|
||||||
|
&mut application.settings.video_download_quality,
|
||||||
|
VideoDownloadQuality::Best,
|
||||||
|
"Best (Highest available)",
|
||||||
|
);
|
||||||
|
ui.radio_value(
|
||||||
|
&mut application.settings.video_download_quality,
|
||||||
|
VideoDownloadQuality::High,
|
||||||
|
"High (Target 1080p)",
|
||||||
|
);
|
||||||
|
ui.radio_value(
|
||||||
|
&mut application.settings.video_download_quality,
|
||||||
|
VideoDownloadQuality::Medium,
|
||||||
|
"Medium (Target 720p)",
|
||||||
|
);
|
||||||
|
ui.radio_value(
|
||||||
|
&mut application.settings.video_download_quality,
|
||||||
|
VideoDownloadQuality::Low,
|
||||||
|
"Low (Target 480p)",
|
||||||
|
);
|
||||||
|
ui.radio_value(
|
||||||
|
&mut application.settings.video_download_quality,
|
||||||
|
VideoDownloadQuality::Worst,
|
||||||
|
"Worst (Lowest available)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.show_about_window {
|
||||||
|
egui::Window::new("About")
|
||||||
|
.open(&mut self.show_about_window)
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.show(ui, |ui: &mut Ui| {
|
||||||
|
ui.heading("yt-offline");
|
||||||
|
ui.label("A simple unified offline YouTube downloader + viewer");
|
||||||
|
ui.label("Fully human generated by Lyra in Rust <3");
|
||||||
|
ui.separator();
|
||||||
|
ui.label("Honorable mentions:");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("yt-dlp");
|
||||||
|
ui.hyperlink("https://github.com/yt-dlp/yt-dlp");
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("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")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use yt_dlp::cache::VideoQuality;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub(crate) struct Queues {
|
||||||
|
pub(crate) video_queue: Vec<Video>,
|
||||||
|
pub(crate) playlists_queue: Vec<Playlist>,
|
||||||
|
pub(crate) added_queue: Vec<String>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Default)]
|
||||||
|
pub enum VideoDownloadQuality {
|
||||||
|
#[default]
|
||||||
|
Best,
|
||||||
|
High,
|
||||||
|
Medium,
|
||||||
|
Low,
|
||||||
|
Worst,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoDownloadQuality {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn to_ytdlp(&self) -> VideoQuality {
|
||||||
|
match self {
|
||||||
|
VideoDownloadQuality::Best => VideoQuality::Best,
|
||||||
|
VideoDownloadQuality::High => VideoQuality::High,
|
||||||
|
VideoDownloadQuality::Medium => VideoQuality::Medium,
|
||||||
|
VideoDownloadQuality::Low => VideoQuality::Low,
|
||||||
|
VideoDownloadQuality::Worst => VideoQuality::Worst,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
VideoDownloadQuality::Best => "best".to_string(),
|
||||||
|
VideoDownloadQuality::High => "high".to_string(),
|
||||||
|
VideoDownloadQuality::Medium => "medium".to_string(),
|
||||||
|
VideoDownloadQuality::Low => "low".to_string(),
|
||||||
|
VideoDownloadQuality::Worst => "worst".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user