├── 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 |
--------------------------------------------------------------------------------