# Media CLI commands

> Canonical HTML: https://screenrig.ai/docs/cli/media

Upload, replace, version, tag, move and delete images, video and audio, with folders, dynamic selectors and availability rules. Every command here runs as described in the [CLI reference](https://screenrig.ai/docs/cli.md): global flags, JSON output, exit codes, revisions and retries.

## Media

A media object is one still, one clip, or one MP3 audio track, stored as exactly one rendition. There is no per-device fallback, so whatever `media upload` stores is what every screen has to decode.

Media folders organize the library. A folder ID starts `mfd_` and a file ID `med_`. An upload lands at the library root unless `--folder` names a folder, `media move` and `media folder update` rearrange the tree, and a playlist `by:folder` selector plays the files directly inside one folder, not those of its subfolders. An organization holds at most 1000 folders across its projects, nested at most 10 levels deep, with names of 1 to 100 characters.

A media item keeps its id when its file changes: `media replace` swaps the file in place, so every playlist and schedule that uses it plays the new one, and `media versions` lists every file the item has had.

### `media upload`

Convert a local file to delivery bytes, upload it, and wait until the object is ready.

```console
media upload <file> [--content-type TYPE] [--tag TAG] [--folder ID] [--no-wait] [--poll-ms MS]
             [--timeout MS] [--no-transcode] [--codec h264|hevc] [--max-fps N]
             [--max-edge PIXELS] [--webp-quality 1-100] [--no-progress]
             [--preset signage-1080p30|signage-4k30] [--no-audio]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<file>` | path | yes | none | The source still, clip, or audio track. The filename becomes the human handle in listings, playlists, and playback reporting, so name it for what it shows. |
| `--content-type TYPE` | string | no | the type of the file extension | Declare the source's media type for a file whose extension does not name it. A type that disagrees with the file's bytes is refused (`usage_error`, exit 2) before any conversion. |
| `--tag TAG` | string | no | none | One to 32 letters or digits, matching `^[A-Za-z0-9]{1,32}$`. Hyphens are rejected. A tag is what makes `media list --tag TAG` a reliable filename-to-id map. |
| `--folder ID` | string | no | library root | An existing `mfd_` media folder, checked before any work starts. The file lands there when it is ready, nothing moves afterwards, and `data.folder_id` reports it. It works with `--no-wait`. An uploader must name one of its folders; see [Uploaders](https://screenrig.ai/docs/authentication.md#uploaders). |
| `--no-wait` | boolean | no | off | Return as soon as the commit is accepted. The object is not ready yet; finish with `operations wait`. |
| `--poll-ms MS` | integer | no | 1000 | Interval between operation polls while waiting. |
| `--timeout MS` | integer | no | 120000 | Deadline for the wait. It also bounds each underlying request. |
| `--no-transcode` | boolean | no | off | Upload the source bytes unchanged. `ffmpeg`, `ffprobe`, and `cwebp` are not run. The file must already be a PNG, JPEG, WebP, GIF, MP4, WebM or MP3, and lossless WebP is still rejected. Cannot combine with `--codec`, `--max-fps`, `--max-edge`, `--webp-quality`, `--preset` or `--no-audio`. |
| `--codec` | string | no | `h264` | `h264` or `hevc`. Use `hevc` only when every screen that will play the clip is a native Player. |
| `--max-fps N` | number | no | 30 | Constant output frame rate. Every video is encoded at exactly this rate whatever its source rate, so consecutive clips never change rate. Greater than 0 and at most 240. |
| `--max-edge PIXELS` | integer | no | 3840 | Bound applied to each edge, from 16 to 3840. Aspect ratio is preserved and a smaller source is never upscaled. |
| `--webp-quality` | integer | no | 90 | Still quality for the WebP encode, from 1 to 100. |
| `--preset` | string | no | unset | `signage-1080p30` or `signage-4k30`. Orientation-aware video size and a constant 30 fps. `--max-edge` and `--max-fps` can tighten a preset but cannot enlarge it. Video only; cannot combine with `--no-transcode`. |
| `--no-audio` | boolean | no | off | Omit the audio track from video delivery. Video only; cannot combine with `--no-transcode`. |
| `--no-progress` | boolean | no | off | Emit no progress output. Progress always goes to stderr; stdout stays reserved for the one result envelope. |

```console
screenrig media upload ./lobby-welcome-loop.mp4 --tag LobbyLoop
```

Video becomes an MP4: H.264 High profile at level 4.2, 5.1 or 5.2, whichever is lowest for the size and rate, or H.265 Main profile at level 5.1 or 5.2 with `--codec hevc`, in `yuv420p` tagged Rec. 709, at most 3840 by 2160 pixels in a frame. An HDR source is tone mapped to Rec. 709. Audio, where the source has any, is AAC at 192 kbit/s, 48 kHz, stereo.

Stills become lossy WebP at quality 90, keeping alpha where the source has it. An animated source loops forever. A WebP that already fits the size bound is passed through rather than re-encoded.

Audio becomes an MP3 for a playlist soundtrack. An MP3 source uploads unchanged; WAV, AAC, M4A, OGG and FLAC sources are converted to a 128 kb/s MP3, which needs an ffmpeg build with libmp3lame. A track runs from 1 second to 3 hours. `playlist init` turns audio inputs into soundtrack tracks rather than pages. The ready object is an `audio` media object with a duration. It cannot be placed on a page; name it in the playlist `audio` soundtrack instead.

The ready object id is `data.media_id`. The same value is `data.id` and `data.operation.result.media_id`. That id is what a playlist selector takes.

Ready media with the same bytes, file name and tag is returned instead of uploading again (`data.reused` is `true`), so rerunning an interrupted upload returns its media ID. `data.timing` reports milliseconds per stage: `prepare_ms` (conversion and hash), `declare_ms`, `transfer_ms`, `commit_ms` and `processing_ms` (until ready, as fine as `--poll-ms`; absent with `--no-wait`).

A low-information filename such as `video.mp4` adds an advisory `generic_filename` warning to the envelope. The upload still succeeds and the CLI never renames the file; pick the name before you run the command.

Conversion runs `ffmpeg` and `ffprobe` from `PATH`, or from the absolute paths in `SCREENRIG_FFMPEG` and `SCREENRIG_FFPROBE`. Stills prefer the ffmpeg `libwebp` encoder and fall back to `cwebp` on `PATH` or `SCREENRIG_CWEBP`. Run `doctor` before the first upload of a session.

`--preset signage-1080p30` bounds the long edge to 1920, the short edge to 1080, and the rate to a constant 30 fps. `signage-4k30` bounds the long edge to 3840, the short edge to 2160, and the rate to a constant 30 fps. Both work with landscape, portrait, and square content. `--no-audio` omits audio encoding for silent clips. These options apply only to video.

```console
screenrig media upload ./lobby.mov --preset signage-1080p30 --no-audio
```

```console
screenrig media upload ./menu.png --folder mfd_01EXAMPLEFOLDER000000000
```

`media upload-batch --folder` puts every uploaded item in the folder the same way.

An image or video may be up to 16383 pixels wide or tall and 67,108,864 pixels (8192 by 8192) in one frame; a larger one is refused with `invalid_request`. The default conversion fits each edge within 3840 pixels, so upload an ultra-wide master such as 9216 by 2160 or 12000 by 540 with `--no-transcode`. One video upload may be at most the largest video upload of the project's plan, which `project capabilities` reports as `media_video_bytes` (see [Pricing](https://screenrig.ai/pricing.md)); a larger video is refused before it is read, so trim it to a shorter clip first. The CLI encodes video at a fixed quality, so a long video can still exceed the limit after encoding and is then refused; with `--no-transcode` the file itself must fit.

A transfer to storage that is cut off is tried up to 4 times while its upload URL is valid. If it still fails, the error names the upload's `operation_id` and the upload is cancelled so it holds no storage; with `--idempotency-key` it is kept for the rerun. Run the same command again.

Under Propose access, or when the project requires review for agent changes,
a new upload waits for a person's review before a tag, folder or `all`
selector plays it; see [Reviews](https://screenrig.ai/docs/cli/reviews.md#reviews).

### `media upload-batch`

Upload many local files from a manifest, resuming from a state file.

```console
media upload-batch <manifest.json> [--state FILE] [--concurrency N] [--tag TAG] [--folder ID]
                   [--poll-ms MS] [--timeout MS] [--no-transcode] [--codec h264|hevc]
                   [--max-fps N] [--max-edge PIXELS] [--webp-quality 1-100] [--no-progress]
                   [--preset signage-1080p30|signage-4k30] [--no-audio]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<manifest.json>` | path | yes | none | `{ "items": [ { "path": "./a.png", "tag"?: "Lobby", "content_type"?: "image/png" } ] }` with 1 to 1000 items. Paths are relative to the manifest file. |
| `--state FILE` | path | no | private file beside config | A 0600 JSON file keyed by the SHA-256 of each source file's local bytes; the default is one file per project, API address and manifest. Items already present with a `media_id` are reported as `resumed`. Do not share one state file across projects. |
| `--concurrency N` | integer | no | 4 | Parallel uploads, 1 to 8. |
| `--no-transcode` | boolean | no | off | Upload the source bytes unchanged, as `media upload` does. Cannot combine with `--codec`, `--max-fps`, `--max-edge`, `--webp-quality`, `--preset` or `--no-audio`. |
| `--poll-ms MS` | integer | no | 1000 | Interval between operation polls while waiting. |
| `--timeout MS` | integer | no | 120000 | Deadline for the wait. It also bounds each underlying request. |
| `--codec` | string | no | `h264` | `h264` or `hevc`. Use `hevc` only when every screen that will play the clip is a native Player. |
| `--max-fps N` | number | no | 30 | Constant output frame rate. Every video is encoded at exactly this rate whatever its source rate, so consecutive clips never change rate. Greater than 0 and at most 240. |
| `--max-edge PIXELS` | integer | no | 3840 | Bound applied to each edge, from 16 to 3840. Aspect ratio is preserved and a smaller source is never upscaled. |
| `--webp-quality` | integer | no | 90 | Still quality for the WebP encode, from 1 to 100. |
| `--preset` | string | no | unset | `signage-1080p30` or `signage-4k30`. Orientation-aware video size and a constant 30 fps. `--max-edge` and `--max-fps` can tighten a preset but cannot enlarge it. Video only; cannot combine with `--no-transcode`. |
| `--no-audio` | boolean | no | off | Omit the audio track from video delivery. Video only; cannot combine with `--no-transcode`. |
| `--tag TAG` | string | no | none | Applied when an item omits its own tag. |
| `--folder ID` | string | no | library root | An existing `mfd_` media folder, checked before any work starts. Every item lands there when it is ready, and `data.folder_id` reports it. An uploader must name one of its folders. |
| `--no-progress` | boolean | no | off | Emit no progress output. |

```console
screenrig media upload-batch ./images.json --state ./upload-state.json
```

The envelope carries `data.items[]` in manifest order, each with `path`, `source_filename`, `sha256`, `outcome` (`accepted` or `resumed`), and `media_id`. `data.failed[]` carries items that did not land, each with its `path` and `problem`; `data.accepted`, `data.resumed`, `data.attempts`, `data.rate_limited`, `data.wait_ms` and `data.transfer_ms` count the run. An item is tried up to 8 times, waiting between tries, while the answer is a rate limit or a server error.

With failures, the exit code follows the last failed item: `7` for a rate limit, `9` for a server error, `2` for a usage error, otherwise `8`. Run the same command again with the same manifest to upload only what did not land.

### `media replace`

Replace the file of a media item in place. Its id, playlists and schedules keep working.

```console
media replace <id> <file> [--expect-rev REVISION] [--note TEXT] [--content-type TYPE] [--no-wait]
              [--poll-ms MS] [--timeout MS] [--no-transcode] [--codec h264|hevc] [--max-fps N]
              [--max-edge PIXELS] [--webp-quality 1-100] [--no-progress]
              [--preset signage-1080p30|signage-4k30] [--no-audio]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `med_` object id. |
| `<file>` | path | yes | none | The new still, clip, or audio track. It must be the same kind as the media: image for image, video for video, audio for audio. Checked before any work starts (`usage_error`, exit 2). |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. A supplied stale value is `revision_conflict` and the declared upload is cancelled: read `media show ID` and run the command again. |
| `--note TEXT` | string | no | none | Kept with the new version and shown to a person who reviews it. At most 280 characters. |
| `--content-type TYPE` | string | no | detected from the uploaded bytes | Declare the media type yourself instead of letting the CLI detect it. |
| `--no-wait` | boolean | no | off | Return as soon as the replacement is accepted. The new file is not in service yet; finish with `operations wait`. |
| `--poll-ms MS` | integer | no | 1000 | Interval between operation polls while waiting. |
| `--timeout MS` | integer | no | 120000 | Deadline for the wait. It also bounds each underlying request. |
| `--no-transcode` | boolean | no | off | Upload the source bytes unchanged. `ffmpeg`, `ffprobe`, and `cwebp` are not run. The file must already be a PNG, JPEG, WebP, GIF, MP4, WebM or MP3, and lossless WebP is still rejected. Cannot combine with `--codec`, `--max-fps`, `--max-edge`, `--webp-quality`, `--preset` or `--no-audio`. |
| `--codec` | string | no | `h264` | `h264` or `hevc`. Use `hevc` only when every screen that will play the clip is a native Player. |
| `--max-fps N` | number | no | 30 | Constant output frame rate. Every video is encoded at exactly this rate whatever its source rate, so consecutive clips never change rate. Greater than 0 and at most 240. |
| `--max-edge PIXELS` | integer | no | 3840 | Bound applied to each edge, from 16 to 3840. Aspect ratio is preserved and a smaller source is never upscaled. |
| `--webp-quality` | integer | no | 90 | Still quality for the WebP encode, from 1 to 100. |
| `--preset` | string | no | unset | `signage-1080p30` or `signage-4k30`. Orientation-aware video size and a constant 30 fps. `--max-edge` and `--max-fps` can tighten a preset but cannot enlarge it. Video only; cannot combine with `--no-transcode`. |
| `--no-audio` | boolean | no | off | Omit the audio track from video delivery. Video only; cannot combine with `--no-transcode`. |
| `--no-progress` | boolean | no | off | Emit no progress output. Progress always goes to stderr; stdout stays reserved for the one result envelope. |

```console
screenrig media replace med_01EXAMPLEIMAGE0000000000 ./menu-v2.png
screenrig media replace med_01EXAMPLEVIDEO0000000000 ./intro.mp4 --expect-rev 3 --note "Spring cut"
```

The media keeps its id, tags and folder, so every playlist and schedule that names it plays the new file at the next manifest, and tag, folder and `all` selectors pick it up too. Nothing is rescheduled. The command runs the same steps as `media upload` (conversion, the plan's video byte limit, the dimension limits, the plan's storage limit, the signed transfer) and then replaces instead of adding new media. It takes no `--tag` or `--folder`.

By default the command waits for processing. `data.media_id` and `data.id` are the media id, `data.version` is the number of the new file in `media versions`, and `data.revision` is the media's new revision. `data.warnings`, `data.operation`, `data.upload`, `data.transcode` and the timing are reported as for `media upload`, and the operation id is on the envelope. With `--no-wait`, `data.operation` is the accepted operation and `data.version` and `data.warnings` are absent.

A changed aspect ratio is allowed. The envelope carries an `aspect_ratio_changed` warning, and `data.warnings[].playlists` names the playlists that use the media, so check how they look. A file with the same bytes as the current one is refused with `resource_conflict` (exit 13).

Every earlier file stays in the media's history, and counts toward stored bytes until its version is deleted; see `media versions`.

Under Propose access, or when the project requires review for agent changes, the replacement waits for a person and the command succeeds (exit 0): `data.status` is `pending_review`, `data.review_id` names the review, warning `pending_review` gives its URL and `data.next` is `screenrig reviews show ID`. The media keeps playing its current file until a person approves, so tell them it is waiting and do not repeat the write. A newer replacement of the same media supersedes the one that waits. See [Reviews](https://screenrig.ai/docs/cli/reviews.md#reviews).

### `media versions`

List the files a media item has had, the file in service first.

```console
media versions <id> [--limit N] [--cursor CURSOR]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `med_` object id. |
| `--limit N` | integer | no | every page | Read one page of this many versions, 1 to 100. |
| `--cursor CURSOR` | string | no | none | Read the page after the `data.next_cursor` of the previous page. |

```console
screenrig media versions med_01EXAMPLEIMAGE0000000000
```

`data.items` has one entry per file the media has had: `number`, `current`, `filename`, `source_filename`, `content_type`, `sha256`, `bytes`, `width`, `height`, `duration_ms`, `codecs`, `source` (`upload`, `replace`, `sync` or `restore`), `note`, `by` (kind `person`, `agent`, `client` or `sync`, with id and name), `restored_from` and `created_at`. The file in service comes first (`current: true`), then the others newest first. Version 1 is the first upload; each replacement, restore or media sync change adds the next number.

Without `--limit` and `--cursor` every page is read and `data.next_cursor` is `null`. With either, one page is read: while `data.next_cursor` is a string, run the command again with `--cursor` set to it. Read only access is enough. A past version's bytes stay stored and count toward the project's stored bytes until `media version delete` drops them; there is no limit on how many versions are kept, and stored bytes above the plan's free storage are billed like any other storage.

### `media restore`

Put a past version back in service as a new version.

```console
media restore <id> --number N [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `med_` object id. |
| `--number N` | integer | yes | none | The version number to put back, as `media versions` lists it. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. A supplied stale value is `revision_conflict`: read `media show ID` and run the command again. |
| `--note TEXT` | string | no | none | Kept with the new version and shown to a person who reviews it. At most 280 characters. |

```console
screenrig media restore med_01EXAMPLEIMAGE0000000000 --number 2
screenrig media restore med_01EXAMPLEIMAGE0000000000 --number 2 --expect-rev 5 --note "Back to the winter menu"
```

The media keeps its id and moves to a new revision, and every playlist and schedule that names it plays that file again. The version it copies stays in the history and shares its bytes; the new version counts toward stored bytes like any other. `data.media` is the media and `data.warnings` carries `aspect_ratio_changed`, naming the playlists that use the media, when the restored file has another aspect ratio.

The version in service cannot be restored (`resource_conflict`, exit 5), and an unknown version is `not_found` (exit 4). Under Propose access, or when the project requires review for agent changes, the restore waits for a person and the command succeeds (exit 0): `data.status` is `pending_review` with `data.review`, warning `pending_review` gives its URL and `data.next` is `screenrig reviews show ID`. The media keeps playing its current file until a person approves, so do not repeat the write.

### `media version delete`

Delete a past version and its stored bytes.

```console
media version delete <id> --number N
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `med_` object id. |
| `--number N` | integer | yes | none | The version number to delete, as `media versions` lists it. |

```console
screenrig media version delete med_01EXAMPLEIMAGE0000000000 --number 2
```

The version's record goes and its bytes leave the project's stored bytes at once. The stored object is removed after 24 hours unless another version or the file in service holds the same file. A deleted version cannot be restored. The version in service cannot be deleted, and no version can be deleted while a replacement of the media waits for review; both are `resource_conflict` (exit 5), and `media versions ID` shows which version is current. An unknown version is `not_found` (exit 4). Deleting needs Manage access: a Propose credential is refused with `insufficient_access`, and the delete is never proposed for review.

### `media show`

Read one media object, including its current revision.

```console
media show <id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `med_` object id. |

```console
screenrig media show med_01EXAMPLEIMAGE0000000000
```

### `media download`

Write a stored rendition to disk for inspection. Never print the bytes.

```console
media download <id> [--output FILE]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `med_` object id. |
| `--output FILE` | path | no | `./<id>.<ext>` in the current directory | A file path, not a directory. An existing file is overwritten. The extension follows the content type. |

```console
screenrig media download med_01EXAMPLEIMAGE0000000000 --output ./lobby.webp
```

The envelope reports `media_id`, `path`, `bytes`, `sha256`, `content_type`, `primitive`, `filename`, optional `source_filename`, and `width` / `height`. It never carries pixels. Use this to proof an image before it goes on glass.

### `media list`

List media objects, optionally filtered by tag, primitive and folder.

```console
media list [--tag TAG] [--primitive image|video|audio] [--folder ID|root] [--library]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--tag TAG` | string | no | none | Match `^[A-Za-z0-9]{1,32}$`. Untagged objects are omitted whenever this filter is present. |
| `--primitive` | string | no | all | `image`, `video`, or `audio`. Restrict the listing to stills, clips, or soundtrack tracks. |
| `--folder ID\|root` | string | no | every folder | Only files directly inside this `mfd_` folder, not its subfolders, or with `root` only files at the library root. The API applies the filter. |
| `--library` | boolean | no | off | List the organization Library's media instead, read-only: rows carry `library: true`. Use their IDs in playlist pages; change them only in the Library project. |

```console
screenrig media list --tag LobbyLoop --primitive video
screenrig media list --folder mfd_01EXAMPLEFOLDER000000000
screenrig media list --library
```

Each row carries `folder_id`, which is absent at the library root.

### `media selector-preview`

Preview which ready files a dynamic media selector matches now, without publishing a playlist or advancing a Player.

```console
media selector-preview <selector.json> --primitive image|video [--rect WxH] [--timezone IANA]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<selector.json>` | path | yes | none | A JSON file holding one `all`, `tag` or `folder` media selector, or `-` for stdin. A tag with a screen token cannot be previewed. |
| `--primitive` | string | yes | none | `image` or `video`: the kind of file the selector matches. |
| `--rect WxH` | string | with `fit: "aspect"` | none | The size of the primitive's rect, such as `1080x1920`, both numbers above 0. |
| `--timezone IANA` | string | no | `UTC` | The time zone the files' [availability](https://screenrig.ai/docs/cli/media.md#media-availability) rules are read in, such as `America/New_York`. |

```console
screenrig media selector-preview selector.json --primitive video
screenrig media selector-preview selector.json --primitive image --timezone America/New_York
```

The selector keys (`order`, `batch_size`, `one_at_a_time`, `weights` and `fit`) are described under [Tag filters and playback batches](https://screenrig.ai/docs/cli/media.md#tag-filters-and-playback-batches) and [Weighted rotation and aspect fit](https://screenrig.ai/docs/cli/media.md#weighted-rotation-and-aspect-fit).

The answer carries `matched_count`, the filename-ordered `candidates` (`media_id`, `filename`, `tags`, `width`, `height`), the normalized `selector`, and in `excluded` the files left out with their `reason`: `aspect_mismatch` for `fit: "aspect"`, and `outside_window` for a file whose availability rule does not hold now. With `order: "weighted"` each candidate carries `weight` and `share_percent`, and `cycle` is the number of plays after which every item has played exactly its weight. More than 1024 matches is `invalid_request`. Read access is enough. The preview reads the authenticated project, never advances a cursor or changes a manifest, and a catalog change after it is picked up at each Player's next loop boundary.

### `media update`

Replace or clear the tags of one object. Its availability rule is set with `media availability`.

```console
media update <id> (--tag TAG | --tags TAG1,TAG2 | --clear-tag) [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `med_` object id. |
| `--tag TAG` | string | one of the three | none | The replacement tag, one to 32 letters or digits. It becomes the object's only tag. |
| `--tags TAG1,TAG2` | string | one of the three | none | The replacement tag set: 1 to 32 unique tags separated by commas, each one to 32 letters or digits. |
| `--clear-tag` | boolean | one of the three | off | Remove every tag and leave the object untagged. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig media update med_01EXAMPLEIMAGE0000000000 --tag LobbyLoop
screenrig media update med_01EXAMPLEIMAGE0000000000 --tags Summer,Drinks
```

Under Propose access, or when the project requires review for agent changes,
retagging approved media answers `ok` with warning `media_review_pending`:
screens keep playing it with its approved tags until a person approves the
change, and a rejection puts the tags back. Its review appears in
`reviews list` within a few seconds; a retag that changes no approved playlist
and no playlist waiting for review applies on its own. See
[Reviews](https://screenrig.ai/docs/cli/reviews.md#reviews).

### `media availability`

Set or clear when dynamic selectors may pick one file.

```console
media availability <id> (--file FILE | --clear) [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `med_` object id. |
| `--file FILE` | path | one of the two | none | A JSON availability rule, or `-` for stdin. |
| `--clear` | boolean | one of the two | off | Remove the rule; the file is always available. |
| `--expect-rev REVISION` | string | no | current revision | Optional revision precondition. Omitted, the command reads the media's current revision first. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig media availability med_01EXAMPLEIMAGE0000000000 --file rule.json
```

The rule's members and behavior are under [Media availability](https://screenrig.ai/docs/cli/media.md#media-availability). A file waiting for a person's review is refused with `media_pending_review`: decide its review first.

### `media delete`

Delete one media object.

```console
media delete <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `med_` object id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig media delete med_01EXAMPLEIMAGE0000000000
```

### `media move`

Move media files and folders into a folder, or back to the library root.

```console
media move <id>... (--folder ID | --root)
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>...` | string | yes | none | One or more `med_` file IDs and `mfd_` folder IDs, at most 1000 in all. |
| `--folder ID` | string | one of the two | none | The destination `mfd_` folder. |
| `--root` | boolean | one of the two | off | Move to the library root. |

```console
screenrig media move med_01EXAMPLEIMAGE0000000000 med_01EXAMPLEVIDEO0000000000 --folder mfd_01EXAMPLEFOLDER000000000
screenrig media move mfd_01EXAMPLEFOLDER000000000 --root
```

The move is one all-or-nothing request. A folder cannot move into itself or a folder inside it, and folders nest at most 10 levels deep. Items already in the destination stay as they are. `data.media` and `data.folders` count what moved.

Moving files changes what the `by:folder` selectors of their old and new folders play, from each screen's next selector resolve; moving a folder changes no folder's own files. Under Propose access, or when the project requires review for agent changes, each moved file waits for a person's review (`reviews list`) and approved playlists keep playing it by the folder it was approved with; see [Reviews](https://screenrig.ai/docs/cli/reviews.md#reviews).

### `media folder list`

List the project's media folders.

```console
media folder list [--library]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--library` | boolean | no | off | List the organization Library's folders instead, read-only: rows carry `library: true`, and a `by:folder` selector may name one to play its files. |

```console
screenrig media folder list
screenrig media folder list --library
```

Returns every folder of the project, at most 1000, ordered by name: `data.items` with `id`, `name`, `parent_id` (`null` at the library root) and `revision`. Build the tree from `parent_id`. A file's folder is `folder_id` on `media list` and `media show`; it is absent at the library root.

### `media folder show`

Read one media folder.

```console
media folder show <id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `mfd_` folder id. |

```console
screenrig media folder show mfd_01EXAMPLEFOLDER000000000
```

The envelope carries the folder's `name`, `parent_id` and `revision`.

### `media folder create`

Create a media folder at the library root or inside another folder.

```console
media folder create --name NAME [--parent ID]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--name NAME` | string | yes | none | 1 to 100 characters, trimmed. Names may repeat. |
| `--parent ID` | string | no | library root | The `mfd_` folder to create it in. |

```console
screenrig media folder create --name Menus
screenrig media folder create --name Lunch --parent mfd_01EXAMPLEFOLDER000000000
```

An organization holds at most 1000 folders across its projects, nested at most 10 levels deep. A playlist plays a folder's files with a `by:folder` selector.

### `media folder update`

Rename a media folder, or move it with everything inside into another folder.

```console
media folder update <id> [--name NAME] [--parent ID | --root] [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `mfd_` folder id. |
| `--name NAME` | string | at least one of `--name`, `--parent`, `--root` | unchanged | The new name, 1 to 100 characters. |
| `--parent ID` | string | at least one of `--name`, `--parent`, `--root` | unchanged | Move the folder, with everything inside, into this `mfd_` folder. Not with `--root`. |
| `--root` | boolean | at least one of `--name`, `--parent`, `--root` | off | Move the folder to the library root. Not with `--parent`. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig media folder update mfd_01EXAMPLEFOLDER000000000 --name Archive
screenrig media folder update mfd_01EXAMPLEFOLDER000000000 --root
```

A folder cannot move into itself or a folder inside it, and a move must keep every folder within 10 levels. Sending the current values is a no-op.

### `media folder delete`

Delete a media folder. Its files and folders move up one level.

```console
media folder delete <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `mfd_` folder id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig media folder delete mfd_01EXAMPLEFOLDER000000000
```

Deletes only the folder. Its files and folders move up into its parent, or into the library root when it has none. A `by:folder` selector that names it then matches nothing, so repoint those playlists first.

### Tag filters and playback batches

Media carries up to 32 unique tags, each 1–32 letters or digits. Replace the
whole set with `screenrig media update med_ID --tags Summer,Drinks`.
`--tag Summer` replaces it with one tag; `--clear-tag` clears every tag.
`media list --tag Summer` matches membership anywhere in the set. Tags are
case-sensitive and are never authorization.

A dynamic selector can use one `tag` or a `tags` filter object:

```json
{
  "by": "tag",
  "tags": { "all": ["Summer", "Drinks"], "any": ["Lobby", "Patio"], "none": ["Expired"] },
  "order": "random",
  "one_at_a_time": false,
  "batch_size": 3
}
```

`all` requires every listed tag, `any` requires at least one, and `none`
excludes any listed tag. Present groups combine with AND; omitted groups
impose no condition. Supply at least one nonempty group. Each group accepts
up to 32 unique tags. `tag` and `tags` are mutually exclusive. A `none`-only
filter selects all ready media of that primitive except the excluded tags.
Image selectors select images and video selectors select videos.

A `folder` selector, `{ "by": "folder", "folder_id": "mfd_…" }`, plays the ready
files directly inside one media folder and none of its subfolders; `media
folder list` shows the ids. It takes the same `order`, `one_at_a_time` and
`batch_size` as `all` and `tag`, and deleting a folder moves its files up a level
and leaves the selector empty.

The candidate pool contains up to 1,024 ready media per selector, with 1,024
media references across the complete loop snapshot. A write counts each `all`, `tag` or `folder` selector as 32 of those references and an `ids` selector as its length. A larger match returns
an error; narrow the filters. `batch_size` is a separate limit of 1–1,024
items per page visit. On a `media_end` page, `one_at_a_time: false` plays the
next batch in sequence; omitting `batch_size` plays the remaining pass.
`one_at_a_time: true` plays one item per appearance and allows only an omitted
batch size or `batch_size: 1`. On duration, application and `audio_end` pages every `all`, `tag`, `folder` and multi-id `ids` selector
requires `one_at_a_time: true`, and `dwell_ms` is refused. Images on `media_end` also require `dwell_ms`; videos
cannot loop on that page.

The Player resolves every dynamic media primitive in one request at playlist
startup and at each loop boundary. Membership is frozen during the loop.
Uploads, removals, tagging changes, and availability edges do not
regenerate the manifest.
At the next refresh, removed or newly excluded items leave the pending queue;
new matches join when the current pass finishes. A final batch may contain
fewer than the requested number; it does not refill from the next pass.
Filename order sorts by filename, then media ID. Random order shuffles once
per complete pass, preserving the remaining order across loop refreshes. Weighted order
follows its weights, below.
The cursor is local to the screen, playlist, page, and primitive, persists
across restart, and resets when the filter, order, or batch policy changes.
Offline playback uses the last successful snapshot. Empty matches skip the
primitive and any resulting empty page.

Save the selector object above as `selector.json`, then inspect its current
candidate catalog before publishing:

```sh
screenrig media selector-preview selector.json --primitive video
```

Preview returns `matched_count`, filename-ordered `candidates` with media
IDs and tags, and the normalized `selector` policy. With random order it
shows the pool and shuffle policy; it does not choose the Player's shuffle,
advance a cursor, create a playback grant, or change a manifest. With weighted
order it returns each candidate's `weight` and `share_percent` and the `cycle`.
`--rect WxH` (API `rect: {width, height}`) is the slot's size and is required
with `fit: "aspect"`; the matches it leaves out are listed in `excluded` with
reason `aspect_mismatch`. Image selectors pass `--primitive image`. The API is
`POST /api/selectors/preview` with `{ "primitive": "video", "selector": ... }`.
It uses the authenticated project and the same matching rules as playback.
Catalog changes after preview are resolved at playback's next loop boundary.

Static bundle export freezes dynamic matches as exact IDs and shuffles a `random` selector once. It preserves
one-at-a-time or complete-pass playback; a bounded runtime sequence using
`batch_size` with `one_at_a_time: false` requires removing that field or
choosing explicit IDs before export. This prevents silently losing a runtime
batch cursor in a static bundle.

### Weighted rotation and aspect fit

`order: "weighted"` rotates one slot's creatives by weight, so each sponsor
keeps its share of plays. `weights` maps a media ID (`med_...`) or `tag:NAME`
to an integer from 1 to 1,000, with at most 64 keys. It works with `by` of
`all`, `tag` or `folder`:

```json
{
  "by": "all",
  "order": "weighted",
  "one_at_a_time": true,
  "weights": { "tag:Acme": 50, "tag:Globex": 30, "tag:Initech": 20 },
  "fit": "aspect"
}
```

Weights of 50, 30 and 20 deliver exactly 5, 3 and 2 of every 10 plays, every
cycle, and a restart resumes mid-cycle. An item's weight is its media ID key
when present; otherwise the first `tag:NAME` key, alphabetically, among the
tags it carries, split equally among the items of the selection that carry it,
so a sponsor's share holds however many creatives it has; otherwise the item
counts as its own group with weight 1. A cycle is at most 4,096 plays; shares
are rounded to it only when the exact ratio needs a longer one.

Weighted order plays one item per visit, so set `one_at_a_time: true` or
`batch_size: 1`. `weights` is required with weighted order and allowed only
with it; otherwise the write returns `invalid_request`. Weighted rotation
is a Plus feature: without it, a write that sets a weight an item does not
already hold answers `feature_not_in_plan`, while equal rotation, keeping
stored weights and clearing them stay open. Every
Player plays weighted order. A Player that does not declare the
`selector.weighted` host capability is sent `random`, and the manifest
diagnostics carry `selector_order_downgraded`.

`fit: "aspect"` on a dynamic selector keeps only files whose width over height
is within 5% of the slot's rectangle, a video wall or display frame included,
read when the selector resolves. A portrait slot never plays a landscape file.
No Player change is involved.

`screenrig media selector-preview` prints the expected share of every file of a weighted
selector and, with `fit: aspect`, lists the files it leaves out; the dashboard page editor shows the same table in
the inspector. Static bundle export freezes a weighted selector
as exact IDs in filename order; weights are not kept.

### Screen tokens in tag selectors

A tag selector's `tag`, and each entry of its `tags` filter, may be
`{screen.field.NAME}` or `{screen.id}`, the screen's identifier without its
`scr_` prefix, which is a tag. A token may stand beside letters and digits in a template of up to 128 characters. The server fills the token for each screen when
it builds that screen's manifest and selector snapshot, so one playlist plays
each screen's own media. Tag the media `Lobby` or `Garage`, set each screen's
field with `screenrig screen update scr_ID --field zone=Lobby`, and give the
slot this selector:

```json
{ "by": "tag", "tag": "{screen.field.zone}", "one_at_a_time": true }
```

The filled value follows the tag rules: 1 to 32 letters or digits. A screen
without the field, or whose value is not a tag, selects nothing: it plays no
file from that slot, and the manifest diagnostics carry `screen_fact_missing`
or `screen_fact_invalid`. Changing a field re-mints that screen's manifest. A
selector with a token cannot be previewed (`media selector-preview` takes a
concrete tag) or frozen by `playlist export`.

### Screen tokens in iframe addresses and application params

An `iframe`'s `src` and `fallback_src` may carry screen tokens in the path, query or fragment of an `http` or `https` address, and so may the optional `params` of an `application` primitive: up to 16 names of 1 to 32 lower case letters, digits or underscores (`ticket` and `policy` are reserved) with values of up to 512 characters. The server fills them for each screen:

| Token | Value |
| --- | --- |
| `{screen.id}` | The screen's identifier without its `scr_` prefix. |
| `{screen.name}` | The screen's name. |
| `{screen.timezone}` | The screen's effective IANA timezone: its override, else the zone its Player reports, else `UTC`. |
| `{screen.field.NAME}` | The screen's field `NAME`, 1 to 32 lower case letters, digits or underscores. |
| `{screen.location.lat}`, `{screen.location.lon}` | Its location in decimal degrees. |
| `{screen.location.name}`, `{screen.location.address}` | Its place name and street address. |

```json
{
  "primitive": "iframe",
  "src": "https://maps.example.com/directory?kiosk={screen.field.kiosk_id}&level={screen.field.level}",
  "title": "Directory"
}
```

The write stores the template as written, braces included. Each screen's manifest carries its own address, every value percent-encoded for its place in the URL: a `kiosk_id` of `B 2&3` becomes `B%202%263`. A token in the scheme, credentials, host or port, or in a YouTube link, a Google or Microsoft document link or a BI view address, is `invalid_request` at `src` (or `fallback_src`). Publishing skips the framing check for a templated `src`, and previews show the template as written.

A screen that lacks a value (an unset field, no location), or whose values fill into an address over 2048 characters or one with a path segment of `.` or `..`, gets no such iframe or application, and its manifest diagnostics carry `screen_fact_missing` or `screen_fact_invalid`; a page left empty is dropped. Every write that reaches such a screen (a playlist write, `screen assign`, `screen update`, a fleet assign, an approved review) answers with a `screen_fact_missing` warning that names the tokens, the first 20 screen ids and how many there are. Changing a field or location re-mints the manifest of the screens it affects.

For an application, the filled params, at most 4096 characters together once percent-encoded, reach it in `context.params` on every Player ([SDK](https://screenrig.ai/docs/sdk.md#m-context-params)). The Player web app and the Players that run it (Samsung Tizen, BrightSign, LG webOS and Vega) also append each one to the launch address after `ticket`, percent-encoded, so the application can read `location.search`. A template's `url` field may be a whole iframe `src`, and its value may itself carry screen tokens, filled when the manifest is built. A tag selector uses `{screen.id}` and `{screen.field.NAME}` the same way (above). Cards keep their own `{screen.…}` tokens.

Application primitives in one playlist that pin the same release carry the same `params`.

### Media availability

`screenrig media availability med_ID --file rule.json` sets when the dynamic
selectors (`all`, `tag` and `folder`) may pick a file. `--clear` removes the
rule and `--expect-rev N` guards the media revision. The API is
`PATCH /api/media/{id}/availability` with `If-Match` and
`{ "availability": {...} | null }`. Setting a rule needs Manage access; a
Propose credential gets `insufficient_access`. Without `--expect-rev` the command reads the media's current revision first. A file waiting for a person's review is refused with `media_pending_review`.

The rule is the schedule's clock grammar, read in each screen's time zone, and
holds at least one member: `from` and `until` (civil `YYYY-MM-DDTHH:MM`, `until`
exclusive), `windows` (weekly `{days, start, end}`), `monthly` (`{day}` or
`{nth, weekday}`, each with optional `start` and `end`), `dates`
(`{date, start, end}`) and `except` (dates that remove an occurrence, overnight
tails included), with up to 16 `windows`, 16 `monthly` entries, 100 `dates` and 100 `except` dates. It has no `data` or `interaction`. A sponsor's creative for the
long weekend, available only on weekday evenings inside the span:

```json
{
  "from": "2026-11-27T00:00",
  "until": "2026-11-30T23:59",
  "windows": [{ "days": ["mon", "tue", "wed", "thu", "fri"], "start": "17:00", "end": "22:00" }]
}
```

Only selectors read the rule: an explicit `id` or `ids` selector ignores it, as
do cards' media references, soundtrack audio and ad creatives. A file leaves the
selection and returns at each screen's next playlist loop, within about a minute
of the edge. No playlist or manifest revision changes, and an offline Player
keeps its last selection until it reconnects.
[`media selector-preview`](https://screenrig.ai/docs/cli/media.md#cmd-media-selector-preview) with
`--timezone ZONE` (default UTC) reads the rules in that zone and lists the files
outside their rule now in `excluded` with reason `outside_window`.

## Other command groups

- [Projects](https://screenrig.ai/docs/cli/projects.md)
- [Service clients](https://screenrig.ai/docs/cli/service-clients.md)
- [Help and support](https://screenrig.ai/docs/cli/help.md)
- [Media sync](https://screenrig.ai/docs/cli/media-sync.md)
- [Playlists](https://screenrig.ai/docs/cli/playlists.md)
- [Cards](https://screenrig.ai/docs/cli/cards.md)
- [Applications](https://screenrig.ai/docs/cli/applications.md)
- [Screens](https://screenrig.ai/docs/cli/screens.md)
- [Schedules](https://screenrig.ai/docs/cli/schedules.md)
- [Campaigns](https://screenrig.ai/docs/cli/campaigns.md)
- [Device health](https://screenrig.ai/docs/cli/device.md)
- [Reviews and notes](https://screenrig.ai/docs/cli/reviews.md)
- [Notifications](https://screenrig.ai/docs/cli/notifications.md)
- [Reports](https://screenrig.ai/docs/cli/reports.md)
- [Data sources](https://screenrig.ai/docs/cli/data-sources.md)
- [BI dashboards](https://screenrig.ai/docs/cli/bi.md)
- [Triggers](https://screenrig.ai/docs/cli/triggers.md)
- [Fleet links](https://screenrig.ai/docs/cli/fleet-links.md)
- [Advertising](https://screenrig.ai/docs/cli/ads.md)
