Skip to content

Commit 38a8075

Browse files
committed
Add user shell commands after phases
1 parent 541e921 commit 38a8075

File tree

10 files changed

+133
-3
lines changed

10 files changed

+133
-3
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ fancy-regex = "0.14.0"
2020
reqwest = { version = "0.11.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
2121
serde = { version = "1.0.140", features = ["derive"] }
2222
serde_json = "1.0.82"
23+
strfmt = "0.2.4"
2324
terminal-spinners = "0.3.2"
2425
toml = "0.5.9"
2526
unicode-general-category = "1.0.0"

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ These are optional flags that affect how the program works.
7575
- `-v`, `--verbose`: Increases output, useful for debugging and reporting issues
7676
- `-V`, `--version`: Print version information [does not require `<TYPE>` or `<INPUT>`]
7777

78+
These optional flags allow commands to be ran after each stage with variables wrapped in curly braces subtituted in.
79+
Supported variables are `{id}`, `{chat_ext}`, and `{video_title}`. Double curly braces for literals, e.g., `{{var}}``"{var}"`.
80+
- `-1`, `--post-json <SHELL_COMMAND>`: Runs after the json is downloaded
81+
- `-2`, `--post-thumbnail <SHELL_COMMAND>`: Runs after the thumbnail is downloaded
82+
- `-3`, `--post-chat <SHELL_COMMAND>`: Runs after the chat is downloaded
83+
- `-4`, `--post-chat-process <SHELL_COMMAND>`: Runs after the chat is processed
84+
- `-5`, `--post-video <SHELL_COMMAND>`: Runs after the video is downloaded
85+
7886
These optional flags are only used when downloading Twitch Clips with both `--clips` and `--channel` options.
7987
- `-i <DURATION>`, `--interval <DURATION>`: Time interval to search for clips from a channel, shorter intervals will take longer but produce more complete results [default: `1hour`]
8088
- `-r <DURATION>`, `--range <DURATION>`: How long ago to start searching for clips [default: `1week`]

package.nix

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
, rustPlatform
33
, installShellFiles
44
, makeWrapper
5+
, bash
56
, yt-dlp
67
, brotli
78
, twitch_downloader
@@ -39,7 +40,7 @@ in rustPlatform.buildRustPackage rec {
3940

4041
postFixup = ''
4142
wrapProgram $out/bin/archiver \
42-
--set PATH ${lib.makeBinPath [ yt-dlp brotli twitch_downloader python3Packages.chat-downloader twitch-chat-downloader ]}
43+
--set PATH ${lib.makeBinPath [ bash yt-dlp brotli twitch_downloader python3Packages.chat-downloader twitch-chat-downloader ]}
4344
'';
4445

4546
meta = with lib; {

src/downloader/common.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::utils::colorize;
1+
use super::utils::{colorize, run_template};
22
use crate::init::{Context, VideoType};
33
use crate::utils::{
44
download_file, error_msg, good_msg, message, sanitize, split_videos, warn_msg, write_file,
@@ -7,6 +7,7 @@ use crate::utils::{
77
use crate::Error;
88
use colored::Color;
99
use fancy_regex::Regex;
10+
use std::collections::HashMap;
1011
use std::path::Path;
1112

1213
pub(super) fn download<T: VideoInfo>(
@@ -29,6 +30,10 @@ pub(super) fn download<T: VideoInfo>(
2930
}
3031
VideoType::Clip => format!("{id}.mp4"),
3132
};
33+
let mut vars = HashMap::new();
34+
vars.insert("id".into(), id.into());
35+
vars.insert("chat_ext".into(), chat_ext.into());
36+
vars.insert("video_title".into(), video_title.clone());
3237

3338
let spinner_text = format!(" Saving JSON {id}.json");
3439
context.spinner.create(&spinner_text);
@@ -40,6 +45,15 @@ pub(super) fn download<T: VideoInfo>(
4045
return Err(error);
4146
}
4247
}
48+
if let Some(template) = &context.post_json {
49+
context.spinner.create(" Running post_json");
50+
let result = run_template(template, &vars);
51+
context.spinner.end();
52+
match result.is_ok() {
53+
true => good_msg(Some("post_json"), "Success", context),
54+
false => error_msg(Some("post_json"), "Failed", context),
55+
}
56+
}
4357

4458
let spinner_text = format!(" Downloading {id}.jpg");
4559
context.spinner.create(&spinner_text);
@@ -52,6 +66,15 @@ pub(super) fn download<T: VideoInfo>(
5266
"Download",
5367
&format!("{id}.jpg"),
5468
);
69+
if let Some(template) = &context.post_thumbnail {
70+
context.spinner.create(" Running post_thumbnail");
71+
let result = run_template(template, &vars);
72+
context.spinner.end();
73+
match result.is_ok() {
74+
true => good_msg(Some("post_thumbnail"), "Success", context),
75+
false => error_msg(Some("post_thumbnail"), "Failed", context),
76+
}
77+
}
5578

5679
let spinner_text = format!(" Downloading {id}{chat_ext}");
5780
context.spinner.create(&spinner_text);
@@ -64,6 +87,15 @@ pub(super) fn download<T: VideoInfo>(
6487
"Download",
6588
&format!("{id}{chat_ext}"),
6689
);
90+
if let Some(template) = &context.post_chat {
91+
context.spinner.create(" Running post_chat");
92+
let result = run_template(template, &vars);
93+
context.spinner.end();
94+
match result.is_ok() {
95+
true => good_msg(Some("post_chat"), "Success", context),
96+
false => error_msg(Some("post_chat"), "Failed", context),
97+
}
98+
}
6799

68100
let spinner_text = format!(" Processing {id}{chat_ext}");
69101
context.spinner.create(&spinner_text);
@@ -76,13 +108,31 @@ pub(super) fn download<T: VideoInfo>(
76108
"Process",
77109
&format!("{id}{chat_ext}.br"),
78110
);
111+
if let Some(template) = &context.post_chat_process {
112+
context.spinner.create(" Running post_chat_process");
113+
let result = run_template(template, &vars);
114+
context.spinner.end();
115+
match result.is_ok() {
116+
true => good_msg(Some("post_chat_process"), "Success", context),
117+
false => error_msg(Some("post_chat_process"), "Failed", context),
118+
}
119+
}
79120

80121
if !context.skip_video {
81122
let spinner_text = format!(" Downloading {video_title}");
82123
context.spinner.create(&spinner_text);
83124
let result = get_video(info, context);
84125
context.spinner.end();
85126
parse_result(&result, context, "video", "Download", &video_title);
127+
if let Some(template) = &context.post_video {
128+
context.spinner.create(" Running post_video");
129+
let result = run_template(template, &vars);
130+
context.spinner.end();
131+
match result.is_ok() {
132+
true => good_msg(Some("post_video"), "Success", context),
133+
false => error_msg(Some("post_video"), "Failed", context),
134+
}
135+
}
86136

87137
message(
88138
&colorize(

src/downloader/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub(crate) enum Error {
2222
Expected,
2323
Token(String),
2424
Config(String),
25+
Template,
2526
}
2627
// todo!() Make better errors with information as to what went wrong
2728

@@ -67,6 +68,12 @@ impl From<string::FromUtf8Error> for Error {
6768
}
6869
}
6970

71+
impl From<strfmt::FmtError> for Error {
72+
fn from(_: strfmt::FmtError) -> Error {
73+
Error::Template
74+
}
75+
}
76+
7077
impl Display for Error {
7178
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
7279
match self {
@@ -87,6 +94,7 @@ impl Display for Error {
8794
Error::CommandFailed(program) => write!(f, "Command failed: {program}"),
8895
Error::Expected => write!(f, "This error is expected"),
8996
Error::Token(message) | Error::Config(message) => write!(f, "{message}"),
97+
Error::Template => write!(f, "Failed to run template"),
9098
}
9199
}
92100
}

src/downloader/utils.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ use std::fmt::{self, Debug, Display, Formatter};
99
use std::fs::{File, OpenOptions};
1010
use std::io::Write;
1111
use std::path::Path;
12-
use std::process::Stdio;
12+
use std::process::{Command, Stdio};
1313
use std::sync::LazyLock;
14+
use strfmt::strfmt;
1415
use terminal_spinners::{SpinnerBuilder, SpinnerHandle, DOTS2};
1516
use unicode_general_category::{get_general_category, GeneralCategory};
1617
use unicode_normalization::UnicodeNormalization;
@@ -273,3 +274,17 @@ impl Spinner {
273274
self.message = None;
274275
}
275276
}
277+
278+
pub(crate) fn run_template(template: &str, vars: &HashMap<String, String>) -> Result<(), Error> {
279+
match Command::new("bash")
280+
.arg("-c")
281+
.arg(&strfmt(template, vars)?)
282+
.stdout(Stdio::inherit())
283+
.stderr(Stdio::inherit())
284+
.status()?
285+
.success()
286+
{
287+
true => Ok(()),
288+
false => Err(Error::Template),
289+
}
290+
}

src/init/args.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ pub(super) struct Args {
1313
pub(super) range: String,
1414
pub(super) interval: String,
1515
pub(super) threads: u16,
16+
pub(super) post_json: Option<String>,
17+
pub(super) post_thumbnail: Option<String>,
18+
pub(super) post_chat: Option<String>,
19+
pub(super) post_chat_process: Option<String>,
20+
pub(super) post_video: Option<String>,
1621
}
1722

1823
pub(super) fn parse() -> Args {
@@ -48,5 +53,10 @@ pub(super) fn parse() -> Args {
4853
cli.range,
4954
cli.interval,
5055
cli.threads,
56+
cli.post_json,
57+
cli.post_thumbnail,
58+
cli.post_chat,
59+
cli.post_chat_process,
60+
cli.post_video,
5161
)
5262
}

src/init/cli.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,24 @@ pub(crate) struct Cli {
8989
/// Hide spinners
9090
#[clap(short = 'q', long, takes_value = false)]
9191
pub(crate) hide_spinners: bool,
92+
93+
/// Template to run after the json is downloaded
94+
#[clap(short = '1', long, value_name = "SHELL_COMMAND")]
95+
pub(crate) post_json: Option<String>,
96+
97+
/// Template to run after the thumbnail is downloaded
98+
#[clap(short = '2', long, value_name = "SHELL_COMMAND")]
99+
pub(crate) post_thumbnail: Option<String>,
100+
101+
/// Template to run after chat is downloaded
102+
#[clap(short = '3', long, value_name = "SHELL_COMMAND")]
103+
pub(crate) post_chat: Option<String>,
104+
105+
/// Template to run after chat is processed
106+
#[clap(short = '4', long, value_name = "SHELL_COMMAND")]
107+
pub(crate) post_chat_process: Option<String>,
108+
109+
/// Template to run after the video is downloaded
110+
#[clap(short = '5', long, value_name = "SHELL_COMMAND")]
111+
pub(crate) post_video: Option<String>,
92112
}

src/init/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ pub(super) fn run() -> Result<Input, Error> {
8080
interval,
8181
logging: args.logging,
8282
spinner,
83+
post_json: args.post_json,
84+
post_thumbnail: args.post_thumbnail,
85+
post_chat: args.post_chat,
86+
post_chat_process: args.post_chat_process,
87+
post_video: args.post_video,
8388
};
8489
Ok(Input::new(args.videos, context))
8590
}
@@ -118,6 +123,11 @@ pub(super) struct Context {
118123
pub(super) interval: Duration,
119124
pub(super) logging: bool,
120125
pub(super) spinner: Spinner,
126+
pub(super) post_json: Option<String>,
127+
pub(super) post_thumbnail: Option<String>,
128+
pub(super) post_chat: Option<String>,
129+
pub(super) post_chat_process: Option<String>,
130+
pub(super) post_video: Option<String>,
121131
}
122132

123133
fn parse_duration(text: &str) -> Duration {

0 commit comments

Comments
 (0)