├── web ├── .well-known │ └── acme-challenge │ │ └── kVrUJ9auML1rSGVXZSEHHNzBVcMfrvLJo5npgUyMOrY ├── completed.css ├── index.css ├── completed.html └── index.html ├── rustfmt.toml ├── .gitignore ├── dockerfile ├── Cargo.toml ├── .vscode └── settings.json ├── src ├── avg.rs ├── main.rs ├── config.rs ├── ffmpeg.rs ├── task.rs └── web.rs └── Cargo.lock /web/.well-known/acme-challenge/kVrUJ9auML1rSGVXZSEHHNzBVcMfrvLJo5npgUyMOrY: -------------------------------------------------------------------------------- 1 | kVrUJ9auML1rSGVXZSEHHNzBVcMfrvLJo5npgUyMOrY.osHBTkuOXYYWVdYdxWt1FN6nNLB50uzaps9E1SQ67vc -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = false 2 | max_width = 100 3 | use_small_heuristics = "Max" 4 | fn_call_width = 100 5 | chain_width = 90 6 | struct_variant_width = 120 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /outputs 3 | /inputs 4 | /prim* 5 | *.mp4 6 | *.mp3 7 | *.mkv 8 | *.webm 9 | *.m4a 10 | logs 11 | temp 12 | config.toml 13 | test_complex_a.txt 14 | keys 15 | ./a.py 16 | ./measure.txt 17 | ./in.webm 18 | certificates 19 | WinTime64.exe 20 | -------------------------------------------------------------------------------- /web/completed.css: -------------------------------------------------------------------------------- 1 | #completed-region { 2 | padding: 1em; 3 | width: fit-content; 4 | } 5 | 6 | #completed-region > video { 7 | border-radius: inherit; 8 | max-height: 500px; 9 | } 10 | 11 | main { 12 | padding-top: unset; 13 | margin: unset; 14 | text-align: center; 15 | width: 100%; 16 | max-width: unset; 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: center; 20 | align-items: center; 21 | } 22 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust as builder 2 | WORKDIR /dist 3 | COPY ./src src 4 | COPY ./Cargo* ./ 5 | 6 | ENV RUSTFLAGS="-C target-cpu=icelake-server" 7 | RUN cargo build --release --target x86_64-unknown-linux-gnu 8 | 9 | FROM alpine 10 | RUN apk add --no-cache gcompat ffmpeg 11 | # RUN apk add --no-cache ffmpeg 12 | COPY ./certificates /certificates 13 | COPY ./web /web 14 | COPY --from=builder /dist/target/x86_64-unknown-linux-gnu/release/voice /voice 15 | EXPOSE 443 16 | ENTRYPOINT ["/voice"] -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "voice" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Jiftoo "] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | axum = { version = "0.6.20", features = ["multipart", "macros", "ws"] } 11 | axum-server = { version = "0.5.1", features = ["rustls", "tls-rustls"] } 12 | rand = "0.8.5" 13 | serde = { version = "1.0.188", features = ["derive"] } 14 | serde_json = "1.0.107" 15 | time = { version = "0.3.29", features = ["serde"] } 16 | tokio = { version = "1.32.0", features = ["full"] } 17 | tokio-util = { version = "0.7.9", features = ["io"] } 18 | toml = "0.8.1" 19 | tower-http = { version = "0.4.4", features = ["fs", "cors"] } 20 | tracing = "0.1.37" 21 | tracing-appender = "0.2.2" 22 | tracing-subscriber = { version = "0.3.17", features = ["regex", "env-filter"] } 23 | which = "4.4.2" 24 | 25 | [profile.dev.package."*"] 26 | opt-level = 1 27 | 28 | [profile.dev] 29 | opt-level = 0 30 | 31 | [profile.release] 32 | opt-level = 3 33 | lto = true 34 | debug = false 35 | strip = true 36 | -------------------------------------------------------------------------------- /web/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #111; 3 | color: #eee; 4 | font-family: "Gloria Hallelujah", cursive; 5 | } 6 | 7 | main { 8 | margin: auto; 9 | text-align: center; 10 | width: 100%; 11 | display: flex; 12 | justify-content: center; 13 | padding-top: 200px; 14 | flex-direction: column; 15 | 16 | max-width: 500px; 17 | } 18 | 19 | h1 { 20 | font-size: 48px; 21 | } 22 | 23 | #upload-region { 24 | width: 100%; 25 | min-height: 200px; 26 | 27 | display: flex; 28 | justify-content: center; 29 | 30 | position: relative; 31 | } 32 | 33 | .dashed { 34 | border: 2px dashed #aaa; 35 | border-radius: 16px; 36 | } 37 | 38 | #upload-region > input { 39 | position: absolute; 40 | width: 100%; /* firefox fix */ 41 | top: 0; 42 | bottom: 0; 43 | left: 0; 44 | right: 0; 45 | opacity: 0; 46 | } 47 | 48 | #upload-region-message { 49 | position: absolute; 50 | top: 50%; 51 | left: 50%; 52 | transform: translate(-50%, -50%); 53 | 54 | font-size: 24px; 55 | 56 | pointer-events: none; 57 | 58 | white-space: pre-line; 59 | } 60 | 61 | .file-hover { 62 | background-color: #333; 63 | } 64 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.js": "javascriptreact", 4 | "*.stpl": "html", 5 | "string": "cpp", 6 | "atomic": "cpp", 7 | "bit": "cpp", 8 | "cctype": "cpp", 9 | "clocale": "cpp", 10 | "cmath": "cpp", 11 | "compare": "cpp", 12 | "concepts": "cpp", 13 | "cstddef": "cpp", 14 | "cstdint": "cpp", 15 | "cstdio": "cpp", 16 | "cstdlib": "cpp", 17 | "cstring": "cpp", 18 | "ctime": "cpp", 19 | "cwchar": "cpp", 20 | "exception": "cpp", 21 | "fstream": "cpp", 22 | "initializer_list": "cpp", 23 | "ios": "cpp", 24 | "iosfwd": "cpp", 25 | "iostream": "cpp", 26 | "istream": "cpp", 27 | "iterator": "cpp", 28 | "limits": "cpp", 29 | "map": "cpp", 30 | "memory": "cpp", 31 | "new": "cpp", 32 | "ostream": "cpp", 33 | "sstream": "cpp", 34 | "stdexcept": "cpp", 35 | "streambuf": "cpp", 36 | "system_error": "cpp", 37 | "tuple": "cpp", 38 | "type_traits": "cpp", 39 | "typeinfo": "cpp", 40 | "utility": "cpp", 41 | "vector": "cpp", 42 | "xfacet": "cpp", 43 | "xiosbase": "cpp", 44 | "xlocale": "cpp", 45 | "xlocinfo": "cpp", 46 | "xlocnum": "cpp", 47 | "xmemory": "cpp", 48 | "xstddef": "cpp", 49 | "xstring": "cpp", 50 | "xtr1common": "cpp", 51 | "xtree": "cpp", 52 | "xutility": "cpp" 53 | } 54 | } -------------------------------------------------------------------------------- /web/completed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Document 12 | 13 | 14 |
15 |

Processed video:

16 |
17 |
18 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/avg.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, ops::Deref, time::Duration}; 2 | 3 | pub struct SlidingAverage { 4 | items: Vec, 5 | size: usize, 6 | } 7 | 8 | #[repr(transparent)] 9 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 10 | pub struct DisplayDuration(pub Duration); 11 | 12 | impl Deref for DisplayDuration { 13 | type Target = Duration; 14 | 15 | fn deref(&self) -> &Self::Target { 16 | &self.0 17 | } 18 | } 19 | 20 | impl Display for DisplayDuration { 21 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 22 | let secs = self.0.as_secs(); 23 | // display the most appropriate unit (seconds, minutes, hours or days) 24 | // hours also show minutes, minutes also show seconds 25 | if secs < 60 { 26 | write!(f, "{}s", secs) 27 | } else if secs < 60 * 60 { 28 | write!(f, "{}m {}s", secs / 60, secs % 60) 29 | } else if secs < 60 * 60 * 24 { 30 | write!(f, "{}h {}m {}s", secs / (60 * 60), (secs / 60) % 60, secs % 60) 31 | } else { 32 | write!(f, "{}d {}h {}m {}s", secs / (60 * 60 * 24), (secs / (60 * 60)) % 24, (secs / 60) % 60, secs % 60) 33 | } 34 | } 35 | } 36 | 37 | impl SlidingAverage { 38 | pub fn new(size: usize) -> Self { 39 | Self { 40 | items: Vec::with_capacity(size), 41 | size, 42 | } 43 | } 44 | 45 | pub fn push(&mut self, item: Duration) -> Duration { 46 | self.items.push(item); 47 | if self.items.len() > self.size { 48 | self.items.remove(0); 49 | } 50 | self.average() 51 | } 52 | 53 | pub fn average(&self) -> Duration { 54 | self.items.iter().sum::() / self.items.len() as u32 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_crate_dependencies)] 2 | 3 | use tracing_subscriber::{prelude::__tracing_subscriber_SubscriberExt, EnvFilter, Layer}; 4 | 5 | mod avg; 6 | mod config; 7 | mod ffmpeg; 8 | mod task; 9 | mod web; 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | /* 14 | 1. parse the config file or create a default one 15 | 2. check if ffmpeg and ffprobe are present, abort if not 16 | 3. set up logging to stdout and a log file, abort if file permissions denied 17 | 4. create the task manager thread 18 | 5. spin up the server, bind to port in the config and abort if it fails 19 | 6. create the stdin listener thread, do nothing if not tty 20 | */ 21 | 22 | // TODO: Better ffmpeg error reporing; maybe a separate ffmpeg log file 23 | // TODO: Clean up after panicked/failed tasks 24 | // TODO: Implement re-encoding, since browsers don't like concatenated mp4. 25 | // TODO: The rest of the frontend and API 26 | // TODO: check if the file is suitable before processing 27 | // TODO: upload files to bucket 28 | 29 | // println until logger is set up 30 | println!("{} v{} by {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_AUTHORS")); 31 | println!(";D"); 32 | 33 | // initialize config 34 | config::reload_config().await; 35 | 36 | // block to drop config_lock 37 | { 38 | let config_lock = config::CONFIG.read().await; 39 | 40 | println!("config: {:#?}", config_lock); 41 | 42 | if !config_lock.encoder_found() { 43 | println!("error: encoder not found. specified path: \"{}\"", config_lock.ffmpeg_executable.display()); 44 | std::process::exit(1); 45 | } 46 | 47 | if !config_lock.web_dir_found() { 48 | println!("error: web directory not found. specified path: \"{}\"", config_lock.web_root.display()); 49 | std::process::exit(1); 50 | } 51 | 52 | // initialize logging 53 | 54 | let log_file_name = "log.txt"; 55 | let stdout_subscriber = tracing_subscriber::fmt::layer() 56 | // .pretty() 57 | .with_writer(std::io::stdout) 58 | .with_filter( 59 | EnvFilter::from_default_env() 60 | .add_directive(format!("voice={:?}", config_lock.log_level).parse().unwrap()) 61 | .add_directive("h2=info".parse().unwrap()) 62 | .add_directive("hyper=info".parse().unwrap()), 63 | ); 64 | let file_subscriber = tracing_subscriber::fmt::layer() 65 | // .pretty() 66 | .with_writer(tracing_appender::rolling::hourly(&config_lock.log_file_root, log_file_name)) 67 | .with_ansi(false) 68 | .with_filter( 69 | EnvFilter::from_default_env() 70 | .add_directive(format!("voice={:?}", config_lock.log_level).parse().unwrap()) 71 | .add_directive("h2=info".parse().unwrap()) 72 | .add_directive("hyper=info".parse().unwrap()), 73 | ); 74 | tracing::subscriber::set_global_default( 75 | tracing_subscriber::registry() 76 | .with(stdout_subscriber) 77 | .with(file_subscriber), 78 | ) 79 | .expect("failed to set global default subscriber"); 80 | 81 | tracing::info!("========================================================================="); 82 | tracing::info!("Logging initialized"); 83 | tracing::info!("Log level: {:?}", config_lock.log_level); 84 | tracing::info!("Writing to file: {}", config_lock.log_file_root.join(log_file_name).display()); 85 | } 86 | 87 | web::initialize_server().await; 88 | } 89 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::{Path, PathBuf}, 3 | sync::OnceLock, 4 | }; 5 | use tokio::sync::RwLock; 6 | 7 | const CONFIG_PATH: &str = "config.toml"; 8 | 9 | /// Returned by [`reload_config`]. 10 | /// [`ConfigReloadResult::Ok`] - Config has been updated or written or initialized 11 | /// 12 | /// [`ConfigReloadResult::Err`] - Some error was encountered. This 13 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 14 | pub enum ConfigReloadResult { 15 | Ok, 16 | Err, 17 | } 18 | 19 | /// Wrapper for [`tracing::Level`] which supports serde 20 | #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] 21 | pub enum LogLevel { 22 | Trace, 23 | Debug, 24 | Info, 25 | Warn, 26 | Error, 27 | } 28 | 29 | impl From for tracing::Level { 30 | fn from(value: LogLevel) -> Self { 31 | match value { 32 | LogLevel::Trace => tracing::Level::TRACE, 33 | LogLevel::Debug => tracing::Level::DEBUG, 34 | LogLevel::Info => tracing::Level::INFO, 35 | LogLevel::Warn => tracing::Level::WARN, 36 | LogLevel::Error => tracing::Level::ERROR, 37 | } 38 | } 39 | } 40 | 41 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 42 | pub struct Config { 43 | /// level to log at 44 | pub log_level: LogLevel, 45 | /// incoming file storage 46 | pub inputs_dir: PathBuf, 47 | /// encoding result storage 48 | pub outputs_dir: PathBuf, 49 | /// log file dir 50 | pub log_file_root: PathBuf, 51 | /// encoder executable path 52 | pub ffmpeg_executable: PathBuf, 53 | /// web file root 54 | pub web_root: PathBuf, 55 | /// max input file size in bytes 56 | pub max_file_size: u64, 57 | /// port to bind to 58 | pub port: u16, 59 | /// delete input/output files after this many minutes 60 | pub delete_files_after_minutes: u64, 61 | /// certificate path 62 | pub cert_pem_path: PathBuf, 63 | /// key path 64 | pub key_pem_path: PathBuf, 65 | } 66 | 67 | impl Default for Config { 68 | fn default() -> Self { 69 | Self { 70 | #[cfg(debug_assertions)] 71 | log_level: LogLevel::Debug, 72 | #[cfg(not(debug_assertions))] 73 | log_level: LogLevel::Info, 74 | inputs_dir: PathBuf::from("./inputs"), 75 | outputs_dir: PathBuf::from("./outputs"), 76 | log_file_root: PathBuf::from("./logs"), 77 | web_root: PathBuf::from("./web"), 78 | ffmpeg_executable: PathBuf::from("ffmpeg"), 79 | max_file_size: 1024 * 1024 * 1024, // 1 GiB 80 | port: 443, 81 | delete_files_after_minutes: 60, 82 | cert_pem_path: PathBuf::from("./certificates/cert.pem"), 83 | key_pem_path: PathBuf::from("./certificates/key.pem"), 84 | } 85 | } 86 | } 87 | 88 | impl Config { 89 | pub fn encoder_found(&self) -> bool { 90 | which::which(&self.ffmpeg_executable).is_ok() 91 | } 92 | 93 | pub fn web_dir_found(&self) -> bool { 94 | if !self.web_root.exists() { 95 | return false; 96 | } 97 | 98 | let mut index = false; 99 | let mut completed = false; 100 | for file in self 101 | .web_root 102 | .read_dir() 103 | .unwrap() 104 | .flatten() 105 | .map(|x| x.file_name().to_string_lossy().to_string()) 106 | { 107 | if file == "index.html" { 108 | index = true; 109 | } 110 | if file == "completed.html" { 111 | completed = true; 112 | } 113 | } 114 | 115 | index && completed 116 | } 117 | 118 | pub fn init_directories(&self) -> std::io::Result<()> { 119 | std::fs::create_dir_all(&self.inputs_dir)?; 120 | std::fs::create_dir_all(&self.outputs_dir)?; 121 | std::fs::create_dir_all(&self.log_file_root) 122 | } 123 | } 124 | 125 | pub struct ConfigStatic(OnceLock>); 126 | 127 | impl ConfigStatic { 128 | pub async fn read(&self) -> tokio::sync::RwLockReadGuard<'_, Config> { 129 | // SAFETY: config is intialized in main 130 | unsafe { self.0.get().unwrap_unchecked().read().await } 131 | } 132 | } 133 | 134 | pub static CONFIG: ConfigStatic = ConfigStatic(OnceLock::new()); 135 | 136 | /// Reloads the app config. 137 | /// 138 | /// Loads the config form the file or writes to the file, if one was created by calling this function. 139 | /// Initializes the app config if it has not been initialized yet. 140 | /// No-op if the app config has not been initialized yet. 141 | pub async fn reload_config() -> ConfigReloadResult { 142 | let config_file_path = Path::new(CONFIG_PATH); 143 | 144 | let mut current_config_lock = CONFIG.0.get_or_init(|| RwLock::new(Config::default())).write().await; 145 | 146 | // Read the config file if it exists. 147 | // Returns on io or parse error, otherwise evaluates to [`Option`], 148 | // depending on whether config file exists 149 | let config_read_option: Option = match config_file_path.try_exists() { 150 | Ok(false) => None, 151 | Ok(true) => { 152 | let Ok(file_data) = std::fs::read_to_string(config_file_path).map_err(|_| ()) else { 153 | return ConfigReloadResult::Err; 154 | }; 155 | toml::from_str(&file_data).ok() 156 | } 157 | Err(_) => return ConfigReloadResult::Err, 158 | }; 159 | 160 | match config_read_option { 161 | // Config file exists => replace current config with the loaded one 162 | Some(new_config) => { 163 | *current_config_lock = new_config; 164 | } 165 | // Config file does not exist => write current config to file 166 | None => { 167 | let config_string = toml::to_string(&*current_config_lock).expect("failed to serialize config to toml"); 168 | // this should never fail, because we already made some syscalls to check if the file exists 169 | std::fs::write(config_file_path, config_string).expect("failed to write config file"); 170 | } 171 | } 172 | 173 | current_config_lock 174 | .init_directories() 175 | .expect("failed to create necessary directories"); 176 | 177 | ConfigReloadResult::Ok 178 | } 179 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Document 11 | 12 | 13 |
14 |

Upload video!

15 |
16 | 17 |
Drop file here
18 |
19 |
20 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /src/ffmpeg.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Display, io, ops::Range, path::PathBuf, process::Stdio, sync::Arc, 3 | time::Duration, 4 | }; 5 | 6 | use tokio::{ 7 | io::AsyncWriteExt, 8 | process::{Child, Command}, 9 | }; 10 | 11 | pub struct FFmpeg { 12 | input: PathBuf, 13 | output: PathBuf, 14 | exec: PathBuf, 15 | } 16 | 17 | const SILENCEDETECT_NOISE: &str = "-50dB"; 18 | const SILENCEDETECT_DURATION: &str = "0.1"; 19 | 20 | enum OutputParser { 21 | Start, 22 | End(f32), 23 | Duration(f32, f32), 24 | } 25 | 26 | impl OutputParser { 27 | fn next(self, line: &str) -> io::Result<(Self, Option>)> { 28 | match self { 29 | OutputParser::Start => Ok(( 30 | OutputParser::End( 31 | Self::parse_line(line, "start")? 32 | .ok_or(io::Error::new(io::ErrorKind::InvalidData, line))?, 33 | ), 34 | None, 35 | )), 36 | OutputParser::End(start) => Ok(( 37 | OutputParser::Duration( 38 | start, 39 | Self::parse_line(line, "end")? 40 | .ok_or(io::Error::new(io::ErrorKind::InvalidData, line))?, 41 | ), 42 | None, 43 | )), 44 | OutputParser::Duration(start, end) => { 45 | Ok((OutputParser::Start, Some(start..end))) 46 | } 47 | } 48 | } 49 | 50 | fn parse_line(line: &str, postfix: &str) -> io::Result> { 51 | let trimmed = line.trim(); 52 | let starts = format!("lavfi.silence_{postfix}="); 53 | if trimmed.starts_with(&starts) { 54 | let n = line 55 | .split_at(starts.len()) 56 | .1 57 | .parse::() 58 | .map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?; 59 | return Ok(Some(n)); 60 | } 61 | Ok(None) 62 | } 63 | } 64 | 65 | pub struct VideoAnalysis { 66 | pub audible: Vec>, 67 | pub duration: Duration, 68 | } 69 | 70 | impl VideoAnalysis { 71 | fn new(silence: Vec>, duration: Duration) -> Self { 72 | let mut audible = Vec::new(); 73 | silence.into_iter().fold(0.0, |prev, range| { 74 | audible.push(prev..range.start); 75 | range.end 76 | }); 77 | Self { audible, duration } 78 | } 79 | } 80 | 81 | #[derive(Debug, Clone)] 82 | pub enum FFmpegError { 83 | FFmpeg(String), 84 | IO(Arc), 85 | } 86 | 87 | impl From for FFmpegError { 88 | fn from(err: io::Error) -> Self { 89 | Self::IO(Arc::new(err)) 90 | } 91 | } 92 | 93 | impl Display for FFmpegError { 94 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 95 | match self { 96 | Self::FFmpeg(str) => write!(f, "{str}"), 97 | Self::IO(io) => write!(f, "{io}"), 98 | } 99 | } 100 | } 101 | 102 | impl FFmpeg { 103 | pub fn new(input: PathBuf, output: PathBuf, exec: PathBuf) -> Self { 104 | Self { input, output, exec } 105 | } 106 | 107 | /// returns an array of silent periods 108 | pub async fn analyze_silence(&self) -> Result { 109 | let mut ffmpeg = self.prepare_command(); 110 | ffmpeg 111 | .arg("-vn") 112 | .arg("-hide_banner") 113 | .arg("-af") 114 | .arg(format!( 115 | "silencedetect=noise={SILENCEDETECT_NOISE}:d={SILENCEDETECT_DURATION},ametadata=mode=print:file=-" 116 | )) 117 | .arg("-f") 118 | .arg("null") 119 | .arg("-"); 120 | 121 | let mut ranges = Vec::new(); 122 | let mut parser_state = OutputParser::Start; 123 | 124 | let output = ffmpeg.output().await?; 125 | let stdout = output.stdout; 126 | let stderr_text = String::from_utf8(output.stderr).unwrap(); 127 | 128 | let perms = tokio::fs::metadata(&self.input).await.unwrap().permissions(); 129 | tracing::debug!("perms: {:?}", perms); 130 | let status = ffmpeg.status().await.unwrap(); 131 | if !status.success() { 132 | tracing::debug!("silence status: {status:?}"); 133 | return Err(FFmpegError::FFmpeg(stderr_text)); 134 | } 135 | 136 | let video_duration = { 137 | // coded in epic hurry 138 | let str = "Duration: "; 139 | let i = stderr_text.find(str).unwrap(); 140 | let split = 141 | stderr_text.split_at(i + str.len()).1.split_once(',').unwrap().0.trim(); 142 | let split = 143 | split.split(':').map(|x| x.parse::().unwrap()).collect::>(); 144 | Duration::from_secs_f32(split[0] * 3600.0 + split[1] * 60.0 + split[2]) 145 | }; 146 | 147 | for line in String::from_utf8(stdout).unwrap().lines() { 148 | // lavfi.silence_*= 149 | if line.starts_with("lavfi") { 150 | let (next_state, range) = parser_state.next(line)?; 151 | parser_state = next_state; 152 | if let Some(range) = range { 153 | ranges.push(range); 154 | } 155 | } 156 | } 157 | 158 | // sometimes the silencedetect doesn't output silence_end 159 | // if close to end of video 160 | if let OutputParser::End(start) = parser_state { 161 | ranges.push(start..video_duration.as_secs_f32()); 162 | } 163 | 164 | Ok(VideoAnalysis::new(ranges, video_duration)) 165 | } 166 | 167 | pub async fn spawn_remove_silence( 168 | &self, 169 | keep_fragments: &[Range], 170 | ) -> io::Result { 171 | if keep_fragments.is_empty() { 172 | return Err(io::Error::new( 173 | io::ErrorKind::InvalidInput, 174 | "no fragments to keep", 175 | )); 176 | } 177 | let filter = keep_fragments 178 | .iter() 179 | .map(|x| format!("between(t\\,{}\\,{})", x.start, x.end)) 180 | .reduce(|a, b| format!("{}+{}", a, b)) 181 | .unwrap(); 182 | 183 | let remove_fragments = keep_fragments 184 | .windows(2) 185 | .map(|ab| ab[0].end..ab[1].start) 186 | .collect::>(); 187 | 188 | let pts_shifts = remove_fragments 189 | .into_iter() 190 | .map(|x| format!("gt(T,{})*({})", x.start, x.end - x.start)) 191 | .reduce(|a, b| format!("{}+{}", a, b)) 192 | .unwrap(); 193 | 194 | let pts_expr = format!("PTS-STARTPTS-({pts_shifts})/TB",); 195 | 196 | let vf = 197 | format!("select='{filter}',setpts='{pts_expr}',scale='trunc(oh*a/2)*2:576'"); 198 | let af = format!("aselect='{filter}',asetpts='{pts_expr}'"); 199 | 200 | let filter_complex = format!("[0:v]{vf}[video];[0:a]{af}[audio]"); 201 | 202 | let mut ffmpeg = self.prepare_command(); 203 | ffmpeg 204 | .arg("-progress") 205 | .arg("-") 206 | .arg("-loglevel") 207 | .arg("error") 208 | .args(["-stats_period", "0.3"]) 209 | .arg("-filter_complex_script") 210 | .arg("pipe:0") 211 | .arg("-map") 212 | .arg("[video]") 213 | .arg("-map") 214 | .arg("[audio]") 215 | .args(["-c:v", "libx264", "-preset", "ultrafast"]) 216 | .args(["-c:a", "libopus"]) 217 | .args(["-f", "mp4"]) 218 | .arg(&self.output); 219 | 220 | tracing::debug!("ffmpeg: {:?}", ffmpeg); 221 | 222 | let mut child = ffmpeg.spawn()?; 223 | 224 | let mut stdin = child.stdin.take().unwrap(); 225 | stdin.write_all(filter_complex.as_bytes()).await.unwrap(); 226 | stdin.shutdown().await.unwrap(); 227 | 228 | Ok(child) 229 | } 230 | 231 | /// creates an ffmpeg `Command` with null pipes and input file 232 | /// as input, loglevel=error, so stderr only contains errors 233 | /// if any 234 | fn prepare_command(&self) -> Command { 235 | let mut cmd = Command::new(&self.exec); 236 | cmd.stdin(Stdio::null()) 237 | .stdout(Stdio::piped()) 238 | .stdin(Stdio::piped()) 239 | .stderr(Stdio::piped()) 240 | .arg("-i") 241 | .arg(&self.input) 242 | .kill_on_drop(true); 243 | cmd 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/task.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Display, 3 | path::{Path, PathBuf}, 4 | sync::Arc, 5 | time::Duration, 6 | }; 7 | 8 | use rand::Rng; 9 | use tokio::{ 10 | io::{self, AsyncBufReadExt, BufReader}, 11 | sync::RwLock, 12 | }; 13 | 14 | use crate::ffmpeg::{FFmpeg, FFmpegError}; 15 | 16 | pub type TaskUpdateSender = tokio::sync::broadcast::Sender; 17 | 18 | #[derive(Debug)] 19 | struct InnerTask { 20 | last_status: TaskStatus, 21 | task_update_tx: TaskUpdateSender, 22 | } 23 | 24 | /// Represents a task that is currently running 25 | /// with a handle to the encoder process 26 | /// 27 | /// `dir_name` is the name of the directory where 28 | /// the task's files are stored. 29 | #[derive(Debug)] 30 | pub struct Task { 31 | pub id: TaskId, 32 | start_time: time::OffsetDateTime, 33 | tokio_handle: tokio::task::JoinHandle<()>, 34 | inner: Arc>, 35 | } 36 | 37 | #[derive(Clone, Debug, serde::Serialize)] 38 | #[serde(rename_all = "camelCase")] 39 | #[serde(tag = "type", content = "data")] 40 | pub enum TaskStatus { 41 | InProgress { 42 | progress: f32, 43 | speed: f32, 44 | }, 45 | #[serde(serialize_with = "display_serialize")] 46 | Error(FFmpegError), 47 | Completed { 48 | end_time: time::OffsetDateTime, 49 | }, 50 | } 51 | 52 | fn display_serialize(x: &T, s: S) -> Result { 53 | s.collect_str(x) 54 | } 55 | 56 | /// id of task 57 | /// also used as name of task's input and output files 58 | pub type TaskId = u64; 59 | pub type TaskUpdateMessage = (TaskId, TaskStatus); 60 | 61 | macro_rules! try_else { 62 | ($expr:expr, $vn:ident, $div:block) => { 63 | match $expr { 64 | Result::Ok(val) => val, 65 | Result::Err($vn) => $div, 66 | } 67 | }; 68 | } 69 | 70 | enum StatsParse { 71 | Time(Duration), 72 | Speed(f32), 73 | /// error or not any other stat 74 | Other, 75 | } 76 | 77 | impl StatsParse { 78 | fn parse_line(line: &str) -> Self { 79 | let mut line = line.split('='); 80 | 81 | let Some(lhs) = line.next() else { 82 | return Self::Other; 83 | }; 84 | let Some(rhs) = line.next() else { 85 | return Self::Other; 86 | }; 87 | 88 | match lhs { 89 | "out_time_ms" => rhs 90 | .parse::() 91 | .map(|x| { 92 | // time is negative during first frame 93 | Self::Time(Duration::from_micros(x.max(0) as u64)) 94 | }) 95 | .unwrap_or(Self::Other), 96 | "speed" => rhs 97 | .trim_end_matches('x') 98 | .parse::() 99 | .map(Self::Speed) 100 | .unwrap_or(Self::Other), 101 | _ => Self::Other, 102 | } 103 | } 104 | } 105 | 106 | impl Task { 107 | /// initialize and start a new task 108 | /// also start a tokio task 109 | /// to observe it 110 | pub fn new( 111 | input_file: PathBuf, 112 | task_id: TaskId, 113 | outputs_dir: &Path, 114 | ffmpeg_executable: PathBuf, 115 | ) -> io::Result { 116 | let output_file = outputs_dir.join(format!("{task_id}")); 117 | 118 | let last_status = TaskStatus::InProgress { 119 | progress: 0.0, 120 | speed: 0.0, 121 | }; 122 | let task_update_tx = tokio::sync::broadcast::Sender::new(8); 123 | 124 | let inner_task = Arc::new(RwLock::new(InnerTask { 125 | last_status, 126 | task_update_tx: task_update_tx.clone(), 127 | })); 128 | 129 | let tokio_handle = tokio::task::spawn({ 130 | let inner_task = inner_task.clone(); 131 | async move { 132 | let conversion_result = 133 | Self::run_conversion(input_file, output_file, ffmpeg_executable, inner_task.clone(), task_id).await; 134 | 135 | // ignore send result 136 | let final_status = match conversion_result { 137 | Ok(_) => TaskStatus::Completed { 138 | end_time: time::OffsetDateTime::now_utc(), 139 | }, 140 | Err(msg) => TaskStatus::Error(msg), 141 | }; 142 | 143 | tracing::debug!("sent last update: {final_status:?}"); 144 | Self::update_status(&mut *inner_task.write().await, task_id, final_status).await; 145 | } 146 | }); 147 | 148 | Ok(Self { 149 | tokio_handle, 150 | id: task_id, 151 | inner: inner_task, 152 | start_time: time::OffsetDateTime::now_utc(), 153 | }) 154 | } 155 | 156 | pub async fn cancel(self) { 157 | self.tokio_handle.abort(); 158 | let mut this = self.inner.write().await; 159 | Self::update_status( 160 | &mut this, 161 | self.id, 162 | TaskStatus::Completed { 163 | end_time: time::OffsetDateTime::now_utc(), 164 | }, 165 | ) 166 | .await; 167 | } 168 | 169 | pub async fn subscribe(&self) -> tokio::sync::broadcast::Receiver { 170 | self.inner.read().await.task_update_tx.subscribe() 171 | } 172 | 173 | pub async fn last_status(&self) -> TaskStatus { 174 | self.inner.read().await.last_status.clone() 175 | } 176 | 177 | pub fn last_status_blocking(&self) -> TaskStatus { 178 | self.inner.blocking_read().last_status.clone() 179 | } 180 | 181 | pub fn gen_id() -> TaskId { 182 | rand::thread_rng().gen() 183 | } 184 | 185 | async fn update_status(inner: &mut InnerTask, id: TaskId, new_status: TaskStatus) { 186 | inner.last_status = new_status.clone(); 187 | let _res = inner.task_update_tx.send((id, new_status.clone())); 188 | } 189 | 190 | async fn run_conversion( 191 | input_file: PathBuf, 192 | output_file: PathBuf, 193 | ffmpeg_executable: PathBuf, 194 | inner: Arc>, 195 | task_id: TaskId, 196 | ) -> Result<(), FFmpegError> { 197 | tracing::debug!("begin task"); 198 | 199 | let ffmpeg = FFmpeg::new(input_file.to_path_buf(), output_file, ffmpeg_executable.to_path_buf()); 200 | 201 | let analysis = try_else!(ffmpeg.analyze_silence().await, err, { 202 | tracing::info!("analyze silence error: {:?}", err); 203 | return Err(err); 204 | }); 205 | 206 | let playtime_after_conversion_s = analysis.audible.iter().map(|x| x.end - x.start).sum::(); 207 | 208 | tracing::debug!( 209 | "total playtime: {}s; playtime after conversion: {}s; playtime reduced by {}%", 210 | analysis.duration.as_secs_f32(), 211 | playtime_after_conversion_s, 212 | (1.0 - playtime_after_conversion_s / analysis.duration.as_secs_f32()) * 100.0 213 | ); 214 | 215 | let mut child = try_else!(ffmpeg.spawn_remove_silence(&analysis.audible).await, err, { 216 | tracing::info!("remove silence error: {:?}", err); 217 | return Err(FFmpegError::IO(err.into())); 218 | }); 219 | 220 | let stdout = child.stdout.take().unwrap(); 221 | let mut lines = BufReader::new(stdout).lines(); 222 | 223 | let stderr = child.stderr.take().unwrap(); 224 | let mut err_lines = BufReader::new(stderr).lines(); 225 | let mut error_log = Vec::new(); 226 | // loop awaits on ffmpeg's stdout 227 | loop { 228 | // race reading an error and reading a line 229 | // break if EOF 230 | let line = tokio::select! { 231 | _ = tokio::time::sleep(Duration::from_secs(1)) => { 232 | tracing::warn!("ffmpeg lagging"); 233 | None 234 | } 235 | x = err_lines.next_line() => { 236 | match x.unwrap() { 237 | Some(x) => { 238 | error_log.push(x); 239 | None 240 | }, 241 | None => None 242 | } 243 | 244 | } 245 | x = lines.next_line() => x.unwrap() 246 | }; 247 | 248 | // abort if the process is done 249 | match child.try_wait() { 250 | Ok(None) => {} 251 | Ok(Some(_)) => { 252 | break; 253 | } 254 | err @ Err(_) => { 255 | tracing::warn!("error while calling try_wait()"); 256 | err.unwrap(); 257 | } 258 | } 259 | 260 | // skip iteration if the line is from stderr 261 | let Some(line) = line else { 262 | continue; 263 | }; 264 | 265 | let mut inner_lock = inner.write().await; 266 | 267 | // output_time_ms is the same as _us bug in ffmpeg 268 | if let TaskStatus::InProgress { 269 | ref mut progress, 270 | ref mut speed, 271 | } = inner_lock.last_status 272 | { 273 | match StatsParse::parse_line(&line) { 274 | StatsParse::Speed(new_speed) => { 275 | *speed = new_speed; 276 | } 277 | StatsParse::Time(new_time) => { 278 | let new_progress = new_time.as_secs_f32() / (playtime_after_conversion_s); 279 | *progress = new_progress.min(1.0); // may overflow a little somtimes 280 | } 281 | _ => { 282 | // nothing was updated 283 | continue; 284 | } 285 | } 286 | } 287 | let last_status = inner_lock.last_status.clone(); 288 | Self::update_status(&mut inner_lock, task_id, last_status).await; 289 | tracing::debug!("status: {:?}", inner_lock.last_status); 290 | } 291 | 292 | let status = child.wait().await.unwrap(); 293 | tracing::debug!("status: {:?} success: {}", status, status.success()); 294 | 295 | if status.success() { 296 | Ok(()) 297 | } else { 298 | Err(FFmpegError::FFmpeg(error_log.join("\n"))) 299 | } 300 | 301 | // if !error_log.is_empty() { 302 | // tracing::info!("errors during task: {}", error_log.join("\n")); 303 | // } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/web.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::HashMap; 3 | use std::io; 4 | 5 | use std::net::SocketAddr; 6 | use std::sync::Arc; 7 | use std::time::Duration; 8 | 9 | use axum::body::{Bytes, StreamBody}; 10 | use axum::extract::multipart::MultipartRejection; 11 | use axum::extract::ws::rejection::WebSocketUpgradeRejection; 12 | use axum::extract::ws::{self, WebSocket}; 13 | use axum::extract::{DefaultBodyLimit, Multipart, Path, Query, State, WebSocketUpgrade}; 14 | use axum::http::header::{CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_TYPE}; 15 | use axum::http::{HeaderMap, Request, StatusCode}; 16 | use axum::middleware::Next; 17 | use axum::response::{IntoResponse, Response}; 18 | use axum::{debug_handler, middleware}; 19 | use axum::{ 20 | routing::{get, post}, 21 | Router, 22 | }; 23 | 24 | use axum_server::tls_rustls::RustlsConfig; 25 | use tokio::sync::{RwLock, RwLockReadGuard}; 26 | use tokio_util::io::ReaderStream; 27 | use tower_http::cors::{AllowHeaders, AllowOrigin}; 28 | use tower_http::services::ServeDir; 29 | 30 | use crate::config::CONFIG; 31 | use crate::task::{Task, TaskId, TaskStatus, TaskUpdateMessage}; 32 | use crate::{config, task}; 33 | 34 | struct TaskManager { 35 | tasks: Arc>>, 36 | } 37 | 38 | impl TaskManager { 39 | fn new() -> Self { 40 | Self { 41 | tasks: Arc::new(RwLock::new(HashMap::new())), 42 | } 43 | } 44 | 45 | async fn new_task(&self, input_data: impl AsRef<[u8]>) -> io::Result { 46 | let config_lock = CONFIG.read().await; 47 | 48 | let task_id = Task::gen_id(); 49 | let task_id_string = task_id.to_string(); 50 | 51 | let input_file_path = config_lock.inputs_dir.join(&task_id_string); 52 | 53 | tokio::fs::write(&input_file_path, input_data).await?; 54 | 55 | let task = 56 | Task::new(input_file_path, task_id, &config_lock.outputs_dir, config_lock.ffmpeg_executable.clone())?; 57 | 58 | self.tasks.write().await.insert(task_id, task); 59 | 60 | Ok(task_id) 61 | } 62 | 63 | async fn cleanup_tasks(&self) { 64 | let config_lock = CONFIG.read().await; 65 | let current_time = time::OffsetDateTime::now_utc(); 66 | let mut keys_to_delete = Vec::new(); 67 | let mut tasks_lock = self.tasks.write().await; 68 | for (id, task) in tasks_lock.iter() { 69 | let status = task.last_status().await; 70 | if let TaskStatus::Completed { end_time } = status { 71 | let to_delete = 72 | end_time + time::Duration::minutes(config_lock.delete_files_after_minutes as i64) < current_time; 73 | if to_delete { 74 | tracing::info!("deleting task {}", id); 75 | keys_to_delete.push(*id); 76 | } 77 | } 78 | } 79 | tasks_lock.retain(|k, _| !keys_to_delete.contains(k)); 80 | } 81 | 82 | async fn cleanup_task_files(&self) { 83 | let config_lock = CONFIG.read().await; 84 | for dir_entry in config_lock.outputs_dir.read_dir().unwrap().flatten() { 85 | let file_name = dir_entry.file_name().to_string_lossy().to_string(); 86 | if let Ok(task_id) = file_name.parse::() { 87 | if !self.tasks.read().await.contains_key(&task_id) { 88 | tracing::info!("cleaning output file {}", task_id); 89 | let _ = tokio::fs::remove_file(dir_entry.path()).await; 90 | } 91 | } 92 | } 93 | for dir_entry in config_lock.inputs_dir.read_dir().unwrap().flatten() { 94 | let file_name = dir_entry.file_name().to_string_lossy().to_string(); 95 | if let Ok(task_id) = file_name.parse::() { 96 | if !self.tasks.read().await.contains_key(&task_id) { 97 | tracing::info!("cleaning input file {}", task_id); 98 | let _ = tokio::fs::remove_file(dir_entry.path()).await; 99 | } 100 | } 101 | } 102 | } 103 | 104 | async fn get_task(&self, id: TaskId) -> Option> { 105 | let a = self.tasks.read().await; 106 | a.get(&id)?; 107 | Some(RwLockReadGuard::map(a, |x| x.get(&id).unwrap())) 108 | } 109 | } 110 | 111 | #[derive(Clone)] 112 | struct AppState { 113 | task_manager: Arc, 114 | } 115 | 116 | fn spawn_task_cleaner(task_manager: Arc) { 117 | tokio::task::spawn(async move { 118 | loop { 119 | task_manager.cleanup_tasks().await; 120 | task_manager.cleanup_task_files().await; 121 | tokio::time::sleep(Duration::from_secs(60)).await; 122 | } 123 | }); 124 | } 125 | 126 | pub async fn initialize_server() { 127 | let app_state: AppState = AppState { 128 | task_manager: Arc::new(TaskManager::new()), 129 | }; 130 | 131 | spawn_task_cleaner(app_state.task_manager.clone()); 132 | 133 | let router = Router::new() 134 | .route("/submit", post(submit)) 135 | .route("/status", get(status)) 136 | .route("/status_ws", get(status_ws)) 137 | .route("/videos/:video", get(videos)) 138 | .fallback_service(ServeDir::new(CONFIG.read().await.web_root.clone())) 139 | .with_state(app_state) 140 | .layer(middleware::from_fn(meta_header_middleware)) 141 | .layer(DefaultBodyLimit::max(config::CONFIG.read().await.max_file_size as usize)) 142 | .layer( 143 | tower_http::cors::CorsLayer::permissive() 144 | .allow_origin(AllowOrigin::mirror_request()) 145 | .allow_credentials(true) 146 | .allow_headers(AllowHeaders::mirror_request()) 147 | .allow_methods(["GET".parse().unwrap(), "POST".parse().unwrap()]) 148 | .expose_headers([CONTENT_TYPE, CONTENT_LENGTH]), 149 | ); 150 | 151 | let config_lock = config::CONFIG.read().await; 152 | 153 | let tls_config = RustlsConfig::from_pem_file(&config_lock.cert_pem_path, &config_lock.key_pem_path) 154 | .await 155 | .unwrap(); 156 | 157 | let addr = SocketAddr::from(([0, 0, 0, 0], config_lock.port)); 158 | tracing::info!("Using tls"); 159 | tracing::debug!("Started server on {}", addr); 160 | axum_server::bind_rustls(addr, tls_config) 161 | .serve(router.into_make_service()) 162 | .await 163 | .unwrap(); 164 | } 165 | 166 | async fn meta_header_middleware(request: Request, next: Next) -> Response { 167 | let mut response = next.run(request).await; 168 | response 169 | .headers_mut() 170 | .insert("Server", format!("axum; voice/{}", env!("CARGO_PKG_VERSION")).parse().unwrap()); 171 | response 172 | } 173 | 174 | enum EndpointResult { 175 | Ok(T), 176 | Accepted(T), 177 | Err(StatusCode, Option>), 178 | } 179 | 180 | impl IntoResponse for EndpointResult { 181 | fn into_response(self) -> axum::response::Response { 182 | match self { 183 | Self::Ok(t) => t.into_response(), 184 | Self::Accepted(r) => (StatusCode::ACCEPTED, r).into_response(), 185 | Self::Err(code, msg) => (code, msg.unwrap_or_default()).into_response(), 186 | } 187 | } 188 | } 189 | 190 | async fn parse_multipart<'a>(multipart: &mut Multipart) -> Result> { 191 | while let Some(a) = multipart.next_field().await.ok().flatten() { 192 | let Some(name) = a.name() else { 193 | return Err("No name for field".into()); 194 | }; 195 | if name == "file" { 196 | let is_good_mime = a.content_type().map(|x| x.starts_with("video/")).unwrap_or(false); 197 | if is_good_mime { 198 | return a.bytes().await.map_err(|_| "Failed to read body".into()); 199 | } 200 | } 201 | } 202 | Err("No file field".into()) 203 | } 204 | 205 | async fn drain_multipart(mut multipart: Multipart) { 206 | while let Some(mut field) = multipart.next_field().await.ok().flatten() { 207 | while let Some(x) = field.chunk().await.ok().flatten() { 208 | // tracing::debug!("drained {} bytes", x.len()); 209 | drop(x); 210 | } 211 | } 212 | } 213 | 214 | /// Submit a video file to be encoded 215 | /// Accepts a `multipart/form-data` request with a `file` field 216 | /// 217 | /// Returns the id of the encoding task, which the client may later query 218 | /// or an error along with an explanation message if the request is malformed. 219 | #[debug_handler] 220 | async fn submit(state: State, multipart: Result) -> EndpointResult { 221 | tracing::debug!("submit {:?}", multipart.as_ref().map(|_| ())); 222 | match multipart { 223 | Ok(mut multipart) => { 224 | let input_data = match parse_multipart(&mut multipart).await { 225 | Ok(x) => x, 226 | Err(msg) => return EndpointResult::Err(StatusCode::BAD_REQUEST, Some(msg)), 227 | }; 228 | 229 | // drain the request so it's possible to send a response 230 | // in case the client sent multiple fields 231 | drain_multipart(multipart).await; 232 | 233 | tracing::debug!("Length of file is {} bytes", input_data.len()); 234 | 235 | let task_id = match state.task_manager.new_task(input_data).await { 236 | Ok(task_id) => task_id, 237 | Err(err) => { 238 | let err_string = err.to_string(); 239 | tracing::error!("Failed to start task: {}", err_string); 240 | return EndpointResult::Err(StatusCode::INTERNAL_SERVER_ERROR, Some(err_string.into())); 241 | } 242 | }; 243 | 244 | EndpointResult::Accepted(task_id.to_string()) 245 | } 246 | Err(err) => EndpointResult::Err(StatusCode::BAD_REQUEST, Some(err.to_string().into())), 247 | } 248 | } 249 | 250 | #[derive(serde::Deserialize)] 251 | struct TaskStatusQuery { 252 | t: TaskId, 253 | } 254 | 255 | async fn status( 256 | state: State, 257 | Query(TaskStatusQuery { t }): Query, 258 | ) -> EndpointResult { 259 | let Some(status) = state.task_manager.get_task(t).await else { 260 | return EndpointResult::Err(StatusCode::NOT_FOUND, Some("task not found".into())); 261 | }; 262 | 263 | EndpointResult::Ok(serde_json::to_string(&status.last_status().await).unwrap()) 264 | } 265 | 266 | #[debug_handler] 267 | async fn status_ws( 268 | ws: Result, 269 | state: State, 270 | Query(TaskStatusQuery { t }): Query, 271 | ) -> EndpointResult { 272 | let ws = match ws { 273 | Ok(x) => x, 274 | Err(err) => { 275 | tracing::info!("ws upgrade rejected: {}", err); 276 | return EndpointResult::Err(err.status(), None); 277 | } 278 | }; 279 | 280 | // reject upgrade if no task found 281 | let (first_status, rx) = match state.task_manager.get_task(t).await { 282 | Some(task) => (task.last_status().await, task.subscribe().await), 283 | None => { 284 | tracing::info!("ws upgrade rejected: task not found"); 285 | return EndpointResult::Err(StatusCode::NOT_FOUND, None); 286 | } 287 | }; 288 | 289 | EndpointResult::Ok( 290 | ws.on_failed_upgrade(|_| tracing::info!("ws upgrade failed")) 291 | .on_upgrade(move |ws| ws_handler(ws, t, first_status, rx)), 292 | ) 293 | } 294 | 295 | async fn ws_handler( 296 | mut ws: WebSocket, 297 | target_task: TaskId, 298 | first_status: TaskStatus, 299 | mut task_rx: tokio::sync::broadcast::Receiver, 300 | ) { 301 | tracing::info!("ws connected"); 302 | if ws.send(ws::Message::Ping(Vec::new())).await.is_err() { 303 | tracing::info!("can't ping ws"); 304 | return; 305 | } 306 | 307 | // send first status because the socked might 308 | // subscribe to channel after the task stops 309 | // and it will never receive the error 310 | let _ = ws 311 | .send(ws::Message::Text(serde_json::to_string(&first_status).unwrap())) 312 | .await; 313 | 314 | tokio::spawn(async move { 315 | loop { 316 | let rcv = task_rx.recv().await; 317 | match rcv { 318 | Ok(msg) => { 319 | if let Err(x) = ws.send(ws::Message::Text(serde_json::to_string(&msg.1).unwrap())).await { 320 | let x = x.into_inner(); 321 | if matches!( 322 | x.downcast_ref::().map(|x| x.kind()), 323 | Some(io::ErrorKind::ConnectionAborted) 324 | ) { 325 | tracing::info!("failed to send ws message for {target_task}: ws closed {x:?}"); 326 | break; 327 | } else { 328 | tracing::info!("failed to send ws message for {target_task}: {x:?}"); 329 | } 330 | }; 331 | 332 | if matches!(msg.1, TaskStatus::Error(_) | TaskStatus::Completed { .. }) { 333 | // give axum time to flush the socket 334 | tokio::time::sleep(Duration::from_millis(500)).await; 335 | let _ = ws.close().await; 336 | break; 337 | } 338 | } 339 | Err(tokio::sync::broadcast::error::RecvError::Closed) => { 340 | tracing::warn!("tx closed for {target_task}"); 341 | } 342 | Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { 343 | tracing::warn!("rx lagged by {n} for {target_task}"); 344 | } 345 | } 346 | } 347 | }); 348 | } 349 | 350 | #[derive(serde::Deserialize)] 351 | struct VideoDlQuery { 352 | dl: u32, 353 | } 354 | 355 | async fn videos( 356 | state: State, 357 | Path(task_id): Path, 358 | query: Option>, 359 | ) -> EndpointResult<(HeaderMap, StreamBody>)> { 360 | if let Some(task) = state.task_manager.get_task(task_id).await { 361 | if matches!(task.last_status().await, TaskStatus::InProgress { .. }) { 362 | return EndpointResult::Err(StatusCode::NOT_FOUND, Some("video not found".into())); 363 | } 364 | } 365 | 366 | let file_path = CONFIG.read().await.outputs_dir.join(task_id.to_string()); 367 | let Ok(file_handle) = tokio::fs::File::open(file_path).await else { 368 | // usually 369 | return EndpointResult::Err(StatusCode::NOT_FOUND, Some("video not found".into())); 370 | }; 371 | 372 | let mut headers = HeaderMap::new(); 373 | headers.append(CONTENT_TYPE, "video/mp4".parse().unwrap()); 374 | headers.append(CONTENT_LENGTH, file_handle.metadata().await.unwrap().len().into()); 375 | if matches!(query, Some(Query(VideoDlQuery { dl: 1 }))) { 376 | headers.append(CONTENT_DISPOSITION, "attachment".parse().unwrap()); 377 | } 378 | 379 | EndpointResult::Ok((headers, StreamBody::new(ReaderStream::new(file_handle)))) 380 | } 381 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "arc-swap" 31 | version = "1.6.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" 34 | 35 | [[package]] 36 | name = "async-trait" 37 | version = "0.1.74" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" 40 | dependencies = [ 41 | "proc-macro2", 42 | "quote", 43 | "syn", 44 | ] 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.1.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 51 | 52 | [[package]] 53 | name = "axum" 54 | version = "0.6.20" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" 57 | dependencies = [ 58 | "async-trait", 59 | "axum-core", 60 | "axum-macros", 61 | "base64", 62 | "bitflags 1.3.2", 63 | "bytes", 64 | "futures-util", 65 | "http", 66 | "http-body", 67 | "hyper", 68 | "itoa", 69 | "matchit", 70 | "memchr", 71 | "mime", 72 | "multer", 73 | "percent-encoding", 74 | "pin-project-lite", 75 | "rustversion", 76 | "serde", 77 | "serde_json", 78 | "serde_path_to_error", 79 | "serde_urlencoded", 80 | "sha1", 81 | "sync_wrapper", 82 | "tokio", 83 | "tokio-tungstenite", 84 | "tower", 85 | "tower-layer", 86 | "tower-service", 87 | ] 88 | 89 | [[package]] 90 | name = "axum-core" 91 | version = "0.3.4" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" 94 | dependencies = [ 95 | "async-trait", 96 | "bytes", 97 | "futures-util", 98 | "http", 99 | "http-body", 100 | "mime", 101 | "rustversion", 102 | "tower-layer", 103 | "tower-service", 104 | ] 105 | 106 | [[package]] 107 | name = "axum-macros" 108 | version = "0.3.8" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "cdca6a10ecad987bda04e95606ef85a5417dcaac1a78455242d72e031e2b6b62" 111 | dependencies = [ 112 | "heck", 113 | "proc-macro2", 114 | "quote", 115 | "syn", 116 | ] 117 | 118 | [[package]] 119 | name = "axum-server" 120 | version = "0.5.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "447f28c85900215cc1bea282f32d4a2f22d55c5a300afdfbc661c8d6a632e063" 123 | dependencies = [ 124 | "arc-swap", 125 | "bytes", 126 | "futures-util", 127 | "http", 128 | "http-body", 129 | "hyper", 130 | "pin-project-lite", 131 | "rustls", 132 | "rustls-pemfile", 133 | "tokio", 134 | "tokio-rustls", 135 | "tower-service", 136 | ] 137 | 138 | [[package]] 139 | name = "backtrace" 140 | version = "0.3.69" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 143 | dependencies = [ 144 | "addr2line", 145 | "cc", 146 | "cfg-if", 147 | "libc", 148 | "miniz_oxide", 149 | "object", 150 | "rustc-demangle", 151 | ] 152 | 153 | [[package]] 154 | name = "base64" 155 | version = "0.21.5" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" 158 | 159 | [[package]] 160 | name = "bitflags" 161 | version = "1.3.2" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 164 | 165 | [[package]] 166 | name = "bitflags" 167 | version = "2.4.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 170 | 171 | [[package]] 172 | name = "block-buffer" 173 | version = "0.10.4" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 176 | dependencies = [ 177 | "generic-array", 178 | ] 179 | 180 | [[package]] 181 | name = "byteorder" 182 | version = "1.5.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 185 | 186 | [[package]] 187 | name = "bytes" 188 | version = "1.5.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 191 | 192 | [[package]] 193 | name = "cc" 194 | version = "1.0.83" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 197 | dependencies = [ 198 | "libc", 199 | ] 200 | 201 | [[package]] 202 | name = "cfg-if" 203 | version = "1.0.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 206 | 207 | [[package]] 208 | name = "cpufeatures" 209 | version = "0.2.11" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" 212 | dependencies = [ 213 | "libc", 214 | ] 215 | 216 | [[package]] 217 | name = "crossbeam-channel" 218 | version = "0.5.8" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" 221 | dependencies = [ 222 | "cfg-if", 223 | "crossbeam-utils", 224 | ] 225 | 226 | [[package]] 227 | name = "crossbeam-utils" 228 | version = "0.8.16" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 231 | dependencies = [ 232 | "cfg-if", 233 | ] 234 | 235 | [[package]] 236 | name = "crypto-common" 237 | version = "0.1.6" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 240 | dependencies = [ 241 | "generic-array", 242 | "typenum", 243 | ] 244 | 245 | [[package]] 246 | name = "data-encoding" 247 | version = "2.5.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" 250 | 251 | [[package]] 252 | name = "deranged" 253 | version = "0.3.9" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" 256 | dependencies = [ 257 | "powerfmt", 258 | "serde", 259 | ] 260 | 261 | [[package]] 262 | name = "digest" 263 | version = "0.10.7" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 266 | dependencies = [ 267 | "block-buffer", 268 | "crypto-common", 269 | ] 270 | 271 | [[package]] 272 | name = "either" 273 | version = "1.9.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 276 | 277 | [[package]] 278 | name = "encoding_rs" 279 | version = "0.8.33" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 282 | dependencies = [ 283 | "cfg-if", 284 | ] 285 | 286 | [[package]] 287 | name = "equivalent" 288 | version = "1.0.1" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 291 | 292 | [[package]] 293 | name = "errno" 294 | version = "0.3.8" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 297 | dependencies = [ 298 | "libc", 299 | "windows-sys 0.52.0", 300 | ] 301 | 302 | [[package]] 303 | name = "fnv" 304 | version = "1.0.7" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 307 | 308 | [[package]] 309 | name = "form_urlencoded" 310 | version = "1.2.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 313 | dependencies = [ 314 | "percent-encoding", 315 | ] 316 | 317 | [[package]] 318 | name = "futures-channel" 319 | version = "0.3.29" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" 322 | dependencies = [ 323 | "futures-core", 324 | ] 325 | 326 | [[package]] 327 | name = "futures-core" 328 | version = "0.3.29" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" 331 | 332 | [[package]] 333 | name = "futures-sink" 334 | version = "0.3.29" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" 337 | 338 | [[package]] 339 | name = "futures-task" 340 | version = "0.3.29" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" 343 | 344 | [[package]] 345 | name = "futures-util" 346 | version = "0.3.29" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" 349 | dependencies = [ 350 | "futures-core", 351 | "futures-sink", 352 | "futures-task", 353 | "pin-project-lite", 354 | "pin-utils", 355 | "slab", 356 | ] 357 | 358 | [[package]] 359 | name = "generic-array" 360 | version = "0.14.7" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 363 | dependencies = [ 364 | "typenum", 365 | "version_check", 366 | ] 367 | 368 | [[package]] 369 | name = "getrandom" 370 | version = "0.2.11" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 373 | dependencies = [ 374 | "cfg-if", 375 | "libc", 376 | "wasi", 377 | ] 378 | 379 | [[package]] 380 | name = "gimli" 381 | version = "0.28.1" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 384 | 385 | [[package]] 386 | name = "h2" 387 | version = "0.3.22" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" 390 | dependencies = [ 391 | "bytes", 392 | "fnv", 393 | "futures-core", 394 | "futures-sink", 395 | "futures-util", 396 | "http", 397 | "indexmap", 398 | "slab", 399 | "tokio", 400 | "tokio-util", 401 | "tracing", 402 | ] 403 | 404 | [[package]] 405 | name = "hashbrown" 406 | version = "0.14.3" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 409 | 410 | [[package]] 411 | name = "heck" 412 | version = "0.4.1" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 415 | 416 | [[package]] 417 | name = "hermit-abi" 418 | version = "0.3.3" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 421 | 422 | [[package]] 423 | name = "home" 424 | version = "0.5.5" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" 427 | dependencies = [ 428 | "windows-sys 0.48.0", 429 | ] 430 | 431 | [[package]] 432 | name = "http" 433 | version = "0.2.11" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" 436 | dependencies = [ 437 | "bytes", 438 | "fnv", 439 | "itoa", 440 | ] 441 | 442 | [[package]] 443 | name = "http-body" 444 | version = "0.4.5" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 447 | dependencies = [ 448 | "bytes", 449 | "http", 450 | "pin-project-lite", 451 | ] 452 | 453 | [[package]] 454 | name = "http-range-header" 455 | version = "0.3.1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" 458 | 459 | [[package]] 460 | name = "httparse" 461 | version = "1.8.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 464 | 465 | [[package]] 466 | name = "httpdate" 467 | version = "1.0.3" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 470 | 471 | [[package]] 472 | name = "hyper" 473 | version = "0.14.27" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" 476 | dependencies = [ 477 | "bytes", 478 | "futures-channel", 479 | "futures-core", 480 | "futures-util", 481 | "h2", 482 | "http", 483 | "http-body", 484 | "httparse", 485 | "httpdate", 486 | "itoa", 487 | "pin-project-lite", 488 | "socket2 0.4.10", 489 | "tokio", 490 | "tower-service", 491 | "tracing", 492 | "want", 493 | ] 494 | 495 | [[package]] 496 | name = "idna" 497 | version = "0.5.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 500 | dependencies = [ 501 | "unicode-bidi", 502 | "unicode-normalization", 503 | ] 504 | 505 | [[package]] 506 | name = "indexmap" 507 | version = "2.1.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 510 | dependencies = [ 511 | "equivalent", 512 | "hashbrown", 513 | ] 514 | 515 | [[package]] 516 | name = "itoa" 517 | version = "1.0.9" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 520 | 521 | [[package]] 522 | name = "lazy_static" 523 | version = "1.4.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 526 | 527 | [[package]] 528 | name = "libc" 529 | version = "0.2.150" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 532 | 533 | [[package]] 534 | name = "linux-raw-sys" 535 | version = "0.4.11" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" 538 | 539 | [[package]] 540 | name = "lock_api" 541 | version = "0.4.11" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 544 | dependencies = [ 545 | "autocfg", 546 | "scopeguard", 547 | ] 548 | 549 | [[package]] 550 | name = "log" 551 | version = "0.4.20" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 554 | 555 | [[package]] 556 | name = "matchers" 557 | version = "0.1.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 560 | dependencies = [ 561 | "regex-automata 0.1.10", 562 | ] 563 | 564 | [[package]] 565 | name = "matchit" 566 | version = "0.7.3" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 569 | 570 | [[package]] 571 | name = "memchr" 572 | version = "2.6.4" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 575 | 576 | [[package]] 577 | name = "mime" 578 | version = "0.3.17" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 581 | 582 | [[package]] 583 | name = "mime_guess" 584 | version = "2.0.4" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 587 | dependencies = [ 588 | "mime", 589 | "unicase", 590 | ] 591 | 592 | [[package]] 593 | name = "miniz_oxide" 594 | version = "0.7.1" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 597 | dependencies = [ 598 | "adler", 599 | ] 600 | 601 | [[package]] 602 | name = "mio" 603 | version = "0.8.9" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" 606 | dependencies = [ 607 | "libc", 608 | "wasi", 609 | "windows-sys 0.48.0", 610 | ] 611 | 612 | [[package]] 613 | name = "multer" 614 | version = "2.1.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" 617 | dependencies = [ 618 | "bytes", 619 | "encoding_rs", 620 | "futures-util", 621 | "http", 622 | "httparse", 623 | "log", 624 | "memchr", 625 | "mime", 626 | "spin", 627 | "version_check", 628 | ] 629 | 630 | [[package]] 631 | name = "nu-ansi-term" 632 | version = "0.46.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 635 | dependencies = [ 636 | "overload", 637 | "winapi", 638 | ] 639 | 640 | [[package]] 641 | name = "num_cpus" 642 | version = "1.16.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 645 | dependencies = [ 646 | "hermit-abi", 647 | "libc", 648 | ] 649 | 650 | [[package]] 651 | name = "object" 652 | version = "0.32.1" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 655 | dependencies = [ 656 | "memchr", 657 | ] 658 | 659 | [[package]] 660 | name = "once_cell" 661 | version = "1.18.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 664 | 665 | [[package]] 666 | name = "overload" 667 | version = "0.1.1" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 670 | 671 | [[package]] 672 | name = "parking_lot" 673 | version = "0.12.1" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 676 | dependencies = [ 677 | "lock_api", 678 | "parking_lot_core", 679 | ] 680 | 681 | [[package]] 682 | name = "parking_lot_core" 683 | version = "0.9.9" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 686 | dependencies = [ 687 | "cfg-if", 688 | "libc", 689 | "redox_syscall", 690 | "smallvec", 691 | "windows-targets 0.48.5", 692 | ] 693 | 694 | [[package]] 695 | name = "percent-encoding" 696 | version = "2.3.1" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 699 | 700 | [[package]] 701 | name = "pin-project" 702 | version = "1.1.3" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" 705 | dependencies = [ 706 | "pin-project-internal", 707 | ] 708 | 709 | [[package]] 710 | name = "pin-project-internal" 711 | version = "1.1.3" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" 714 | dependencies = [ 715 | "proc-macro2", 716 | "quote", 717 | "syn", 718 | ] 719 | 720 | [[package]] 721 | name = "pin-project-lite" 722 | version = "0.2.13" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 725 | 726 | [[package]] 727 | name = "pin-utils" 728 | version = "0.1.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 731 | 732 | [[package]] 733 | name = "powerfmt" 734 | version = "0.2.0" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 737 | 738 | [[package]] 739 | name = "ppv-lite86" 740 | version = "0.2.17" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 743 | 744 | [[package]] 745 | name = "proc-macro2" 746 | version = "1.0.70" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 749 | dependencies = [ 750 | "unicode-ident", 751 | ] 752 | 753 | [[package]] 754 | name = "quote" 755 | version = "1.0.33" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 758 | dependencies = [ 759 | "proc-macro2", 760 | ] 761 | 762 | [[package]] 763 | name = "rand" 764 | version = "0.8.5" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 767 | dependencies = [ 768 | "libc", 769 | "rand_chacha", 770 | "rand_core", 771 | ] 772 | 773 | [[package]] 774 | name = "rand_chacha" 775 | version = "0.3.1" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 778 | dependencies = [ 779 | "ppv-lite86", 780 | "rand_core", 781 | ] 782 | 783 | [[package]] 784 | name = "rand_core" 785 | version = "0.6.4" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 788 | dependencies = [ 789 | "getrandom", 790 | ] 791 | 792 | [[package]] 793 | name = "redox_syscall" 794 | version = "0.4.1" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 797 | dependencies = [ 798 | "bitflags 1.3.2", 799 | ] 800 | 801 | [[package]] 802 | name = "regex" 803 | version = "1.10.2" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 806 | dependencies = [ 807 | "aho-corasick", 808 | "memchr", 809 | "regex-automata 0.4.3", 810 | "regex-syntax 0.8.2", 811 | ] 812 | 813 | [[package]] 814 | name = "regex-automata" 815 | version = "0.1.10" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 818 | dependencies = [ 819 | "regex-syntax 0.6.29", 820 | ] 821 | 822 | [[package]] 823 | name = "regex-automata" 824 | version = "0.4.3" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 827 | dependencies = [ 828 | "aho-corasick", 829 | "memchr", 830 | "regex-syntax 0.8.2", 831 | ] 832 | 833 | [[package]] 834 | name = "regex-syntax" 835 | version = "0.6.29" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 838 | 839 | [[package]] 840 | name = "regex-syntax" 841 | version = "0.8.2" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 844 | 845 | [[package]] 846 | name = "ring" 847 | version = "0.17.6" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" 850 | dependencies = [ 851 | "cc", 852 | "getrandom", 853 | "libc", 854 | "spin", 855 | "untrusted", 856 | "windows-sys 0.48.0", 857 | ] 858 | 859 | [[package]] 860 | name = "rustc-demangle" 861 | version = "0.1.23" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 864 | 865 | [[package]] 866 | name = "rustix" 867 | version = "0.38.25" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" 870 | dependencies = [ 871 | "bitflags 2.4.1", 872 | "errno", 873 | "libc", 874 | "linux-raw-sys", 875 | "windows-sys 0.48.0", 876 | ] 877 | 878 | [[package]] 879 | name = "rustls" 880 | version = "0.21.9" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" 883 | dependencies = [ 884 | "log", 885 | "ring", 886 | "rustls-webpki", 887 | "sct", 888 | ] 889 | 890 | [[package]] 891 | name = "rustls-pemfile" 892 | version = "1.0.4" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 895 | dependencies = [ 896 | "base64", 897 | ] 898 | 899 | [[package]] 900 | name = "rustls-webpki" 901 | version = "0.101.7" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 904 | dependencies = [ 905 | "ring", 906 | "untrusted", 907 | ] 908 | 909 | [[package]] 910 | name = "rustversion" 911 | version = "1.0.14" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 914 | 915 | [[package]] 916 | name = "ryu" 917 | version = "1.0.15" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 920 | 921 | [[package]] 922 | name = "scopeguard" 923 | version = "1.2.0" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 926 | 927 | [[package]] 928 | name = "sct" 929 | version = "0.7.1" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" 932 | dependencies = [ 933 | "ring", 934 | "untrusted", 935 | ] 936 | 937 | [[package]] 938 | name = "serde" 939 | version = "1.0.193" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 942 | dependencies = [ 943 | "serde_derive", 944 | ] 945 | 946 | [[package]] 947 | name = "serde_derive" 948 | version = "1.0.193" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 951 | dependencies = [ 952 | "proc-macro2", 953 | "quote", 954 | "syn", 955 | ] 956 | 957 | [[package]] 958 | name = "serde_json" 959 | version = "1.0.108" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 962 | dependencies = [ 963 | "itoa", 964 | "ryu", 965 | "serde", 966 | ] 967 | 968 | [[package]] 969 | name = "serde_path_to_error" 970 | version = "0.1.14" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" 973 | dependencies = [ 974 | "itoa", 975 | "serde", 976 | ] 977 | 978 | [[package]] 979 | name = "serde_spanned" 980 | version = "0.6.4" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" 983 | dependencies = [ 984 | "serde", 985 | ] 986 | 987 | [[package]] 988 | name = "serde_urlencoded" 989 | version = "0.7.1" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 992 | dependencies = [ 993 | "form_urlencoded", 994 | "itoa", 995 | "ryu", 996 | "serde", 997 | ] 998 | 999 | [[package]] 1000 | name = "sha1" 1001 | version = "0.10.6" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1004 | dependencies = [ 1005 | "cfg-if", 1006 | "cpufeatures", 1007 | "digest", 1008 | ] 1009 | 1010 | [[package]] 1011 | name = "sharded-slab" 1012 | version = "0.1.7" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1015 | dependencies = [ 1016 | "lazy_static", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "signal-hook-registry" 1021 | version = "1.4.1" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1024 | dependencies = [ 1025 | "libc", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "slab" 1030 | version = "0.4.9" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1033 | dependencies = [ 1034 | "autocfg", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "smallvec" 1039 | version = "1.11.2" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 1042 | 1043 | [[package]] 1044 | name = "socket2" 1045 | version = "0.4.10" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" 1048 | dependencies = [ 1049 | "libc", 1050 | "winapi", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "socket2" 1055 | version = "0.5.5" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 1058 | dependencies = [ 1059 | "libc", 1060 | "windows-sys 0.48.0", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "spin" 1065 | version = "0.9.8" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1068 | 1069 | [[package]] 1070 | name = "syn" 1071 | version = "2.0.39" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 1074 | dependencies = [ 1075 | "proc-macro2", 1076 | "quote", 1077 | "unicode-ident", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "sync_wrapper" 1082 | version = "0.1.2" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1085 | 1086 | [[package]] 1087 | name = "thiserror" 1088 | version = "1.0.50" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 1091 | dependencies = [ 1092 | "thiserror-impl", 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "thiserror-impl" 1097 | version = "1.0.50" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 1100 | dependencies = [ 1101 | "proc-macro2", 1102 | "quote", 1103 | "syn", 1104 | ] 1105 | 1106 | [[package]] 1107 | name = "thread_local" 1108 | version = "1.1.7" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 1111 | dependencies = [ 1112 | "cfg-if", 1113 | "once_cell", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "time" 1118 | version = "0.3.30" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" 1121 | dependencies = [ 1122 | "deranged", 1123 | "itoa", 1124 | "powerfmt", 1125 | "serde", 1126 | "time-core", 1127 | "time-macros", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "time-core" 1132 | version = "0.1.2" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1135 | 1136 | [[package]] 1137 | name = "time-macros" 1138 | version = "0.2.15" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" 1141 | dependencies = [ 1142 | "time-core", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "tinyvec" 1147 | version = "1.6.0" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1150 | dependencies = [ 1151 | "tinyvec_macros", 1152 | ] 1153 | 1154 | [[package]] 1155 | name = "tinyvec_macros" 1156 | version = "0.1.1" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1159 | 1160 | [[package]] 1161 | name = "tokio" 1162 | version = "1.34.0" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" 1165 | dependencies = [ 1166 | "backtrace", 1167 | "bytes", 1168 | "libc", 1169 | "mio", 1170 | "num_cpus", 1171 | "parking_lot", 1172 | "pin-project-lite", 1173 | "signal-hook-registry", 1174 | "socket2 0.5.5", 1175 | "tokio-macros", 1176 | "windows-sys 0.48.0", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "tokio-macros" 1181 | version = "2.2.0" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 1184 | dependencies = [ 1185 | "proc-macro2", 1186 | "quote", 1187 | "syn", 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "tokio-rustls" 1192 | version = "0.24.1" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 1195 | dependencies = [ 1196 | "rustls", 1197 | "tokio", 1198 | ] 1199 | 1200 | [[package]] 1201 | name = "tokio-tungstenite" 1202 | version = "0.20.1" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" 1205 | dependencies = [ 1206 | "futures-util", 1207 | "log", 1208 | "tokio", 1209 | "tungstenite", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "tokio-util" 1214 | version = "0.7.10" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 1217 | dependencies = [ 1218 | "bytes", 1219 | "futures-core", 1220 | "futures-sink", 1221 | "pin-project-lite", 1222 | "tokio", 1223 | "tracing", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "toml" 1228 | version = "0.8.8" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" 1231 | dependencies = [ 1232 | "serde", 1233 | "serde_spanned", 1234 | "toml_datetime", 1235 | "toml_edit", 1236 | ] 1237 | 1238 | [[package]] 1239 | name = "toml_datetime" 1240 | version = "0.6.5" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 1243 | dependencies = [ 1244 | "serde", 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "toml_edit" 1249 | version = "0.21.0" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" 1252 | dependencies = [ 1253 | "indexmap", 1254 | "serde", 1255 | "serde_spanned", 1256 | "toml_datetime", 1257 | "winnow", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "tower" 1262 | version = "0.4.13" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1265 | dependencies = [ 1266 | "futures-core", 1267 | "futures-util", 1268 | "pin-project", 1269 | "pin-project-lite", 1270 | "tokio", 1271 | "tower-layer", 1272 | "tower-service", 1273 | "tracing", 1274 | ] 1275 | 1276 | [[package]] 1277 | name = "tower-http" 1278 | version = "0.4.4" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" 1281 | dependencies = [ 1282 | "bitflags 2.4.1", 1283 | "bytes", 1284 | "futures-core", 1285 | "futures-util", 1286 | "http", 1287 | "http-body", 1288 | "http-range-header", 1289 | "httpdate", 1290 | "mime", 1291 | "mime_guess", 1292 | "percent-encoding", 1293 | "pin-project-lite", 1294 | "tokio", 1295 | "tokio-util", 1296 | "tower-layer", 1297 | "tower-service", 1298 | "tracing", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "tower-layer" 1303 | version = "0.3.2" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 1306 | 1307 | [[package]] 1308 | name = "tower-service" 1309 | version = "0.3.2" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1312 | 1313 | [[package]] 1314 | name = "tracing" 1315 | version = "0.1.40" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1318 | dependencies = [ 1319 | "log", 1320 | "pin-project-lite", 1321 | "tracing-attributes", 1322 | "tracing-core", 1323 | ] 1324 | 1325 | [[package]] 1326 | name = "tracing-appender" 1327 | version = "0.2.3" 1328 | source = "registry+https://github.com/rust-lang/crates.io-index" 1329 | checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" 1330 | dependencies = [ 1331 | "crossbeam-channel", 1332 | "thiserror", 1333 | "time", 1334 | "tracing-subscriber", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "tracing-attributes" 1339 | version = "0.1.27" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1342 | dependencies = [ 1343 | "proc-macro2", 1344 | "quote", 1345 | "syn", 1346 | ] 1347 | 1348 | [[package]] 1349 | name = "tracing-core" 1350 | version = "0.1.32" 1351 | source = "registry+https://github.com/rust-lang/crates.io-index" 1352 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1353 | dependencies = [ 1354 | "once_cell", 1355 | "valuable", 1356 | ] 1357 | 1358 | [[package]] 1359 | name = "tracing-log" 1360 | version = "0.2.0" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1363 | dependencies = [ 1364 | "log", 1365 | "once_cell", 1366 | "tracing-core", 1367 | ] 1368 | 1369 | [[package]] 1370 | name = "tracing-subscriber" 1371 | version = "0.3.18" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 1374 | dependencies = [ 1375 | "matchers", 1376 | "nu-ansi-term", 1377 | "once_cell", 1378 | "regex", 1379 | "sharded-slab", 1380 | "smallvec", 1381 | "thread_local", 1382 | "tracing", 1383 | "tracing-core", 1384 | "tracing-log", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "try-lock" 1389 | version = "0.2.4" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1392 | 1393 | [[package]] 1394 | name = "tungstenite" 1395 | version = "0.20.1" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" 1398 | dependencies = [ 1399 | "byteorder", 1400 | "bytes", 1401 | "data-encoding", 1402 | "http", 1403 | "httparse", 1404 | "log", 1405 | "rand", 1406 | "sha1", 1407 | "thiserror", 1408 | "url", 1409 | "utf-8", 1410 | ] 1411 | 1412 | [[package]] 1413 | name = "typenum" 1414 | version = "1.17.0" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1417 | 1418 | [[package]] 1419 | name = "unicase" 1420 | version = "2.7.0" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 1423 | dependencies = [ 1424 | "version_check", 1425 | ] 1426 | 1427 | [[package]] 1428 | name = "unicode-bidi" 1429 | version = "0.3.13" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1432 | 1433 | [[package]] 1434 | name = "unicode-ident" 1435 | version = "1.0.12" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1438 | 1439 | [[package]] 1440 | name = "unicode-normalization" 1441 | version = "0.1.22" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1444 | dependencies = [ 1445 | "tinyvec", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "untrusted" 1450 | version = "0.9.0" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1453 | 1454 | [[package]] 1455 | name = "url" 1456 | version = "2.5.0" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1459 | dependencies = [ 1460 | "form_urlencoded", 1461 | "idna", 1462 | "percent-encoding", 1463 | ] 1464 | 1465 | [[package]] 1466 | name = "utf-8" 1467 | version = "0.7.6" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1470 | 1471 | [[package]] 1472 | name = "valuable" 1473 | version = "0.1.0" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1476 | 1477 | [[package]] 1478 | name = "version_check" 1479 | version = "0.9.4" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1482 | 1483 | [[package]] 1484 | name = "voice" 1485 | version = "0.3.0" 1486 | dependencies = [ 1487 | "axum", 1488 | "axum-server", 1489 | "rand", 1490 | "serde", 1491 | "serde_json", 1492 | "time", 1493 | "tokio", 1494 | "tokio-util", 1495 | "toml", 1496 | "tower-http", 1497 | "tracing", 1498 | "tracing-appender", 1499 | "tracing-subscriber", 1500 | "which", 1501 | ] 1502 | 1503 | [[package]] 1504 | name = "want" 1505 | version = "0.3.1" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1508 | dependencies = [ 1509 | "try-lock", 1510 | ] 1511 | 1512 | [[package]] 1513 | name = "wasi" 1514 | version = "0.11.0+wasi-snapshot-preview1" 1515 | source = "registry+https://github.com/rust-lang/crates.io-index" 1516 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1517 | 1518 | [[package]] 1519 | name = "which" 1520 | version = "4.4.2" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 1523 | dependencies = [ 1524 | "either", 1525 | "home", 1526 | "once_cell", 1527 | "rustix", 1528 | ] 1529 | 1530 | [[package]] 1531 | name = "winapi" 1532 | version = "0.3.9" 1533 | source = "registry+https://github.com/rust-lang/crates.io-index" 1534 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1535 | dependencies = [ 1536 | "winapi-i686-pc-windows-gnu", 1537 | "winapi-x86_64-pc-windows-gnu", 1538 | ] 1539 | 1540 | [[package]] 1541 | name = "winapi-i686-pc-windows-gnu" 1542 | version = "0.4.0" 1543 | source = "registry+https://github.com/rust-lang/crates.io-index" 1544 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1545 | 1546 | [[package]] 1547 | name = "winapi-x86_64-pc-windows-gnu" 1548 | version = "0.4.0" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1551 | 1552 | [[package]] 1553 | name = "windows-sys" 1554 | version = "0.48.0" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1557 | dependencies = [ 1558 | "windows-targets 0.48.5", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "windows-sys" 1563 | version = "0.52.0" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1566 | dependencies = [ 1567 | "windows-targets 0.52.0", 1568 | ] 1569 | 1570 | [[package]] 1571 | name = "windows-targets" 1572 | version = "0.48.5" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1575 | dependencies = [ 1576 | "windows_aarch64_gnullvm 0.48.5", 1577 | "windows_aarch64_msvc 0.48.5", 1578 | "windows_i686_gnu 0.48.5", 1579 | "windows_i686_msvc 0.48.5", 1580 | "windows_x86_64_gnu 0.48.5", 1581 | "windows_x86_64_gnullvm 0.48.5", 1582 | "windows_x86_64_msvc 0.48.5", 1583 | ] 1584 | 1585 | [[package]] 1586 | name = "windows-targets" 1587 | version = "0.52.0" 1588 | source = "registry+https://github.com/rust-lang/crates.io-index" 1589 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 1590 | dependencies = [ 1591 | "windows_aarch64_gnullvm 0.52.0", 1592 | "windows_aarch64_msvc 0.52.0", 1593 | "windows_i686_gnu 0.52.0", 1594 | "windows_i686_msvc 0.52.0", 1595 | "windows_x86_64_gnu 0.52.0", 1596 | "windows_x86_64_gnullvm 0.52.0", 1597 | "windows_x86_64_msvc 0.52.0", 1598 | ] 1599 | 1600 | [[package]] 1601 | name = "windows_aarch64_gnullvm" 1602 | version = "0.48.5" 1603 | source = "registry+https://github.com/rust-lang/crates.io-index" 1604 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1605 | 1606 | [[package]] 1607 | name = "windows_aarch64_gnullvm" 1608 | version = "0.52.0" 1609 | source = "registry+https://github.com/rust-lang/crates.io-index" 1610 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 1611 | 1612 | [[package]] 1613 | name = "windows_aarch64_msvc" 1614 | version = "0.48.5" 1615 | source = "registry+https://github.com/rust-lang/crates.io-index" 1616 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1617 | 1618 | [[package]] 1619 | name = "windows_aarch64_msvc" 1620 | version = "0.52.0" 1621 | source = "registry+https://github.com/rust-lang/crates.io-index" 1622 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 1623 | 1624 | [[package]] 1625 | name = "windows_i686_gnu" 1626 | version = "0.48.5" 1627 | source = "registry+https://github.com/rust-lang/crates.io-index" 1628 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1629 | 1630 | [[package]] 1631 | name = "windows_i686_gnu" 1632 | version = "0.52.0" 1633 | source = "registry+https://github.com/rust-lang/crates.io-index" 1634 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 1635 | 1636 | [[package]] 1637 | name = "windows_i686_msvc" 1638 | version = "0.48.5" 1639 | source = "registry+https://github.com/rust-lang/crates.io-index" 1640 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1641 | 1642 | [[package]] 1643 | name = "windows_i686_msvc" 1644 | version = "0.52.0" 1645 | source = "registry+https://github.com/rust-lang/crates.io-index" 1646 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 1647 | 1648 | [[package]] 1649 | name = "windows_x86_64_gnu" 1650 | version = "0.48.5" 1651 | source = "registry+https://github.com/rust-lang/crates.io-index" 1652 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1653 | 1654 | [[package]] 1655 | name = "windows_x86_64_gnu" 1656 | version = "0.52.0" 1657 | source = "registry+https://github.com/rust-lang/crates.io-index" 1658 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 1659 | 1660 | [[package]] 1661 | name = "windows_x86_64_gnullvm" 1662 | version = "0.48.5" 1663 | source = "registry+https://github.com/rust-lang/crates.io-index" 1664 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1665 | 1666 | [[package]] 1667 | name = "windows_x86_64_gnullvm" 1668 | version = "0.52.0" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 1671 | 1672 | [[package]] 1673 | name = "windows_x86_64_msvc" 1674 | version = "0.48.5" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1677 | 1678 | [[package]] 1679 | name = "windows_x86_64_msvc" 1680 | version = "0.52.0" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 1683 | 1684 | [[package]] 1685 | name = "winnow" 1686 | version = "0.5.19" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" 1689 | dependencies = [ 1690 | "memchr", 1691 | ] 1692 | --------------------------------------------------------------------------------