about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-23 13:11:09 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-23 13:14:13 +0200
commit94c656ad40a7aae570e5a5fb61ad32632acc6d46 (patch)
tree269614af20caf10d76643c302e0115bd36fd2378
parentrefactor(yt_dlp): Also move the `crates` subdirectory (diff)
downloadyt-94c656ad40a7aae570e5a5fb61ad32632acc6d46.tar.gz
yt-94c656ad40a7aae570e5a5fb61ad32632acc6d46.zip
feat(treewide): Use a configuration file
This allows use to avoid duplication of default values in the codebase
and obviously also facilitates changing these without having to
re-compile.
-rw-r--r--Cargo.lock53
-rw-r--r--Cargo.toml1
-rw-r--r--src/app.rs14
-rw-r--r--src/cli.rs23
-rw-r--r--src/config/default.rs100
-rw-r--r--src/config/file_system.rs123
-rw-r--r--src/config/mod.rs52
-rw-r--r--src/constants.rs74
-rw-r--r--src/download/download_options.rs9
-rw-r--r--src/download/mod.rs15
-rw-r--r--src/main.rs5
-rw-r--r--src/select/mod.rs9
-rw-r--r--src/select/selection_file/display.rs2
-rw-r--r--src/storage/video_database/mod.rs11
-rw-r--r--src/storage/video_database/setters.rs9
-rw-r--r--src/watch/events.rs3
-rw-r--r--src/watch/mod.rs5
17 files changed, 387 insertions, 121 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 587c33a..ed5f824 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1432,6 +1432,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "serde_spanned"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
+dependencies = [
+ "serde",
+]
+
+[[package]]
 name = "sha1"
 version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1896,6 +1905,40 @@ dependencies = [
 ]
 
 [[package]]
+name = "toml"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
 name = "tracing"
 version = "0.1.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2286,6 +2329,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
+name = "winnow"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
 name = "xdg"
 version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2311,6 +2363,7 @@ dependencies = [
  "stderrlog",
  "tempfile",
  "tokio",
+ "toml",
  "trinitry",
  "url",
  "xdg",
diff --git a/Cargo.toml b/Cargo.toml
index c722845..8b77b16 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,6 +42,7 @@ yt_dlp = { path = "./crates/yt_dlp/" }
 libmpv2 = { path = "./crates/libmpv2" }
 bytes = { path = "./crates/bytes" }
 trinitry = { version = "0.2.2" }
+toml = "0.8.19"
 
 [[bin]]
 name = "yt"
diff --git a/src/app.rs b/src/app.rs
index f956251..b7d136e 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -8,19 +8,20 @@
 // 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::path::PathBuf;
-
 use anyhow::{Context, Result};
 use sqlx::{query, sqlite::SqliteConnectOptions, SqlitePool};
 
+use crate::config::Config;
+
 pub struct App {
     pub database: SqlitePool,
+    pub config: Config,
 }
 
 impl App {
-    pub async fn new(db_name: PathBuf) -> Result<Self> {
+    pub async fn new(config: Config) -> Result<Self> {
         let options = SqliteConnectOptions::new()
-            .filename(db_name)
+            .filename(&config.paths.database_path)
             .optimize_on_close(true, None)
             .create_if_missing(true);
 
@@ -32,6 +33,9 @@ impl App {
             .execute(&pool)
             .await?;
 
-        Ok(App { database: pool })
+        Ok(App {
+            database: pool,
+            config,
+        })
     }
 }
diff --git a/src/cli.rs b/src/cli.rs
index c567828..05c78de 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -10,14 +10,17 @@
 
 use std::path::PathBuf;
 
-use anyhow::{bail, Error};
+use anyhow::Context;
 use chrono::NaiveDate;
 use clap::{ArgAction, Args, Parser, Subcommand};
 use url::Url;
 use bytes::Bytes;
 
 use crate::{
-    constants, select::selection_file::duration::Duration,
+    config::{
+        default::{download, select, update},
+    },
+    select::selection_file::duration::Duration,
     storage::video_database::extractor_hash::LazyExtractorHash,
 };
 
@@ -33,10 +36,15 @@ pub struct CliArgs {
     #[arg(long="verbose", short = 'v', action = ArgAction::Count)]
     pub verbosity: u8,
 
-    /// Set the path to the videos.db. Otherwise use the default location
+    /// Set the path to the videos.db. This overrides the default and the config file.
     #[arg(long, short)]
     pub db_path: Option<PathBuf>,
 
+    /// Set the path to the config.toml.
+    /// This overrides the default.
+    #[arg(long, short)]
+    pub config_path: Option<PathBuf>,
+
     /// Silence all output
     #[arg(long, short = 'q')]
     pub quiet: bool,
@@ -52,7 +60,7 @@ pub enum Command {
 
         /// The maximum size the download dir should have. Beware that the value must be given in
         /// bytes.
-        #[arg(short, long, default_value = "3 GiB", value_parser = byte_parser)]
+        #[arg(short, long, default_value = download::max_cache_size(), value_parser = byte_parser)]
         max_cache_size: u64,
     },
 
@@ -88,7 +96,7 @@ pub enum Command {
 
     /// Update the video database
     Update {
-        #[arg(short, long, default_value = "20")]
+        #[arg(short, long, default_value_t = update::max_backlog() as u32)]
         /// The number of videos to updating
         max_backlog: u32,
 
@@ -192,12 +200,11 @@ pub enum SelectCommand {
         shared: SharedSelectionCommandArgs,
 
         /// The subtitles to download (e.g. 'en,de,sv')
-        #[arg(short = 'l', long, default_value = constants::DEFAULT_SUBTITLE_LANGS)]
+        #[arg(short = 'l', long, default_value = select::subtitle_langs())]
         subtitle_langs: String,
 
         /// The speed to set mpv to
-        // NOTE: KEEP THIS IN SYNC WITH THE `DEFAULT_MPV_PLAYBACK_SPEED` in `constants.rs` <2024-08-20>
-        #[arg(short, long, default_value = "2.7")]
+        #[arg(short, long, default_value_t = select::playback_speed())]
         speed: f64,
     },
 
diff --git a/src/config/default.rs b/src/config/default.rs
new file mode 100644
index 0000000..131c289
--- /dev/null
+++ b/src/config/default.rs
@@ -0,0 +1,100 @@
+// 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::path::PathBuf;
+
+use anyhow::{Context, Result};
+
+fn get_runtime_path(name: &'static str) -> Result<PathBuf> {
+    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?;
+    xdg_dirs
+        .place_runtime_file(name)
+        .with_context(|| format!("Failed to place runtime file: '{}'", name))
+}
+fn get_data_path(name: &'static str) -> Result<PathBuf> {
+    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?;
+    xdg_dirs
+        .place_data_file(name)
+        .with_context(|| format!("Failed to place data file: '{}'", name))
+}
+fn get_config_path(name: &'static str) -> Result<PathBuf> {
+    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?;
+    xdg_dirs
+        .place_config_file(name)
+        .with_context(|| format!("Failed to place config file: '{}'", name))
+}
+
+pub(super) fn create_path(path: PathBuf) -> Result<PathBuf> {
+    if !path.exists() {
+        std::fs::create_dir_all(&path)
+            .with_context(|| format!("Failed to create the '{}' directory", path.display()))?
+    }
+
+    Ok(path)
+}
+
+pub const PREFIX: &str = "yt";
+
+pub mod select {
+    pub fn playback_speed() -> f64 {
+        2.7
+    }
+    pub fn subtitle_langs() -> &'static str {
+        ""
+    }
+}
+
+pub mod watch {
+    pub fn local_comments_length() -> i64 {
+        1000
+    }
+}
+
+pub mod update {
+    pub fn max_backlog() -> i64 {
+        20
+    }
+}
+
+pub mod paths {
+    use std::{env::temp_dir, path::PathBuf};
+
+    use anyhow::Result;
+
+    use super::{create_path, get_config_path, get_data_path, get_runtime_path, PREFIX};
+
+    // We download to the temp dir to avoid taxing the disk
+    pub fn download_dir() -> Result<PathBuf> {
+        let temp_dir = temp_dir();
+
+        create_path(temp_dir.join(PREFIX))
+    }
+    pub fn mpv_config_path() -> Result<PathBuf> {
+        get_config_path("mpv.conf")
+    }
+    pub fn mpv_input_path() -> Result<PathBuf> {
+        get_config_path("mpv.input.conf")
+    }
+    pub fn database_path() -> Result<PathBuf> {
+        get_data_path("videos.sqlite")
+    }
+    pub fn config_path() -> Result<PathBuf> {
+        get_config_path("config.toml")
+    }
+    pub fn last_selection_path() -> Result<PathBuf> {
+        get_runtime_path("selected.yts")
+    }
+}
+
+pub mod download {
+    pub fn max_cache_size() -> &'static str {
+        "3 GiB"
+    }
+}
diff --git a/src/config/file_system.rs b/src/config/file_system.rs
new file mode 100644
index 0000000..8528130
--- /dev/null
+++ b/src/config/file_system.rs
@@ -0,0 +1,123 @@
+// 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 crate::config::{DownloadConfig, PathsConfig, SelectConfig, WatchConfig};
+
+use super::{
+    default::{create_path, download, paths, select, update, watch},
+    Config, UpdateConfig,
+};
+
+use std::{fs::read_to_string, path::PathBuf};
+
+use anyhow::{Context, Result};
+use toml::Table;
+use bytes::Bytes;
+
+macro_rules! get {
+    ($default:path, $config:expr, $get_fn:ident, $key_one:expr, $($keys:expr),*) => {
+        try_get!{@default $default, $config, $get_fn, $key_one, $($keys),*}
+             .with_context(|| format!("Failed to parse '{}' as a '{}'", stringify!($key_one), stringify!($get_fn)))?
+    };
+    (@path_if_none $config:expr, $option_default:expr, $default:path, $key_one:expr, $($keys:expr),*) => {
+        {
+            let maybe_download_dir =
+                try_get! {@option $config, as_str, $key_one, $($keys),*};
+
+            let down_dir = if let Some(dir) = maybe_download_dir {
+                PathBuf::from(dir)
+            } else {
+                if let Some(path) = $option_default {
+                    path
+                } else {
+                    $default()
+                        .with_context(|| format!("Failed to get default path for: '{}.{}'", stringify!($key_one), stringify!($($keys),*)))?
+                }
+            };
+            create_path(down_dir)?
+        }
+    };
+    (@path $config:expr, $default:path, $key_one:expr, $($keys:expr),*) => {
+        get! {@path_if_none $config, None, $default, $key_one, $($keys),*}
+    };
+}
+macro_rules! try_get {
+    (@option $config:expr, $get_fn:ident, $key_one:expr, $($keys:expr),*) => {
+        $config.get($key_one).map(|val| {
+            try_get! {@option val, $get_fn, $($keys),*}
+        }).flatten().flatten()
+    };
+    (@option $config:expr, $get_fn:ident, $key_one:expr) => {
+        $config.get($key_one).map(|val| val.$get_fn())
+    };
+
+    (@default $default:path, $config:expr, $get_fn:ident, $key_one:expr, $($keys:expr),*) => {
+        if let Some(a) = $config.get($key_one) {
+            try_get! {@default $default, a, $get_fn, $($keys),*}
+        } else {
+            Some($default())
+        }
+    };
+    (@default $default:path, $config:expr, $get_fn:ident, $key_one:expr) => {
+        if let Some(a) = $config.get($key_one) {
+            a.$get_fn()
+        } else {
+            Some($default())
+        }
+    };
+}
+
+impl Config {
+    pub fn from_config_file(
+        db_path: Option<PathBuf>,
+        config_path: Option<PathBuf>,
+    ) -> Result<Self> {
+        let config_file_path = config_path
+            .map(|val| Ok(val))
+            .unwrap_or_else(|| -> Result<_> { paths::config_path() })?;
+
+        let config: Table = read_to_string(config_file_path)?
+            .parse()
+            .context("Failed to parse the config file as toml")?;
+
+        Ok(Self {
+            select: SelectConfig {
+                playback_speed: get! {select::playback_speed, config, as_float, "select", "playback_speed"},
+                subtitle_langs:
+                    get! {select::subtitle_langs, config, as_str, "select", "subtitle_langs"}
+                        .to_owned(),
+            },
+            watch: WatchConfig {
+                local_comments_length: get! {watch::local_comments_length, config, as_integer, "watch", "local_comments_length"}
+                    as usize,
+            },
+            update: UpdateConfig {
+                max_backlog: get! {update::max_backlog, config, as_integer, "update", "max_backlog"}
+                    as u32,
+            },
+            paths: PathsConfig {
+                download_dir: get! {@path config, paths::download_dir, "paths", "download_dir"},
+                mpv_config_path: get! {@path config, paths::mpv_config_path, "paths", "mpv_config_path"},
+                mpv_input_path: get! {@path config, paths::mpv_input_path, "paths", "mpv_input_path"},
+                database_path: get! {@path_if_none config, db_path, paths::database_path, "paths", "database_path"},
+                last_selection_path: get! {@path config, paths::last_selection_path, "paths", "last_selection_path"},
+            },
+            download: DownloadConfig {
+                max_cache_size: {
+                    let bytes_str = get! {download::max_cache_size, config, as_str, "download", "max_cache_path"};
+                    let number: Bytes = bytes_str
+                        .parse()
+                        .context("Failed to parse max_cache_size")?;
+                    number.as_u64()
+                },
+            },
+        })
+    }
+}
diff --git a/src/config/mod.rs b/src/config/mod.rs
new file mode 100644
index 0000000..8ee7cc7
--- /dev/null
+++ b/src/config/mod.rs
@@ -0,0 +1,52 @@
+// 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::path::PathBuf;
+
+pub mod default;
+pub mod file_system;
+
+pub struct Config {
+    pub select: SelectConfig,
+    pub watch: WatchConfig,
+    pub paths: PathsConfig,
+    pub download: DownloadConfig,
+    pub update: UpdateConfig,
+}
+pub struct UpdateConfig {
+    pub max_backlog: u32,
+}
+pub struct DownloadConfig {
+    pub max_cache_size: u64,
+}
+pub struct SelectConfig {
+    pub playback_speed: f64,
+    pub subtitle_langs: String,
+}
+pub struct WatchConfig {
+    pub local_comments_length: usize,
+}
+pub struct PathsConfig {
+    pub download_dir: PathBuf,
+    pub mpv_config_path: PathBuf,
+    pub mpv_input_path: PathBuf,
+    pub database_path: PathBuf,
+    pub last_selection_path: PathBuf,
+}
+
+// pub fn status_path() -> anyhow::Result<PathBuf> {
+//     const STATUS_PATH: &str = "running.info.json";
+//     get_runtime_path(STATUS_PATH)
+// }
+
+// pub fn subscriptions() -> anyhow::Result<PathBuf> {
+//     const SUBSCRIPTIONS: &str = "subscriptions.json";
+//     get_data_path(SUBSCRIPTIONS)
+// }
diff --git a/src/constants.rs b/src/constants.rs
index cb8388d..54cae89 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -8,78 +8,4 @@
 // 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::{env::temp_dir, fs, path::PathBuf};
-
-use anyhow::{Context, Result};
-
 pub const HELP_STR: &str = include_str!("./select/selection_file/help.str");
-pub const LOCAL_COMMENTS_LENGTH: usize = 1000;
-
-// NOTE: KEEP THIS IN SYNC WITH THE `mpv_playback_speed` in `cli.rs` <2024-08-20>
-pub const DEFAULT_MPV_PLAYBACK_SPEED: f64 = 2.7;
-pub const DEFAULT_SUBTITLE_LANGS: &str = "";
-
-pub const CONCURRENT_DOWNLOADS: u32 = 5;
-// We download to the temp dir to avoid taxing the disk
-pub fn download_dir(create: bool) -> Result<PathBuf> {
-    const DOWNLOAD_DIR: &str = "/tmp/yt";
-    let dir = PathBuf::from(DOWNLOAD_DIR);
-
-    if !dir.exists() && create {
-        fs::create_dir_all(&dir).context("Failed to create the download directory")?
-    }
-
-    Ok(dir)
-}
-
-const PREFIX: &str = "yt";
-fn get_runtime_path(name: &'static str) -> anyhow::Result<PathBuf> {
-    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?;
-    xdg_dirs
-        .place_runtime_file(name)
-        .with_context(|| format!("Failed to place runtime file: '{}'", name))
-}
-fn get_data_path(name: &'static str) -> anyhow::Result<PathBuf> {
-    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?;
-    xdg_dirs
-        .place_data_file(name)
-        .with_context(|| format!("Failed to place data file: '{}'", name))
-}
-fn get_config_path(name: &'static str) -> anyhow::Result<PathBuf> {
-    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?;
-    xdg_dirs
-        .place_config_file(name)
-        .with_context(|| format!("Failed to place config file: '{}'", name))
-}
-
-pub fn mpv_config_path() -> anyhow::Result<PathBuf> {
-    const MPV_CONFIG_PATH: &str = "mpv.conf";
-    get_config_path(MPV_CONFIG_PATH)
-}
-pub fn mpv_input_path() -> anyhow::Result<PathBuf> {
-    const MPV_INPUT_CONFIG_PATH: &str = "mpv.input.conf";
-    get_config_path(MPV_INPUT_CONFIG_PATH)
-}
-
-pub fn status_path() -> anyhow::Result<PathBuf> {
-    const STATUS_PATH: &str = "running.info.json";
-    get_runtime_path(STATUS_PATH)
-}
-pub fn last_select() -> anyhow::Result<PathBuf> {
-    const LAST_SELECT: &str = "selected.yts";
-    get_runtime_path(LAST_SELECT)
-}
-
-pub fn database() -> anyhow::Result<PathBuf> {
-    const DATABASE: &str = "videos.sqlite";
-    get_data_path(DATABASE)
-}
-pub fn subscriptions() -> anyhow::Result<PathBuf> {
-    const SUBSCRIPTIONS: &str = "subscriptions.json";
-    get_data_path(SUBSCRIPTIONS)
-}
-
-pub fn cache_path() -> PathBuf {
-    let temp_dir = temp_dir();
-    temp_dir.join("ytc")
-}
diff --git a/src/download/download_options.rs b/src/download/download_options.rs
index 04c1600..e93170a 100644
--- a/src/download/download_options.rs
+++ b/src/download/download_options.rs
@@ -10,7 +10,7 @@
 
 use serde_json::{json, Value};
 
-use crate::{constants, storage::video_database::YtDlpOptions};
+use crate::{app::App, storage::video_database::YtDlpOptions};
 
 // {
 //     "ratelimit": conf.ratelimit if conf.ratelimit > 0 else None,
@@ -22,7 +22,10 @@ use crate::{constants, storage::video_database::YtDlpOptions};
 //     "logger": _ytdl_logger
 // }
 
-pub fn download_opts(additional_opts: YtDlpOptions) -> serde_json::Map<String, serde_json::Value> {
+pub fn download_opts(
+    app: &App,
+    additional_opts: YtDlpOptions,
+) -> serde_json::Map<String, serde_json::Value> {
     match json!({
       "extract_flat": false,
       "extractor_args": {
@@ -50,7 +53,7 @@ pub fn download_opts(additional_opts: YtDlpOptions) -> serde_json::Map<String, s
       "writeautomaticsub": true,
 
       "outtmpl": {
-        "default": constants::download_dir(false).expect("We're not creating this dir, thus this function can't error").join("%(channel)s/%(title)s.%(ext)s"),
+        "default": app.config.paths.download_dir.join("%(channel)s/%(title)s.%(ext)s"),
         "chapter": "%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s"
       },
       "compat_opts": {},
diff --git a/src/download/mod.rs b/src/download/mod.rs
index c3d79b7..707f281 100644
--- a/src/download/mod.rs
+++ b/src/download/mod.rs
@@ -12,7 +12,6 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
 
 use crate::{
     app::App,
-    constants::download_dir,
     download::download_options::download_opts,
     storage::video_database::{
         downloader::{get_next_uncached_video, set_video_cache_path},
@@ -72,8 +71,8 @@ impl Downloader {
     /// This will run, until the database doesn't contain any watchable videos
     pub async fn consume(&mut self, app: Arc<App>, max_cache_size: u64) -> Result<()> {
         while let Some(next_video) = get_next_uncached_video(&app).await? {
-            if Self::get_current_cache_allocation().await?
-                + self.get_approx_video_size(&next_video).await?
+            if Self::get_current_cache_allocation(&app).await?
+                + self.get_approx_video_size(&app, &next_video).await?
                 >= max_cache_size
             {
                 warn!(
@@ -134,7 +133,7 @@ impl Downloader {
         Ok(())
     }
 
-    async fn get_current_cache_allocation() -> Result<u64> {
+    async fn get_current_cache_allocation(app: &App) -> Result<u64> {
         fn dir_size(mut dir: fs::ReadDir) -> BoxFuture<'static, Result<u64>> {
             async move {
                 let mut acc = 0;
@@ -155,14 +154,14 @@ impl Downloader {
             .boxed()
         }
 
-        let val = dir_size(fs::read_dir(download_dir(true)?).await?).await;
+        let val = dir_size(fs::read_dir(&app.config.paths.download_dir).await?).await;
         if let Ok(val) = val.as_ref() {
             info!("Cache dir has a size of '{}'", val);
         }
         val
     }
 
-    async fn get_approx_video_size(&mut self, video: &Video) -> Result<u64> {
+    async fn get_approx_video_size(&mut self, app: &App, video: &Video) -> Result<u64> {
         if let Some(value) = self.video_size_cache.get(&video.extractor_hash) {
             Ok(*value)
         } else {
@@ -170,7 +169,7 @@ impl Downloader {
             let add_opts = YtDlpOptions {
                 subtitle_langs: "".to_owned(),
             };
-            let opts = &download_opts(add_opts);
+            let opts = &download_opts(&app, add_opts);
 
             let result = yt_dlp::extract_info(&opts, &video.url, false, true)
                 .await
@@ -201,7 +200,7 @@ impl Downloader {
 
         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))
+        let result = yt_dlp::download(&[video.url.clone()], &download_opts(&app, addional_opts))
             .await
             .with_context(|| format!("Failed to download video: '{}'", video.title))?;
 
diff --git a/src/main.rs b/src/main.rs
index 7852aa0..a6766f6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,6 +15,7 @@ use app::App;
 use cache::invalidate;
 use clap::Parser;
 use cli::{CacheCommand, CheckCommand, SelectCommand, SubscriptionCommand};
+use config::Config;
 use log::info;
 use select::cmds::handle_select_cmd;
 use tokio::{
@@ -31,6 +32,7 @@ pub mod cli;
 
 pub mod cache;
 pub mod comments;
+pub mod config;
 pub mod constants;
 pub mod download;
 pub mod select;
@@ -54,7 +56,8 @@ async fn main() -> Result<()> {
         .init()
         .expect("Let's just hope that this does not panic");
 
-    let app = App::new(args.db_path.unwrap_or(constants::database()?)).await?;
+    let config = Config::from_config_file(args.db_path, args.config_path)?;
+    let app = App::new(config).await?;
 
     match args.command.unwrap_or(Command::default()) {
         Command::Download {
diff --git a/src/select/mod.rs b/src/select/mod.rs
index 6774ce6..2288e1a 100644
--- a/src/select/mod.rs
+++ b/src/select/mod.rs
@@ -18,7 +18,7 @@ use std::{
 use crate::{
     app::App,
     cli::CliArgs,
-    constants::{last_select, HELP_STR},
+    constants::HELP_STR,
     storage::video_database::{getters::get_videos, VideoStatus},
 };
 
@@ -111,11 +111,8 @@ pub async fn select(app: &App, done: bool) -> Result<()> {
     }
 
     let read_file = temp_file.reopen()?;
-    fs::copy(
-        temp_file.path(),
-        last_select().context("Failed to get the persistent selection file path")?,
-    )
-    .context("Failed to persist selection file")?;
+    fs::copy(temp_file.path(), &app.config.paths.last_selection_path)
+        .context("Failed to persist selection file")?;
 
     let reader = BufReader::new(&read_file);
 
diff --git a/src/select/selection_file/display.rs b/src/select/selection_file/display.rs
index 12d128c..0a0ce96 100644
--- a/src/select/selection_file/display.rs
+++ b/src/select/selection_file/display.rs
@@ -32,7 +32,7 @@ impl Video {
 
         let opts = get_video_opts(app, &self.extractor_hash)
             .await?
-            .to_cli_flags();
+            .to_cli_flags(&app);
         let opts_white = if !opts.is_empty() { " " } else { "" };
 
         let publish_date = if let Some(date) = self.publish_date {
diff --git a/src/storage/video_database/mod.rs b/src/storage/video_database/mod.rs
index 28263ca..da08f8f 100644
--- a/src/storage/video_database/mod.rs
+++ b/src/storage/video_database/mod.rs
@@ -12,10 +12,7 @@ use std::{fmt::Write, path::PathBuf};
 
 use url::Url;
 
-use crate::{
-    constants::{DEFAULT_MPV_PLAYBACK_SPEED, DEFAULT_SUBTITLE_LANGS},
-    storage::video_database::extractor_hash::ExtractorHash,
-};
+use crate::{app::App, storage::video_database::extractor_hash::ExtractorHash};
 
 pub mod downloader;
 pub mod extractor_hash;
@@ -55,13 +52,13 @@ impl VideoOptions {
 
     /// This will write out the options that are different from the defaults.
     /// Beware, that this does not set the priority.
-    pub fn to_cli_flags(self) -> String {
+    pub fn to_cli_flags(self, app: &App) -> String {
         let mut f = String::new();
 
-        if self.mpv.playback_speed != DEFAULT_MPV_PLAYBACK_SPEED {
+        if self.mpv.playback_speed != app.config.select.playback_speed {
             write!(f, " --speed '{}'", self.mpv.playback_speed).expect("Works");
         }
-        if self.yt_dlp.subtitle_langs != DEFAULT_SUBTITLE_LANGS {
+        if self.yt_dlp.subtitle_langs != app.config.select.subtitle_langs {
             write!(f, " --subtitle-langs '{}'", self.yt_dlp.subtitle_langs).expect("Works");
         }
 
diff --git a/src/storage/video_database/setters.rs b/src/storage/video_database/setters.rs
index 42875ce..e2b38e6 100644
--- a/src/storage/video_database/setters.rs
+++ b/src/storage/video_database/setters.rs
@@ -16,7 +16,10 @@ use log::debug;
 use sqlx::query;
 use tokio::fs;
 
-use crate::{app::App, constants, storage::video_database::extractor_hash::ExtractorHash};
+use crate::{
+    app::App,
+    storage::video_database::extractor_hash::ExtractorHash,
+};
 
 use super::{Video, VideoOptions, VideoStatus};
 
@@ -213,8 +216,8 @@ pub async fn add_video(app: &App, video: Video) -> Result<()> {
     let url = video.url.to_string();
     let extractor_hash = video.extractor_hash.hash().to_string();
 
-    let default_subtitle_langs = constants::DEFAULT_SUBTITLE_LANGS;
-    let default_mpv_playback_speed = constants::DEFAULT_MPV_PLAYBACK_SPEED;
+    let default_subtitle_langs = &app.config.select.subtitle_langs;
+    let default_mpv_playback_speed = app.config.select.playback_speed;
 
     query!(
         r#"
diff --git a/src/watch/events.rs b/src/watch/events.rs
index adb35e5..df414ff 100644
--- a/src/watch/events.rs
+++ b/src/watch/events.rs
@@ -18,7 +18,6 @@ use tokio::process::Command;
 use crate::{
     app::App,
     comments::get_comments,
-    constants::LOCAL_COMMENTS_LENGTH,
     storage::video_database::{
         extractor_hash::ExtractorHash,
         getters::{get_video_by_hash, get_video_mpv_opts, get_videos},
@@ -213,7 +212,7 @@ impl MpvEventHandler {
                             .replace("\"", "")
                             .replace("'", "")
                             .chars()
-                            .take(LOCAL_COMMENTS_LENGTH)
+                            .take(app.config.watch.local_comments_length)
                             .collect();
 
                         mpv.execute("show-text", &[&format!("'{}'", comments), "6000"])?;
diff --git a/src/watch/mod.rs b/src/watch/mod.rs
index 374c1d7..815e208 100644
--- a/src/watch/mod.rs
+++ b/src/watch/mod.rs
@@ -16,7 +16,6 @@ use log::{debug, info, warn};
 use crate::{
     app::App,
     cache::maintain,
-    constants::{mpv_config_path, mpv_input_path},
     storage::video_database::{extractor_hash::ExtractorHash, getters::get_videos, VideoStatus},
 };
 
@@ -41,7 +40,7 @@ pub async fn watch(app: &App) -> Result<()> {
         Ok(())
     })?;
 
-    let config_path = mpv_config_path()?;
+    let config_path = &app.config.paths.mpv_config_path;
     if config_path.try_exists()? {
         info!("Found mpv.conf at '{}'!", config_path.display());
         mpv.execute(
@@ -55,7 +54,7 @@ pub async fn watch(app: &App) -> Result<()> {
         );
     }
 
-    let input_path = mpv_input_path()?;
+    let input_path = &app.config.paths.mpv_input_path;
     if input_path.try_exists()? {
         info!("Found mpv.input.conf at '{}'!", input_path.display());
         mpv.execute(