From 2513959d487cbef72e2119477be103a2d3ca2f72 Mon Sep 17 00:00:00 2001 From: Dennis Stratmann Date: Tue, 7 Apr 2026 14:21:00 +0100 Subject: [PATCH] v0.6.0: add more supported sites --- .github/assets/TokyBay.gif | Bin 531376 -> 496891 bytes .github/workflows/release.yml | 2 +- .gitignore | 5 +- CLAUDE.md | 6 +- README.md | 13 ++- TokyBay/Pages/MainPage.cs | 4 +- TokyBay/Pages/SearchTokybookPage.cs | 6 +- TokyBay/Program.cs | 2 +- TokyBay/Scraper/Base/BaseScraperStrategy.cs | 6 +- .../Scraper/Configuration/ScraperConfig.cs | 2 +- .../Strategies/GoldenAudiobookStrategy.cs | 14 ++- .../Strategies/PlaylistAudiobookStrategy.cs | 100 ++++++++++++++++++ .../Scraper/Strategies/TokybookStrategy.cs | 10 +- .../Scraper/Strategies/ZAudiobooksStrategy.cs | 6 +- TokyBay/ScraperServiceExtensions.cs | 1 + TokyBay/Services/DownloadService.cs | 7 ++ TokyBay/Services/IpifyService.cs | 6 +- TokyBay/Services/SettingsService.cs | 2 +- TokyBay/TokyBay.csproj | 6 +- 19 files changed, 164 insertions(+), 34 deletions(-) create mode 100644 TokyBay/Scraper/Strategies/PlaylistAudiobookStrategy.cs diff --git a/.github/assets/TokyBay.gif b/.github/assets/TokyBay.gif index 340d2126883969968e038101ff5771761807978d..16d001806e34c6916fd261dde1dc1afe5e683a35 100644 GIT binary patch delta 81 zcmdn6UE%jixd|$asT);K$Tzbox3eiT0x=U1GXpUT5VHa?+jcf(_FILF8rvUEVV7c- e{3q(0n3Aeco>`Ki5S(0;nwl4q@1B{)U=0BIxfs9z delta 29458 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 311e054..a39fd68 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,7 +72,7 @@ jobs: run: | printf '%s' "$PR_BODY" > release_notes.md gh release create "v${{ steps.version.outputs.value }}" \ - --title "v${{ steps.version.outputs.value }}" \ + --title "Release v${{ steps.version.outputs.value }}" \ --notes-file release_notes.md \ --latest \ publish/tokybay-win-x64.zip \ diff --git a/.gitignore b/.gitignore index d1fcb25..3f17b7f 100644 --- a/.gitignore +++ b/.gitignore @@ -214,4 +214,7 @@ AppPackages/ ##### # End of core ignore list, below put you custom 'per project' settings (patterns or path) -##### \ No newline at end of file +##### + +# Claude Code +.claude/settings.local.json \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 8b20d2e..71c4947 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,11 @@ IScraperStrategy (Scraper/Abstractions/) └── BaseScraperStrategy (Scraper/Base/) ├── TokybookStrategy (Scraper/Strategies/) — tokybook.com ├── ZAudiobooksStrategy (Scraper/Strategies/) — zaudiobooks / freeaudiobooks.top - └── GoldenAudiobookStrategy (Scraper/Strategies/) — goldenaudiobook.net + ├── GoldenAudiobookStrategy (Scraper/Strategies/) — alle Sites mit `` Struktur: + │ goldenaudiobook.net, fulllengthaudiobooks.net, bigaudiobooks.net, + │ findaudiobook.com, bookaudiobook.net, hotaudiobooks.com, audiozaic.com + └── PlaylistAudiobookStrategy (Scraper/Strategies/) — alle Sites mit `data-playlist` JSON-Attribut: + hdaudiobooks.net ``` - `ScraperFactory` wählt per `CanHandle(url)` die passende Strategie aus diff --git a/README.md b/README.md index 949825f..2f400ca 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,20 @@ That's it. TokyBay will guide you through the rest. ## Supported Sites +*Last checked: 2026-04-07* + | Site | Status | |------|--------| | [tokybook.com](https://tokybook.com) | Website temporarily offline, API operational | -| [zaudiobooks.com](https://zaudiobooks.com) / [freeaudiobooks.top](https://freeaudiobooks.top) | Working | +| [zaudiobooks.com](https://zaudiobooks.com) / [freeaudiobooks.top](https://freeaudiobooks.top) | Currently unreachable | | [goldenaudiobook.net](https://goldenaudiobook.net) | Working | +| [fulllengthaudiobooks.net](https://fulllengthaudiobooks.net) | Working | +| [bigaudiobooks.net](https://bigaudiobooks.net) | Working | +| [findaudiobook.com](https://findaudiobook.com) | Working | +| [bookaudiobook.net](https://bookaudiobook.net) | Working | +| [hotaudiobooks.com](https://hotaudiobooks.com) | Working | +| [hdaudiobooks.net](https://hdaudiobooks.net) | Working | +| [audiozaic.com](https://audiozaic.com) | Working | ## Features @@ -75,7 +84,7 @@ That's it. TokyBay will guide you through the rest. 2. **Direct URL download**: Download any audiobook directly by URL 3. **M4B conversion**: Automatically convert to the M4B audiobook format after download 4. **MP3 conversion**: Automatically convert to MP3 format after download -5. **Multi-site support**: Works with Tokybook, ZAudiobooks/FreeAudiobooks, and GoldenAudiobook +5. **Multi-site support**: Works with Tokybook, ZAudiobooks/FreeAudiobooks, GoldenAudiobook, and FullLengthAudiobooks 6. **Settings**: Persistent in-app settings — download path, conversion preferences ## Usage diff --git a/TokyBay/Pages/MainPage.cs b/TokyBay/Pages/MainPage.cs index bec971b..53a7e1b 100644 --- a/TokyBay/Pages/MainPage.cs +++ b/TokyBay/Pages/MainPage.cs @@ -3,12 +3,12 @@ namespace TokyBay.Pages { public class MainPage( - IPageService pageHandler, + IPageService pageService, SearchTokybookPage searchPage, DownloadPage downloadPage, SettingsPage settingsPage) { - private readonly IPageService _pageService = pageHandler; + private readonly IPageService _pageService = pageService; private readonly SearchTokybookPage _searchPage = searchPage; private readonly DownloadPage _downloadPage = downloadPage; private readonly SettingsPage _settingsPage = settingsPage; diff --git a/TokyBay/Pages/SearchTokybookPage.cs b/TokyBay/Pages/SearchTokybookPage.cs index 4172e55..a001b27 100644 --- a/TokyBay/Pages/SearchTokybookPage.cs +++ b/TokyBay/Pages/SearchTokybookPage.cs @@ -8,7 +8,7 @@ namespace TokyBay.Pages public class SearchTokybookPage( IAnsiConsole console, IPageService pageService, - IHttpService httpUtil, + IHttpService httpService, IIpifyService ipifyService, DownloadService downloadService) { @@ -18,7 +18,7 @@ public class SearchTokybookPage( private readonly IAnsiConsole _console = console; private readonly IPageService _pageService = pageService; - private readonly IHttpService _httpUtil = httpUtil; + private readonly IHttpService _httpService = httpService; private readonly IIpifyService _ipifyService = ipifyService; private readonly DownloadService _downloadService = downloadService; @@ -126,7 +126,7 @@ await _console.Status() }; var content = new StringContent(payload.ToString(), Encoding.UTF8, "application/json"); - var response = await _httpUtil.PostAsync(TokybookUrl + SearchApiPath, content); + var response = await _httpService.PostAsync(TokybookUrl + SearchApiPath, content); if (!response.IsSuccessStatusCode) { diff --git a/TokyBay/Program.cs b/TokyBay/Program.cs index 454aee8..e8448e3 100644 --- a/TokyBay/Program.cs +++ b/TokyBay/Program.cs @@ -45,7 +45,7 @@ private static void ConfigureServices(IServiceCollection services) config.MaxParallelConversions = 3; config.MaxSegmentsPerTrack = 8; config.RetryAttempts = 3; - config.RetryDelayMs = 1000; + config.RetryDelayMs = 100; }); services.AddTransient(); diff --git a/TokyBay/Scraper/Base/BaseScraperStrategy.cs b/TokyBay/Scraper/Base/BaseScraperStrategy.cs index 526de01..64a5c14 100644 --- a/TokyBay/Scraper/Base/BaseScraperStrategy.cs +++ b/TokyBay/Scraper/Base/BaseScraperStrategy.cs @@ -11,12 +11,12 @@ namespace TokyBay.Scraper.Base { public abstract partial class BaseScraperStrategy( IAnsiConsole console, - IHttpService httpUtil, + IHttpService httpService, ISettingsService settingsService, ScraperConfig? config = null) : IScraperStrategy { protected readonly IAnsiConsole _console = console; - protected readonly IHttpService _httpUtil = httpUtil; + protected readonly IHttpService _httpService = httpService; protected readonly UserSettings _settings = settingsService.GetSettings(); protected readonly ScraperConfig _config = config ?? new ScraperConfig(); @@ -137,7 +137,7 @@ protected async Task DownloadDirectFileAsync(string trackSrc, string tra var filePath = Path.Combine(folderPath, trackTitle); try { - var response = await _httpUtil.GetAsync(trackSrc); + var response = await _httpService.GetAsync(trackSrc); if (!response.IsSuccessStatusCode) { _console.MarkupLine($"[red]Could not download chapter: {trackTitle}[/]"); diff --git a/TokyBay/Scraper/Configuration/ScraperConfig.cs b/TokyBay/Scraper/Configuration/ScraperConfig.cs index 9359d22..0c84291 100644 --- a/TokyBay/Scraper/Configuration/ScraperConfig.cs +++ b/TokyBay/Scraper/Configuration/ScraperConfig.cs @@ -6,6 +6,6 @@ public class ScraperConfig public int MaxParallelConversions { get; set; } = 2; public int MaxSegmentsPerTrack { get; set; } = 5; public int RetryAttempts { get; set; } = 3; - public int RetryDelayMs { get; set; } = 1000; + public int RetryDelayMs { get; set; } = 100; } } diff --git a/TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs b/TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs index 34aa11c..4550616 100644 --- a/TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs +++ b/TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs @@ -9,9 +9,9 @@ namespace TokyBay.Scraper.Strategies { public partial class GoldenAudiobookStrategy( IAnsiConsole console, - IHttpService httpUtil, + IHttpService httpService, ISettingsService settingsService, - ScraperConfig? config = null) : BaseScraperStrategy(console, httpUtil, settingsService, config) + ScraperConfig? config = null) : BaseScraperStrategy(console, httpService, settingsService, config) { // Matches tags with type="audio/mpeg" in either attribute order [GeneratedRegex(@"]*type=""audio/mpeg""[^>]*>", RegexOptions.IgnoreCase)] @@ -22,7 +22,13 @@ public partial class GoldenAudiobookStrategy( public override bool CanHandle(string bookUrl) { - return bookUrl.Contains("goldenaudiobook.net", StringComparison.OrdinalIgnoreCase); + return bookUrl.Contains("goldenaudiobook.net", StringComparison.OrdinalIgnoreCase) + || bookUrl.Contains("fulllengthaudiobooks.net", StringComparison.OrdinalIgnoreCase) + || bookUrl.Contains("bigaudiobooks.net", StringComparison.OrdinalIgnoreCase) + || bookUrl.Contains("findaudiobook.com", StringComparison.OrdinalIgnoreCase) + || bookUrl.Contains("bookaudiobook.net", StringComparison.OrdinalIgnoreCase) + || bookUrl.Contains("hotaudiobooks.com", StringComparison.OrdinalIgnoreCase) + || bookUrl.Contains("audiozaic.com", StringComparison.OrdinalIgnoreCase); } public override async Task DownloadBookAsync(string bookUrl) @@ -66,7 +72,7 @@ await _console.Status() { try { - var response = await _httpUtil.GetAsync(bookUrl); + var response = await _httpService.GetAsync(bookUrl); response.EnsureSuccessStatusCode(); var html = await response.Content.ReadAsStringAsync(); diff --git a/TokyBay/Scraper/Strategies/PlaylistAudiobookStrategy.cs b/TokyBay/Scraper/Strategies/PlaylistAudiobookStrategy.cs new file mode 100644 index 0000000..17db6a5 --- /dev/null +++ b/TokyBay/Scraper/Strategies/PlaylistAudiobookStrategy.cs @@ -0,0 +1,100 @@ +using Newtonsoft.Json.Linq; +using Spectre.Console; +using System.Text.RegularExpressions; +using TokyBay.Models; +using TokyBay.Scraper.Base; +using TokyBay.Scraper.Configuration; +using TokyBay.Services; + +namespace TokyBay.Scraper.Strategies +{ + public partial class PlaylistAudiobookStrategy( + IAnsiConsole console, + IHttpService httpService, + ISettingsService settingsService, + ScraperConfig? config = null) : BaseScraperStrategy(console, httpService, settingsService, config) + { + [GeneratedRegex(@"data-playlist='([^']+)'", RegexOptions.Singleline)] + private static partial Regex DataPlaylistRegex(); + + public override bool CanHandle(string bookUrl) + { + return bookUrl.Contains("hdaudiobooks.net", StringComparison.OrdinalIgnoreCase); + } + + public override async Task DownloadBookAsync(string bookUrl) + { + var metadata = await FetchMetadataAsync(bookUrl); + if (metadata == null || metadata.ChapterUrls.Count == 0) + { + ShowErrorMessage("No valid tracks found"); + return; + } + + SetFFmpegPath(); + + var folderPath = PrepareOutputFolder(metadata.Title); + + _console.MarkupLine($"[green]Found {metadata.ChapterUrls.Count} tracks[/]"); + _console.MarkupLine($"[blue]Parallel downloads:[/] {_config.MaxParallelDownloads}"); + _console.MarkupLine($"[blue]Parallel conversions:[/] {_config.MaxParallelConversions}"); + + await ProcessDirectFilesInParallelAsync(metadata, folderPath); + + ShowCompletionMessage(folderPath); + } + + private async Task FetchMetadataAsync(string bookUrl) + { + SimpleAudiobookMetadata? metadata = null; + + await _console.Status() + .SpinnerStyle(Style.Parse("blue bold")) + .StartAsync("Preparing download...", async ctx => + { + ctx.Status("Fetching page..."); + metadata = await GetChapterUrlsAsync(bookUrl); + }); + + return metadata; + } + + private async Task GetChapterUrlsAsync(string bookUrl) + { + try + { + var response = await _httpService.GetAsync(bookUrl); + response.EnsureSuccessStatusCode(); + + var html = await response.Content.ReadAsStringAsync(); + + var match = DataPlaylistRegex().Match(html); + if (!match.Success) + return null; + + var json = System.Net.WebUtility.HtmlDecode(match.Groups[1].Value); + var playlist = JArray.Parse(json); + + var chapterUrls = playlist + .Select(t => t["src"]?.ToString()) + .Where(src => !string.IsNullOrEmpty(src)) + .Select(src => src!) + .ToList(); + + if (chapterUrls.Count == 0) + return null; + + return new SimpleAudiobookMetadata + { + Title = ExtractTitleFromH1(html), + ChapterUrls = chapterUrls + }; + } + catch (Exception ex) + { + _console.MarkupLine($"[red]Error fetching chapter URLs: {ex.Message}[/]"); + return null; + } + } + } +} diff --git a/TokyBay/Scraper/Strategies/TokybookStrategy.cs b/TokyBay/Scraper/Strategies/TokybookStrategy.cs index d663163..71c1206 100644 --- a/TokyBay/Scraper/Strategies/TokybookStrategy.cs +++ b/TokyBay/Scraper/Strategies/TokybookStrategy.cs @@ -11,10 +11,10 @@ namespace TokyBay.Scraper.Strategies { public class TokybookStrategy( IAnsiConsole console, - IHttpService httpUtil, + IHttpService httpService, IIpifyService ipifyService, ISettingsService settingsService, - ScraperConfig? config = null) : BaseScraperStrategy(console, httpUtil, settingsService, config) + ScraperConfig? config = null) : BaseScraperStrategy(console, httpService, settingsService, config) { private const string TokybookBaseUrl = "https://tokybook.com"; private const string PostDetailsApiPath = "/api/v1/search/post-details"; @@ -374,7 +374,7 @@ private async Task ConvertSegmentedTrackAsync(SegmentedTrackData track) }; var content = new StringContent(payload.ToString(), Encoding.UTF8, "application/json"); - var response = await _httpUtil.PostAsync(TokybookBaseUrl + PostDetailsApiPath, content); + var response = await _httpService.PostAsync(TokybookBaseUrl + PostDetailsApiPath, content); return response.IsSuccessStatusCode ? JObject.Parse(await response.Content.ReadAsStringAsync()) : null; } @@ -397,7 +397,7 @@ private async Task ConvertSegmentedTrackAsync(SegmentedTrackData track) }; var content = new StringContent(payload.ToString(), Encoding.UTF8, "application/json"); - var response = await _httpUtil.PostAsync(TokybookBaseUrl + PlaylistApiPath, content); + var response = await _httpService.PostAsync(TokybookBaseUrl + PlaylistApiPath, content); return response.IsSuccessStatusCode ? JObject.Parse(await response.Content.ReadAsStringAsync()) : null; } @@ -417,7 +417,7 @@ private async Task GetTokybookTracksAsync(string url, strin { "x-track-src", trackSrc } }; - return await _httpUtil.GetAsync(url, headers); + return await _httpService.GetAsync(url, headers); } private static string ExtractDynamicSlugId(string url) diff --git a/TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs b/TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs index cf27bb4..0d27189 100644 --- a/TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs +++ b/TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs @@ -8,9 +8,9 @@ namespace TokyBay.Scraper.Strategies { public class ZAudiobooksStrategy( IAnsiConsole console, - IHttpService httpUtil, + IHttpService httpService, ISettingsService settingsService, - ScraperConfig? config = null) : BaseScraperStrategy(console, httpUtil, settingsService, config) + ScraperConfig? config = null) : BaseScraperStrategy(console, httpService, settingsService, config) { private const string BaseUrl = "https://files01.freeaudiobooks.top/audio/"; @@ -64,7 +64,7 @@ await _console.Status() { try { - var response = await _httpUtil.GetAsync(bookUrl); + var response = await _httpService.GetAsync(bookUrl); response.EnsureSuccessStatusCode(); var html = await response.Content.ReadAsStringAsync(); diff --git a/TokyBay/ScraperServiceExtensions.cs b/TokyBay/ScraperServiceExtensions.cs index 6a101c2..96d6186 100644 --- a/TokyBay/ScraperServiceExtensions.cs +++ b/TokyBay/ScraperServiceExtensions.cs @@ -20,6 +20,7 @@ public static IServiceCollection AddScraperServices( services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); diff --git a/TokyBay/Services/DownloadService.cs b/TokyBay/Services/DownloadService.cs index ae02435..192e53a 100644 --- a/TokyBay/Services/DownloadService.cs +++ b/TokyBay/Services/DownloadService.cs @@ -32,6 +32,13 @@ public IEnumerable GetSupportedDomains() { "tokybook.com", "goldenaudiobook.net", + "fulllengthaudiobooks.net", + "bigaudiobooks.net", + "findaudiobook.com", + "bookaudiobook.net", + "hotaudiobooks.com", + "hdaudiobooks.net", + "audiozaic.com", "freeaudiobooks.top", "zaudiobooks.com" }; diff --git a/TokyBay/Services/IpifyService.cs b/TokyBay/Services/IpifyService.cs index 47ddd48..360229a 100644 --- a/TokyBay/Services/IpifyService.cs +++ b/TokyBay/Services/IpifyService.cs @@ -3,18 +3,18 @@ namespace TokyBay.Services { - public class IpifyService(IHttpService httpUtil, IAnsiConsole console) : IIpifyService + public class IpifyService(IHttpService httpService, IAnsiConsole console) : IIpifyService { private const string IpifyUrl = "https://api.ipify.org?format=json"; - private readonly IHttpService _httpUtil = httpUtil; + private readonly IHttpService _httpService = httpService; private readonly IAnsiConsole _console = console; public async Task GetUserIdentityAsync() { try { - var response = await _httpUtil.GetAsync(IpifyUrl); + var response = await _httpService.GetAsync(IpifyUrl); var json = await response.Content.ReadAsStringAsync(); var data = JObject.Parse(json); diff --git a/TokyBay/Services/SettingsService.cs b/TokyBay/Services/SettingsService.cs index d574e4a..9427a75 100644 --- a/TokyBay/Services/SettingsService.cs +++ b/TokyBay/Services/SettingsService.cs @@ -10,7 +10,7 @@ namespace TokyBay.Services public class SettingsService(IAnsiConsole console) : ISettingsService { private readonly IAnsiConsole _console = console; - private UserSettings _userSettings = new UserSettings(); + private readonly UserSettings _userSettings = new UserSettings(); public UserSettings GetSettings() => _userSettings; diff --git a/TokyBay/TokyBay.csproj b/TokyBay/TokyBay.csproj index f90b727..bfd705e 100644 --- a/TokyBay/TokyBay.csproj +++ b/TokyBay/TokyBay.csproj @@ -5,9 +5,9 @@ net9.0 enable enable - 0.5.3 - 0.5.3 - 0.5.3 + 0.6.0 + 0.6.0 + 0.6.0