Skip to content

Getting Started

NihilDigit edited this page Sep 30, 2026 · 4 revisions

Getting Started

Installing

repositories { mavenCentral() }
dependencies {
    implementation("io.github.nihildigit:pikpak-kotlin:2.0.0")
    implementation("io.ktor:ktor-client-core:<your-ktor-version>")
    implementation("io.ktor:ktor-client-okhttp:<your-ktor-version>")   // or -darwin on iOS, or any engine
}

Ktor is compileOnly in the SDK so it never changes the Ktor version you pinned; you bring the core and one engine. The SDK is compiled against Ktor 3.1.1, the lowest supported, so its bytecode works against every later 3.x at runtime. It parses JSON itself, so ContentNegotiation is neither needed nor wanted (see Injected HTTP clients).

Targets: jvm, android (AAR, artifactId pikpak-kotlin-android), iosArm64, iosSimulatorArm64.

A client

val client = PikPakClient(account = "you@example.com", password = "...")

One client is one authenticated account. Construction does no I/O and is cheap, so several accounts are several clients; the SDK has no account pool on purpose. Call close() when done if the SDK built its own HTTP clients (the default).

The primary constructor takes a password supplier rather than a password. It is called only when a full sign-in is needed — never on a cached session or a refresh — so an app that keeps the password in a keychain, or asks the user, never holds it for the client's lifetime:

PikPakClient(account, passwordSupplier = { keychain.read(account) ?: askUser() }, sessionStore = store)

Sessions

A session is an access token, a refresh token, the user id and an expiry. It persists through a SessionStore:

  • FileSessionStore() — the default on the JVM and iOS. One JSON file per account under $XDG_CONFIG_HOME/pikpak-kotlin or ~/.config/pikpak-kotlin, named by the MD5 of the account so the file name does not reveal it. Written to a temporary file and moved into place, so a crash cannot leave half a session.
  • Android has no default. user.home is the process root there and not writable; a PikPakClient or FileSessionStore built with the defaults throws at construction, naming what to pass instead: FileSessionStore(dir = Path(context.filesDir.absolutePath, "pikpak-kotlin")).
  • InMemorySessionStore() — process lifetime only.
  • Your own: three suspend functions, load, save, clear.

Losing the session is not harmless: the refresh token lives nowhere else, and without it the next start signs in with the password. A store that fails to save therefore fails the login, rather than leaving the session in memory only.

Session.toString() leaves both tokens out, so a session printed into a log or a crash report does not hand the account over.

Logging in

client.login() is optional: every API call logs in first when it has no usable session. Both go down the same ladder, cheapest first:

  1. the in-memory session, if it has not reached its expiry;
  2. the stored one — another process sharing the store may have rotated it;
  3. a refresh with each refresh token on hand;
  4. a password sign-in, once.

A refresh token the server calls dead (error_code 4126) is cleared from memory and the store before the sign-in, so a sign-in that then fails does not leave a dead token to be retried on every start. What reaches the caller is the sign-in's outcome; isRefreshTokenInvalid is never thrown from login().

The expiry is set a few minutes before the server's own when the token arrives, and a request made after it refreshes first, so no request goes out on a token about to lapse. A 401 anyway re-authenticates and retries once. See Errors and Retries for captcha handling.

logout() drops the session from memory and the store, and the captcha token with it.

Injected HTTP clients

By default the SDK builds two clients: one for the JSON API and one for the CDN and OSS, tuned per platform to allow as many connections per host as the account budget. You may pass your own:

PikPakClient(account, password, httpClient = myApiClient, cdnHttpClient = PikPakClient.tunedCdnClient())

A client you pass must not install ContentNegotiation (it adds an Accept: application/json the CDN answers with 406) or HttpRequestRetry (it compounds with the SDK's own retry). App-wide clients usually have both; give the SDK its own. An injected httpClient is reused for the CDN unless cdnHttpClient is passed too, which costs the per-host tuning — hence tunedCdnClient(). You own the lifecycle of clients you pass.

Tuning

Parameter Default Meaning
rateLimiter 5 per second, burst 5 API calls only; the CDN is not rate limited. Share one instance across clients to share a budget.
retryPolicy 3 attempts, 200 ms doubling to at most 5 s, no jitter See Errors and Retries.
connectionBudget 8 Connections one file reads over. The CDN refuses a ninth on one link.
accountConnectionBudget 16 Connections across every file of the account. See Measurements before raising it.
domain PikPakDomain.MYPIKPAK_COM Root domain of the API. Also a var; see Root domains.
steerEdgeHosts true Lets reads move off a CDN host measured to be far slower than a sibling. See Playback.

On a slow line, lower accountConnectionBudget to about the bandwidth divided by 0.8 MB/s: past what the line carries, more connections only make each one slower, and the slowest ones much slower.

Root domains

PikPak's API answers under four roots — mypikpak.com, mypikpak.net, pikpak.me, pikpakdrive.com — the same four its own web client knows. They reach the same service and accept the same tokens, and differ only in what a network can see: the name in DNS, the TLS SNI and the Host header, and for the API hosts the address DNS returns. A network that filters or shapes by name or address may treat one root worse than another, so a client may want to switch.

val probe = client.probeDomain(PikPakDomain.MYPIKPAK_NET)   // measures, does not switch
if (probe.usable) client.domain = PikPakDomain.MYPIKPAK_NET  // next API request goes there
  • Switching at runtime takes effect on the next API request. The session carries over. Links already minted keep their root and stay valid until they expire, so a read in progress is not disturbed; links minted afterwards come back under the new root on their own.
  • What moves: api-drive.* and user.*, so every API call, token refresh and captcha refresh. The captcha actions keep naming user.mypikpak.com: they are strings the server signs, not addresses. Upload endpoints and share links are used as the server returns them, and share-link parsing recognises mypikpak.com only.
  • probeDomain opens a fresh connection to api-drive.<root>, sends one request, sends it again on the same connection, then sends one to user.<root>. The requests carry no token — no rate-limit token, no captcha, no effect on the session — and the root counts as usable only if both hosts answer the way PikPak's gateway answers an unauthenticated call (HTTP 401, error_code 16). A captive portal or another service behind a valid wildcard certificate fails that. DomainProbe carries usable, firstRequest (lookup, TCP, TLS and one exchange), warmRequest (one exchange on the open connection) and failure. A TLS session the platform already holds shortens firstRequest for a root in use; compare warmRequest. With an injected httpClient the probe uses that client, whose pool may already hold a connection.
  • The SDK picks nothing. Nothing is cached and nothing runs in the background; choosing a root, and when, is the caller's.
  • Not verified under another root: password sign-in. Token refresh and captcha refresh were.

The SDK never asks PikPak's region check (access.<root>); see Measurements.

Clone this wiki locally