diff --git a/.github/assets/TokyBay.gif b/.github/assets/TokyBay.gif index 340d212..16d0018 100644 Binary files a/.github/assets/TokyBay.gif and b/.github/assets/TokyBay.gif differ 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