lib.rs (3385B)
1 pub mod config; 2 pub mod error; 3 pub mod post; 4 pub mod event; 5 6 use error::Error; 7 use post::Post; 8 use event::Event; 9 10 pub fn run(config: config::Config, tx: std::sync::mpsc::Sender<Event>) -> Result<(), Error> { 11 let channel = url_to_channel(&config.url)?; 12 13 tx.send(Event::GetPostsStarted(config.url.clone())).ok(); 14 let posts = match get_posts(&channel, &tx) { 15 Ok(posts) => { 16 tx.send(Event::GetPostsFinished(config.url.clone())).ok(); 17 posts 18 } 19 Err(e) => { 20 tx.send(Event::GetPostsFailed(config.url.clone(), e.to_string())).ok(); 21 return Err(e); 22 } 23 }; 24 25 std::fs::create_dir_all(&config.output_dir)?; 26 for post in posts { 27 tx.send(Event::DownloadPostStarted(post.name.clone())).ok(); 28 if config.resume { 29 let local_path = config.output_dir.join(&post.filename); 30 if let Ok(meta) = std::fs::metadata(&local_path) { 31 if post.content_length(&tx).map_or(false, |remote| meta.len() == remote) { 32 tx.send(Event::DownloadPostSkipped(post.name.clone())).ok(); 33 continue; 34 } 35 } 36 } 37 match post.download(&config.output_dir, &tx) { 38 Ok(()) => tx.send(Event::DownloadPostFinished(post.name.clone())).ok(), 39 Err(e) => tx.send(Event::DownloadPostFailed(post.name.clone(), e.to_string())).ok(), 40 }; 41 std::thread::sleep(std::time::Duration::from_millis(100)); 42 } 43 44 tx.send(Event::Done).ok(); 45 Ok(()) 46 } 47 48 /// Extract the channel URL part from an Odysee URL, e.g. "https://odysee.com/@channel:1" -> "@channel:1" 49 fn url_to_channel(url: &str) -> Result<String, Error> { 50 let rest = url 51 .split("odysee.com/") 52 .nth(1) 53 .ok_or(Error::InvalidUrl)?; 54 let channel = rest.split('?').next().unwrap_or(rest); 55 Ok(channel.to_string()) 56 } 57 58 fn get_posts(channel: &str, tx: &std::sync::mpsc::Sender<Event>) -> Result<Vec<Post>, Error> { 59 let mut page = 1usize; 60 let mut posts = Vec::new(); 61 62 loop { 63 let body = serde_json::json!({ 64 "method": "claim_search", 65 "params": { 66 "channel": channel, 67 "page": page, 68 "page_size": 50, 69 "claim_type": ["stream"], 70 } 71 }); 72 73 let body_bytes = serde_json::to_vec(&body)?; 74 let response: serde_json::Value = post::with_retry( 75 || { 76 ureq::post("https://api.na-backend.odysee.com/api/v1/proxy") 77 .header("Content-Type", "application/json") 78 .send(body_bytes.clone()) 79 }, 80 || { tx.send(Event::RateLimited("API".to_string())).ok(); }, 81 )? 82 .body_mut() 83 .read_json()?; 84 85 let items = response["result"]["items"] 86 .as_array() 87 .ok_or_else(|| Error::Http("items not found".to_string()))?; 88 89 if items.is_empty() { 90 break; 91 } 92 93 let page_posts: Vec<Post> = items 94 .iter() 95 .map(Post::from_json) 96 .collect::<Result<Vec<Post>, Error>>()?; 97 98 let total_pages = response["result"]["total_pages"].as_u64().unwrap_or(1) as usize; 99 posts.extend(page_posts); 100 101 if page >= total_pages { 102 break; 103 } 104 page += 1; 105 } 106 107 Ok(posts) 108 }