aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-07-10 16:41:05 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-07-10 16:41:05 +0200
commit97537059b44f5ed336a915a1ba805be215cf6566 (patch)
tree51c008494c5126942d2f227837e2b6433e62327c
parentrefactor(crates/yt_dlp): Port to `pyo3` again (diff)
downloadyt-97537059b44f5ed336a915a1ba805be215cf6566.zip
refactor(crates/yt/config): Use a macro to generate the config parsing code
This makes adding new config values easier and makes it harder to introduce slight bugs (with the old config system the cli `--db-path` flag did only take effect, after the value in the config file).
Diffstat (limited to '')
-rw-r--r--crates/yt/src/config/default.rs110
-rw-r--r--crates/yt/src/config/definitions.rs67
-rw-r--r--crates/yt/src/config/file_system.rs120
-rw-r--r--crates/yt/src/config/mod.rs134
-rw-r--r--crates/yt/src/config/paths.rs50
-rw-r--r--crates/yt/src/config/support.rs151
-rw-r--r--crates/yt/src/main.rs8
7 files changed, 274 insertions, 366 deletions
diff --git a/crates/yt/src/config/default.rs b/crates/yt/src/config/default.rs
deleted file mode 100644
index 4ed643b..0000000
--- a/crates/yt/src/config/default.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 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() {
- if let Some(parent) = path.parent() {
- std::fs::create_dir_all(parent)
- .with_context(|| format!("Failed to create the '{}' directory", path.display()))?;
- }
- }
-
- Ok(path)
-}
-
-pub(crate) const PREFIX: &str = "yt";
-
-pub(crate) mod global {
- pub(crate) fn display_colors() -> bool {
- // TODO: This should probably check if the output is a tty and otherwise return `false` <2025-02-14>
- true
- }
-}
-
-pub(crate) mod select {
- pub(crate) fn playback_speed() -> f64 {
- 2.7
- }
- pub(crate) fn subtitle_langs() -> &'static str {
- ""
- }
-}
-
-pub(crate) mod watch {
- pub(crate) fn local_displays_length() -> usize {
- 1000
- }
-}
-
-pub(crate) mod update {
- pub(crate) fn max_backlog() -> usize {
- 20
- }
-}
-
-pub(crate) mod paths {
- use std::{env::temp_dir, path::PathBuf};
-
- use anyhow::Result;
-
- use super::{PREFIX, create_path, get_config_path, get_data_path, get_runtime_path};
-
- // We download to the temp dir to avoid taxing the disk
- pub(crate) fn download_dir() -> Result<PathBuf> {
- let temp_dir = temp_dir();
-
- create_path(temp_dir.join(PREFIX))
- }
- pub(crate) fn mpv_config_path() -> Result<PathBuf> {
- get_config_path("mpv.conf")
- }
- pub(crate) fn mpv_input_path() -> Result<PathBuf> {
- get_config_path("mpv.input.conf")
- }
- pub(crate) fn database_path() -> Result<PathBuf> {
- get_data_path("videos.sqlite")
- }
- pub(crate) fn config_path() -> Result<PathBuf> {
- get_config_path("config.toml")
- }
- pub(crate) fn last_selection_path() -> Result<PathBuf> {
- get_runtime_path("selected.yts")
- }
-}
-
-pub(crate) mod download {
- pub(crate) fn max_cache_size() -> &'static str {
- "3 GiB"
- }
-}
diff --git a/crates/yt/src/config/definitions.rs b/crates/yt/src/config/definitions.rs
deleted file mode 100644
index ce8c0d4..0000000
--- a/crates/yt/src/config/definitions.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 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 serde::Deserialize;
-
-#[derive(Debug, Deserialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct ConfigFile {
- pub global: Option<GlobalConfig>,
- pub select: Option<SelectConfig>,
- pub watch: Option<WatchConfig>,
- pub paths: Option<PathsConfig>,
- pub download: Option<DownloadConfig>,
- pub update: Option<UpdateConfig>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct GlobalConfig {
- pub display_colors: Option<bool>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct UpdateConfig {
- pub max_backlog: Option<usize>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct DownloadConfig {
- /// This will then be converted to an u64
- pub max_cache_size: Option<String>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct SelectConfig {
- pub playback_speed: Option<f64>,
- pub subtitle_langs: Option<String>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct WatchConfig {
- pub local_displays_length: Option<usize>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct PathsConfig {
- pub download_dir: Option<PathBuf>,
- pub mpv_config_path: Option<PathBuf>,
- pub mpv_input_path: Option<PathBuf>,
- pub database_path: Option<PathBuf>,
- pub last_selection_path: Option<PathBuf>,
-}
diff --git a/crates/yt/src/config/file_system.rs b/crates/yt/src/config/file_system.rs
deleted file mode 100644
index 2463e9d..0000000
--- a/crates/yt/src/config/file_system.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 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::{
- Config, GlobalConfig, UpdateConfig,
- default::{create_path, download, global, paths, select, update, watch},
-};
-
-use std::{fs::read_to_string, path::PathBuf};
-
-use anyhow::{Context, Result};
-use bytes::Bytes;
-
-macro_rules! get {
- ($default:path, $config:expr, $key_one:ident, $($keys:ident),*) => {
- {
- let maybe_value = get!{@option $config, $key_one, $($keys),*};
- if let Some(value) = maybe_value {
- value
- } else {
- $default().to_owned()
- }
- }
- };
-
- (@option $config:expr, $key_one:ident, $($keys:ident),*) => {
- if let Some(key) = $config.$key_one.clone() {
- get!{@option key, $($keys),*}
- } else {
- None
- }
- };
- (@option $config:expr, $key_one:ident) => {
- $config.$key_one
- };
-
- (@path_if_none $config:expr, $option_default:expr, $default:path, $key_one:ident, $($keys:ident),*) => {
- {
- let maybe_download_dir: Option<PathBuf> =
- get! {@option $config, $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:ident, $($keys:ident),*) => {
- get! {@path_if_none $config, None, $default, $key_one, $($keys),*}
- };
-}
-
-impl Config {
- pub fn from_config_file(
- db_path: Option<PathBuf>,
- config_path: Option<PathBuf>,
- display_colors: Option<bool>,
- ) -> Result<Self> {
- let config_file_path =
- config_path.map_or_else(|| -> Result<_> { paths::config_path() }, Ok)?;
-
- let config: super::definitions::ConfigFile =
- toml::from_str(&read_to_string(config_file_path).unwrap_or(String::new()))
- .context("Failed to parse the config file as toml")?;
-
- Ok(Self {
- global: GlobalConfig {
- display_colors: {
- let config_value: Option<bool> = get! {@option config, global, display_colors};
-
- display_colors.unwrap_or(config_value.unwrap_or_else(global::display_colors))
- },
- },
- select: SelectConfig {
- playback_speed: get! {select::playback_speed, config, select, playback_speed},
- subtitle_langs: get! {select::subtitle_langs, config, select, subtitle_langs},
- },
- watch: WatchConfig {
- local_displays_length: get! {watch::local_displays_length, config, watch, local_displays_length},
- },
- update: UpdateConfig {
- max_backlog: get! {update::max_backlog, config, update, max_backlog},
- },
- 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: String =
- get! {download::max_cache_size, config, download, max_cache_size};
- let number: Bytes = bytes_str
- .parse()
- .context("Failed to parse max_cache_size")?;
- number
- },
- },
- })
- }
-}
diff --git a/crates/yt/src/config/mod.rs b/crates/yt/src/config/mod.rs
index a10f7c2..154a109 100644
--- a/crates/yt/src/config/mod.rs
+++ b/crates/yt/src/config/mod.rs
@@ -1,76 +1,74 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 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::support::mk_config;
-#![allow(clippy::module_name_repetitions)]
+mod paths;
+mod support;
-use std::path::PathBuf;
+mk_config! {
+ use std::path::PathBuf;
+ use std::io::IsTerminal;
-use bytes::Bytes;
-use serde::Serialize;
+ use crate::shared::bytes::Bytes;
-mod default;
-mod definitions;
-pub mod file_system;
+ use super::paths::ensure_parent_dir;
-#[derive(Serialize, Debug)]
-pub struct Config {
- pub global: GlobalConfig,
- pub select: SelectConfig,
- pub watch: WatchConfig,
- pub paths: PathsConfig,
- pub download: DownloadConfig,
- pub update: UpdateConfig,
-}
-// These structures could get non-copy fields in the future.
+ struct Config {
+ global: GlobalConfig = {
+ /// Whether to display colors.
+ display_colors: bool where display_color: Option<bool> =! {|config_value: Option<bool>|
+ Ok::<_, anyhow::Error>(
+ display_color
+ .unwrap_or(
+ config_value
+ .unwrap_or_else(|| std::io::stderr().is_terminal())
+ )
+ )
+ },
+ },
+ select: SelectConfig = {
+ /// The playback speed to use, when it is not overridden.
+ playback_speed: f64 =: 2.7,
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct GlobalConfig {
- pub display_colors: bool,
-}
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct UpdateConfig {
- pub max_backlog: usize,
-}
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct DownloadConfig {
- pub max_cache_size: Bytes,
-}
-#[derive(Serialize, Debug)]
-pub struct SelectConfig {
- pub playback_speed: f64,
- pub subtitle_langs: String,
-}
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct WatchConfig {
- pub local_displays_length: usize,
-}
-#[derive(Serialize, Debug)]
-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,
-}
+ /// The subtitle langs to download, when it is not overridden.
+ subtitle_langs: String =: String::new(),
+ },
+ watch: WatchConfig = {
+ /// How many chars to display at most, when displaying information on mpv's local on screen
+ /// display.
+ local_displays_length: usize =: 1000,
+ },
+ paths: PathsConfig = {
+ /// Where to store downloaded files.
+ download_dir: PathBuf =: {
+ // We download to the temp dir to avoid taxing the disk
+ let temp_dir = std::env::temp_dir();
+
+ temp_dir.join(super::paths::PREFIX)
+ } => ensure_parent_dir,
+
+ /// Path to the mpv configuration file.
+ mpv_config_path: PathBuf =? super::paths::get_config_path("mpv.conf") => ensure_parent_dir,
-// pub fn status_path() -> anyhow::Result<PathBuf> {
-// const STATUS_PATH: &str = "running.info.json";
-// get_runtime_path(STATUS_PATH)
-// }
+ /// Path to the mpv input configuration file.
+ mpv_input_path: PathBuf =? super::paths::get_config_path("mpv.input.conf") => ensure_parent_dir,
-// pub fn subscriptions() -> anyhow::Result<PathBuf> {
-// const SUBSCRIPTIONS: &str = "subscriptions.json";
-// get_data_path(SUBSCRIPTIONS)
-// }
+ /// Which path to use for mpv ipc socket creation.
+ mpv_ipc_socket_path: PathBuf =? super::paths::get_runtime_path("mpv.ipc.socket") => ensure_parent_dir,
+
+ /// Path to the video database.
+ database_path: PathBuf where db_path: Option<PathBuf> =! {|config_value: Option<PathBuf>| {
+ db_path.map_or_else(|| config_value.map_or_else(|| super::paths::get_data_path("videos.sqlite"), Ok), Ok)
+ }} => ensure_parent_dir,
+
+ /// Where to store the selection file before applying it.
+ last_selection_path: PathBuf =? super::paths::get_runtime_path("selected.yts") => ensure_parent_dir,
+ },
+ download: DownloadConfig = {
+ /// The maximum cache size.
+ max_cache_size: Bytes =? "3 GiB".parse(),
+ },
+ update: UpdateConfig = {
+ /// How many videos to download, when checking for new ones.
+ max_backlog: usize =: 20,
+ },
+ }
+}
diff --git a/crates/yt/src/config/paths.rs b/crates/yt/src/config/paths.rs
new file mode 100644
index 0000000..224891b
--- /dev/null
+++ b/crates/yt/src/config/paths.rs
@@ -0,0 +1,50 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 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::{Path, PathBuf};
+
+use anyhow::{Context, Result};
+
+pub(super) 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}'"))
+}
+pub(super) 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}'"))
+}
+pub(super) 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 ensure_parent_dir(path: &Path) -> Result<()> {
+ if !path.exists() {
+ if let Some(parent) = path.parent() {
+ std::fs::create_dir_all(parent)
+ .with_context(|| format!("Failed to create the '{}' directory", path.display()))?;
+ }
+ }
+
+ Ok(())
+}
+
+pub(super) fn config_path() -> Result<PathBuf> {
+ get_config_path("config.toml")
+}
+
+pub(crate) const PREFIX: &str = "yt";
diff --git a/crates/yt/src/config/support.rs b/crates/yt/src/config/support.rs
new file mode 100644
index 0000000..880eba7
--- /dev/null
+++ b/crates/yt/src/config/support.rs
@@ -0,0 +1,151 @@
+macro_rules! mk_config {
+ (
+ $(use $usage_path:path;)*
+
+ struct $name:ident {
+ $(
+ $(#[$attr0:meta])*
+ $subconfig_name:ident : $subconfig_type:ident = {
+ $(
+ $(#[$attr1:meta])*
+ $field_name:ident : $field_type:ty $(
+ where $extra_input:ident: $extra_input_type:ty
+ ),* = $errors:tt $default:expr $(=> $finalizer:ident)?
+ ),*
+ $(,)?
+ }
+ ),*
+ $(,)?
+ }
+ ) => {
+ mod _inner {
+ #![allow(non_snake_case)]
+
+ $(use $usage_path;)*
+
+ #[derive(serde::Serialize, Debug)]
+ pub(crate) struct $name {
+ $(
+ $(#[$attr0])*
+ pub(crate) $subconfig_name: $subconfig_type
+ ),*
+ }
+
+ #[derive(Debug, serde::Deserialize, PartialEq)]
+ #[serde(deny_unknown_fields)]
+ #[allow(non_camel_case_types)]
+ struct config {
+ $(
+ $subconfig_name: Option<$subconfig_name>
+ ),*
+ }
+
+ impl $name {
+ pub(crate) fn from_config_file(
+ config_file_path: Option<std::path::PathBuf>,
+ $(
+ $(
+ $(
+ $extra_input: $extra_input_type,
+ )*
+ )*
+ )*
+ ) -> anyhow::Result<Self> {
+ use anyhow::Context;
+
+ let config_file_path =
+ config_file_path.map_or_else(|| -> anyhow::Result<_> { super::paths::config_path() }, Ok)?;
+
+ let config: config =
+ toml::from_str(&std::fs::read_to_string(config_file_path).unwrap_or(String::new()))
+ .context("Failed to parse the config file as toml")?;
+
+ Ok(Self {
+ $(
+ $subconfig_name: {
+ let toplevel = config.$subconfig_name.unwrap_or_default();
+ $subconfig_type {
+ $(
+ $field_name: $field_name(toplevel.$field_name, $($extra_input),*)?
+ ),*
+ }
+ }
+ ),*
+ })
+ }
+
+ pub(crate) fn run_finalizers(&self) -> anyhow::Result<()> {
+ #[allow(unused_imports)]
+ use anyhow::Context;
+
+ $(
+ $(
+ $(
+ $finalizer(&self.$subconfig_name.$field_name)
+ .context(
+ concat!(
+ "While running the finalizer for config value '",
+ stringify!($subconfig_name),
+ ".",
+ stringify!($field_name),
+ "'"
+ )
+ )?;
+ )?
+ )*
+ )*
+
+ Ok(())
+ }
+ }
+
+ $(
+ #[derive(serde::Serialize, Debug)]
+ pub(crate) struct $subconfig_type {
+ $(
+ $(#[$attr1])*
+ pub(crate) $field_name: $field_type
+ ),*
+ }
+
+ #[derive(Debug, Default, serde::Deserialize, PartialEq)]
+ #[serde(deny_unknown_fields)]
+ #[allow(non_camel_case_types)]
+ struct $subconfig_name {
+ $(
+ $field_name: Option<$field_type>
+ ),*
+ }
+
+ $(
+ fn $field_name(
+ config_value: Option<$field_type>,
+ $($extra_input: $extra_input_type),*
+ ) -> anyhow::Result<$field_type> {
+ use anyhow::Context;
+
+ let expr = $crate::config::support::maybe_wrap_type!($field_type =$errors $default)(config_value);
+
+ expr.context(concat!("Failed to generate default config value for '", stringify!($field_name),"'"))
+ }
+ )*
+ )*
+ }
+ pub(crate) use self::_inner::*;
+ };
+}
+
+macro_rules! maybe_wrap_type {
+ ($ty:ty =! $val:expr) => {
+ (|config_value: Option<$ty>| $val(config_value))
+ };
+ ($ty:ty =? $val:expr) => {
+ (|config_value: Option<$ty>| config_value.map_or_else(|| $val, Ok))
+ };
+ ($ty:ty =: $val:expr) => {
+ (|config_value: Option<$ty>| Ok::<_, anyhow::Error>(config_value.unwrap_or_else(|| $val)))
+ };
+}
+
+pub(crate) use maybe_wrap_type;
+pub(crate) use mk_config;
diff --git a/crates/yt/src/main.rs b/crates/yt/src/main.rs
index f6f18be..faee401 100644
--- a/crates/yt/src/main.rs
+++ b/crates/yt/src/main.rs
@@ -87,12 +87,18 @@ async fn main() -> Result<()> {
}
});
- let config = Config::from_config_file(args.db_path, args.config_path, args.color)?;
+ let config = Config::from_config_file(args.config_path, args.color, args.db_path)?;
if args.version {
version::show(&config).await?;
return Ok(());
}
+ // Perform config finalization _after_ checking for the version
+ // so that version always works.
+ config
+ .run_finalizers()
+ .context("Failed to finalize config for usage")?;
+
let app = App::new(config, !args.no_migrate_db).await?;
match args.command.unwrap_or(Command::default()) {