From 085f63b1fa770214f9ad3abe5236e1200d40089a Mon Sep 17 00:00:00 2001 From: Dennis Stratmann Date: Tue, 7 Apr 2026 10:48:09 +0100 Subject: [PATCH] v0.5.2: add release pipeline --- .github/workflows/release.yml | 74 +++++++++++++++++++ CLAUDE.md | 71 ++++++++++++++++++ TokyBay/EscapeCancellableConsole.cs | 1 + TokyBay/Scraper/Base/BaseScraperStrategy.cs | 3 +- .../Strategies/GoldenAudiobookStrategy.cs | 2 +- .../Scraper/Strategies/TokybookStrategy.cs | 2 +- .../Scraper/Strategies/ZAudiobooksStrategy.cs | 5 +- TokyBay/TokyBay.csproj | 18 ++--- 8 files changed, 161 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 CLAUDE.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..afd552e --- /dev/null +++ b/.github/workflows/release.yml @@ -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 '(?<=)[^<]+' 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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8b20d2e --- /dev/null +++ b/CLAUDE.md @@ -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` + `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(); + ``` + +## 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()` aus `BaseScraperStrategy` verwenden (exponentielles Delay) +- Temp-Verzeichnisse immer via `SafeDeleteDirectory()` aufräumen diff --git a/TokyBay/EscapeCancellableConsole.cs b/TokyBay/EscapeCancellableConsole.cs index 66d2839..6e70296 100644 --- a/TokyBay/EscapeCancellableConsole.cs +++ b/TokyBay/EscapeCancellableConsole.cs @@ -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 writer) => console.WriteAnsi(writer); public Task PromptAsync(IPrompt prompt, CancellationToken cancellationToken = default) => AnsiConsoleExtensions.PromptAsync(this, prompt, GetMergedCancellationToken(cancellationToken)); diff --git a/TokyBay/Scraper/Base/BaseScraperStrategy.cs b/TokyBay/Scraper/Base/BaseScraperStrategy.cs index 7c92ab5..526de01 100644 --- a/TokyBay/Scraper/Base/BaseScraperStrategy.cs +++ b/TokyBay/Scraper/Base/BaseScraperStrategy.cs @@ -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); } diff --git a/TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs b/TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs index 818a86b..34aa11c 100644 --- a/TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs +++ b/TokyBay/Scraper/Strategies/GoldenAudiobookStrategy.cs @@ -44,7 +44,7 @@ public override async Task DownloadBookAsync(string bookUrl) await ProcessDirectFilesInParallelAsync(metadata, folderPath); - ShowCompletionMessage(); + ShowCompletionMessage(folderPath); } private async Task FetchMetadataAsync(string bookUrl) diff --git a/TokyBay/Scraper/Strategies/TokybookStrategy.cs b/TokyBay/Scraper/Strategies/TokybookStrategy.cs index 5636da9..d663163 100644 --- a/TokyBay/Scraper/Strategies/TokybookStrategy.cs +++ b/TokyBay/Scraper/Strategies/TokybookStrategy.cs @@ -47,7 +47,7 @@ public override async Task DownloadBookAsync(string bookUrl) await ProcessTracksInParallelAsync(metadata, folderPath); - ShowCompletionMessage(); + ShowCompletionMessage(folderPath); } private async Task FetchMetadataAsync(string bookUrl) diff --git a/TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs b/TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs index 43be210..cf27bb4 100644 --- a/TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs +++ b/TokyBay/Scraper/Strategies/ZAudiobooksStrategy.cs @@ -1,5 +1,4 @@ using Spectre.Console; -using System.Text.RegularExpressions; using TokyBay.Models; using TokyBay.Scraper.Base; using TokyBay.Scraper.Configuration; @@ -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); } @@ -43,7 +42,7 @@ public override async Task DownloadBookAsync(string bookUrl) await ProcessDirectFilesInParallelAsync(metadata, folderPath); - ShowCompletionMessage(); + ShowCompletionMessage(folderPath); } private async Task FetchMetadataAsync(string bookUrl) diff --git a/TokyBay/TokyBay.csproj b/TokyBay/TokyBay.csproj index 1d27a1c..f6627b6 100644 --- a/TokyBay/TokyBay.csproj +++ b/TokyBay/TokyBay.csproj @@ -5,19 +5,19 @@ net9.0 enable enable - 0.5.1 - 0.5.1 - 0.5.1 + 0.5.2 + 0.5.2 + 0.5.2 - - - - - + + + + + - +