Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 809b7c81a4 | |||
| 854215bc45 | |||
| 87db938735 | |||
| 07db31973c | |||
| ead15f54d6 | |||
| df93f93384 | |||
| c8942af595 | |||
| 0f3831275e | |||
| 99efd89abc |
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
/.gitignore export-ignore
|
||||||
|
/.gitattributes export-ignore
|
||||||
|
|
||||||
|
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
assets/bin/** filter=lfs diff=lfs merge=lfs -text
|
||||||
|
assets/bin/**/*.env -filter -diff -merge
|
||||||
|
assets/bin/**/*.txt -filter -diff -merge
|
||||||
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,18 +1,11 @@
|
|||||||
# ---> Rust
|
.idea/
|
||||||
# Generated by Cargo
|
.vscode/
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
debug/
|
||||||
target/
|
target/
|
||||||
|
assets/.cache
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
Cargo.lock
|
||||||
**/*.rs.bk
|
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
|
||||||
*.pdb
|
*.pdb
|
||||||
|
**/*.rs.bk
|
||||||
# RustRover
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|||||||
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "new_vegas_radio"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dotenvy = "^0"
|
||||||
|
ron = "^0"
|
||||||
|
toml = "^1"
|
||||||
|
serde_json = "^1"
|
||||||
|
serde = { version = "^1", features = ["derive"] }
|
||||||
|
|
||||||
|
thiserror = "^2"
|
||||||
|
tracing-subscriber = { version = "^0", features = ["env-filter"] }
|
||||||
181
README.md
181
README.md
@@ -1,2 +1,183 @@
|
|||||||
# NewVegasRadio
|
# NewVegasRadio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Assets-Directory Structure:
|
||||||
|
|
||||||
|
```terminaloutput
|
||||||
|
assets/
|
||||||
|
config.(json|toml|ron)
|
||||||
|
bin/
|
||||||
|
x86_64/
|
||||||
|
aarch64/
|
||||||
|
songs/
|
||||||
|
my_song/
|
||||||
|
my_song.mp3
|
||||||
|
descriptor.(json|toml|ron)
|
||||||
|
intro/
|
||||||
|
Intro_MrNewVegas_my_song.mp3 (optional)
|
||||||
|
generic/
|
||||||
|
intro/
|
||||||
|
Intro_MrNewVegas_N.mp3
|
||||||
|
shared/
|
||||||
|
Story_MrNewVegas_N.mp3
|
||||||
|
news/
|
||||||
|
story_N/
|
||||||
|
story.toml (optional, ignored)
|
||||||
|
Story_MrNewVegas_N.mp3
|
||||||
|
Story_Guest_N.mp3
|
||||||
|
Story_MrNewVegas_N.mp3 -> symlink shared/Story_MrNewVegas_N.mp3
|
||||||
|
.cache/ (automatically generated)
|
||||||
|
songs/
|
||||||
|
my_song/
|
||||||
|
song.pcm (f32 pcm)
|
||||||
|
intro.pcm (f32 pcm)
|
||||||
|
cache.json
|
||||||
|
generic/
|
||||||
|
intro/
|
||||||
|
intro_N.pcm (f32 pcm)
|
||||||
|
intro_N.cache.json
|
||||||
|
shared/
|
||||||
|
story_N.pcm (f32 pcm)
|
||||||
|
story_N.cache.json
|
||||||
|
news/
|
||||||
|
story_N/
|
||||||
|
part_N.pcm (f32 pcm)
|
||||||
|
part_N.pcm (f32 pcm)
|
||||||
|
part_N.pcm (f32 pcm) -> symlink shared/story_N.pcm
|
||||||
|
cache.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `config.json`:
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"defaults": {
|
||||||
|
"songs_directory": "songs",
|
||||||
|
"generic_directory": "generic",
|
||||||
|
"log_directory": "logs",
|
||||||
|
"temp_directory": "temp"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"weight": 1.0,
|
||||||
|
"volume": 1.0,
|
||||||
|
"crossfade": 2.0, // in seconds
|
||||||
|
"weights": {
|
||||||
|
"news": {
|
||||||
|
"chance": 0.2
|
||||||
|
},
|
||||||
|
"genre": {
|
||||||
|
"floor": 0.55,
|
||||||
|
"recover_after": 3.0
|
||||||
|
},
|
||||||
|
"composite": {
|
||||||
|
"floor": 0.05,
|
||||||
|
"recover_after": 5.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"shuffle_mode": "weighted", // options: "weighted", "strict_random"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
OR
|
||||||
|
#### `config.toml`:
|
||||||
|
```toml
|
||||||
|
[defaults]
|
||||||
|
songs_directory = "songs"
|
||||||
|
generic_directory = "generic"
|
||||||
|
log_directory = "logs"
|
||||||
|
temp_directory = "temp"
|
||||||
|
|
||||||
|
[settings]
|
||||||
|
weight = 1.0
|
||||||
|
volume = 1.0
|
||||||
|
crossfade = 2.0 # in seconds
|
||||||
|
[settings.weights.news]
|
||||||
|
chance = 0.2
|
||||||
|
[settings.weights.genre]
|
||||||
|
floor = 0.55
|
||||||
|
recover_after = 3.0
|
||||||
|
[settings.weights.composite]
|
||||||
|
floor = 0.05
|
||||||
|
recover_after = 5.0
|
||||||
|
|
||||||
|
[rules]
|
||||||
|
shuffle_mode = "weighted" # "weighted", "strict_random"
|
||||||
|
```
|
||||||
|
OR
|
||||||
|
#### `config.ron`
|
||||||
|
```ron
|
||||||
|
(
|
||||||
|
defaults: (
|
||||||
|
songs_directory: "songs",
|
||||||
|
generic_directory: "generic",
|
||||||
|
log_directory: "logs",
|
||||||
|
temp_directory: "temp",
|
||||||
|
),
|
||||||
|
settings: (
|
||||||
|
debug: true,
|
||||||
|
weight: 1.0,
|
||||||
|
volume: 1.0,
|
||||||
|
crossfade: 2.0, // in seconds
|
||||||
|
weights: (
|
||||||
|
news: (
|
||||||
|
chance: 0.2,
|
||||||
|
),
|
||||||
|
genre: (
|
||||||
|
floor: 0.55,
|
||||||
|
recover_after: 3.0,
|
||||||
|
),
|
||||||
|
composite: (
|
||||||
|
floor: 0.05,
|
||||||
|
recover_after: 5.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
rules: (
|
||||||
|
shuffle_mode: weighted, // weighted, strict_random
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `descriptor.json`:
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"title": "[I Got Spurs That] Jingle, Jangle, Jingle",
|
||||||
|
"artist": "Kay Kyser",
|
||||||
|
"tags": ["orchestra", "swing", "jazz"],
|
||||||
|
"genre": ["swing", "jazz"],
|
||||||
|
"intro": true,
|
||||||
|
"weight": 1.0, // optional
|
||||||
|
"volume": 1.0, // optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
OR
|
||||||
|
#### `descriptor.toml`:
|
||||||
|
```toml
|
||||||
|
title = "[I Got Spurs That] Jingle, Jangle, Jingle"
|
||||||
|
artist = "Kay Kyser"
|
||||||
|
tags = ["orchestra", "swing", "jazz"]
|
||||||
|
genre = ["swing", "jazz"]
|
||||||
|
intro = true
|
||||||
|
weight = 1.0 # optional
|
||||||
|
volume = 1.0 # optional
|
||||||
|
```
|
||||||
|
OR
|
||||||
|
#### `descriptor.ron`
|
||||||
|
```ron
|
||||||
|
(
|
||||||
|
title: "[I Got Spurs That] Jingle, Jangle, Jingle",
|
||||||
|
artist: "Kay Kyser",
|
||||||
|
tags: ["orchestra", "swing", "jazz"],
|
||||||
|
genre: ["swing", "jazz"],
|
||||||
|
intro: true,
|
||||||
|
weight: 1.0, // optional
|
||||||
|
volume: 1.0, // optional
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|||||||
2
assets/bin/aarch64/.env
Normal file
2
assets/bin/aarch64/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
LOCAL_ASSETS_DIR=../../../assets
|
||||||
|
LOCAL_BIN_DIR=.
|
||||||
BIN
assets/bin/aarch64/ffmpeg
(Stored with Git LFS)
Executable file
BIN
assets/bin/aarch64/ffmpeg
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
assets/bin/aarch64/ffplay
(Stored with Git LFS)
Executable file
BIN
assets/bin/aarch64/ffplay
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
assets/bin/aarch64/ffprobe
(Stored with Git LFS)
Executable file
BIN
assets/bin/aarch64/ffprobe
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
assets/bin/aarch64/rnv_utils
(Stored with Git LFS)
Executable file
BIN
assets/bin/aarch64/rnv_utils
(Stored with Git LFS)
Executable file
Binary file not shown.
11
assets/bin/aarch64/sources.txt
Normal file
11
assets/bin/aarch64/sources.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
LGPL-3.0
|
||||||
|
ffmpeg, ffplay, ffprobe
|
||||||
|
https://github.com/BtbN/FFmpeg-Builds/releases/tag/autobuild-2026-02-18-13-03
|
||||||
|
https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-linux64-lgpl-7.1.tar.xz
|
||||||
|
sha256:5ec371abbf6da81fa0a7dfe013035754fadf77f8206b93cb784ec582b0870607
|
||||||
|
|
||||||
|
LGPL-3.0
|
||||||
|
rnv_utils
|
||||||
|
locally compiled (arm64), golang
|
||||||
|
sha256:44d2aa338f3a2f5dca65d50d9a120a1ad02b18210e8ba1d114db4496e1056d14
|
||||||
2
assets/bin/x86_64/.env
Normal file
2
assets/bin/x86_64/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
LOCAL_ASSETS_DIR=../../../assets
|
||||||
|
LOCAL_BIN_DIR=.
|
||||||
BIN
assets/bin/x86_64/ffmpeg
(Stored with Git LFS)
Executable file
BIN
assets/bin/x86_64/ffmpeg
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
assets/bin/x86_64/ffplay
(Stored with Git LFS)
Executable file
BIN
assets/bin/x86_64/ffplay
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
assets/bin/x86_64/ffprobe
(Stored with Git LFS)
Executable file
BIN
assets/bin/x86_64/ffprobe
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
assets/bin/x86_64/rnv_utils
(Stored with Git LFS)
Executable file
BIN
assets/bin/x86_64/rnv_utils
(Stored with Git LFS)
Executable file
Binary file not shown.
11
assets/bin/x86_64/sources.txt
Normal file
11
assets/bin/x86_64/sources.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
LGPL-3.0
|
||||||
|
ffmpeg, ffplay, ffprobe
|
||||||
|
https://github.com/BtbN/FFmpeg-Builds/releases/tag/autobuild-2026-02-18-13-03
|
||||||
|
https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-linux64-lgpl-7.1.tar.xz
|
||||||
|
sha256:5ec371abbf6da81fa0a7dfe013035754fadf77f8206b93cb784ec582b0870607
|
||||||
|
|
||||||
|
LGPL-3.0
|
||||||
|
rnv_utils
|
||||||
|
locally compiled (amd64), golang
|
||||||
|
sha256:6c68de4d77d058b9ae9506a29617fe3181277ed6aebdd1507d06284d2519f3ea
|
||||||
129
src/app/config.rs
Normal file
129
src/app/config.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
sync::{ Arc, LazyLock },
|
||||||
|
path::{ Path, PathBuf, Component },
|
||||||
|
};
|
||||||
|
use serde::{ Serialize, Deserialize };
|
||||||
|
|
||||||
|
use crate::app::parser::FromConfig;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub defaults: Defaults,
|
||||||
|
pub settings: Settings,
|
||||||
|
pub rules: Rules,
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
static CFG: LazyLock<Arc<Config>> = LazyLock::new(|| {
|
||||||
|
Arc::new(<Config as FromConfig>::load())
|
||||||
|
});
|
||||||
|
|
||||||
|
impl Config
|
||||||
|
{
|
||||||
|
/// Arc to a Singleton Instance of Config.
|
||||||
|
pub fn load() -> Arc<Self> { CFG.clone() }
|
||||||
|
|
||||||
|
fn resolve(&mut self, root: impl AsRef<Path>)
|
||||||
|
{
|
||||||
|
let d = &mut self.defaults;
|
||||||
|
let r = root.as_ref();
|
||||||
|
|
||||||
|
d.songs_directory = Self::normalize(r.join(&d.songs_directory));
|
||||||
|
d.generic_directory = Self::normalize(r.join(&d.generic_directory));
|
||||||
|
d.cache_directory = Self::normalize(r.join(&d.cache_directory));
|
||||||
|
d.log_directory = Self::normalize(r.join(&d.log_directory));
|
||||||
|
d.temp_directory = Self::normalize(r.join(&d.temp_directory));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize(path: impl AsRef<Path>) -> PathBuf
|
||||||
|
{
|
||||||
|
path.as_ref().components().fold(PathBuf::new(), |mut acc, c| {
|
||||||
|
match c { Component::ParentDir => { acc.pop(); acc }, _ => { acc.push(c); acc } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromConfig for Config
|
||||||
|
{
|
||||||
|
fn load() -> Self
|
||||||
|
{
|
||||||
|
let root = env::current_exe()
|
||||||
|
.expect("Unable to locate executable.").parent()
|
||||||
|
.expect("Unable to locate root directory.").to_owned();
|
||||||
|
|
||||||
|
// Removed expectation for dockerized-environments.
|
||||||
|
dotenvy::dotenv()
|
||||||
|
.map(|_| ())
|
||||||
|
.or_else(|_| dotenvy::from_path(root.join(".env")))
|
||||||
|
.ok(); //.expect("Unable to load .env file.");
|
||||||
|
|
||||||
|
let assets_directory: PathBuf = env::var("RNV_ASSETS_DIRECTORY")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.map(|p: PathBuf| root.join(p))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Self::deserialize_config_with_resolver(
|
||||||
|
assets_directory.join("config"),
|
||||||
|
&[
|
||||||
|
("ron", Self::from_ron),
|
||||||
|
("toml", Self::from_toml),
|
||||||
|
("json", Self::from_json),
|
||||||
|
],
|
||||||
|
|cfg: &mut Self| cfg.resolve(&assets_directory)
|
||||||
|
).expect("Failed to deserialize config.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Defaults {
|
||||||
|
pub songs_directory: PathBuf,
|
||||||
|
pub generic_directory: PathBuf,
|
||||||
|
pub cache_directory: PathBuf,
|
||||||
|
|
||||||
|
pub log_directory: PathBuf,
|
||||||
|
pub temp_directory: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub debug: bool,
|
||||||
|
pub weight: f64,
|
||||||
|
pub volume: f64,
|
||||||
|
pub crossfade: f64,
|
||||||
|
pub weights: Weights,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Weights {
|
||||||
|
pub news: NewsWeight,
|
||||||
|
pub genre: RecoveringWeight,
|
||||||
|
pub composite: RecoveringWeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||||
|
pub struct NewsWeight {
|
||||||
|
pub chance: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||||
|
pub struct RecoveringWeight {
|
||||||
|
pub floor: f64,
|
||||||
|
pub recover_after: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Rules {
|
||||||
|
pub shuffle_mode: ShuffleMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ShuffleMode {
|
||||||
|
Weighted,
|
||||||
|
StrictRandom,
|
||||||
|
}
|
||||||
7
src/app/mod.rs
Normal file
7
src/app/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod state;
|
||||||
|
pub mod models;
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
mod parser;
|
||||||
|
|
||||||
|
pub use parser::FromConfig;
|
||||||
2
src/app/models.rs
Normal file
2
src/app/models.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
pub struct Scheduler {}
|
||||||
3
src/app/parser/mod.rs
Normal file
3
src/app/parser/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod parser;
|
||||||
|
|
||||||
|
pub use parser::FromConfig;
|
||||||
130
src/app/parser/parser.rs
Normal file
130
src/app/parser/parser.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// A trait for types that can be loaded from a configuration file.
|
||||||
|
///
|
||||||
|
/// Implementors must define [`load`](FromConfig::load), which is responsible for
|
||||||
|
/// resolving the config path and calling [`deserialize_config`](FromConfig::deserialize_config).
|
||||||
|
/// All parsing methods have default implementations supporting [`RON`](FromConfig::from_ron),
|
||||||
|
/// [`TOML`](FromConfig::from_toml), and [`JSON`](FromConfig::from_json),
|
||||||
|
/// and can be overridden if custom parsing behaviour is needed.
|
||||||
|
pub trait FromConfig
|
||||||
|
{
|
||||||
|
/// Loads and deserializes the config into `Self`.
|
||||||
|
///
|
||||||
|
/// Implementations should resolve the config file path and call
|
||||||
|
/// [`deserialize_config`](FromConfig::deserialize_config) with the desired formats.
|
||||||
|
fn load() -> Self;
|
||||||
|
|
||||||
|
/// Deserializes a config file into a rust struct.
|
||||||
|
///
|
||||||
|
/// Returns [`ConfigError::NotFound`] if no file matching any of the provided extensions
|
||||||
|
/// exists, [`ConfigError::Io`] if the file cannot be read, or the appropriate parse
|
||||||
|
/// error variant if deserialisation fails.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```
|
||||||
|
/// let cfg: Config = Config::deserialize_config(
|
||||||
|
/// "path/to/config/file",
|
||||||
|
/// &[
|
||||||
|
/// ("ron", Config::from_ron),
|
||||||
|
/// ("toml", Config::from_toml),
|
||||||
|
/// ("json", Config::from_json),
|
||||||
|
/// ],
|
||||||
|
/// )?;
|
||||||
|
/// ```
|
||||||
|
fn deserialize_config<T>(file: impl AsRef<Path>, formats: &[(&str, ParseFn<T>)]) -> Result<T, ConfigError>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
{
|
||||||
|
let base = file.as_ref();
|
||||||
|
|
||||||
|
for (ext, parse) in formats
|
||||||
|
{
|
||||||
|
let path = base.with_extension(ext);
|
||||||
|
if path.exists()
|
||||||
|
{
|
||||||
|
let raw = std::fs::read_to_string(&path)
|
||||||
|
.map_err(ConfigError::Io)?;
|
||||||
|
return parse(&raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(ConfigError::NotFound { path: file.as_ref().to_string_lossy().into() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes a config file into a Rust struct, then applies a post-processing
|
||||||
|
/// resolver before returning.
|
||||||
|
///
|
||||||
|
/// This is identical to [`deserialize_config`](FromConfig::deserialize_config), except
|
||||||
|
/// that after a successful parse, `resolver` is called with a mutable reference to the
|
||||||
|
/// deserialized value. Use it to fill in derived fields, apply default overrides, or
|
||||||
|
/// validate and normalise values that cannot be expressed in the raw config format.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```
|
||||||
|
/// let cfg: Config = Config::deserialize_config(
|
||||||
|
/// "path/to/config/file",
|
||||||
|
/// &[
|
||||||
|
/// ("ron", Config::from_ron),
|
||||||
|
/// ("toml", Config::from_toml),
|
||||||
|
/// ("json", Config::from_json),
|
||||||
|
/// ],
|
||||||
|
/// |cfg: &mut Self| cfg.resolve(&assets_directory)
|
||||||
|
/// )?;
|
||||||
|
/// ```
|
||||||
|
fn deserialize_config_with_resolver<T>(file: impl AsRef<Path>, formats: &[(&str, ParseFn<T>)], resolver: impl Fn(&mut T)) -> Result<T, ConfigError>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
{
|
||||||
|
let base = file.as_ref();
|
||||||
|
|
||||||
|
for (ext, parse) in formats
|
||||||
|
{
|
||||||
|
let path = base.with_extension(ext);
|
||||||
|
if path.exists()
|
||||||
|
{
|
||||||
|
let raw = std::fs::read_to_string(&path)
|
||||||
|
.map_err(ConfigError::Io)?;
|
||||||
|
|
||||||
|
let mut cfg = parse(&raw)?;
|
||||||
|
|
||||||
|
resolver(&mut cfg);
|
||||||
|
return Ok(cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(ConfigError::NotFound { path: file.as_ref().to_string_lossy().into() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_ron<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, ConfigError> {
|
||||||
|
ron::from_str(s).map_err(ConfigError::Ron)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_toml<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, ConfigError> {
|
||||||
|
toml::from_str(s).map_err(ConfigError::Toml)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_json<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, ConfigError> {
|
||||||
|
serde_json::from_str(s).map_err(ConfigError::Json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
pub type ParseFn<T> = fn(&str) -> Result<T, ConfigError>;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("No config file found at '{path}.(ron|toml|json)'.")]
|
||||||
|
NotFound { path: String },
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("Ron parse error: {0}")]
|
||||||
|
Ron(#[from] ron::error::SpannedError),
|
||||||
|
#[error("TOML parse error: {0}")]
|
||||||
|
Toml(#[from] toml::de::Error),
|
||||||
|
#[error("JSON parse error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
30
src/app/state.rs
Normal file
30
src/app/state.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use tracing_subscriber::{
|
||||||
|
EnvFilter,
|
||||||
|
fmt::{
|
||||||
|
SubscriberBuilder,
|
||||||
|
format::{ Format, DefaultFields }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::{
|
||||||
|
models::*,
|
||||||
|
config::Config
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub config: Arc<Config>,
|
||||||
|
pub scheduler: Scheduler
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState
|
||||||
|
{
|
||||||
|
pub fn new(config: Arc<Config>, scheduler: Scheduler) -> Self
|
||||||
|
{
|
||||||
|
Self { config, scheduler }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tracer(filter: impl Into<EnvFilter>) -> SubscriberBuilder<DefaultFields, Format, EnvFilter> {
|
||||||
|
tracing_subscriber::fmt().with_env_filter(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod app;
|
||||||
|
|
||||||
|
pub use app::{ config, state };
|
||||||
13
src/main.rs
Normal file
13
src/main.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use new_vegas_radio::app::{ config::Config, state::AppState, models::Scheduler };
|
||||||
|
|
||||||
|
fn main()
|
||||||
|
{
|
||||||
|
let cfg = Config::load();
|
||||||
|
let _app = AppState::new(cfg.clone(), Scheduler {});
|
||||||
|
|
||||||
|
if cfg.settings.debug {
|
||||||
|
AppState::get_tracer("new_vegas_radio=debug").init();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{:?}", cfg);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user