Metadata-Version: 2.4
Name: slike
Version: 0.2.2
Summary: Python SDK for publishing media on Slike platform
Project-URL: Homepage, https://code.sli.ke/python/slike-media-package
Project-URL: Documentation, https://code.sli.ke/python/slike-media-package#readme
Project-URL: Repository, https://code.sli.ke/python/slike-media-package
Project-URL: Issues, https://code.sli.ke/python/slike-media-package/issues
Author: Arpit Kumar
License-Expression: MIT
License-File: LICENSE
Keywords: api,media,publishing,sdk,slike
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.7
Requires-Dist: requests>=2.25.0
Requires-Dist: tomli>=1.2; python_version < '3.11'
Description-Content-Type: text/markdown

# slike

Python and TypeScript/JavaScript SDK for the [Slike](https://sli.ke) platform.

Supports **chunked file upload** (4 MiB chunks), **editor-video registration**, **publish-by-URL** (Google Drive, YouTube, direct links), **media status checks**, **metadata retrieval**, **agency listing**, **media update**, **upload cancellation** (local + server-side), **CMS user creation**, and **team-by-host lookup**. Two auth flows: **direct Slike token** or **Denmark JWT**.

- Python requires **3.7+**
- TypeScript/JS requires **Node 18+** (or modern browser with Fetch API)

---

## Install

**Python**
```bash
pip3 install slike
```

**JavaScript / TypeScript**
```bash
npm install slike
```

---

## Python SDK

### Quick start

```python
from slike import SlikeMedia

client = SlikeMedia(token="your-slike-token", environment="prod")
client.initialize()   # validates token; raises SlikeAPIError on failure

# Upload a local file (blocking)
result = client.register_media(
    file_path="/path/to/video.mp4",
    title="My Video",
    description="Description",
    on_progress=lambda done, total: print(f"{done}/{total} chunks"),
)
print("Media ID:", result["id"])
```

### Denmark JWT auth

```python
client = SlikeMedia(
    denmark_token="<jwt>",
    service="slike",        # optional, default: "slike"
    environment="prod",
)
client.initialize()         # exchanges JWT for a Slike session token
print(client.token)         # resolved Slike session token
```

Provide exactly one of `token` or `denmark_token`.

---

### Methods

#### `initialize()`

Validates credentials. Must be called once before any other method.

```python
client.initialize()
```

---

#### `register_media`

Register media on Slike. Provide exactly one of `file_path` (chunked upload) or `editor_settings` (editor-generated video — no bytes uploaded).

**Blocking file upload** (waits for all chunks to finish):

```python
result = client.register_media(
    file_path="/path/to/video.mp4",
    title="My Video",
    description="Description",
    asset_type="video",                          # optional, default: "video"
    tags=["news", "sports"],                     # optional
    preset_config={"meta": "", "social": ""},    # optional
    team="team-id",                              # optional, team ID from get_required_metadata
    business="biz-id",                           # optional, business ID paired with team
    agency="agency-id",                          # optional, agency ID from get_agency_list
    on_progress=lambda done, total: print(f"{done}/{total} chunks"),
)
print("Media ID:", result["id"])
```

Pass `team` and its paired `business` together — both come from the same team object returned by `get_required_metadata`:

```python
meta = client.get_required_metadata()
selected_team = meta["default_team"]["team"]   # or whichever team the user picks

result = client.register_media(
    file_path="/path/to/video.mp4",
    title="My Video",
    description="Description",
    team=selected_team["value"],
    business=selected_team["business"],
)
```

**Non-blocking file upload** (upload runs in a background thread; callback fires the moment chunks finish):

```python
def on_done(res, err):
    if err:
        print("Upload failed:", err)
    else:
        print("Upload complete! ID:", res["id"])

result = client.register_media(
    file_path="/path/to/video.mp4",
    title="My Video",
    description="Description",
    on_upload_complete=on_done,   # fires immediately when chunks finish
)
# result["id"] is available right away — no need to wait
print("Registered, ID:", result["id"])
```

**Editor video** (no file — server renders from spec):

```python
client.register_media(
    title="My Editor Video",
    description="Description",
    tags=["news", "sports"],
    editor_settings={
        "source_width": 1080,
        "source_height": 1920,
        "clips": [...],
        "background_music": {...},
    },
)
```

---

#### `get_status`

Get basic media details and processing readiness by media ID.

```python
status = client.get_status("media_id_here")

print(status["media_id"])  # media ID
print(status["title"])     # media title
print(status["desc"])      # media description
print(status["tags"])      # list of tags
print(status["status"])    # raw status code
print(status["is_ready"])  # True when status==5 and state==10
print(status["msg"])       # "Media is ready." or "Media is not ready yet."
```

Media is ready when `is_ready` is `True` (`status == 5` and `state == 10`).

---

#### `cancel`

Cancel **all** in-progress background uploads on this client (local abort only).

```python
import threading

result = client.register_media(
    file_path="/path/to/video.mp4",
    title="My Video",
    description="Description",
    on_upload_complete=lambda res, err: print("cancelled" if isinstance(err, CancelError) else err or res["id"]),
)

# Cancel from a UI button handler or another thread:
threading.Timer(5.0, client.cancel).start()
```

`cancel()` is a no-op if nothing is in progress. All background uploads on the client are stopped; retry backoff sleeps are woken immediately.

---

#### `get_required_metadata`

Fetch all dropdown options for building a media-update form. Returns static constants at the top level, per-team filtered languages and categories, and a flat agency list.

```python
meta = client.get_required_metadata()

# Static constants (from SDK)
print(meta["content_types"])    # [{"value": "package", "label": "Package"}, ...]
print(meta["production_types"]) # [{"value": "In House Originals", "label": "In House Originals"}, ...]
print(meta["content_ratings"])  # [{"value": "1", "label": "Made for Kids"}, ...]
print(meta["media_states"])     # [{"value": 0, "label": "Draft"}, ...]

# Agencies (flat list sorted by recent activity)
for agency in meta["agencies"]:
    print(agency["id"], agency["name"])

# Default team (pre-select on load); None if no teams exist
print(meta["default_team"]["team"])       # {"value": "team-id-1", "label": "Team 1", "is_default": True, "business": "biz-id"}
print(meta["default_team"]["languages"])  # [{"value": "Language 1", "label": "Language 1"}, ...]
print(meta["default_team"]["categories"]) # [{"value": "Category 1", "label": "Category 1", "subcategories": [...]}, ...]

# All teams (populate team switcher)
for entry in meta["teams"]:
    print(entry["team"])        # {"value": ..., "label": ..., "is_default": ..., "business": ...}
    print(entry["languages"])   # languages allowed for this team
    print(entry["categories"])  # categories allowed for this team
```

---

#### `get_agency_list`

List active agencies, optionally filtered by name. Returns up to 50 results sorted by recent activity.

```python
# All active agencies
result = client.get_agency_list()
print(result["count"])   # total count
for a in result["list"]:
    print(a["id"], a["name"])

# Search by name
result = client.get_agency_list(name="toi")
```

| Parameter | Type | Description |
|---|---|---|
| `name` | `str` | Optional name filter |

---

#### `update_media`

Update metadata fields on an existing media item. All fields except `id` are optional — only provided fields are sent.

```python
result = client.update_media(
    id="media-id-here",
    title="Updated Title",
    desc="Updated description",
    tags=["news", "sports"],
    lang="English",
    category="News",
    subcategory="Politics",
    ctype="story",
    production_type="In House Originals",
    mfk=2,                  # content rating: 1=Kids, 2=General, 3=18+
    media_state=1,           # 0=Draft, 1=Active
    expiry="2026-12-31",
    youtube_id="dQw4w9WgXcQ",
    parentid="parent-id",
)
print(result)  # dict with updated media fields
```

| Parameter | Type | Description |
|---|---|---|
| `id` | `str` | **Required.** Media ID to update |
| `title` | `str` | **Required.** Media title |
| `title_url` | `str` | URL-friendly title slug |
| `desc` | `str` | Description |
| `tags` | `list[str]` | Tags |
| `agency_credits` | `list[str]` | Agency credits |
| `ctype` | `str` | Content type (see `CONTENT_TYPES`) |
| `production_type` | `str` | Production type (see `PRODUCTION_TYPES`) |
| `lang` | `str` | Language name |
| `category` | `str` | Category name |
| `subcategory` | `str` | Sub-category name |
| `mfk` | `int` | Content rating: `1`=Kids, `2`=General, `3`=18+ |
| `images` | `dict` | Image metadata |
| `expiry` | `str` | Expiry date string |
| `youtube_id` | `str` | YouTube video ID |
| `parentid` | `str` | Parent media ID |
| `media_state` | `int` | `0`=Draft, `1`=Active |

---

#### `cancel_upload`

Cancel a specific upload and **notify the Slike server** to mark the media as cancelled (calls `media.update` with cancel status values). Use this when the user explicitly cancels from the UI.

```python
result = client.register_media(
    file_path="/path/to/video.mp4",
    title="My Video",
    description="Description",
    on_upload_complete=lambda res, err: print(err or "done"),
)

media_id = result["id"]

# When user clicks Cancel in the UI:
client.cancel_upload(media_id)
```

This performs two actions atomically:
1. Stops all local chunk upload threads
2. Calls `media.update` on the Slike server to mark the media as cancelled

---

#### `publish_media`

Publish media by URL (Google Drive, YouTube, direct link).

```python
client.publish_media(
    url="https://drive.google.com/file/d/FILE_ID/view",
    title="My Video",
    description="Description",
    type="gdrive",              # gdrive | youtube | url
    asset_type="video",         # optional
    tags=["news", "sports"],    # optional
    auto_publish=True,          # optional, default: True
)
```

---

#### `create_user`

Create a CMS user. Send only `name`, `email` and `team` (a list of team ids);
the backend fills in all remaining user defaults. Calls the `user.createcms` RPC
on the **accounts endpoint** (`https://accounts.sli.ke/rpc`, dev
`https://accounts-dev.sli.ke/rpc`).

```python
result = client.create_user(
    name="Test User",
    email="test.user@example.com",
    team=["nd78ug9zoo", "ndbxuuzzko"],
)
```

Returns a concise confirmation:

```python
{
    "id": "ws41sgoouo",
    "name": "Test User",
    "email": "test.user@example.com",
    "msg": "User 'Test User' created successfully with id ws41sgoouo.",
}
```

Equivalent raw request (same call the SDK makes):

```bash
curl -X POST https://accounts.sli.ke/rpc \
  -H "Content-Type: application/json" \
  --cookie "token=<YOUR_SLIKE_TOKEN>" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "user.createcms",
    "params": {
      "name": "Test User",
      "email": "test.user@example.com",
      "team": ["nd78ug9zoo", "ndbxuuzzko"]
    }
  }'
```

Dev uses `https://accounts-dev.sli.ke/rpc` with cookie `token-dev=<...>`. The token
may also be sent as a `token` header (`-H "token: <...>"`) — the JS SDK uses the
header form. The raw reply is `{"result": {"id": "...", ...}}`.

---

#### `get_teams_by_host`

List the teams attached to a host. Calls the `team.byhost` RPC on the same
**accounts endpoint**. Multiple teams can share a host, so the result is always a list.

```python
teams = client.get_teams_by_host(hostid="153")
for t in teams:
    print(t["id"], t["name"], t["hostid"])
```

| Parameter | Type | Description |
|---|---|---|
| `hostid` | `str` | Host id to look up (required) |

Returns a list of `{ "id", "name", "hostid" }`:

```python
[
    {"id": "nd78ug9zoo", "name": "Team A", "hostid": "153"},
    {"id": "ndbxuuzzko", "name": "Team B", "hostid": "153"},
]
```

---

### Error handling

```python
from slike import SlikeMedia, SlikeAPIError, CancelError

try:
    client = SlikeMedia(token="...", environment="prod")
    client.initialize()
    result = client.register_media(file_path="video.mp4", title="T", description="D")
except ValueError as e:
    print(f"Bad input: {e}")
except CancelError:
    print("Upload was cancelled")
except SlikeAPIError as e:
    print(f"API error: {e} (code: {e.code})")
    if e.conflicts:
        for c in e.conflicts:
            print(f"  conflict: field={c['field']} id={c['id']} value={c['value']}")
```

#### Duplicate file (unique constraint)

When a file with the same `fid` (derived from filename + size) is already registered on the server, `register_media` raises `SlikeAPIError` with `conflicts` populated:

```python
except SlikeAPIError as e:
    if e.conflicts:
        # File already exists — e.conflicts[0]["id"] is the existing media ID
        existing_id = e.conflicts[0]["id"]
        print(f"Already uploaded as media ID: {existing_id}")
    else:
        print(f"API error: {e} (code: {e.code})")
```

`SlikeAPIError` fields:

| Field | Type | Description |
|---|---|---|
| `message` | `str` | Human-readable error message |
| `code` | `int \| None` | HTTP or RPC error code (e.g. `400`) |
| `conflicts` | `list[dict] \| None` | Conflict entries when a unique constraint is violated |

Each conflict entry has `field`, `id`, and `value` keys.

---

## TypeScript / JavaScript SDK

### Quick start

```typescript
import { SlikeMedia } from 'slike';

const client = new SlikeMedia({ token: 'your-slike-token', environment: 'prod' });
await client.initialize();

// Upload a file (blocking — waits for all chunks)
const result = await client.uploadMedia({
  file,           // File object
  title: 'My Video',
  description: 'Description',
  onProgress: (done, total) => console.log(`${done}/${total} chunks`),
});
console.log('Media ID:', result.id);
```

### Denmark JWT auth

```typescript
const client = new SlikeMedia({
  denmarkToken: '<jwt>',
  service: 'slike',       // optional
  environment: 'prod',
});
await client.initialize();
console.log(client.token);
```

---

### Methods

#### `initialize()`

```typescript
await client.initialize();
```

#### `uploadMedia(opts)` — blocking upload

Registers and uploads all chunks. Returns when complete.

```typescript
const result = await client.uploadMedia({
  file,
  title: 'My Video',
  description: 'Description',
  tags: ['news', 'sports'],
  team: 'team-id',        // optional — from getRequiredMetadata
  business: 'biz-id',     // optional — paired with team
  agency: 'agency-id',    // optional — from getAgencyList
  onProgress: (done, total) => console.log(`${done}/${total}`),
  signal: abortController.signal,   // optional AbortSignal
});
```

Pass `team` and `business` together — both come from the same team object:

```typescript
const meta = await client.getRequiredMetadata();
const selectedTeam = meta.defaultTeam?.team;  // or whichever team the user picks

const result = await client.uploadMedia({
  file,
  title: 'My Video',
  description: 'Description',
  team: selectedTeam?.value,
  business: selectedTeam?.business,
});
```

#### `uploadMediaBackground(opts)` — non-blocking upload

Returns an `UploadHandle` immediately. `onUploadComplete` fires the moment chunks finish — caller does not need to wait.

```typescript
const handle = await client.uploadMediaBackground({
  file,
  title: 'My Video',
  description: 'Description',
  onProgress: (done, total) => console.log(`${done}/${total}`),
  onUploadComplete: (result, error) => {
    if (error) console.error('Upload failed:', error);
    else console.log('Upload complete! ID:', result.id);
  },
});

console.log('Media ID (available now):', handle.mediaId);

// Cancel from UI — stops chunks locally AND marks media cancelled on Slike server:
cancelButton.onclick = () => handle.cancel();

// Or await completion:
const result = await handle.completion;
```

`UploadHandle`:

| Property | Type | Description |
|---|---|---|
| `mediaId` | `string` | Media ID (available immediately) |
| `cancel()` | `() => void` | Abort chunks + notify Slike server |
| `completion` | `Promise<RegisterMediaResult>` | Resolves when upload finishes |

#### `getStatus(mediaId, signal?)`

Get basic media details and processing readiness.

```typescript
const status = await client.getStatus('media_id_here');

console.log(status.mediaId);  // media ID
console.log(status.title);    // media title
console.log(status.desc);     // media description
console.log(status.tags);     // string[] | undefined
console.log(status.status);   // raw status code
console.log(status.isReady);  // true when status===5 and state===10
console.log(status.msg);      // "Media is ready." or "Media is not ready yet."
```

#### `getRequiredMetadata(signal?)`

Fetch all dropdown options for building a media-update form. Returns static constants at the top level, per-team filtered languages and categories, and a flat agency list.

```typescript
const meta = await client.getRequiredMetadata();

// Static constants (from SDK)
console.log(meta.contentTypes);    // [{ value: 'package', label: 'Package' }, ...]
console.log(meta.productionTypes); // [{ value: 'In House Originals', label: 'In House Originals' }, ...]
console.log(meta.contentRatings);  // [{ value: '1', label: 'Made for Kids' }, ...]
console.log(meta.mediaStates);     // [{ value: 0, label: 'Draft' }, ...]

// Agencies (flat list sorted by recent activity)
console.log(meta.agencies);        // [{ id: 'agency-id', name: 'Agency Name' }, ...]

// Default team (pre-select on load)
console.log(meta.defaultTeam.team);       // { value: 'team-id-1', label: 'Team 1', is_default: true, business: 'biz-id' }
console.log(meta.defaultTeam.languages);  // [{ value: 'language-1', label: 'Language 1' }, ...]
console.log(meta.defaultTeam.categories); // [{ value: 'category-1', label: 'Category 1', subcategories: [...] }, ...]

// All teams (populate team switcher)
meta.teams.forEach(({ team, languages, categories }) => {
  console.log(team);       // { value, label, is_default, business }
  console.log(languages);  // languages allowed for this team
  console.log(categories); // categories allowed for this team
});
```

You can also import the static constants directly:

```typescript
import { CONTENT_TYPES, PRODUCTION_TYPES, CONTENT_RATINGS, MEDIA_STATES } from 'slike';
```

**Example response:**

```json
{
  "contentTypes":    [{ "value": "content-type-1", "label": "Content Type 1" }],
  "productionTypes": [{ "value": "production-type-1", "label": "Production Type 1" }],
  "contentRatings":  [{ "value": "1", "label": "Rating 1" }],
  "mediaStates":     [{ "value": 0, "label": "State 1" }],
  "agencies":        [{ "id": "agency-id-1", "name": "Agency Name" }],
  "defaultTeam": {
    "team":       { "value": "team-id-1", "label": "Team 1", "is_default": true, "business": "biz-id-1" },
    "languages":  [{ "value": "language-1", "label": "Language 1" }],
    "categories": [{ "value": "category-1", "label": "Category 1", "subcategories": [{ "value": "sub-category-1", "label": "Sub Category 1" }] }]
  },
  "teams": [
    {
      "team":       { "value": "team-id-1", "label": "Team 1", "is_default": true, "business": "biz-id-1" },
      "languages":  [{ "value": "language-1", "label": "Language 1" }],
      "categories": [{ "value": "category-1", "label": "Category 1", "subcategories": [] }]
    },
    {
      "team":       { "value": "team-id-2", "label": "Team 2", "is_default": false, "business": "biz-id-1" },
      "languages":  [{ "value": "language-1", "label": "Language 1" }],
      "categories": [{ "value": "category-1", "label": "Category 1", "subcategories": [{ "value": "sub-category-1", "label": "Sub Category 1" }] }]
    }
  ]
}
```

#### `getAgencyList(opts?, signal?)`

List active agencies, optionally filtered by name. Returns up to 50 results sorted by recent activity.

```typescript
// All active agencies
const result = await client.getAgencyList();
console.log(result.count);         // total count
console.log(result.list);          // [{ id: 'agency-id', name: 'Agency Name' }, ...]

// Search by name
const byName = await client.getAgencyList({ name: 'toi' });
```

`AgencySearchOptions`:

| Field | Type | Description |
|---|---|---|
| `name` | `string` | Optional name filter |

`AgencyListResult`: `{ list: AgencyOption[]; count: number }`
`AgencyOption`: `{ id: string; name: string }`

#### `updateMedia(opts, signal?)`

Update metadata fields on an existing media item. All fields except `id` are optional.

```typescript
const result = await client.updateMedia({
  id: 'media-id-here',
  title: 'Updated Title',
  desc: 'Updated description',
  tags: ['news', 'sports'],
  lang: 'English',
  category: 'News',
  subcategory: 'Politics',
  ctype: 'story',
  productionType: 'In House Originals',
  mfk: 2,               // content rating: 1=Kids, 2=General, 3=18+
  media_state: 1,        // 0=Draft, 1=Active
  expiry: '2026-12-31',
  youtube_id: 'dQw4w9WgXcQ',
  parentid: 'parent-id',
});
```

`UpdateMediaOptions`:

| Field | Type | Description |
|---|---|---|
| `id` | `string` | **Required.** Media ID to update |
| `title` | `string` | **Required.** Media title |
| `title_url` | `string` | URL-friendly title slug |
| `desc` | `string` | Description |
| `tags` | `string[]` | Tags |
| `agency_credits` | `string[]` | Agency credits |
| `ctype` | `string` | Content type (see `CONTENT_TYPES`) |
| `productionType` | `string` | Production type (see `PRODUCTION_TYPES`) |
| `lang` | `string` | Language name |
| `category` | `string` | Category name |
| `subcategory` | `string` | Sub-category name |
| `mfk` | `number` | Content rating: `1`=Kids, `2`=General, `3`=18+ |
| `images` | `object` | Image metadata |
| `expiry` | `string` | Expiry date string |
| `youtube_id` | `string` | YouTube video ID |
| `parentid` | `string` | Parent media ID |
| `media_state` | `0 \| 1` | `0`=Draft, `1`=Active |

#### `cancelUpload(mediaId, signal?)`

Mark a specific media as cancelled on the Slike server (calls `media.update` with cancel status values). Use when the user explicitly cancels from the UI and you need the server to reflect that.

```typescript
await client.cancelUpload('media_id_here');
```

> Note: `UploadHandle.cancel()` already calls this automatically. Use `cancelUpload()` directly only when you have a `mediaId` without a handle (e.g. cancelling a previously registered upload on page reload).

#### `publishMedia(opts)`

```typescript
await client.publishMedia({
  url: 'https://drive.google.com/file/d/FILE_ID/view',
  title: 'My Video',
  description: 'Description',
  type: 'gdrive',           // 'gdrive' | 'youtube' | 'url'
  tags: ['news', 'sports'],
  autoPublish: true,
});
```

---

#### `createUser(opts, signal?)`

Create a CMS user. Provide only `name`, `email` and `team` (an array of team ids);
the backend fills in all remaining user defaults. Calls the `user.createcms` RPC
on the **accounts endpoint** (`https://accounts.sli.ke/rpc`, dev
`https://accounts-dev.sli.ke/rpc`).

```typescript
const result = await client.createUser({
  name: 'Test User',
  email: 'test.user@example.com',
  team: ['nd78ug9zoo', 'ndbxuuzzko'],
});
```

Returns a concise confirmation:

```typescript
{
  id: 'ws41sgoouo',
  name: 'Test User',
  email: 'test.user@example.com',
  msg: "User 'Test User' created successfully with id ws41sgoouo.",
}
```

---

#### `getTeamsByHost(hostid, signal?)`

List the teams attached to a host. Calls the `team.byhost` RPC on the same
**accounts endpoint**. Multiple teams can share a host, so the result is always an array.

```typescript
const teams = await client.getTeamsByHost('153');
// [{ id: 'nd78ug9zoo', name: 'Team A', hostid: '153' }, ...]
```

| Parameter | Type | Description |
|---|---|---|
| `hostid` | `string` | Host id to look up (required) |
| `signal` | `AbortSignal` | Optional abort signal |

Returns `HostTeam[]` — a list of `{ id, name, hostid }`.

---

### Error handling (TypeScript)

```typescript
import { SlikeMedia, SlikeAPIError, CancelError } from 'slike';

try {
  await client.uploadMedia({ file, title: 'T', description: 'D' });
} catch (err) {
  if (err instanceof CancelError) {
    console.log('Upload cancelled');
  } else if (err instanceof SlikeAPIError) {
    console.error('API error:', err.message, 'code:', err.code);
    if (err.conflicts) {
      for (const c of err.conflicts) {
        console.log(`conflict: field=${c.field} id=${c.id} value=${c.value}`);
      }
    }
  }
}
```

#### Duplicate file (unique constraint)

When a file with the same `fid` is already registered, `registerMedia` throws `SlikeAPIError` with the message _"This file has already been uploaded…"_ and `conflicts` populated:

```typescript
import { SlikeAPIError, type ConflictEntry } from 'slike';

try {
  await client.registerMedia({ file, title: 'T', description: 'D' });
} catch (err) {
  if (err instanceof SlikeAPIError && err.conflicts) {
    // File already exists — err.conflicts[0].id is the existing media ID
    const existing = err.conflicts[0];
    console.log(`Already uploaded as media ID: ${existing.id}`);
  }
}
```

`SlikeAPIError` fields:

| Field | Type | Description |
|---|---|---|
| `message` | `string` | Human-readable error message |
| `code` | `number \| undefined` | HTTP or RPC error code (e.g. `400`) |
| `conflicts` | `ConflictEntry[] \| undefined` | Conflict entries when a unique constraint is violated |

`ConflictEntry` fields: `field`, `id`, `value`.

---

## Use Cases

### 1. Upload a file and check status after

```python
# Python
client.initialize()
result = client.register_media(file_path="video.mp4", title="T", description="D")

status = client.get_status(result["id"])
print(status["msg"])   # "Media is not ready yet." (processing takes time)
```

```typescript
// TypeScript
await client.initialize();
const result = await client.uploadMedia({ file, title: 'T', description: 'D' });

const status = await client.getStatus(result.id);
console.log(status.msg);   // "Media is not ready yet."
```

---

### 2. Background upload with completion callback + cancel from UI

```python
# Python
result = client.register_media(
    file_path="video.mp4",
    title="My Video",
    description="Description",
    on_upload_complete=lambda res, err: (
        print("failed:", err) if err else print("ready to process, ID:", res["id"])
    ),
)

# Store media_id for potential cancel
media_id = result["id"]

# When user clicks Cancel:
client.cancel_upload(media_id)
```

```typescript
// TypeScript
const handle = await client.uploadMediaBackground({
  file,
  title: 'My Video',
  description: 'Description',
  onUploadComplete: (result, error) => {
    if (error) console.error('failed:', error);
    else console.log('chunks done, media processing started:', result.id);
  },
});

// Wire to UI cancel button — aborts chunks AND marks cancelled on server:
cancelBtn.onclick = () => handle.cancel();
```

---

### 3. Check if media is ready before serving

```python
# Python
status = client.get_status(media_id)
if status["is_ready"]:
    print("Serve URL from your CDN using media_id:", status["media_id"])
else:
    print(status["msg"])
```

```typescript
// TypeScript
const status = await client.getStatus(mediaId);
if (status.isReady) {
  serveMedia(status.mediaId);
} else {
  showMessage(status.msg);
}
```

---

### 4. Cancel a previously registered upload (e.g. on page reload)

```typescript
// If you stored the mediaId in localStorage before the page unloaded:
const mediaId = localStorage.getItem('pendingMediaId');
if (mediaId) {
  await client.cancelUpload(mediaId);
  localStorage.removeItem('pendingMediaId');
}
```

---

## Configuration (CLI / Python only)

### Priority (highest wins)

1. CLI flags (`--token`, `--denmark-token`, `--env`, `--config`)
2. Environment variables (`SLIKE_TOKEN`, `SLIKE_ENVIRONMENT`, `SLIKE_CONFIG`)
3. `./config.toml` → `~/.slike/config.toml`

### Config file

```toml
token       = "your-token-here"
environment = "prod"              # "dev" or "prod"
```

### Environment variables

| Variable            | Description                    |
|---------------------|-------------------------------|
| `SLIKE_TOKEN`       | Slike auth token              |
| `SLIKE_ENVIRONMENT` | `dev` or `prod`               |
| `SLIKE_CONFIG`      | Path to a specific config file |

---

## CLI

```bash
# Upload a local file
slike-upload video.mp4 --title "My Video" --desc "Description"

# Publish by URL (Google Drive)
slike-upload --url https://drive.google.com/file/d/FILE_ID/view \
             --type gdrive --title "My Video" --desc "Description"

# Denmark JWT auth
slike-upload video.mp4 --title "T" --desc "D" --denmark-token <jwt>
```

| Flag              | Description                                              |
|-------------------|----------------------------------------------------------|
| `--url`           | Media URL to ingest (publish mode)                       |
| `--type`          | `gdrive`, `youtube`, or `url` (default: `url`)           |
| `--title`         | Media title                                              |
| `--desc`          | Media description                                        |
| `--asset-type`    | e.g. `video`, `shorts` (default: `video`)                |
| `--tags`          | Comma-separated tags                                     |
| `--no-publish`    | Save as draft, skip auto-publish                         |
| `--token`         | Slike auth token                                         |
| `--denmark-token` | Denmark JWT (exchanged for a Slike session token)        |
| `--service`       | Service name for Denmark auth (default: `slike`)         |
| `--env`           | `dev` or `prod`                                          |
| `--config`        | Path to config file                                      |

---

## Environments

| Environment | RPC endpoint                 | Upload endpoint                   | Auth endpoint                       |
|-------------|------------------------------|-----------------------------------|-------------------------------------|
| `prod`      | `https://b2b.sli.ke/rpc`     | `https://asset.sli.ke/upload`     | `https://accounts.sli.ke/login`     |
| `dev`       | `https://app-dev.sli.ke/rpc` | `https://asset-dev.sli.ke/upload` | `https://accounts-dev.sli.ke/login` |

---

## How it works

**Auth (`initialize()`):**
- `token`: calls `user.me` RPC to verify validity.
- `denmark_token`: POSTs `{type: 4, jwt, service}` to `/login`, reads `data.token`.

**Chunked file upload:**
1. `media.register` RPC → returns `media_id` + short-lived upload JWT
2. File split into **4 MiB chunks**, each POSTed as `multipart/form-data`
3. Failed chunks retry up to **3 times** with exponential backoff (interruptible by cancel)

**Editor video:** `media.register` with `type: "videoeditor"` + editor spec. No bytes uploaded.

**Status check (`get_status` / `getStatus`):** calls `media.get` RPC, returns shaped object with `is_ready` (`status==5 AND state==10`) and a human-readable `msg`.

**Cancel upload (`cancel_upload` / `cancelUpload`):** stops local chunk threads and calls `media.update` RPC with `status=-2, state=-2, media_status=-1` to mark the media cancelled on the server.

**Required metadata (`get_required_metadata` / `getRequiredMetadata`):** fires `batch.lists` (keys: `["language", "category", "team"]`) and `agency.list` in parallel. Filters languages and categories to `status === 1`, groups them per team using each team's `meta.language` and `meta.category` ID allowlists, and surfaces `business` (business ID) on each team. Returns `agencies` (flat `[{id, name}]` list), `defaultTeam` (team with `is_default: true`), all `teams` with their filtered dropdowns, and static SDK constants at the top level.

**Agency list (`get_agency_list` / `getAgencyList`):** calls `agency.list` RPC with a standard descending sort by activity. Adds a `must` filter only when `name` or `status` is provided.

**Media update (`update_media` / `updateMedia`):** calls `media.update` RPC with only the fields you provide — omitted fields are not sent.

---

## Development

```bash
git clone <repo>
cd slike-media-package

# Python
python3 -m pip install -e .
python3 -m unittest test_slike -v

# TypeScript
cd js
npm install
npx tsc --noEmit    # type-check
npm run build       # compile to dist/
```

---

## License

MIT
