├── .cargo └── config.toml ├── radio-webapp ├── src │ ├── vite-env.d.ts │ ├── assets │ │ ├── noimage.png │ │ ├── touhoudb.jpg │ │ └── youtube.png │ ├── index.tsx │ ├── index.css │ ├── App.tsx │ ├── Player.tsx │ ├── App.css │ └── Mediainfo.tsx ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── package.json ├── tsconfig.json ├── README.md └── pnpm-lock.yaml ├── .gitignore ├── Cargo.toml ├── src ├── files.rs ├── audio.rs ├── cmd.rs ├── config.rs ├── main.rs └── player.rs ├── README.md └── Cargo.lock /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | BASE_URL = "/" 3 | -------------------------------------------------------------------------------- /radio-webapp/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.mp3 3 | *.flac 4 | info.json 5 | mediainfo.json 6 | *.exe 7 | *.js 8 | audacity 9 | -------------------------------------------------------------------------------- /radio-webapp/src/assets/noimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiftoo/radio/master/radio-webapp/src/assets/noimage.png -------------------------------------------------------------------------------- /radio-webapp/src/assets/touhoudb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiftoo/radio/master/radio-webapp/src/assets/touhoudb.jpg -------------------------------------------------------------------------------- /radio-webapp/src/assets/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiftoo/radio/master/radio-webapp/src/assets/youtube.png -------------------------------------------------------------------------------- /radio-webapp/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from "vite"; 2 | import solid from "vite-plugin-solid"; 3 | 4 | export default defineConfig({ 5 | plugins: [solid()], 6 | base: process.env.BASE_URL, 7 | }); 8 | -------------------------------------------------------------------------------- /radio-webapp/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import {render} from "solid-js/web"; 3 | 4 | import "./index.css"; 5 | import App from "./App"; 6 | 7 | const root = document.getElementById("root"); 8 | 9 | render(() => , root!); 10 | -------------------------------------------------------------------------------- /radio-webapp/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /radio-webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /radio-webapp/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Minecraft, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | 4 | color-scheme: light dark; 5 | background-color: #272726; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | } 11 | 12 | #root { 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: center; 16 | } 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | -------------------------------------------------------------------------------- /radio-webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Now playing: ... 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /radio-webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radio-webapp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "solid-js": "^1.8.15" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^20.11.24", 16 | "typescript": "^5.2.2", 17 | "vite": "^5.1.4", 18 | "vite-plugin-solid": "^2.10.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /radio-webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "solid-js", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"], 25 | "references": [{"path": "./tsconfig.node.json"}] 26 | } 27 | -------------------------------------------------------------------------------- /radio-webapp/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ```bash 4 | $ npm install # or pnpm install or yarn install 5 | ``` 6 | 7 | ### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) 8 | 9 | ## Available Scripts 10 | 11 | In the project directory, you can run: 12 | 13 | ### `npm run dev` 14 | 15 | Runs the app in the development mode.
16 | Open [http://localhost:5173](http://localhost:5173) to view it in the browser. 17 | 18 | ### `npm run build` 19 | 20 | Builds the app for production to the `dist` folder.
21 | It correctly bundles Solid in production mode and optimizes the build for the best performance. 22 | 23 | The build is minified and the filenames include the hashes.
24 | Your app is ready to be deployed! 25 | 26 | ## Deployment 27 | 28 | Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html) 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "radio" 3 | version = "0.13.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | async-trait = "0.1.77" 8 | axum = { version = "0.7.4", features = [ 9 | "macros", 10 | "ws", 11 | "http1", 12 | "query", 13 | ], default-features = false } 14 | clap = { version = "4.5.1", features = ["derive"] } 15 | futures-core = "0.3.30" 16 | is-root = "0.1.3" 17 | jwalk = "0.8.1" 18 | mime_guess = "2.0.4" 19 | rand = "0.8.5" 20 | rayon = "1.9.0" 21 | rust-embed = { version = "8.3.0", features = ["axum"], optional = true } 22 | serde = { version = "1.0.197", features = ["derive"] } 23 | serde_json = "1.0.114" 24 | tokio = { version = "1.36.0", features = ["rt-multi-thread", "process"] } 25 | tokio-stream = { version = "0.1.14", default-features = false, features = [ 26 | "sync", 27 | ] } 28 | toml = "0.8.10" 29 | tower-http = { version = "0.5.2", features = ["cors"] } 30 | 31 | [target.'cfg(windows)'.dependencies] 32 | windirs = "1.0.1" 33 | 34 | [features] 35 | default = ["webapp"] 36 | webapp = ["dep:rust-embed"] 37 | h2 = ["axum/http2"] 38 | -------------------------------------------------------------------------------- /radio-webapp/src/App.tsx: -------------------------------------------------------------------------------- 1 | import {createSignal} from "solid-js"; 2 | import "./App.css"; 3 | import {Mediainfo} from "./Mediainfo"; 4 | import {Player} from "./Player"; 5 | 6 | const HOSTNAME = new URL(import.meta.env.BASE_URL, location.origin); 7 | // const HOSTNAME = new URL(import.meta.env.BASE_URL, "http://localhost:9005"); 8 | 9 | export function makeUrl(protocol: "http" | "ws", path: string) { 10 | let newProtocol: string = protocol; 11 | if (protocol === "ws") { 12 | if (location.protocol === "http:") { 13 | newProtocol = "ws:"; 14 | } else { 15 | newProtocol = "wss:"; 16 | } 17 | } else { 18 | newProtocol = location.protocol; 19 | } 20 | 21 | let newUrl = new URL(HOSTNAME); 22 | newUrl.protocol = newProtocol; 23 | let url = newUrl.toString(); 24 | if (path.startsWith("/")) { 25 | path = path.slice(1); 26 | } 27 | if (!url.endsWith("/")) { 28 | url += "/"; 29 | } 30 | // console.log("url + path", newUrl + path); 31 | return newUrl + path; 32 | } 33 | 34 | export default function App() { 35 | return ( 36 |
37 | 38 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /radio-webapp/src/Player.tsx: -------------------------------------------------------------------------------- 1 | import {JSX, createSignal} from "solid-js"; 2 | import {makeUrl} from "./App"; 3 | 4 | export function Player() { 5 | const [playing, setPlaying] = createSignal(false); 6 | const [volume, setVolume] = createSignal(33); 7 | 8 | let audioRef: undefined | HTMLAudioElement; 9 | 10 | const changeVolume: JSX.EventHandlerUnion = (ev) => { 11 | setVolume(+ev.currentTarget.value / 100); 12 | audioRef!.volume = +ev.currentTarget.value / 100; 13 | }; 14 | 15 | const togglePlay = () => { 16 | const audio = audioRef!; 17 | setPlaying((v) => { 18 | if (v) { 19 | audio.pause(); 20 | } else { 21 | if (isFinite(audio.duration)) { 22 | audio.currentTime = Math.max(audio.duration - 1, 0); 23 | } 24 | audio.play(); 25 | } 26 | return !v; 27 | }); 28 | }; 29 | 30 | return ( 31 |
32 |
33 | 36 | Volume: 37 | 38 | 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/files.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::{Path, PathBuf}, 3 | sync::Arc, 4 | }; 5 | 6 | use rayon::{prelude::*, ThreadPool}; 7 | 8 | use crate::config::{self, DirectoryConfig}; 9 | 10 | const SUPPORTED_FORMATS: [&str; 4] = ["mp3", "flac", "opus", "wav"]; 11 | 12 | pub fn collect(path: &[config::DirectoryConfig]) -> Vec { 13 | let pool = Arc::new(rayon::ThreadPoolBuilder::new().build().unwrap()); 14 | 15 | pool.install(|| path.iter().flat_map(|x| walk(x.clone(), pool.clone())).collect()) 16 | } 17 | 18 | fn walk(path: DirectoryConfig, pool: Arc) -> Vec { 19 | jwalk::WalkDir::new(path.root) 20 | .parallelism(jwalk::Parallelism::RayonExistingPool { pool, busy_timeout: None }) 21 | .into_iter() 22 | .par_bridge() 23 | .flatten() 24 | .filter(|x| match &path.mode { 25 | config::DirectoryConfigMode::Exclude(dirs) => { 26 | if dirs.is_empty() { 27 | return true; 28 | } 29 | dirs.iter().any(|y| !x.path().starts_with(y)) 30 | } 31 | config::DirectoryConfigMode::Include(dirs) => { 32 | dirs.iter().any(|y| x.path().starts_with(y)) 33 | } 34 | }) 35 | .flat_map(|x| { 36 | let cond = x.file_type.is_file() 37 | && Path::new(&x.file_name) 38 | .extension() 39 | .and_then(|x| x.to_str()) 40 | .is_some_and(|x| SUPPORTED_FORMATS.contains(&x)); 41 | cond.then(|| x.path()) 42 | }) 43 | .collect::>() 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stream mp3 audio to the world. Wrote this for myself to be able to listen to my music collection from anywhere. 2 | 3 | Recursively walks a directory and serves all (mp3, flac, opus, wav) files from the directory tree. 4 | Requires `ffmpeg` and `ffprobe`, probably any version as long as it can read the formats above and has libmp3lame enabled. 5 | 6 | Here's the output of help as of now: 7 | ``` 8 | Usage: radio [OPTIONS] --root 9 | 10 | Options: 11 | --generate-config [] 12 | Overwrite existing or create a new config file. Optionally pass a path to the config file to be created (not directory). 13 | Doesn't work right yet. 14 | 15 | --use-config [] 16 | Use the config file instead of the command line. Generates a new config if none exists. 17 | All arguments except '--generate-config' are ignored if this is present. 18 | Optionally pass a path to the config file to be created/read (not directory). 19 | 20 | --host 21 | The host to bind to. 22 | 23 | [default: 127.0.0.1] 24 | 25 | --port 26 | [default: 9005] 27 | 28 | --enable-webui 29 | not implemented. 30 | 31 | --shuffle 32 | Choose next song randomly. 33 | 34 | --bitrate 35 | The bitrate to use for transcoding. Plain value for bps and suffixed with 'k' for kbps. 36 | 37 | [default: 128k] 38 | 39 | --enable-mediainfo 40 | Enable /mediainfo endpoint. It serves metadata for the current song in JSON format. 41 | 42 | --mediainfo-history 43 | The size of song history to keep track of. Must be greater than 0. 44 | 45 | [default: 16] 46 | 47 | --transcode-all[=] 48 | Transcode files that can be sent without transcoding. Set to true if you want to reduce bandwidth a little. 49 | 50 | [default: false] 51 | [possible values: true, false] 52 | 53 | --root 54 | The root directory to recursively search for music. 55 | Note: --use-config allows to specify multiple root directories. 56 | 57 | --include 58 | Include these directories or files. 59 | 60 | --exclude 61 | Exclude these directories or files. 62 | 63 | -h, --help 64 | Print help (see a summary with '-h') 65 | 66 | -V, --version 67 | Print version 68 | 69 | ``` 70 | --- 71 | Here's a lucky commit hash I pulled: 72 | 73 | -------------------------------------------------------------------------------- /radio-webapp/src/App.css: -------------------------------------------------------------------------------- 1 | #container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | width: 800px; 6 | padding: 4px; 7 | } 8 | 9 | #mediainfo { 10 | display: flex; 11 | flex-flow: row; 12 | justify-content: flex-start; 13 | align-items: center; 14 | gap: 1em; 15 | flex-shrink: 1; 16 | } 17 | 18 | #mediainfo > #image { 19 | padding: 4px; 20 | width: 400px; 21 | height: 400px; 22 | object-fit: contain; 23 | } 24 | 25 | #mediainfo > #image > img { 26 | width: 100%; 27 | height: 100%; 28 | object-fit: contain; 29 | border-radius: 5px; 30 | } 31 | 32 | #mediainfo > #info { 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: center; 36 | padding-left: 1em; 37 | padding-right: 1em; 38 | width: 350px; 39 | } 40 | 41 | #mediainfo > #info > ul { 42 | margin: 0; 43 | display: flex; 44 | flex-direction: column; 45 | justify-content: space-between; 46 | padding: 0; 47 | list-style: none; 48 | gap: 2px; 49 | } 50 | 51 | #player { 52 | height: 50px; 53 | padding: 4px; 54 | width: 100%; 55 | } 56 | 57 | #player > #controls { 58 | display: flex; 59 | align-items: center; 60 | gap: 4px; 61 | } 62 | 63 | #player > #controls > button { 64 | appearance: none; 65 | font-size: 16px; 66 | background-color: #cf8439; 67 | border: 1px solid #ab6b2a; 68 | border-radius: 8px; 69 | padding: 0.5em 1.33em; 70 | margin-right: 1em; 71 | cursor: pointer; 72 | } 73 | 74 | #player > #controls > button:active { 75 | transform: scale(0.95); 76 | } 77 | 78 | #player > #controls > input[type="range"] { 79 | -webkit-appearance: none; 80 | appearance: none; 81 | width: 33%; 82 | height: 10px; 83 | border-radius: 5px; 84 | background: #d3d3d3; 85 | outline: none; 86 | } 87 | 88 | #player > #controls > input[type="range"]::-moz-range-thumb { 89 | appearance: none; 90 | width: 20px; 91 | height: 20px; 92 | border-radius: 50%; 93 | background: #cf8439; 94 | cursor: pointer; 95 | border: 1px solid #ab6b2a; 96 | } 97 | 98 | #player > #controls > input[type="range"]::-webkit-slider-thumb { 99 | appearance: none; 100 | width: 20px; 101 | height: 20px; 102 | border-radius: 50%; 103 | background: #cf8439; 104 | cursor: pointer; 105 | border: 1px solid #ab6b2a; 106 | } 107 | 108 | @media (width <= 768px) { 109 | #container { 110 | width: 100%; 111 | height: 100vh; 112 | height: 100dvh; 113 | justify-content: space-between; 114 | } 115 | #mediainfo { 116 | flex-grow: 1; 117 | flex-direction: column; 118 | } 119 | #mediainfo > #info { 120 | flex-grow: 1; 121 | align-self: flex-start; 122 | } 123 | #mediainfo > #info > ul { 124 | margin-bottom: auto; 125 | } 126 | #mediainfo > #image { 127 | width: 100%; 128 | height: auto; 129 | flex-shrink: 1; 130 | } 131 | #mediainfo > #image > img { 132 | max-height: 60vh; 133 | } 134 | } 135 | 136 | #media-links { 137 | display: flex; 138 | flex-direction: row; 139 | padding-top: 4px; 140 | gap: 4px; 141 | } 142 | 143 | #media-links img { 144 | width: 32px; 145 | height: 32px; 146 | } 147 | -------------------------------------------------------------------------------- /src/audio.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::{Path, PathBuf}, 3 | time::Duration, 4 | }; 5 | 6 | use tokio::io::AsyncReadExt; 7 | 8 | use crate::cmd::{self}; 9 | 10 | #[derive(Debug)] 11 | pub enum Data { 12 | Audio(usize), 13 | Error(String), 14 | } 15 | 16 | #[async_trait::async_trait] 17 | pub trait AudioReader: Send { 18 | async fn read_data(&mut self, buf: &mut [u8]) -> Result; 19 | async fn read_metadata(&mut self) -> Result; 20 | } 21 | 22 | pub struct FFMpegAudioReader { 23 | file: PathBuf, 24 | metadata: Option, 25 | error_buf: Vec, 26 | handle: tokio::process::Child, 27 | stdout: tokio::process::ChildStdout, 28 | stderr: tokio::process::ChildStderr, 29 | } 30 | 31 | impl FFMpegAudioReader { 32 | pub fn start( 33 | input: impl AsRef, 34 | sweeper: Option>, 35 | bitrate: u32, 36 | copy_codec: bool, 37 | ) -> Self { 38 | let mut handle = cmd::spawn_ffmpeg( 39 | input.as_ref(), 40 | sweeper.as_ref().map(|x| x.as_ref()), 41 | bitrate, 42 | copy_codec, 43 | ); 44 | let stdout = handle.stdout.take().unwrap(); 45 | let stderr = handle.stderr.take().unwrap(); 46 | Self { 47 | file: input.as_ref().to_path_buf(), 48 | error_buf: Default::default(), 49 | handle, 50 | stdout, 51 | stderr, 52 | metadata: None, 53 | } 54 | } 55 | } 56 | 57 | #[async_trait::async_trait] 58 | impl AudioReader for FFMpegAudioReader { 59 | async fn read_data(&mut self, buf: &mut [u8]) -> Result { 60 | tokio::select! { 61 | biased; 62 | read_result = self.stdout.read(buf) => { 63 | match read_result { 64 | Ok(read) => Ok(Data::Audio(read)), 65 | Err(e) => Err(e), 66 | } 67 | }, 68 | Ok(x) = self.stderr.read_buf(&mut self.error_buf) => { 69 | if x > 0 { 70 | // i'm scared of locking up the whole thing on read_buf 71 | // the following is tested and doesn't work very well. 72 | // tokio::time::sleep(Duration::from_millis(200)).await; 73 | // tokio::select! { 74 | // biased; 75 | // _ = self.stderr.read_buf(&mut self.error_buf) => {}, 76 | // _ = std::future::ready(()) => {}, 77 | // } 78 | 79 | let (tx, mut rx) = tokio::sync::oneshot::channel(); 80 | tokio::spawn(async move { 81 | tokio::time::sleep(Duration::from_millis(200)).await; 82 | tx.send(()).unwrap(); 83 | }); 84 | loop { 85 | tokio::select! { 86 | _ = self.stderr.read_buf(&mut self.error_buf) => {}, 87 | _ = &mut rx => break, 88 | } 89 | } 90 | Ok(Data::Error(String::from_utf8_lossy(&self.error_buf).to_string())) 91 | } else { 92 | Ok(Data::Audio(0)) 93 | } 94 | }, 95 | } 96 | } 97 | 98 | async fn read_metadata(&mut self) -> Result { 99 | match self.metadata { 100 | Some(ref x) => Ok(x.clone()), 101 | None => { 102 | let metadata = cmd::mediainfo(&self.file).await?; 103 | self.metadata = Some(metadata.clone()); 104 | Ok(metadata) 105 | } 106 | } 107 | } 108 | } 109 | 110 | impl Drop for FFMpegAudioReader { 111 | fn drop(&mut self) { 112 | self.handle.start_kill().unwrap(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /radio-webapp/src/Mediainfo.tsx: -------------------------------------------------------------------------------- 1 | import {JSX, Show, createSignal} from "solid-js"; 2 | import {makeUrl} from "./App"; 3 | import youtubeIcon from "./assets/youtube.png"; 4 | import touhoudbIcon from "./assets/touhoudb.jpg"; 5 | import noImage from "./assets/noimage.png"; 6 | 7 | async function fetchMediainfo() { 8 | return await fetch(makeUrl("http", "/mediainfo")) 9 | .then((v) => v.json()) 10 | .catch(() => {}); 11 | } 12 | 13 | function snakeCaseToTitleCase(str: string) { 14 | return str 15 | .split("_") 16 | .map((v) => v.charAt(0).toUpperCase() + v.slice(1)) 17 | .join(" "); 18 | } 19 | 20 | // rotate between foo and bar to actually update the image when url changes 21 | function makeImageUrl(filename: string) { 22 | return makeUrl("http", `/album_art?n=${encodeURIComponent(filename)}`); 23 | } 24 | 25 | export function Mediainfo() { 26 | const [mediainfo, setMediainfo] = createSignal>>(null); 27 | 28 | // const ws = new WebSocket("wss://" + window.location.host + "/mediainfo/ws"); 29 | const ws = new WebSocket(makeUrl("ws", "/mediainfo/ws")); 30 | ws.onmessage = async () => { 31 | setMediainfo(await fetchMediainfo()); 32 | document.title = "Now playing: " + joinTitleDate(bestTitleData(mediainfo()![0])); 33 | }; 34 | 35 | ws.onmessage(null as any); 36 | 37 | const lastSong = () => { 38 | return mediainfo()![0]; 39 | }; 40 | 41 | return ( 42 |
43 |
44 | 45 | { 48 | ev.currentTarget.src = noImage; 49 | }} 50 | /> 51 | 52 |
53 |
54 |
    55 | 56 | {Object.entries(lastSong()).map(([key, value]) => { 57 | return ; 58 | })} 59 | 60 | 61 |
62 |
63 |
64 | ); 65 | } 66 | 67 | function MediainfoEntry(props: {key: string; value: string}) { 68 | const value = () => { 69 | if (!props.value) { 70 | return "N/A"; 71 | } else if (props.key === "bitrate") { 72 | return Math.floor(+props.value / 1000) + "kbps"; 73 | } 74 | return props.value; 75 | }; 76 | return ( 77 |
  • 78 | {snakeCaseToTitleCase(props.key)}: 79 | {value()} 80 |
  • 81 | ); 82 | } 83 | 84 | function bestTitleData(mediainfo: Record): {title: string; artist: string | null} { 85 | let title = null; 86 | if (mediainfo.title) { 87 | title = mediainfo.title; 88 | } else if (mediainfo.publisher) { 89 | title = mediainfo.publisher; 90 | } else { 91 | title = mediainfo.filename; 92 | } 93 | 94 | let artist = null; 95 | if (mediainfo.album_artist) { 96 | artist = mediainfo.album_artist; 97 | } else if (mediainfo.artist) { 98 | artist = mediainfo.artist; 99 | } 100 | 101 | return {title, artist}; 102 | } 103 | 104 | function joinTitleDate(data: {title: string; artist: string | null}) { 105 | return (data.artist ? data.artist + " - " : "") + data.title; 106 | } 107 | 108 | function MediaLinks(props: {mediainfo: Record}) { 109 | const youtube = () => { 110 | const titleData = bestTitleData(props.mediainfo); 111 | const query = joinTitleDate(titleData); 112 | const url = `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`; 113 | return {url, query}; 114 | }; 115 | const touhoudb = () => { 116 | const {title} = bestTitleData(props.mediainfo); 117 | const url = `https://touhoudb.com/Search?searchType=Song&filter=${encodeURIComponent(title)}`; 118 | return {url, query: title}; 119 | }; 120 | return ( 121 | 129 | ); 130 | } 131 | -------------------------------------------------------------------------------- /src/cmd.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::{Path, PathBuf}, 3 | process::Stdio, 4 | }; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use tokio::process::Command; 8 | 9 | pub fn check_executables() -> (bool, Vec<(String, bool)>) { 10 | let info = ["ffmpeg", "ffprobe"] 11 | .into_iter() 12 | .map(|x| { 13 | (x.to_string(), { std::process::Command::new(x).spawn().map(|mut x| x.kill()).is_ok() }) 14 | }) 15 | .collect::>(); 16 | (info.iter().all(|x| x.1), info) 17 | } 18 | 19 | #[allow(clippy::option_if_let_else)] 20 | pub fn spawn_ffmpeg( 21 | input: &Path, 22 | sweeper: Option<&Path>, 23 | bitrate_bps: u32, 24 | copy_codec: bool, 25 | ) -> tokio::process::Child { 26 | if let Some(sweeper) = sweeper { 27 | build_with_sweeper(input, sweeper, bitrate_bps) 28 | } else { 29 | build_without_sweeper(input, bitrate_bps, copy_codec) 30 | } 31 | .kill_on_drop(true) 32 | .spawn() 33 | .unwrap() 34 | } 35 | 36 | fn build_without_sweeper(input: &Path, bitrate_bps: u32, copy_codec: bool) -> Command { 37 | let mut cmd = Command::new("ffmpeg"); 38 | cmd.args(["-hide_banner", "-loglevel", "fatal"]) 39 | .args(["-re", "-threads", "1", "-i"]) 40 | .arg(input); 41 | if copy_codec { 42 | cmd.args(["-c:a", "copy"]); 43 | } else { 44 | cmd.args(["-c:a", "mp3", "-b:a", &bitrate_bps.to_string()]); 45 | } 46 | cmd.args([ 47 | "-write_xing", 48 | "0", 49 | "-id3v2_version", 50 | "0", 51 | "-map_metadata", 52 | "-1", 53 | "-vn", 54 | // this speeds up encoding a little for some reason 55 | "-map", 56 | "0:a", 57 | "-f", 58 | "mp3", 59 | "-", 60 | ]) 61 | .stdout(Stdio::piped()) 62 | .stderr(Stdio::piped()) 63 | .stdin(Stdio::null()); 64 | cmd 65 | } 66 | 67 | // temporarily permanent i think 68 | pub const SWEEPER_DIR: &str = "./sweepers"; 69 | 70 | pub fn build_with_sweeper( 71 | input: impl AsRef, 72 | sweeper: impl AsRef, 73 | bitrate_bps: u32, 74 | ) -> Command { 75 | let mut cmd = Command::new("ffmpeg"); 76 | cmd.args(["-hide_banner", "-loglevel", "fatal"]) 77 | .args(["-re", "-threads", "1"]) 78 | .arg("-i") 79 | .arg(input.as_ref()) 80 | .arg("-i") 81 | .arg(sweeper.as_ref()) 82 | .args(["-c:a", "mp3", "-b:a", &bitrate_bps.to_string()]); 83 | cmd.args([ 84 | "-write_xing", 85 | "0", 86 | "-id3v2_version", 87 | "0", 88 | "-map_metadata", 89 | "-1", 90 | "-vn", 91 | "-filter_complex", 92 | "[0]atrim=0:1[in];[1]adelay=1s:all=1[voice];[in][voice][0]amix=inputs=3:weights='1, 1, 0.1':dropout_transition=0.5[out]", 93 | "-map", 94 | "[out]", 95 | "-f", 96 | "mp3", 97 | "-", 98 | ]) 99 | .stdout(Stdio::piped()) 100 | .stderr(Stdio::piped()) 101 | .stdin(Stdio::null()); 102 | cmd 103 | } 104 | 105 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 106 | pub struct Mediainfo { 107 | pub filename: PathBuf, 108 | pub title: Option, 109 | pub album: Option, 110 | pub artist: Option, 111 | pub album_artist: Option, 112 | pub publisher: Option, 113 | pub disc: Option, 114 | pub track: Option, 115 | pub genre: Option, 116 | pub bitrate: Option, 117 | pub codec: String, 118 | } 119 | 120 | pub async fn mediainfo(input: &Path) -> Result { 121 | let child = Command::new("ffprobe") 122 | .args([ 123 | "-loglevel", 124 | "fatal", 125 | "-select_streams", 126 | "a:0", 127 | "-show_entries", 128 | "format_tags:stream=codec_name,bit_rate:format=filename,bit_rate", 129 | "-of", 130 | "json=c=1", 131 | ]) 132 | .arg(input) 133 | .stdout(Stdio::piped()) 134 | .stderr(Stdio::piped()) 135 | .stdin(Stdio::null()) 136 | .spawn() 137 | .unwrap(); 138 | let output = child.wait_with_output().await.unwrap(); 139 | 140 | if !output.status.success() { 141 | return Err(format!("ffprobe failed: {}", String::from_utf8_lossy(&output.stderr))); 142 | } 143 | 144 | #[derive(Deserialize)] 145 | struct P { 146 | streams: [PStreams; 1], 147 | format: PFormat, 148 | } 149 | 150 | #[derive(Deserialize)] 151 | struct PStreams { 152 | codec_name: String, 153 | bit_rate: Option, 154 | } 155 | 156 | #[derive(Deserialize)] 157 | struct PFormat { 158 | filename: PathBuf, 159 | bit_rate: Option, 160 | tags: Option, 161 | } 162 | 163 | #[derive(Deserialize)] 164 | pub struct PMediainfo { 165 | #[serde(alias = "TITLE")] 166 | pub title: Option, 167 | #[serde(alias = "ALBUM")] 168 | pub album: Option, 169 | #[serde(alias = "ARTIST")] 170 | pub artist: Option, 171 | #[serde(alias = "ALBUM_ARTIST")] 172 | pub album_artist: Option, 173 | #[serde(alias = "PUBLISHER")] 174 | pub publisher: Option, 175 | #[serde(alias = "DISC")] 176 | pub disc: Option, 177 | #[serde(alias = "TRACK")] 178 | pub track: Option, 179 | #[serde(alias = "GENRE")] 180 | pub genre: Option, 181 | } 182 | 183 | let output: P = match serde_json::from_str(&String::from_utf8_lossy(&output.stdout)) { 184 | Ok(x) => x, 185 | Err(e) => { 186 | return Err(format!("ffprobe failed for {}: {}", input.display(), e)); 187 | } 188 | }; 189 | 190 | let [stream] = output.streams; 191 | let Some(tags) = output.format.tags else { 192 | return Ok(Mediainfo { 193 | filename: output.format.filename.file_name().unwrap_or_default().into(), 194 | title: None, 195 | album: None, 196 | artist: None, 197 | album_artist: None, 198 | publisher: None, 199 | disc: None, 200 | track: None, 201 | genre: None, 202 | bitrate: stream.bit_rate.or(output.format.bit_rate).and_then(|x| x.parse().ok()), 203 | codec: stream.codec_name, 204 | }); 205 | }; 206 | Ok(Mediainfo { 207 | filename: output.format.filename.file_name().unwrap_or_default().into(), 208 | title: tags.title, 209 | album: tags.album, 210 | artist: tags.artist, 211 | album_artist: tags.album_artist, 212 | publisher: tags.publisher, 213 | disc: tags.disc, 214 | track: tags.track, 215 | genre: tags.genre, 216 | bitrate: stream.bit_rate.or(output.format.bit_rate).and_then(|x| x.parse().ok()), 217 | codec: stream.codec_name, 218 | }) 219 | } 220 | 221 | /// returns true if album art was found 222 | /// sets length to 0 if no art is found 223 | pub async fn album_art_png(input: &Path) -> Result>, String> { 224 | let child = Command::new("ffmpeg") 225 | .args(["-loglevel", "fatal", "-i"]) 226 | .arg(input) 227 | .args(["-an", "-c:v", "png", "-f", "image2pipe", "-"]) 228 | .stdout(Stdio::piped()) 229 | .stderr(Stdio::piped()) 230 | .stdin(Stdio::null()) 231 | .kill_on_drop(true) 232 | .spawn() 233 | .unwrap(); 234 | let output = child.wait_with_output().await.unwrap(); 235 | 236 | if !output.status.success() { 237 | let msg = String::from_utf8(output.stderr).unwrap(); 238 | if msg.contains("Output file does not contain any stream") { 239 | return Ok(None); 240 | } 241 | return Err(format!("ffmpeg failed: {}", msg)); 242 | } 243 | 244 | if output.stdout.is_empty() { 245 | return Ok(None); 246 | } 247 | 248 | Ok(Some(output.stdout)) 249 | } 250 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use clap::ArgAction; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{ 4 | fmt::{Display, Formatter}, 5 | num::{NonZeroU32, NonZeroUsize}, 6 | path::{Path, PathBuf}, 7 | str::FromStr, 8 | }; 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | pub struct Config { 12 | pub host: String, 13 | pub port: u16, 14 | pub dirs: Box<[DirectoryConfig]>, 15 | pub enable_webui: bool, 16 | pub shuffle: bool, 17 | pub bitrate: u32, 18 | pub transcode_all: bool, 19 | pub sweeper_chance: f32, 20 | pub enable_mediainfo: bool, 21 | pub mediainfo_history: NonZeroUsize, 22 | } 23 | 24 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 25 | pub enum UseConfigArg { 26 | Default, 27 | Custom(PathBuf), 28 | } 29 | 30 | impl FromStr for UseConfigArg { 31 | type Err = String; 32 | fn from_str(s: &str) -> Result { 33 | if s.is_empty() { 34 | Ok(Self::Default) 35 | } else { 36 | Ok(Self::Custom(PathBuf::from(s))) 37 | } 38 | } 39 | } 40 | 41 | impl Display for UseConfigArg { 42 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 43 | match self { 44 | Self::Default => write!(f, "Default"), 45 | Self::Custom(x) => write!(f, "Custom({})", x.display()), 46 | } 47 | } 48 | } 49 | 50 | #[repr(transparent)] 51 | #[derive(Clone, Copy, Debug, Serialize, Deserialize)] 52 | pub struct ZeroOneF32(f32); 53 | 54 | impl FromStr for ZeroOneF32 { 55 | type Err = ::Err; 56 | fn from_str(s: &str) -> Result { 57 | let x: f32 = s.parse()?; 58 | (0.0..=1.0).contains(&x).then_some(Self(x)).ok_or_else(|| "a".parse::().unwrap_err()) 59 | } 60 | } 61 | 62 | impl Display for ZeroOneF32 { 63 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 64 | self.0.fmt(f) 65 | } 66 | } 67 | 68 | #[derive(clap::Parser, Debug)] 69 | pub struct PreCliConfig { 70 | #[clap( 71 | long = "generate-config", 72 | default_missing_value = "", 73 | num_args(0..=1), 74 | )] 75 | pub generate_config: Option, 76 | #[clap( 77 | long = "use-config", 78 | default_missing_value = "", 79 | num_args(0..=1), 80 | )] 81 | pub use_config: Option, 82 | } 83 | 84 | #[derive(Debug, Serialize, Deserialize, clap::Parser)] 85 | #[command(version, about, long_about = None)] 86 | pub struct CliConfig { 87 | // these are here just to appear in --help 88 | #[clap( 89 | long = "generate-config", 90 | value_name = "FILE", 91 | help = "Overwrite existing or create a new config file. Optionally pass a path to the config file to be created (not directory). 92 | Doesn't work right yet.", 93 | default_missing_value = "", 94 | num_args(0..=1), 95 | group = "config", 96 | )] 97 | _generate: Option, 98 | #[clap( 99 | long = "use-config", 100 | value_name = "FILE", 101 | long_help = "Use the config file instead of the command line. Generates a new config if none exists. 102 | All arguments except '--generate-config' are ignored if this is present. 103 | Optionally pass a path to the config file to be created/read (not directory).", 104 | default_missing_value = "", 105 | num_args(0..=1), 106 | group = "config", 107 | )] 108 | _use: Option, 109 | 110 | #[clap(long, help = "The host to bind to.", default_value = "127.0.0.1")] 111 | pub host: String, 112 | #[clap(long, default_value_t = 9005)] 113 | pub port: u16, 114 | #[clap( 115 | long, 116 | action, 117 | help = "Enable /webui endpoint. Lets you view some statistics.", 118 | default_value_t = true 119 | )] 120 | pub enable_webui: bool, 121 | #[clap(long, action, help = "Choose next song randomly.", default_value_t = true)] 122 | pub shuffle: bool, 123 | #[clap( 124 | value_name = "0..1", 125 | long, 126 | action, 127 | help = format!("123"), 128 | default_value_t = ZeroOneF32(0.0) 129 | )] 130 | pub sweeper_chance: ZeroOneF32, 131 | #[clap( 132 | long = "bitrate", 133 | help = "The bitrate to use for transcoding. Plain value for bps and suffixed with 'k' for kbps.", 134 | default_value = "128k" 135 | )] 136 | pub transcode_bitrate: Bitrate, 137 | #[clap( 138 | long, 139 | action, 140 | help = "Enable /mediainfo endpoint. It serves metadata for the current song in JSON format.", 141 | default_value_t = true, 142 | group = "mediainfo" 143 | )] 144 | pub enable_mediainfo: bool, 145 | #[clap( 146 | long, 147 | action, 148 | value_name = "SIZE", 149 | help = "The size of song history to keep track of. Must be greater than 0.", 150 | default_value = "16", 151 | group = "mediainfo", 152 | requires = "mediainfo" 153 | )] 154 | pub mediainfo_history: NonZeroUsize, 155 | #[clap( 156 | long, 157 | action(ArgAction::Set), 158 | require_equals = true, 159 | num_args(0..=1), 160 | help = "Transcode files that can be sent without transcoding. Set to true if you want to reduce bandwidth a little.", 161 | default_value_t = false, 162 | default_missing_value = "true" 163 | )] 164 | pub transcode_all: bool, 165 | #[clap( 166 | long, 167 | help = "The root directory to recursively search for music. 168 | Note: --use-config allows to specify multiple root directories." 169 | )] 170 | pub root: PathBuf, 171 | #[command(flatten, help = "Optionally include or exclude directories or files.")] 172 | pub mode: Option, 173 | } 174 | 175 | #[derive(Debug, Serialize, Deserialize, clap::Parser, Clone)] 176 | pub struct Bitrate { 177 | pub bits_per_second: NonZeroU32, 178 | } 179 | 180 | impl FromStr for Bitrate { 181 | type Err = String; 182 | fn from_str(s: &str) -> Result { 183 | let bits_per_second = s.parse::().map_err(|x| x.to_string()).or_else(|x| { 184 | let last_char = 185 | s.chars().last().ok_or_else(|| "Empty string".to_string())?.to_ascii_lowercase(); 186 | 187 | if last_char == 'k' { 188 | s[..s.len() - 1] 189 | .parse::() 190 | .map(|x| (x.get() * 1000).try_into().unwrap()) 191 | .map_err(|x| x.to_string()) 192 | } else { 193 | Err(x) 194 | } 195 | })?; 196 | Ok(Self { bits_per_second }) 197 | } 198 | } 199 | 200 | #[derive(Debug, Serialize, Deserialize, clap::Args)] 201 | #[group(required = false, multiple = false)] 202 | pub struct DirectoryConfigModeCli { 203 | #[clap(long, help = "Include these directories or files.")] 204 | include: Vec, 205 | #[clap(long, help = "Exclude these directories or files.")] 206 | exclude: Vec, 207 | } 208 | 209 | impl From for Config { 210 | fn from(cli: CliConfig) -> Self { 211 | let mut dir = Vec::new(); 212 | 213 | let mode = match cli.mode { 214 | Some(DirectoryConfigModeCli { include, exclude }) => { 215 | if !include.is_empty() { 216 | Some(DirectoryConfig { 217 | root: cli.root.clone(), 218 | mode: DirectoryConfigMode::Include(include.into_boxed_slice()), 219 | }) 220 | } else if !exclude.is_empty() { 221 | Some(DirectoryConfig { 222 | root: cli.root.clone(), 223 | mode: DirectoryConfigMode::Exclude(exclude.into_boxed_slice()), 224 | }) 225 | } else { 226 | // unreachable!() 227 | None 228 | } 229 | } 230 | None => Some(DirectoryConfig { 231 | root: cli.root, 232 | mode: DirectoryConfigMode::Exclude([].into()), 233 | }), 234 | }; 235 | 236 | if let Some(mode) = mode { 237 | dir.push(mode); 238 | } 239 | 240 | Self { 241 | host: cli.host, 242 | port: cli.port, 243 | dirs: dir.into_boxed_slice(), 244 | enable_webui: cli.enable_webui, 245 | shuffle: cli.shuffle, 246 | sweeper_chance: cli.sweeper_chance.0, 247 | bitrate: cli.transcode_bitrate.bits_per_second.get(), 248 | transcode_all: cli.transcode_all, 249 | enable_mediainfo: cli.enable_mediainfo, 250 | mediainfo_history: cli.mediainfo_history, 251 | } 252 | } 253 | } 254 | 255 | impl Default for Config { 256 | fn default() -> Self { 257 | Self { 258 | host: "0.0.0.0".to_string(), 259 | port: 9005, 260 | dirs: Box::new([DirectoryConfig { 261 | root: PathBuf::from("./"), 262 | mode: DirectoryConfigMode::Exclude([].into()), 263 | }]), 264 | shuffle: true, 265 | sweeper_chance: 0.0, 266 | enable_webui: true, 267 | bitrate: 128_000, 268 | transcode_all: false, 269 | enable_mediainfo: true, 270 | mediainfo_history: NonZeroUsize::new(16).unwrap(), 271 | } 272 | } 273 | } 274 | 275 | #[derive(Debug, Serialize, Deserialize, Clone)] 276 | #[serde(rename_all = "lowercase")] 277 | #[serde(tag = "mode", content = "paths")] 278 | pub enum DirectoryConfigMode { 279 | Include(Box<[PathBuf]>), 280 | Exclude(Box<[PathBuf]>), 281 | } 282 | 283 | #[derive(Debug, Serialize, Deserialize, Clone)] 284 | pub struct DirectoryConfig { 285 | pub root: PathBuf, 286 | pub mode: DirectoryConfigMode, 287 | } 288 | 289 | pub enum Error { 290 | Parse(String), 291 | Io(std::io::Error), 292 | } 293 | 294 | impl From for Error { 295 | fn from(value: std::io::Error) -> Self { 296 | Self::Io(value) 297 | } 298 | } 299 | 300 | impl From for Error { 301 | fn from(value: toml::de::Error) -> Self { 302 | Self::Parse(value.to_string()) 303 | } 304 | } 305 | 306 | pub fn generate_or_load(path: &Path) -> Result { 307 | if !path.exists() { 308 | generate_config_file(path) 309 | } else { 310 | let x = std::fs::read_to_string(path).unwrap(); 311 | Ok(toml::from_str(&x)?) 312 | } 313 | } 314 | 315 | pub fn generate_config_file(path: &Path) -> Result { 316 | if !path.exists() { 317 | path.parent() 318 | .ok_or_else(|| { 319 | std::io::Error::new(std::io::ErrorKind::InvalidData, "No parent directory") 320 | }) 321 | .and_then(std::fs::create_dir_all)?; 322 | } 323 | let config = Config::default(); 324 | std::fs::write(path, toml::to_string(&config).unwrap())?; 325 | 326 | Ok(config) 327 | } 328 | 329 | pub fn config_path() -> PathBuf { 330 | #[cfg(target_os = "linux")] 331 | if is_root::is_root() { 332 | PathBuf::from("/etc/radio/config.toml") 333 | } else { 334 | PathBuf::from("~/.config/radio/config.toml") 335 | } 336 | #[cfg(target_os = "windows")] 337 | if is_root::is_root() { 338 | windirs::known_folder_path(windirs::FolderId::RoamingAppData) 339 | } else { 340 | windirs::known_folder_path(windirs::FolderId::LocalAppData) 341 | } 342 | .unwrap() 343 | .join("radio/config.toml") 344 | } 345 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::nursery)] 2 | #![allow(clippy::redundant_pub_crate)] 3 | #![deny(clippy::semicolon_if_nothing_returned)] 4 | #![allow(unused)] 5 | 6 | mod audio; 7 | mod cmd; 8 | mod config; 9 | mod files; 10 | mod player; 11 | 12 | use axum::{ 13 | body::Body, 14 | debug_handler, 15 | extract::{ 16 | ws::{self, rejection::WebSocketUpgradeRejection}, 17 | Path, Query, State, WebSocketUpgrade, 18 | }, 19 | http::{header, HeaderMap, HeaderValue, StatusCode, Uri}, 20 | response::{Html, IntoResponse, Redirect}, 21 | routing::get, 22 | Router, 23 | }; 24 | use clap::Parser; 25 | 26 | use player::Player; 27 | use std::{fmt::Write, sync::Arc, time::Duration}; 28 | use tokio::time::Interval; 29 | 30 | use crate::config::DirectoryConfig; 31 | 32 | #[tokio::main] 33 | async fn main() { 34 | match cmd::check_executables() { 35 | (true, _) => {} 36 | (false, missing) => { 37 | println!( 38 | "Could not execute: {}", 39 | missing.into_iter().map(|x| format!("{:?}", x.0)).collect::>().join(", ") 40 | ); 41 | let x = std::env::current_dir() 42 | .map(|x| format!(" ({:?})", x.display())) 43 | .unwrap_or_default(); 44 | println!("Make sure ffmpeg is installed and accessible. Or just put those two in the current directory{x}."); 45 | 46 | return; 47 | } 48 | } 49 | 50 | // i dont want to see this code 51 | // into a function it goes 52 | let Some(config) = config_shit() else { 53 | return; 54 | }; 55 | 56 | let sweeper_list = files::collect(&[DirectoryConfig { 57 | mode: config::DirectoryConfigMode::Exclude(vec![].into_boxed_slice()), 58 | root: cmd::SWEEPER_DIR.into(), 59 | }]); 60 | 61 | if config.sweeper_chance > 0.0 && sweeper_list.is_empty() { 62 | println!( 63 | "Sweeper chance is set to {}, but no sweepers found in {}", 64 | config.sweeper_chance, 65 | cmd::SWEEPER_DIR 66 | ); 67 | return; 68 | } 69 | 70 | let port = config.port; 71 | 72 | let player = match Player::new(files::collect(&config.dirs), sweeper_list, config.clone()) { 73 | Ok(player) => player, 74 | Err(e) => { 75 | println!("Player error: {:?}", e); 76 | return; 77 | } 78 | }; 79 | 80 | println!("Playlist:"); 81 | let take = 10; 82 | for x in player.files().iter().take(take) { 83 | println!(" {}", x.display()); 84 | } 85 | if player.files().len() > take { 86 | println!(" ... and {} more", player.files().len() - take); 87 | } 88 | 89 | let app = define_routes(Router::new(), &config) 90 | .layer(tower_http::cors::CorsLayer::permissive()) 91 | .with_state(player.clone()); 92 | 93 | let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await.unwrap(); 94 | println!("Listening on port {}", port); 95 | 96 | axum::serve(listener, app).await.unwrap(); 97 | } 98 | 99 | fn config_shit() -> Option> { 100 | let pre_config = config::PreCliConfig::try_parse(); 101 | let mut use_config = None; 102 | if let Ok(config::PreCliConfig { generate_config, use_config: use_config_arg, .. }) = pre_config 103 | { 104 | if let Some(x) = generate_config { 105 | let path = match x { 106 | config::UseConfigArg::Custom(path) => path, 107 | config::UseConfigArg::Default => config::config_path(), 108 | }; 109 | if let Err(error) = config::generate_config_file(&path) { 110 | match error { 111 | config::Error::Io(e) => println!("Could not generate config file: {}", e), 112 | config::Error::Parse(_) => unreachable!(), 113 | } 114 | } else { 115 | println!("Config file generated in {}", path.display()); 116 | } 117 | } 118 | use_config = use_config_arg; 119 | } 120 | 121 | let config: Arc = if let Some(use_config) = use_config { 122 | let path = match use_config { 123 | config::UseConfigArg::Custom(path) => path, 124 | config::UseConfigArg::Default => config::config_path(), 125 | }; 126 | println!("Loading config from {}", path.display()); 127 | match config::generate_or_load(&path) { 128 | Ok(x) => Arc::new(x), 129 | Err(error) => { 130 | match error { 131 | config::Error::Io(e) => println!("Could not generate or load config: {}", e), 132 | config::Error::Parse(e) => println!("Could not parse config:\n{}", e), 133 | } 134 | return None; 135 | } 136 | } 137 | } else { 138 | Arc::new(config::CliConfig::parse().into()) 139 | }; 140 | 141 | Some(config) 142 | } 143 | 144 | fn define_routes(r: Router, config: &Arc) -> Router { 145 | let mut r = r 146 | .route("/stream", get(stream)) 147 | .route("/", get(webpage)) 148 | .route("/album_art", get(album_art)); 149 | #[cfg(feature = "webapp")] 150 | { 151 | r = r.route("/*file", get(webpage_assets)); 152 | } 153 | if config.enable_mediainfo { 154 | r = r.route("/mediainfo", get(mediainfo)); 155 | r = r.route("/mediainfo/ws", get(mediainfo_ws)); 156 | } 157 | if config.enable_webui { 158 | r = r.route("/webui", get(webui)); 159 | } 160 | r 161 | } 162 | 163 | #[cfg(feature = "webapp")] 164 | #[derive(rust_embed::RustEmbed)] 165 | #[folder = "radio-webapp/dist/"] 166 | struct WebappAssets; 167 | 168 | async fn webpage() -> impl IntoResponse { 169 | #[cfg(feature = "webapp")] 170 | { 171 | Html(WebappAssets::get("index.html").unwrap().data) 172 | } 173 | #[cfg(not(feature = "webapp"))] 174 | { 175 | Redirect::to("mediainfo") 176 | } 177 | } 178 | 179 | #[cfg(feature = "webapp")] 180 | async fn webpage_assets(Path(path): Path) -> impl IntoResponse { 181 | if path.starts_with("assets/") { 182 | path.replace("assets/", ""); 183 | } else { 184 | return StatusCode::NOT_FOUND.into_response(); 185 | } 186 | 187 | match WebappAssets::get(&path) { 188 | Some(x) => { 189 | let mime = mime_guess::from_path(path).first_or_octet_stream(); 190 | ([(header::CONTENT_TYPE, mime.as_ref())], x.data).into_response() 191 | } 192 | None => StatusCode::NOT_FOUND.into_response(), 193 | } 194 | } 195 | 196 | #[debug_handler] 197 | async fn stream(State(player): State) -> Result { 198 | let stream = player.subscribe(); 199 | 200 | let mut headers = axum::http::HeaderMap::new(); 201 | headers.insert("Content-Type", "audio/mpeg".parse().unwrap()); 202 | headers.insert( 203 | "Cache-Control", 204 | "no-store, no-cache, must-revalidate, s-max-age=0".parse().unwrap(), 205 | ); 206 | headers.insert( 207 | "x-bitrate", 208 | if player.config().transcode_all { 209 | player.config().bitrate.to_string().parse().unwrap() 210 | } else { 211 | "vary".parse().unwrap() 212 | }, 213 | ); 214 | 215 | Ok((headers, Body::from_stream(stream).into_response())) 216 | } 217 | 218 | async fn mediainfo(State(player): State) -> impl IntoResponse { 219 | let mediainfo_json = player.read_mediainfo(|x| serde_json::to_string(x).unwrap()).await; 220 | ([(header::CONTENT_TYPE, "application/json")], mediainfo_json) 221 | } 222 | 223 | async fn mediainfo_ws( 224 | State(player): State, 225 | ws: Result, 226 | headers: HeaderMap, 227 | ) -> impl IntoResponse { 228 | let (ws) = match ws { 229 | Ok(x) => x, 230 | Err(x) => { 231 | println!("No websocket upgrade: {x:?}"); 232 | return Err(x); 233 | } 234 | }; 235 | 236 | Ok(ws 237 | .on_failed_upgrade(|x| { 238 | println!("Failed to upgrade: {:?}", x); 239 | }) 240 | .on_upgrade(move |mut socket| async move { 241 | let mut rx = player.subscribe_next_song(); 242 | let mut interaval = tokio::time::interval(Duration::from_secs(19)); 243 | loop { 244 | tokio::select! { 245 | biased; 246 | None = socket.recv() => break, 247 | _ = interaval.tick() => { 248 | // println!("pinging socket"); 249 | socket.send(ws::Message::Ping(vec![])).await; 250 | } 251 | _ = rx.changed() => { 252 | // println!("sending new song to socket"); 253 | let _ = socket.send(ws::Message::Text("next".to_string())).await; 254 | }, 255 | } 256 | } 257 | })) 258 | } 259 | 260 | async fn webui(State(player): State) -> impl IntoResponse { 261 | fn display_bytes(x: usize) -> String { 262 | match x { 263 | x if x < 1024 => format!("{x} B"), 264 | x if x < 1024 * 1024 => format!("{:.2} KiB", x as f64 / 1024.0), 265 | x if x < 1024 * 1024 * 1024 => format!("{:.2} MiB", x as f64 / 1024.0 / 1024.0), 266 | x => format!("{:.2} GiB", x as f64 / 1024.0 / 1024.0 / 1024.0), 267 | } 268 | } 269 | 270 | fn display_time(x: std::time::Duration) -> String { 271 | let mut x = x.as_secs(); 272 | let seconds = x % 60; 273 | x /= 60; 274 | let minutes = x % 60; 275 | x /= 60; 276 | let hours = x; 277 | format!("{hours:02}:{minutes:02}:{seconds:02}") 278 | } 279 | 280 | let body = { 281 | let mut body = String::new(); 282 | let stats = player.statistics().read().await; 283 | writeln!(&mut body, "Time played: {}", display_time(stats.time_played)).unwrap(); 284 | writeln!(&mut body, "Listeners: {}", stats.listeners).unwrap(); 285 | writeln!(&mut body, "Max listeners: {}", stats.max_listeners).unwrap(); 286 | writeln!(&mut body, "Sent: {}", display_bytes(stats.bytes_sent)).unwrap(); 287 | writeln!(&mut body, "Transcoded: {}", display_bytes(stats.bytes_transcoded)).unwrap(); 288 | writeln!(&mut body, "Copied: {}", display_bytes(stats.bytes_copied)).unwrap(); 289 | writeln!(&mut body, "Target bandwidth: {}/s", display_bytes(stats.target_badwidth)) 290 | .unwrap(); 291 | body 292 | }; 293 | ([(header::CONTENT_TYPE, "text/plain")], body) 294 | } 295 | 296 | #[derive(serde::Deserialize)] 297 | struct NQuery { 298 | n: String, 299 | } 300 | 301 | #[allow(clippy::significant_drop_tightening)] 302 | async fn album_art( 303 | State(player): State, 304 | headers: HeaderMap, 305 | n_query: Option>, 306 | ) -> impl IntoResponse { 307 | let album_art = &player.album_art().read().await; 308 | 309 | let checksum_header_value = 310 | album_art.checksum().map(HeaderValue::from).unwrap_or(HeaderValue::from_static("no-image")); 311 | 312 | if let Some(x) = headers.get(header::IF_NONE_MATCH) { 313 | if x == checksum_header_value { 314 | return StatusCode::NOT_MODIFIED.into_response(); 315 | } 316 | } 317 | 318 | let album_art = album_art.get(); 319 | 320 | let body = album_art.map_or_else(|| Err(StatusCode::NO_CONTENT), |x| Ok(x.to_owned())); 321 | 322 | let mut headers = HeaderMap::new(); 323 | headers.insert(header::CONTENT_TYPE, "image/png".parse().unwrap()); 324 | if n_query.is_some() { 325 | headers.insert(header::CACHE_CONTROL, "max-age=1800".parse().unwrap()); 326 | } else { 327 | headers.insert(header::CACHE_CONTROL, "no-store".parse().unwrap()); 328 | } 329 | headers.insert("ETag", checksum_header_value); 330 | 331 | (headers, body).into_response() 332 | } 333 | -------------------------------------------------------------------------------- /src/player.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::VecDeque, 3 | path::{Path, PathBuf}, 4 | pin::Pin, 5 | sync::{ 6 | atomic::{AtomicUsize, Ordering}, 7 | Arc, 8 | }, 9 | time::Duration, 10 | }; 11 | 12 | use axum::body::Bytes; 13 | use futures_core::Stream; 14 | use rand::{seq::IteratorRandom, Rng}; 15 | use tokio::{ 16 | sync::{oneshot, RwLock}, 17 | task::JoinSet, 18 | }; 19 | 20 | use crate::{ 21 | audio::{self, AudioReader, FFMpegAudioReader}, 22 | cmd, config, 23 | }; 24 | 25 | #[derive(Clone)] 26 | pub struct Player { 27 | inner: Arc, 28 | } 29 | 30 | pub struct FixedDeque(VecDeque, usize); 31 | 32 | impl FixedDeque { 33 | pub fn new(size: usize) -> Self { 34 | Self(VecDeque::with_capacity(size), size) 35 | } 36 | 37 | pub fn push(&mut self, value: T) { 38 | let x = &mut self.0; 39 | if x.len() == self.1 { 40 | x.pop_back(); 41 | } 42 | x.push_front(value); 43 | x.make_contiguous(); 44 | } 45 | 46 | pub fn as_slice(&self) -> &[T] { 47 | let (a, b) = self.0.as_slices(); 48 | assert!(b.is_empty()); 49 | a 50 | } 51 | } 52 | 53 | pub struct TrackDropStream(T, Option>); 54 | 55 | impl TrackDropStream { 56 | fn create(stream: T) -> (Self, oneshot::Receiver<()>) { 57 | let (tx, drop_rx) = oneshot::channel(); 58 | let this = Self(stream, Some(tx)); 59 | (this, drop_rx) 60 | } 61 | } 62 | 63 | impl Drop for TrackDropStream { 64 | fn drop(&mut self) { 65 | let _ = self.1.take().unwrap().send(()); 66 | } 67 | } 68 | 69 | impl futures_core::Stream for TrackDropStream { 70 | type Item = T::Item; 71 | 72 | fn poll_next( 73 | mut self: std::pin::Pin<&mut Self>, 74 | cx: &mut std::task::Context<'_>, 75 | ) -> std::task::Poll> { 76 | Pin::new(&mut self.0).poll_next(cx) 77 | } 78 | } 79 | 80 | #[derive(Debug, Default)] 81 | pub struct Statistics { 82 | pub time_played: Duration, 83 | pub listeners: usize, 84 | pub max_listeners: usize, 85 | pub bytes_transcoded: usize, 86 | pub bytes_copied: usize, 87 | pub bytes_sent: usize, 88 | pub target_badwidth: usize, 89 | } 90 | 91 | pub struct Inner { 92 | playlist: Box<[PathBuf]>, 93 | sweeper_list: Box<[PathBuf]>, 94 | album_art: RwLock, 95 | index: AtomicUsize, 96 | mediainfo: RwLock>, 97 | tx: tokio::sync::broadcast::Sender, 98 | next_song_tx: tokio::sync::watch::Sender<()>, 99 | task_control_tx: tokio::sync::watch::Sender, 100 | config: Arc, 101 | statistics: RwLock, 102 | } 103 | 104 | #[derive(Debug, Default)] 105 | pub struct AlbumImage(Vec); 106 | 107 | impl AlbumImage { 108 | // public method for getting the album art if it exists 109 | pub fn get(&self) -> Option<&[u8]> { 110 | (!self.0.is_empty()).then_some(&self.0) 111 | } 112 | 113 | pub fn checksum(&self) -> Option { 114 | self.get().map(|x| x.iter().map(|x| (*x).into()).fold(0i64, |a, b| a.wrapping_add(b))) 115 | } 116 | 117 | // get the inner buffer for modification by player 118 | fn set(&mut self, new: Vec) { 119 | self.0 = new; 120 | } 121 | 122 | fn clear(&mut self) { 123 | self.0.clear(); 124 | } 125 | } 126 | 127 | #[derive(Clone, Copy)] 128 | enum TaskControlMessage { 129 | Play, 130 | Pause, 131 | } 132 | 133 | pub type PlayerRx = TrackDropStream>; 134 | 135 | #[derive(Debug)] 136 | pub enum Error { 137 | EmptyPlayilist, 138 | } 139 | 140 | impl Player { 141 | pub fn new( 142 | playlist: Vec, 143 | sweeper_list: Vec, 144 | config: Arc, 145 | ) -> Result { 146 | if playlist.is_empty() { 147 | return Err(Error::EmptyPlayilist); 148 | } 149 | 150 | let index = 151 | if config.shuffle { rand::thread_rng().gen_range(0..playlist.len()) } else { 0 }; 152 | let tx = tokio::sync::broadcast::channel(4).0; 153 | let next_song_tx = tokio::sync::watch::channel(()).0; 154 | 155 | let player = Self { 156 | inner: Arc::new(Inner { 157 | playlist: playlist.into_boxed_slice(), 158 | sweeper_list: sweeper_list.into_boxed_slice(), 159 | album_art: Default::default(), 160 | index: index.into(), 161 | mediainfo: FixedDeque::new(config.mediainfo_history.get()).into(), 162 | tx, 163 | next_song_tx, 164 | task_control_tx: tokio::sync::watch::channel(TaskControlMessage::Play).0, 165 | config, 166 | statistics: Default::default(), 167 | }), 168 | }; 169 | 170 | player.clone().spawn_task(); 171 | 172 | Ok(player) 173 | } 174 | 175 | fn spawn_task(self) { 176 | tokio::spawn({ 177 | async move { 178 | let mut rx = self.inner.task_control_tx.subscribe(); 179 | let player_init_instant = tokio::time::Instant::now(); 180 | loop { 181 | let msg = *rx.borrow(); 182 | match msg { 183 | TaskControlMessage::Play => loop { 184 | tokio::select! { 185 | _ = rx.changed() => break, 186 | _ = self.play_next(player_init_instant) => (), 187 | } 188 | }, 189 | TaskControlMessage::Pause => { 190 | tokio::select! { 191 | _ = rx.changed() => break, 192 | _ = tokio::time::sleep(Duration::from_secs(999)) => (), 193 | } 194 | } 195 | } 196 | } 197 | } 198 | }); 199 | } 200 | 201 | // #[allow(clippy::significant_drop_tightening, clippy::significant_drop_in_scrutinee)] 202 | async fn play_next(&self, player_init_instant: tokio::time::Instant) { 203 | let Inner { playlist, sweeper_list, album_art, index, tx, config, .. } = &*self.inner; 204 | let index = index.load(Ordering::Relaxed); 205 | 206 | let input = &playlist[index]; 207 | 208 | let mediainfo = match cmd::mediainfo(input).await { 209 | Ok(x) => x, 210 | Err(x) => { 211 | println!("{:?}\tbroken file - skipping: {x}", playlist[index].file_name().unwrap()); 212 | tokio::time::sleep(Duration::from_secs(1)).await; 213 | self.next(); 214 | return; 215 | } 216 | }; 217 | 218 | let album_image_path = match try_album_arts(input).await { 219 | Some(data) => { 220 | album_art.write().await.set(data.1); 221 | Some(data.0) 222 | } 223 | None => { 224 | album_art.write().await.clear(); 225 | None 226 | } 227 | }; 228 | 229 | let sweeper_path = { 230 | let mut rng = rand::thread_rng(); 231 | (rng.gen::() <= config.sweeper_chance).then(|| { 232 | // checked non empty in main 233 | sweeper_list.iter().choose(&mut rng).unwrap() 234 | }) 235 | }; 236 | let copy_codec = !config.transcode_all && mediainfo.codec == "mp3"; 237 | 238 | println!( 239 | "{:?}\t(codec: {}, copy: {}, sweeper: {}, album image: {})", 240 | playlist[index].file_name().unwrap(), 241 | mediainfo.codec, 242 | if copy_codec { "yes" } else { "no" }, 243 | sweeper_path.as_ref().map(|x| x.file_name().unwrap().to_str().unwrap()).unwrap_or("no"), 244 | album_image_path 245 | .as_ref() 246 | .map(|x| x.file_name().unwrap().to_str().unwrap()) 247 | .unwrap_or("none"), 248 | ); 249 | 250 | self.inner.mediainfo.write().await.push(mediainfo); 251 | 252 | // notify about next song after everything is updated 253 | let _ = self.inner.next_song_tx.send(()); 254 | 255 | #[allow(clippy::significant_drop_tightening)] 256 | let transmit_reader = |mut reader: FFMpegAudioReader| async move { 257 | let buf = &mut [0u8; 4096]; 258 | let mut bandwidth_instant = tokio::time::Instant::now(); 259 | let mut bandwidth_acc = 0; 260 | loop { 261 | let data = reader.read_data(buf).await.unwrap(); 262 | match data { 263 | audio::Data::Audio(0) => break, 264 | audio::Data::Audio(read) => { 265 | let _ = tx.send(Bytes::copy_from_slice(&buf[..read])); 266 | bandwidth_acc += read; 267 | 268 | // clippy::significant_drop_tightening 269 | let mut stats = self.inner.statistics.write().await; 270 | if copy_codec { 271 | stats.bytes_copied += read; 272 | } else { 273 | stats.bytes_transcoded += read; 274 | } 275 | stats.bytes_sent += read * tx.receiver_count(); 276 | 277 | if bandwidth_instant.elapsed() >= Duration::from_secs(1) { 278 | stats.target_badwidth = bandwidth_acc * tx.receiver_count(); 279 | bandwidth_acc = 0; 280 | bandwidth_instant = tokio::time::Instant::now(); 281 | } 282 | } 283 | audio::Data::Error(err) => { 284 | println!("ffmpeg error: {:?}", err); 285 | break; 286 | } 287 | } 288 | self.inner.statistics.write().await.time_played = player_init_instant.elapsed(); 289 | } 290 | }; 291 | 292 | transmit_reader(audio::FFMpegAudioReader::start( 293 | input, 294 | sweeper_path, 295 | config.bitrate, 296 | copy_codec, 297 | )) 298 | .await; 299 | self.next(); 300 | } 301 | 302 | pub fn set_index(&mut self, index: usize) { 303 | self.inner.index.store(index, Ordering::Relaxed); 304 | } 305 | 306 | pub fn index(&self) -> usize { 307 | self.inner.index.load(Ordering::Relaxed) 308 | } 309 | 310 | pub fn current(&self) -> &Path { 311 | &self.inner.playlist[self.inner.index.load(Ordering::Relaxed)] 312 | } 313 | 314 | pub fn files(&self) -> &[PathBuf] { 315 | &self.inner.playlist 316 | } 317 | 318 | pub fn subscribe(&self) -> PlayerRx { 319 | let _tx = self.inner.tx.subscribe(); 320 | let stream = tokio_stream::wrappers::BroadcastStream::new(self.inner.tx.subscribe()); 321 | let (stream, drop_rx) = TrackDropStream::create(stream); 322 | 323 | tokio::spawn({ 324 | let inner = self.inner.clone(); 325 | async move { 326 | let statistics = &inner.statistics; 327 | { 328 | let mut statistics = statistics.write().await; 329 | statistics.listeners += 1; 330 | statistics.max_listeners = statistics.max_listeners.max(statistics.listeners); 331 | } 332 | drop_rx.await.unwrap(); 333 | { 334 | let mut statistics = statistics.write().await; 335 | statistics.listeners -= 1; 336 | } 337 | } 338 | }); 339 | 340 | stream 341 | } 342 | 343 | pub fn subscribe_next_song(&self) -> tokio::sync::watch::Receiver<()> { 344 | self.inner.next_song_tx.subscribe() 345 | } 346 | 347 | pub fn config(&self) -> &config::Config { 348 | &self.inner.config 349 | } 350 | 351 | pub fn statistics(&self) -> &RwLock { 352 | &self.inner.statistics 353 | } 354 | 355 | pub fn album_art(&self) -> &RwLock { 356 | &self.inner.album_art 357 | } 358 | 359 | pub async fn read_mediainfo R>(&self, f: F) -> R { 360 | f(self.inner.mediainfo.read().await.as_slice()) 361 | } 362 | 363 | fn next(&self) { 364 | let Inner { index, playlist, config, .. } = &*self.inner; 365 | let shuffle = config.shuffle; 366 | 367 | let mut loaded_index = index.load(Ordering::Relaxed); 368 | if shuffle { 369 | let mut rng = rand::thread_rng(); 370 | loaded_index = loop { 371 | let new_index = rng.gen_range(0..playlist.len()); 372 | if new_index != loaded_index || playlist.len() == 1 { 373 | break new_index; 374 | } 375 | } 376 | } else { 377 | loaded_index = (loaded_index + 1) % playlist.len(); 378 | } 379 | 380 | index.store(loaded_index, Ordering::Relaxed); 381 | } 382 | } 383 | 384 | /// try to read embedded album art and if it fails, try to read some image from the same directory 385 | async fn try_album_arts(input: impl AsRef + Send) -> Option<(PathBuf, Vec)> { 386 | async fn try_album_art(input: impl AsRef + Send) -> Option<(PathBuf, Vec)> { 387 | match cmd::album_art_png(input.as_ref()).await { 388 | Ok(None) => None, 389 | Ok(Some(buf)) => Some((input.as_ref().to_owned(), buf)), 390 | Err(_) => None, 391 | } 392 | } 393 | 394 | // try from the file itself 395 | if let Some(x) = try_album_art(input.as_ref()).await { 396 | return Some(x); 397 | } 398 | 399 | let mut futures_vec = vec![]; 400 | // then try from the same directory 401 | if let Some(x) = input.as_ref().parent() { 402 | let images = std::fs::read_dir(x) 403 | .unwrap() 404 | .flatten() 405 | .filter(|x| { 406 | let Ok(file_type) = x.file_type() else { 407 | return false; 408 | }; 409 | file_type.is_file() 410 | && x.path() 411 | .extension() 412 | .map_or(false, |x| x == "png" || x == "jpg" || x == "jpeg") 413 | }) 414 | .collect::>(); 415 | // try to read "cover.*" from the same directory 416 | if let Some(x) = 417 | images.iter().find(|x| x.path().file_stem().map_or(false, |x| x == "cover")) 418 | { 419 | futures_vec.push(try_album_art(x.path())); 420 | } else { 421 | for x in images { 422 | futures_vec.push(try_album_art(x.path())); 423 | } 424 | } 425 | } 426 | 427 | let mut joins = JoinSet::new(); 428 | for x in futures_vec { 429 | joins.spawn(x); 430 | } 431 | 432 | let mut result = None; 433 | while let Some(fut) = joins.join_next().await { 434 | if let Ok(Some(x)) = fut { 435 | result = Some(x); 436 | joins.abort_all(); 437 | break; 438 | } 439 | } 440 | 441 | result 442 | } 443 | -------------------------------------------------------------------------------- /radio-webapp/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | solid-js: 9 | specifier: ^1.8.15 10 | version: 1.8.15 11 | 12 | devDependencies: 13 | '@types/node': 14 | specifier: ^20.11.24 15 | version: 20.11.24 16 | typescript: 17 | specifier: ^5.2.2 18 | version: 5.3.3 19 | vite: 20 | specifier: ^5.1.4 21 | version: 5.1.5(@types/node@20.11.24) 22 | vite-plugin-solid: 23 | specifier: ^2.10.1 24 | version: 2.10.1(solid-js@1.8.15)(vite@5.1.5) 25 | 26 | packages: 27 | 28 | /@ampproject/remapping@2.3.0: 29 | resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 30 | engines: {node: '>=6.0.0'} 31 | dependencies: 32 | '@jridgewell/gen-mapping': 0.3.5 33 | '@jridgewell/trace-mapping': 0.3.25 34 | dev: true 35 | 36 | /@babel/code-frame@7.23.5: 37 | resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} 38 | engines: {node: '>=6.9.0'} 39 | dependencies: 40 | '@babel/highlight': 7.23.4 41 | chalk: 2.4.2 42 | dev: true 43 | 44 | /@babel/compat-data@7.23.5: 45 | resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} 46 | engines: {node: '>=6.9.0'} 47 | dev: true 48 | 49 | /@babel/core@7.24.0: 50 | resolution: {integrity: sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==} 51 | engines: {node: '>=6.9.0'} 52 | dependencies: 53 | '@ampproject/remapping': 2.3.0 54 | '@babel/code-frame': 7.23.5 55 | '@babel/generator': 7.23.6 56 | '@babel/helper-compilation-targets': 7.23.6 57 | '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) 58 | '@babel/helpers': 7.24.0 59 | '@babel/parser': 7.24.0 60 | '@babel/template': 7.24.0 61 | '@babel/traverse': 7.24.0 62 | '@babel/types': 7.24.0 63 | convert-source-map: 2.0.0 64 | debug: 4.3.4 65 | gensync: 1.0.0-beta.2 66 | json5: 2.2.3 67 | semver: 6.3.1 68 | transitivePeerDependencies: 69 | - supports-color 70 | dev: true 71 | 72 | /@babel/generator@7.23.6: 73 | resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} 74 | engines: {node: '>=6.9.0'} 75 | dependencies: 76 | '@babel/types': 7.24.0 77 | '@jridgewell/gen-mapping': 0.3.5 78 | '@jridgewell/trace-mapping': 0.3.25 79 | jsesc: 2.5.2 80 | dev: true 81 | 82 | /@babel/helper-compilation-targets@7.23.6: 83 | resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} 84 | engines: {node: '>=6.9.0'} 85 | dependencies: 86 | '@babel/compat-data': 7.23.5 87 | '@babel/helper-validator-option': 7.23.5 88 | browserslist: 4.23.0 89 | lru-cache: 5.1.1 90 | semver: 6.3.1 91 | dev: true 92 | 93 | /@babel/helper-environment-visitor@7.22.20: 94 | resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} 95 | engines: {node: '>=6.9.0'} 96 | dev: true 97 | 98 | /@babel/helper-function-name@7.23.0: 99 | resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} 100 | engines: {node: '>=6.9.0'} 101 | dependencies: 102 | '@babel/template': 7.24.0 103 | '@babel/types': 7.24.0 104 | dev: true 105 | 106 | /@babel/helper-hoist-variables@7.22.5: 107 | resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} 108 | engines: {node: '>=6.9.0'} 109 | dependencies: 110 | '@babel/types': 7.24.0 111 | dev: true 112 | 113 | /@babel/helper-module-imports@7.18.6: 114 | resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} 115 | engines: {node: '>=6.9.0'} 116 | dependencies: 117 | '@babel/types': 7.24.0 118 | dev: true 119 | 120 | /@babel/helper-module-imports@7.22.15: 121 | resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} 122 | engines: {node: '>=6.9.0'} 123 | dependencies: 124 | '@babel/types': 7.24.0 125 | dev: true 126 | 127 | /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.0): 128 | resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} 129 | engines: {node: '>=6.9.0'} 130 | peerDependencies: 131 | '@babel/core': ^7.0.0 132 | dependencies: 133 | '@babel/core': 7.24.0 134 | '@babel/helper-environment-visitor': 7.22.20 135 | '@babel/helper-module-imports': 7.22.15 136 | '@babel/helper-simple-access': 7.22.5 137 | '@babel/helper-split-export-declaration': 7.22.6 138 | '@babel/helper-validator-identifier': 7.22.20 139 | dev: true 140 | 141 | /@babel/helper-plugin-utils@7.24.0: 142 | resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==} 143 | engines: {node: '>=6.9.0'} 144 | dev: true 145 | 146 | /@babel/helper-simple-access@7.22.5: 147 | resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} 148 | engines: {node: '>=6.9.0'} 149 | dependencies: 150 | '@babel/types': 7.24.0 151 | dev: true 152 | 153 | /@babel/helper-split-export-declaration@7.22.6: 154 | resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} 155 | engines: {node: '>=6.9.0'} 156 | dependencies: 157 | '@babel/types': 7.24.0 158 | dev: true 159 | 160 | /@babel/helper-string-parser@7.23.4: 161 | resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} 162 | engines: {node: '>=6.9.0'} 163 | dev: true 164 | 165 | /@babel/helper-validator-identifier@7.22.20: 166 | resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} 167 | engines: {node: '>=6.9.0'} 168 | dev: true 169 | 170 | /@babel/helper-validator-option@7.23.5: 171 | resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} 172 | engines: {node: '>=6.9.0'} 173 | dev: true 174 | 175 | /@babel/helpers@7.24.0: 176 | resolution: {integrity: sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==} 177 | engines: {node: '>=6.9.0'} 178 | dependencies: 179 | '@babel/template': 7.24.0 180 | '@babel/traverse': 7.24.0 181 | '@babel/types': 7.24.0 182 | transitivePeerDependencies: 183 | - supports-color 184 | dev: true 185 | 186 | /@babel/highlight@7.23.4: 187 | resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} 188 | engines: {node: '>=6.9.0'} 189 | dependencies: 190 | '@babel/helper-validator-identifier': 7.22.20 191 | chalk: 2.4.2 192 | js-tokens: 4.0.0 193 | dev: true 194 | 195 | /@babel/parser@7.24.0: 196 | resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} 197 | engines: {node: '>=6.0.0'} 198 | hasBin: true 199 | dependencies: 200 | '@babel/types': 7.24.0 201 | dev: true 202 | 203 | /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.24.0): 204 | resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} 205 | engines: {node: '>=6.9.0'} 206 | peerDependencies: 207 | '@babel/core': ^7.0.0-0 208 | dependencies: 209 | '@babel/core': 7.24.0 210 | '@babel/helper-plugin-utils': 7.24.0 211 | dev: true 212 | 213 | /@babel/template@7.24.0: 214 | resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} 215 | engines: {node: '>=6.9.0'} 216 | dependencies: 217 | '@babel/code-frame': 7.23.5 218 | '@babel/parser': 7.24.0 219 | '@babel/types': 7.24.0 220 | dev: true 221 | 222 | /@babel/traverse@7.24.0: 223 | resolution: {integrity: sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==} 224 | engines: {node: '>=6.9.0'} 225 | dependencies: 226 | '@babel/code-frame': 7.23.5 227 | '@babel/generator': 7.23.6 228 | '@babel/helper-environment-visitor': 7.22.20 229 | '@babel/helper-function-name': 7.23.0 230 | '@babel/helper-hoist-variables': 7.22.5 231 | '@babel/helper-split-export-declaration': 7.22.6 232 | '@babel/parser': 7.24.0 233 | '@babel/types': 7.24.0 234 | debug: 4.3.4 235 | globals: 11.12.0 236 | transitivePeerDependencies: 237 | - supports-color 238 | dev: true 239 | 240 | /@babel/types@7.24.0: 241 | resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} 242 | engines: {node: '>=6.9.0'} 243 | dependencies: 244 | '@babel/helper-string-parser': 7.23.4 245 | '@babel/helper-validator-identifier': 7.22.20 246 | to-fast-properties: 2.0.0 247 | dev: true 248 | 249 | /@esbuild/aix-ppc64@0.19.12: 250 | resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} 251 | engines: {node: '>=12'} 252 | cpu: [ppc64] 253 | os: [aix] 254 | requiresBuild: true 255 | dev: true 256 | optional: true 257 | 258 | /@esbuild/android-arm64@0.19.12: 259 | resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} 260 | engines: {node: '>=12'} 261 | cpu: [arm64] 262 | os: [android] 263 | requiresBuild: true 264 | dev: true 265 | optional: true 266 | 267 | /@esbuild/android-arm@0.19.12: 268 | resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} 269 | engines: {node: '>=12'} 270 | cpu: [arm] 271 | os: [android] 272 | requiresBuild: true 273 | dev: true 274 | optional: true 275 | 276 | /@esbuild/android-x64@0.19.12: 277 | resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} 278 | engines: {node: '>=12'} 279 | cpu: [x64] 280 | os: [android] 281 | requiresBuild: true 282 | dev: true 283 | optional: true 284 | 285 | /@esbuild/darwin-arm64@0.19.12: 286 | resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} 287 | engines: {node: '>=12'} 288 | cpu: [arm64] 289 | os: [darwin] 290 | requiresBuild: true 291 | dev: true 292 | optional: true 293 | 294 | /@esbuild/darwin-x64@0.19.12: 295 | resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} 296 | engines: {node: '>=12'} 297 | cpu: [x64] 298 | os: [darwin] 299 | requiresBuild: true 300 | dev: true 301 | optional: true 302 | 303 | /@esbuild/freebsd-arm64@0.19.12: 304 | resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} 305 | engines: {node: '>=12'} 306 | cpu: [arm64] 307 | os: [freebsd] 308 | requiresBuild: true 309 | dev: true 310 | optional: true 311 | 312 | /@esbuild/freebsd-x64@0.19.12: 313 | resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} 314 | engines: {node: '>=12'} 315 | cpu: [x64] 316 | os: [freebsd] 317 | requiresBuild: true 318 | dev: true 319 | optional: true 320 | 321 | /@esbuild/linux-arm64@0.19.12: 322 | resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} 323 | engines: {node: '>=12'} 324 | cpu: [arm64] 325 | os: [linux] 326 | requiresBuild: true 327 | dev: true 328 | optional: true 329 | 330 | /@esbuild/linux-arm@0.19.12: 331 | resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} 332 | engines: {node: '>=12'} 333 | cpu: [arm] 334 | os: [linux] 335 | requiresBuild: true 336 | dev: true 337 | optional: true 338 | 339 | /@esbuild/linux-ia32@0.19.12: 340 | resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} 341 | engines: {node: '>=12'} 342 | cpu: [ia32] 343 | os: [linux] 344 | requiresBuild: true 345 | dev: true 346 | optional: true 347 | 348 | /@esbuild/linux-loong64@0.19.12: 349 | resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} 350 | engines: {node: '>=12'} 351 | cpu: [loong64] 352 | os: [linux] 353 | requiresBuild: true 354 | dev: true 355 | optional: true 356 | 357 | /@esbuild/linux-mips64el@0.19.12: 358 | resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} 359 | engines: {node: '>=12'} 360 | cpu: [mips64el] 361 | os: [linux] 362 | requiresBuild: true 363 | dev: true 364 | optional: true 365 | 366 | /@esbuild/linux-ppc64@0.19.12: 367 | resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} 368 | engines: {node: '>=12'} 369 | cpu: [ppc64] 370 | os: [linux] 371 | requiresBuild: true 372 | dev: true 373 | optional: true 374 | 375 | /@esbuild/linux-riscv64@0.19.12: 376 | resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} 377 | engines: {node: '>=12'} 378 | cpu: [riscv64] 379 | os: [linux] 380 | requiresBuild: true 381 | dev: true 382 | optional: true 383 | 384 | /@esbuild/linux-s390x@0.19.12: 385 | resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} 386 | engines: {node: '>=12'} 387 | cpu: [s390x] 388 | os: [linux] 389 | requiresBuild: true 390 | dev: true 391 | optional: true 392 | 393 | /@esbuild/linux-x64@0.19.12: 394 | resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} 395 | engines: {node: '>=12'} 396 | cpu: [x64] 397 | os: [linux] 398 | requiresBuild: true 399 | dev: true 400 | optional: true 401 | 402 | /@esbuild/netbsd-x64@0.19.12: 403 | resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} 404 | engines: {node: '>=12'} 405 | cpu: [x64] 406 | os: [netbsd] 407 | requiresBuild: true 408 | dev: true 409 | optional: true 410 | 411 | /@esbuild/openbsd-x64@0.19.12: 412 | resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} 413 | engines: {node: '>=12'} 414 | cpu: [x64] 415 | os: [openbsd] 416 | requiresBuild: true 417 | dev: true 418 | optional: true 419 | 420 | /@esbuild/sunos-x64@0.19.12: 421 | resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} 422 | engines: {node: '>=12'} 423 | cpu: [x64] 424 | os: [sunos] 425 | requiresBuild: true 426 | dev: true 427 | optional: true 428 | 429 | /@esbuild/win32-arm64@0.19.12: 430 | resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} 431 | engines: {node: '>=12'} 432 | cpu: [arm64] 433 | os: [win32] 434 | requiresBuild: true 435 | dev: true 436 | optional: true 437 | 438 | /@esbuild/win32-ia32@0.19.12: 439 | resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} 440 | engines: {node: '>=12'} 441 | cpu: [ia32] 442 | os: [win32] 443 | requiresBuild: true 444 | dev: true 445 | optional: true 446 | 447 | /@esbuild/win32-x64@0.19.12: 448 | resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} 449 | engines: {node: '>=12'} 450 | cpu: [x64] 451 | os: [win32] 452 | requiresBuild: true 453 | dev: true 454 | optional: true 455 | 456 | /@jridgewell/gen-mapping@0.3.5: 457 | resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 458 | engines: {node: '>=6.0.0'} 459 | dependencies: 460 | '@jridgewell/set-array': 1.2.1 461 | '@jridgewell/sourcemap-codec': 1.4.15 462 | '@jridgewell/trace-mapping': 0.3.25 463 | dev: true 464 | 465 | /@jridgewell/resolve-uri@3.1.2: 466 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 467 | engines: {node: '>=6.0.0'} 468 | dev: true 469 | 470 | /@jridgewell/set-array@1.2.1: 471 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 472 | engines: {node: '>=6.0.0'} 473 | dev: true 474 | 475 | /@jridgewell/sourcemap-codec@1.4.15: 476 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 477 | dev: true 478 | 479 | /@jridgewell/trace-mapping@0.3.25: 480 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 481 | dependencies: 482 | '@jridgewell/resolve-uri': 3.1.2 483 | '@jridgewell/sourcemap-codec': 1.4.15 484 | dev: true 485 | 486 | /@rollup/rollup-android-arm-eabi@4.12.0: 487 | resolution: {integrity: sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==} 488 | cpu: [arm] 489 | os: [android] 490 | requiresBuild: true 491 | dev: true 492 | optional: true 493 | 494 | /@rollup/rollup-android-arm64@4.12.0: 495 | resolution: {integrity: sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==} 496 | cpu: [arm64] 497 | os: [android] 498 | requiresBuild: true 499 | dev: true 500 | optional: true 501 | 502 | /@rollup/rollup-darwin-arm64@4.12.0: 503 | resolution: {integrity: sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==} 504 | cpu: [arm64] 505 | os: [darwin] 506 | requiresBuild: true 507 | dev: true 508 | optional: true 509 | 510 | /@rollup/rollup-darwin-x64@4.12.0: 511 | resolution: {integrity: sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==} 512 | cpu: [x64] 513 | os: [darwin] 514 | requiresBuild: true 515 | dev: true 516 | optional: true 517 | 518 | /@rollup/rollup-linux-arm-gnueabihf@4.12.0: 519 | resolution: {integrity: sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==} 520 | cpu: [arm] 521 | os: [linux] 522 | requiresBuild: true 523 | dev: true 524 | optional: true 525 | 526 | /@rollup/rollup-linux-arm64-gnu@4.12.0: 527 | resolution: {integrity: sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==} 528 | cpu: [arm64] 529 | os: [linux] 530 | requiresBuild: true 531 | dev: true 532 | optional: true 533 | 534 | /@rollup/rollup-linux-arm64-musl@4.12.0: 535 | resolution: {integrity: sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==} 536 | cpu: [arm64] 537 | os: [linux] 538 | requiresBuild: true 539 | dev: true 540 | optional: true 541 | 542 | /@rollup/rollup-linux-riscv64-gnu@4.12.0: 543 | resolution: {integrity: sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==} 544 | cpu: [riscv64] 545 | os: [linux] 546 | requiresBuild: true 547 | dev: true 548 | optional: true 549 | 550 | /@rollup/rollup-linux-x64-gnu@4.12.0: 551 | resolution: {integrity: sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==} 552 | cpu: [x64] 553 | os: [linux] 554 | requiresBuild: true 555 | dev: true 556 | optional: true 557 | 558 | /@rollup/rollup-linux-x64-musl@4.12.0: 559 | resolution: {integrity: sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==} 560 | cpu: [x64] 561 | os: [linux] 562 | requiresBuild: true 563 | dev: true 564 | optional: true 565 | 566 | /@rollup/rollup-win32-arm64-msvc@4.12.0: 567 | resolution: {integrity: sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==} 568 | cpu: [arm64] 569 | os: [win32] 570 | requiresBuild: true 571 | dev: true 572 | optional: true 573 | 574 | /@rollup/rollup-win32-ia32-msvc@4.12.0: 575 | resolution: {integrity: sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==} 576 | cpu: [ia32] 577 | os: [win32] 578 | requiresBuild: true 579 | dev: true 580 | optional: true 581 | 582 | /@rollup/rollup-win32-x64-msvc@4.12.0: 583 | resolution: {integrity: sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==} 584 | cpu: [x64] 585 | os: [win32] 586 | requiresBuild: true 587 | dev: true 588 | optional: true 589 | 590 | /@types/babel__core@7.20.5: 591 | resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 592 | dependencies: 593 | '@babel/parser': 7.24.0 594 | '@babel/types': 7.24.0 595 | '@types/babel__generator': 7.6.8 596 | '@types/babel__template': 7.4.4 597 | '@types/babel__traverse': 7.20.5 598 | dev: true 599 | 600 | /@types/babel__generator@7.6.8: 601 | resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} 602 | dependencies: 603 | '@babel/types': 7.24.0 604 | dev: true 605 | 606 | /@types/babel__template@7.4.4: 607 | resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 608 | dependencies: 609 | '@babel/parser': 7.24.0 610 | '@babel/types': 7.24.0 611 | dev: true 612 | 613 | /@types/babel__traverse@7.20.5: 614 | resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} 615 | dependencies: 616 | '@babel/types': 7.24.0 617 | dev: true 618 | 619 | /@types/estree@1.0.5: 620 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 621 | dev: true 622 | 623 | /@types/node@20.11.24: 624 | resolution: {integrity: sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==} 625 | dependencies: 626 | undici-types: 5.26.5 627 | dev: true 628 | 629 | /ansi-styles@3.2.1: 630 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 631 | engines: {node: '>=4'} 632 | dependencies: 633 | color-convert: 1.9.3 634 | dev: true 635 | 636 | /babel-plugin-jsx-dom-expressions@0.37.17(@babel/core@7.24.0): 637 | resolution: {integrity: sha512-1bv8rOTzs6TR3DVyVZ7ElxyPEhnS556FMWRIsB3gBPfkn/cSKaLvXLGk+X1lvI+SzcUo4G+UcmJrn3vr1ig8mQ==} 638 | peerDependencies: 639 | '@babel/core': ^7.20.12 640 | dependencies: 641 | '@babel/core': 7.24.0 642 | '@babel/helper-module-imports': 7.18.6 643 | '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.24.0) 644 | '@babel/types': 7.24.0 645 | html-entities: 2.3.3 646 | validate-html-nesting: 1.2.2 647 | dev: true 648 | 649 | /babel-preset-solid@1.8.15(@babel/core@7.24.0): 650 | resolution: {integrity: sha512-P2yOQbB7Hn/m4YvpXV6ExHIMcgNWXWXcvY4kJzG3yqAB3hKS58OZRsvJ7RObsZWqXRvZTITBIwnpK0BMGu+ZIQ==} 651 | peerDependencies: 652 | '@babel/core': ^7.0.0 653 | dependencies: 654 | '@babel/core': 7.24.0 655 | babel-plugin-jsx-dom-expressions: 0.37.17(@babel/core@7.24.0) 656 | dev: true 657 | 658 | /browserslist@4.23.0: 659 | resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} 660 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 661 | hasBin: true 662 | dependencies: 663 | caniuse-lite: 1.0.30001594 664 | electron-to-chromium: 1.4.692 665 | node-releases: 2.0.14 666 | update-browserslist-db: 1.0.13(browserslist@4.23.0) 667 | dev: true 668 | 669 | /caniuse-lite@1.0.30001594: 670 | resolution: {integrity: sha512-VblSX6nYqyJVs8DKFMldE2IVCJjZ225LW00ydtUWwh5hk9IfkTOffO6r8gJNsH0qqqeAF8KrbMYA2VEwTlGW5g==} 671 | dev: true 672 | 673 | /chalk@2.4.2: 674 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 675 | engines: {node: '>=4'} 676 | dependencies: 677 | ansi-styles: 3.2.1 678 | escape-string-regexp: 1.0.5 679 | supports-color: 5.5.0 680 | dev: true 681 | 682 | /color-convert@1.9.3: 683 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 684 | dependencies: 685 | color-name: 1.1.3 686 | dev: true 687 | 688 | /color-name@1.1.3: 689 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 690 | dev: true 691 | 692 | /convert-source-map@2.0.0: 693 | resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 694 | dev: true 695 | 696 | /csstype@3.1.3: 697 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 698 | 699 | /debug@4.3.4: 700 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 701 | engines: {node: '>=6.0'} 702 | peerDependencies: 703 | supports-color: '*' 704 | peerDependenciesMeta: 705 | supports-color: 706 | optional: true 707 | dependencies: 708 | ms: 2.1.2 709 | dev: true 710 | 711 | /electron-to-chromium@1.4.692: 712 | resolution: {integrity: sha512-d5rZRka9n2Y3MkWRN74IoAsxR0HK3yaAt7T50e3iT9VZmCCQDT3geXUO5ZRMhDToa1pkCeQXuNo+0g+NfDOVPA==} 713 | dev: true 714 | 715 | /esbuild@0.19.12: 716 | resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} 717 | engines: {node: '>=12'} 718 | hasBin: true 719 | requiresBuild: true 720 | optionalDependencies: 721 | '@esbuild/aix-ppc64': 0.19.12 722 | '@esbuild/android-arm': 0.19.12 723 | '@esbuild/android-arm64': 0.19.12 724 | '@esbuild/android-x64': 0.19.12 725 | '@esbuild/darwin-arm64': 0.19.12 726 | '@esbuild/darwin-x64': 0.19.12 727 | '@esbuild/freebsd-arm64': 0.19.12 728 | '@esbuild/freebsd-x64': 0.19.12 729 | '@esbuild/linux-arm': 0.19.12 730 | '@esbuild/linux-arm64': 0.19.12 731 | '@esbuild/linux-ia32': 0.19.12 732 | '@esbuild/linux-loong64': 0.19.12 733 | '@esbuild/linux-mips64el': 0.19.12 734 | '@esbuild/linux-ppc64': 0.19.12 735 | '@esbuild/linux-riscv64': 0.19.12 736 | '@esbuild/linux-s390x': 0.19.12 737 | '@esbuild/linux-x64': 0.19.12 738 | '@esbuild/netbsd-x64': 0.19.12 739 | '@esbuild/openbsd-x64': 0.19.12 740 | '@esbuild/sunos-x64': 0.19.12 741 | '@esbuild/win32-arm64': 0.19.12 742 | '@esbuild/win32-ia32': 0.19.12 743 | '@esbuild/win32-x64': 0.19.12 744 | dev: true 745 | 746 | /escalade@3.1.2: 747 | resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} 748 | engines: {node: '>=6'} 749 | dev: true 750 | 751 | /escape-string-regexp@1.0.5: 752 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 753 | engines: {node: '>=0.8.0'} 754 | dev: true 755 | 756 | /fsevents@2.3.3: 757 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 758 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 759 | os: [darwin] 760 | requiresBuild: true 761 | dev: true 762 | optional: true 763 | 764 | /gensync@1.0.0-beta.2: 765 | resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 766 | engines: {node: '>=6.9.0'} 767 | dev: true 768 | 769 | /globals@11.12.0: 770 | resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 771 | engines: {node: '>=4'} 772 | dev: true 773 | 774 | /has-flag@3.0.0: 775 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 776 | engines: {node: '>=4'} 777 | dev: true 778 | 779 | /html-entities@2.3.3: 780 | resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 781 | dev: true 782 | 783 | /is-what@4.1.16: 784 | resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 785 | engines: {node: '>=12.13'} 786 | dev: true 787 | 788 | /js-tokens@4.0.0: 789 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 790 | dev: true 791 | 792 | /jsesc@2.5.2: 793 | resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} 794 | engines: {node: '>=4'} 795 | hasBin: true 796 | dev: true 797 | 798 | /json5@2.2.3: 799 | resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 800 | engines: {node: '>=6'} 801 | hasBin: true 802 | dev: true 803 | 804 | /lru-cache@5.1.1: 805 | resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 806 | dependencies: 807 | yallist: 3.1.1 808 | dev: true 809 | 810 | /merge-anything@5.1.7: 811 | resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 812 | engines: {node: '>=12.13'} 813 | dependencies: 814 | is-what: 4.1.16 815 | dev: true 816 | 817 | /ms@2.1.2: 818 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 819 | dev: true 820 | 821 | /nanoid@3.3.7: 822 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 823 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 824 | hasBin: true 825 | dev: true 826 | 827 | /node-releases@2.0.14: 828 | resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} 829 | dev: true 830 | 831 | /picocolors@1.0.0: 832 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 833 | dev: true 834 | 835 | /postcss@8.4.35: 836 | resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} 837 | engines: {node: ^10 || ^12 || >=14} 838 | dependencies: 839 | nanoid: 3.3.7 840 | picocolors: 1.0.0 841 | source-map-js: 1.0.2 842 | dev: true 843 | 844 | /rollup@4.12.0: 845 | resolution: {integrity: sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==} 846 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 847 | hasBin: true 848 | dependencies: 849 | '@types/estree': 1.0.5 850 | optionalDependencies: 851 | '@rollup/rollup-android-arm-eabi': 4.12.0 852 | '@rollup/rollup-android-arm64': 4.12.0 853 | '@rollup/rollup-darwin-arm64': 4.12.0 854 | '@rollup/rollup-darwin-x64': 4.12.0 855 | '@rollup/rollup-linux-arm-gnueabihf': 4.12.0 856 | '@rollup/rollup-linux-arm64-gnu': 4.12.0 857 | '@rollup/rollup-linux-arm64-musl': 4.12.0 858 | '@rollup/rollup-linux-riscv64-gnu': 4.12.0 859 | '@rollup/rollup-linux-x64-gnu': 4.12.0 860 | '@rollup/rollup-linux-x64-musl': 4.12.0 861 | '@rollup/rollup-win32-arm64-msvc': 4.12.0 862 | '@rollup/rollup-win32-ia32-msvc': 4.12.0 863 | '@rollup/rollup-win32-x64-msvc': 4.12.0 864 | fsevents: 2.3.3 865 | dev: true 866 | 867 | /semver@6.3.1: 868 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 869 | hasBin: true 870 | dev: true 871 | 872 | /seroval-plugins@1.0.4(seroval@1.0.4): 873 | resolution: {integrity: sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==} 874 | engines: {node: '>=10'} 875 | peerDependencies: 876 | seroval: ^1.0 877 | dependencies: 878 | seroval: 1.0.4 879 | 880 | /seroval@1.0.4: 881 | resolution: {integrity: sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg==} 882 | engines: {node: '>=10'} 883 | 884 | /solid-js@1.8.15: 885 | resolution: {integrity: sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==} 886 | dependencies: 887 | csstype: 3.1.3 888 | seroval: 1.0.4 889 | seroval-plugins: 1.0.4(seroval@1.0.4) 890 | 891 | /solid-refresh@0.6.3(solid-js@1.8.15): 892 | resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} 893 | peerDependencies: 894 | solid-js: ^1.3 895 | dependencies: 896 | '@babel/generator': 7.23.6 897 | '@babel/helper-module-imports': 7.22.15 898 | '@babel/types': 7.24.0 899 | solid-js: 1.8.15 900 | dev: true 901 | 902 | /source-map-js@1.0.2: 903 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 904 | engines: {node: '>=0.10.0'} 905 | dev: true 906 | 907 | /supports-color@5.5.0: 908 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 909 | engines: {node: '>=4'} 910 | dependencies: 911 | has-flag: 3.0.0 912 | dev: true 913 | 914 | /to-fast-properties@2.0.0: 915 | resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 916 | engines: {node: '>=4'} 917 | dev: true 918 | 919 | /typescript@5.3.3: 920 | resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} 921 | engines: {node: '>=14.17'} 922 | hasBin: true 923 | dev: true 924 | 925 | /undici-types@5.26.5: 926 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 927 | dev: true 928 | 929 | /update-browserslist-db@1.0.13(browserslist@4.23.0): 930 | resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} 931 | hasBin: true 932 | peerDependencies: 933 | browserslist: '>= 4.21.0' 934 | dependencies: 935 | browserslist: 4.23.0 936 | escalade: 3.1.2 937 | picocolors: 1.0.0 938 | dev: true 939 | 940 | /validate-html-nesting@1.2.2: 941 | resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} 942 | dev: true 943 | 944 | /vite-plugin-solid@2.10.1(solid-js@1.8.15)(vite@5.1.5): 945 | resolution: {integrity: sha512-kfVdNLWaJqaJVL52U6iCCKNW/nXE7bS1VVGOWPGllOkJfcNILymVSY0LCBLSnyy0iYnRtrXpiHm14rMuzeC7CA==} 946 | peerDependencies: 947 | '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* 948 | solid-js: ^1.7.2 949 | vite: ^3.0.0 || ^4.0.0 || ^5.0.0 950 | peerDependenciesMeta: 951 | '@testing-library/jest-dom': 952 | optional: true 953 | dependencies: 954 | '@babel/core': 7.24.0 955 | '@types/babel__core': 7.20.5 956 | babel-preset-solid: 1.8.15(@babel/core@7.24.0) 957 | merge-anything: 5.1.7 958 | solid-js: 1.8.15 959 | solid-refresh: 0.6.3(solid-js@1.8.15) 960 | vite: 5.1.5(@types/node@20.11.24) 961 | vitefu: 0.2.5(vite@5.1.5) 962 | transitivePeerDependencies: 963 | - supports-color 964 | dev: true 965 | 966 | /vite@5.1.5(@types/node@20.11.24): 967 | resolution: {integrity: sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==} 968 | engines: {node: ^18.0.0 || >=20.0.0} 969 | hasBin: true 970 | peerDependencies: 971 | '@types/node': ^18.0.0 || >=20.0.0 972 | less: '*' 973 | lightningcss: ^1.21.0 974 | sass: '*' 975 | stylus: '*' 976 | sugarss: '*' 977 | terser: ^5.4.0 978 | peerDependenciesMeta: 979 | '@types/node': 980 | optional: true 981 | less: 982 | optional: true 983 | lightningcss: 984 | optional: true 985 | sass: 986 | optional: true 987 | stylus: 988 | optional: true 989 | sugarss: 990 | optional: true 991 | terser: 992 | optional: true 993 | dependencies: 994 | '@types/node': 20.11.24 995 | esbuild: 0.19.12 996 | postcss: 8.4.35 997 | rollup: 4.12.0 998 | optionalDependencies: 999 | fsevents: 2.3.3 1000 | dev: true 1001 | 1002 | /vitefu@0.2.5(vite@5.1.5): 1003 | resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} 1004 | peerDependencies: 1005 | vite: ^3.0.0 || ^4.0.0 || ^5.0.0 1006 | peerDependenciesMeta: 1007 | vite: 1008 | optional: true 1009 | dependencies: 1010 | vite: 5.1.5(@types/node@20.11.24) 1011 | dev: true 1012 | 1013 | /yallist@3.1.1: 1014 | resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1015 | dev: true 1016 | -------------------------------------------------------------------------------- /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 = "anstream" 22 | version = "0.6.13" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "utf8parse", 32 | ] 33 | 34 | [[package]] 35 | name = "anstyle" 36 | version = "1.0.6" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 39 | 40 | [[package]] 41 | name = "anstyle-parse" 42 | version = "0.2.3" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 45 | dependencies = [ 46 | "utf8parse", 47 | ] 48 | 49 | [[package]] 50 | name = "anstyle-query" 51 | version = "1.0.2" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 54 | dependencies = [ 55 | "windows-sys 0.52.0", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle-wincon" 60 | version = "3.0.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 63 | dependencies = [ 64 | "anstyle", 65 | "windows-sys 0.52.0", 66 | ] 67 | 68 | [[package]] 69 | name = "async-trait" 70 | version = "0.1.77" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" 73 | dependencies = [ 74 | "proc-macro2", 75 | "quote", 76 | "syn", 77 | ] 78 | 79 | [[package]] 80 | name = "autocfg" 81 | version = "1.1.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 84 | 85 | [[package]] 86 | name = "axum" 87 | version = "0.7.4" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" 90 | dependencies = [ 91 | "async-trait", 92 | "axum-core", 93 | "axum-macros", 94 | "base64", 95 | "bytes", 96 | "futures-util", 97 | "http", 98 | "http-body", 99 | "http-body-util", 100 | "hyper", 101 | "hyper-util", 102 | "itoa", 103 | "matchit", 104 | "memchr", 105 | "mime", 106 | "percent-encoding", 107 | "pin-project-lite", 108 | "rustversion", 109 | "serde", 110 | "serde_urlencoded", 111 | "sha1", 112 | "sync_wrapper", 113 | "tokio", 114 | "tokio-tungstenite", 115 | "tower", 116 | "tower-layer", 117 | "tower-service", 118 | ] 119 | 120 | [[package]] 121 | name = "axum-core" 122 | version = "0.4.3" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" 125 | dependencies = [ 126 | "async-trait", 127 | "bytes", 128 | "futures-util", 129 | "http", 130 | "http-body", 131 | "http-body-util", 132 | "mime", 133 | "pin-project-lite", 134 | "rustversion", 135 | "sync_wrapper", 136 | "tower-layer", 137 | "tower-service", 138 | ] 139 | 140 | [[package]] 141 | name = "axum-macros" 142 | version = "0.4.1" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" 145 | dependencies = [ 146 | "heck", 147 | "proc-macro2", 148 | "quote", 149 | "syn", 150 | ] 151 | 152 | [[package]] 153 | name = "backtrace" 154 | version = "0.3.69" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 157 | dependencies = [ 158 | "addr2line", 159 | "cc", 160 | "cfg-if", 161 | "libc", 162 | "miniz_oxide", 163 | "object", 164 | "rustc-demangle", 165 | ] 166 | 167 | [[package]] 168 | name = "base64" 169 | version = "0.21.7" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 172 | 173 | [[package]] 174 | name = "bitflags" 175 | version = "2.4.2" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 178 | 179 | [[package]] 180 | name = "block-buffer" 181 | version = "0.10.4" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 184 | dependencies = [ 185 | "generic-array", 186 | ] 187 | 188 | [[package]] 189 | name = "byteorder" 190 | version = "1.5.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 193 | 194 | [[package]] 195 | name = "bytes" 196 | version = "1.5.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 199 | 200 | [[package]] 201 | name = "cc" 202 | version = "1.0.90" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" 205 | 206 | [[package]] 207 | name = "cfg-if" 208 | version = "1.0.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 211 | 212 | [[package]] 213 | name = "clap" 214 | version = "4.5.2" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" 217 | dependencies = [ 218 | "clap_builder", 219 | "clap_derive", 220 | ] 221 | 222 | [[package]] 223 | name = "clap_builder" 224 | version = "4.5.2" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 227 | dependencies = [ 228 | "anstream", 229 | "anstyle", 230 | "clap_lex", 231 | "strsim", 232 | ] 233 | 234 | [[package]] 235 | name = "clap_derive" 236 | version = "4.5.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" 239 | dependencies = [ 240 | "heck", 241 | "proc-macro2", 242 | "quote", 243 | "syn", 244 | ] 245 | 246 | [[package]] 247 | name = "clap_lex" 248 | version = "0.7.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 251 | 252 | [[package]] 253 | name = "colorchoice" 254 | version = "1.0.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 257 | 258 | [[package]] 259 | name = "cpufeatures" 260 | version = "0.2.12" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 263 | dependencies = [ 264 | "libc", 265 | ] 266 | 267 | [[package]] 268 | name = "crossbeam" 269 | version = "0.8.4" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" 272 | dependencies = [ 273 | "crossbeam-channel", 274 | "crossbeam-deque", 275 | "crossbeam-epoch", 276 | "crossbeam-queue", 277 | "crossbeam-utils", 278 | ] 279 | 280 | [[package]] 281 | name = "crossbeam-channel" 282 | version = "0.5.12" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" 285 | dependencies = [ 286 | "crossbeam-utils", 287 | ] 288 | 289 | [[package]] 290 | name = "crossbeam-deque" 291 | version = "0.8.5" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 294 | dependencies = [ 295 | "crossbeam-epoch", 296 | "crossbeam-utils", 297 | ] 298 | 299 | [[package]] 300 | name = "crossbeam-epoch" 301 | version = "0.9.18" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 304 | dependencies = [ 305 | "crossbeam-utils", 306 | ] 307 | 308 | [[package]] 309 | name = "crossbeam-queue" 310 | version = "0.3.11" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" 313 | dependencies = [ 314 | "crossbeam-utils", 315 | ] 316 | 317 | [[package]] 318 | name = "crossbeam-utils" 319 | version = "0.8.19" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 322 | 323 | [[package]] 324 | name = "crypto-common" 325 | version = "0.1.6" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 328 | dependencies = [ 329 | "generic-array", 330 | "typenum", 331 | ] 332 | 333 | [[package]] 334 | name = "data-encoding" 335 | version = "2.5.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" 338 | 339 | [[package]] 340 | name = "digest" 341 | version = "0.10.7" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 344 | dependencies = [ 345 | "block-buffer", 346 | "crypto-common", 347 | ] 348 | 349 | [[package]] 350 | name = "either" 351 | version = "1.10.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 354 | 355 | [[package]] 356 | name = "equivalent" 357 | version = "1.0.1" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 360 | 361 | [[package]] 362 | name = "fnv" 363 | version = "1.0.7" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 366 | 367 | [[package]] 368 | name = "form_urlencoded" 369 | version = "1.2.1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 372 | dependencies = [ 373 | "percent-encoding", 374 | ] 375 | 376 | [[package]] 377 | name = "futures-channel" 378 | version = "0.3.30" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 381 | dependencies = [ 382 | "futures-core", 383 | ] 384 | 385 | [[package]] 386 | name = "futures-core" 387 | version = "0.3.30" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 390 | 391 | [[package]] 392 | name = "futures-sink" 393 | version = "0.3.30" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 396 | 397 | [[package]] 398 | name = "futures-task" 399 | version = "0.3.30" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 402 | 403 | [[package]] 404 | name = "futures-util" 405 | version = "0.3.30" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 408 | dependencies = [ 409 | "futures-core", 410 | "futures-sink", 411 | "futures-task", 412 | "pin-project-lite", 413 | "pin-utils", 414 | "slab", 415 | ] 416 | 417 | [[package]] 418 | name = "generic-array" 419 | version = "0.14.7" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 422 | dependencies = [ 423 | "typenum", 424 | "version_check", 425 | ] 426 | 427 | [[package]] 428 | name = "getrandom" 429 | version = "0.2.12" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 432 | dependencies = [ 433 | "cfg-if", 434 | "libc", 435 | "wasi", 436 | ] 437 | 438 | [[package]] 439 | name = "gimli" 440 | version = "0.28.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 443 | 444 | [[package]] 445 | name = "h2" 446 | version = "0.4.2" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" 449 | dependencies = [ 450 | "bytes", 451 | "fnv", 452 | "futures-core", 453 | "futures-sink", 454 | "futures-util", 455 | "http", 456 | "indexmap", 457 | "slab", 458 | "tokio", 459 | "tokio-util", 460 | "tracing", 461 | ] 462 | 463 | [[package]] 464 | name = "hashbrown" 465 | version = "0.14.3" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 468 | 469 | [[package]] 470 | name = "heck" 471 | version = "0.4.1" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 474 | 475 | [[package]] 476 | name = "hermit-abi" 477 | version = "0.3.9" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 480 | 481 | [[package]] 482 | name = "http" 483 | version = "1.1.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 486 | dependencies = [ 487 | "bytes", 488 | "fnv", 489 | "itoa", 490 | ] 491 | 492 | [[package]] 493 | name = "http-body" 494 | version = "1.0.0" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" 497 | dependencies = [ 498 | "bytes", 499 | "http", 500 | ] 501 | 502 | [[package]] 503 | name = "http-body-util" 504 | version = "0.1.1" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" 507 | dependencies = [ 508 | "bytes", 509 | "futures-core", 510 | "http", 511 | "http-body", 512 | "pin-project-lite", 513 | ] 514 | 515 | [[package]] 516 | name = "httparse" 517 | version = "1.8.0" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 520 | 521 | [[package]] 522 | name = "httpdate" 523 | version = "1.0.3" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 526 | 527 | [[package]] 528 | name = "hyper" 529 | version = "1.2.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" 532 | dependencies = [ 533 | "bytes", 534 | "futures-channel", 535 | "futures-util", 536 | "h2", 537 | "http", 538 | "http-body", 539 | "httparse", 540 | "httpdate", 541 | "itoa", 542 | "pin-project-lite", 543 | "smallvec", 544 | "tokio", 545 | ] 546 | 547 | [[package]] 548 | name = "hyper-util" 549 | version = "0.1.3" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" 552 | dependencies = [ 553 | "bytes", 554 | "futures-util", 555 | "http", 556 | "http-body", 557 | "hyper", 558 | "pin-project-lite", 559 | "socket2", 560 | "tokio", 561 | ] 562 | 563 | [[package]] 564 | name = "idna" 565 | version = "0.5.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 568 | dependencies = [ 569 | "unicode-bidi", 570 | "unicode-normalization", 571 | ] 572 | 573 | [[package]] 574 | name = "indexmap" 575 | version = "2.2.5" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" 578 | dependencies = [ 579 | "equivalent", 580 | "hashbrown", 581 | ] 582 | 583 | [[package]] 584 | name = "is-root" 585 | version = "0.1.3" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "9df98242b01855f02bede53e1c2c90411af31b71d744936272cb21ab3a77ee9e" 588 | dependencies = [ 589 | "users", 590 | "winapi", 591 | ] 592 | 593 | [[package]] 594 | name = "itoa" 595 | version = "1.0.10" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 598 | 599 | [[package]] 600 | name = "jwalk" 601 | version = "0.8.1" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" 604 | dependencies = [ 605 | "crossbeam", 606 | "rayon", 607 | ] 608 | 609 | [[package]] 610 | name = "libc" 611 | version = "0.2.153" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 614 | 615 | [[package]] 616 | name = "log" 617 | version = "0.4.21" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 620 | 621 | [[package]] 622 | name = "matchit" 623 | version = "0.7.3" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 626 | 627 | [[package]] 628 | name = "memchr" 629 | version = "2.7.1" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 632 | 633 | [[package]] 634 | name = "mime" 635 | version = "0.3.17" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 638 | 639 | [[package]] 640 | name = "mime_guess" 641 | version = "2.0.4" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 644 | dependencies = [ 645 | "mime", 646 | "unicase", 647 | ] 648 | 649 | [[package]] 650 | name = "miniz_oxide" 651 | version = "0.7.2" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 654 | dependencies = [ 655 | "adler", 656 | ] 657 | 658 | [[package]] 659 | name = "mio" 660 | version = "0.8.11" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 663 | dependencies = [ 664 | "libc", 665 | "wasi", 666 | "windows-sys 0.48.0", 667 | ] 668 | 669 | [[package]] 670 | name = "num_cpus" 671 | version = "1.16.0" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 674 | dependencies = [ 675 | "hermit-abi", 676 | "libc", 677 | ] 678 | 679 | [[package]] 680 | name = "object" 681 | version = "0.32.2" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 684 | dependencies = [ 685 | "memchr", 686 | ] 687 | 688 | [[package]] 689 | name = "once_cell" 690 | version = "1.19.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 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.5" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 705 | dependencies = [ 706 | "pin-project-internal", 707 | ] 708 | 709 | [[package]] 710 | name = "pin-project-internal" 711 | version = "1.1.5" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 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 = "ppv-lite86" 734 | version = "0.2.17" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 737 | 738 | [[package]] 739 | name = "proc-macro2" 740 | version = "1.0.79" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 743 | dependencies = [ 744 | "unicode-ident", 745 | ] 746 | 747 | [[package]] 748 | name = "quote" 749 | version = "1.0.35" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 752 | dependencies = [ 753 | "proc-macro2", 754 | ] 755 | 756 | [[package]] 757 | name = "radio" 758 | version = "0.13.0" 759 | dependencies = [ 760 | "async-trait", 761 | "axum", 762 | "clap", 763 | "futures-core", 764 | "is-root", 765 | "jwalk", 766 | "mime_guess", 767 | "rand", 768 | "rayon", 769 | "rust-embed", 770 | "serde", 771 | "serde_json", 772 | "tokio", 773 | "tokio-stream", 774 | "toml", 775 | "tower-http", 776 | "windirs", 777 | ] 778 | 779 | [[package]] 780 | name = "rand" 781 | version = "0.8.5" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 784 | dependencies = [ 785 | "libc", 786 | "rand_chacha", 787 | "rand_core", 788 | ] 789 | 790 | [[package]] 791 | name = "rand_chacha" 792 | version = "0.3.1" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 795 | dependencies = [ 796 | "ppv-lite86", 797 | "rand_core", 798 | ] 799 | 800 | [[package]] 801 | name = "rand_core" 802 | version = "0.6.4" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 805 | dependencies = [ 806 | "getrandom", 807 | ] 808 | 809 | [[package]] 810 | name = "rayon" 811 | version = "1.9.0" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" 814 | dependencies = [ 815 | "either", 816 | "rayon-core", 817 | ] 818 | 819 | [[package]] 820 | name = "rayon-core" 821 | version = "1.12.1" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 824 | dependencies = [ 825 | "crossbeam-deque", 826 | "crossbeam-utils", 827 | ] 828 | 829 | [[package]] 830 | name = "rust-embed" 831 | version = "8.3.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745" 834 | dependencies = [ 835 | "axum", 836 | "rust-embed-impl", 837 | "rust-embed-utils", 838 | "walkdir", 839 | ] 840 | 841 | [[package]] 842 | name = "rust-embed-impl" 843 | version = "8.3.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" 846 | dependencies = [ 847 | "proc-macro2", 848 | "quote", 849 | "rust-embed-utils", 850 | "syn", 851 | "walkdir", 852 | ] 853 | 854 | [[package]] 855 | name = "rust-embed-utils" 856 | version = "8.3.0" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581" 859 | dependencies = [ 860 | "sha2", 861 | "walkdir", 862 | ] 863 | 864 | [[package]] 865 | name = "rustc-demangle" 866 | version = "0.1.23" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 869 | 870 | [[package]] 871 | name = "rustversion" 872 | version = "1.0.14" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 875 | 876 | [[package]] 877 | name = "ryu" 878 | version = "1.0.17" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 881 | 882 | [[package]] 883 | name = "same-file" 884 | version = "1.0.6" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 887 | dependencies = [ 888 | "winapi-util", 889 | ] 890 | 891 | [[package]] 892 | name = "serde" 893 | version = "1.0.197" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 896 | dependencies = [ 897 | "serde_derive", 898 | ] 899 | 900 | [[package]] 901 | name = "serde_derive" 902 | version = "1.0.197" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 905 | dependencies = [ 906 | "proc-macro2", 907 | "quote", 908 | "syn", 909 | ] 910 | 911 | [[package]] 912 | name = "serde_json" 913 | version = "1.0.114" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 916 | dependencies = [ 917 | "itoa", 918 | "ryu", 919 | "serde", 920 | ] 921 | 922 | [[package]] 923 | name = "serde_spanned" 924 | version = "0.6.5" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 927 | dependencies = [ 928 | "serde", 929 | ] 930 | 931 | [[package]] 932 | name = "serde_urlencoded" 933 | version = "0.7.1" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 936 | dependencies = [ 937 | "form_urlencoded", 938 | "itoa", 939 | "ryu", 940 | "serde", 941 | ] 942 | 943 | [[package]] 944 | name = "sha1" 945 | version = "0.10.6" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 948 | dependencies = [ 949 | "cfg-if", 950 | "cpufeatures", 951 | "digest", 952 | ] 953 | 954 | [[package]] 955 | name = "sha2" 956 | version = "0.10.8" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 959 | dependencies = [ 960 | "cfg-if", 961 | "cpufeatures", 962 | "digest", 963 | ] 964 | 965 | [[package]] 966 | name = "signal-hook-registry" 967 | version = "1.4.1" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 970 | dependencies = [ 971 | "libc", 972 | ] 973 | 974 | [[package]] 975 | name = "slab" 976 | version = "0.4.9" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 979 | dependencies = [ 980 | "autocfg", 981 | ] 982 | 983 | [[package]] 984 | name = "smallvec" 985 | version = "1.13.1" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 988 | 989 | [[package]] 990 | name = "socket2" 991 | version = "0.5.6" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" 994 | dependencies = [ 995 | "libc", 996 | "windows-sys 0.52.0", 997 | ] 998 | 999 | [[package]] 1000 | name = "strsim" 1001 | version = "0.11.0" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 1004 | 1005 | [[package]] 1006 | name = "syn" 1007 | version = "2.0.52" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" 1010 | dependencies = [ 1011 | "proc-macro2", 1012 | "quote", 1013 | "unicode-ident", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "sync_wrapper" 1018 | version = "0.1.2" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1021 | 1022 | [[package]] 1023 | name = "thiserror" 1024 | version = "1.0.58" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" 1027 | dependencies = [ 1028 | "thiserror-impl", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "thiserror-impl" 1033 | version = "1.0.58" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" 1036 | dependencies = [ 1037 | "proc-macro2", 1038 | "quote", 1039 | "syn", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "tinyvec" 1044 | version = "1.6.0" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1047 | dependencies = [ 1048 | "tinyvec_macros", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "tinyvec_macros" 1053 | version = "0.1.1" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1056 | 1057 | [[package]] 1058 | name = "tokio" 1059 | version = "1.36.0" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" 1062 | dependencies = [ 1063 | "backtrace", 1064 | "bytes", 1065 | "libc", 1066 | "mio", 1067 | "num_cpus", 1068 | "pin-project-lite", 1069 | "signal-hook-registry", 1070 | "socket2", 1071 | "tokio-macros", 1072 | "windows-sys 0.48.0", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "tokio-macros" 1077 | version = "2.2.0" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 1080 | dependencies = [ 1081 | "proc-macro2", 1082 | "quote", 1083 | "syn", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "tokio-stream" 1088 | version = "0.1.14" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" 1091 | dependencies = [ 1092 | "futures-core", 1093 | "pin-project-lite", 1094 | "tokio", 1095 | "tokio-util", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "tokio-tungstenite" 1100 | version = "0.21.0" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" 1103 | dependencies = [ 1104 | "futures-util", 1105 | "log", 1106 | "tokio", 1107 | "tungstenite", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "tokio-util" 1112 | version = "0.7.10" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 1115 | dependencies = [ 1116 | "bytes", 1117 | "futures-core", 1118 | "futures-sink", 1119 | "pin-project-lite", 1120 | "tokio", 1121 | "tracing", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "toml" 1126 | version = "0.8.11" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" 1129 | dependencies = [ 1130 | "serde", 1131 | "serde_spanned", 1132 | "toml_datetime", 1133 | "toml_edit", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "toml_datetime" 1138 | version = "0.6.5" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 1141 | dependencies = [ 1142 | "serde", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "toml_edit" 1147 | version = "0.22.7" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" 1150 | dependencies = [ 1151 | "indexmap", 1152 | "serde", 1153 | "serde_spanned", 1154 | "toml_datetime", 1155 | "winnow", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "tower" 1160 | version = "0.4.13" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1163 | dependencies = [ 1164 | "futures-core", 1165 | "futures-util", 1166 | "pin-project", 1167 | "pin-project-lite", 1168 | "tokio", 1169 | "tower-layer", 1170 | "tower-service", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "tower-http" 1175 | version = "0.5.2" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" 1178 | dependencies = [ 1179 | "bitflags", 1180 | "bytes", 1181 | "http", 1182 | "http-body", 1183 | "http-body-util", 1184 | "pin-project-lite", 1185 | "tower-layer", 1186 | "tower-service", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "tower-layer" 1191 | version = "0.3.2" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 1194 | 1195 | [[package]] 1196 | name = "tower-service" 1197 | version = "0.3.2" 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" 1199 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1200 | 1201 | [[package]] 1202 | name = "tracing" 1203 | version = "0.1.40" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1206 | dependencies = [ 1207 | "pin-project-lite", 1208 | "tracing-core", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "tracing-core" 1213 | version = "0.1.32" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1216 | dependencies = [ 1217 | "once_cell", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "tungstenite" 1222 | version = "0.21.0" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" 1225 | dependencies = [ 1226 | "byteorder", 1227 | "bytes", 1228 | "data-encoding", 1229 | "http", 1230 | "httparse", 1231 | "log", 1232 | "rand", 1233 | "sha1", 1234 | "thiserror", 1235 | "url", 1236 | "utf-8", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "typenum" 1241 | version = "1.17.0" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1244 | 1245 | [[package]] 1246 | name = "unicase" 1247 | version = "2.7.0" 1248 | source = "registry+https://github.com/rust-lang/crates.io-index" 1249 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 1250 | dependencies = [ 1251 | "version_check", 1252 | ] 1253 | 1254 | [[package]] 1255 | name = "unicode-bidi" 1256 | version = "0.3.15" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1259 | 1260 | [[package]] 1261 | name = "unicode-ident" 1262 | version = "1.0.12" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1265 | 1266 | [[package]] 1267 | name = "unicode-normalization" 1268 | version = "0.1.23" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1271 | dependencies = [ 1272 | "tinyvec", 1273 | ] 1274 | 1275 | [[package]] 1276 | name = "url" 1277 | version = "2.5.0" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1280 | dependencies = [ 1281 | "form_urlencoded", 1282 | "idna", 1283 | "percent-encoding", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "users" 1288 | version = "0.11.0" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" 1291 | dependencies = [ 1292 | "libc", 1293 | "log", 1294 | ] 1295 | 1296 | [[package]] 1297 | name = "utf-8" 1298 | version = "0.7.6" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1301 | 1302 | [[package]] 1303 | name = "utf8parse" 1304 | version = "0.2.1" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1307 | 1308 | [[package]] 1309 | name = "version_check" 1310 | version = "0.9.4" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1313 | 1314 | [[package]] 1315 | name = "walkdir" 1316 | version = "2.5.0" 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" 1318 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1319 | dependencies = [ 1320 | "same-file", 1321 | "winapi-util", 1322 | ] 1323 | 1324 | [[package]] 1325 | name = "wasi" 1326 | version = "0.11.0+wasi-snapshot-preview1" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1329 | 1330 | [[package]] 1331 | name = "winapi" 1332 | version = "0.3.9" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1335 | dependencies = [ 1336 | "winapi-i686-pc-windows-gnu", 1337 | "winapi-x86_64-pc-windows-gnu", 1338 | ] 1339 | 1340 | [[package]] 1341 | name = "winapi-i686-pc-windows-gnu" 1342 | version = "0.4.0" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1345 | 1346 | [[package]] 1347 | name = "winapi-util" 1348 | version = "0.1.6" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 1351 | dependencies = [ 1352 | "winapi", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "winapi-x86_64-pc-windows-gnu" 1357 | version = "0.4.0" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1360 | 1361 | [[package]] 1362 | name = "windirs" 1363 | version = "1.0.1" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "85cba2d95c9364ef67ebf049be2c87e7483c890681ca030a664dab6a537419bc" 1366 | dependencies = [ 1367 | "winapi", 1368 | ] 1369 | 1370 | [[package]] 1371 | name = "windows-sys" 1372 | version = "0.48.0" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1375 | dependencies = [ 1376 | "windows-targets 0.48.5", 1377 | ] 1378 | 1379 | [[package]] 1380 | name = "windows-sys" 1381 | version = "0.52.0" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1384 | dependencies = [ 1385 | "windows-targets 0.52.4", 1386 | ] 1387 | 1388 | [[package]] 1389 | name = "windows-targets" 1390 | version = "0.48.5" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1393 | dependencies = [ 1394 | "windows_aarch64_gnullvm 0.48.5", 1395 | "windows_aarch64_msvc 0.48.5", 1396 | "windows_i686_gnu 0.48.5", 1397 | "windows_i686_msvc 0.48.5", 1398 | "windows_x86_64_gnu 0.48.5", 1399 | "windows_x86_64_gnullvm 0.48.5", 1400 | "windows_x86_64_msvc 0.48.5", 1401 | ] 1402 | 1403 | [[package]] 1404 | name = "windows-targets" 1405 | version = "0.52.4" 1406 | source = "registry+https://github.com/rust-lang/crates.io-index" 1407 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 1408 | dependencies = [ 1409 | "windows_aarch64_gnullvm 0.52.4", 1410 | "windows_aarch64_msvc 0.52.4", 1411 | "windows_i686_gnu 0.52.4", 1412 | "windows_i686_msvc 0.52.4", 1413 | "windows_x86_64_gnu 0.52.4", 1414 | "windows_x86_64_gnullvm 0.52.4", 1415 | "windows_x86_64_msvc 0.52.4", 1416 | ] 1417 | 1418 | [[package]] 1419 | name = "windows_aarch64_gnullvm" 1420 | version = "0.48.5" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1423 | 1424 | [[package]] 1425 | name = "windows_aarch64_gnullvm" 1426 | version = "0.52.4" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 1429 | 1430 | [[package]] 1431 | name = "windows_aarch64_msvc" 1432 | version = "0.48.5" 1433 | source = "registry+https://github.com/rust-lang/crates.io-index" 1434 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1435 | 1436 | [[package]] 1437 | name = "windows_aarch64_msvc" 1438 | version = "0.52.4" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 1441 | 1442 | [[package]] 1443 | name = "windows_i686_gnu" 1444 | version = "0.48.5" 1445 | source = "registry+https://github.com/rust-lang/crates.io-index" 1446 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1447 | 1448 | [[package]] 1449 | name = "windows_i686_gnu" 1450 | version = "0.52.4" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 1453 | 1454 | [[package]] 1455 | name = "windows_i686_msvc" 1456 | version = "0.48.5" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1459 | 1460 | [[package]] 1461 | name = "windows_i686_msvc" 1462 | version = "0.52.4" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 1465 | 1466 | [[package]] 1467 | name = "windows_x86_64_gnu" 1468 | version = "0.48.5" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1471 | 1472 | [[package]] 1473 | name = "windows_x86_64_gnu" 1474 | version = "0.52.4" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 1477 | 1478 | [[package]] 1479 | name = "windows_x86_64_gnullvm" 1480 | version = "0.48.5" 1481 | source = "registry+https://github.com/rust-lang/crates.io-index" 1482 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1483 | 1484 | [[package]] 1485 | name = "windows_x86_64_gnullvm" 1486 | version = "0.52.4" 1487 | source = "registry+https://github.com/rust-lang/crates.io-index" 1488 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 1489 | 1490 | [[package]] 1491 | name = "windows_x86_64_msvc" 1492 | version = "0.48.5" 1493 | source = "registry+https://github.com/rust-lang/crates.io-index" 1494 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1495 | 1496 | [[package]] 1497 | name = "windows_x86_64_msvc" 1498 | version = "0.52.4" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 1501 | 1502 | [[package]] 1503 | name = "winnow" 1504 | version = "0.6.5" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" 1507 | dependencies = [ 1508 | "memchr", 1509 | ] 1510 | --------------------------------------------------------------------------------