Initial implementation
parent
08b114f11d
commit
f6fd59c9e8
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "svr"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
futures = { version = "0.3.28", features = ["std"] }
|
||||
humantime-serde = "1.1.1"
|
||||
serde = { version = "1.0.160", features = ["derive"] }
|
||||
thiserror = "1.0.40"
|
||||
tokio = { version = "1.28.0", features = ["macros", "rt-multi-thread", "fs", "io-util", "time"] }
|
||||
toml = "0.7.3"
|
||||
xflags = "0.3.1"
|
||||
youtube_dl = { version = "0.8.0", features = ["tokio"] }
|
@ -0,0 +1,35 @@
|
||||
# Stream VOD Recorder
|
||||
|
||||
`svr` is a utility that monitors and downloads livestreams for any site with a youtube-dl extractor.
|
||||
|
||||
Example `config.toml`:
|
||||
```toml
|
||||
output = "/media/livestreams"
|
||||
|
||||
[[streams]]
|
||||
subpath = "ThePrimeagen"
|
||||
type = "youtube-dl"
|
||||
url = "https://twitch.tv/ThePrimeagen"
|
||||
frequency = "10m"
|
||||
|
||||
[[streams]]
|
||||
subpath = "LofiGirl"
|
||||
type = "youtube-dl"
|
||||
url = "https://www.youtube.com/@LofiGirl/live"
|
||||
frequency = "2h"
|
||||
|
||||
[[streams]]
|
||||
subpath = "LofiGirl"
|
||||
type = "youtube-dl"
|
||||
url = "https://www.youtube.com/watch?v=jfKfPfyJRdk"
|
||||
frequency = "2h"
|
||||
```
|
||||
|
||||
## TODO
|
||||
- Implement credential management for protected streams
|
||||
- More configuration options for individual streams
|
||||
- Handle server/stream interruptions (e.g. merging multiple stream files)
|
||||
- Monitor playlists and other non-live content (monitor a channel???)
|
||||
- Automatic deletion after e.g. a period of time
|
||||
- Improved logging (tracing?)
|
||||
- Save metadata somewhere
|
@ -0,0 +1 @@
|
||||
wrap_comments = true
|
@ -0,0 +1,92 @@
|
||||
use crate::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use youtube_dl::{YoutubeDl, YoutubeDlOutput};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub youtube_dl_path: Option<PathBuf>,
|
||||
pub output: PathBuf,
|
||||
pub streams: Vec<Stream>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Stream {
|
||||
#[serde(
|
||||
rename = "youtube-dl",
|
||||
alias = "yt-dl",
|
||||
alias = "ytdl",
|
||||
alias = "yt-dlp",
|
||||
alias = "ytdlp"
|
||||
)]
|
||||
YoutubeDl {
|
||||
url: String,
|
||||
#[serde(with = "humantime_serde")]
|
||||
frequency: Duration,
|
||||
quality: Option<String>,
|
||||
// Relative to Config::output. If `None`, will be downloaded directly to Config::output
|
||||
subpath: Option<PathBuf>,
|
||||
// Overrides Config::output, still respects subpath (becomes relative to new output)
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
/// # Errors
|
||||
/// Should only return an error on an unrecoverable error, things like
|
||||
/// e.g. a stream not being live or a video not being found are
|
||||
/// to be handled internally.
|
||||
pub async fn watch(&self, config: &Config) -> Result<(), Error> {
|
||||
match self {
|
||||
Stream::YoutubeDl {
|
||||
url,
|
||||
frequency,
|
||||
quality,
|
||||
subpath,
|
||||
output,
|
||||
} => {
|
||||
loop {
|
||||
// TODO: Can this dl struct be reused?
|
||||
let mut dl = YoutubeDl::new(url);
|
||||
dl.download(true);
|
||||
|
||||
if let Some(path) = &config.youtube_dl_path {
|
||||
dl.youtube_dl_path(path);
|
||||
}
|
||||
|
||||
if let Some(quality) = quality {
|
||||
dl.format(quality);
|
||||
}
|
||||
|
||||
let mut output_dir = output.clone().unwrap_or(config.output.clone());
|
||||
if let Some(subpath) = subpath {
|
||||
output_dir = output_dir.join(subpath);
|
||||
}
|
||||
dl.output_directory(output_dir.to_string_lossy());
|
||||
|
||||
match dl.run_async().await {
|
||||
Ok(YoutubeDlOutput::SingleVideo(video)) => {
|
||||
println!("Successfully downloading video `{url:#?}`: {video:#?}");
|
||||
}
|
||||
Ok(YoutubeDlOutput::Playlist(playlist)) => {
|
||||
println!("Successfully downloading playlist `{url:#?}`: {playlist:#?}");
|
||||
}
|
||||
Err(e) => {
|
||||
let err = if let youtube_dl::Error::ExitCode { stderr, .. } = e {
|
||||
stderr
|
||||
} else if let youtube_dl::Error::Io(ioerr) = e {
|
||||
ioerr.to_string()
|
||||
} else {
|
||||
e.to_string()
|
||||
};
|
||||
eprintln!("Error with video at URL `{url:#?}`: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(*frequency).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
xflags::xflags! {
|
||||
cmd svr {
|
||||
optional -c, --config config_path: PathBuf
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
#![feature(async_closure)]
|
||||
|
||||
use config::Config;
|
||||
use futures::future::join_all;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tokio::{
|
||||
fs,
|
||||
io::{self, AsyncReadExt},
|
||||
};
|
||||
|
||||
pub mod config;
|
||||
mod flags;
|
||||
|
||||
const DEFAULT_CONFIG_PATH: &str = "./config.toml";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("IoError: {0:#?}")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("Invalid config: {0}")]
|
||||
InvalidConfig(#[from] toml::de::Error),
|
||||
#[error("Invalid command line flags: {0:#?}")]
|
||||
Arguments(#[from] xflags::Error),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let flags = flags::Svr::from_env()?;
|
||||
|
||||
let config_path = flags.config.unwrap_or_else(|| DEFAULT_CONFIG_PATH.into());
|
||||
|
||||
let mut config = String::new();
|
||||
fs::File::open(&config_path)
|
||||
.await?
|
||||
.read_to_string(&mut config)
|
||||
.await?;
|
||||
|
||||
let config: Arc<Config> = Arc::new(toml::from_str(&config)?);
|
||||
|
||||
// Spawn a task for each stream to watch it
|
||||
// This config.streams.clone seems unnecessary since we only need an immutable
|
||||
// reference
|
||||
join_all(config.streams.clone().into_iter().map(|stream| {
|
||||
let config = Arc::clone(&config);
|
||||
tokio::spawn(async move {
|
||||
stream.watch(&config).await.unwrap();
|
||||
})
|
||||
}))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue