Skip to content

Architecture

NihilDigit edited this page Sep 30, 2026 · 3 revisions

Architecture

Principles

  • Atomic endpoints. Every PikPak operation is one suspend extension function on PikPakClient. No hidden orchestration: account pools, sync engines, polling loops, cleanup heuristics and CLIs belong to the caller. The test for a new feature is whether a caller could not reasonably build it on the atomic calls.
  • Hard problems inside. What every caller would otherwise get wrong the same way — session persistence and refresh, captcha re-authentication, retry, rate limiting, content hashing, OSS signing, connection budgets, link expiry — happens inside the client.
  • Measured, not guessed. Budgets, deadlines and block sizes come from Measurements; a number without one is a bug in the documentation.
  • Multiplatform. commonMain depends only on multiplatform libraries: Ktor (compileOnly), kotlinx.coroutines, kotlinx.serialization, kotlinx-io, kotlinx-datetime, KotlinCrypto. Platform code is limited to the CDN client's engine tuning, the default session directory, and telling a failure before sending from one after.

Layers

PikPakClient ─────────────── account state: session, captcha token, rate limiter, root domain,
   │                          account gate, foreground count, host health and speeds, folder memo
   │
   ├── endpoints (extension functions, one file per domain)
   │       └── HttpEngine.request  ── JSON API: rate limit, headers, login on demand,
   │                                  captcha once, 401 once, retry
   │       └── HttpEngine.sendRaw  ── CDN / OSS: no PikPak headers, not rate limited,
   │                                  401/403 → UrlExpiredException, live body
   │
   └── playback
           PikPakStreamReader → PikPakFileCache → PikPakFileHandle → RangeReader → sendRaw
  • PikPakClient holds configuration and the state every file on the account shares. Its own API is small: login, logout, prewarm, currentSession/sessionFlow, domain, clearFolderIdCache, close; probeDomain is an extension beside PikPakDomain.
  • HttpEngine (internal) runs both pipelines over one retry loop. The JSON pipeline moves the official API hosts under domain in one place, so endpoints keep naming api-drive.mypikpak.com and user.mypikpak.com; the CDN pipeline never rewrites a link's root.
  • HostHealth (internal) is the account's view of CDN edge hosts: which went silent, how fast each delivers, which sibling a link may be sent to. RangeReader asks it for every attempt's host and reports every attempt back. AuthApi (internal) owns the session ladder and the captcha signing.
  • Endpoints are grouped by API domain: Endpoints.kt (quota, listing, filters), FolderEndpoints.kt, SearchEndpoint.kt, StarEndpoint.kt, EventEndpoint.kt, ShareEndpoint.kt, ArchiveEndpoint.kt, UserEndpoint.kt, TransferQuotaEndpoint.kt, UploadEndpoint.kt, CidEndpoint.kt, MagnetResolveEndpoint.kt, UrlOfflineEndpoint.kt, OfflinePruneEndpoint.kt, Tasks.kt, VariantEndpoint.kt, RangeStreamEndpoint.kt, RangeDownloadEndpoint.kt.
  • Playback is described in Playback.

Invariants

Break one of these and something fails only under concurrency, only on one platform, or only in production.

  • Snapshot before the request, not after the failure. A request captures the session and captcha token it sends, builds its headers from those snapshots, and hands them to the refresh on failure; the refresh does nothing if the current value already differs. That collapses N concurrent failures into one handshake. A snapshot taken inside the refresh, or headers re-read from live state, break it — the first bit only on a two-core CI runner.
  • Never retry once the response block has started, and never replay a POST whose effect is unknown. See Errors and Retries.
  • Releases run under NonCancellable. A cancelled coroutine that must wait for a mutex to give a gate slot back would throw first and leak the slot for good; a gate that has lost them all hangs every later acquire.
  • The file's gate belongs to the handle, not to the reader. Reads on a retired reader keep running; a replacement with its own gate lets one link carry twice its cap.
  • Gates are taken per file, then per account. That bounds one file to its budget of account slots.
  • A variant is chosen once and locked by mediaId. resolveVariant is the only place that falls back to the original.
  • The gcid is the identity of content; a file id is a cache of it.
  • A reroute never looks like a changed link. Every refresh a reader performs is keyed on the link, not on the host an attempt went to; a sibling's failure sets the sibling aside and never reaches the link's refresh, which would otherwise mint for nothing or, handed the same link back, fail the read.
  • The foreground count is re-synchronised until it agrees with the demand. Several threads change it without a common lock, and a single check-then-set left the account's count wrong.

Shape decisions

  • Priorities are integers on one scale shared by every file on the account, with named points (INDEX, BLOCKING, READ_AHEAD, WARM, BACKGROUND_*). Free integers let a caller invert the order; the named points are the supported way to place a read.
  • The cache tuning (block size, read-ahead, memory cap, wide-block threshold) is not public. The four are coupled by one check; four free parameters would hand callers an IllegalArgumentException for a relation nobody told them about.
  • close() is not suspending anywhere a player could need it: releasing input happens in lifecycle callbacks with no coroutine.
  • The SDK writes nothing to disk on its own, the session store aside. Persistence of bytes is a BlockStore the caller supplies and decides for.

Design review, 2026-09-27

A full read of the SDK (every source file, four areas in parallel) produced 78 findings. The bugs, dead APIs and wrong documentation were fixed in 1.0.0 — see History for what was removed. What remains are shapes that work but are known to be wrong, in order of how much they cost:

  1. downloadTo is a second scheduler. Settled in 2.0.0, differently from what this item proposed. It suggested a sequential background cursor on the cache with an in-order sink; what shipped is a download as a warm whose blocks must reach a DurableBlockStore. An in-order sink cannot take the blocks a player fetches out of order — the head, then the index at the tail, then wherever it seeks — so it would either drop them or hold them in memory, and the file would still be fetched twice. A store keyed by block takes them as they come. downloadTo stays for a caller who wants the plain file whose length is its progress.
  2. Two overlapping file models. FileStat (listings) and FileDetail (getFile) repeat about ten fields and neither is a superset: a detail cannot say whether it is starred, a listing has no links. Callers refetch to cross over. One model with optional links and variants, or a shared interface, would end that.
  3. Pagination is written per endpoint. Each listing loop is its own do { page } while (token), parameter order differs between listFilesPaged and listMyShares, and nothing guards a token that repeats — which matters because at least one filter (system_tag) is known to make the server ignore the page size. One internal pager would fix all of them.
  4. PikPakFileHandle carries three jobs — the gcid identity and rebuild ladder, link minting with host avoidance, and ownership of the block cache. Split in 2.0.0, when downloads would have grown it again: the cache is PikPakFileCache, reading through the handle as any RangeSource.
  5. One captcha token for every action. captcha/init is asked per action, but the client keeps one token and reuses it across actions. It has always been accepted; if the server ever binds tokens to actions, the token and its refresh have to be keyed by action.

Two more from the review were breaking and went into 1.0.0 rather than wait for 2.0: the task record is DriveTask (it was OfflineTask, a name from the first task type the SDK read), and every gcid the SDK hands out is upper case (local hashing used to produce lower case, so a stored hash compared with == against a listing read as different content).

Consumers that shaped these choices: Piko (player behind a loopback proxy, random-clip feed, downloads, resumable uploads, magnets and offline packs, archives) and Animeko's PikPak engine.

Clone this wiki locally