├── favicon.ico ├── favicon.png ├── clean.cmd ├── Juro.WebApi ├── publish-linux.cmd ├── publish-windows.cmd ├── Juro.WebApi.http ├── appsettings.Development.json ├── appsettings.json ├── Controllers │ ├── Anime │ │ ├── KaidoController.cs │ │ ├── AniwaveController.cs │ │ ├── AniwatchController.cs │ │ ├── AnimePaheController.cs │ │ ├── NineAnimeController.cs │ │ ├── OtakuDesuController.cs │ │ ├── AnimeController.cs │ │ ├── GogoanimeController.cs │ │ └── AnimeBaseController.cs │ ├── Manga │ │ ├── MangaController.cs │ │ ├── MangadexController.cs │ │ ├── MangaPillController.cs │ │ ├── MangaKakalotController.cs │ │ ├── MangaKatanaController.cs │ │ └── MangaBaseController.cs │ ├── ProvidersController.cs │ └── WeatherForecastController.cs ├── WeatherForecast.cs ├── Juro.WebApi.service ├── Juro.WebApi.csproj ├── Properties │ └── launchSettings.json └── Program.cs ├── Juro.Core ├── Models │ ├── Movie │ │ ├── TvType.cs │ │ ├── EpisodeServer.cs │ │ ├── Episode.cs │ │ ├── MovieResult.cs │ │ └── MovieInfo.cs │ ├── SubtitleType.cs │ ├── ProviderType.cs │ ├── Videos │ │ ├── VideoType.cs │ │ ├── StreamingServers.cs │ │ ├── VideoServer.cs │ │ └── VideoSource.cs │ ├── Manga │ │ ├── Mangadex │ │ │ ├── MangadexChapter.cs │ │ │ ├── MangadexDescription.cs │ │ │ ├── MangadexInfo.cs │ │ │ └── MangadexResult.cs │ │ ├── MediaStatus.cs │ │ ├── IMangaChapter.cs │ │ ├── IMangaResult.cs │ │ ├── IMangaChapterPage.cs │ │ ├── MangaChapter.cs │ │ ├── MangaResult.cs │ │ ├── MangaChapterPage.cs │ │ ├── IMangaInfo.cs │ │ └── MangaInfo.cs │ ├── AssemblyPluginType.cs │ ├── Anime │ │ ├── Indonesian │ │ │ └── OtakuDesu │ │ │ │ └── OtakuDesuAnimeInfo.cs │ │ ├── SubDub.cs │ │ ├── AnimePahe │ │ │ └── AnimePaheInfo.cs │ │ ├── Genre.cs │ │ ├── Episode.cs │ │ ├── IAnimeInfo.cs │ │ ├── AnimeInfo.cs │ │ └── AnimeSites.cs │ ├── Provider.cs │ ├── FileUrl.cs │ └── Subtitle.cs ├── IClientConfig.cs ├── AssemblyInfo.cs ├── Providers │ ├── IKey.cs │ ├── Base │ │ ├── ISourceProvider.cs │ │ └── IVideoExtractorProvider.cs │ ├── Anime │ │ ├── IAiringProvider.cs │ │ ├── INewSeasonProvider.cs │ │ ├── ILastUpdatedProvider.cs │ │ ├── IRecentlyAddedProvider.cs │ │ ├── IPopularProvider.cs │ │ └── IAnimeProvider.cs │ ├── IMovieProvider.cs │ └── IMangaProvider.cs ├── Utils │ ├── Html.cs │ ├── Extensions │ │ ├── ArrayExtensions.cs │ │ ├── DateTimeExtensions.cs │ │ ├── JsonExtensions.cs │ │ └── StreamExtensions.cs │ ├── Polyfills.Streams.cs │ ├── Logger.cs │ ├── Unbaser.cs │ ├── Tasks │ │ ├── ResizableSemaphore.netcore.cs │ │ ├── ResizableSemaphore.shared.cs │ │ ├── ResizableSemaphore.netstandard.cs │ │ └── TaskEx.cs │ └── Http.cs ├── HttpClientFactory.cs ├── Attributes │ └── PluginAssemblyAttribute.cs ├── Converters │ ├── IntegerConverter.cs │ ├── BoolConverter.cs │ └── JsonStringEnumConverter.cs ├── IHttpClientFactory.cs └── Juro.Core.csproj ├── Juro.Tests ├── xunit.runner.json ├── Specs │ ├── MainSpecs.cs │ ├── Manga │ │ ├── MangadexSpecs.cs │ │ ├── MangaPillSpecs.cs │ │ ├── AsuraScansSpecs.cs │ │ ├── MangaKatanaSpecs.cs │ │ └── MangaKakalotSpecs.cs │ └── Anime │ │ ├── KaidoSpecs.cs │ │ ├── AniwaveSpecs.cs │ │ ├── AniwatchSpecs.cs │ │ └── AnimePaheSpecs.cs └── Juro.Tests.csproj ├── Juro.Providers ├── AssemblyInfo.cs ├── Aniskip │ ├── AniSkipInterval.cs │ ├── AniSkipResponse.cs │ ├── SkipType.cs │ ├── Stamp.cs │ └── AniskipClient.cs ├── Config.cs ├── Anime │ ├── AniPlayProviderModel.cs │ ├── AniPlayEpisode.cs │ ├── Kaido.cs │ └── AnimeBaseProvider.cs ├── Juro.Providers.csproj └── Movie │ └── MovieBaseProvider.cs ├── .github ├── dependabot.yml └── workflows │ ├── remove-old-artifacts.yml │ └── nuget.yml ├── Juro.Demo.Cli2 ├── Juro.Demo.Cli2.csproj └── Program.cs ├── clean.ps1 ├── Juro.DemoConsole ├── Juro.DemoConsole.csproj ├── Utils │ └── ConsoleProgress.cs └── Program.cs ├── Juro.DataBuilder ├── Models │ ├── GenreModel.cs │ ├── AnimeModel.cs │ └── ManamiAnimeItem.cs ├── Juro.DataBuilder.csproj └── JuroContext.cs ├── Juro ├── IJuroClient.cs ├── Plugin.cs ├── Clients │ ├── AnimeClient.cs │ ├── MangaClient.cs │ ├── MovieClient.cs │ ├── ClientBase.cs │ └── MangaApiClient.cs ├── Utils │ ├── PluginLoadContext.cs │ └── AssemblyEx.cs └── Juro.csproj ├── Juro.Extractors ├── Juro.Extractors.csproj ├── IVideoExtractor.cs ├── OkRuExtractor.cs ├── StreamTapeExtractor.cs ├── FilemoonExtractor.cs ├── AWishExtractor.cs ├── VidStreamExtractor.cs ├── FPlayerExtractor.cs ├── ALionsExtractor.cs ├── YourUploadExtractor.cs ├── AniwaveExtractor.cs ├── Mp4uploadExtractor.cs ├── StreamSBExtractor.cs ├── DoodExtractor.cs └── StreamSBProExtractor.cs ├── README.md ├── Directory.Build.props ├── .gitattributes └── .editorconfig /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerry08/Juro/HEAD/favicon.ico -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerry08/Juro/HEAD/favicon.png -------------------------------------------------------------------------------- /clean.cmd: -------------------------------------------------------------------------------- 1 | powershell -ExecutionPolicy Bypass -File clean.ps1 2 | 3 | pause -------------------------------------------------------------------------------- /Juro.WebApi/publish-linux.cmd: -------------------------------------------------------------------------------- 1 | dotnet publish -c Release --os linux -o build/linux 2 | pause -------------------------------------------------------------------------------- /Juro.WebApi/publish-windows.cmd: -------------------------------------------------------------------------------- 1 | dotnet publish -c Release -r win-x64 -o build/windows 2 | pause -------------------------------------------------------------------------------- /Juro.Core/Models/Movie/TvType.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Movie; 2 | 3 | public enum TvType 4 | { 5 | Movie, 6 | TvSeries, 7 | } 8 | -------------------------------------------------------------------------------- /Juro.Core/Models/SubtitleType.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models; 2 | 3 | public enum SubtitleType 4 | { 5 | VTT, 6 | ASS, 7 | SRT, 8 | } 9 | -------------------------------------------------------------------------------- /Juro.Core/Models/ProviderType.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models; 2 | 3 | public enum ProviderType 4 | { 5 | Anime, 6 | Manga, 7 | Movie, 8 | } 9 | -------------------------------------------------------------------------------- /Juro.Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "methodDisplayOptions": "all", 4 | "methodDisplay": "method" 5 | } -------------------------------------------------------------------------------- /Juro.WebApi/Juro.WebApi.http: -------------------------------------------------------------------------------- 1 | @Juro.WebApi_HostAddress = http://localhost:5023 2 | 3 | GET {{Juro.WebApi_HostAddress}}/weatherforecast/ 4 | Accept: application/json 5 | 6 | ### 7 | -------------------------------------------------------------------------------- /Juro.Core/Models/Videos/VideoType.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Videos; 2 | 3 | public enum VideoType 4 | { 5 | Container, 6 | M3u8, 7 | Hls, 8 | Dash, 9 | } 10 | -------------------------------------------------------------------------------- /Juro.WebApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/Mangadex/MangadexChapter.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Manga.Mangadex; 2 | 3 | public class MangadexChapter : MangaChapter 4 | { 5 | public int Pages { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /Juro.WebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/MediaStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Manga; 2 | 3 | public enum MediaStatus 4 | { 5 | Unknown, 6 | Completed, 7 | Ongoing, 8 | Hiatus, 9 | Cancelled, 10 | } 11 | -------------------------------------------------------------------------------- /Juro.Providers/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using Juro.Core.Attributes; 2 | using Juro.Core.Models; 3 | 4 | [assembly: PluginAssembly( 5 | AssemblyPluginType.Anime | AssemblyPluginType.Manga | AssemblyPluginType.Movie 6 | )] 7 | -------------------------------------------------------------------------------- /Juro.Providers/Aniskip/AniSkipInterval.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Providers.Aniskip; 2 | 3 | public class AniSkipInterval 4 | { 5 | public double StartTime { get; set; } 6 | 7 | public double EndTime { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Juro.Core/IClientConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Juro.Core; 4 | 5 | public interface IClientConfig 6 | { 7 | string RepositoryUrl { get; } 8 | 9 | Version MinimumSupportedVersion { get; } 10 | } 11 | -------------------------------------------------------------------------------- /Juro.Core/Models/Movie/EpisodeServer.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Movie; 2 | 3 | public class EpisodeServer 4 | { 5 | public string Name { get; set; } = default!; 6 | 7 | public string Url { get; set; } = default!; 8 | } 9 | -------------------------------------------------------------------------------- /Juro.Core/Models/AssemblyPluginType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Juro.Core.Models; 4 | 5 | [Flags] 6 | public enum AssemblyPluginType 7 | { 8 | None, 9 | Movie, 10 | Manga, 11 | Anime, 12 | Novel, 13 | } 14 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/Mangadex/MangadexDescription.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Manga.Mangadex; 2 | 3 | public class MangadexDescription 4 | { 5 | public string? Language { get; set; } 6 | 7 | public string? Description { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Juro.Core/Models/Anime/Indonesian/OtakuDesu/OtakuDesuAnimeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Anime.Indonesian.OtakuDesu; 2 | 3 | /// 4 | public class OtakuDesuAnimeInfo : AnimeInfo 5 | { 6 | public float Rating { get; set; } 7 | 8 | public string? Studio { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /Juro.Providers/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Juro.Core; 3 | 4 | namespace Juro.Providers; 5 | 6 | public class Config : IClientConfig 7 | { 8 | public string RepositoryUrl => "https://github.com/jerry08/Juro"; 9 | 10 | public Version MinimumSupportedVersion => new(1, 0, 0); 11 | } 12 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Anime/KaidoController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Anime; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Anime; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class KaidoController(Kaido provider) : AnimeBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Anime/AniwaveController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Anime; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Anime; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class AniwaveController(Aniwave provider) : AnimeBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Manga/MangaController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Manga; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Manga; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class MangaController(Mangadex provider) : MangadexController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.Core/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Juro")] 4 | [assembly: InternalsVisibleTo("Juro.DataBuilder")] 5 | [assembly: InternalsVisibleTo("Juro.Extractors")] 6 | [assembly: InternalsVisibleTo("Juro.Providers")] 7 | [assembly: InternalsVisibleTo("Juro.Providers.Adult")] 8 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Anime/AniwatchController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Anime; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Anime; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class AniwatchController(Aniwatch provider) : AnimeBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Manga/MangadexController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Manga; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Manga; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class MangadexController(Mangadex provider) : MangaBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Anime/AnimePaheController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Anime; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Anime; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class AnimePaheController(AnimePahe provider) : AnimeBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Anime/NineAnimeController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Anime; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Anime; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class NineAnimeController(NineAnime provider) : AnimeBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Manga/MangaPillController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Manga; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Manga; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class MangaPillController(MangaPill provider) : MangaBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.WebApi/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.WebApi; 2 | 3 | public class WeatherForecast 4 | { 5 | public DateOnly Date { get; set; } 6 | 7 | public int TemperatureC { get; set; } 8 | 9 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 10 | 11 | public string? Summary { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Manga/MangaKakalotController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Manga; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Manga; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class MangaKakalotController(MangaKakalot provider) : MangaBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Manga/MangaKatanaController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Manga; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Manga; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class MangaKatanaController(MangaKatana provider) : MangaBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.Core/Providers/IKey.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Providers; 2 | 3 | /// 4 | /// Interface to get unique unchanging key from providers. 5 | /// 6 | public interface IKey 7 | { 8 | /// 9 | /// Unique unchanging key of the provider. 10 | /// 11 | public string Key { get; } 12 | } 13 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Anime/OtakuDesuController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Anime.Indonesian; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Anime; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class OtakuDesuController(OtakuDesu provider) : AnimeBaseController(provider) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Html.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | 3 | namespace Juro.Core.Utils; 4 | 5 | internal static class Html 6 | { 7 | public static HtmlDocument Parse(string source) 8 | { 9 | var document = new HtmlDocument(); 10 | document.LoadHtml(HtmlEntity.DeEntitize(source) ?? string.Empty); 11 | return document; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/IMangaChapter.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Manga; 2 | 3 | public interface IMangaChapter 4 | { 5 | public string Id { get; set; } 6 | 7 | public float Number { get; set; } 8 | 9 | public string? Title { get; set; } 10 | 11 | public string? Views { get; set; } 12 | 13 | public string? ReleasedDate { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/Mangadex/MangadexInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Manga.Mangadex; 4 | 5 | public class MangadexInfo : MangaInfo 6 | { 7 | /// 8 | /// Year released 9 | /// 10 | public int ReleaseDate { get; set; } 11 | 12 | public List Themes { get; set; } = []; 13 | } 14 | -------------------------------------------------------------------------------- /Juro.Core/Models/Videos/StreamingServers.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Videos; 2 | 3 | public enum StreamingServers 4 | { 5 | AsianLoad, 6 | GogoCDN, 7 | StreamSB, 8 | MixDrop, 9 | UpCloud, 10 | VidCloud, 11 | StreamTape, 12 | VizCloud, 13 | 14 | // same as vizcloud 15 | MyCloud, 16 | Filemoon, 17 | VidStreaming, 18 | } 19 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Anime/AnimeController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Anime; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Anime; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | [Obsolete("Gogoanime/Anitaku is officially dead.")] 9 | public class AnimeController(Gogoanime provider) : GogoanimeController(provider) 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/IMangaResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Manga; 4 | 5 | public interface IMangaResult 6 | { 7 | public string Id { get; set; } 8 | 9 | public string? Title { get; set; } 10 | 11 | public string? Image { get; set; } 12 | 13 | public Dictionary Headers { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/IMangaChapterPage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Manga; 4 | 5 | public interface IMangaChapterPage 6 | { 7 | public string Image { get; set; } 8 | 9 | public int Page { get; set; } 10 | 11 | public string? Title { get; set; } 12 | 13 | public Dictionary Headers { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Juro.Core/Models/Provider.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models; 2 | 3 | public class Provider 4 | { 5 | public string Key { get; set; } = default!; 6 | 7 | public string Name { get; set; } = default!; 8 | 9 | public string Language { get; set; } = default!; 10 | 11 | public ProviderType Type { get; set; } 12 | 13 | public override string ToString() => Name; 14 | } 15 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Anime/GogoanimeController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Providers.Anime; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers.Anime; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | [Obsolete("Gogoanime/Anitaku is officially dead.")] 9 | public class GogoanimeController(Gogoanime provider) : AnimeBaseController(provider) 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /Juro.Core/Models/Movie/Episode.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Movie; 2 | 3 | public class Episode 4 | { 5 | public string Id { get; set; } = default!; 6 | 7 | public string Title { get; set; } = default!; 8 | 9 | public int Number { get; set; } = default!; 10 | 11 | public int Season { get; set; } = default!; 12 | 13 | public string Url { get; set; } = default!; 14 | } 15 | -------------------------------------------------------------------------------- /Juro.Core/Models/Movie/MovieResult.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Movie; 2 | 3 | public class MovieResult 4 | { 5 | public string Id { get; set; } = default!; 6 | 7 | public string? Title { get; set; } 8 | 9 | public string? Url { get; set; } 10 | 11 | public string? Image { get; set; } 12 | 13 | public string? ReleasedDate { get; set; } 14 | 15 | public TvType Type { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/ProvidersController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Core.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Juro.WebApi.Controllers; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class ProvidersController : ControllerBase 9 | { 10 | [HttpGet] 11 | public IEnumerable Get(ProviderType type) => 12 | Program.Providers.Where(x => x.Type == type); 13 | } 14 | -------------------------------------------------------------------------------- /Juro.Core/HttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace Juro.Core; 5 | 6 | internal class HttpClientFactory(Func httpClientFunc) : IHttpClientFactory 7 | { 8 | private readonly Func _httpClientFunc = httpClientFunc; 9 | 10 | public HttpClientFactory() 11 | : this(() => new()) { } 12 | 13 | public HttpClient CreateClient() => _httpClientFunc(); 14 | } 15 | -------------------------------------------------------------------------------- /Juro.Providers/Aniskip/AniSkipResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Juro.Providers.Aniskip; 5 | 6 | public class AniSkipResponse 7 | { 8 | [JsonPropertyName("found")] 9 | public bool IsFound { get; set; } 10 | 11 | public List? Results { get; set; } 12 | 13 | public string? Message { get; set; } 14 | 15 | public int StatusCode { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Juro.Core/Providers/Base/ISourceProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Providers; 2 | 3 | /// 4 | /// Base interface for main providers. 5 | /// 6 | public interface ISourceProvider 7 | { 8 | /// 9 | /// Name of the provider. 10 | /// 11 | public string Name { get; } 12 | 13 | /// 14 | /// Language of the provider. 15 | /// 16 | public string Language { get; } 17 | } 18 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Extensions/ArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Utils.Extensions; 2 | 3 | internal static class ArrayExtensions 4 | { 5 | public static T[] CopyOfRange(this T[] source, int fromIndex, int toIndex) 6 | { 7 | var len = toIndex - fromIndex; 8 | var dest = new T[len]; 9 | 10 | for (var i = 0; i < len; i++) 11 | dest[i] = source[fromIndex + i]; 12 | 13 | return dest; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/MangaChapter.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Manga; 2 | 3 | public class MangaChapter : IMangaChapter 4 | { 5 | public string Id { get; set; } = default!; 6 | 7 | public float Number { get; set; } 8 | 9 | public string? Title { get; set; } 10 | 11 | public string? Views { get; set; } 12 | 13 | public string? ReleasedDate { get; set; } 14 | 15 | public override string ToString() => $"({Number}) {Title}"; 16 | } 17 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/MangaResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Manga; 4 | 5 | public class MangaResult : IMangaResult 6 | { 7 | public string Id { get; set; } = default!; 8 | 9 | public string? Title { get; set; } 10 | 11 | public string? Image { get; set; } 12 | 13 | public Dictionary Headers { get; set; } = []; 14 | 15 | public override string ToString() => $"{Title}"; 16 | } 17 | -------------------------------------------------------------------------------- /Juro.Providers/Aniskip/SkipType.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Juro.Providers.Aniskip; 4 | 5 | public enum SkipType 6 | { 7 | [EnumMember(Value = "op")] 8 | Opening, 9 | 10 | [EnumMember(Value = "ed")] 11 | Ending, 12 | 13 | [EnumMember(Value = "recap")] 14 | Recap, 15 | 16 | [EnumMember(Value = "mixed-op")] 17 | MixedOpening, 18 | 19 | [EnumMember(Value = "mixed-ed")] 20 | MixedEnding, 21 | } 22 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/MangaChapterPage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Manga; 4 | 5 | public class MangaChapterPage : IMangaChapterPage 6 | { 7 | public string Image { get; set; } = default!; 8 | 9 | public int Page { get; set; } 10 | 11 | public string? Title { get; set; } 12 | 13 | public Dictionary Headers { get; set; } = []; 14 | 15 | public override string ToString() => $"{Title}"; 16 | } 17 | -------------------------------------------------------------------------------- /Juro.Core/Models/Anime/SubDub.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Anime; 2 | 3 | /// 4 | /// Used to select sub or dub for anime. 5 | /// 6 | public enum SubDub 7 | { 8 | /// 9 | /// Selects all available (sub and dub). 10 | /// 11 | All, 12 | 13 | /// 14 | /// Selects sub. 15 | /// 16 | Sub, 17 | 18 | /// 19 | /// Selects dub. 20 | /// 21 | Dub, 22 | } 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | labels: 8 | - enhancement 9 | groups: 10 | actions: 11 | patterns: 12 | - "*" 13 | - package-ecosystem: nuget 14 | directory: "/" 15 | schedule: 16 | interval: monthly 17 | labels: 18 | - enhancement 19 | groups: 20 | nuget: 21 | patterns: 22 | - "*" 23 | -------------------------------------------------------------------------------- /Juro.Providers/Aniskip/Stamp.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JsonStringEnumConverter = Juro.Core.Converters.JsonStringEnumConverter; 3 | 4 | namespace Juro.Providers.Aniskip; 5 | 6 | public class Stamp 7 | { 8 | public AniSkipInterval Interval { get; set; } = default!; 9 | 10 | [JsonConverter(typeof(JsonStringEnumConverter))] 11 | public SkipType SkipType { get; set; } 12 | 13 | public string SkipId { get; set; } = default!; 14 | 15 | public double EpisodeLength { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Juro.Core/Attributes/PluginAssemblyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Juro.Core.Models; 3 | 4 | namespace Juro.Core.Attributes; 5 | 6 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] 7 | public sealed class PluginAssemblyAttribute : Attribute 8 | { 9 | public AssemblyPluginType PluginType { get; } = AssemblyPluginType.None; 10 | 11 | public PluginAssemblyAttribute() { } 12 | 13 | public PluginAssemblyAttribute(AssemblyPluginType type) 14 | { 15 | PluginType = type; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Juro.Core/Models/FileUrl.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models; 4 | 5 | public class FileUrl 6 | { 7 | public string Url { get; set; } = default!; 8 | 9 | public Dictionary Headers { get; set; } = []; 10 | 11 | public FileUrl() { } 12 | 13 | public FileUrl(string url) 14 | { 15 | Url = url; 16 | } 17 | 18 | public FileUrl(string url, Dictionary headers) 19 | { 20 | Url = url; 21 | Headers = headers; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/MainSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using FluentAssertions; 3 | using Juro.Clients; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs; 7 | 8 | public class MainSpecs 9 | { 10 | [Fact] 11 | public void Each_provider_has_a_unique_key() 12 | { 13 | // Arrange 14 | var client = new AnimeClient(); 15 | 16 | // Act 17 | var results = client.GetAllProviders().GroupBy(x => x.Key).Where(x => x.Count() > 1); 18 | 19 | // Assert 20 | results.Should().HaveCount(0); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Juro.Demo.Cli2/Juro.Demo.Cli2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | ../favicon.ico 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Juro.WebApi/Juro.WebApi.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Juro.WebApi 3 | 4 | [Service] 5 | WorkingDirectory=/var/www/test2/ 6 | ExecStart=/usr/bin/dotnet /var/www/test2/Juro.WebApi.dll 7 | Restart=always 8 | # Restart service after 10 seconds if the dotnet service crashes: 9 | RestartSec=10 10 | KillSignal=SIGINT 11 | SyslogIdentifier=dotnet-LinuxWebAppTest 12 | User=root 13 | Environment=ASPNETCORE_ENVIRONMENT=Production 14 | Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false 15 | Environment=ASPNETCORE_URLS=http://localhost:5001 16 | 17 | [Install] 18 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /clean.ps1: -------------------------------------------------------------------------------- 1 | # Define files and directories to delete 2 | $include = @("bin","obj","packages","x64","build","node_modules") 3 | 4 | # Define files and directories to exclude 5 | $exclude = @() 6 | 7 | $items = Get-ChildItem . -recurse -force -include $include -exclude $exclude 8 | 9 | if ($items) { 10 | foreach ($item in $items) { 11 | Remove-Item $item.FullName -Force -Recurse -ErrorAction SilentlyContinue 12 | Write-Host "Deleted" $item.FullName 13 | } 14 | } 15 | 16 | Write-Host "Press any key to continue . . ." 17 | $x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") -------------------------------------------------------------------------------- /.github/workflows/remove-old-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Remove old artifacts 2 | 3 | on: 4 | schedule: 5 | # Every day at 1am 6 | - cron: '0 1 * * *' 7 | 8 | jobs: 9 | remove-old-artifacts: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 10 12 | permissions: write-all 13 | 14 | steps: 15 | - name: Remove old artifacts 16 | uses: c-hive/gha-remove-artifacts@v1 17 | with: 18 | age: '1 day' # ' ', e.g. 5 days, 2 years, 90 seconds, parsed by Moment.js 19 | # Optional inputs 20 | # skip-tags: true 21 | # skip-recent: 5 -------------------------------------------------------------------------------- /Juro.Providers/Anime/AniPlayProviderModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Juro.Providers.Anime; 5 | 6 | public class AniPlayProviderModel 7 | { 8 | [JsonPropertyName("episodes")] 9 | public List Episodes { get; set; } = []; 10 | 11 | [JsonPropertyName("providerId")] 12 | public string ProviderId { get; set; } = default!; 13 | 14 | [JsonPropertyName("default")] 15 | public bool IsDefault { get; set; } 16 | 17 | [JsonPropertyName("dub")] 18 | public bool IsDub { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /Juro.Core/Models/Anime/AnimePahe/AnimePaheInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Anime.AnimePahe; 2 | 3 | /// 4 | public class AnimePaheInfo : AnimeInfo 5 | { 6 | public int AnilistId { get; set; } 7 | 8 | public int KitsuId { get; set; } 9 | 10 | public int AnidbId { get; set; } 11 | 12 | public int AnimeNewsNetworkId { get; set; } 13 | 14 | public int MalId { get; set; } 15 | 16 | public int Episodes { get; set; } 17 | 18 | public string Season { get; set; } = default!; 19 | 20 | public int Year { get; set; } 21 | 22 | public float Score { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /Juro.Core/Models/Anime/Genre.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Anime; 2 | 3 | /// 4 | /// The Class which contains all the information about a Genre. 5 | /// 6 | public class Genre 7 | { 8 | public string Name { get; set; } = default!; 9 | 10 | public string? Url { get; set; } 11 | 12 | public Genre() { } 13 | 14 | public Genre(string name) 15 | { 16 | Name = name; 17 | } 18 | 19 | public Genre(string name, string url) 20 | { 21 | Name = name; 22 | Url = url; 23 | } 24 | 25 | public override string ToString() => $"{Name}"; 26 | } 27 | -------------------------------------------------------------------------------- /Juro.DemoConsole/Juro.DemoConsole.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | ../favicon.ico 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Juro.DataBuilder/Models/GenreModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Juro.DataBuilder.Models; 4 | 5 | public class GenreModel 6 | { 7 | [Key] 8 | public int GenreId { get; set; } 9 | 10 | public string Name { get; set; } = default!; 11 | 12 | public string? Url { get; set; } 13 | 14 | public GenreModel() { } 15 | 16 | public GenreModel(string name) 17 | { 18 | Name = name; 19 | } 20 | 21 | public GenreModel(string name, string url) 22 | { 23 | Name = name; 24 | Url = url; 25 | } 26 | 27 | public override string ToString() => $"{Name}"; 28 | } 29 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/Mangadex/MangadexResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Manga.Mangadex; 4 | 5 | public class MangadexResult : MangaResult 6 | { 7 | public List AltTitles { get; set; } = []; 8 | 9 | public List Descriptions { get; set; } = []; 10 | 11 | public MediaStatus Status { get; set; } 12 | 13 | /// 14 | /// Year released 15 | /// 16 | public int? ReleaseDate { get; set; } 17 | 18 | public string? ContentRating { get; set; } 19 | 20 | public string? LastVolume { get; set; } 21 | 22 | public string? LastChapter { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /Juro.Core/Providers/Anime/IAiringProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Anime; 5 | 6 | namespace Juro.Core.Providers; 7 | 8 | /// 9 | /// Interface for basic operations related to airing sources. 10 | /// 11 | public interface IAiringProvider 12 | { 13 | /// 14 | /// Search for anime. 15 | /// 16 | /// A of s. 17 | ValueTask> GetAiringAsync( 18 | int page = 1, 19 | CancellationToken cancellationToken = default 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /Juro.Core/Models/Anime/Episode.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Anime; 2 | 3 | /// 4 | /// The Class which contains all the information about an Episode 5 | /// 6 | public class Episode 7 | { 8 | public string Id { get; set; } = default!; 9 | 10 | public string? Name { get; set; } 11 | 12 | public string? Description { get; set; } 13 | 14 | public float Number { get; set; } 15 | 16 | public float Duration { get; set; } 17 | 18 | public string? Link { get; set; } 19 | 20 | public string? Image { get; set; } 21 | 22 | public float Progress { get; set; } 23 | 24 | public override string ToString() => $"Episode {Number}"; 25 | } 26 | -------------------------------------------------------------------------------- /Juro.Core/Providers/Anime/INewSeasonProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Anime; 5 | 6 | namespace Juro.Core.Providers; 7 | 8 | /// 9 | /// Interface for basic operations related to new seasonal sources. 10 | /// 11 | public interface INewSeasonProvider 12 | { 13 | /// 14 | /// Search for anime. 15 | /// 16 | /// A of s. 17 | ValueTask> GetNewSeasonAsync( 18 | int page = 1, 19 | CancellationToken cancellationToken = default 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /Juro.Core/Providers/Anime/ILastUpdatedProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Anime; 5 | 6 | namespace Juro.Core.Providers; 7 | 8 | /// 9 | /// Interface for basic operations related to last updated sources. 10 | /// 11 | public interface ILastUpdatedProvider 12 | { 13 | /// 14 | /// Search for anime. 15 | /// 16 | /// A of s. 17 | ValueTask> GetLastUpdatedAsync( 18 | int page = 1, 19 | CancellationToken cancellationToken = default 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /Juro.Core/Providers/Anime/IRecentlyAddedProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Anime; 5 | 6 | namespace Juro.Core.Providers; 7 | 8 | /// 9 | /// Interface for basic operations related to recently added sources. 10 | /// 11 | public interface IRecentlyAddedProvider 12 | { 13 | /// 14 | /// Search for anime. 15 | /// 16 | /// A of s. 17 | ValueTask> GetRecentlyAddedAsync( 18 | int page = 1, 19 | CancellationToken cancellationToken = default 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /Juro.WebApi/Juro.WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/IMangaInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Manga; 4 | 5 | public interface IMangaInfo 6 | { 7 | public string Id { get; set; } 8 | 9 | public string? Title { get; set; } 10 | 11 | public List AltTitles { get; set; } 12 | 13 | public string? Description { get; set; } 14 | 15 | public string? Image { get; set; } 16 | 17 | public Dictionary Headers { get; set; } 18 | 19 | public List Genres { get; set; } 20 | 21 | public MediaStatus Status { get; set; } 22 | 23 | public string? Views { get; set; } 24 | 25 | public List Authors { get; set; } 26 | 27 | public List Chapters { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Juro.Core/Providers/Anime/IPopularProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Anime; 5 | 6 | namespace Juro.Core.Providers; 7 | 8 | /// 9 | /// Interface for basic operations related to popular sources. 10 | /// 11 | public interface IPopularProvider 12 | { 13 | /// 14 | /// Search for anime. 15 | /// 16 | /// 17 | /// 18 | /// A of s. 19 | ValueTask> GetPopularAsync( 20 | int page = 1, 21 | CancellationToken cancellationToken = default 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /Juro/IJuroClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Juro.Core.Providers; 3 | 4 | namespace Juro; 5 | 6 | public interface IJuroClient 7 | where IProvider : ISourceProvider 8 | { 9 | /// 10 | /// Gets all providers in currenly loaded plugin. 11 | /// 12 | IList GetAllProviders(); 13 | 14 | /// 15 | /// Gets providers in currenly loaded assemblies. 16 | /// 17 | /// File path to plugin. 18 | /// Language (Culture name) of the provider. 19 | IList GetProviders(string? filePath = null, string? language = null); 20 | } 21 | 22 | public interface IJuroClient : IJuroClient { } 23 | -------------------------------------------------------------------------------- /Juro/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using Juro.Core; 5 | 6 | namespace Juro; 7 | 8 | public class Plugin 9 | { 10 | public string Name { get; set; } = default!; 11 | 12 | public string FilePath { get; set; } = default!; 13 | 14 | public Version? Version { get; set; } 15 | 16 | public IClientConfig? ClientConfig { get; set; } 17 | 18 | public Plugin() { } 19 | 20 | public Plugin(string? name, string filePath) 21 | { 22 | Name = name ?? Path.GetFileNameWithoutExtension(filePath); 23 | FilePath = filePath; 24 | 25 | _ = Version.TryParse(FileVersionInfo.GetVersionInfo(filePath).FileVersion, out var version); 26 | Version = version; 27 | } 28 | 29 | public override string ToString() => $"{Name} {Version}"; 30 | } 31 | -------------------------------------------------------------------------------- /Juro.Core/Providers/Base/IVideoExtractorProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Videos; 5 | 6 | namespace Juro.Core.Providers; 7 | 8 | /// 9 | /// Interface for basic operations related to an anime provider. 10 | /// 11 | public interface IVideoExtractorProvider 12 | { 13 | /// 14 | /// Gets video sources for episode from server. 15 | /// 16 | /// The server of the episode. 17 | /// 18 | /// A of s. 19 | ValueTask> GetVideosAsync( 20 | VideoServer server, 21 | CancellationToken cancellationToken = default 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /Juro.Core/Models/Anime/IAnimeInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Anime; 4 | 5 | /// 6 | /// The Class which contains all the information about an Anime 7 | /// 8 | public interface IAnimeInfo 9 | { 10 | public string Id { get; set; } 11 | 12 | public AnimeSites Site { get; set; } 13 | 14 | public string Title { get; set; } 15 | 16 | public string? Released { get; set; } 17 | 18 | public string? Category { get; set; } 19 | 20 | public string? Link { get; set; } 21 | 22 | public string? Image { get; set; } 23 | 24 | public string? Type { get; set; } 25 | 26 | public string? Status { get; set; } 27 | 28 | public string? OtherNames { get; set; } 29 | 30 | public string? Summary { get; set; } 31 | 32 | public List Genres { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /Juro.Core/Models/Manga/MangaInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Manga; 4 | 5 | public class MangaInfo : IMangaInfo 6 | { 7 | public string Id { get; set; } = default!; 8 | 9 | public string? Title { get; set; } 10 | 11 | public List AltTitles { get; set; } = []; 12 | 13 | public string? Description { get; set; } 14 | 15 | public string? Image { get; set; } 16 | 17 | public Dictionary Headers { get; set; } = []; 18 | 19 | public List Genres { get; set; } = []; 20 | 21 | public MediaStatus Status { get; set; } 22 | 23 | public string? Views { get; set; } 24 | 25 | public List Authors { get; set; } = []; 26 | 27 | public List Chapters { get; set; } = []; 28 | 29 | public override string ToString() => $"{Title}"; 30 | } 31 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Extensions/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Juro.Core.Utils.Extensions; 4 | 5 | internal static class DateTimeExtensions 6 | { 7 | #if NETCOREAPP || NETSTANDARD2_1 8 | private static readonly DateTime Jan1st1970 = DateTime.UnixEpoch; 9 | #else 10 | private static readonly DateTime Jan1st1970 = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 11 | #endif 12 | 13 | public static long CurrentTimeMillis() 14 | { 15 | return (long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds; 16 | } 17 | 18 | public static long CurrentTimeMillis(this DateTime date) 19 | { 20 | return (long)(date - Jan1st1970).TotalMilliseconds; 21 | } 22 | 23 | public static long ToUnixTimeMilliseconds(this DateTime dateTime) 24 | { 25 | return (long)(dateTime - Jan1st1970).TotalMilliseconds; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Juro.Core/Models/Movie/MovieInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Movie; 4 | 5 | public class MovieInfo 6 | { 7 | public string Id { get; set; } = default!; 8 | 9 | public string? Title { get; set; } 10 | 11 | public string? Image { get; set; } 12 | 13 | public string? Description { get; set; } 14 | 15 | public string? ReleasedDate { get; set; } 16 | 17 | public TvType Type { get; set; } 18 | 19 | public List Genres { get; set; } = []; 20 | 21 | public List Tags { get; set; } = []; 22 | 23 | public List Casts { get; set; } = []; 24 | 25 | public string? Production { get; set; } 26 | 27 | public string? Country { get; set; } 28 | 29 | public string? Duration { get; set; } 30 | 31 | public string? Rating { get; set; } 32 | 33 | public List Episodes { get; set; } = []; 34 | } 35 | -------------------------------------------------------------------------------- /Juro.Core/Converters/IntegerConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Juro.Core.Converters; 6 | 7 | internal class IntegerConverter : JsonConverter 8 | { 9 | public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) => 10 | writer.WriteNumberValue(value); 11 | 12 | public override int Read( 13 | ref Utf8JsonReader reader, 14 | Type typeToConvert, 15 | JsonSerializerOptions options 16 | ) => 17 | reader.TokenType switch 18 | { 19 | JsonTokenType.String => int.TryParse(reader.GetString(), out var b) 20 | ? b 21 | : throw new JsonException(), 22 | JsonTokenType.Number => reader.TryGetInt32(out var l) ? l : throw new JsonException(), 23 | _ => throw new JsonException(), 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /Juro.DataBuilder/Juro.DataBuilder.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Juro.Extractors/Juro.Extractors.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;net9.0 4 | true 5 | 6 | 7 | 8 | Extractors for source providers. 9 | 10 | favicon.png 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | false 22 | runtime 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Juro/Clients/AnimeClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using Juro.Core; 4 | using Juro.Core.Providers; 5 | using Juro.Core.Utils; 6 | 7 | namespace Juro.Clients; 8 | 9 | /// 10 | /// Client for managining all available anime providers. 11 | /// 12 | /// 13 | /// Initializes an instance of . 14 | /// 15 | public class AnimeClient(IHttpClientFactory httpClientFactory) 16 | : ClientBase(httpClientFactory) 17 | { 18 | /// 19 | /// Initializes an instance of . 20 | /// 21 | public AnimeClient(Func httpClientProvider) 22 | : this(new HttpClientFactory(httpClientProvider)) { } 23 | 24 | /// 25 | /// Initializes an instance of . 26 | /// 27 | public AnimeClient() 28 | : this(Http.ClientProvider) { } 29 | } 30 | -------------------------------------------------------------------------------- /Juro/Clients/MangaClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using Juro.Core; 4 | using Juro.Core.Providers; 5 | using Juro.Core.Utils; 6 | 7 | namespace Juro.Clients; 8 | 9 | /// 10 | /// Client for managining all available manga providers. 11 | /// 12 | /// 13 | /// Initializes an instance of . 14 | /// 15 | public class MangaClient(IHttpClientFactory httpClientFactory) 16 | : ClientBase(httpClientFactory) 17 | { 18 | /// 19 | /// Initializes an instance of . 20 | /// 21 | public MangaClient(Func httpClientProvider) 22 | : this(new HttpClientFactory(httpClientProvider)) { } 23 | 24 | /// 25 | /// Initializes an instance of . 26 | /// 27 | public MangaClient() 28 | : this(Http.ClientProvider) { } 29 | } 30 | -------------------------------------------------------------------------------- /Juro/Clients/MovieClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using Juro.Core; 4 | using Juro.Core.Providers; 5 | using Juro.Core.Utils; 6 | 7 | namespace Juro.Clients; 8 | 9 | /// 10 | /// Client for managining all available movie providers. 11 | /// 12 | /// 13 | /// Initializes an instance of . 14 | /// 15 | public class MovieClient(IHttpClientFactory httpClientFactory) 16 | : ClientBase(httpClientFactory) 17 | { 18 | /// 19 | /// Initializes an instance of . 20 | /// 21 | public MovieClient(Func httpClientProvider) 22 | : this(new HttpClientFactory(httpClientProvider)) { } 23 | 24 | /// 25 | /// Initializes an instance of . 26 | /// 27 | public MovieClient() 28 | : this(Http.ClientProvider) { } 29 | } 30 | -------------------------------------------------------------------------------- /Juro.DataBuilder/Models/AnimeModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Juro.Core.Models.Anime; 3 | 4 | namespace Juro.DataBuilder.Models; 5 | 6 | public class AnimeModel 7 | { 8 | [Key] 9 | public int AnimeId { get; set; } 10 | 11 | public string Id { get; set; } = default!; 12 | 13 | public AnimeSites Site { get; set; } 14 | 15 | public string Title { get; set; } = default!; 16 | 17 | public string? Released { get; set; } 18 | 19 | public string? Category { get; set; } 20 | 21 | public string? Link { get; set; } 22 | 23 | public string? Image { get; set; } 24 | 25 | public string? Type { get; set; } 26 | 27 | public string? Status { get; set; } 28 | 29 | public string? OtherNames { get; set; } 30 | 31 | public string? Summary { get; set; } 32 | 33 | public List Genres { get; set; } = []; 34 | 35 | public override string ToString() => $"{Title}"; 36 | } 37 | -------------------------------------------------------------------------------- /Juro.Core/Models/Anime/AnimeInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Anime; 4 | 5 | /// 6 | /// The Class which contains all the information about an Anime 7 | /// 8 | public class AnimeInfo : IAnimeInfo 9 | { 10 | public string Id { get; set; } = default!; 11 | 12 | public AnimeSites Site { get; set; } 13 | 14 | public string Title { get; set; } = default!; 15 | 16 | public string? Released { get; set; } 17 | 18 | public string? Category { get; set; } 19 | 20 | public string? Link { get; set; } 21 | 22 | public string? Image { get; set; } 23 | 24 | public string? Type { get; set; } 25 | 26 | public string? Status { get; set; } 27 | 28 | public string? OtherNames { get; set; } 29 | 30 | public string? Summary { get; set; } 31 | 32 | public List Genres { get; set; } = []; 33 | 34 | public override string ToString() => $"{Title}"; 35 | } 36 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Extensions/JsonExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace Juro.Core.Utils.Extensions; 6 | 7 | public static class JsonExtensions 8 | { 9 | public static bool IsValidJson(string value) 10 | { 11 | if (string.IsNullOrWhiteSpace(value)) 12 | return false; 13 | value = value.Trim(); 14 | if ( 15 | (value.StartsWith("{") && value.EndsWith("}")) 16 | || (value.StartsWith("[") && value.EndsWith("]")) 17 | ) 18 | { 19 | try 20 | { 21 | var obj = JsonNode.Parse(value); 22 | return obj is not null; 23 | } 24 | catch (Exception ex) 25 | { 26 | Debug.WriteLine(ex.ToString()); 27 | return false; 28 | } 29 | } 30 | else 31 | { 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Juro.Extractors/IVideoExtractor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Videos; 5 | 6 | namespace Juro.Extractors; 7 | 8 | /// 9 | /// Interface for basic operations related to a video extractor. 10 | /// 11 | public interface IVideoExtractor 12 | { 13 | /// 14 | /// Name of the video server. 15 | /// 16 | public string ServerName { get; } 17 | 18 | /// 19 | /// Extracts the videos by url. 20 | /// 21 | ValueTask> ExtractAsync( 22 | string url, 23 | CancellationToken cancellationToken = default 24 | ); 25 | 26 | /// 27 | ValueTask> ExtractAsync( 28 | string url, 29 | Dictionary headers, 30 | CancellationToken cancellationToken = default 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /Juro.Providers/Anime/AniPlayEpisode.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Juro.Providers.Anime; 4 | 5 | public class AniPlayEpisode 6 | { 7 | [JsonPropertyName("id")] 8 | public string Id { get; set; } = default!; 9 | 10 | [JsonPropertyName("number")] 11 | public int Number { get; set; } 12 | 13 | [JsonPropertyName("title")] 14 | public string? Title { get; set; } 15 | 16 | [JsonPropertyName("hasDub")] 17 | public bool HasDub { get; set; } 18 | 19 | [JsonPropertyName("isFiller")] 20 | public bool IsFiller { get; set; } 21 | 22 | [JsonPropertyName("dubId")] 23 | public string? DubId { get; set; } 24 | 25 | [JsonPropertyName("img")] 26 | public string? Image { get; set; } 27 | 28 | [JsonPropertyName("description")] 29 | public string? Description { get; set; } 30 | 31 | [JsonPropertyName("createdAt")] 32 | public string? CreatedAt { get; set; } 33 | 34 | public override string ToString() => $"Episode {Number}"; 35 | } 36 | -------------------------------------------------------------------------------- /Juro.Core/Models/Subtitle.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models; 4 | 5 | public class Subtitle 6 | { 7 | public string Url { get; set; } = default!; 8 | 9 | public string Language { get; set; } = default!; 10 | 11 | public SubtitleType Type { get; set; } = SubtitleType.VTT; 12 | 13 | public Dictionary Headers { get; set; } = []; 14 | 15 | public Subtitle() { } 16 | 17 | public Subtitle(string url, string language, SubtitleType type = SubtitleType.VTT) 18 | { 19 | Url = url; 20 | Language = language; 21 | Type = type; 22 | } 23 | 24 | public Subtitle( 25 | string url, 26 | string language, 27 | Dictionary headers, 28 | SubtitleType type = SubtitleType.VTT 29 | ) 30 | { 31 | Url = url; 32 | Language = language; 33 | Headers = headers; 34 | Type = type; 35 | } 36 | 37 | /// 38 | public override string ToString() => Language; 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Juro 3 |

4 | 5 | An API for downloading (or streaming) anime, movies and manga. 6 | 7 |

8 | 9 | 10 |

11 | 12 | ## Install 13 | 14 | - 📦 [NuGet](https://nuget.org/packages/Juro.Providers): `dotnet add package Juro.Providers` (**providers package**) 15 | 16 | ### 🌟STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT! 17 | 18 |
19 | 20 |
21 | -------------------------------------------------------------------------------- /Juro.Core/Models/Anime/AnimeSites.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Anime; 2 | 3 | /// 4 | /// Filter applied to anime sites. 5 | /// 6 | public enum AnimeSites 7 | { 8 | /// 9 | /// Parses anime and information from GogoAnime. 10 | /// 11 | GogoAnime, 12 | 13 | /// 14 | /// Parses anime and information from Aniwatch. 15 | /// 16 | Aniwatch, 17 | 18 | /// 19 | /// Parses anime and information from AnimePahe. 20 | /// 21 | AnimePahe, 22 | 23 | /// 24 | /// Parses anime and information from Aniwave. 25 | /// 26 | Aniwave, 27 | 28 | /// 29 | /// Parses anime and information from Aniflix. 30 | /// 31 | Aniflix, 32 | 33 | /// 34 | /// Parses anime and information from OtakuDesu. 35 | /// 36 | OtakuDesu, 37 | 38 | /// 39 | /// Parses anime and information from Kaido. 40 | /// 41 | Kaido, 42 | } 43 | -------------------------------------------------------------------------------- /Juro.Providers/Juro.Providers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;net9.0 4 | true 5 | true 6 | 7 | 8 | 9 | Source providers. 10 | 11 | favicon.png 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | false 23 | runtime 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Juro.Core/Converters/BoolConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Juro.Core.Converters; 6 | 7 | internal class BoolConverter : JsonConverter 8 | { 9 | public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) => 10 | writer.WriteBooleanValue(value); 11 | 12 | public override bool Read( 13 | ref Utf8JsonReader reader, 14 | Type typeToConvert, 15 | JsonSerializerOptions options 16 | ) => 17 | reader.TokenType switch 18 | { 19 | JsonTokenType.True => true, 20 | JsonTokenType.False => false, 21 | JsonTokenType.String => bool.TryParse(reader.GetString(), out var b) 22 | ? b 23 | : throw new JsonException(), 24 | JsonTokenType.Number => reader.TryGetInt64(out var l) 25 | ? Convert.ToBoolean(l) 26 | : reader.TryGetDouble(out var d) && Convert.ToBoolean(d), 27 | _ => throw new JsonException(), 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /Juro.Tests/Juro.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Manga/MangaBaseController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Core.Models.Manga; 2 | using Juro.Core.Providers; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Juro.WebApi.Controllers.Manga; 6 | 7 | [ApiController] 8 | [Route("api/[controller]")] 9 | public class MangaBaseController(IMangaProvider provider) : ControllerBase 10 | { 11 | [HttpGet("{id}")] 12 | public async Task GetAsync(string id) 13 | { 14 | id = Uri.UnescapeDataString(id); 15 | 16 | return await provider.GetMangaInfoAsync(id); 17 | } 18 | 19 | [HttpGet("Search")] 20 | public async Task> SearchAsync([FromQuery(Name = "q")] string query) 21 | { 22 | query = Uri.UnescapeDataString(query); 23 | 24 | return await provider.SearchAsync(query); 25 | } 26 | 27 | [HttpGet] 28 | [Route("ChapterPages/{id}")] 29 | public async Task> GetChapterPagesAsync(string id) 30 | { 31 | id = Uri.UnescapeDataString(id); 32 | 33 | return await provider.GetChapterPagesAsync(id); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Juro.Providers/Anime/Kaido.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using Juro.Core; 4 | using Juro.Core.Models.Anime; 5 | using Juro.Core.Utils; 6 | 7 | namespace Juro.Providers.Anime; 8 | 9 | /// 10 | /// Initializes an instance of . 11 | /// 12 | public class Kaido(IHttpClientFactory httpClientFactory) : Aniwatch(httpClientFactory) 13 | { 14 | public override string Key => Name; 15 | 16 | public override string Name => "Kaido"; 17 | 18 | public override string BaseUrl => "https://kaido.to"; 19 | 20 | public override string AjaxUrl => $"{BaseUrl}/ajax"; 21 | 22 | protected override AnimeSites AnimeSite => AnimeSites.Kaido; 23 | 24 | /// 25 | /// Initializes an instance of . 26 | /// 27 | public Kaido(Func httpClientProvider) 28 | : this(new HttpClientFactory(httpClientProvider)) { } 29 | 30 | /// 31 | /// Initializes an instance of . 32 | /// 33 | public Kaido() 34 | : this(Http.ClientProvider) { } 35 | } 36 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Polyfills.Streams.cs: -------------------------------------------------------------------------------- 1 | #if !NET5_0 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | internal static class StreamPolyfills 8 | { 9 | #if !NETSTANDARD2_1 && !NETCOREAPP3_0 10 | public static async Task ReadAsync( 11 | this Stream stream, 12 | byte[] buffer, 13 | CancellationToken cancellationToken 14 | ) => await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); 15 | #endif 16 | 17 | public static async Task ReadAsStreamAsync( 18 | this HttpContent httpContent, 19 | CancellationToken cancellationToken 20 | ) 21 | { 22 | cancellationToken.ThrowIfCancellationRequested(); 23 | return await httpContent.ReadAsStreamAsync(); 24 | } 25 | 26 | public static async Task ReadAsStringAsync( 27 | this HttpContent httpContent, 28 | CancellationToken cancellationToken 29 | ) 30 | { 31 | cancellationToken.ThrowIfCancellationRequested(); 32 | return await httpContent.ReadAsStringAsync(); 33 | } 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /Juro/Utils/PluginLoadContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.Loader; 4 | 5 | namespace Juro.Utils; 6 | 7 | // DotNet Sample: https://github.com/dotnet/samples/blob/main/core/extensions/AppWithPlugin 8 | internal class PluginLoadContext(string pluginPath) : AssemblyLoadContext 9 | { 10 | private readonly AssemblyDependencyResolver _resolver = new AssemblyDependencyResolver( 11 | pluginPath 12 | ); 13 | 14 | protected override Assembly? Load(AssemblyName assemblyName) 15 | { 16 | var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); 17 | if (assemblyPath is not null) 18 | { 19 | return LoadFromAssemblyPath(assemblyPath); 20 | } 21 | 22 | return null; 23 | } 24 | 25 | protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) 26 | { 27 | var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); 28 | if (!string.IsNullOrEmpty(libraryPath)) 29 | { 30 | return LoadUnmanagedDllFromPath(libraryPath); 31 | } 32 | 33 | return IntPtr.Zero; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Extensions/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Juro.Core.Utils.Extensions; 7 | 8 | public static class StreamExtensions 9 | { 10 | public static async ValueTask CopyToAsync( 11 | this Stream source, 12 | Stream destination, 13 | IProgress? progress = null, 14 | long totalLength = 0, 15 | int bufferSize = 0x1000, 16 | CancellationToken cancellationToken = default 17 | ) 18 | { 19 | var buffer = new byte[bufferSize]; 20 | int bytesRead; 21 | long totalRead = 0; 22 | while ( 23 | (bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0 24 | ) 25 | { 26 | await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken); 27 | cancellationToken.ThrowIfCancellationRequested(); 28 | totalRead += bytesRead; 29 | //progress?.Report(totalRead); 30 | //Report as percentage 31 | progress?.Report(totalRead / (double)totalLength * 100 / 100); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Juro.Core/IHttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace Juro.Core; 4 | 5 | /// 6 | /// A factory abstraction for a component that can create instances with custom 7 | /// configuration for a given logical name. 8 | /// 9 | /// 10 | /// A default can be registered as a service. 11 | /// 12 | public interface IHttpClientFactory 13 | { 14 | /// 15 | /// Creates and configures an instance. 16 | /// 17 | /// A new instance. 18 | /// 19 | /// 20 | /// Each call to is guaranteed to return a new 21 | /// instance. It is generally not necessary to dispose of the as the 22 | /// tracks and disposes resources used by the . 23 | /// 24 | /// 25 | /// Callers are also free to mutate the returned instance's public properties 26 | /// as desired. 27 | /// 28 | /// 29 | HttpClient CreateClient(); 30 | } 31 | -------------------------------------------------------------------------------- /Juro.DataBuilder/JuroContext.cs: -------------------------------------------------------------------------------- 1 | using Juro.DataBuilder.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace Juro.DataBuilder; 5 | 6 | public class JuroContext : DbContext 7 | { 8 | public DbSet AnimeItems { get; set; } 9 | 10 | public DbSet AnimeSeasons { get; set; } 11 | 12 | public DbSet Gogoanime { get; set; } 13 | 14 | public DbSet AnimePahe { get; set; } 15 | 16 | public DbSet Kaido { get; set; } 17 | 18 | public DbSet Aniwave { get; set; } 19 | 20 | public DbSet OtakuDesu { get; set; } 21 | 22 | public string DbPath { get; } 23 | 24 | public JuroContext() 25 | { 26 | var folder = Environment.SpecialFolder.LocalApplicationData; 27 | var path = Environment.GetFolderPath(folder); 28 | DbPath = Path.Join(path, "juro.db"); 29 | } 30 | 31 | public JuroContext(string dbPath) 32 | { 33 | DbPath = dbPath; 34 | } 35 | 36 | // The following configures EF to create a Sqlite database file in the 37 | // special "local" folder for your platform. 38 | protected override void OnConfiguring(DbContextOptionsBuilder options) => 39 | options.UseSqlite($"Data Source={DbPath}"); 40 | } 41 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Juro.Core.Utils; 4 | 5 | internal static class Logger 6 | { 7 | public static bool DebugEnabled { get; set; } = 8 | #if DEBUG 9 | true; 10 | #else 11 | false; 12 | #endif 13 | 14 | public static void Debug(string format, params object[] parameters) 15 | { 16 | if (DebugEnabled) 17 | DiagnosticLog("DEBUG " + format, parameters); 18 | } 19 | 20 | public static void Error(string format, params object[] parameters) 21 | { 22 | DiagnosticLog("ERROR " + format, parameters); 23 | } 24 | 25 | public static void Error(Exception exception) 26 | { 27 | Error( 28 | $"{exception.Message}{Environment.NewLine}{exception.Source}{Environment.NewLine}{exception.StackTrace}" 29 | ); 30 | } 31 | 32 | private static void DiagnosticLog(string format, params object[] parameters) 33 | { 34 | var formatWithHeader = " Juro " + DateTime.Now.ToString("MM-dd H:mm:ss.fff ") + format; 35 | #if DEBUG 36 | System.Diagnostics.Debug.WriteLine(formatWithHeader, parameters); 37 | Console.WriteLine(formatWithHeader, parameters); 38 | #else 39 | Console.WriteLine(formatWithHeader, parameters); 40 | #endif 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Juro.WebApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:21851", 8 | "sslPort": 44349 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5023", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7033;http://localhost:5023", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Juro.Core/Models/Videos/VideoServer.cs: -------------------------------------------------------------------------------- 1 | namespace Juro.Core.Models.Videos; 2 | 3 | /// 4 | /// A simple class containing name and link of the embed which shows the video present on the site. 5 | /// 6 | public class VideoServer 7 | { 8 | public string Name { get; set; } = default!; 9 | 10 | public FileUrl Embed { get; set; } = default!; 11 | 12 | /// 13 | /// Initializes an instance of . 14 | /// 15 | public VideoServer() { } 16 | 17 | /// 18 | /// Initializes an instance of . 19 | /// 20 | public VideoServer(string url) 21 | { 22 | Name = "Default Server"; 23 | Embed = new(url); 24 | } 25 | 26 | /// 27 | /// Initializes an instance of . 28 | /// 29 | public VideoServer(string name, FileUrl embed) 30 | { 31 | Name = name; 32 | Embed = embed; 33 | } 34 | 35 | /// 36 | /// Initializes an instance of . 37 | /// 38 | public VideoServer(string name, string url) 39 | { 40 | Name = name; 41 | Embed = new(url); 42 | } 43 | 44 | /// 45 | public override string ToString() => Name; 46 | } 47 | -------------------------------------------------------------------------------- /Juro.DemoConsole/Utils/ConsoleProgress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | 5 | namespace Juro.DemoConsole.Utils; 6 | 7 | internal class ConsoleProgress(TextWriter writer) : IProgress, IDisposable 8 | { 9 | private readonly TextWriter _writer = writer; 10 | private readonly int _posX = Console.CursorLeft; 11 | private readonly int _posY = Console.CursorTop; 12 | 13 | private int _lastLength; 14 | 15 | private Timer? timer; 16 | private double currentProgress = 0; 17 | 18 | public ConsoleProgress() 19 | : this(Console.Out) { } 20 | 21 | private void TimerHandler(object? state) 22 | { 23 | Write($"{currentProgress:P1}"); 24 | } 25 | 26 | private void EraseLast() 27 | { 28 | if (_lastLength > 0) 29 | { 30 | Console.SetCursorPosition(_posX, _posY); 31 | _writer.Write(new string(' ', _lastLength)); 32 | Console.SetCursorPosition(_posX, _posY); 33 | } 34 | } 35 | 36 | private void Write(string text) 37 | { 38 | EraseLast(); 39 | _writer.Write(text); 40 | _lastLength = text.Length; 41 | } 42 | 43 | public void Report(double progress) 44 | { 45 | timer ??= new Timer(TimerHandler, null, 0, 200); 46 | currentProgress = progress; 47 | 48 | //Write($"{progress:P1}"); 49 | } 50 | 51 | public void Dispose() => EraseLast(); 52 | } 53 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Unbaser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Juro.Core.Utils.Extensions; 5 | 6 | namespace Juro.Core.Utils; 7 | 8 | internal class Unbaser(int @base) 9 | { 10 | private readonly int _base = @base; 11 | 12 | private readonly Dictionary _alphabet = new() 13 | { 14 | [52] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP", 15 | [54] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR", 16 | [62] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 17 | [95] = 18 | " !\\\"#\\$%&\\\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", 19 | }; 20 | 21 | public int Unbase(string value) 22 | { 23 | if (_base is >= 2 and <= 36) 24 | return value.ToIntOrNull(_base) ?? 0; 25 | 26 | var selector = _base switch 27 | { 28 | > 62 => 95, 29 | > 54 => 62, 30 | > 52 => 54, 31 | _ => 52, 32 | }; 33 | 34 | var dict = _alphabet[selector]?.ToCharArray(); 35 | 36 | var returnVal = 0; 37 | 38 | var valArray = value.ToCharArray().Reverse().ToArray(); 39 | for (var i = 0; i < valArray.Length; i++) 40 | { 41 | var cipher = valArray[i]; 42 | returnVal += (int)(Math.Pow(_base, i) * (dict?[cipher] ?? 0)); 43 | } 44 | 45 | return returnVal; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Tasks/ResizableSemaphore.netcore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Juro.Utils.Tasks; 7 | 8 | internal partial class ResizableSemaphore : IDisposable 9 | { 10 | private readonly Queue _waiters = new(); 11 | 12 | private void Refresh() 13 | { 14 | lock (_lock) 15 | { 16 | while (_count < MaxCount && _waiters.TryDequeue(out var waiter)) 17 | { 18 | // Don't increment if the waiter has ben canceled 19 | if (waiter.TrySetResult()) 20 | _count++; 21 | } 22 | } 23 | } 24 | 25 | public async Task AcquireAsync(CancellationToken cancellationToken = default) 26 | { 27 | if (_isDisposed) 28 | throw new ObjectDisposedException(GetType().Name); 29 | 30 | var waiter = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 31 | 32 | await using (_cts.Token.Register(() => waiter.TrySetCanceled(_cts.Token))) 33 | await using (cancellationToken.Register(() => waiter.TrySetCanceled(cancellationToken))) 34 | { 35 | lock (_lock) 36 | { 37 | _waiters.Enqueue(waiter); 38 | Refresh(); 39 | } 40 | 41 | await waiter.Task; 42 | 43 | return new AcquiredAccess(this); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Tasks/ResizableSemaphore.shared.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace Juro.Utils.Tasks; 5 | 6 | internal partial class ResizableSemaphore : IDisposable 7 | { 8 | private readonly object _lock = new(); 9 | private readonly CancellationTokenSource _cts = new(); 10 | 11 | private bool _isDisposed; 12 | private int _maxCount = int.MaxValue; 13 | private int _count; 14 | 15 | public bool IsBusy => MaxCount > 0; 16 | 17 | public int MaxCount 18 | { 19 | get 20 | { 21 | lock (_lock) 22 | { 23 | return _maxCount; 24 | } 25 | } 26 | set 27 | { 28 | lock (_lock) 29 | { 30 | _maxCount = value; 31 | Refresh(); 32 | } 33 | } 34 | } 35 | 36 | public IDisposable Acquire() => AcquireAsync().GetAwaiter().GetResult(); 37 | 38 | public void Release() 39 | { 40 | lock (_lock) 41 | { 42 | _count--; 43 | Refresh(); 44 | } 45 | } 46 | 47 | public void Dispose() 48 | { 49 | _isDisposed = true; 50 | _cts.Cancel(); 51 | _cts.Dispose(); 52 | } 53 | } 54 | 55 | internal partial class ResizableSemaphore 56 | { 57 | private class AcquiredAccess(ResizableSemaphore semaphore) : IDisposable 58 | { 59 | private readonly ResizableSemaphore _semaphore = semaphore; 60 | 61 | public void Dispose() => _semaphore.Release(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Juro.WebApi.Controllers; 4 | 5 | [ApiController] 6 | [Route("[controller]")] 7 | //public class WeatherForecastController(ILogger logger) : ControllerBase 8 | public class WeatherForecastController : ControllerBase 9 | { 10 | private static readonly string[] Summaries = 11 | [ 12 | "Freezing", 13 | "Bracing", 14 | "Chilly", 15 | "Cool", 16 | "Mild", 17 | "Warm", 18 | "Balmy", 19 | "Hot", 20 | "Sweltering", 21 | "Scorching" 22 | ]; 23 | 24 | [HttpGet(Name = "GetWeatherForecast")] 25 | public IEnumerable Get() 26 | { 27 | return Enumerable 28 | .Range(1, 5) 29 | .Select(index => new WeatherForecast 30 | { 31 | Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), 32 | TemperatureC = Random.Shared.Next(-20, 55), 33 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 34 | }) 35 | .ToArray(); 36 | } 37 | 38 | [HttpGet("Test2")] 39 | public IEnumerable Test2() 40 | { 41 | return Enumerable 42 | .Range(1, 5) 43 | .Select(index => new WeatherForecast 44 | { 45 | Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), 46 | TemperatureC = Random.Shared.Next(-20, 55), 47 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 48 | }) 49 | .ToArray(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0.0.0-dev 5 | Berry 6 | Copyright (C) Jerry Berry 7 | latest 8 | enable 9 | 10 | true 11 | false 12 | CA1304,RCS1155,CS8620 13 | 14 | 15 | 16 | 17 | annotations 18 | 19 | 20 | 21 | $(Company) 22 | anime video manga movie parser scraping crawler gogo zoro kaido flixhq mangadex mangakakalot 23 | https://github.com/jerry08/Juro 24 | 26 | MIT 27 | true 28 | true 29 | true 30 | embedded 31 | 32 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Tasks/ResizableSemaphore.netstandard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Juro.Utils.Tasks; 7 | 8 | internal partial class ResizableSemaphore : IDisposable 9 | { 10 | private readonly Queue> _waiters = new(); 11 | 12 | private void Refresh() 13 | { 14 | lock (_lock) 15 | { 16 | while (_count < MaxCount) 17 | { 18 | try 19 | { 20 | var waiter = _waiters.Dequeue(); 21 | 22 | // Don't increment if the waiter has ben canceled 23 | if (waiter.TrySetResult(true)) 24 | _count++; 25 | } 26 | catch 27 | { 28 | break; 29 | } 30 | } 31 | } 32 | } 33 | 34 | public async Task AcquireAsync(CancellationToken cancellationToken = default) 35 | { 36 | if (_isDisposed) 37 | throw new ObjectDisposedException(GetType().Name); 38 | 39 | var waiter = new TaskCompletionSource( 40 | TaskCreationOptions.RunContinuationsAsynchronously 41 | ); 42 | 43 | using (_cts.Token.Register(() => waiter.TrySetCanceled(_cts.Token))) 44 | using (cancellationToken.Register(() => waiter.TrySetCanceled(cancellationToken))) 45 | { 46 | lock (_lock) 47 | { 48 | _waiters.Enqueue(waiter); 49 | Refresh(); 50 | } 51 | 52 | await waiter.Task; 53 | 54 | return new AcquiredAccess(this); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Juro.Core/Providers/IMovieProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Movie; 5 | using Juro.Core.Models.Videos; 6 | 7 | namespace Juro.Core.Providers; 8 | 9 | /// 10 | /// Interface for basic operations related to a movie provider. 11 | /// 12 | public interface IMovieProvider : ISourceProvider, IVideoExtractorProvider, IKey 13 | { 14 | /// 15 | /// Search for movies. 16 | /// 17 | /// The search query. 18 | /// 19 | /// A of s. 20 | ValueTask> SearchAsync( 21 | string query, 22 | CancellationToken cancellationToken = default 23 | ); 24 | 25 | /// 26 | /// Gets the movie info by Id. 27 | /// 28 | /// The movie Id. 29 | /// 30 | /// An instance of for the provider. 31 | ValueTask GetMediaInfoAsync( 32 | string mediaId, 33 | CancellationToken cancellationToken = default 34 | ); 35 | 36 | /// 37 | /// Gets episodes for movie. 38 | /// 39 | /// EpisodeId takes episode link or movie id. 40 | /// MediaId takes movie link or id (found on movie info object). 41 | /// 42 | /// A of s. 43 | ValueTask> GetEpisodeServersAsync( 44 | string episodeId, 45 | string mediaId, 46 | CancellationToken cancellationToken = default 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /Juro.Providers/Aniskip/AniskipClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text.Json; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Juro.Core.Utils; 8 | using Juro.Core.Utils.Extensions; 9 | 10 | namespace Juro.Providers.Aniskip; 11 | 12 | /// 13 | /// Client for interacting with aniskip api. 14 | /// 15 | /// 16 | /// Initializes an instance of . 17 | /// 18 | public class AniskipClient(Func httpClientProvider) 19 | { 20 | private readonly HttpClient _http = httpClientProvider(); 21 | 22 | /// 23 | /// Initializes an instance of . 24 | /// 25 | public AniskipClient() 26 | : this(Http.ClientProvider) { } 27 | 28 | /// 29 | /// Gets the skip times associated with the episode. 30 | /// 31 | public async ValueTask?> GetAsync( 32 | int malId, 33 | int episodeNumber, 34 | long episodeLength, 35 | CancellationToken cancellationToken = default 36 | ) 37 | { 38 | var url = 39 | $"https://api.aniskip.com/v2/skip-times/{malId}/{episodeNumber}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength={episodeLength}"; 40 | 41 | var response = await _http.ExecuteAsync(url, cancellationToken); 42 | if (response is null) 43 | return null; 44 | 45 | var opt = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }; 46 | 47 | var result = JsonSerializer.Deserialize( 48 | response, 49 | new JsonSerializerOptions { PropertyNameCaseInsensitive = true } 50 | ); 51 | 52 | return result?.IsFound == true ? result.Results : null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Juro.Core/Providers/IMangaProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Manga; 5 | 6 | namespace Juro.Core.Providers; 7 | 8 | /// 9 | /// Interface for basic operations related to a manga provider. 10 | /// 11 | public interface IMangaProvider : ISourceProvider, IKey 12 | { 13 | /// 14 | /// Base url of the provider. 15 | /// 16 | public string BaseUrl { get; } 17 | 18 | /// 19 | /// Logo of the provider. 20 | /// 21 | public string Logo { get; } 22 | 23 | /// 24 | /// Search for manga. 25 | /// 26 | /// The search query. 27 | /// 28 | /// An for the provider. 29 | ValueTask> SearchAsync( 30 | string query, 31 | CancellationToken cancellationToken = default 32 | ); 33 | 34 | /// 35 | /// Gets the manga info by Id. 36 | /// 37 | /// The Id of the manga. 38 | /// 39 | /// An interface of type for the provider. 40 | ValueTask GetMangaInfoAsync( 41 | string mangaId, 42 | CancellationToken cancellationToken = default! 43 | ); 44 | 45 | /// 46 | /// Gets chapter pages for manga. 47 | /// 48 | /// The Id of the chapter. 49 | /// 50 | /// An interface of type for the provider. 51 | ValueTask> GetChapterPagesAsync( 52 | string chapterId, 53 | CancellationToken cancellationToken = default! 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/nuget.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: windows-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Install .NET 13 | uses: actions/setup-dotnet@v4 14 | with: 15 | dotnet-version: 8.0.x 16 | 17 | - name: Restore dependencies 18 | run: dotnet restore 19 | 20 | - name: Build 21 | run: dotnet build --no-restore -p:CSharpier_Bypass=true 22 | 23 | pack: 24 | if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} 25 | 26 | runs-on: windows-latest 27 | 28 | permissions: 29 | actions: write 30 | contents: read 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Install .NET 36 | uses: actions/setup-dotnet@v4 37 | with: 38 | dotnet-version: 8.0.x 39 | 40 | - name: Get version information from tag 41 | id: get_version 42 | uses: battila7/get-version-action@v2 43 | 44 | - name: Pack 45 | run: > 46 | dotnet pack 47 | -p:ContinuousIntegrationBuild=true 48 | -c Release 49 | -p:Version=${{ steps.get_version.outputs.version-without-v }} 50 | -p:CSharpier_Bypass=true 51 | 52 | - name: Upload artifacts 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: packages 56 | path: "**/*.nupkg" 57 | 58 | deploy: 59 | runs-on: windows-latest 60 | permissions: 61 | actions: read 62 | 63 | needs: 64 | - pack 65 | 66 | steps: 67 | - name: Install .NET 68 | uses: actions/setup-dotnet@v4 69 | with: 70 | dotnet-version: 8.0.x 71 | 72 | - name: Download artifacts 73 | uses: actions/download-artifact@v4 74 | with: 75 | name: packages 76 | 77 | - name: Push packages 78 | run: > 79 | dotnet nuget push **/*.nupkg 80 | --source "https://api.nuget.org/v3/index.json" 81 | --api-key ${{ secrets.NUGET_TOKEN }} -------------------------------------------------------------------------------- /Juro/Juro.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | true 5 | 6 | 7 | 8 | Client for source providers. 9 | 10 | favicon.png 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Juro/Utils/AssemblyEx.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | 4 | namespace Juro.Utils; 5 | 6 | public static class AssemblyEx 7 | { 8 | public static void LoadReferencedAssemblies() 9 | { 10 | var assembly = Assembly.GetEntryAssembly(); 11 | if (assembly is null) 12 | return; 13 | 14 | foreach (var reference in assembly.GetReferencedAssemblies()) 15 | { 16 | Assembly.Load(reference); 17 | } 18 | } 19 | 20 | public static IEnumerable GetReferencedAssemblies() 21 | { 22 | var assembly = Assembly.GetEntryAssembly(); 23 | if (assembly is null) 24 | yield break; 25 | 26 | foreach (var reference in assembly.GetReferencedAssemblies()) 27 | { 28 | yield return Assembly.Load(reference); 29 | } 30 | } 31 | 32 | public static IEnumerable GetReferencedAssemblies(Assembly? assembly) 33 | { 34 | if (assembly is null) 35 | yield break; 36 | 37 | foreach (var reference in assembly.GetReferencedAssemblies()) 38 | { 39 | yield return Assembly.Load(reference); 40 | } 41 | } 42 | 43 | public static IEnumerable GetAllAssemblies() 44 | { 45 | var asm = Assembly.GetEntryAssembly(); 46 | //var asm = Assembly.GetExecutingAssembly(); 47 | if (asm is null) 48 | yield break; 49 | 50 | var list = new List(); 51 | var stack = new Stack(); 52 | stack.Push(asm); 53 | 54 | do 55 | { 56 | var assembly = stack.Pop(); 57 | 58 | yield return assembly; 59 | 60 | var gg = assembly.GetReferencedAssemblies(); 61 | 62 | foreach (var reference in assembly.GetReferencedAssemblies()) 63 | { 64 | if (!list.Contains(reference.FullName)) 65 | { 66 | stack.Push(Assembly.Load(reference)); 67 | list.Add(reference.FullName); 68 | } 69 | } 70 | } while (stack.Count > 0); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Juro.Core/Models/Videos/VideoSource.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Juro.Core.Models.Videos; 4 | 5 | public class VideoSource 6 | { 7 | /// 8 | /// Will represent quality to user in form of `$"{quality}p"` (1080p). 9 | /// If quality is null, shows "Unknown Quality". 10 | /// If isM3U8 is true, shows "Multi Quality" 11 | /// 12 | public string? Title { get; set; } 13 | 14 | /// 15 | /// Will represent quality to user in form of `$"{quality}p"` (1080p). 16 | /// If quality is null, shows "Unknown Quality". 17 | /// If isM3U8 is true, shows "Multi Quality" 18 | /// 19 | public string? Resolution { get; set; } 20 | 21 | /// 22 | /// The direct url to the Video. 23 | /// Supports mp4, mkv and m3u8 for now, afaik 24 | /// 25 | public string VideoUrl { get; set; } = default!; 26 | 27 | /// 28 | /// Size in bytes. 29 | /// 30 | public long? Size { get; set; } 31 | 32 | /// 33 | /// The direct url to the Video 34 | /// Supports mp4, mkv, dash & m3u8, afaik 35 | /// 36 | public string? FileType { get; set; } 37 | 38 | /// 39 | /// If not a "CONTAINER" format, the app show video as a "Multi Quality" Link 40 | /// "CONTAINER" formats are Mp4 & Mkv 41 | /// 42 | public VideoType Format { get; set; } 43 | 44 | /*/// 45 | /// The direct url to the Video 46 | /// Supports mp4, mkv, dash & m3u8, afaik 47 | /// 48 | public FileUrl Url { get; set; } = default!;*/ 49 | 50 | /// 51 | /// In case, you want to show some extra notes to the User 52 | /// Ex: "Backup" which could be used if the site provides some 53 | /// 54 | public string? ExtraNote { get; set; } 55 | 56 | /// 57 | /// Http headers for making requests. 58 | /// 59 | public Dictionary Headers { get; set; } = []; 60 | 61 | /// 62 | /// Subtitles for videos if available. 63 | /// 64 | public List Subtitles { get; set; } = []; 65 | 66 | public VideoServer? VideoServer { get; set; } 67 | } 68 | -------------------------------------------------------------------------------- /Juro.Providers/Movie/MovieBaseProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Juro.Core; 6 | using Juro.Core.Models.Videos; 7 | using Juro.Core.Providers; 8 | using Juro.Extractors; 9 | 10 | namespace Juro.Providers.Anime; 11 | 12 | public class MovieBaseProvider(IHttpClientFactory httpClientFactory) : IVideoExtractorProvider 13 | { 14 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 15 | 16 | public virtual IVideoExtractor? GetVideoExtractor(VideoServer server) 17 | { 18 | var domain = new Uri(server.Embed.Url).Host; 19 | if (domain.StartsWith("www.")) 20 | domain = domain.Substring(4); 21 | 22 | return domain.ToLower() switch 23 | { 24 | "filemoon.to" or "filemoon.sx" => new FilemoonExtractor(_httpClientFactory), 25 | "rapid-cloud.co" => new RapidCloudExtractor(_httpClientFactory), 26 | "streamtape.com" => new StreamTapeExtractor(_httpClientFactory), 27 | "vidstream.pro" => new VidStreamExtractor(_httpClientFactory), 28 | "mp4upload.com" => new Mp4uploadExtractor(_httpClientFactory), 29 | "playtaku.net" or "goone.pro" => new GogoCDNExtractor(_httpClientFactory), 30 | "alions.pro" => new ALionsExtractor(_httpClientFactory), 31 | "awish.pro" => new AWishExtractor(_httpClientFactory), 32 | "dood.wf" => new DoodExtractor(_httpClientFactory), 33 | "ok.ru" => new OkRuExtractor(_httpClientFactory), 34 | "streamlare.com" => null, 35 | _ => null, 36 | }; 37 | } 38 | 39 | /// 40 | public virtual async ValueTask> GetVideosAsync( 41 | VideoServer server, 42 | CancellationToken cancellationToken = default 43 | ) 44 | { 45 | if (!Uri.IsWellFormedUriString(server.Embed.Url, UriKind.Absolute)) 46 | return []; 47 | 48 | var extractor = GetVideoExtractor(server); 49 | if (extractor is null) 50 | return []; 51 | 52 | var videos = await extractor.ExtractAsync(server.Embed.Url, cancellationToken); 53 | 54 | videos.ForEach(x => x.VideoServer = server); 55 | 56 | return videos; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Juro.Core/Providers/Anime/IAnimeProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Juro.Core.Models.Anime; 5 | using Juro.Core.Models.Videos; 6 | 7 | namespace Juro.Core.Providers; 8 | 9 | /// 10 | /// Interface for basic operations related to an anime provider. 11 | /// 12 | public interface IAnimeProvider : ISourceProvider, IVideoExtractorProvider, IKey 13 | { 14 | /// 15 | /// If dub is available separately for the provider. 16 | /// 17 | public bool IsDubAvailableSeparately { get; } 18 | 19 | /// 20 | /// Search for anime. 21 | /// 22 | /// The search query. 23 | /// 24 | /// A of s. 25 | ValueTask> SearchAsync( 26 | string query, 27 | CancellationToken cancellationToken = default 28 | ); 29 | 30 | /// 31 | /// Gets the anime info by Id. 32 | /// 33 | /// The anime Id. 34 | /// 35 | /// An instance of for the provider. 36 | ValueTask GetAnimeInfoAsync( 37 | string animeId, 38 | CancellationToken cancellationToken = default 39 | ); 40 | 41 | /// 42 | /// Gets episodes for anime. 43 | /// 44 | /// The anime Id. 45 | /// 46 | /// A of s. 47 | ValueTask> GetEpisodesAsync( 48 | string animeId, 49 | CancellationToken cancellationToken = default 50 | ); 51 | 52 | /// 53 | /// Gets video servers for episode. 54 | /// 55 | /// The episode Id. 56 | /// 57 | /// A of s. 58 | ValueTask> GetVideoServersAsync( 59 | string episodeId, 60 | CancellationToken cancellationToken = default 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /Juro.Extractors/OkRuExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text.RegularExpressions; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Juro.Core; 8 | using Juro.Core.Models.Videos; 9 | using Juro.Core.Utils; 10 | using Juro.Core.Utils.Extensions; 11 | 12 | namespace Juro.Extractors; 13 | 14 | /// 15 | /// Extractor for OkRu. 16 | /// 17 | /// 18 | /// Initializes an instance of . 19 | /// 20 | public class OkRuExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 21 | { 22 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 23 | 24 | /// 25 | public string ServerName => "OkRu"; 26 | 27 | /// 28 | /// Initializes an instance of . 29 | /// 30 | public OkRuExtractor(Func httpClientProvider) 31 | : this(new HttpClientFactory(httpClientProvider)) { } 32 | 33 | /// 34 | /// Initializes an instance of . 35 | /// 36 | public OkRuExtractor() 37 | : this(Http.ClientProvider) { } 38 | 39 | /// 40 | public async ValueTask> ExtractAsync( 41 | string url, 42 | CancellationToken cancellationToken = default 43 | ) => await ExtractAsync(url, [], cancellationToken); 44 | 45 | /// 46 | public async ValueTask> ExtractAsync( 47 | string url, 48 | Dictionary headers, 49 | CancellationToken cancellationToken = default 50 | ) 51 | { 52 | var http = _httpClientFactory.CreateClient(); 53 | 54 | var response = await http.ExecuteAsync(url, cancellationToken); 55 | 56 | var mediaUrl = new Regex("https://vd\\d+\\.mycdn\\.me/e[^\\\\]+").Match(response); 57 | 58 | return 59 | [ 60 | new() 61 | { 62 | Format = VideoType.M3u8, 63 | VideoUrl = mediaUrl.Value, 64 | Title = ServerName, 65 | }, 66 | new() 67 | { 68 | Format = VideoType.Dash, 69 | VideoUrl = mediaUrl.NextMatch().Value, 70 | Title = ServerName, 71 | }, 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Juro.Demo.Cli2/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Juro.Clients; 5 | 6 | namespace Juro.Demo.Cli2; 7 | 8 | interface ITest1 { } 9 | 10 | interface ITest2 { } 11 | 12 | interface ITest3 : ITest2 { } 13 | 14 | class Test1 : ITest1 { } 15 | 16 | class Test2 : ITest2 { } 17 | 18 | class Test3 : ITest3 { } 19 | 20 | internal static class Program 21 | { 22 | static async Task Main() 23 | { 24 | //var test1 = (ITest2?)((ITest1)new Test1()); 25 | var test2 = (ITest3?)((ITest2)new Test3()); 26 | 27 | var client = new AnimeClient(); 28 | 29 | // At this point, the list will be empty because no assemblies/plugins 30 | // with classes that implement `IAnimeProvider` interface are loaded. 31 | var providers = client.GetAllProviders(); 32 | 33 | //var dir1 = Environment.CurrentDirectory; 34 | var dirPath2 = @"..\..\..\..\Juro.Providers\bin\Debug\net6.0"; 35 | //var dirInfo = new DirectoryInfo(Environment.CurrentDirectory); 36 | //var parent1 = dirInfo.Parent; 37 | //var parent2 = dirInfo.Parent?.Parent; 38 | 39 | //var dirInfo2 = new DirectoryInfo(dirPath2); 40 | 41 | var loadedPlugins = PluginLoader.LoadPlugins(dirPath2); 42 | var plugins = PluginLoader.GetPlugins(); 43 | var configs = PluginLoader.GetClientConfigs().ToList(); 44 | 45 | foreach (var plugin in plugins) 46 | { 47 | Console.WriteLine($"{plugin.ClientConfig?.RepositoryUrl}"); 48 | Console.WriteLine($"{plugin.Name} ({plugin.Version}) loaded from '${plugin.FilePath}'"); 49 | } 50 | 51 | foreach (var config in configs) 52 | { 53 | Console.WriteLine($"{config.RepositoryUrl}"); 54 | } 55 | 56 | // At this point, the list will be populated now that assemblies are loaded 57 | providers = client.GetAllProviders(); 58 | 59 | var selectedProvider = providers[3]; 60 | 61 | Console.WriteLine($"Searching {selectedProvider.Name}..."); 62 | 63 | var searchResult = await selectedProvider.SearchAsync("naruto"); 64 | Console.WriteLine($"Search count: {searchResult.Count}"); 65 | 66 | Console.WriteLine("Getting episodes..."); 67 | 68 | var episodes = await selectedProvider.GetEpisodesAsync(searchResult[0].Id); 69 | Console.WriteLine($"Episodes count: {episodes.Count}"); 70 | 71 | Console.Read(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Juro.Providers/Anime/AnimeBaseProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Juro.Core; 6 | using Juro.Core.Models.Videos; 7 | using Juro.Core.Providers; 8 | using Juro.Extractors; 9 | 10 | namespace Juro.Providers.Anime; 11 | 12 | public class AnimeBaseProvider(IHttpClientFactory httpClientFactory) : IVideoExtractorProvider 13 | { 14 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 15 | 16 | public virtual IVideoExtractor? GetVideoExtractor(VideoServer server) 17 | { 18 | var domain = new Uri(server.Embed.Url).Host; 19 | if (domain.StartsWith("www.")) 20 | domain = domain.Substring(4); 21 | 22 | return domain.ToLower() switch 23 | { 24 | "filemoon.to" or "filemoon.sx" => new FilemoonExtractor(_httpClientFactory), 25 | "rapid-cloud.co" => new RapidCloudExtractor(_httpClientFactory), 26 | "megacloud.tv" or "megacloud.blog" => new MegaCloudExtractor(_httpClientFactory), 27 | "streamtape.com" => new StreamTapeExtractor(_httpClientFactory), 28 | "vidstream.pro" => new VidStreamExtractor(_httpClientFactory), 29 | "mp4upload.com" => new Mp4uploadExtractor(_httpClientFactory), 30 | "playtaku.net" or "goone.pro" or "embtaku.pro" or "embtaku.com" or "s3taku.com" => 31 | new GogoCDNExtractor(_httpClientFactory), 32 | "alions.pro" => new ALionsExtractor(_httpClientFactory), 33 | "awish.pro" => new AWishExtractor(_httpClientFactory), 34 | "dood.wf" => new DoodExtractor(_httpClientFactory), 35 | "ok.ru" => new OkRuExtractor(_httpClientFactory), 36 | "streamlare.com" => null, 37 | _ => null, 38 | }; 39 | } 40 | 41 | /// 42 | public virtual async ValueTask> GetVideosAsync( 43 | VideoServer server, 44 | CancellationToken cancellationToken = default 45 | ) 46 | { 47 | if (!Uri.IsWellFormedUriString(server.Embed.Url, UriKind.Absolute)) 48 | return []; 49 | 50 | var extractor = GetVideoExtractor(server); 51 | if (extractor is null) 52 | return []; 53 | 54 | var videos = await extractor.ExtractAsync(server.Embed.Url, cancellationToken); 55 | 56 | videos.ForEach(x => x.VideoServer = server); 57 | 58 | return videos; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Juro.Core/Juro.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;net9.0 4 | true 5 | 6 | 7 | 8 | Core plugins for source providers. 9 | 10 | favicon.png 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Juro/Clients/ClientBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Juro.Core; 5 | using Juro.Core.Providers; 6 | 7 | namespace Juro.Clients; 8 | 9 | public class ClientBase(IHttpClientFactory httpClientFactory) : IJuroClient 10 | where IProvider : ISourceProvider 11 | { 12 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 13 | 14 | public IList GetAllProviders() => GetProviders(); 15 | 16 | //public IList GetProviders(string? language = null) 17 | // => Assembly.GetExecutingAssembly().GetTypes() 18 | // .Where(x => x.GetInterfaces().Contains(typeof(IProvider)) 19 | // && x.GetConstructor(Type.EmptyTypes) is not null) 20 | // .Select(x => (IProvider)Activator.CreateInstance(x, new object[] { _httpClientFactory })!) 21 | // .Where(x => string.IsNullOrEmpty(language) 22 | // || x.Language.Equals(language, StringComparison.OrdinalIgnoreCase)) 23 | // .ToList(); 24 | 25 | public IList GetProviders(string? filePath = null, string? language = null) => 26 | PluginLoader 27 | .GetAssemblies() 28 | .Where(x => 29 | string.IsNullOrWhiteSpace(filePath) 30 | || string.Equals(x.Location, filePath, StringComparison.OrdinalIgnoreCase) 31 | ) 32 | .SelectMany(a => a.GetTypes()) 33 | .Where(x => 34 | x.GetInterfaces().Contains(typeof(IProvider)) 35 | && x.GetConstructor(Type.EmptyTypes) is not null 36 | ) 37 | .Select(x => (IProvider)Activator.CreateInstance(x, [_httpClientFactory])!) 38 | .Where(x => 39 | string.IsNullOrEmpty(language) 40 | || x.Language.Equals(language, StringComparison.OrdinalIgnoreCase) 41 | ) 42 | .ToList(); 43 | 44 | public IList GetProviderTypes(string? filePath = null) => 45 | PluginLoader 46 | .GetAssemblies() 47 | .Where(x => 48 | string.IsNullOrWhiteSpace(filePath) 49 | || string.Equals(x.Location, filePath, StringComparison.OrdinalIgnoreCase) 50 | ) 51 | .SelectMany(a => a.GetTypes()) 52 | .Where(x => 53 | x.GetInterfaces().Contains(typeof(IProvider)) 54 | && x.GetConstructor(Type.EmptyTypes) is not null 55 | ) 56 | .ToList(); 57 | } 58 | -------------------------------------------------------------------------------- /Juro.DataBuilder/Models/ManamiAnimeItem.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Juro.DataBuilder.Models; 5 | 6 | public class License 7 | { 8 | [JsonPropertyName("name")] 9 | public string Name { get; set; } = default!; 10 | 11 | [JsonPropertyName("url")] 12 | public string Url { get; set; } = default!; 13 | } 14 | 15 | public class Root 16 | { 17 | [JsonPropertyName("license")] 18 | public License License { get; set; } = default!; 19 | 20 | [JsonPropertyName("repository")] 21 | public string Repository { get; set; } = default!; 22 | 23 | [JsonPropertyName("lastUpdate")] 24 | public string LastUpdate { get; set; } = default!; 25 | 26 | [JsonPropertyName("data")] 27 | public List Data { get; set; } = []; 28 | } 29 | 30 | public class ManamiAnimeItem 31 | { 32 | [Key] 33 | [JsonIgnore] 34 | public int Id { get; set; } 35 | 36 | [JsonPropertyName("sources")] 37 | public List Sources { get; set; } = []; 38 | 39 | [JsonPropertyName("title")] 40 | public string Title { get; set; } = default!; 41 | 42 | [JsonPropertyName("type")] 43 | public string Type { get; set; } = default!; 44 | 45 | [JsonPropertyName("episodes")] 46 | public int Episodes { get; set; } 47 | 48 | [JsonPropertyName("status")] 49 | public int Status { get; set; } 50 | 51 | [JsonPropertyName("animeSeason")] 52 | public AnimeSeason AnimeSeason { get; set; } = default!; 53 | 54 | [JsonPropertyName("picture")] 55 | public string Picture { get; set; } = default!; 56 | 57 | [JsonPropertyName("thumbnail")] 58 | public string Thumbnail { get; set; } = default!; 59 | 60 | [JsonPropertyName("synonyms")] 61 | public List Synonyms { get; set; } = []; 62 | 63 | [JsonPropertyName("relatedAnime")] 64 | public List RelatedAnime { get; set; } = []; 65 | 66 | [JsonPropertyName("tags")] 67 | public List Tags { get; set; } = []; 68 | 69 | public string? GogoanimeId { get; set; } 70 | public string? AnimePaheId { get; set; } 71 | public string? KaidoId { get; set; } 72 | public string? AniwaveId { get; set; } 73 | public string? OtakuDesuId { get; set; } 74 | } 75 | 76 | public class AnimeSeason 77 | { 78 | [Key] 79 | [JsonIgnore] 80 | public int Id { get; set; } 81 | 82 | [JsonPropertyName("season")] 83 | public string Season { get; set; } = default!; 84 | 85 | [JsonPropertyName("year")] 86 | public int Year { get; set; } 87 | } -------------------------------------------------------------------------------- /Juro.DemoConsole/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Httpz; 4 | using Juro.Clients; 5 | using Juro.Providers.Anime; 6 | using Juro.Providers.Manga; 7 | using Juro.Providers.Movie; 8 | 9 | namespace Juro.DemoConsole; 10 | 11 | internal static class Program 12 | { 13 | static async Task Main() 14 | { 15 | await AnimeDemo(); 16 | await MangaDemo(); 17 | //await MovieDemo(); 18 | } 19 | 20 | private static async Task AnimeDemo() 21 | { 22 | var client = new AnimeClient(); 23 | 24 | var allProviders = client.GetAllProviders(); 25 | 26 | var provider = new Aniwatch(); 27 | 28 | var animes = await provider.SearchAsync("jujutsu kaisen season 2"); 29 | var animeInfo = await provider.GetAnimeInfoAsync(animes[0].Id); 30 | var episodes = await provider.GetEpisodesAsync(animes[0].Id); 31 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 32 | var videos = await provider.GetVideosAsync(videoServers[1]); 33 | 34 | var filePath = @"D:\Downloads\svs.mp4"; 35 | 36 | var downloader = new HlsDownloader(); 37 | var qualities = await downloader.GetQualitiesAsync(videos[0].VideoUrl, videos[0].Headers); 38 | await downloader.DownloadAllThenMergeAsync( 39 | qualities[0].Stream!, 40 | videos[0].Headers, 41 | filePath 42 | ); 43 | } 44 | 45 | private static async Task MovieDemo() 46 | { 47 | var provider = new FlixHQ(); 48 | var result = await provider.SearchAsync("ano"); 49 | var movieInfo = await provider.GetMediaInfoAsync(result[0].Id); 50 | var episodes = await provider.GetEpisodeServersAsync(result[0].Id, movieInfo.Id); 51 | } 52 | 53 | private static async Task MangaDemo() 54 | { 55 | var client = new MangaClient(); 56 | 57 | var allProviders = client.GetAllProviders(); 58 | 59 | var provider = new MangaPill(); 60 | 61 | //var results = await provider.SearchAsync("Tomodachi Game"); 62 | var results = await provider.SearchAsync("solo leveling"); 63 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 64 | var pages = await provider.GetChapterPagesAsync(mangaInfo.Chapters[0].Id); 65 | 66 | // Download the image 67 | var fileName = $@"{Environment.CurrentDirectory}\page1.png"; 68 | 69 | var downloader = new Downloader(); 70 | await downloader.DownloadAsync(pages[0].Image, fileName, headers: pages[0].Headers); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Juro.Extractors/StreamTapeExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text.RegularExpressions; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Juro.Core; 8 | using Juro.Core.Models.Videos; 9 | using Juro.Core.Utils; 10 | using Juro.Core.Utils.Extensions; 11 | 12 | namespace Juro.Extractors; 13 | 14 | /// 15 | /// Extractor for StreamTape. 16 | /// 17 | /// 18 | /// Initializes an instance of . 19 | /// 20 | public class StreamTapeExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 21 | { 22 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 23 | 24 | private readonly Regex _linkRegex = new(@"'robotlink'\)\.innerHTML = '(.+?)'\+ \('(.+?)'\)"); 25 | 26 | /// 27 | public string ServerName => "StreamTape"; 28 | 29 | /// 30 | /// Initializes an instance of . 31 | /// 32 | public StreamTapeExtractor(Func httpClientProvider) 33 | : this(new HttpClientFactory(httpClientProvider)) { } 34 | 35 | /// 36 | /// Initializes an instance of . 37 | /// 38 | public StreamTapeExtractor() 39 | : this(Http.ClientProvider) { } 40 | 41 | /// 42 | public async ValueTask> ExtractAsync( 43 | string url, 44 | CancellationToken cancellationToken = default 45 | ) => await ExtractAsync(url, [], cancellationToken); 46 | 47 | /// 48 | public async ValueTask> ExtractAsync( 49 | string url, 50 | Dictionary headers, 51 | CancellationToken cancellationToken = default 52 | ) 53 | { 54 | var http = _httpClientFactory.CreateClient(); 55 | 56 | var id = url.Split(new[] { "/e/" }, StringSplitOptions.None)[1]; 57 | 58 | var response = await http.ExecuteAsync( 59 | url.Replace("tape.com", "adblocker.xyz"), 60 | cancellationToken 61 | ); 62 | 63 | var reg = _linkRegex.Match(response); 64 | 65 | var vidUrl = $"https:{reg.Groups[1]!.Value + reg.Groups[2]!.Value.Substring(3)}"; 66 | 67 | return 68 | [ 69 | new() 70 | { 71 | Format = VideoType.M3u8, 72 | VideoUrl = vidUrl, 73 | Resolution = "Multi Quality", 74 | }, 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/Manga/MangadexSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using Juro.Providers.Manga; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs.Manga; 7 | 8 | public class MangadexSpecs 9 | { 10 | [Theory] 11 | [InlineData("solo leveling")] 12 | public async Task I_can_get_results_from_a_search_query(string query) 13 | { 14 | // Arrange 15 | var provider = new Mangadex(); 16 | 17 | // Act 18 | var results = await provider.SearchAsync(query); 19 | 20 | // Assert 21 | results.Should().NotBeEmpty(); 22 | } 23 | 24 | [Theory] 25 | [InlineData("solo leveling")] 26 | public async Task I_can_get_details_from_a_manga(string query) 27 | { 28 | // Arrange 29 | var provider = new Mangadex(); 30 | 31 | // Act 32 | var results = await provider.SearchAsync(query); 33 | 34 | // Assert 35 | results.Should().NotBeEmpty(); 36 | 37 | // Act 38 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 39 | 40 | // Assert 41 | mangaInfo.Should().NotBeNull(); 42 | } 43 | 44 | [Theory] 45 | [InlineData("solo leveling")] 46 | public async Task I_can_get_chapter_results_from_a_manga(string query) 47 | { 48 | // Arrange 49 | var provider = new Mangadex(); 50 | 51 | // Act 52 | var results = await provider.SearchAsync(query); 53 | 54 | // Assert 55 | results.Should().NotBeEmpty(); 56 | 57 | // Act 58 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 59 | 60 | // Assert 61 | mangaInfo.Should().NotBeNull(); 62 | 63 | // Assert 64 | mangaInfo.Chapters.Should().NotBeEmpty(); 65 | } 66 | 67 | [Theory] 68 | [InlineData("solo leveling")] 69 | public async Task I_can_get_chapter_pages_from_a_manga(string query) 70 | { 71 | // Arrange 72 | var provider = new Mangadex(); 73 | 74 | // Act 75 | var results = await provider.SearchAsync(query); 76 | 77 | // Assert 78 | results.Should().NotBeEmpty(); 79 | 80 | // Act 81 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 82 | 83 | // Assert 84 | mangaInfo.Should().NotBeNull(); 85 | 86 | // Assert 87 | mangaInfo.Chapters.Should().NotBeEmpty(); 88 | 89 | // Act 90 | var pages = await provider.GetChapterPagesAsync(mangaInfo.Chapters[0].Id); 91 | 92 | // Assert 93 | pages.Should().NotBeEmpty(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/Manga/MangaPillSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using Juro.Providers.Manga; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs.Manga; 7 | 8 | public class MangaPillSpecs 9 | { 10 | [Theory] 11 | [InlineData("solo leveling")] 12 | public async Task I_can_get_results_from_a_search_query(string query) 13 | { 14 | // Arrange 15 | var provider = new MangaPill(); 16 | 17 | // Act 18 | var results = await provider.SearchAsync(query); 19 | 20 | // Assert 21 | results.Should().NotBeEmpty(); 22 | } 23 | 24 | [Theory] 25 | [InlineData("solo leveling")] 26 | public async Task I_can_get_details_from_a_manga(string query) 27 | { 28 | // Arrange 29 | var provider = new MangaPill(); 30 | 31 | // Act 32 | var results = await provider.SearchAsync(query); 33 | 34 | // Assert 35 | results.Should().NotBeEmpty(); 36 | 37 | // Act 38 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 39 | 40 | // Assert 41 | mangaInfo.Should().NotBeNull(); 42 | } 43 | 44 | [Theory] 45 | [InlineData("solo leveling")] 46 | public async Task I_can_get_chapter_results_from_a_manga(string query) 47 | { 48 | // Arrange 49 | var provider = new MangaPill(); 50 | 51 | // Act 52 | var results = await provider.SearchAsync(query); 53 | 54 | // Assert 55 | results.Should().NotBeEmpty(); 56 | 57 | // Act 58 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 59 | 60 | // Assert 61 | mangaInfo.Should().NotBeNull(); 62 | 63 | // Assert 64 | mangaInfo.Chapters.Should().NotBeEmpty(); 65 | } 66 | 67 | [Theory] 68 | [InlineData("solo leveling")] 69 | public async Task I_can_get_chapter_pages_from_a_manga(string query) 70 | { 71 | // Arrange 72 | var provider = new MangaPill(); 73 | 74 | // Act 75 | var results = await provider.SearchAsync(query); 76 | 77 | // Assert 78 | results.Should().NotBeEmpty(); 79 | 80 | // Act 81 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 82 | 83 | // Assert 84 | mangaInfo.Should().NotBeNull(); 85 | 86 | // Assert 87 | mangaInfo.Chapters.Should().NotBeEmpty(); 88 | 89 | // Act 90 | var pages = await provider.GetChapterPagesAsync(mangaInfo.Chapters[0].Id); 91 | 92 | // Assert 93 | pages.Should().NotBeEmpty(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/Manga/AsuraScansSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using Juro.Providers.Manga; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs.Manga; 7 | 8 | public class AsuraScansSpecs 9 | { 10 | [Theory] 11 | [InlineData("solo leveling")] 12 | public async Task I_can_get_results_from_a_search_query(string query) 13 | { 14 | // Arrange 15 | var provider = new AsuraScans(); 16 | 17 | // Act 18 | var results = await provider.SearchAsync(query); 19 | 20 | // Assert 21 | results.Should().NotBeEmpty(); 22 | } 23 | 24 | [Theory] 25 | [InlineData("solo leveling")] 26 | public async Task I_can_get_details_from_a_manga(string query) 27 | { 28 | // Arrange 29 | var provider = new AsuraScans(); 30 | 31 | // Act 32 | var results = await provider.SearchAsync(query); 33 | 34 | // Assert 35 | results.Should().NotBeEmpty(); 36 | 37 | // Act 38 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 39 | 40 | // Assert 41 | mangaInfo.Should().NotBeNull(); 42 | } 43 | 44 | [Theory] 45 | [InlineData("solo leveling")] 46 | public async Task I_can_get_chapter_results_from_a_manga(string query) 47 | { 48 | // Arrange 49 | var provider = new AsuraScans(); 50 | 51 | // Act 52 | var results = await provider.SearchAsync(query); 53 | 54 | // Assert 55 | results.Should().NotBeEmpty(); 56 | 57 | // Act 58 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 59 | 60 | // Assert 61 | mangaInfo.Should().NotBeNull(); 62 | 63 | // Assert 64 | mangaInfo.Chapters.Should().NotBeEmpty(); 65 | } 66 | 67 | [Theory] 68 | [InlineData("solo leveling")] 69 | public async Task I_can_get_chapter_pages_from_a_manga(string query) 70 | { 71 | // Arrange 72 | var provider = new AsuraScans(); 73 | 74 | // Act 75 | var results = await provider.SearchAsync(query); 76 | 77 | // Assert 78 | results.Should().NotBeEmpty(); 79 | 80 | // Act 81 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 82 | 83 | // Assert 84 | mangaInfo.Should().NotBeNull(); 85 | 86 | // Assert 87 | mangaInfo.Chapters.Should().NotBeEmpty(); 88 | 89 | // Act 90 | var pages = await provider.GetChapterPagesAsync(mangaInfo.Chapters[0].Id); 91 | 92 | // Assert 93 | pages.Should().NotBeEmpty(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/Manga/MangaKatanaSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using Juro.Providers.Manga; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs.Manga; 7 | 8 | public class MangaKatanaSpecs 9 | { 10 | [Theory] 11 | [InlineData("solo leveling")] 12 | public async Task I_can_get_results_from_a_search_query(string query) 13 | { 14 | // Arrange 15 | var provider = new MangaKatana(); 16 | 17 | // Act 18 | var results = await provider.SearchAsync(query); 19 | 20 | // Assert 21 | results.Should().NotBeEmpty(); 22 | } 23 | 24 | [Theory] 25 | [InlineData("solo leveling")] 26 | public async Task I_can_get_details_from_a_manga(string query) 27 | { 28 | // Arrange 29 | var provider = new MangaKatana(); 30 | 31 | // Act 32 | var results = await provider.SearchAsync(query); 33 | 34 | // Assert 35 | results.Should().NotBeEmpty(); 36 | 37 | // Act 38 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 39 | 40 | // Assert 41 | mangaInfo.Should().NotBeNull(); 42 | } 43 | 44 | [Theory] 45 | [InlineData("solo leveling")] 46 | public async Task I_can_get_chapter_results_from_a_manga(string query) 47 | { 48 | // Arrange 49 | var provider = new MangaKatana(); 50 | 51 | // Act 52 | var results = await provider.SearchAsync(query); 53 | 54 | // Assert 55 | results.Should().NotBeEmpty(); 56 | 57 | // Act 58 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 59 | 60 | // Assert 61 | mangaInfo.Should().NotBeNull(); 62 | 63 | // Assert 64 | mangaInfo.Chapters.Should().NotBeEmpty(); 65 | } 66 | 67 | [Theory] 68 | [InlineData("solo leveling")] 69 | public async Task I_can_get_chapter_pages_from_a_manga(string query) 70 | { 71 | // Arrange 72 | var provider = new MangaKatana(); 73 | 74 | // Act 75 | var results = await provider.SearchAsync(query); 76 | 77 | // Assert 78 | results.Should().NotBeEmpty(); 79 | 80 | // Act 81 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 82 | 83 | // Assert 84 | mangaInfo.Should().NotBeNull(); 85 | 86 | // Assert 87 | mangaInfo.Chapters.Should().NotBeEmpty(); 88 | 89 | // Act 90 | var pages = await provider.GetChapterPagesAsync(mangaInfo.Chapters[0].Id); 91 | 92 | // Assert 93 | pages.Should().NotBeEmpty(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/Manga/MangaKakalotSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using Juro.Providers.Manga; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs.Manga; 7 | 8 | public class MangaKakalotSpecs 9 | { 10 | [Theory] 11 | [InlineData("solo leveling")] 12 | public async Task I_can_get_results_from_a_search_query(string query) 13 | { 14 | // Arrange 15 | var provider = new MangaKakalot(); 16 | 17 | // Act 18 | var results = await provider.SearchAsync(query); 19 | 20 | // Assert 21 | results.Should().NotBeEmpty(); 22 | } 23 | 24 | [Theory] 25 | [InlineData("solo leveling")] 26 | public async Task I_can_get_details_from_a_manga(string query) 27 | { 28 | // Arrange 29 | var provider = new MangaKakalot(); 30 | 31 | // Act 32 | var results = await provider.SearchAsync(query); 33 | 34 | // Assert 35 | results.Should().NotBeEmpty(); 36 | 37 | // Act 38 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 39 | 40 | // Assert 41 | mangaInfo.Should().NotBeNull(); 42 | } 43 | 44 | [Theory] 45 | [InlineData("solo leveling")] 46 | public async Task I_can_get_chapter_results_from_a_manga(string query) 47 | { 48 | // Arrange 49 | var provider = new MangaKakalot(); 50 | 51 | // Act 52 | var results = await provider.SearchAsync(query); 53 | 54 | // Assert 55 | results.Should().NotBeEmpty(); 56 | 57 | // Act 58 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 59 | 60 | // Assert 61 | mangaInfo.Should().NotBeNull(); 62 | 63 | // Assert 64 | mangaInfo.Chapters.Should().NotBeEmpty(); 65 | } 66 | 67 | [Theory] 68 | [InlineData("solo leveling")] 69 | public async Task I_can_get_chapter_pages_from_a_manga(string query) 70 | { 71 | // Arrange 72 | var provider = new MangaKakalot(); 73 | 74 | // Act 75 | var results = await provider.SearchAsync(query); 76 | 77 | // Assert 78 | results.Should().NotBeEmpty(); 79 | 80 | // Act 81 | var mangaInfo = await provider.GetMangaInfoAsync(results[0].Id); 82 | 83 | // Assert 84 | mangaInfo.Should().NotBeNull(); 85 | 86 | // Assert 87 | mangaInfo.Chapters.Should().NotBeEmpty(); 88 | 89 | // Act 90 | var pages = await provider.GetChapterPagesAsync(mangaInfo.Chapters[0].Id); 91 | 92 | // Assert 93 | pages.Should().NotBeEmpty(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Http.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace Juro.Core.Utils; 5 | 6 | internal static class Http 7 | { 8 | public static Func ClientProvider => 9 | () => 10 | { 11 | var handler = new HttpClientHandler 12 | { 13 | //UseCookies = false 14 | //AllowAutoRedirect = true 15 | }; 16 | 17 | //handler.MaxAutomaticRedirections = 2; 18 | 19 | //if (handler.SupportsAutomaticDecompression) 20 | // handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; 21 | 22 | //handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => { return true; }; 23 | 24 | var httpClient = new HttpClient(handler, true); 25 | 26 | if (!httpClient.DefaultRequestHeaders.Contains("User-Agent")) 27 | { 28 | httpClient.DefaultRequestHeaders.Add("User-Agent", ChromeUserAgent()); 29 | } 30 | 31 | return httpClient; 32 | }; 33 | 34 | /// 35 | /// Generates a random User-Agent from the Chrome browser. 36 | /// 37 | /// Random User-Agent from Chrome browser. 38 | public static string ChromeUserAgent() 39 | { 40 | var random = new Random(); 41 | 42 | var major = random.Next(62, 70); 43 | var build = random.Next(2100, 3538); 44 | var branchBuild = random.Next(170); 45 | 46 | return $"Mozilla/5.0 ({RandomWindowsVersion()}) AppleWebKit/537.36 (KHTML, like Gecko) " 47 | + $"Chrome/{major}.0.{build}.{branchBuild} Safari/537.36"; 48 | } 49 | 50 | private static string RandomWindowsVersion() 51 | { 52 | var random = new Random(); 53 | 54 | var windowsVersion = "Windows NT "; 55 | var val = random.Next(99) + 1; 56 | 57 | // Windows 10 = 45% popularity 58 | if (val >= 1 && val <= 45) 59 | windowsVersion += "10.0"; 60 | // Windows 7 = 35% popularity 61 | else if (val > 45 && val <= 80) 62 | windowsVersion += "6.1"; 63 | // Windows 8.1 = 15% popularity 64 | else if (val > 80 && val <= 95) 65 | windowsVersion += "6.3"; 66 | // Windows 8 = 5% popularity 67 | else 68 | windowsVersion += "6.2"; 69 | 70 | // Append WOW64 for X64 system 71 | if (random.NextDouble() <= 0.65) 72 | windowsVersion += random.NextDouble() <= 0.5 ? "; WOW64" : "; Win64; x64"; 73 | 74 | return windowsVersion; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Juro.WebApi/Controllers/Anime/AnimeBaseController.cs: -------------------------------------------------------------------------------- 1 | using Juro.Core.Models.Anime; 2 | using Juro.Core.Models.Videos; 3 | using Juro.Core.Providers; 4 | using Microsoft.AspNetCore.Mvc; 5 | using TaskExecutor; 6 | 7 | namespace Juro.WebApi.Controllers.Anime; 8 | 9 | [ApiController] 10 | [Route("api/[controller]")] 11 | //[ApiExplorerSettings(IgnoreApi = true)] 12 | public class AnimeBaseController(IAnimeProvider animeProvider) : ControllerBase 13 | { 14 | //[HttpGet] 15 | //[Route("Get/{id}")] 16 | [HttpGet("{id}")] 17 | public async Task GetAsync(string id) 18 | { 19 | id = Uri.UnescapeDataString(id); 20 | 21 | return await animeProvider.GetAnimeInfoAsync(id); 22 | } 23 | 24 | [HttpGet("Search")] 25 | public async Task> SearchAsync(string query) 26 | { 27 | return await animeProvider.SearchAsync(query); 28 | } 29 | 30 | [HttpGet] 31 | [Route("Episodes/{id}")] 32 | public async Task> GetEpisodesAsync(string id) 33 | { 34 | id = Uri.UnescapeDataString(id); 35 | 36 | return await animeProvider.GetEpisodesAsync(id); 37 | } 38 | 39 | [HttpGet] 40 | [Route("VideoServers/{id}")] 41 | public async Task> GetVideoServersAsync(string id) 42 | { 43 | id = Uri.UnescapeDataString(id); 44 | 45 | return await animeProvider.GetVideoServersAsync(id); 46 | } 47 | 48 | [HttpGet] 49 | //[Route("Videos{url}")] 50 | [Route("Videos")] 51 | public async Task> GetVideosAsync([FromQuery(Name = "q")] string query) 52 | { 53 | query = Uri.UnescapeDataString(query); 54 | 55 | if ( 56 | Uri.IsWellFormedUriString(query, UriKind.Absolute) 57 | && !query.Contains("animepahe.ru/play/", StringComparison.OrdinalIgnoreCase) 58 | ) 59 | { 60 | var server = new VideoServer(query); 61 | return await animeProvider.GetVideosAsync(server); 62 | } 63 | 64 | var servers = await animeProvider.GetVideoServersAsync(query); 65 | var functions = servers.Select(server => 66 | (Func>>)( 67 | async () => 68 | { 69 | var list = new List(); 70 | 71 | try 72 | { 73 | list.AddRange(await animeProvider.GetVideosAsync(server)); 74 | } 75 | catch { } 76 | 77 | return list; 78 | } 79 | ) 80 | ); 81 | 82 | var results = await TaskEx.Run(functions, 5); 83 | 84 | return results.SelectMany(x => x); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Juro.Extractors/FilemoonExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Juro.Core; 8 | using Juro.Core.Models.Videos; 9 | using Juro.Core.Utils; 10 | using Juro.Core.Utils.Extensions; 11 | 12 | namespace Juro.Extractors; 13 | 14 | /// 15 | /// Extractor for Filemoon. 16 | /// 17 | /// 18 | /// Initializes an instance of . 19 | /// 20 | public class FilemoonExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 21 | { 22 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 23 | 24 | /// 25 | public string ServerName => "Filemoon"; 26 | 27 | /// 28 | /// Initializes an instance of . 29 | /// 30 | public FilemoonExtractor(Func httpClientProvider) 31 | : this(new HttpClientFactory(httpClientProvider)) { } 32 | 33 | /// 34 | /// Initializes an instance of . 35 | /// 36 | public FilemoonExtractor() 37 | : this(Http.ClientProvider) { } 38 | 39 | /// 40 | public async ValueTask> ExtractAsync( 41 | string url, 42 | CancellationToken cancellationToken = default 43 | ) => await ExtractAsync(url, [], cancellationToken); 44 | 45 | /// 46 | public async ValueTask> ExtractAsync( 47 | string url, 48 | Dictionary headers, 49 | CancellationToken cancellationToken = default 50 | ) 51 | { 52 | var http = _httpClientFactory.CreateClient(); 53 | 54 | var response = await http.ExecuteAsync(url, cancellationToken); 55 | 56 | var document = Html.Parse(response); 57 | 58 | var scriptNode = document 59 | .DocumentNode.Descendants() 60 | .FirstOrDefault(x => x.Name == "script" && x.InnerText?.Contains("eval") == true); 61 | 62 | var unpacked = JsUnpacker.UnpackAndCombine(scriptNode?.InnerText); 63 | 64 | // Work in progress (subtitles) 65 | //if (unpacked.Contains("fetch('")) 66 | //{ 67 | // var subtitleString = unpacked.SubstringAfter("fetch('") 68 | // .Split(new[] { "')." }, StringSplitOptions.None)[0]; 69 | //} 70 | 71 | var masterUrl = unpacked 72 | .SubstringAfter("{file:\"") 73 | .Split(new[] { "\"}" }, StringSplitOptions.None)[0]; 74 | 75 | return 76 | [ 77 | new() 78 | { 79 | Format = VideoType.M3u8, 80 | VideoUrl = masterUrl, 81 | Resolution = "Multi Quality", 82 | }, 83 | ]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Juro.Extractors/AWishExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text.RegularExpressions; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Juro.Core; 9 | using Juro.Core.Models.Videos; 10 | using Juro.Core.Utils; 11 | using Juro.Core.Utils.Extensions; 12 | 13 | namespace Juro.Extractors; 14 | 15 | /// 16 | /// Extractor for AWish. 17 | /// 18 | /// 19 | /// Initializes an instance of . 20 | /// 21 | public class AWishExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 22 | { 23 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 24 | 25 | /// 26 | public string ServerName => "AWish"; 27 | 28 | /// 29 | /// Initializes an instance of . 30 | /// 31 | public AWishExtractor(Func httpClientProvider) 32 | : this(new HttpClientFactory(httpClientProvider)) { } 33 | 34 | /// 35 | /// Initializes an instance of . 36 | /// 37 | public AWishExtractor() 38 | : this(Http.ClientProvider) { } 39 | 40 | /// 41 | public async ValueTask> ExtractAsync( 42 | string url, 43 | CancellationToken cancellationToken = default 44 | ) => await ExtractAsync(url, [], cancellationToken); 45 | 46 | /// 47 | public async ValueTask> ExtractAsync( 48 | string url, 49 | Dictionary headers, 50 | CancellationToken cancellationToken = default 51 | ) 52 | { 53 | var http = _httpClientFactory.CreateClient(); 54 | 55 | var response = await http.ExecuteAsync(url, cancellationToken); 56 | 57 | var document = Html.Parse(response); 58 | 59 | var script = document 60 | .DocumentNode.Descendants() 61 | .FirstOrDefault(x => x.Name == "script" && x.InnerText?.Contains("m3u8") == true) 62 | ?.InnerText; 63 | 64 | // Sometimes the script body is packed, sometimes it isn't 65 | var scriptBody = JsUnpacker.IsPacked(script) ? JsUnpacker.UnpackAndCombine(script) : script; 66 | 67 | if (string.IsNullOrEmpty(scriptBody)) 68 | return []; 69 | 70 | //var mediaUrl = new Regex("file:\"([^\"]+)\"\\}").Match(scriptBody) 71 | var mediaUrl = new Regex("file:\"([^\"]+)\"") 72 | .Match(scriptBody) 73 | .Groups.OfType() 74 | .ToList()[1] 75 | .Value; 76 | 77 | return 78 | [ 79 | new() 80 | { 81 | Format = VideoType.M3u8, 82 | VideoUrl = mediaUrl, 83 | Title = ServerName, 84 | }, 85 | ]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Juro.Extractors/VidStreamExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Juro.Core; 7 | using Juro.Core.Models.Videos; 8 | using Juro.Core.Utils; 9 | using Juro.Core.Utils.Extensions; 10 | 11 | namespace Juro.Extractors; 12 | 13 | /// 14 | /// Extractor for VidStream. 15 | /// 16 | /// 17 | /// Initializes an instance of . 18 | /// 19 | public class VidStreamExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 20 | { 21 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 22 | 23 | /// 24 | public string ServerName => "VidStream"; 25 | 26 | /// 27 | /// Initializes an instance of . 28 | /// 29 | public VidStreamExtractor(Func httpClientProvider) 30 | : this(new HttpClientFactory(httpClientProvider)) { } 31 | 32 | /// 33 | /// Initializes an instance of . 34 | /// 35 | public VidStreamExtractor() 36 | : this(Http.ClientProvider) { } 37 | 38 | /// 39 | public async ValueTask> ExtractAsync( 40 | string url, 41 | CancellationToken cancellationToken = default 42 | ) => await ExtractAsync(url, [], cancellationToken); 43 | 44 | /// 45 | public async ValueTask> ExtractAsync( 46 | string url, 47 | Dictionary headers, 48 | CancellationToken cancellationToken = default 49 | ) 50 | { 51 | var http = _httpClientFactory.CreateClient(); 52 | 53 | var response = await http.ExecuteAsync(url, cancellationToken); 54 | 55 | if (url.Contains("srcd")) 56 | { 57 | var link = response.FindBetween("\"file\": '", "',"); 58 | 59 | return 60 | [ 61 | new() 62 | { 63 | Format = VideoType.M3u8, 64 | VideoUrl = link, 65 | Title = ServerName, 66 | }, 67 | ]; 68 | } 69 | 70 | var document = Html.Parse(response); 71 | 72 | var mediaUrl = document 73 | .DocumentNode.SelectSingleNode(".//iframe") 74 | ?.Attributes["src"] 75 | ?.Value; 76 | if (string.IsNullOrWhiteSpace(mediaUrl)) 77 | return []; 78 | 79 | if (mediaUrl!.Contains("filemoon")) 80 | return await new FilemoonExtractor(_httpClientFactory).ExtractAsync( 81 | mediaUrl, 82 | cancellationToken 83 | ); 84 | 85 | return await new GogoCDNExtractor(_httpClientFactory).ExtractAsync( 86 | mediaUrl, 87 | cancellationToken 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Juro.Extractors/FPlayerExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text.Json.Nodes; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Juro.Core; 8 | using Juro.Core.Models.Videos; 9 | using Juro.Core.Utils; 10 | using Juro.Core.Utils.Extensions; 11 | 12 | namespace Juro.Extractors; 13 | 14 | /// 15 | /// Extractor for FPlayer. 16 | /// 17 | /// 18 | /// Initializes an instance of . 19 | /// 20 | public class FPlayerExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 21 | { 22 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 23 | 24 | /// 25 | public string ServerName => "FPlayer"; 26 | 27 | /// 28 | /// Initializes an instance of . 29 | /// 30 | public FPlayerExtractor(Func httpClientProvider) 31 | : this(new HttpClientFactory(httpClientProvider)) { } 32 | 33 | /// 34 | /// Initializes an instance of . 35 | /// 36 | public FPlayerExtractor() 37 | : this(Http.ClientProvider) { } 38 | 39 | /// 40 | public async ValueTask> ExtractAsync( 41 | string url, 42 | CancellationToken cancellationToken = default 43 | ) => await ExtractAsync(url, [], cancellationToken); 44 | 45 | /// 46 | public async ValueTask> ExtractAsync( 47 | string url, 48 | Dictionary headers, 49 | CancellationToken cancellationToken = default 50 | ) 51 | { 52 | var http = _httpClientFactory.CreateClient(); 53 | 54 | var apiLink = url.Replace("/v/", "/api/source/"); 55 | 56 | var list = new List(); 57 | 58 | try 59 | { 60 | headers = new Dictionary() { { "Referer", url } }; 61 | 62 | var json = await http.PostAsync(apiLink, headers, cancellationToken); 63 | if (!string.IsNullOrWhiteSpace(json)) 64 | { 65 | var data = JsonNode.Parse(JsonNode.Parse(json)!["data"]!.ToString())!.AsArray(); 66 | for (var i = 0; i < data.Count; i++) 67 | { 68 | list.Add( 69 | new() 70 | { 71 | VideoUrl = data[i]!["file"]!.ToString(), 72 | Resolution = data[i]!["label"]!.ToString(), 73 | Format = VideoType.Container, 74 | FileType = data[i]!["type"]!.ToString(), 75 | } 76 | ); 77 | } 78 | 79 | return list; 80 | } 81 | } 82 | catch 83 | { 84 | // Ignore 85 | } 86 | 87 | return list; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Juro.Extractors/ALionsExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text.RegularExpressions; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Juro.Core; 9 | using Juro.Core.Models.Videos; 10 | using Juro.Core.Utils; 11 | using Juro.Core.Utils.Extensions; 12 | 13 | namespace Juro.Extractors; 14 | 15 | /// 16 | /// Extractor for ALions. 17 | /// 18 | /// 19 | /// Initializes an instance of . 20 | /// 21 | public class ALionsExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 22 | { 23 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 24 | 25 | /// 26 | public string ServerName => "ALions / Vidhide"; 27 | 28 | /// 29 | /// Initializes an instance of . 30 | /// 31 | public ALionsExtractor(Func httpClientProvider) 32 | : this(new HttpClientFactory(httpClientProvider)) { } 33 | 34 | /// 35 | /// Initializes an instance of . 36 | /// 37 | public ALionsExtractor() 38 | : this(Http.ClientProvider) { } 39 | 40 | /// 41 | public async ValueTask> ExtractAsync( 42 | string url, 43 | CancellationToken cancellationToken = default 44 | ) => await ExtractAsync(url, [], cancellationToken); 45 | 46 | /// 47 | public async ValueTask> ExtractAsync( 48 | string url, 49 | Dictionary headers, 50 | CancellationToken cancellationToken = default 51 | ) 52 | { 53 | var http = _httpClientFactory.CreateClient(); 54 | 55 | var response = await http.ExecuteAsync(url, cancellationToken); 56 | 57 | var script = 58 | new Regex("") 59 | .Match(response) 60 | .Groups.OfType() 61 | .ElementAtOrDefault(1) 62 | ?.Value 63 | ?? new Regex("") 64 | .Match(response) 65 | .Groups.OfType() 66 | .ElementAtOrDefault(1) 67 | ?.Value; 68 | 69 | if (string.IsNullOrEmpty(script)) 70 | return []; 71 | 72 | var unpackedScript = JsUnpacker.UnpackAndCombine(script); 73 | 74 | var mediaUrl = new Regex("file:\"([^\"]+)\"\\}") 75 | .Match(unpackedScript) 76 | .Groups.OfType() 77 | .ElementAtOrDefault(1) 78 | ?.Value; 79 | 80 | if (string.IsNullOrEmpty(mediaUrl)) 81 | return []; 82 | 83 | return 84 | [ 85 | new() 86 | { 87 | Format = VideoType.M3u8, 88 | VideoUrl = mediaUrl!, 89 | Title = ServerName, 90 | }, 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Juro.Extractors/YourUploadExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Juro.Core; 8 | using Juro.Core.Models.Videos; 9 | using Juro.Core.Utils; 10 | using Juro.Core.Utils.Extensions; 11 | 12 | namespace Juro.Extractors; 13 | 14 | /// 15 | /// Extractor for YourUpload. 16 | /// 17 | /// 18 | /// Initializes an instance of . 19 | /// 20 | public class YourUploadExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 21 | { 22 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 23 | 24 | /// 25 | public string ServerName => "YourUpload"; 26 | 27 | /// 28 | /// Initializes an instance of . 29 | /// 30 | public YourUploadExtractor(Func httpClientProvider) 31 | : this(new HttpClientFactory(httpClientProvider)) { } 32 | 33 | /// 34 | /// Initializes an instance of . 35 | /// 36 | public YourUploadExtractor() 37 | : this(Http.ClientProvider) { } 38 | 39 | /// 40 | public async ValueTask> ExtractAsync( 41 | string url, 42 | CancellationToken cancellationToken = default 43 | ) => await ExtractAsync(url, [], cancellationToken); 44 | 45 | /// 46 | public async ValueTask> ExtractAsync( 47 | string url, 48 | Dictionary headers, 49 | CancellationToken cancellationToken = default 50 | ) => await ExtractAsync(url, quality: null, cancellationToken); 51 | 52 | public async ValueTask> ExtractAsync( 53 | string url, 54 | string? quality = null, 55 | CancellationToken cancellationToken = default 56 | ) 57 | { 58 | var http = _httpClientFactory.CreateClient(); 59 | 60 | var headers = new Dictionary() 61 | { 62 | ["Referer"] = "https://www.yourupload.com/", 63 | }; 64 | 65 | var response = await http.ExecuteAsync(url, headers, cancellationToken); 66 | 67 | var document = Html.Parse(response); 68 | 69 | var baseData = document 70 | .DocumentNode.Descendants() 71 | .FirstOrDefault(x => 72 | x.Name == "script" && x.InnerText?.Contains("jwplayerOptions") == true 73 | ) 74 | ?.InnerText; 75 | 76 | if (string.IsNullOrEmpty(baseData)) 77 | return []; 78 | 79 | var basicUrl = baseData!.RemoveWhitespaces().SubstringAfter("file:'").SubstringBefore("',"); 80 | 81 | return 82 | [ 83 | new() 84 | { 85 | Format = VideoType.Container, 86 | VideoUrl = basicUrl, 87 | Resolution = quality, 88 | Headers = headers, 89 | Title = $"{quality} - {ServerName}", 90 | }, 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Juro.Core/Converters/JsonStringEnumConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace Juro.Core.Converters; 8 | 9 | // Copied from https://github.com/dotnet/runtime/issues/74385#issuecomment-1456725149 10 | internal class JsonStringEnumConverter : JsonConverterFactory 11 | { 12 | public override bool CanConvert(Type typeToConvert) 13 | { 14 | return typeToConvert.IsEnum; 15 | } 16 | 17 | public override JsonConverter? CreateConverter( 18 | Type typeToConvert, 19 | JsonSerializerOptions options 20 | ) 21 | { 22 | var type = typeof(JsonStringEnumConverter<>).MakeGenericType(typeToConvert); 23 | return (JsonConverter)Activator.CreateInstance(type)!; 24 | } 25 | } 26 | 27 | internal class JsonStringEnumConverter : JsonConverter 28 | where TEnum : struct, Enum 29 | { 30 | private readonly Dictionary _enumToString = []; 31 | private readonly Dictionary _stringToEnum = []; 32 | private readonly Dictionary _numberToEnum = []; 33 | 34 | public JsonStringEnumConverter() 35 | { 36 | var type = typeof(TEnum); 37 | foreach (TEnum value in Enum.GetValues(type)) 38 | { 39 | var enumMember = type.GetMember(value.ToString())[0]; 40 | var attr = enumMember 41 | .GetCustomAttributes(typeof(JsonPropertyNameAttribute), false) 42 | .Cast() 43 | .FirstOrDefault(); 44 | 45 | var num = Convert.ToInt32(type.GetField("value__")?.GetValue(value)); 46 | if (attr?.Name != null) 47 | { 48 | _enumToString.Add(value, attr.Name); 49 | _stringToEnum.Add(attr.Name, value); 50 | _numberToEnum.Add(num, value); 51 | } 52 | else 53 | { 54 | _enumToString.Add(value, value.ToString()); 55 | _stringToEnum.Add(value.ToString(), value); 56 | _numberToEnum.Add(num, value); 57 | } 58 | } 59 | } 60 | 61 | public override TEnum Read( 62 | ref Utf8JsonReader reader, 63 | Type typeToConvert, 64 | JsonSerializerOptions options 65 | ) 66 | { 67 | var type = reader.TokenType; 68 | if (type == JsonTokenType.String) 69 | { 70 | var stringValue = reader.GetString(); 71 | 72 | if (stringValue != null && _stringToEnum.TryGetValue(stringValue, out var enumValue)) 73 | { 74 | return enumValue; 75 | } 76 | } 77 | else if (type == JsonTokenType.Number) 78 | { 79 | var numValue = reader.GetInt32(); 80 | _numberToEnum.TryGetValue(numValue, out var enumValue); 81 | return enumValue; 82 | } 83 | 84 | return default; 85 | } 86 | 87 | public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) 88 | { 89 | writer.WriteStringValue(_enumToString[value]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Juro.Extractors/AniwaveExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text.Json.Nodes; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Juro.Core; 9 | using Juro.Core.Models.Videos; 10 | using Juro.Core.Utils; 11 | using Juro.Core.Utils.Extensions; 12 | 13 | namespace Juro.Extractors; 14 | 15 | /// 16 | /// Extractor for Aniwave. 17 | /// 18 | /// 19 | /// Initializes an instance of . 20 | /// 21 | public class AniwaveExtractor(IHttpClientFactory httpClientFactory, string serverName) 22 | : IVideoExtractor 23 | { 24 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 25 | 26 | /// 27 | /// Name of the video server for Aniwave. It can either be "Mcloud" or "Vizcloud" 28 | /// 29 | public string ServerName { get; } = serverName; 30 | 31 | /// 32 | /// Initializes an instance of . 33 | /// 34 | public AniwaveExtractor(Func httpClientProvider, string serverName) 35 | : this(new HttpClientFactory(httpClientProvider), serverName) { } 36 | 37 | /// 38 | /// Initializes an instance of . 39 | /// 40 | public AniwaveExtractor(string serverName) 41 | : this(Http.ClientProvider, serverName) { } 42 | 43 | /// 44 | public async ValueTask> ExtractAsync( 45 | string url, 46 | CancellationToken cancellationToken = default 47 | ) => await ExtractAsync(url, [], cancellationToken); 48 | 49 | /// 50 | public async ValueTask> ExtractAsync( 51 | string url, 52 | Dictionary headers, 53 | CancellationToken cancellationToken = default 54 | ) 55 | { 56 | var http = _httpClientFactory.CreateClient(); 57 | 58 | var list = new List(); 59 | 60 | var isMcloud = ServerName.Equals("MyCloud", StringComparison.OrdinalIgnoreCase); 61 | var server = isMcloud ? "Mcloud" : "Vizcloud"; 62 | var vidId = new Stack(url.Split('/')).Pop()?.Split('?').FirstOrDefault(); 63 | var url2 = $"https://9anime.eltik.net/raw{server}?query={vidId}&apikey=chayce"; 64 | 65 | var response = await http.ExecuteAsync(url2, cancellationToken); 66 | var apiUrl = JsonNode.Parse(response)?["rawURL"]?.ToString(); 67 | if (string.IsNullOrWhiteSpace(apiUrl)) 68 | return list; 69 | 70 | var referer = isMcloud ? "https://mcloud.to/" : "https://9anime.to/"; 71 | 72 | response = await http.ExecuteAsync( 73 | apiUrl!, 74 | new() { ["Referer"] = referer }, 75 | cancellationToken 76 | ); 77 | 78 | var data = JsonNode.Parse(response)!["data"]!; 79 | 80 | var file = data["media"]!["sources"]![0]!["file"]!.ToString(); 81 | 82 | list.Add( 83 | new() 84 | { 85 | VideoUrl = file, 86 | Headers = new() { ["Referer"] = referer }, 87 | Format = VideoType.M3u8, 88 | Resolution = "Multi Quality", 89 | } 90 | ); 91 | 92 | return list; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Juro.Extractors/Mp4uploadExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Juro.Core; 8 | using Juro.Core.Models.Videos; 9 | using Juro.Core.Utils; 10 | using Juro.Core.Utils.Extensions; 11 | 12 | namespace Juro.Extractors; 13 | 14 | /// 15 | /// Extractor for Mp4upload. 16 | /// 17 | /// 18 | /// Initializes an instance of . 19 | /// 20 | public class Mp4uploadExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 21 | { 22 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 23 | 24 | /// 25 | public string ServerName => "Mp4upload"; 26 | 27 | /// 28 | /// Initializes an instance of . 29 | /// 30 | public Mp4uploadExtractor(Func httpClientProvider) 31 | : this(new HttpClientFactory(httpClientProvider)) { } 32 | 33 | /// 34 | /// Initializes an instance of . 35 | /// 36 | public Mp4uploadExtractor() 37 | : this(Http.ClientProvider) { } 38 | 39 | /// 40 | public async ValueTask> ExtractAsync( 41 | string url, 42 | CancellationToken cancellationToken = default 43 | ) => await ExtractAsync(url, [], cancellationToken); 44 | 45 | /// 46 | public async ValueTask> ExtractAsync( 47 | string url, 48 | Dictionary headers, 49 | CancellationToken cancellationToken = default 50 | ) 51 | { 52 | var http = _httpClientFactory.CreateClient(); 53 | 54 | headers = new Dictionary() { ["Referer"] = "https://mp4upload.com/" }; 55 | 56 | var response = await http.ExecuteAsync(url, headers, cancellationToken); 57 | 58 | var document = Html.Parse(response); 59 | 60 | var link = document 61 | .DocumentNode.Descendants() 62 | .Where(x => x.Name == "script") 63 | .FirstOrDefault(x => x.InnerText.Contains("src: ")) 64 | ?.InnerText.SubstringAfter("src: \"") 65 | .SubstringBefore("\""); 66 | if (!string.IsNullOrWhiteSpace(link)) 67 | { 68 | var host = link!.SubstringAfter("https://").SubstringBefore("/"); 69 | headers.Add("host", host); 70 | 71 | return 72 | [ 73 | new() 74 | { 75 | Format = VideoType.Container, 76 | VideoUrl = link!, 77 | Resolution = "Default Quality", 78 | Headers = headers, 79 | }, 80 | ]; 81 | } 82 | 83 | var packed = response 84 | .SubstringAfter("eval(function(p,a,c,k,e,d)") 85 | .Split(new[] { "" }, StringSplitOptions.None)[0]; 86 | 87 | var unpacked = JsUnpacker.UnpackAndCombine($"eval(function(p,a,c,k,e,d){packed}"); 88 | 89 | if (string.IsNullOrEmpty(unpacked)) 90 | return []; 91 | 92 | var videoUrl = unpacked 93 | .SubstringAfter("player.src(\"") 94 | .Split(new[] { "\");" }, StringSplitOptions.None)[0]; 95 | 96 | return 97 | [ 98 | new() 99 | { 100 | Format = VideoType.Container, 101 | VideoUrl = videoUrl, 102 | Resolution = "Default Quality", 103 | Headers = headers, 104 | }, 105 | ]; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Juro.Extractors/StreamSBExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text; 5 | using System.Text.Json.Nodes; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Juro.Core; 9 | using Juro.Core.Models.Videos; 10 | using Juro.Core.Utils; 11 | using Juro.Core.Utils.Extensions; 12 | 13 | namespace Juro.Extractors; 14 | 15 | /// 16 | /// Extractor for StreamSB. 17 | /// 18 | /// 19 | /// Initializes an instance of . 20 | /// 21 | public class StreamSBExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 22 | { 23 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 24 | 25 | private readonly char[] hexArray = "0123456789ABCDEF".ToCharArray(); 26 | 27 | /// 28 | public string ServerName => "StreamSB"; 29 | 30 | /// 31 | /// Initializes an instance of . 32 | /// 33 | public StreamSBExtractor(Func httpClientProvider) 34 | : this(new HttpClientFactory(httpClientProvider)) { } 35 | 36 | /// 37 | /// Initializes an instance of . 38 | /// 39 | public StreamSBExtractor() 40 | : this(Http.ClientProvider) { } 41 | 42 | /// 43 | public async ValueTask> ExtractAsync( 44 | string url, 45 | CancellationToken cancellationToken = default 46 | ) => await ExtractAsync(url, [], cancellationToken); 47 | 48 | /// 49 | public async ValueTask> ExtractAsync( 50 | string url, 51 | Dictionary headers, 52 | CancellationToken cancellationToken = default 53 | ) 54 | { 55 | var http = _httpClientFactory.CreateClient(); 56 | 57 | var id = url.FindBetween("/e/", ".html"); 58 | if (string.IsNullOrWhiteSpace(id)) 59 | id = url.Split(new[] { "/e/" }, StringSplitOptions.None)[1]; 60 | 61 | var bytes = Encoding.ASCII.GetBytes($"||{id}||||streamsb"); 62 | var bytesToHex = BytesToHex(bytes); 63 | 64 | var source = await http.ExecuteAsync( 65 | "https://raw.githubusercontent.com/jerry08/anistream-extras/main/streamsb.txt", 66 | cancellationToken 67 | ); 68 | 69 | var jsonLink = $"{source.Trim()}/{bytesToHex}/"; 70 | 71 | headers = new Dictionary() 72 | { 73 | //{ "watchsb", "streamsb" }, 74 | { "watchsb", "sbstream" }, 75 | { "User-Agent", Http.ChromeUserAgent() }, 76 | { "Referer", url }, 77 | }; 78 | 79 | var response = await http.ExecuteAsync(jsonLink, headers, cancellationToken); 80 | 81 | var data = JsonNode.Parse(response)!; 82 | var masterUrl = data["stream_data"]?["file"]?.ToString().Trim('"')!; 83 | 84 | return 85 | [ 86 | new() 87 | { 88 | Format = VideoType.M3u8, 89 | VideoUrl = masterUrl, 90 | Headers = headers, 91 | Resolution = "Multi Quality", 92 | }, 93 | ]; 94 | } 95 | 96 | private string BytesToHex(byte[] bytes) 97 | { 98 | var hexChars = new char[bytes.Length * 2]; 99 | for (var j = 0; j < bytes.Length; j++) 100 | { 101 | var v = bytes[j] & 0xFF; 102 | 103 | hexChars[j * 2] = hexArray[v >> 4]; 104 | hexChars[(j * 2) + 1] = hexArray[v & 0x0F]; 105 | } 106 | 107 | return new string(hexChars); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Juro.WebApi/Program.cs: -------------------------------------------------------------------------------- 1 | using Juro.Clients; 2 | using Juro.Core.Models; 3 | using Juro.Utils; 4 | using Juro.WebApi.Controllers.Anime; 5 | using Juro.WebApi.Controllers.Manga; 6 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 7 | 8 | namespace Juro.WebApi; 9 | 10 | public class Program 11 | { 12 | public static List Providers { get; set; } = []; 13 | 14 | public static void Main(string[] args) 15 | { 16 | var builder = WebApplication.CreateBuilder(args); 17 | 18 | // Add services to the container. 19 | 20 | builder.Services.AddControllers(); 21 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 22 | builder.Services.AddEndpointsApiExplorer(); 23 | builder.Services.AddSwaggerGen(); 24 | 25 | builder.Services.AddMvc(c => c.Conventions.Add(new ApiExplorerConvention())); 26 | 27 | //builder.Services.AddSingleton(); 28 | 29 | //var gg = typeof(Gogoanime); 30 | AssemblyEx.LoadReferencedAssemblies(); 31 | 32 | //var tt1 = AppDomain.CurrentDomain.GetAssemblies().ToList(); 33 | //var tt2 = tt1.Where(x => 34 | // x.GetCustomAttributes(typeof(PluginAssemblyAttribute), false).Length > 0 35 | // ) 36 | // .ToList(); 37 | 38 | var animeClient = new AnimeClient(); 39 | 40 | Providers.AddRange( 41 | animeClient 42 | .GetProviders() 43 | .Select(provider => new Provider() 44 | { 45 | Key = provider.Key, 46 | Name = provider.Name, 47 | Language = provider.Language, 48 | Type = ProviderType.Anime 49 | }) 50 | ); 51 | 52 | var animeProviderTypes = animeClient.GetProviderTypes(); 53 | 54 | foreach (var type in animeProviderTypes) 55 | { 56 | builder.Services.Add( 57 | //new ServiceDescriptor(typeof(IAnimeProvider), type, ServiceLifetime.Singleton) 58 | new ServiceDescriptor(type, type, ServiceLifetime.Singleton) 59 | ); 60 | } 61 | 62 | var mangaClient = new MangaClient(); 63 | 64 | Providers.AddRange( 65 | mangaClient 66 | .GetProviders() 67 | .Select(provider => new Provider() 68 | { 69 | Key = provider.Key, 70 | Name = provider.Name, 71 | Language = provider.Language, 72 | Type = ProviderType.Manga 73 | }) 74 | ); 75 | 76 | var mangaProviderTypes = mangaClient.GetProviderTypes(); 77 | 78 | foreach (var type in mangaProviderTypes) 79 | { 80 | builder.Services.Add(new ServiceDescriptor(type, type, ServiceLifetime.Singleton)); 81 | } 82 | 83 | var app = builder.Build(); 84 | 85 | // Configure the HTTP request pipeline. 86 | if (app.Environment.IsDevelopment()) 87 | { 88 | app.UseSwagger(); 89 | app.UseSwaggerUI(); 90 | } 91 | 92 | app.UseHttpsRedirection(); 93 | 94 | app.UseAuthorization(); 95 | 96 | app.MapControllers(); 97 | 98 | app.Run(); 99 | } 100 | } 101 | 102 | public class ApiExplorerConvention : IActionModelConvention 103 | { 104 | public void Apply(ActionModel action) 105 | { 106 | //action.ApiExplorer.IsVisible = 107 | // action.Controller.ControllerType.BaseType == typeof(ControllerBase); 108 | 109 | action.ApiExplorer.IsVisible = 110 | action.Controller.ControllerType != typeof(AnimeBaseController) 111 | && action.Controller.ControllerType != typeof(MangaBaseController); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CS1591: Missing XML comment for publicly visible type or member 4 | dotnet_diagnostic.CS1591.severity = none 5 | csharp_indent_labels = one_less_than_current 6 | csharp_space_around_binary_operators = before_and_after 7 | csharp_using_directive_placement = outside_namespace:silent 8 | csharp_prefer_simple_using_statement = true:suggestion 9 | csharp_prefer_braces = true:silent 10 | csharp_style_namespace_declarations = block_scoped:silent 11 | csharp_style_prefer_method_group_conversion = true:silent 12 | csharp_style_prefer_top_level_statements = true:silent 13 | csharp_style_expression_bodied_methods = false:silent 14 | csharp_style_expression_bodied_constructors = false:silent 15 | csharp_style_expression_bodied_operators = false:silent 16 | csharp_style_expression_bodied_properties = true:silent 17 | csharp_style_expression_bodied_indexers = true:silent 18 | csharp_style_expression_bodied_accessors = true:silent 19 | csharp_style_expression_bodied_lambdas = true:silent 20 | csharp_style_expression_bodied_local_functions = false:silent 21 | csharp_style_inlined_variable_declaration = true:suggestion 22 | csharp_style_deconstructed_variable_declaration = true:suggestion 23 | csharp_style_var_for_built_in_types = true:suggestion 24 | csharp_style_var_when_type_is_apparent = true:suggestion 25 | csharp_style_var_elsewhere = true:suggestion 26 | 27 | [*.{cs,vb}] 28 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 29 | tab_width = 4 30 | indent_size = 4 31 | end_of_line = crlf 32 | dotnet_style_coalesce_expression = true:suggestion 33 | dotnet_style_null_propagation = true:suggestion 34 | [*.{cs,vb}] 35 | #### Naming styles #### 36 | 37 | # Naming rules 38 | 39 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 40 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 41 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 42 | 43 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 44 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 45 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 46 | 47 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 48 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 49 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 50 | 51 | # Symbol specifications 52 | 53 | dotnet_naming_symbols.interface.applicable_kinds = interface 54 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 55 | dotnet_naming_symbols.interface.required_modifiers = 56 | 57 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 58 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 59 | dotnet_naming_symbols.types.required_modifiers = 60 | 61 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 62 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 63 | dotnet_naming_symbols.non_field_members.required_modifiers = 64 | 65 | # Naming styles 66 | 67 | dotnet_naming_style.begins_with_i.required_prefix = I 68 | dotnet_naming_style.begins_with_i.required_suffix = 69 | dotnet_naming_style.begins_with_i.word_separator = 70 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 71 | 72 | dotnet_naming_style.pascal_case.required_prefix = 73 | dotnet_naming_style.pascal_case.required_suffix = 74 | dotnet_naming_style.pascal_case.word_separator = 75 | dotnet_naming_style.pascal_case.capitalization = pascal_case 76 | 77 | dotnet_naming_style.pascal_case.required_prefix = 78 | dotnet_naming_style.pascal_case.required_suffix = 79 | dotnet_naming_style.pascal_case.word_separator = 80 | dotnet_naming_style.pascal_case.capitalization = pascal_case 81 | -------------------------------------------------------------------------------- /Juro.Extractors/DoodExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Juro.Core; 8 | using Juro.Core.Models.Videos; 9 | using Juro.Core.Utils; 10 | using Juro.Core.Utils.Extensions; 11 | 12 | namespace Juro.Extractors; 13 | 14 | /// 15 | /// Extractor for Dood. 16 | /// 17 | /// 18 | /// Initializes an instance of . 19 | /// 20 | public class DoodExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 21 | { 22 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 23 | 24 | /// 25 | public string ServerName => "Dood"; 26 | 27 | /// 28 | /// Initializes an instance of . 29 | /// 30 | public DoodExtractor(Func httpClientProvider) 31 | : this(new HttpClientFactory(httpClientProvider)) { } 32 | 33 | /// 34 | /// Initializes an instance of . 35 | /// 36 | public DoodExtractor() 37 | : this(Http.ClientProvider) { } 38 | 39 | /// 40 | public async ValueTask> ExtractAsync( 41 | string url, 42 | CancellationToken cancellationToken = default 43 | ) => await ExtractAsync(url, [], cancellationToken); 44 | 45 | /// 46 | public async ValueTask> ExtractAsync( 47 | string url, 48 | Dictionary headers, 49 | CancellationToken cancellationToken = default 50 | ) 51 | { 52 | var http = _httpClientFactory.CreateClient(); 53 | 54 | var list = new List(); 55 | 56 | try 57 | { 58 | var response = await http.ExecuteAsync( 59 | url, 60 | new Dictionary() { ["User-Agent"] = "Juro" }, 61 | cancellationToken 62 | ); 63 | 64 | if (!response.Contains("'/pass_md5/")) 65 | return []; 66 | 67 | var doodTld = url.SubstringAfter("https://dood.").SubstringBefore("/"); 68 | var md5 = response.SubstringAfter("'/pass_md5/").SubstringBefore("',"); 69 | var token = md5.Split(new[] { "/" }, StringSplitOptions.None).LastOrDefault(); 70 | var randomString = RandomString(); 71 | var expiry = DateTime.Now.CurrentTimeMillis(); 72 | 73 | var videoUrlStart = await http.ExecuteAsync( 74 | $"https://dood.{doodTld}/pass_md5/{md5}", 75 | new Dictionary() { ["Referer"] = url, ["User-Agent"] = "Juro" }, 76 | cancellationToken 77 | ); 78 | 79 | var videoUrl = $"{videoUrlStart}{randomString}?token={token}&expiry={expiry}"; 80 | 81 | list.Add( 82 | new() 83 | { 84 | Format = VideoType.Container, 85 | VideoUrl = videoUrl, 86 | Resolution = "Default Quality", 87 | Headers = new() 88 | { 89 | ["User-Agent"] = "Juro", 90 | ["Referer"] = $"https://dood.{doodTld}", 91 | }, 92 | } 93 | ); 94 | } 95 | catch 96 | { 97 | // Ignore 98 | } 99 | 100 | return list; 101 | } 102 | 103 | private static Random random = new(); 104 | 105 | private static string RandomString(int length = 10) 106 | { 107 | const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 108 | return new string( 109 | Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray() 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Juro.Core/Utils/Tasks/TaskEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Juro.Utils.Tasks; 8 | 9 | namespace Juro.Core.Utils.Tasks; 10 | 11 | internal static class TaskEx 12 | { 13 | private static readonly TaskFactory _myTaskFactory = new( 14 | CancellationToken.None, 15 | TaskCreationOptions.None, 16 | TaskContinuationOptions.None, 17 | TaskScheduler.Default 18 | ); 19 | 20 | public static TResult RunSync(Func> func) 21 | { 22 | var cultureUi = CultureInfo.CurrentUICulture; 23 | var culture = CultureInfo.CurrentCulture; 24 | return _myTaskFactory 25 | .StartNew(() => 26 | { 27 | Thread.CurrentThread.CurrentCulture = culture; 28 | Thread.CurrentThread.CurrentUICulture = cultureUi; 29 | return func(); 30 | }) 31 | .Unwrap() 32 | .GetAwaiter() 33 | .GetResult(); 34 | } 35 | 36 | public static void RunSync(Func func) 37 | { 38 | var cultureUi = CultureInfo.CurrentUICulture; 39 | var culture = CultureInfo.CurrentCulture; 40 | _myTaskFactory 41 | .StartNew(() => 42 | { 43 | Thread.CurrentThread.CurrentCulture = culture; 44 | Thread.CurrentThread.CurrentUICulture = cultureUi; 45 | return func(); 46 | }) 47 | .Unwrap() 48 | .GetAwaiter() 49 | .GetResult(); 50 | } 51 | 52 | public static Task Run( 53 | IEnumerable>> actions, 54 | int maxCount = 1, 55 | IProgress? progress = null 56 | ) 57 | { 58 | return InternalRun(actions.ToArray(), maxCount, progress); 59 | } 60 | 61 | public static Task Run( 62 | IEnumerable> actions, 63 | int maxCount = 1, 64 | IProgress? progress = null 65 | ) 66 | { 67 | return InternalRun(actions.ToArray(), maxCount, progress); 68 | } 69 | 70 | private static Task InternalRun( 71 | Func>[] actions, 72 | int maxCount = 1, 73 | IProgress? progress = null 74 | ) 75 | { 76 | var semaphore = new ResizableSemaphore { MaxCount = maxCount }; 77 | 78 | var totalCompleted = 0; 79 | 80 | var newTasks = Enumerable 81 | .Range(0, actions.Length) 82 | .Select(i => 83 | Task.Run(async () => 84 | { 85 | using var access = await semaphore.AcquireAsync(); 86 | 87 | var result = await actions[i](); 88 | totalCompleted++; 89 | progress?.Report(totalCompleted); 90 | 91 | return result; 92 | }) 93 | ) 94 | .ToArray(); 95 | 96 | return Task.WhenAll(newTasks); 97 | } 98 | 99 | private static Task InternalRun( 100 | Func[] actions, 101 | int maxCount = 1, 102 | IProgress? progress = null 103 | ) 104 | { 105 | var semaphore = new ResizableSemaphore { MaxCount = maxCount }; 106 | 107 | var totalCompleted = 0; 108 | 109 | var newTasks = Enumerable 110 | .Range(0, actions.Length) 111 | .Select(i => 112 | Task.Run(async () => 113 | { 114 | using var access = await semaphore.AcquireAsync(); 115 | 116 | await actions[i](); 117 | totalCompleted++; 118 | progress?.Report(totalCompleted); 119 | }) 120 | ) 121 | .ToArray(); 122 | 123 | return Task.WhenAll(newTasks); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Juro.Extractors/StreamSBProExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text.Json.Nodes; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Juro.Core; 9 | using Juro.Core.Models.Videos; 10 | using Juro.Core.Utils; 11 | using Juro.Core.Utils.Extensions; 12 | 13 | namespace Juro.Extractors; 14 | 15 | /// 16 | /// Extractor for StreamSB Pro. 17 | /// 18 | /// 19 | /// Initializes an instance of . 20 | /// 21 | public class StreamSBProExtractor(IHttpClientFactory httpClientFactory) : IVideoExtractor 22 | { 23 | private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; 24 | 25 | private readonly string _alphabet = 26 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 27 | 28 | /// 29 | public string ServerName => "StreamSB Pro"; 30 | 31 | /// 32 | /// Initializes an instance of . 33 | /// 34 | public StreamSBProExtractor(Func httpClientProvider) 35 | : this(new HttpClientFactory(httpClientProvider)) { } 36 | 37 | /// 38 | /// Initializes an instance of . 39 | /// 40 | public StreamSBProExtractor() 41 | : this(Http.ClientProvider) { } 42 | 43 | /// 44 | public async ValueTask> ExtractAsync( 45 | string url, 46 | CancellationToken cancellationToken = default 47 | ) => await ExtractAsync(url, [], cancellationToken); 48 | 49 | /// 50 | public async ValueTask> ExtractAsync( 51 | string url, 52 | Dictionary headers, 53 | CancellationToken cancellationToken = default 54 | ) 55 | { 56 | var http = _httpClientFactory.CreateClient(); 57 | 58 | var id = url.FindBetween("/e/", ".html"); 59 | if (string.IsNullOrWhiteSpace(id)) 60 | id = url.Split(new[] { "/e/" }, StringSplitOptions.None)[1]; 61 | 62 | var source = await http.ExecuteAsync( 63 | "https://raw.githubusercontent.com/jerry08/juro-data/main/streamsb.txt", 64 | cancellationToken 65 | ); 66 | 67 | var jsonLink = $"{source.Trim()}/{Encode(id)}"; 68 | 69 | headers = new Dictionary() 70 | { 71 | //{ "watchsb", "streamsb" }, 72 | { "watchsb", "sbstream" }, 73 | { "User-Agent", Http.ChromeUserAgent() }, 74 | { "Referer", url }, 75 | }; 76 | 77 | var response = await http.ExecuteAsync(jsonLink, headers, cancellationToken); 78 | 79 | var data = JsonNode.Parse(response); 80 | var masterUrl = data?["stream_data"]?["file"]?.ToString().Trim('"')!; 81 | 82 | return 83 | [ 84 | new() 85 | { 86 | Format = VideoType.M3u8, 87 | VideoUrl = masterUrl, 88 | Headers = headers, 89 | Resolution = "Multi Quality", 90 | }, 91 | ]; 92 | } 93 | 94 | private string Encode(string id) 95 | { 96 | id = $"{MakeId(12)}||{id}||{MakeId(12)}||streamsb"; 97 | 98 | var output = ""; 99 | var arr = id.ToArray(); 100 | 101 | for (var i = 0; i < arr.Length; i++) 102 | { 103 | output += Convert.ToString(Convert.ToInt32(((int)arr[i]).ToString(), 10), 16); 104 | } 105 | 106 | return output; 107 | } 108 | 109 | private string MakeId(int length) 110 | { 111 | var output = ""; 112 | 113 | for (var i = 0; i < length; i++) 114 | { 115 | output += _alphabet[(int)Math.Floor(new Random().NextDouble() * _alphabet.Length)]; 116 | } 117 | 118 | return output; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Juro/Clients/MangaApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization.Metadata; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Juro.Core; 9 | using Juro.Core.Models; 10 | using Juro.Core.Models.Manga; 11 | using Juro.Core.Utils; 12 | using Juro.Core.Utils.Extensions; 13 | 14 | namespace Juro.Clients; 15 | 16 | public class MangaApiClient(string baseUrl, IHttpClientFactory httpClientFactory) 17 | { 18 | private readonly HttpClient _http = httpClientFactory.CreateClient(); 19 | private readonly JsonSerializerOptions _options = new() 20 | { 21 | PropertyNameCaseInsensitive = true, 22 | TypeInfoResolver = new DefaultJsonTypeInfoResolver 23 | { 24 | Modifiers = 25 | { 26 | static typeInfo => 27 | { 28 | if (typeInfo.Type == typeof(IMangaInfo)) 29 | typeInfo.CreateObject = () => new MangaInfo(); 30 | else if (typeInfo.Type == typeof(IMangaResult)) 31 | typeInfo.CreateObject = () => new MangaResult(); 32 | else if (typeInfo.Type == typeof(IMangaChapter)) 33 | typeInfo.CreateObject = () => new MangaChapter(); 34 | else if (typeInfo.Type == typeof(IMangaChapterPage)) 35 | typeInfo.CreateObject = () => new MangaChapterPage(); 36 | }, 37 | }, 38 | }, 39 | }; 40 | 41 | public string BaseUrl { get; set; } = baseUrl; 42 | 43 | public string ProviderKey { get; set; } = "Manga"; 44 | 45 | /// 46 | /// Initializes an instance of . 47 | /// 48 | public MangaApiClient(string baseUrl, Func httpClientProvider) 49 | : this(baseUrl, new HttpClientFactory(httpClientProvider)) { } 50 | 51 | /// 52 | /// Initializes an instance of . 53 | /// 54 | public MangaApiClient(string baseUrl) 55 | : this(baseUrl, Http.ClientProvider) { } 56 | 57 | public async ValueTask> GetProvidersAsync( 58 | CancellationToken cancellationToken = default 59 | ) 60 | { 61 | var response = await _http.ExecuteAsync( 62 | $"{BaseUrl}/Providers?type={(int)ProviderType.Manga}", 63 | cancellationToken: cancellationToken 64 | ); 65 | 66 | return JsonSerializer.Deserialize>(response, _options)!; 67 | } 68 | 69 | public async ValueTask GetAsync( 70 | string id, 71 | CancellationToken cancellationToken = default 72 | ) 73 | { 74 | var response = await _http.ExecuteAsync( 75 | $"{BaseUrl}/{ProviderKey}/{Uri.EscapeDataString(id)}", 76 | cancellationToken: cancellationToken 77 | ); 78 | 79 | return JsonSerializer.Deserialize(response, _options)!; 80 | } 81 | 82 | public async ValueTask> SearchAsync( 83 | string query, 84 | CancellationToken cancellationToken = default 85 | ) 86 | { 87 | var response = await _http.ExecuteAsync( 88 | $"{BaseUrl}/{ProviderKey}/Search?q={Uri.EscapeDataString(query)}", 89 | cancellationToken: cancellationToken 90 | ); 91 | 92 | return JsonSerializer.Deserialize>(response, _options) ?? []; 93 | } 94 | 95 | public async ValueTask> GetChapterPagesAsync( 96 | string id, 97 | CancellationToken cancellationToken = default 98 | ) 99 | { 100 | var response = await _http.ExecuteAsync( 101 | $"{BaseUrl}/{ProviderKey}/ChapterPages/{Uri.EscapeDataString(id)}", 102 | cancellationToken: cancellationToken 103 | ); 104 | 105 | return JsonSerializer.Deserialize>(response, _options) ?? []; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/Anime/KaidoSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using Juro.Providers.Anime; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs.Anime; 7 | 8 | public class KaidoSpecs 9 | { 10 | [Fact] 11 | public async Task I_can_get_results_from_a_search_query() 12 | { 13 | // Arrange 14 | var provider = new Kaido(); 15 | 16 | // Act 17 | var results = await provider.SearchAsync("naruto"); 18 | 19 | // Assert 20 | results.Should().NotBeEmpty(); 21 | } 22 | 23 | [Fact] 24 | public async Task I_can_get_more_details_from_an_anime() 25 | { 26 | // Arrange 27 | var provider = new Kaido(); 28 | 29 | // Act 30 | var results = await provider.SearchAsync("naruto"); 31 | 32 | // Assert 33 | results.Should().NotBeEmpty(); 34 | 35 | // Act 36 | var animeInfo = await provider.GetAnimeInfoAsync(results[0].Id); 37 | 38 | // Assert 39 | animeInfo.Should().NotBeNull(); 40 | } 41 | 42 | [Fact] 43 | public async Task I_can_get_episode_results_from_an_anime() 44 | { 45 | // Arrange 46 | var provider = new Kaido(); 47 | 48 | // Act 49 | var results = await provider.SearchAsync("naruto"); 50 | 51 | // Assert 52 | results.Should().NotBeEmpty(); 53 | 54 | // Act 55 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 56 | 57 | // Assert 58 | episodes.Should().NotBeEmpty(); 59 | } 60 | 61 | [Fact] 62 | public async Task I_can_get_video_server_results_from_an_episode() 63 | { 64 | // Arrange 65 | var provider = new Kaido(); 66 | 67 | // Act 68 | var results = await provider.SearchAsync("naruto"); 69 | 70 | // Assert 71 | results.Should().NotBeEmpty(); 72 | 73 | // Act 74 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 75 | 76 | // Assert 77 | episodes.Should().NotBeEmpty(); 78 | 79 | // Act 80 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 81 | 82 | // Assert 83 | videoServers.Should().NotBeEmpty(); 84 | } 85 | 86 | [Fact] 87 | public async Task I_can_get_video_results_from_a_video_server() 88 | { 89 | // Arrange 90 | var provider = new Kaido(); 91 | 92 | // Act 93 | var results = await provider.SearchAsync("naruto"); 94 | 95 | // Assert 96 | results.Should().NotBeEmpty(); 97 | 98 | // Act 99 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 100 | 101 | // Assert 102 | episodes.Should().NotBeEmpty(); 103 | 104 | // Act 105 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 106 | 107 | // Assert 108 | videoServers.Should().NotBeEmpty(); 109 | 110 | // Act 111 | var videos = await provider.GetVideosAsync(videoServers[0]); 112 | 113 | // Assert 114 | videos.Should().NotBeEmpty(); 115 | } 116 | 117 | [Fact] 118 | public async Task I_can_get_video_results_from_all_video_servers() 119 | { 120 | // Arrange 121 | var provider = new Kaido(); 122 | 123 | // Act 124 | var results = await provider.SearchAsync("naruto"); 125 | 126 | // Assert 127 | results.Should().NotBeEmpty(); 128 | 129 | // Act 130 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 131 | 132 | // Assert 133 | episodes.Should().NotBeEmpty(); 134 | 135 | // Act 136 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 137 | 138 | // Assert 139 | videoServers.Should().NotBeEmpty(); 140 | 141 | // Act 142 | foreach (var videoServer in videoServers) 143 | { 144 | var videos = await provider.GetVideosAsync(videoServer); 145 | 146 | // Assert 147 | videos.Should().NotBeEmpty(); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/Anime/AniwaveSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using Juro.Providers.Anime; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs.Anime; 7 | 8 | public class AniwaveSpecs 9 | { 10 | [Fact] 11 | public async Task I_can_get_results_from_a_search_query() 12 | { 13 | // Arrange 14 | var provider = new Aniwave(); 15 | 16 | // Act 17 | var results = await provider.SearchAsync("naruto"); 18 | 19 | // Assert 20 | results.Should().NotBeEmpty(); 21 | } 22 | 23 | [Fact] 24 | public async Task I_can_get_more_details_from_an_anime() 25 | { 26 | // Arrange 27 | var provider = new Aniwave(); 28 | 29 | // Act 30 | var results = await provider.SearchAsync("naruto"); 31 | 32 | // Assert 33 | results.Should().NotBeEmpty(); 34 | 35 | // Act 36 | var animeInfo = await provider.GetAnimeInfoAsync(results[0].Id); 37 | 38 | // Assert 39 | animeInfo.Should().NotBeNull(); 40 | } 41 | 42 | [Fact] 43 | public async Task I_can_get_episode_results_from_an_anime() 44 | { 45 | // Arrange 46 | var provider = new Aniwave(); 47 | 48 | // Act 49 | var results = await provider.SearchAsync("naruto"); 50 | 51 | // Assert 52 | results.Should().NotBeEmpty(); 53 | 54 | // Act 55 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 56 | 57 | // Assert 58 | episodes.Should().NotBeEmpty(); 59 | } 60 | 61 | [Fact] 62 | public async Task I_can_get_video_server_results_from_an_episode() 63 | { 64 | // Arrange 65 | var provider = new Aniwave(); 66 | 67 | // Act 68 | var results = await provider.SearchAsync("naruto"); 69 | 70 | // Assert 71 | results.Should().NotBeEmpty(); 72 | 73 | // Act 74 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 75 | 76 | // Assert 77 | episodes.Should().NotBeEmpty(); 78 | 79 | // Act 80 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 81 | 82 | // Assert 83 | videoServers.Should().NotBeEmpty(); 84 | } 85 | 86 | [Fact] 87 | public async Task I_can_get_video_results_from_a_video_server() 88 | { 89 | // Arrange 90 | var provider = new Aniwave(); 91 | 92 | // Act 93 | var results = await provider.SearchAsync("naruto"); 94 | 95 | // Assert 96 | results.Should().NotBeEmpty(); 97 | 98 | // Act 99 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 100 | 101 | // Assert 102 | episodes.Should().NotBeEmpty(); 103 | 104 | // Act 105 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 106 | 107 | // Assert 108 | videoServers.Should().NotBeEmpty(); 109 | 110 | // Act 111 | var videos = await provider.GetVideosAsync(videoServers[0]); 112 | 113 | // Assert 114 | videos.Should().NotBeEmpty(); 115 | } 116 | 117 | [Fact] 118 | public async Task I_can_get_video_results_from_all_video_servers() 119 | { 120 | // Arrange 121 | var provider = new Aniwave(); 122 | 123 | // Act 124 | var results = await provider.SearchAsync("naruto"); 125 | 126 | // Assert 127 | results.Should().NotBeEmpty(); 128 | 129 | // Act 130 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 131 | 132 | // Assert 133 | episodes.Should().NotBeEmpty(); 134 | 135 | // Act 136 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 137 | 138 | // Assert 139 | videoServers.Should().NotBeEmpty(); 140 | 141 | // Act 142 | foreach (var videoServer in videoServers) 143 | { 144 | var videos = await provider.GetVideosAsync(videoServer); 145 | 146 | // Assert 147 | videos.Should().NotBeEmpty(); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/Anime/AniwatchSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using Juro.Providers.Anime; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs.Anime; 7 | 8 | public class AniwatchSpecs 9 | { 10 | [Fact] 11 | public async Task I_can_get_results_from_a_search_query() 12 | { 13 | // Arrange 14 | var provider = new Aniwatch(); 15 | 16 | // Act 17 | var results = await provider.SearchAsync("naruto"); 18 | 19 | // Assert 20 | results.Should().NotBeEmpty(); 21 | } 22 | 23 | [Fact] 24 | public async Task I_can_get_more_details_from_an_anime() 25 | { 26 | // Arrange 27 | var provider = new Aniwatch(); 28 | 29 | // Act 30 | var results = await provider.SearchAsync("naruto"); 31 | 32 | // Assert 33 | results.Should().NotBeEmpty(); 34 | 35 | // Act 36 | var animeInfo = await provider.GetAnimeInfoAsync(results[0].Id); 37 | 38 | // Assert 39 | animeInfo.Should().NotBeNull(); 40 | } 41 | 42 | [Fact] 43 | public async Task I_can_get_episode_results_from_an_anime() 44 | { 45 | // Arrange 46 | var provider = new Aniwatch(); 47 | 48 | // Act 49 | var results = await provider.SearchAsync("naruto"); 50 | 51 | // Assert 52 | results.Should().NotBeEmpty(); 53 | 54 | // Act 55 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 56 | 57 | // Assert 58 | episodes.Should().NotBeEmpty(); 59 | } 60 | 61 | [Fact] 62 | public async Task I_can_get_video_server_results_from_an_episode() 63 | { 64 | // Arrange 65 | var provider = new Aniwatch(); 66 | 67 | // Act 68 | var results = await provider.SearchAsync("naruto"); 69 | 70 | // Assert 71 | results.Should().NotBeEmpty(); 72 | 73 | // Act 74 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 75 | 76 | // Assert 77 | episodes.Should().NotBeEmpty(); 78 | 79 | // Act 80 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 81 | 82 | // Assert 83 | videoServers.Should().NotBeEmpty(); 84 | } 85 | 86 | [Fact] 87 | public async Task I_can_get_video_results_from_a_video_server() 88 | { 89 | // Arrange 90 | var provider = new Aniwatch(); 91 | 92 | // Act 93 | var results = await provider.SearchAsync("naruto"); 94 | 95 | // Assert 96 | results.Should().NotBeEmpty(); 97 | 98 | // Act 99 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 100 | 101 | // Assert 102 | episodes.Should().NotBeEmpty(); 103 | 104 | // Act 105 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 106 | 107 | // Assert 108 | videoServers.Should().NotBeEmpty(); 109 | 110 | // Act 111 | var videos = await provider.GetVideosAsync(videoServers[0]); 112 | 113 | // Assert 114 | videos.Should().NotBeEmpty(); 115 | } 116 | 117 | [Fact] 118 | public async Task I_can_get_video_results_from_all_video_servers() 119 | { 120 | // Arrange 121 | var provider = new Aniwatch(); 122 | 123 | // Act 124 | var results = await provider.SearchAsync("naruto"); 125 | 126 | // Assert 127 | results.Should().NotBeEmpty(); 128 | 129 | // Act 130 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 131 | 132 | // Assert 133 | episodes.Should().NotBeEmpty(); 134 | 135 | // Act 136 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 137 | 138 | // Assert 139 | videoServers.Should().NotBeEmpty(); 140 | 141 | // Act 142 | foreach (var videoServer in videoServers) 143 | { 144 | var videos = await provider.GetVideosAsync(videoServer); 145 | 146 | // Assert 147 | videos.Should().NotBeEmpty(); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Juro.Tests/Specs/Anime/AnimePaheSpecs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using Juro.Providers.Anime; 4 | using Xunit; 5 | 6 | namespace Juro.Tests.Specs.Anime; 7 | 8 | public class AnimePaheSpecs 9 | { 10 | [Fact] 11 | public async Task I_can_get_results_from_a_search_query() 12 | { 13 | // Arrange 14 | var provider = new AnimePahe(); 15 | 16 | // Act 17 | var results = await provider.SearchAsync("naruto"); 18 | 19 | // Assert 20 | results.Should().NotBeEmpty(); 21 | } 22 | 23 | [Fact] 24 | public async Task I_can_get_more_details_from_an_anime() 25 | { 26 | // Arrange 27 | var provider = new AnimePahe(); 28 | 29 | // Act 30 | var results = await provider.SearchAsync("naruto"); 31 | 32 | // Assert 33 | results.Should().NotBeEmpty(); 34 | 35 | // Act 36 | var animeInfo = await provider.GetAnimeInfoAsync(results[0].Id); 37 | 38 | // Assert 39 | animeInfo.Should().NotBeNull(); 40 | } 41 | 42 | [Fact] 43 | public async Task I_can_get_episode_results_from_an_anime() 44 | { 45 | // Arrange 46 | var provider = new AnimePahe(); 47 | 48 | // Act 49 | var results = await provider.SearchAsync("naruto"); 50 | 51 | // Assert 52 | results.Should().NotBeEmpty(); 53 | 54 | // Act 55 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 56 | 57 | // Assert 58 | episodes.Should().NotBeEmpty(); 59 | } 60 | 61 | [Fact] 62 | public async Task I_can_get_video_server_results_from_an_episode() 63 | { 64 | // Arrange 65 | var provider = new AnimePahe(); 66 | 67 | // Act 68 | var results = await provider.SearchAsync("naruto"); 69 | 70 | // Assert 71 | results.Should().NotBeEmpty(); 72 | 73 | // Act 74 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 75 | 76 | // Assert 77 | episodes.Should().NotBeEmpty(); 78 | 79 | // Act 80 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 81 | 82 | // Assert 83 | videoServers.Should().NotBeEmpty(); 84 | } 85 | 86 | [Fact] 87 | public async Task I_can_get_video_results_from_a_video_server() 88 | { 89 | // Arrange 90 | var provider = new AnimePahe(); 91 | 92 | // Act 93 | var results = await provider.SearchAsync("naruto"); 94 | 95 | // Assert 96 | results.Should().NotBeEmpty(); 97 | 98 | // Act 99 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 100 | 101 | // Assert 102 | episodes.Should().NotBeEmpty(); 103 | 104 | // Act 105 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 106 | 107 | // Assert 108 | videoServers.Should().NotBeEmpty(); 109 | 110 | // Act 111 | var videos = await provider.GetVideosAsync(videoServers[0]); 112 | 113 | // Assert 114 | videos.Should().NotBeEmpty(); 115 | } 116 | 117 | [Fact] 118 | public async Task I_can_get_video_results_from_all_video_servers() 119 | { 120 | // Arrange 121 | var provider = new AnimePahe(); 122 | 123 | // Act 124 | var results = await provider.SearchAsync("naruto"); 125 | 126 | // Assert 127 | results.Should().NotBeEmpty(); 128 | 129 | // Act 130 | var episodes = await provider.GetEpisodesAsync(results[0].Id); 131 | 132 | // Assert 133 | episodes.Should().NotBeEmpty(); 134 | 135 | // Act 136 | var videoServers = await provider.GetVideoServersAsync(episodes[0].Id); 137 | 138 | // Assert 139 | videoServers.Should().NotBeEmpty(); 140 | 141 | // Act 142 | foreach (var videoServer in videoServers) 143 | { 144 | var videos = await provider.GetVideosAsync(videoServer); 145 | 146 | // Assert 147 | videos.Should().NotBeEmpty(); 148 | } 149 | } 150 | } 151 | --------------------------------------------------------------------------------