mirror of
https://github.com/bybrooklyn/alchemist.git
synced 2026-04-18 01:43:34 -04:00
feat: establish core application structure, CLI/server modes, and interactive configuration wizard
This commit is contained in:
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Linguist overrides for GitHub language statistics
|
||||||
|
|
||||||
|
# Exclude Dockerfile from language stats
|
||||||
|
Dockerfile linguist-vendored
|
||||||
|
|
||||||
|
# Exclude generated/build files
|
||||||
|
target/** linguist-generated
|
||||||
|
*.lock linguist-generated
|
||||||
|
Cargo.lock linguist-generated
|
||||||
|
|
||||||
|
# Mark as documentation (optional)
|
||||||
|
*.md linguist-documentation
|
||||||
137
Cargo.lock
generated
137
Cargo.lock
generated
@@ -36,6 +36,7 @@ dependencies = [
|
|||||||
"console_log",
|
"console_log",
|
||||||
"futures",
|
"futures",
|
||||||
"gloo-net 0.5.0",
|
"gloo-net 0.5.0",
|
||||||
|
"inquire",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
"leptos_dom",
|
"leptos_dom",
|
||||||
@@ -290,6 +291,12 @@ version = "1.8.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82"
|
checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
@@ -605,6 +612,31 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"libc",
|
||||||
|
"mio 0.8.11",
|
||||||
|
"parking_lot",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy"
|
name = "crunchy"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -726,6 +758,12 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
|
checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@@ -915,6 +953,24 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fuzzy-matcher"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||||
|
dependencies = [
|
||||||
|
"thread_local",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fxhash"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -1351,6 +1407,23 @@ dependencies = [
|
|||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inquire"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"crossterm",
|
||||||
|
"dyn-clone",
|
||||||
|
"fuzzy-matcher",
|
||||||
|
"fxhash",
|
||||||
|
"newline-converter",
|
||||||
|
"once_cell",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@@ -1665,7 +1738,7 @@ version = "0.1.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall 0.7.0",
|
"redox_syscall 0.7.0",
|
||||||
]
|
]
|
||||||
@@ -1803,6 +1876,18 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -1831,6 +1916,15 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "newline-converter"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@@ -2252,7 +2346,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2261,7 +2355,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2339,7 +2433,7 @@ version = "1.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
@@ -2601,6 +2695,27 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio 0.8.11",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.8"
|
version = "1.4.8"
|
||||||
@@ -2782,7 +2897,7 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2825,7 +2940,7 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
@@ -3071,7 +3186,7 @@ checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio 1.1.1",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
@@ -3191,7 +3306,7 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.4.0",
|
"http 1.4.0",
|
||||||
@@ -3349,6 +3464,12 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ subprocess = { version = "=0.2.9", optional = true }
|
|||||||
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "macros", "chrono"], optional = true }
|
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "macros", "chrono"], optional = true }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
num_cpus = "1.16"
|
num_cpus = "1.16"
|
||||||
|
inquire = { version = "0.7", optional = true }
|
||||||
futures = { version = "0.3" }
|
futures = { version = "0.3" }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
leptos = { version = "=0.6.14", features = ["ssr"] }
|
leptos = { version = "=0.6.14", features = ["ssr"] }
|
||||||
@@ -53,6 +54,7 @@ ssr = [
|
|||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos_meta/ssr",
|
"leptos_meta/ssr",
|
||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
|
"dep:inquire",
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.leptos]
|
[package.metadata.leptos]
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -36,6 +36,24 @@ cargo build --release
|
|||||||
./target/release/alchemist --server
|
./target/release/alchemist --server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### First-Boot Configuration
|
||||||
|
|
||||||
|
When you start Alchemist server for the first time, an interactive configuration wizard will run automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First time running the server
|
||||||
|
./target/release/alchemist --server
|
||||||
|
|
||||||
|
# The wizard runs automatically and guides you through:
|
||||||
|
# - Transcoding settings (quality thresholds, concurrency)
|
||||||
|
# - Hardware configuration (GPU/CPU encoding preferences)
|
||||||
|
# - Auto-scan directories (media library paths)
|
||||||
|
#
|
||||||
|
# After completion, config.toml is created and the server starts
|
||||||
|
```
|
||||||
|
|
||||||
|
On subsequent runs, the server loads your saved configuration.
|
||||||
|
|
||||||
### Docker Deployment
|
### Docker Deployment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ pub mod orchestrator;
|
|||||||
pub mod processor;
|
pub mod processor;
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod wizard;
|
||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
|
|||||||
34
src/main.rs
34
src/main.rs
@@ -56,12 +56,36 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
// 0. Load Configuration
|
// 0. Load Configuration (with first-boot wizard)
|
||||||
let config_path = std::path::Path::new("config.toml");
|
let config_path = std::path::Path::new("config.toml");
|
||||||
let config = config::Config::load(config_path).unwrap_or_else(|e| {
|
let config = if !config_path.exists() && args.server {
|
||||||
warn!("Failed to load config.toml: {}. Using defaults.", e);
|
// First boot: Run configuration wizard
|
||||||
config::Config::default()
|
info!("No configuration file found. Starting configuration wizard...");
|
||||||
});
|
info!("");
|
||||||
|
|
||||||
|
match alchemist::wizard::ConfigWizard::run(config_path) {
|
||||||
|
Ok(cfg) => {
|
||||||
|
info!("");
|
||||||
|
info!("Configuration complete! Continuing with server startup...");
|
||||||
|
info!("");
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Configuration wizard failed: {}", e);
|
||||||
|
error!("You can:");
|
||||||
|
error!(" 1. Run the wizard again: alchemist --server");
|
||||||
|
error!(" 2. Create config.toml manually");
|
||||||
|
error!(" 3. Use Python wizard: python setup/configure.py");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal boot or CLI mode: Load config or use defaults
|
||||||
|
config::Config::load(config_path).unwrap_or_else(|e| {
|
||||||
|
warn!("Failed to load config.toml: {}. Using defaults.", e);
|
||||||
|
config::Config::default()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// Log Configuration
|
// Log Configuration
|
||||||
info!("Configuration:");
|
info!("Configuration:");
|
||||||
|
|||||||
382
src/wizard.rs
Normal file
382
src/wizard.rs
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::{AlchemistError, Result};
|
||||||
|
use inquire::{Confirm, Select, Text};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Interactive configuration wizard
|
||||||
|
pub struct ConfigWizard;
|
||||||
|
|
||||||
|
impl ConfigWizard {
|
||||||
|
/// Run the configuration wizard and create config.toml
|
||||||
|
pub fn run(config_path: &Path) -> Result<Config> {
|
||||||
|
println!("\n╔═══════════════════════════════════════════════════════════════╗");
|
||||||
|
println!("║ ⚗️ ALCHEMIST CONFIGURATION WIZARD ║");
|
||||||
|
println!("╚═══════════════════════════════════════════════════════════════╝\n");
|
||||||
|
|
||||||
|
println!("Welcome! This wizard will help you configure Alchemist.");
|
||||||
|
println!("Press Enter to accept the default value shown in [brackets].\n");
|
||||||
|
|
||||||
|
if config_path.exists() {
|
||||||
|
let overwrite = Confirm::new("config.toml already exists. Overwrite it?")
|
||||||
|
.with_default(false)
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
if !overwrite {
|
||||||
|
return Err(AlchemistError::Config("Configuration cancelled".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section 1: Transcoding
|
||||||
|
println!("\n┌─────────────────────────────────────────┐");
|
||||||
|
println!("│ Section 1/3: Transcoding Settings │");
|
||||||
|
println!("└─────────────────────────────────────────┘\n");
|
||||||
|
|
||||||
|
let size_threshold = Self::prompt_size_threshold()?;
|
||||||
|
let min_bpp = Self::prompt_min_bpp()?;
|
||||||
|
let min_file_size = Self::prompt_min_file_size()?;
|
||||||
|
let concurrent_jobs = Self::prompt_concurrent_jobs()?;
|
||||||
|
|
||||||
|
// Section 2: Hardware
|
||||||
|
println!("\n┌─────────────────────────────────────────┐");
|
||||||
|
println!("│ Section 2/3: Hardware Settings │");
|
||||||
|
println!("└─────────────────────────────────────────┘\n");
|
||||||
|
|
||||||
|
let allow_cpu_fallback = Self::prompt_cpu_fallback()?;
|
||||||
|
let allow_cpu_encoding = Self::prompt_cpu_encoding()?;
|
||||||
|
let cpu_preset = Self::prompt_cpu_preset()?;
|
||||||
|
let preferred_vendor = Self::prompt_preferred_vendor()?;
|
||||||
|
|
||||||
|
// Section 3: Scanner
|
||||||
|
println!("\n┌─────────────────────────────────────────┐");
|
||||||
|
println!("│ Section 3/3: Scanner Settings │");
|
||||||
|
println!("└─────────────────────────────────────────┘\n");
|
||||||
|
|
||||||
|
let directories = Self::prompt_directories()?;
|
||||||
|
|
||||||
|
// Build config
|
||||||
|
let config = Config {
|
||||||
|
transcode: crate::config::TranscodeConfig {
|
||||||
|
size_reduction_threshold: size_threshold,
|
||||||
|
min_bpp_threshold: min_bpp,
|
||||||
|
min_file_size_mb: min_file_size,
|
||||||
|
concurrent_jobs,
|
||||||
|
},
|
||||||
|
hardware: crate::config::HardwareConfig {
|
||||||
|
preferred_vendor,
|
||||||
|
device_path: None,
|
||||||
|
allow_cpu_fallback,
|
||||||
|
cpu_preset,
|
||||||
|
allow_cpu_encoding,
|
||||||
|
},
|
||||||
|
scanner: crate::config::ScannerConfig { directories },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show summary
|
||||||
|
Self::show_summary(&config);
|
||||||
|
|
||||||
|
let confirm = Confirm::new("Save this configuration?")
|
||||||
|
.with_default(true)
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
if !confirm {
|
||||||
|
return Err(AlchemistError::Config("Configuration cancelled".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
Self::write_config(config_path, &config)?;
|
||||||
|
|
||||||
|
println!("\n✅ Configuration saved to {}", config_path.display());
|
||||||
|
println!("\nYou can now use Alchemist!");
|
||||||
|
println!(" • Run: alchemist --server");
|
||||||
|
println!(" • Edit: {}\n", config_path.display());
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_size_threshold() -> Result<f64> {
|
||||||
|
println!("📏 Size Reduction Threshold");
|
||||||
|
println!(" How much smaller must the output file be to keep it?\n");
|
||||||
|
println!(" • 0.3 (30%) - Balanced (recommended)");
|
||||||
|
println!(" • 0.2 (20%) - More aggressive");
|
||||||
|
println!(" • 0.5 (50%) - Conservative\n");
|
||||||
|
println!(" Files that don't compress enough are kept as-is.\n");
|
||||||
|
|
||||||
|
let input = Text::new("Size reduction threshold:")
|
||||||
|
.with_default("0.3")
|
||||||
|
.with_help_message("Enter a value between 0.0 and 1.0")
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
input
|
||||||
|
.parse::<f64>()
|
||||||
|
.map_err(|_| AlchemistError::Config("Invalid number".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_min_bpp() -> Result<f64> {
|
||||||
|
println!("\n🎨 Minimum Bits Per Pixel (BPP)");
|
||||||
|
println!(" Skip files that are already heavily compressed.\n");
|
||||||
|
println!(" • 0.10 - Good default (recommended)");
|
||||||
|
println!(" • 0.05 - Very aggressive");
|
||||||
|
println!(" • 0.20 - Conservative\n");
|
||||||
|
println!(" Lower BPP = already compressed. Re-encoding destroys quality.\n");
|
||||||
|
|
||||||
|
let input = Text::new("Minimum BPP threshold:")
|
||||||
|
.with_default("0.1")
|
||||||
|
.with_help_message("Typical range: 0.05 to 0.30")
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
input
|
||||||
|
.parse::<f64>()
|
||||||
|
.map_err(|_| AlchemistError::Config("Invalid number".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_min_file_size() -> Result<u64> {
|
||||||
|
println!("\n📦 Minimum File Size");
|
||||||
|
println!(" Skip small files to avoid wasting time.\n");
|
||||||
|
println!(" • 50 MB - Good for most libraries (recommended)");
|
||||||
|
println!(" • 100 MB - Focus on movies/TV episodes");
|
||||||
|
println!(" • 10 MB - Process everything\n");
|
||||||
|
|
||||||
|
let input = Text::new("Minimum file size (MB):")
|
||||||
|
.with_default("50")
|
||||||
|
.with_help_message("Files smaller than this are skipped")
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
input
|
||||||
|
.parse::<u64>()
|
||||||
|
.map_err(|_| AlchemistError::Config("Invalid number".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_concurrent_jobs() -> Result<usize> {
|
||||||
|
println!("\n⚡ Concurrent Jobs");
|
||||||
|
println!(" How many videos to transcode simultaneously.\n");
|
||||||
|
println!(" • 1 - Safe default (CPU or single GPU)");
|
||||||
|
println!(" • 2-4 - Powerful systems with dedicated GPU");
|
||||||
|
println!(" • More = faster BUT uses more resources\n");
|
||||||
|
println!(" ⚠️ CPU users: Use 1 only (CPU is very slow)\n");
|
||||||
|
|
||||||
|
let input = Text::new("Number of concurrent jobs:")
|
||||||
|
.with_default("1")
|
||||||
|
.with_help_message("Recommended: 1-2 for most systems")
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
input
|
||||||
|
.parse::<usize>()
|
||||||
|
.map_err(|_| AlchemistError::Config("Invalid number".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_cpu_fallback() -> Result<bool> {
|
||||||
|
println!("💻 CPU Fallback");
|
||||||
|
println!(" Allow software encoding if no GPU is detected?\n");
|
||||||
|
println!(" If enabled: App uses CPU when no GPU found (SLOW)");
|
||||||
|
println!(" If disabled: App fails to start without GPU\n");
|
||||||
|
println!(" ⚠️ CPU encoding is 10-50x slower than GPU!\n");
|
||||||
|
|
||||||
|
Confirm::new("Enable CPU fallback?")
|
||||||
|
.with_default(true)
|
||||||
|
.with_help_message("Recommended: yes for compatibility")
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_cpu_encoding() -> Result<bool> {
|
||||||
|
println!("\n🔧 CPU Encoding");
|
||||||
|
println!(" Explicitly allow CPU encoding in production?\n");
|
||||||
|
println!(" Safety flag to prevent accidental slow transcodes.");
|
||||||
|
println!(" Even with fallback enabled, you can block CPU jobs.\n");
|
||||||
|
|
||||||
|
Confirm::new("Allow CPU encoding?")
|
||||||
|
.with_default(true)
|
||||||
|
.with_help_message("Recommended: yes (same as fallback)")
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_cpu_preset() -> Result<String> {
|
||||||
|
println!("\n⚙️ CPU Encoding Preset");
|
||||||
|
println!(" How fast/slow should CPU encoding be?\n");
|
||||||
|
println!(" • slow (0-4) - Best quality, very slow");
|
||||||
|
println!(" • medium (5-8) - Balanced (recommended)");
|
||||||
|
println!(" • fast (9-12) - Lower quality, faster");
|
||||||
|
println!(" • faster (13) - Lowest quality, fastest\n");
|
||||||
|
|
||||||
|
let choices = vec!["medium", "slow", "fast", "faster"];
|
||||||
|
|
||||||
|
Select::new("CPU preset:", choices)
|
||||||
|
.with_help_message("Recommendation: medium for CPU encoding")
|
||||||
|
.prompt()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_preferred_vendor() -> Result<Option<String>> {
|
||||||
|
println!("\n🎯 Preferred GPU Vendor (Optional)");
|
||||||
|
println!(" Force a specific GPU if multiple are available.\n");
|
||||||
|
println!(" Leave blank to auto-detect (recommended).\n");
|
||||||
|
|
||||||
|
let set_vendor = Confirm::new("Set preferred vendor?")
|
||||||
|
.with_default(false)
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
if !set_vendor {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let choices = vec!["nvidia", "intel", "amd", "apple"];
|
||||||
|
|
||||||
|
Select::new("Vendor:", choices)
|
||||||
|
.prompt()
|
||||||
|
.map(|s| Some(s.to_string()))
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_directories() -> Result<Vec<String>> {
|
||||||
|
println!("📁 Auto-Scan Directories");
|
||||||
|
println!(" Directories to automatically scan for media files.\n");
|
||||||
|
println!(" Used in server mode for automatic discovery.");
|
||||||
|
println!(" In CLI mode, you specify directories at runtime.\n");
|
||||||
|
|
||||||
|
let add_dirs = Confirm::new("Add auto-scan directories?")
|
||||||
|
.with_default(false)
|
||||||
|
.with_help_message("Optional: can configure later")
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
if !add_dirs {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut directories = Vec::new();
|
||||||
|
println!("\nEnter directories one at a time. Leave empty to finish.\n");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let dir = Text::new(&format!("Directory {}:", directories.len() + 1))
|
||||||
|
.with_default("")
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
if dir.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = Path::new(&dir);
|
||||||
|
if !path.exists() {
|
||||||
|
println!("⚠️ Warning: {} does not exist", dir);
|
||||||
|
let add_anyway = Confirm::new("Add anyway?")
|
||||||
|
.with_default(true)
|
||||||
|
.prompt()
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Prompt failed: {}", e)))?;
|
||||||
|
|
||||||
|
if !add_anyway {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
directories.push(dir);
|
||||||
|
println!("✓ Added");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(directories)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_summary(config: &Config) {
|
||||||
|
println!("\n┌─────────────────────────────────────────┐");
|
||||||
|
println!("│ 📋 Configuration Summary │");
|
||||||
|
println!("└─────────────────────────────────────────┘\n");
|
||||||
|
|
||||||
|
println!("Transcoding:");
|
||||||
|
println!(
|
||||||
|
" Size Reduction: {:.0}%",
|
||||||
|
config.transcode.size_reduction_threshold * 100.0
|
||||||
|
);
|
||||||
|
println!(" Min BPP: {:.2}", config.transcode.min_bpp_threshold);
|
||||||
|
println!(" Min File Size: {} MB", config.transcode.min_file_size_mb);
|
||||||
|
println!(" Concurrent Jobs: {}\n", config.transcode.concurrent_jobs);
|
||||||
|
|
||||||
|
println!("Hardware:");
|
||||||
|
println!(
|
||||||
|
" CPU Fallback: {}",
|
||||||
|
if config.hardware.allow_cpu_fallback {
|
||||||
|
"Enabled"
|
||||||
|
} else {
|
||||||
|
"Disabled"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" CPU Encoding: {}",
|
||||||
|
if config.hardware.allow_cpu_encoding {
|
||||||
|
"Enabled"
|
||||||
|
} else {
|
||||||
|
"Disabled"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
println!(" CPU Preset: {}", config.hardware.cpu_preset);
|
||||||
|
if let Some(ref vendor) = config.hardware.preferred_vendor {
|
||||||
|
println!(" Preferred Vendor: {}\n", vendor);
|
||||||
|
} else {
|
||||||
|
println!(" Preferred Vendor: Auto-detect\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Scanner:");
|
||||||
|
if config.scanner.directories.is_empty() {
|
||||||
|
println!(" (No auto-scan directories)\n");
|
||||||
|
} else {
|
||||||
|
for d in &config.scanner.directories {
|
||||||
|
println!(" 📁 {}", d);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_config(path: &Path, config: &Config) -> Result<()> {
|
||||||
|
let toml_content = format!(
|
||||||
|
r#"# Alchemist Configuration File
|
||||||
|
# Generated by configuration wizard
|
||||||
|
|
||||||
|
[transcode]
|
||||||
|
size_reduction_threshold = {} # Require this % size reduction
|
||||||
|
min_bpp_threshold = {} # Skip files with BPP below this
|
||||||
|
min_file_size_mb = {} # Skip files smaller than this (MB)
|
||||||
|
concurrent_jobs = {} # Number of parallel transcodes
|
||||||
|
|
||||||
|
[hardware]
|
||||||
|
allow_cpu_fallback = {} # Allow software encoding
|
||||||
|
allow_cpu_encoding = {} # Enable CPU encoding
|
||||||
|
cpu_preset = "{}" # CPU encoding speed: slow|medium|fast|faster
|
||||||
|
{}
|
||||||
|
[scanner]
|
||||||
|
directories = [ # Auto-scan directories (server mode)
|
||||||
|
{}]
|
||||||
|
"#,
|
||||||
|
config.transcode.size_reduction_threshold,
|
||||||
|
config.transcode.min_bpp_threshold,
|
||||||
|
config.transcode.min_file_size_mb,
|
||||||
|
config.transcode.concurrent_jobs,
|
||||||
|
config.hardware.allow_cpu_fallback,
|
||||||
|
config.hardware.allow_cpu_encoding,
|
||||||
|
config.hardware.cpu_preset,
|
||||||
|
config
|
||||||
|
.hardware
|
||||||
|
.preferred_vendor
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| format!("preferred_vendor = \"{}\"\n", v))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
config
|
||||||
|
.scanner
|
||||||
|
.directories
|
||||||
|
.iter()
|
||||||
|
.map(|d| format!(" \"{}\",", d))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::write(path, toml_content)
|
||||||
|
.map_err(|e| AlchemistError::Config(format!("Failed to write config: {}", e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user