commit b7512ed10c6da2bd23df1352994f6b09d9e75681
parent 62ba058fad473fb02b83e82619cdb92885a1379a
Author: egor-achkasov <eaachkasov@gmail.com>
Date: Sat, 9 May 2026 05:36:10 +0000
Wait on HTTP 429
Diffstat:
4 files changed, 64 insertions(+), 28 deletions(-)
diff --git a/src/bin/cli/main.rs b/src/bin/cli/main.rs
@@ -108,6 +108,7 @@ fn render_event(event: &Event) {
Event::DownloadPostFailed(_, err) => println!(" Failed: {}", err),
Event::DownloadPostSkipped(_) => println!(" Skipped"),
Event::DownloadPostFinished(_) => println!(" Done"),
+ Event::RateLimited(ctx) => println!("\nRate limited ({}), waiting 60s...", ctx),
Event::Done => println!("Done."),
}
}
diff --git a/src/lib/event.rs b/src/lib/event.rs
@@ -13,6 +13,7 @@ pub enum Event {
DownloadPostFailed(String, String), // post name, error message
DownloadPostSkipped(String), // post name (already exists, skipping)
DownloadPostFinished(String), // post name
+ RateLimited(String), // context (post name or "API")
Done,
}
diff --git a/src/lib/lib.rs b/src/lib/lib.rs
@@ -11,7 +11,7 @@ pub fn run(config: config::Config, tx: std::sync::mpsc::Sender<Event>) -> Result
let channel = url_to_channel(&config.url)?;
tx.send(Event::GetPostsStarted(config.url.clone())).ok();
- let posts = match get_posts(&channel) {
+ let posts = match get_posts(&channel, &tx) {
Ok(posts) => {
tx.send(Event::GetPostsFinished(config.url.clone())).ok();
posts
@@ -28,13 +28,13 @@ pub fn run(config: config::Config, tx: std::sync::mpsc::Sender<Event>) -> Result
if config.resume {
let local_path = config.output_dir.join(&post.filename);
if let Ok(meta) = std::fs::metadata(&local_path) {
- if post.content_length().map_or(false, |remote| meta.len() == remote) {
+ if post.content_length(&tx).map_or(false, |remote| meta.len() == remote) {
tx.send(Event::DownloadPostSkipped(post.name.clone())).ok();
continue;
}
}
}
- match post.download(&config.output_dir) {
+ match post.download(&config.output_dir, &tx) {
Ok(()) => tx.send(Event::DownloadPostFinished(post.name.clone())).ok(),
Err(e) => tx.send(Event::DownloadPostFailed(post.name.clone(), e.to_string())).ok(),
};
@@ -55,7 +55,7 @@ fn url_to_channel(url: &str) -> Result<String, Error> {
Ok(channel.to_string())
}
-fn get_posts(channel: &str) -> Result<Vec<Post>, Error> {
+fn get_posts(channel: &str, tx: &std::sync::mpsc::Sender<Event>) -> Result<Vec<Post>, Error> {
let mut page = 1usize;
let mut posts = Vec::new();
@@ -70,11 +70,17 @@ fn get_posts(channel: &str) -> Result<Vec<Post>, Error> {
}
});
- let response: serde_json::Value = ureq::post("https://api.na-backend.odysee.com/api/v1/proxy")
- .header("Content-Type", "application/json")
- .send(serde_json::to_vec(&body)?)?
- .body_mut()
- .read_json()?;
+ let body_bytes = serde_json::to_vec(&body)?;
+ let response: serde_json::Value = post::with_retry(
+ || {
+ ureq::post("https://api.na-backend.odysee.com/api/v1/proxy")
+ .header("Content-Type", "application/json")
+ .send(body_bytes.clone())
+ },
+ || { tx.send(Event::RateLimited("API".to_string())).ok(); },
+ )?
+ .body_mut()
+ .read_json()?;
let items = response["result"]["items"]
.as_array()
diff --git a/src/lib/post.rs b/src/lib/post.rs
@@ -1,4 +1,22 @@
use crate::error::Error;
+use crate::event::Event;
+
+pub(crate) fn with_retry<F, T, C>(f: F, on_rate_limited: C) -> Result<T, Error>
+where
+ F: Fn() -> Result<T, ureq::Error>,
+ C: Fn(),
+{
+ loop {
+ match f() {
+ Ok(val) => return Ok(val),
+ Err(ureq::Error::StatusCode(429)) => {
+ on_rate_limited();
+ std::thread::sleep(std::time::Duration::from_secs(60));
+ }
+ Err(e) => return Err(e.into()),
+ }
+ }
+}
pub struct Post {
pub name: String,
@@ -31,28 +49,38 @@ impl Post {
Ok(Post { name, filename, streaming_url })
}
- pub fn content_length(&self) -> Option<u64> {
- ureq::head(&self.streaming_url)
- .header("Referer", "https://odysee.com/")
- .header("Origin", "https://odysee.com")
- .call()
- .ok()
- .and_then(|r| {
- r.headers()
- .get("content-length")?
- .to_str()
- .ok()?
- .parse()
- .ok()
- })
+ pub fn content_length(&self, tx: &std::sync::mpsc::Sender<Event>) -> Option<u64> {
+ with_retry(
+ || {
+ ureq::head(&self.streaming_url)
+ .header("Referer", "https://odysee.com/")
+ .header("Origin", "https://odysee.com")
+ .call()
+ },
+ || { tx.send(Event::RateLimited(self.name.clone())).ok(); },
+ )
+ .ok()
+ .and_then(|r| {
+ r.headers()
+ .get("content-length")?
+ .to_str()
+ .ok()?
+ .parse()
+ .ok()
+ })
}
- pub fn download(&self, dir: &std::path::Path) -> Result<(), Error> {
+ pub fn download(&self, dir: &std::path::Path, tx: &std::sync::mpsc::Sender<Event>) -> Result<(), Error> {
let path = dir.join(&self.filename);
- let mut response = ureq::get(&self.streaming_url)
- .header("Referer", "https://odysee.com/")
- .header("Origin", "https://odysee.com")
- .call()?;
+ let mut response = with_retry(
+ || {
+ ureq::get(&self.streaming_url)
+ .header("Referer", "https://odysee.com/")
+ .header("Origin", "https://odysee.com")
+ .call()
+ },
+ || { tx.send(Event::RateLimited(self.name.clone())).ok(); },
+ )?;
let mut file = std::fs::File::create(&path)?;
let mut reader = response.body_mut().as_reader();
std::io::copy(&mut reader, &mut file)?;