Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .github/assets/TokyBay.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,7 @@ AppPackages/

#####
# End of core ignore list, below put you custom 'per project' settings (patterns or path)
#####
#####

# Claude Code
.claude/settings.local.json
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<source type="audio/mpeg">` 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
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,28 @@ 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

1. **Search**: Search and find audiobooks by title
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
Expand Down
4 changes: 2 additions & 2 deletions TokyBay/Pages/MainPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions TokyBay/Pages/SearchTokybookPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace TokyBay.Pages
public class SearchTokybookPage(
IAnsiConsole console,
IPageService pageService,
IHttpService httpUtil,
IHttpService httpService,
IIpifyService ipifyService,
DownloadService downloadService)
{
Expand All @@ -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;

Expand Down Expand Up @@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion TokyBay/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MainPage>();
Expand Down
6 changes: 3 additions & 3 deletions TokyBay/Scraper/Base/BaseScraperStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -137,7 +137,7 @@ protected async Task<string> 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}[/]");
Expand Down
2 changes: 1 addition & 1 deletion TokyBay/Scraper/Configuration/ScraperConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
14 changes: 10 additions & 4 deletions TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <source> tags with type="audio/mpeg" in either attribute order
[GeneratedRegex(@"<source\b[^>]*type=""audio/mpeg""[^>]*>", RegexOptions.IgnoreCase)]
Expand All @@ -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)
Expand Down Expand Up @@ -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();
Expand Down
100 changes: 100 additions & 0 deletions TokyBay/Scraper/Strategies/PlaylistAudiobookStrategy.cs
Original file line number Diff line number Diff line change
@@ -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<SimpleAudiobookMetadata?> 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<SimpleAudiobookMetadata?> 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;
}
}
}
}
10 changes: 5 additions & 5 deletions TokyBay/Scraper/Strategies/TokybookStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -417,7 +417,7 @@ private async Task<HttpResponseMessage> 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)
Expand Down
6 changes: 3 additions & 3 deletions TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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/";

Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions TokyBay/ScraperServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static IServiceCollection AddScraperServices(
services.AddTransient<IScraperStrategy, TokybookStrategy>();
services.AddTransient<IScraperStrategy, ZAudiobooksStrategy>();
services.AddTransient<IScraperStrategy, GoldenAudiobookStrategy>();
services.AddTransient<IScraperStrategy, PlaylistAudiobookStrategy>();

services.AddSingleton<ScraperFactory>();
services.AddSingleton<DownloadService>();
Expand Down
7 changes: 7 additions & 0 deletions TokyBay/Services/DownloadService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ public IEnumerable<string> GetSupportedDomains()
{
"tokybook.com",
"goldenaudiobook.net",
"fulllengthaudiobooks.net",
"bigaudiobooks.net",
"findaudiobook.com",
"bookaudiobook.net",
"hotaudiobooks.com",
"hdaudiobooks.net",
"audiozaic.com",
"freeaudiobooks.top",
"zaudiobooks.com"
};
Expand Down
6 changes: 3 additions & 3 deletions TokyBay/Services/IpifyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JObject> 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);

Expand Down
2 changes: 1 addition & 1 deletion TokyBay/Services/SettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading