From c39917d4d9f107475702793d8b2d9bed4c032e62 Mon Sep 17 00:00:00 2001 From: Jettcodey Date: Fri, 4 Oct 2024 19:23:39 +0200 Subject: [PATCH] 1.4.0 --- Cargo.lock | 2 +- Cargo.toml | 6 +- LICENSE => LICENSE.txt | 5 +- README.md | 116 +++++++++++++++++++++--- src/cli.rs | 36 ++++++-- src/downloaders/aniwave.rs | 53 ++++++----- src/downloaders/aniworldserienstream.rs | 95 +++++++++++-------- src/downloaders/mod.rs | 2 + 8 files changed, 231 insertions(+), 84 deletions(-) rename LICENSE => LICENSE.txt (82%) diff --git a/Cargo.lock b/Cargo.lock index ec3d88f..b9c5611 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2106,7 +2106,7 @@ dependencies = [ [[package]] name = "sdl" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "arc4", diff --git a/Cargo.toml b/Cargo.toml index 3481386..faed610 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,13 @@ [package] name = "sdl" -version = "0.1.3" +authors = ["Funami580","jettcodey"] +version = "0.1.4" edition = "2021" rust-version = "1.75" readme = "README.md" -repository = "https://github.com/Funami580/sdl" +repository = "https://github.com/jettcodey/sdl" license = "MIT" +license-file = "LICENSE.txt" categories = ["command-line-utilities"] [dependencies] diff --git a/LICENSE b/LICENSE.txt similarity index 82% rename from LICENSE rename to LICENSE.txt index 57a47d6..b2fbd15 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2023 Funami580 +Copyright (c) 2023 Funami580 +Copyright (c) 2024 Jettcodey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,6 +10,8 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Note: The license terms of Jettcodey apply only to the Forked version 1.4.0 and later. For earlier versions, the license terms of the original author apply. + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. diff --git a/README.md b/README.md index 23b13aa..c1024ce 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,87 @@ -# sdl -Download multiple episodes from streaming sites - -**Warning: This project has been only tested on Linux, and is generally not well tested.** - +# SDL + +
+ Table of Contents +
    +
  1. + About The Project + +
  2. +
  3. Usage
  4. + +
  5. Help output
  6. +
  7. Notes
  8. +
  9. Build from source
  10. +
  11. Thanks
  12. +
  13. O
  14. +
  15. O
  16. +
+
+ +**Warning: This project is generally not well tested.** +> [!NOTE] +> This is a Fork of https://github.com/Funami580/sdl. +> +> All the credit for making this Command line utility goes to [Funami580](https://github.com/Funami580). + +### About the Project +Download Various Anime and Series from Aniwave.se, Aniworld.to and S.to + + +This project is [licensed](https://github.com/Jettcodey/sdl/blob/master/LICENSE.txt) under the terms of the [MIT license](https://opensource.org/license/mit). + ## Supported sites ### English -* [Aniwave](https://aniwave.to) +* [Aniwave.se](https://aniwave.se) <-- Most likely Not Working right now. ### German -* [AniWorld](https://aniworld.to) +* [AniWorld.to](https://aniworld.to) * [S.to](https://s.to) +

Back to top

+ ## Supported extractors * Filemoon * Streamtape * Vidoza * Voe +

Back to top

+ ## Usage + ### Downloading a single episode By URL: ```bash -sdl 'https://aniwave.to/watch/yuruyuri.p6q/ep-11' +sdl 'https://aniwave.se/anime-watch/yuruyuri/ep-11' ``` -By specifying it explicitly: +By specifying it explicitly ```bash -sdl -e 11 'https://aniwave.to/watch/yuruyuri.p6q' +sdl -s 2 -e 5 'https://aniworld.to/anime/stream/rent-a-girlfriend' ``` +(Aniwave ONLY): +```bash +sdl -e 11 'https://aniwave.se/anime-watch/yuruyuri' +``` +

Back to top

+ ### Downloading an entire season By URL: ```bash -sdl 'https://aniwave.to/watch/yuruyuri.p6q' +sdl 'https://aniwave.se/anime-watch/yuruyuri' sdl 'https://aniworld.to/anime/stream/yuruyuri-happy-go-lily/staffel-2' sdl 'https://aniworld.to/anime/stream/yuruyuri-happy-go-lily/filme' ``` @@ -40,43 +90,68 @@ By specifying it explicitly: sdl -s 2 'https://aniworld.to/anime/stream/yuruyuri-happy-go-lily' sdl -s 0 'https://aniworld.to/anime/stream/yuruyuri-happy-go-lily' ``` +

Back to top

+ ### Downloading multiple episodes ```bash -sdl -e 1,2-6,9 'https://aniwave.to/watch/yuruyuri.p6q' +sdl -e 1,2-6,9 'https://aniwave.se/anime-watch/yuruyuri' ``` +

Back to top

+ ### Downloading multiple seasons ```bash sdl -s 1-2,4 'https://aniworld.to/anime/stream/yuruyuri-happy-go-lily' ``` +

Back to top

+ ### Downloading all seasons ```bash sdl 'https://aniworld.to/anime/stream/yuruyuri-happy-go-lily' ``` +

Back to top

+ ### Downloading in other languages ```bash sdl -t gersub 'https://s.to/serie/stream/higurashi-no-naku-koro-ni/staffel-1/episode-1' -sdl -t engdub 'https://aniwave.to/watch/case-closed.myz/ep-1' +sdl -t engdub 'https://aniwave.se/anime-watch/detective-conan/ep-1' ``` Either dub or sub: ```bash sdl -t ger 'https://s.to/serie/stream/higurashi-no-naku-koro-ni/staffel-1/episode-1' sdl -t german 'https://s.to/serie/stream/higurashi-no-naku-koro-ni/staffel-1/episode-1' ``` +

Back to top

+ + +### Full Examples +Download Season 2 Ep 5 in German Audio(GerDub): +```bash +sdl -s 2 -e 1 -t gerdub 'https://aniworld.to/anime/stream/rent-a-girlfriend' +``` +Download Full Season 3 in Japanese Audio&English Sub(EngSub): +```bash +sdl -s 3 -t engsub 'https://aniworld.to/anime/stream/rent-a-girlfriend' +``` + If an episode has multiple languages, the general language preference is as follows: * English Anime Website: EngSub > EngDub * German Anime Website: GerDub > GerSub > EngSub > EngDub * German non-Anime Website: GerDub > GerSub > EngDub > EngSub +

Back to top

+ ### Downloading with extractor directly ```bash sdl -u 'https://streamtape.com/e/DXYPVBeKrpCkMwD' sdl -u=voe 'https://prefulfilloverdoor.com/e/8cu8qkojpsx9' ``` +

Back to top

+ ### Help output ``` Usage: sdl [OPTIONS] @@ -114,20 +189,33 @@ Options: -V, --version Print version ``` +

Back to top

+ ## Notes If FFmpeg and ChromeDriver are not found in the `PATH`, they will be downloaded automatically. Also, I don't plan to add new sites or extractors, but you're welcome to create a Pull Request if you want to add one. By the way, it's also possible to use `sdl` as a library. +

Back to top

+ ## Build from source Currently, Rust 1.75 or newer is required. ``` cargo build --release ``` The resulting executable is found at `target/release/sdl`. +

Back to top

+ + +## Report a bug/Request a Feature +See the [open issues](https://github.com/Jettcodey/sdl/issues) for a full list of proposed features (and known issues). +##### Always report bugs and issues in English! If you report in any other language, your issue will be ignored and closed. +

Back to top

## Thanks -* [aniworld_scraper](https://github.com/wolfswolke/aniworld_scraper) for the inspiration and showing how it could be done +* [aniworld_scraper](https://github.com/wolfswolke/aniworld_scraper) for the inspiration and showing how it could be done. +* [Funami580](https://github.com/Funami580) for Creating this Project. +

Back to top

\ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index b4b84be..c0f4886 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -29,7 +29,7 @@ pub(crate) struct Args { pub(crate) episodes: SimpleRanges, /// Only download specific seasons - #[arg(short, long, value_parser = parse_ranges, default_value_t = SimpleRanges::Unspecified, hide_default_value = true, conflicts_with_all = ["episodes"], value_name = "RANGES")] + #[arg(short, long, value_parser = parse_ranges, default_value_t = SimpleRanges::Unspecified, hide_default_value = true/*, conflicts_with_all = ["episodes"]*/, value_name = "RANGES")] pub(crate) seasons: SimpleRanges, /// Use underlying extractors directly @@ -81,22 +81,46 @@ impl Args { pub(crate) fn get_episodes_request(self) -> EpisodesRequest { match (self.episodes, self.seasons) { (SimpleRanges::Unspecified, SimpleRanges::Unspecified) => EpisodesRequest::Unspecified, + + // If only episodes are specified (SimpleRanges::Custom(episodes), SimpleRanges::Unspecified) => { EpisodesRequest::Episodes(AllOrSpecific::Specific(episodes)) } + + // If only seasons are specified (SimpleRanges::Unspecified, SimpleRanges::Custom(seasons)) => { EpisodesRequest::Seasons(AllOrSpecific::Specific(seasons)) } + + // Handle all episodes being requested (SimpleRanges::All, SimpleRanges::Unspecified) => EpisodesRequest::Episodes(AllOrSpecific::All), (SimpleRanges::Unspecified, SimpleRanges::All) => EpisodesRequest::Seasons(AllOrSpecific::All), - (SimpleRanges::All, SimpleRanges::All) | (SimpleRanges::Custom(_), SimpleRanges::Custom(_)) => { - unreachable!() + + // Handle both episodes and seasons specified + (SimpleRanges::Custom(episodes), SimpleRanges::Custom(seasons)) => { + EpisodesRequest::Combined { + seasons: AllOrSpecific::Specific(seasons), + episodes: AllOrSpecific::Specific(episodes), + } } - (SimpleRanges::All, SimpleRanges::Custom(_)) | (SimpleRanges::Custom(_), SimpleRanges::All) => { - unreachable!() + + // Handle cases where one is `All` and the other is `Custom` + (SimpleRanges::All, SimpleRanges::Custom(seasons)) => { + EpisodesRequest::Combined { + seasons: AllOrSpecific::Specific(seasons), + episodes: AllOrSpecific::All, + } } + (SimpleRanges::Custom(episodes), SimpleRanges::All) => { + EpisodesRequest::Combined { + seasons: AllOrSpecific::All, + episodes: AllOrSpecific::Specific(episodes), + } + } + + (SimpleRanges::All, SimpleRanges::All) => EpisodesRequest::All, } - } + } pub(crate) fn get_download_settings(&self) -> DownloadSettings Duration> { let wait_duration = Duration::from_millis(self.ddos_wait_ms as u64); diff --git a/src/downloaders/aniwave.rs b/src/downloaders/aniwave.rs index 7370545..dee7fcb 100644 --- a/src/downloaders/aniwave.rs +++ b/src/downloaders/aniwave.rs @@ -17,9 +17,11 @@ use crate::extractors::{ exists_extractor_with_name, extract_video_url_with_extractor_from_source, extract_video_url_with_extractor_from_url_unchecked, extractor_supports_source, }; - +/* +Aniwave.to is now Aniwave.se! TODO: Fix Aniwave code ! +*/ static URL_REGEX: Lazy = - Lazy::new(|| Regex::new(r#"(?i)^https?://(?:www\.)?aniwave\.to/watch/([^/\s]+)(?:/ep-([^/\s]+))?$"#).unwrap()); + Lazy::new(|| Regex::new(r#"(?i)^https?://(?:www\.)?aniwave\.se/aninme-watch/([^/\s]+)(?:/ep-([^/\s]+))?$"#).unwrap()); pub struct Aniwave<'driver> { driver: &'driver thirtyfour::WebDriver, @@ -152,7 +154,7 @@ impl TryFrom<&str> for ParsedUrl { impl ParsedUrl { fn get_anime_url(&self) -> String { - format!("https://aniwave.to/watch/{}", self.anime_id) + format!("https://aniwave.se/anime-watch/{}", self.anime_id) } fn get_episode_url(&self, episode_id: &str) -> String { @@ -192,7 +194,7 @@ impl<'driver, 'url, F: FnMut() -> Duration> Scraper<'driver, 'url, F> { async fn scrape(&mut self) -> Result<(), anyhow::Error> { let episodes_request = std::mem::replace(&mut self.request.episodes, EpisodesRequest::Unspecified); - + match episodes_request { EpisodesRequest::Unspecified => { if let Some(episode_id) = &self.parsed_url.episode_id { @@ -205,8 +207,17 @@ impl<'driver, 'url, F: FnMut() -> Duration> Scraper<'driver, 'url, F> { EpisodesRequest::Seasons(_) => { anyhow::bail!("AniWave does not support explicit seasons"); } + EpisodesRequest::Combined { .. } => { + // Handle combined case by returning an error + anyhow::bail!("AniWave does not support combined seasons and episodes"); + } + EpisodesRequest::All { .. } => { + // Handle All case by returning an error + anyhow::bail!("AniWave does not support All seasons and episodes"); + } } } + async fn scrape_season(&mut self, episodes: &AllOrSpecific) -> Result<(), anyhow::Error> { let anime_url = self.parsed_url.get_anime_url(); @@ -546,13 +557,13 @@ mod tests { #[tokio::test] async fn test_supports_url() { let is_supported = [ - "https://aniwave.to/watch/case-closed.myz/ep-316317", - "https://aniwave.to/watch/case-closed.myz/ep-345-b", - "https://aniwave.to/watch/case-closed.myz/ep-222-224", - "https://aniwave.to/watch/case-closed-crossroad-in-the-ancient-capital.mq2x", - "http://aniwave.to/watch/case-closed-crossroad-in-the-ancient-capital.mq2x", - "http://www.aniwave.to/watch/case-closed-crossroad-in-the-ancient-capital.mq2x", - "https://www.aniwave.to/watch/case-closed-crossroad-in-the-ancient-capital.mq2x", + "https://aniwave.se/anime-watch/detective-conan/ep-316317", + "https://aniwave.se/anime-watch/detective-conan/ep-345-b", + "https://aniwave.se/anime-watch/detective-conan/ep-222-224", + "https://aniwave.se/anime-watch/case-closed-crossroad-in-the-ancient-capital", + "http://aniwave.se/anime-watch/case-closed-crossroad-in-the-ancient-capital", + "http://www.aniwave.se/anime-watch/case-closed-crossroad-in-the-ancient-capital", + "https://www.aniwave.se/anime-watch/case-closed-crossroad-in-the-ancient-capital", ]; for url in is_supported { @@ -562,33 +573,33 @@ mod tests { #[test] fn test_parsed_url() { - let url1 = "https://aniwave.to/watch/case-closed.myz/ep-316317"; + let url1 = "https://aniwave.se/anime-watch/detective-conan/ep-316317"; let expected1 = ParsedUrl { - anime_id: "case-closed.myz".to_string(), + anime_id: "detective-conan".to_string(), episode_id: Some("316317".to_string()), }; - let url2 = "https://aniwave.to/watch/case-closed.myz/ep-345-b"; + /*let url2 = "https://aniwave.se/anime-watch/detective-conan/ep-345-b"; let expected2 = ParsedUrl { - anime_id: "case-closed.myz".to_string(), + anime_id: "detective-conan".to_string(), episode_id: Some("345-b".to_string()), - }; + };*/ - let url3 = "https://aniwave.to/watch/case-closed.myz/ep-222-224"; + let url3 = "https://aniwave.se/anime-watch/detective-conan/ep-222-224"; let expected3 = ParsedUrl { - anime_id: "case-closed.myz".to_string(), + anime_id: "detective-conan".to_string(), episode_id: Some("222-224".to_string()), }; - let url4 = "https://aniwave.to/watch/case-closed-crossroad-in-the-ancient-capital.mq2x"; + let url4 = "https://aniwave.to/watch/case-closed-crossroad-in-the-ancient-capital"; let expected4 = ParsedUrl { - anime_id: "case-closed-crossroad-in-the-ancient-capital.mq2x".to_string(), + anime_id: "case-closed-crossroad-in-the-ancient-capital".to_string(), episode_id: None, }; let tests = [ (url1, expected1), - (url2, expected2), + //(url2, expected2), (url3, expected3), (url4, expected4), ]; diff --git a/src/downloaders/aniworldserienstream.rs b/src/downloaders/aniworldserienstream.rs index d2820f4..fd0c06f 100644 --- a/src/downloaders/aniworldserienstream.rs +++ b/src/downloaders/aniworldserienstream.rs @@ -227,7 +227,7 @@ impl<'driver, 'url, F: FnMut() -> Duration> Scraper<'driver, 'url, F> { async fn scrape(&mut self) -> Result<(), anyhow::Error> { let episodes_request = std::mem::replace(&mut self.request.episodes, EpisodesRequest::Unspecified); - + match episodes_request { EpisodesRequest::Unspecified => { if let Some(season) = &self.parsed_url.season { @@ -244,9 +244,26 @@ impl<'driver, 'url, F: FnMut() -> Duration> Scraper<'driver, 'url, F> { let season = self.parsed_url.season.as_ref().map(|season| season.season).unwrap_or(1); self.scrape_season(season, &episodes).await } - EpisodesRequest::Seasons(seasons) => self.scrape_seasons(&seasons).await, + EpisodesRequest::Seasons(seasons) => { + self.scrape_seasons(&seasons).await + } + EpisodesRequest::Combined { seasons, episodes } => { + if let AllOrSpecific::Specific(season_ranges) = seasons { + for season_range in season_ranges { + for season in season_range.clone() { + self.scrape_season(season, &episodes).await?; + } + } + Ok(()) + } else { + Err(anyhow::anyhow!("Combined requests must specify specific seasons, not All.")) + } + } + EpisodesRequest::All { .. } => { + anyhow::bail!("AniWorld does not support All seasons and episodes"); + } } - } + } async fn scrape_seasons(&mut self, seasons: &AllOrSpecific) -> Result<(), anyhow::Error> { let first_episode_url = self.parsed_url.get_episode_url(1, 1); @@ -281,52 +298,52 @@ impl<'driver, 'url, F: FnMut() -> Duration> Scraper<'driver, 'url, F> { } async fn scrape_season(&mut self, season: u32, episodes: &AllOrSpecific) -> Result<(), anyhow::Error> { - let first_episode_url = self.parsed_url.get_episode_url(season, 1); - let mut already_is_on_page = false; - - if let Ok(current_url) = self.driver.current_url().await { - if current_url.as_str().eq_ignore_ascii_case(&first_episode_url) { - already_is_on_page = true; - } - } + let first_episode_url = self.parsed_url.get_episode_url(season, 1); + let mut already_is_on_page = false; - if !already_is_on_page { - self.driver - .goto(first_episode_url) - .await - .with_context(|| "failed to go to episode page")?; - sleep_random(1000..=2000).await; // wait until page has loaded - self.settings.maybe_ddos_wait().await; + if let Ok(current_url) = self.driver.current_url().await { + if current_url.as_str().eq_ignore_ascii_case(&first_episode_url) { + already_is_on_page = true; } + } - let max_episodes = self - .get_episode_info(season, 1) + if !already_is_on_page { + self.driver + .goto(first_episode_url) .await - .with_context(|| "failed to get episode info")? - .max_episode_number_in_season - .with_context(|| "failed to get maximum episode number in season")?; - - let mut goto = false; - let mut got_error = false; + .with_context(|| "failed to go to episode page")?; + sleep_random(1000..=2000).await; // wait until page has loaded + self.settings.maybe_ddos_wait().await; + } - for episode in 1..=max_episodes { - if episodes.contains(episode) { - if let Err(err) = self.scrape_episode(season, episode, goto).await { - log::warn!("Failed to get video url for S{season:02}E{episode:03}: {err:#}"); - got_error = true; - } + let max_episodes = self + .get_episode_info(season, 1) + .await + .with_context(|| "failed to get episode info")? + .max_episode_number_in_season + .with_context(|| "failed to get maximum episode number in season")?; + + let mut goto = false; + let mut got_error = false; + + // Scrape either all episodes or the specified ones + for episode in 1..=max_episodes { + if episodes.contains(episode) { + if let Err(err) = self.scrape_episode(season, episode, goto).await { + log::warn!("Failed to get video url for S{season:02}E{episode:03}: {err:#}"); + got_error = true; } - - goto = true; - } - - if got_error { - anyhow::bail!("failed to download complete season"); } + goto = true; + } - Ok(()) + if got_error { + anyhow::bail!("failed to download complete season"); } + Ok(()) +} + async fn scrape_episode(&mut self, season: u32, episode: u32, goto: bool) -> Result<(), anyhow::Error> { if goto { self.driver diff --git a/src/downloaders/mod.rs b/src/downloaders/mod.rs index 3a60678..15a7338 100644 --- a/src/downloaders/mod.rs +++ b/src/downloaders/mod.rs @@ -241,8 +241,10 @@ pub struct DownloadRequest { #[derive(Debug, Clone, PartialEq, Eq)] pub enum EpisodesRequest { Unspecified, + All, Episodes(AllOrSpecific), Seasons(AllOrSpecific), + Combined { seasons: AllOrSpecific, episodes: AllOrSpecific }, } #[derive(Debug, Clone, PartialEq, Eq)]