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
74 changes: 74 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Release on PR Merge

on:
pull_request:
types: [closed]
branches: [master]

jobs:
release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- name: Extract version from csproj
id: version
run: |
VERSION=$(grep -oP '(?<=<Version>)[^<]+' TokyBay/TokyBay.csproj)
echo "value=$VERSION" >> $GITHUB_OUTPUT

- name: Publish win-x64
run: |
dotnet publish TokyBay/TokyBay.csproj \
-c Release -r win-x64 --self-contained \
-p:PublishSingleFile=true \
-p:PublishTrimmed=false \
-p:DebugType=none \
-o publish/win-x64

- name: Publish linux-x64
run: |
dotnet publish TokyBay/TokyBay.csproj \
-c Release -r linux-x64 --self-contained \
-p:PublishSingleFile=true \
-p:PublishTrimmed=false \
-p:DebugType=none \
-o publish/linux-x64

- name: Publish linux-arm64
run: |
dotnet publish TokyBay/TokyBay.csproj \
-c Release -r linux-arm64 --self-contained \
-p:PublishSingleFile=true \
-p:PublishTrimmed=false \
-p:DebugType=none \
-o publish/linux-arm64

- name: Zip artifacts
run: |
cd publish
zip -j tokybay-win-x64.zip win-x64/*
zip -j tokybay-linux-x64.zip linux-x64/*
zip -j tokybay-linux-arm64.zip linux-arm64/*

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.version.outputs.value }}
name: v${{ steps.version.outputs.value }}
body: ${{ github.event.pull_request.body }}
make_latest: true
files: |
publish/tokybay-win-x64.zip
publish/tokybay-linux-x64.zip
publish/tokybay-linux-arm64.zip
71 changes: 71 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# TokyBay

C# .NET 9 Console App zum Scrapen und Konvertieren von Audiobüchern (M4B/MP3) von mehreren Websites.

## Stack

- **.NET 9**, C# mit Nullable-Referenztypen und Primary Constructors
- **Spectre.Console** — alle Konsolenausgaben (Markup mit `[green]...[/]` etc., Status-Spinner)
- **Xabe.FFmpeg** — Audio-Konvertierung und Segment-Merging; Binaries via `Xabe.FFmpeg.Downloader`
- **Newtonsoft.Json** — JSON-Parsing der API-Antworten
- **Microsoft.Extensions.DependencyInjection** — DI-Container

## Architektur

### Strategy Pattern (Scraper)

```
IScraperStrategy (Scraper/Abstractions/)
└── BaseScraperStrategy (Scraper/Base/)
├── TokybookStrategy (Scraper/Strategies/) — tokybook.com
├── ZAudiobooksStrategy (Scraper/Strategies/) — zaudiobooks / freeaudiobooks.top
└── GoldenAudiobookStrategy (Scraper/Strategies/) — goldenaudiobook.net
```

- `ScraperFactory` wählt per `CanHandle(url)` die passende Strategie aus
- `ScraperConfig` steuert Parallelismus-Parameter (Defaults: 3 parallele Downloads, 2 Konvertierungen, 5 Segmente/Track)

### Download-Pipeline

Downloads und Konvertierungen laufen entkoppelt über `Channel<T>` + `SemaphoreSlim`:
1. Download-Tasks schreiben fertige Tracks in einen bounded Channel
2. Konvertierungs-Tasks lesen aus dem Channel und rufen FFmpeg auf
3. Channel wird nach `Task.WhenAll(downloadTasks)` geschlossen

### Track-Typen

- `SegmentedTrackData` — für HLS-Streams (`.m3u8` → `.ts`-Segmente → merge via FFmpeg concat)
- `DirectFileTrackData` — für direkte MP3/Audio-Downloads (zaudiobooks, goldenaudiobook); Konvertierung wird übersprungen wenn Quelldatei bereits im Zielformat vorliegt

## Neue Website hinzufügen

1. Neue Klasse in `TokyBay/Scraper/Strategies/` anlegen, die von `BaseScraperStrategy` erbt
2. `CanHandle(string url)` implementieren — URL-basierte Erkennung
3. `DownloadBookAsync(string url)` implementieren — Metadata fetch, dann `ProcessTracksInParallelAsync`
4. In `ScraperServiceExtensions.cs` registrieren:
```csharp
services.AddTransient<IScraperStrategy, NeueStrategy>();
```

## Build & Run

```sh
dotnet build
dotnet run --project TokyBay -- -d "C:\Pfad\zum\Download"
```

### Publish (Cross-Platform)

```sh
dotnet publish -c Release -r win-x64 --self-contained
dotnet publish -c Release -r linux-x64 --self-contained
dotnet publish -c Release -r linux-arm64 --self-contained
```

## Konventionen

- Konsolenausgaben immer via `_console.MarkupLine(...)` (Spectre), nie `Console.WriteLine`
- Fehlermeldungen in `[red]`, Erfolg in `[green]`, Info in `[blue]`, Konvertierungen in `[cyan]`
- Dateinamen werden via `SanitizeName()` bereinigt (`[^A-Za-z0-9]+` → `_`)
- Retry-Logik: `RetryAsync<T>()` aus `BaseScraperStrategy` verwenden (exponentielles Delay)
- Temp-Verzeichnisse immer via `SafeDeleteDirectory()` aufräumen
1 change: 1 addition & 0 deletions TokyBay/EscapeCancellableConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public void ResetCancellationToken()

public void Clear(bool home) => console.Clear(home);
public void Write(IRenderable renderable) => console.Write(renderable);
public void WriteAnsi(Action<AnsiWriter> writer) => console.WriteAnsi(writer);

public Task<T> PromptAsync<T>(IPrompt<T> prompt, CancellationToken cancellationToken = default)
=> AnsiConsoleExtensions.PromptAsync(this, prompt, GetMergedCancellationToken(cancellationToken));
Expand Down
3 changes: 2 additions & 1 deletion TokyBay/Scraper/Base/BaseScraperStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,10 @@ protected string PrepareOutputFolder(string bookTitle)
return folderPath;
}

protected void ShowCompletionMessage()
protected void ShowCompletionMessage(string folderPath)
{
_console.MarkupLine("[green]Download finished[/]");
_console.MarkupLine($"[grey]Audiobook saved in:[/] {folderPath}");
_console.MarkupLine("Press any key to continue");
_console.Input.ReadKey(true);
}
Expand Down
2 changes: 1 addition & 1 deletion TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public override async Task DownloadBookAsync(string bookUrl)

await ProcessDirectFilesInParallelAsync(metadata, folderPath);

ShowCompletionMessage();
ShowCompletionMessage(folderPath);
}

private async Task<SimpleAudiobookMetadata?> FetchMetadataAsync(string bookUrl)
Expand Down
2 changes: 1 addition & 1 deletion TokyBay/Scraper/Strategies/TokybookStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public override async Task DownloadBookAsync(string bookUrl)

await ProcessTracksInParallelAsync(metadata, folderPath);

ShowCompletionMessage();
ShowCompletionMessage(folderPath);
}

private async Task<StreamingAudiobookMetadata?> FetchMetadataAsync(string bookUrl)
Expand Down
5 changes: 2 additions & 3 deletions TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Spectre.Console;
using System.Text.RegularExpressions;
using TokyBay.Models;
using TokyBay.Scraper.Base;
using TokyBay.Scraper.Configuration;
Expand All @@ -17,7 +16,7 @@ public class ZAudiobooksStrategy(

public override bool CanHandle(string bookUrl)
{
return bookUrl.Contains("freeaudiobooks.top", StringComparison.OrdinalIgnoreCase) ||
return bookUrl.Contains("freeaudiobooks", StringComparison.OrdinalIgnoreCase) ||
bookUrl.Contains("zaudiobooks", StringComparison.OrdinalIgnoreCase);
}

Expand All @@ -43,7 +42,7 @@ public override async Task DownloadBookAsync(string bookUrl)

await ProcessDirectFilesInParallelAsync(metadata, folderPath);

ShowCompletionMessage();
ShowCompletionMessage(folderPath);
}

private async Task<SimpleAudiobookMetadata?> FetchMetadataAsync(string bookUrl)
Expand Down
18 changes: 9 additions & 9 deletions TokyBay/TokyBay.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.5.1</Version>
<AssemblyVersion>0.5.1</AssemblyVersion>
<FileVersion>0.5.1</FileVersion>
<Version>0.5.2</Version>
<AssemblyVersion>0.5.2</AssemblyVersion>
<FileVersion>0.5.2</FileVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Spectre.Console" Version="0.54.0" />
<PackageReference Include="Spectre.Console" Version="0.55.0" />
<PackageReference Include="Xabe.FFmpeg" Version="6.0.2" />
<PackageReference Include="Xabe.FFmpeg.Downloader" Version="6.0.2" />
</ItemGroup>
Expand Down