odysee-dl

odysee.com channel content downloader
git clone https://git.ea.contact/odysee-dl
Log | Files | Refs | README

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 }