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
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ on:
push:
branches: ["master"]
tags: ["v*"]

pull_request:
branches: ["master"]

jobs:
build-and-test:
name: Build & Test
name: Build & Unit Tests
runs-on: ubuntu-latest

steps:
Expand All @@ -29,7 +30,7 @@ jobs:
- name: Build
run: dotnet build TiktokExplode.NET.slnx --no-restore --configuration Release

- name: Run tests
- name: Run Unit Tests
run: dotnet test TiktokExplode.Tests/TiktokExplode.Tests.csproj --no-build --configuration Release --logger "console;verbosity=normal"

publish:
Expand Down
145 changes: 112 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The library follows **Clean Architecture**: the domain layer (`TiktokExplode`) h
- Fetch full video metadata: author, stats, duration, language, location, bitrates, and more
- Download videos without watermark or with watermark
- Download the static cover image (JPEG) or the animated cover (WebP)
- **Search TikTok by keyword** — streams results lazily via `IAsyncEnumerable<Media>` (videos and carousels)
- **Carousel/slideshow support** — `Carousel` entity with full image collection metadata
- Progress reporting during download via `IProgress<double>`
- Automatic WAF/bot-detection retry with configurable backoff
- **Strategy pattern** — choose between Playwright (reliable) or HTTP-only (lightweight) page fetching
Expand Down Expand Up @@ -115,31 +117,82 @@ await using var client = new TiktokClient(myFetcher, new TikTokOptions());

#### Methods

| Method | Returns | Description |
| ---------------------------------------------------- | ------------ | --------------------------------- |
| `GetVideoAsync(string url, CancellationToken)` | `Video` | Fetches full video metadata |
| `DownloadAsync(Video, CancellationToken)` | `StreamInfo` | Downloads video without watermark |
| `DownloadWatermarkedAsync(Video, CancellationToken)` | `StreamInfo` | Downloads video with watermark |
| Method | Returns | Description |
| ------------------------------------------------------------ | ------------ | --------------------------------------- |
| `GetVideoAsync(string url, CancellationToken)` | `Video` | Fetches full video metadata |
| `DownloadAsync(Video, CancellationToken)` | `StreamInfo` | Downloads video without watermark |
| `DownloadWatermarkedAsync(Video, CancellationToken)` | `StreamInfo` | Downloads video with watermark |
| `DownloadImageAsync(CarouselImage, CancellationToken)` | `StreamInfo` | Downloads a carousel image stream |

`TiktokClient` implements `IAsyncDisposable` — always use `await using`.

#### Extension methods (via `TiktokClientExtensions`)
---

### `TiktokSearchClient`

| Method | Description |
| --- | --- |
| `DownloadAsync(video, filePath, progress?, ct)` | Downloads video without watermark to a file, with optional progress |
| `DownloadWatermarkedAsync(video, filePath, progress?, ct)` | Downloads video with watermark to a file, with optional progress |
| `DownloadImageAsync(video, filePath, ct)` | Downloads the static cover image (JPEG) |
| `DownloadAnimatedImageAsync(video, filePath, ct)` | Downloads the animated cover (WebP) |
`TiktokSearchClient` searches TikTok by keyword and streams results as `Media` objects (either `Video` or `Carousel`) using `IAsyncEnumerable<Media>`. It **requires Playwright** because TikTok's search API signs requests with dynamic tokens that can only be generated by a real browser session.

```csharp
// Download to file path with optional progress
await client.DownloadAsync(video, "output.mp4", progress, cancellationToken);
await client.DownloadWatermarkedAsync(video, "output_wm.mp4", progress, cancellationToken);
await using var client = TiktokSearchClient.CreateWithBrowser();

await foreach (var media in client.SearchAsync("funny cats"))
{
Console.WriteLine($"{media.Author.UniqueId}: {media.Description}");

if (media is Video video)
{
// Standard video post
await client.DownloadAsync(video, $"{video.Id}.mp4");
}
else if (media is Carousel carousel)
{
// Slideshow post — contains a collection of images
Console.WriteLine($"Carousel with {carousel.Post.Images.Count} images");
}
}
```

> **Note:** `TiktokSearchClient` implements `ISearchClient`, which extends `IDownloadClient`. This means all extension methods (`DownloadAsync(filePath)`, `DownloadWatermarkedAsync(filePath)`, `DownloadImageAsync`, `DownloadAnimatedImageAsync`, `DownloadCarouselImagesAsync`) are available directly on the search client — no need for a separate `TiktokClient` instance.

> **Search scope:** The current implementation returns results from the **first page** of TikTok's search API (~10–20 results).

#### Methods

| Method | Returns | Description |
| -------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------ |
| `SearchAsync(string keyword, CancellationToken)` | `IAsyncEnumerable<Media>` | Streams media results (videos and carousels) for the keyword |
| `DownloadAsync(Video, CancellationToken)` | `StreamInfo` | Downloads video without watermark |
| `DownloadWatermarkedAsync(Video, CancellationToken)` | `StreamInfo` | Downloads video with watermark |
| `DownloadImageAsync(CarouselImage, CancellationToken)` | `StreamInfo` | Downloads a carousel image stream |

`TiktokSearchClient` implements `IAsyncDisposable` — always use `await using`.

#### Extension methods (via `DownloadClientExtensions`)

| Method | Description |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `DownloadAsync(video, filePath, progress?, ct)` | Downloads video without watermark to a file, with optional progress |
| `DownloadWatermarkedAsync(video, filePath, progress?, ct)` | Downloads video with watermark to a file, with optional progress |
| `DownloadImageAsync(image, filePath, progress?, ct)` | Downloads a single carousel image to a file, with optional progress |
| `DownloadCarouselImagesAsync(carousel, directoryPath, progress?, ct)` | Downloads all carousel images to a directory with overall progress |
| `DownloadImageAsync(video, filePath, ct)` | Downloads the static cover image of a video (JPEG) |
| `DownloadAnimatedImageAsync(video, filePath, ct)` | Downloads the animated cover of a video (WebP) |

```csharp
// Download video to file with optional progress
IProgress<double> progress = new Progress<double>(p => Console.Write($"\rProgress: {p:P0}"));
await client.DownloadAsync(video, "output.mp4", progress);
await client.DownloadWatermarkedAsync(video, "output_wm.mp4", progress);

// Download cover images
await client.DownloadImageAsync(video, "cover.jpg");
await client.DownloadAnimatedImageAsync(video, "cover.webp");

// Download a single carousel image
await client.DownloadImageAsync(carousel.Post.Images[0], "image_1.jpg", progress);

// Download all carousel images to a directory (named image_1.jpg, image_2.jpg, ...)
await client.DownloadCarouselImagesAsync(carousel, "my_carousel_folder", progress);
```

> **Note:** Animated covers are served by TikTok as animated WebP files. Not all videos have an animated cover — if `Cover.AnimatedUrl` is empty, the video only has a static cover.
Expand Down Expand Up @@ -183,21 +236,37 @@ Returned by `DownloadAsync` and `DownloadWatermarkedAsync`. Implements `IAsyncDi

---

### `Video` model
### `Media` model (base)

All search results are `Media` objects. Use pattern matching (`is Video`, `is Carousel`) to access type-specific properties.

| Property | Type | Description |
| ------------- | ---------------- | -------------------------------------------------- |
| `Id` | `string` | TikTok video ID |
| `Id` | `string` | TikTok post ID |
| `Description` | `string` | Caption / description |
| `Author` | `Author` | Author entity |
| `Duration` | `VideoDuration` | Duration in seconds and precise seconds |
| `Stats` | `VideoStats` | Views, likes, comments, shares, favorites, reposts |
| `Info` | `VideoInfo` | Technical info, bitrates, and download URLs |
| `Language` | `VideoLanguage` | Detected content language |
| `Stats` | `MediaStats` | Views, likes, comments, shares, favorites, reposts |
| `Language` | `MediaLanguage` | Detected content language |
| `Location` | `string` | Location tag (if any) |
| `Cover` | `VideoCover` | Static (JPEG) and animated (WebP) cover image URLs |
| `Cover` | `MediaCover` | Static (JPEG) and animated (WebP) cover image URLs |
| `Music` | `MediaMusic` | Track metadata and artwork |
| `CreatedAt` | `DateTimeOffset` | Upload date |

### `Video : Media` model

| Property | Type | Description |
| ---------- | --------------- | ------------------------------------------- |
| `Info` | `VideoInfo` | Technical info, bitrates, and download URLs |
| `Duration` | `VideoDuration` | Duration in seconds and precise seconds |

### `Carousel : Media` model

Represents a TikTok slideshow post (multiple images + audio).

| Property | Type | Description |
| -------- | -------------- | ------------------------------------ |
| `Post` | `CarouselPost` | Collection of images with their URLs |

### `Author` model

| Property | Type | Description |
Expand Down Expand Up @@ -227,19 +296,24 @@ services.AddTiktokExplode(b => b
.UsePlaywrightFetcher(o => o.Headless = false));

// HTTP fetcher — lighter, no browser dependency
// Note: ISearchClient is NOT available with HTTP fetcher (see below)
services.AddTiktokExplode(b => b
.UseHttpFetcher(o => o.WarmupDelay = TimeSpan.Zero)
.ConfigureTiktok(o => o.MaxWafRetries = 5));
```

Registered services:

| Service | Implementation | Lifetime |
| --- | --- | --- |
| `IVideoClient` | `TiktokClient` | Singleton |
| `IPageFetcher` | `PlaywrightFetcher` or `HttpFetcher` | Singleton |
| `TikTokOptions` | — | Singleton |
| `PlaywrightFetcherOptions` or `HttpFetcherOptions` | — | Singleton |
| Service | Implementation | Lifetime | Fetcher |
| -------------------------------------------------- | ------------------------------------ | --------- | ------------------- |
| `IVideoClient` | `TiktokClient` | Singleton | Both |
| `ISearchClient` | `TiktokSearchClient` | Singleton | **Playwright only** |
| `IPageFetcher` | `PlaywrightFetcher` or `HttpFetcher` | Singleton | Both |
| `ISearchFetcher` | `PlaywrightSearchFetcher` | Singleton | **Playwright only** |
| `TikTokOptions` | — | Singleton | Both |
| `PlaywrightFetcherOptions` or `HttpFetcherOptions` | — | Singleton | Both |

> **Important:** `ISearchClient` and `ISearchFetcher` are only registered when using the Playwright fetcher (the default). If you call `UseHttpFetcher()`, these services will **not** be present in the container. Attempting to inject `ISearchClient` with an HTTP-configured builder will throw a DI resolution error at runtime. This is by design — TikTok's search API requires a real browser to generate signed requests.

```csharp
// Consume in your services via constructor injection
Expand Down Expand Up @@ -289,18 +363,23 @@ catch (TiktokException ex)
```
TiktokExplode/ # Domain — zero external dependencies
Domain/
Entities/ # Video, Author
ValueObjects/ # VideoInfo, StreamInfo, VideoStats, VideoDuration, etc.
Abstractions/ # IVideoClient
Entities/ # Media (abstract), Video, Carousel, Author
ValueObjects/
Media/ # MediaStats, MediaLanguage, MediaCover, MediaMusic, MediaDuration
Videos/ # VideoInfo, VideoDuration, VideoDownloadLinks, Bitrate
Carousels/ # CarouselPost and related types
Authors/ # AuthorStats, ProfileImageVariants
Abstractions/ # IVideoClient, ISearchClient, IDownloadClient
Exceptions/ # TiktokException hierarchy
Utilities/ # URL validation

TiktokExplode.Infrastructure/ # HTTP + browser automation (Playwright + AngleSharp)
Clients/ # TiktokClient : IVideoClient
Clients/ # TiktokClient : IVideoClient, TiktokSearchClient : ISearchClient
Fetchers/ # IPageFetcher, PlaywrightFetcher, HttpFetcher
Fetchers/Search/ # ISearchFetcher, PlaywrightSearchFetcher
Http/ # TikTokDownloadClient — CDN download management
Browser/ # TikTokBrowser — internal Playwright wrapper
Parsers/ # TikTokVideoParser — JSON extraction from hydration script
Parsers/ # TikTokVideoParser, TikTokSearchParser — JSON extraction
Options/ # TikTokOptions, PlaywrightFetcherOptions, HttpFetcherOptions
Common/ # StreamExtensions, TiktokClientExtensions

Expand Down
2 changes: 1 addition & 1 deletion TiktokExplode.All/TiktokExplode.All.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<PropertyGroup>
<PackageId>TiktokExplode.All</PackageId>
<Version>1.1.1</Version>
<Version>1.2.0</Version>
<Authors>Ts-Pytham</Authors>
<Owners>Ts-Pytham</Owners>
<Description>Meta-package that installs TiktokExplode (domain layer) and TiktokExplode.Infrastructure (HTTP/browser fetchers, parser, download client) in a single command.</Description>
Expand Down
Loading
Loading