├── crates ├── libytdlr │ ├── migrations │ │ ├── .keep │ │ └── 0000-00-00-000000_init │ │ │ ├── down.sql │ │ │ └── up.sql │ ├── src │ │ ├── spawn │ │ │ ├── mod.rs │ │ │ ├── editor.rs │ │ │ ├── ytdl.rs │ │ │ └── ffmpeg.rs │ │ ├── main │ │ │ ├── archive │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── download │ │ │ │ ├── download_options.rs │ │ │ │ ├── parse_linetype.rs │ │ │ │ └── assemble_cmd.rs │ │ │ ├── sql_utils.rs │ │ │ └── rethumbnail.rs │ │ ├── data │ │ │ ├── cache │ │ │ │ ├── mod.rs │ │ │ │ ├── media_provider.rs │ │ │ │ └── media_info.rs │ │ │ ├── sql_schema.rs │ │ │ ├── mod.rs │ │ │ └── sql_models.rs │ │ ├── lib.rs │ │ ├── utils.rs │ │ └── error.rs │ ├── diesel.toml │ ├── examples │ │ ├── rethumbnail.rs │ │ └── simple.rs │ ├── Cargo.toml │ └── README.md └── ytdlr │ ├── src │ ├── commands │ │ ├── mod.rs │ │ ├── completions.rs │ │ ├── rethumbnail.rs │ │ ├── unicode_test.rs │ │ ├── import.rs │ │ └── search.rs │ ├── logger.rs │ ├── main.rs │ ├── state.rs │ └── utils.rs │ ├── build.rs │ └── Cargo.toml ├── .gitignore ├── fmt.sh ├── .editorconfig ├── clippy_pedantic.sh ├── .vscode ├── settings.json └── launch.json ├── .github ├── workflows │ ├── audit.yml │ ├── stale.yml │ └── tests.yml └── CONTRIBUTING.md ├── clippy.sh ├── rustfmt.toml ├── LICENSE ├── Cargo.toml ├── CHANGELOG.md └── README.md /crates/libytdlr/migrations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /crates/libytdlr/migrations/0000-00-00-000000_init/down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX media_archive_unique; 2 | 3 | DROP TABLE media_archive; 4 | -------------------------------------------------------------------------------- /crates/libytdlr/src/spawn/mod.rs: -------------------------------------------------------------------------------- 1 | //! index of spawning commands 2 | 3 | pub mod editor; 4 | pub mod ffmpeg; 5 | pub mod ytdl; 6 | -------------------------------------------------------------------------------- /fmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # this file is an shorthand for the command below, to not forget the nightly 4 | cargo +nightly fmt --all 5 | -------------------------------------------------------------------------------- /crates/libytdlr/src/main/archive/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module for all Archive related functionality (like `ytldr archive ...`) 2 | 3 | pub mod import; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = tab 7 | 8 | [*.{yml, yaml}] 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /crates/libytdlr/src/main/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module for all the main functionality in the library (to keep everything sorted) 2 | pub mod archive; 3 | pub mod download; 4 | pub mod rethumbnail; 5 | pub mod sql_utils; 6 | -------------------------------------------------------------------------------- /crates/libytdlr/src/data/cache/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module for Cache Media Information 2 | //! This is used while ytdl is downloading & to recover from premature termination 3 | 4 | pub mod media_info; 5 | pub mod media_provider; 6 | -------------------------------------------------------------------------------- /crates/libytdlr/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see https://diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/data/sql_schema.rs" 6 | 7 | [migrations_directory] 8 | dir = "migrations" 9 | -------------------------------------------------------------------------------- /crates/ytdlr/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module for all (longer) commands 2 | 3 | pub mod completions; 4 | pub mod download; 5 | pub mod import; 6 | pub mod rethumbnail; 7 | pub mod search; 8 | #[cfg(debug_assertions)] 9 | pub mod unicode_test; 10 | -------------------------------------------------------------------------------- /clippy_pedantic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This File is for invoking clippy::pedantic, but with some lints ignored that will not be fixed or a heavily false-positive 4 | 5 | cargo clippy --all-features "$@" -- \ 6 | -W clippy::pedantic \ 7 | -A clippy::doc_markdown \ 8 | -A clippy::module_name_repetitions \ 9 | -A clippy::uninlined_format_args 10 | -------------------------------------------------------------------------------- /crates/libytdlr/src/data/sql_schema.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::implicit_return)] 2 | #![allow(missing_docs)] 3 | // @generated automatically by Diesel CLI. 4 | 5 | diesel::table! { 6 | media_archive (_id) { 7 | _id -> BigInt, 8 | media_id -> Text, 9 | provider -> Text, 10 | title -> Text, 11 | inserted_at -> Timestamp, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/libytdlr/migrations/0000-00-00-000000_init/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE media_archive ( 2 | _id INTEGER NOT NULL PRIMARY KEY, 3 | media_id VARCHAR NOT NULL, 4 | provider VARCHAR NOT NULL, 5 | title VARCHAR NOT NULL, 6 | inserted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 7 | ); 8 | 9 | CREATE UNIQUE INDEX media_archive_unique ON media_archive (media_id, provider); 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.wordWrapColumn": 120, 4 | "editor.detectIndentation": false, 5 | "editor.insertSpaces": false, 6 | "[yaml]": { 7 | "editor.detectIndentation": false, 8 | "editor.tabSize": 2, 9 | "editor.insertSpaces": true, 10 | }, 11 | "lldb.displayFormat": "auto", 12 | "lldb.showDisassembly": "never", 13 | "lldb.dereferencePointers": true 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | push: 4 | paths: 5 | - '**/Cargo.toml' 6 | - '**/Cargo.lock' 7 | schedule: 8 | - cron: '0 0 * * *' 9 | 10 | jobs: 11 | security_audit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions-rs/audit-check@v1 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /crates/libytdlr/src/spawn/editor.rs: -------------------------------------------------------------------------------- 1 | //! Module that contains all logic for spawning the "editor" command 2 | use std::{ 3 | path::Path, 4 | process::Command, 5 | }; 6 | 7 | /// Create a new editor instance with the given filepath as a argument 8 | #[inline] 9 | #[must_use] 10 | pub fn base_editor(editor: &Path, filepath: &Path) -> Command { 11 | let mut cmd = Command::new(editor); 12 | cmd.arg(filepath); 13 | 14 | return cmd; 15 | } 16 | -------------------------------------------------------------------------------- /clippy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # this file is an shorthand for the command below 4 | cargo clippy --all-features "$@" -- 5 | # the following options have been transferred to /Cargo.toml#workspace.lints.clippy 6 | #-D clippy::correctness -W clippy::style -W clippy::complexity -W clippy::perf -A clippy::needless_return -D clippy::implicit_return -A clippy::needless_doctest_main -A clippy::tabs_in_doc_comments 7 | 8 | # the following options were also enabled, but are not necessary anymore 9 | # CLIPPY_DISABLE_DOCS_LINKS=1 10 | # -Z unstable-options 11 | # +nightly 12 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # used toolchain: nightly-x86_64-unknown-linux-gnu 2 | 3 | hard_tabs = true 4 | # imports_indent = "Block" # unstable 5 | imports_layout = "Vertical" # unstable 6 | # indent_style = "Block" # unstable 7 | # control_brace_style = "AlwaysSameLine" # unstable 8 | max_width = 120 9 | newline_style = "Unix" 10 | # binop_separator = "Front" # unstable 11 | # brace_style = "SameLineWhere" # unstable 12 | enum_discrim_align_threshold = 20 # unstable 13 | struct_field_align_threshold = 20 # unstable 14 | match_block_trailing_comma = true 15 | trailing_semicolon = true # unstable 16 | -------------------------------------------------------------------------------- /crates/libytdlr/src/data/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module for all common Data, like structs & their implementations 2 | pub mod cache; 3 | pub mod sql_models; 4 | /// SQL Schemas generated by Diesel 5 | pub mod sql_schema; 6 | 7 | /// Common type for a unknown field, where nothing was provided 8 | /// 9 | /// Example: importing a ytdl archive which is "id provider" only 10 | pub const UNKNOWN_NONE_PROVIDED: &str = "unknown (none-provided)"; 11 | /// Common type for a unknown field, which is required but unavailable 12 | /// 13 | /// Example: if parsing fails in a infallible method 14 | pub const UNKNOWN: &str = "unknown"; 15 | -------------------------------------------------------------------------------- /crates/ytdlr/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn main() { 4 | // set what version string to use for the build 5 | // currently it depends on what git outputs, or if failed use "unknown" 6 | { 7 | println!("cargo:rerun-if-changed=build.rs"); 8 | println!("cargo:rerun-if-changed=.git/HEAD"); 9 | 10 | let version = Command::new("git") 11 | .args(["describe", "--tags", "--always", "--dirty"]) 12 | .output() 13 | .ok() 14 | .and_then(|v| return String::from_utf8(v.stdout).ok()) 15 | .unwrap_or(String::from("unknown")); 16 | println!("cargo:rustc-env=YTDLR_VERSION={version}"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/libytdlr/examples/rethumbnail.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use libytdlr::main::rethumbnail::re_thumbnail_with_tmp; 4 | 5 | fn main() -> Result<(), libytdlr::Error> { 6 | let mut args = std::env::args(); 7 | 8 | let _ = args.next(); 9 | 10 | let media_path = PathBuf::from(args.next().expect("Expected First argument to be for the media file")); 11 | let image_path = PathBuf::from(args.next().expect("Expected Second argument to be for the image file")); 12 | let output_path = PathBuf::from(args.next().expect("Expected Third argument to be for the output file")); 13 | 14 | re_thumbnail_with_tmp(&media_path, &image_path, &output_path)?; 15 | 16 | println!("Done"); 17 | 18 | return Ok(()); 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/stale 2 | 3 | name: Mark stale issues and pull requests 4 | 5 | on: 6 | schedule: 7 | - cron: '30 1 * * *' 8 | 9 | jobs: 10 | stale: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/stale@v7 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Marking Issue as stale, will be closed in 7 days if no more activity is seen' 17 | close-issue-message: 'Closing Issue because it is marked as stale' 18 | stale-issue-label: stale 19 | exempt-issue-labels: help wanted,docs,enhancement,feature,dependencies,discussion 20 | days-before-stale: 60 21 | days-before-close: 7 22 | remove-stale-when-updated: true 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 hasezoey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/libytdlr/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Library of "YTDL-Rust", contains all the logic needed for the binary 2 | 3 | #![allow(clippy::needless_return)] 4 | #![allow(special_module_name)] // because of module "main", dont have a better name for that 5 | #![warn(clippy::implicit_return)] 6 | #![warn(missing_docs)] 7 | 8 | #[macro_use] 9 | extern crate log; 10 | 11 | pub mod data; 12 | pub mod error; 13 | pub mod main; 14 | pub mod spawn; 15 | pub mod utils; 16 | pub use error::Error; 17 | 18 | /// Debug function to start vscode-lldb debugger from external console. 19 | /// Only compiled when the target is "debug". 20 | #[cfg(debug_assertions)] 21 | pub fn invoke_vscode_debugger() { 22 | println!("Requesting Debugger"); 23 | // Request VSCode to open a debugger for the current PID 24 | let url = format!( 25 | "vscode://vadimcn.vscode-lldb/launch/config?{{'request':'attach','pid':{}}}", 26 | std::process::id() 27 | ); 28 | std::process::Command::new("code") 29 | .arg("--open-url") 30 | .arg(url) 31 | .output() 32 | .unwrap(); 33 | 34 | println!("Press ENTER to continue"); 35 | let _ = std::io::stdin().read_line(&mut String::new()); // wait until attached, then press ENTER to continue 36 | } 37 | 38 | pub use chrono; 39 | pub use diesel; 40 | -------------------------------------------------------------------------------- /crates/libytdlr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libytdlr" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | repository.workspace = true 9 | description = "A library to interact with youtube-dl/p with a custom archive" 10 | 11 | [dependencies] 12 | serde = { version = "1.0", features = ["derive"] } 13 | regex.workspace = true 14 | log.workspace = true 15 | dirs.workspace = true 16 | path-absolutize = "3.1" # Replace with official implementation, RFC: https://github.com/rust-lang/rfcs/issues/2208 17 | diesel = { version = "2.1", features = ["sqlite", "chrono"] } 18 | diesel_migrations = { version = "2.1" } 19 | chrono = "0.4" 20 | duct = "1.0.0" # required to pipe stderr into stdout 21 | thiserror = "2.0" 22 | lofty = "0.22.4" 23 | 24 | [dev-dependencies] 25 | serde_test = "1.0" 26 | uuid = { version = "1.8", features = ["v4"] } 27 | tempfile.workspace = true 28 | 29 | [lib] 30 | name = "libytdlr" 31 | path = "src/lib.rs" 32 | 33 | [lints] 34 | workspace = true 35 | 36 | [[example]] 37 | name = "simple" 38 | path = "examples/simple.rs" 39 | required-features = [] 40 | 41 | [[example]] 42 | name = "rethumbnail" 43 | path = "examples/rethumbnail.rs" 44 | required-features = [] 45 | -------------------------------------------------------------------------------- /crates/ytdlr/src/commands/completions.rs: -------------------------------------------------------------------------------- 1 | use std::io::{ 2 | BufWriter, 3 | Write, 4 | }; 5 | 6 | use clap::CommandFactory; 7 | use clap_complete::generate; 8 | use libytdlr::error::IOErrorToError; 9 | 10 | use crate::clap_conf::{ 11 | CliDerive, 12 | CommandCompletions, 13 | }; 14 | 15 | /// Handler function for the "completions" subcommand 16 | /// This function is mainly to keep the code structured and sorted 17 | #[inline] 18 | pub fn command_completions(_main_args: &CliDerive, sub_args: &CommandCompletions) -> Result<(), crate::Error> { 19 | let mut writer: BufWriter> = match &sub_args.output_file_path { 20 | Some(v) => { 21 | if v.exists() { 22 | return Err(crate::Error::other("Output file already exists")); 23 | } 24 | let v_parent = v.parent().expect("Expected input filename to have a parent"); 25 | std::fs::create_dir_all(v_parent).attach_path_err(v_parent)?; 26 | BufWriter::new(Box::from(std::fs::File::create(v).attach_path_err(v)?)) 27 | }, 28 | None => BufWriter::new(Box::from(std::io::stdout())), 29 | }; 30 | let mut parsed = CliDerive::command(); 31 | let bin_name = parsed 32 | .get_bin_name() 33 | .expect("Expected binary to have a binary name") 34 | .to_string(); 35 | generate(sub_args.shell, &mut parsed, bin_name, &mut writer); 36 | 37 | return Ok(()); 38 | } 39 | -------------------------------------------------------------------------------- /crates/ytdlr/src/commands/rethumbnail.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::clap_conf::{ 4 | CliDerive, 5 | CommandReThumbnail, 6 | }; 7 | use libytdlr::{ 8 | main::rethumbnail::re_thumbnail_with_tmp, 9 | spawn::ffmpeg::require_ffmpeg_installed, 10 | }; 11 | 12 | /// Handler function for the "rethumbnail" subcommand 13 | /// This function is mainly to keep the code structured and sorted 14 | #[inline] 15 | pub fn command_rethumbnail(_main_args: &CliDerive, sub_args: &CommandReThumbnail) -> Result<(), crate::Error> { 16 | require_ffmpeg_installed()?; 17 | 18 | // helper aliases to make it easier to access 19 | let input_image_path: &PathBuf = &sub_args.input_image_path; 20 | let input_media_path: &PathBuf = &sub_args.input_media_path; 21 | let output_media_path: &PathBuf = sub_args 22 | .output_media_path 23 | .as_ref() 24 | .expect("Expected trait \"Check\" to be run on \"CommandReThumbnail\" before this point"); 25 | 26 | println!( 27 | "Re-Applying Thumbnail image \"{}\" to media file \"{}\"", 28 | input_image_path.to_string_lossy(), 29 | input_media_path.to_string_lossy() 30 | ); 31 | 32 | re_thumbnail_with_tmp(input_media_path, input_image_path, output_media_path)?; 33 | 34 | println!( 35 | "Re-Applied Thumbnail to media, as \"{}\"", 36 | output_media_path.to_string_lossy() 37 | ); 38 | 39 | return Ok(()); 40 | } 41 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "0.12.0" 7 | authors = ["hasezoey "] 8 | edition = "2024" 9 | license = "MIT" 10 | rust-version = "1.85" 11 | repository = "https://github.com/hasezoey/yt-downloader-rust" 12 | 13 | [workspace.dependencies] 14 | # NOTE: keep this version in-sync with the version above, see https://github.com/rust-lang/cargo/issues/11133 for why this is not possible by default 15 | libytdlr = { path = "./crates/libytdlr", version = "0.12.0" } 16 | log = "0.4.27" 17 | regex = "1.11" 18 | dirs = "6.0" 19 | tempfile = "3.20" 20 | 21 | [workspace.lints.clippy] 22 | correctness = { level = "deny", priority = -1 } 23 | style = { level = "warn", priority = -1 } 24 | complexity = { level = "warn", priority = -1 } 25 | perf = { level = "warn", priority = -1 } 26 | needless_return = "allow" 27 | implicit_return = "deny" 28 | needless_doctest_main = "allow" 29 | tabs_in_doc_comments = "allow" 30 | wildcard_imports = "warn" 31 | semicolon_if_nothing_returned = "warn" 32 | default_trait_access = "warn" 33 | manual_assert = "warn" 34 | map_unwrap_or = "warn" 35 | ignored_unit_patterns = "warn" 36 | manual_let_else = "warn" 37 | single_match_else = "warn" 38 | if_not_else = "warn" 39 | manual_string_new = "warn" 40 | used_underscore_binding = "warn" 41 | return_self_not_must_use = "warn" 42 | inefficient_to_string = "warn" 43 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug unit tests in library 'libytdlr'", 11 | "cargo": { 12 | "args": [ 13 | "test", 14 | "--no-run", 15 | "--lib", 16 | "--package=yt-downloader-rust" 17 | ], 18 | "filter": { 19 | "name": "libytdlr", 20 | "kind": "lib" 21 | } 22 | }, 23 | "args": [], 24 | "cwd": "${workspaceFolder}" 25 | }, 26 | { 27 | "type": "lldb", 28 | "request": "launch", 29 | "name": "Debug executable 'ytdlr'", 30 | "cargo": { 31 | "args": [ 32 | "build", 33 | "--bin=ytdlr", 34 | "--package=yt-downloader-rust" 35 | ], 36 | "filter": { 37 | "name": "ytdlr", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | }, 44 | { 45 | "type": "lldb", 46 | "request": "launch", 47 | "name": "Debug unit tests in executable 'ytdlr'", 48 | "cargo": { 49 | "args": [ 50 | "test", 51 | "--no-run", 52 | "--bin=ytdlr", 53 | "--package=yt-downloader-rust" 54 | ], 55 | "filter": { 56 | "name": "ytdlr", 57 | "kind": "bin" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /crates/libytdlr/src/data/sql_models.rs: -------------------------------------------------------------------------------- 1 | //! Module for SQL Diesel Models 2 | 3 | use crate::data::sql_schema::media_archive; 4 | use chrono::NaiveDateTime; 5 | use diesel::prelude::*; 6 | 7 | /// Struct representing a Media table entry 8 | #[derive(Debug, Clone, PartialEq, Queryable)] 9 | #[diesel(table_name = media_archive)] 10 | pub struct Media { 11 | /// The ID of the video, auto-incremented upwards 12 | #[allow(clippy::pub_underscore_fields)] // field name in sql 13 | pub _id: i64, 14 | /// The ID of the media given used by the provider 15 | pub media_id: String, 16 | /// The Provider from where this media was downloaded from 17 | pub provider: String, 18 | /// The Title the media has 19 | pub title: String, 20 | /// The Time this media was inserted into the database 21 | pub inserted_at: NaiveDateTime, 22 | } 23 | 24 | /// Struct for inserting a [Media] into the database 25 | #[derive(Debug, Clone, PartialEq, Insertable)] 26 | #[diesel(table_name = media_archive)] 27 | pub struct InsMedia<'a> { 28 | /// The ID of the media given used by the provider 29 | pub media_id: &'a str, 30 | /// The Provider from where this media was downloaded from 31 | pub provider: &'a str, 32 | /// The Title the media has 33 | pub title: &'a str, 34 | } 35 | 36 | impl<'a> InsMedia<'a> { 37 | /// Create a new instance of [InsMedia] 38 | pub fn new(media_id: &'a str, provider: &'a str, title: &'a str) -> Self { 39 | return Self { 40 | media_id, 41 | provider, 42 | title, 43 | }; 44 | } 45 | } 46 | 47 | impl<'a> From<&'a Media> for InsMedia<'a> { 48 | fn from(value: &'a Media) -> Self { 49 | return Self { 50 | media_id: &value.media_id, 51 | provider: &value.provider, 52 | title: &value.title, 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/ytdlr/src/commands/unicode_test.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | clap_conf::{ 3 | CliDerive, 4 | CommandUnicodeTerminalTest, 5 | }, 6 | utils::msg_to_cluster, 7 | }; 8 | 9 | /// Handler function for the "unicode_test" subcommand 10 | /// This function is mainly to keep the code structured and sorted 11 | /// 12 | /// There are a lot of unicode and terminal problems, for example a wcwidth and wcswidth mismatch, or some terminals deciding some character is 2 wide instead of 1, 13 | /// this command exists to find to find and debug those kinds of problems more easily 14 | #[inline] 15 | pub fn command_unicodeterminaltest( 16 | _main_args: &CliDerive, 17 | sub_args: &CommandUnicodeTerminalTest, 18 | ) -> Result<(), crate::Error> { 19 | let msg = &sub_args.string; 20 | println!("Unicode Terminal Test"); 21 | 22 | // dont run anything on a empty message, because it makes the code below a lot easier with less cases (and 0 width evaluation is likely not needed) 23 | if msg.is_empty() { 24 | return Err(crate::Error::other("input is empty, not running any tests!")); 25 | } 26 | 27 | println!("Message (raw):\n{:#?}", msg); 28 | println!("Message (printed):\n{}", msg); 29 | 30 | let details = msg_to_cluster(&msg); 31 | let last_char = details.last().expect("Expected to return earlier at this case"); 32 | println!( 33 | "{}^", 34 | (1..last_char.display_pos).map(|_| return ' ').collect::() 35 | ); 36 | println!( 37 | "App thinks display-width is {}, above \"^\" means where it thinks in terms of display", 38 | last_char.display_pos, 39 | ); 40 | 41 | if sub_args.print_content { 42 | println!("msg_to_cluster array:\n{:#?}", details); 43 | } else { 44 | println!("msg_to_cluster array: not enabled (use -c)"); 45 | } 46 | 47 | return Ok(()); 48 | } 49 | -------------------------------------------------------------------------------- /crates/libytdlr/README.md: -------------------------------------------------------------------------------- 1 | # libYTDLR 2 | 3 | A library to interact with `youtube-dl` / `yt-dlp` from rust, with custom archive support. 4 | 5 | This library is mainly made for [`ytdlr`](https://github.com/hasezoey/yt-downloader-rust/tree/master/crates/ytdlr) binary, but can also be consumed standalone. 6 | 7 | For build / run requirements please see [the project README](https://github.com/hasezoey/yt-downloader-rust/blob/master/README.md#requirements). 8 | 9 | ## Functions provided 10 | 11 | ### download 12 | 13 | The main functionality: interacting with `yt-dlp`; downloading actual media. 14 | 15 | Small example: 16 | 17 | ```rs 18 | libytdlr::main::download::download_single(connection, &options, progress_callback, &mut result_vec)?; 19 | ``` 20 | 21 | For a full example see [`examples/simple`](https://github.com/hasezoey/yt-downloader-rust/tree/master/crates/libytdlr/examples/simple.rs). 22 | 23 | ### rethumbnail 24 | 25 | Extra functionality to re-apply a thumbnail to a video or audio container: 26 | 27 | Small example: 28 | 29 | ```rs 30 | libytdlr::main::rethumbnail::re_thumbnail_with_tmp(&media_path, &image_path, &output_path)?; 31 | ``` 32 | 33 | For a full example see [`examples/rehtumbnail`](https://github.com/hasezoey/yt-downloader-rust/tree/master/crates/libytdlr/examples/rethumbnail.rs). 34 | 35 | ### archive interaction 36 | 37 | The custom archive `libytdlr` uses is based on SQLite and provides full read & write ability. 38 | It is recommended to only do reads from outside functions. 39 | 40 | The main function that will be necessary to be called to make use of the archive is: 41 | 42 | ```rs 43 | libytdlr::main::sql_utils::migrate_and_connect(&database_path, progress_callback)?; 44 | // or without any format migration: 45 | libytdlr::main::sql_utils::sqlite_connect(&database_path)?; 46 | ``` 47 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Rust Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | workflow_dispatch: 9 | inputs: 10 | git-ref: 11 | description: Git Ref (Optional) 12 | required: false 13 | 14 | jobs: 15 | # Check rustfmt 16 | rustfmt: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: dtolnay/rust-toolchain@stable 21 | with: 22 | toolchain: nightly 23 | components: rustfmt 24 | - run: sh ./fmt.sh 25 | 26 | # Check clippy. This doesn't check ARM though. 27 | clippy: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: dtolnay/rust-toolchain@stable 32 | with: 33 | toolchain: stable 34 | components: clippy 35 | - run: sh ./clippy.sh 36 | 37 | tests: 38 | runs-on: ubuntu-latest 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | toolchain: [stable, nightly, "1.85"] 43 | steps: 44 | - uses: actions/checkout@v4 45 | if: github.event.inputs.git-ref == '' 46 | - uses: actions/checkout@v4 47 | if: github.event.inputs.git-ref != '' 48 | with: 49 | ref: ${{ github.event.inputs.git-ref }} 50 | - name: Install dev dependencies 51 | # This seemingly worked without this in 2024, but not in 2025 CI 52 | run: sudo apt update && sudo apt install libsqlite3-dev 53 | - name: Install Toolchain 54 | uses: dtolnay/rust-toolchain@stable 55 | with: 56 | toolchain: ${{ matrix.toolchain }} 57 | - name: Run syntax check 58 | run: cargo build --workspace --all-features 59 | - name: Tests 60 | run: cargo test --workspace --all-features --no-fail-fast 61 | env: 62 | RUST_BACKTRACE: full 63 | -------------------------------------------------------------------------------- /crates/ytdlr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ytdlr" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | build = "build.rs" 9 | repository.workspace = true 10 | description = "A better youtube-dl/p CLI interface" 11 | readme = "../../README.md" 12 | 13 | [dependencies] 14 | clap = { version = "~4.5", features = ["derive", "wrap_help", "env"] } 15 | clap_complete = "~4.5" 16 | indicatif = { version = "0.17.9", features = ["improved_unicode"] } 17 | colored = "3.0.0" 18 | log.workspace = true 19 | flexi_logger = "0.29" # this logger, because "env_logger" and "simple_logger" do not provide setting the log level dynamically 20 | is-terminal = "0.4" 21 | libytdlr.workspace = true 22 | dirs.workspace = true 23 | terminal_size = "0.4" 24 | regex.workspace = true 25 | sysinfo = { version = "0.35.2", default-features = false, features = ["system"]} 26 | ctrlc = { version = "3", features = ["termination"] } 27 | # the following 2 are required to get the correct boundaries to truncate at 28 | unicode-segmentation = "1.11" # cluster all characters into display-able characters 29 | unicode-width = "0.2" # get display width of a given string 30 | 31 | [dev-dependencies] 32 | tempfile.workspace = true 33 | 34 | [[bin]] 35 | name = "ytdlr" 36 | path = "src/main.rs" 37 | 38 | [lints] 39 | workspace = true 40 | 41 | [features] 42 | default = [ 43 | # included as default, because unicode-width is basically only used to count available space for progress-bar message truncation 44 | # which will only result in terminals which display 2 to work correctly (not going to a new-line) and terminals which display 1 to just have less characters displayed 45 | "workaround_fe0f", 46 | ] 47 | # Feature to count unicode code-point "\u{fe0f}" (VS16, render emoji in emoji-style) as a additional display position 48 | # this basically works-around any terminal that displays it as 2 characters, but unicode-width only thinking it is 1 character 49 | # Example terminals which display this as 2: 50 | # - KDE Konsole (23.08.4) 51 | # Example terminals which display this as 1: 52 | # - Alacritty (0.12.3) 53 | workaround_fe0f = [] 54 | -------------------------------------------------------------------------------- /crates/ytdlr/src/logger.rs: -------------------------------------------------------------------------------- 1 | //! Module for all Logger related things 2 | 3 | use colored::{ 4 | Color, 5 | Colorize, 6 | }; 7 | use flexi_logger::{ 8 | DeferredNow, 9 | Logger, 10 | LoggerHandle, 11 | Record, 12 | style, 13 | }; 14 | 15 | /// Function for setting up the logger 16 | /// This function is mainly to keep the code structured and sorted 17 | #[inline] 18 | pub fn setup_logger() -> LoggerHandle { 19 | let handle = Logger::try_with_env_or_str("warn") 20 | .expect("Expected flexi_logger to be able to parse env or string") 21 | .adaptive_format_for_stderr(flexi_logger::AdaptiveFormat::Custom(log_format, color_log_format)) 22 | .log_to_stderr() 23 | .start() 24 | .expect("Expected flexi_logger to be able to start"); 25 | 26 | return handle; 27 | } 28 | 29 | /// Logging format for log files and non-interactive formats 30 | /// Not Colored and not padded 31 | /// 32 | /// Example Lines: 33 | /// `[2022-03-02T13:42:43.374+0100 ERROR module]: test line` 34 | /// `[2022-03-02T13:42:43.374+0100 WARN module::deeper]: test line` 35 | pub fn log_format(w: &mut dyn std::io::Write, now: &mut DeferredNow, record: &Record) -> Result<(), std::io::Error> { 36 | return write!( 37 | w, 38 | "[{} {} {}]: {}", // dont pad anything for non-interactive logs 39 | now.format_rfc3339(), 40 | record.level(), 41 | record.module_path().unwrap_or(""), 42 | &record.args() 43 | ); 44 | } 45 | 46 | /// Logging format for a tty for interactive formats 47 | /// Colored and padded 48 | /// 49 | /// Example Lines: 50 | /// `[2022-03-02T13:42:43.374+0100 ERROR module]: test line` 51 | /// `[2022-03-02T13:42:43.374+0100 WARN module::deeper]: test line` 52 | pub fn color_log_format( 53 | w: &mut dyn std::io::Write, 54 | now: &mut DeferredNow, 55 | record: &Record, 56 | ) -> Result<(), std::io::Error> { 57 | let level = record.level(); 58 | return write!( 59 | w, 60 | "[{} {} {}]: {}", 61 | now.format_rfc3339().color(Color::BrightBlack), // Bright Black = Grey 62 | style(level).paint(format!("{level:5}")), // pad level to 2 characters, cannot be done in the string itself, because of the color characters 63 | record.module_path().unwrap_or(""), 64 | &record.args() // dont apply any color to the input, so that the input can dynamically set the color 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /crates/ytdlr/src/commands/import.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use crate::{ 4 | clap_conf::{ 5 | ArchiveImport, 6 | CliDerive, 7 | }, 8 | utils, 9 | }; 10 | use indicatif::{ 11 | ProgressBar, 12 | ProgressStyle, 13 | }; 14 | use libytdlr::main::archive::import::{ 15 | ImportProgress, 16 | import_any_archive, 17 | }; 18 | 19 | /// Handler function for the "archive import" subcommand 20 | /// This function is mainly to keep the code structured and sorted 21 | #[inline] 22 | pub fn command_import(main_args: &CliDerive, sub_args: &ArchiveImport) -> Result<(), crate::Error> { 23 | println!("Importing Archive from \"{}\"", sub_args.file_path.to_string_lossy()); 24 | 25 | let input_path = &sub_args.file_path; 26 | 27 | let Some(archive_path) = main_args.archive_path.as_ref() else { 28 | return Err(crate::Error::other("Archive is required for Import!")); 29 | }; 30 | 31 | static IMPORT_STYLE: LazyLock = LazyLock::new(|| { 32 | return ProgressStyle::default_bar() 33 | .template("[{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})") 34 | .expect("Expected ProgressStyle template to be valid") 35 | .progress_chars("#>-"); 36 | }); 37 | 38 | let bar: ProgressBar = ProgressBar::hidden().with_style(IMPORT_STYLE.clone()); 39 | crate::utils::set_progressbar(&bar, main_args); 40 | 41 | let (_new_archive, mut connection) = utils::handle_connect(archive_path, &bar, main_args)?; 42 | 43 | let pgcb_import = |imp| { 44 | if main_args.is_interactive() { 45 | match imp { 46 | ImportProgress::Starting => bar.set_position(0), 47 | ImportProgress::SizeHint(v) => bar.set_length(v.try_into().expect("Failed to convert usize to u64")), 48 | ImportProgress::Increase(c, _i) => bar.inc(c.try_into().expect("Failed to convert usize to u64")), 49 | ImportProgress::Finished(v) => bar.finish_with_message(format!("Finished Importing {v} elements")), 50 | } 51 | } else { 52 | match imp { 53 | ImportProgress::Starting => println!("Starting Import"), 54 | ImportProgress::SizeHint(v) => println!("Import SizeHint: {v}"), 55 | ImportProgress::Increase(c, i) => println!("Import Increase: {c}, Current Index: {i}"), 56 | ImportProgress::Finished(v) => println!("Import Finished, Successfull Imports: {v}"), 57 | } 58 | } 59 | }; 60 | 61 | import_any_archive(input_path, &mut connection, pgcb_import)?; 62 | 63 | return Ok(()); 64 | } 65 | -------------------------------------------------------------------------------- /crates/libytdlr/src/main/download/download_options.rs: -------------------------------------------------------------------------------- 1 | //! Module for various Context traits 2 | 3 | use std::{ 4 | ffi::OsStr, 5 | path::Path, 6 | }; 7 | 8 | use diesel::SqliteConnection; 9 | 10 | /// The Format argument to use for the command. 11 | /// 12 | /// See [yt-dlp Post-Processing Options](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#post-processing-options) `--remux-video` 13 | /// for possible rules. 14 | pub type FormatArgument<'a> = &'a str; 15 | 16 | /// Options specific for the [`crate::main::download::download_single`] function 17 | pub trait DownloadOptions { 18 | /// Get if the "audio-only" flag should be added 19 | fn audio_only(&self) -> bool; 20 | 21 | /// Get Extra Arguments that should be added to the ytdl command 22 | fn extra_ytdl_arguments(&self) -> Vec<&OsStr>; 23 | 24 | /// Get the path to where the Media should be downloaded to 25 | fn download_path(&self) -> &Path; 26 | 27 | /// Get a iterator over all the lines for a ytdl archive 28 | /// All required videos should be made available with this function 29 | /// 30 | /// Returning [None] means that not archive file will be create, which also means ytdl will not output any archive. 31 | /// Use `Some(Box::new([].into_iter()))` to still create a archive, but without initial content 32 | fn gen_archive<'a>(&'a self, connection: &'a mut SqliteConnection) 33 | -> Option + 'a>>; 34 | 35 | /// Get the URL to download 36 | fn get_url(&self) -> &str; 37 | 38 | /// Get whether or not to print out Command STDOUT & STDERR (in this case ytdl) 39 | /// STDERR is always printed (using [`log::trace`]) 40 | /// With this returning `true`, the STDOUT output is also printed with [`log::trace`] 41 | fn print_command_log(&self) -> bool; 42 | 43 | /// Get whether or not to save the Command STDOUT & STDERR to a file in the temporary directory 44 | fn save_command_log(&self) -> bool; 45 | 46 | /// Get which subtitle languages to download 47 | /// see for what is available 48 | /// [None] disables adding subtitles 49 | fn sub_langs(&self) -> Option<&str>; 50 | 51 | /// Get the current youtube-dl version in use as a chrono date 52 | fn ytdl_version(&self) -> chrono::NaiveDate; 53 | 54 | /// Get the format for audio-only/audio-extract downloads 55 | /// 56 | /// Only set extensions supported by youtube-dl 57 | fn get_audio_format(&self) -> FormatArgument<'_>; 58 | 59 | /// Get the format for video downloads 60 | /// 61 | /// Only set extensions supported by youtube-dl 62 | fn get_video_format(&self) -> FormatArgument<'_>; 63 | } 64 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Commit Structure 4 | 5 | This Repository uses the [Angular Commit Message Format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) 6 | This format is used by `semantic-release` to automatically release new versions based on changes 7 | 8 | ### Commit Message Format 9 | 10 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 11 | format that includes a **type**, a **scope** and a **subject**: 12 | 13 | ```txt 14 | (): 15 | 16 | 17 | 18 |