File Format Specification
GRFR manages library information with two kinds of JSON files: a catalog file that records which videos are registered and per-library settings, and metadata files that record per-video metadata. This page is the published specification for those two file formats. Following this specification, you can safely read and write the files with a text editor, your own scripts, AI agents, or other tools besides GRFR.
The current specification version is grfr-v1. Machine-readable schema definitions are also published as JSON Schema (details below).
- Metadata file:
https://grfr.shikakun.com/schema/v1/metadata.json - Catalog file:
https://grfr.shikakun.com/schema/v1/catalog.json
GRFR is a tool for managing content that you have lawfully purchased or licensed, or for which you hold the copyright. It has no functions for acquiring, distributing, or sharing content. Do not use it to manage content that has been copied or distributed in infringement of copyright. See Section 6 of the Terms of Service.
Design Principles
Neither file is GRFR’s database; both belong to you. Everything in this specification follows from that premise.
First, they must be human-readable. The contents are ordinary JSON that you can open and edit in a text editor. You can edit them even while GRFR is running; when you save, the changes appear on screen automatically.
Second, they must not depend on the app. Metadata files do not include GRFR-internal identifiers, so each file remains meaningful on its own even if you stop using GRFR. The internal ID that GRFR uses to manage videos lives only in the catalog file, as a mapping of which ID corresponds to which file.
Third, they must coexist with other tools. If a file contains keys that this specification does not define, GRFR preserves them when saving (including unknown keys inside the catalog’s settings).
File Locations
The catalog file is created at ~/Movies/grfr/catalog.json by default, and you can change the location in the app settings. Metadata files live in the same folder as the video file, with the video’s extension replaced by .json. Video folders may be anywhere.
movies/
├── sample_video.mp4 ← video file
├── sample_video.json ← metadata file
└── sample_video.jpg ← cover image
If the same folder contains videos that differ only by extension (sample_video.mp4 and sample_video.mov, for example), their metadata file names would collide. In that case, GRFR skips the video you try to register later.
Common Rules
Both files are read and written under the same rules. The character encoding is UTF-8, and the top level must be a JSON object. When GRFR saves, it pretty-prints with two-space indentation, orders keys in a fixed sequence (keys defined in this specification in definition order, then any other keys in lexicographic order), and appends a trailing newline. Writes go to a temporary file first, then replace the original, so a mid-write failure does not corrupt the original file.
This formatting exists for readable diffs and version control. It is not required when reading, so hand-edited files that are messy will be cleaned up the next time GRFR saves them.
schema and $schema
Both files begin with two similarly named keys. Their roles differ.
schema is a marker that the file follows GRFR’s specification. Its value is always "grfr-v1", combining the app name and version, and is defined by this specification. Because these files may be taken elsewhere on their own, this single line makes the origin and version clear even outside the app. In metadata files, a missing schema key is still loaded as the current version, and GRFR adds it when saving (in catalog files it is required; see below).
$schema points to a schema definition for machine validation of the JSON. Some text editors, such as Visual Studio Code, use this key for key completion and value checking. When saving, if the key is absent, GRFR writes the corresponding public schema URL (the two listed above). If a value is already present, GRFR does not overwrite it. You can therefore point it at your own schema that also validates custom fields.
Metadata File
A file that records metadata for one video.
{
"$schema": "https://grfr.shikakun.com/schema/v1/metadata.json",
"schema": "grfr-v1",
"title": "The Long Winter",
"cover": "sample_video.jpg",
"maker": "Harborlight Studios",
"label": "Harborlight Prestige",
"series": "The Long Winter",
"performers": [
"Ava Marlowe",
"Julian Crowe",
"Nora Keane"
],
"product_id": [
{
"key": "imdb",
"value": "tt8000000"
},
{
"key": "tmdb",
"value": "50000"
},
{
"key": "letterboxd",
"value": "the-long-winter"
},
{
"key": "criterion",
"value": "NW1000"
}
],
"release_date": "2008-03-14",
"description": "A three-part family chronicle set during the hardest winter on record in a northern mill town.",
"director": "Elena Voss",
"tags": [
"Drama",
"Period",
"Family",
"Slow Burn"
],
"rating": 5,
"part": "1/3",
"created_at": "2026-07-03T10:00:00+09:00",
"updated_at": "2026-07-03T10:00:00+09:00"
}
| Key | Type | Description |
|---|---|---|
| $schema | string | Reference to the schema definition |
| schema | string | Specification version. Always "grfr-v1" |
| title | string | Display title |
| cover | string / null | Cover image file name |
| maker | string / null | Maker |
| label | string / null | Label |
| series | string / null | Series |
| performers | array of string | Performer names |
| product_id | array of object | Work IDs per service |
| release_date | string / null | Release date (YYYY-MM-DD) |
| description | string / null | Description |
| director | string / null | Director |
| tags | array of string | Tags. Duplicates and empty strings are not allowed |
| rating | integer / null | Rating. Integer from 1 to 5. Unrated is null |
| part | string / null | Part notation. Which part of a split work, as "n/m" (e.g. "1/4") |
| created_at | string | Creation datetime |
| updated_at | string | Last update datetime |
Every key is optional. Missing keys are filled with defaults (null or empty arrays) when GRFR loads the file. Notes on fields that need more explanation follow.
title
If omitted, empty, or null, the video file name without its extension is used as the title.
cover
The cover image file name. It refers to a file in the same folder as the video by name only (paths that cross folders and URLs are not allowed).
product_id
An array of work IDs per service. Each element has two strings: key (service name) and value (work ID). Service names are not a fixed vocabulary; users and tools choose freely.
"product_id": [
{ "key": "example-store", "value": "ABC-123" },
{ "key": "example-archive", "value": "2026-0042" }
]
If a value other than an array is written, it is treated as a type mismatch and loaded as an empty array.
release_date, created_at, and updated_at
release_date is a date-only string such as 2026-01-15, with no time or time zone.
created_at and updated_at are datetime strings. GRFR writes them in ISO 8601 with a time zone, precise to the second, such as 2026-07-03T10:00:00+09:00. When reading, fractional seconds or a missing time zone are still accepted.
updated_at changes only when the user edits metadata. GRFR does not write app-internal state such as playback position into the metadata file, so browsing or playing a video does not change this file.
part
When one work is split across multiple files (for example, parts 1 and 2), this records which part of the whole the file is, as "n/m" (for example, the first of four files is "1/4"). n and m are integers of 1 or greater, and n is less than or equal to m.
Videos in the same folder whose title and total count m match are treated as one group and always sorted by part number in the grid. Values that do not match this form are treated as unset, but are preserved without being rewritten.
Catalog File
A file that records which videos are registered and per-library settings.
{
"$schema": "https://grfr.shikakun.com/schema/v1/catalog.json",
"schema": "grfr-v1",
"settings": {
"sort_field": "created_at",
"sort_ascending": false
},
"fields": [
{ "key": "studio", "name": "Studio", "type": "string" }
],
"filters": [
{
"id": "3F2504E0-4F89-41D3-9A0C-0305E82C3301",
"name": "Unwatched",
"match": "all",
"rules": [
{ "field": "watched", "op": "is", "value": false }
]
}
],
"entries": [
{
"id": "1E9F0A62-6F2B-4A5B-9C3D-8F1B2C3D4E5F",
"path": "movies/sample_video.mp4"
}
]
}
| Key | Type | Description |
|---|---|---|
| $schema | string | Reference to the schema definition |
| schema | string | Specification version. Always "grfr-v1" |
| settings | object | Per-library app settings. Optional |
| fields | array of object | Custom field declarations. Optional |
| filters | array of object | Saved filters. Optional |
| entries | array of object | List of registered videos |
schema and entries are required. If either is missing, GRFR cannot load the file as a catalog.
entries — Registered Videos
Each element has id and path.
id is a UUID that GRFR generates at registration time. It is an internal identifier that ties thumbnail caches and playback position to the video, so rewriting it by hand breaks those associations. Leave it as is.
path is the location of the video file. It is normally a path relative to the folder that contains the catalog file, and becomes an absolute path only when a relative path is not possible (for example, on another volume). If you move or rename a video, you may rewrite path by hand.
Any other keys on an element are discarded when GRFR saves, leaving only id and path. Put notes or custom information about a video in that video’s metadata file.
settings — Per-Library Settings
Values you change in GRFR’s settings screen or toolbar are stored in this object. All keys are optional; missing keys are treated as defaults. Out-of-range values are clamped or replaced with a nearby default, and do not cause errors. Some keys you are likely to edit by hand:
| Key | Type | Default | Description |
|---|---|---|---|
| sort_field / sort_ascending | string / boolean | “created_at” / false | Sort order of the list |
| thumbnail_size | number | 240 | Thumbnail size (160–480) |
| thumbnail_aspect | string | “16:9” | Thumbnail aspect ratio. "16:9" or "2:3" |
| parts_display | string | “all” | How to show split videos (part key). "all" shows every part; "first" collapses a group to the first part |
| scan_extensions | string | “mp4,mov,m4v,avi,mkv,webm,wmv,flv,mpg,mpeg,ts,m2ts,3gp” | Extensions treated as video when registering a folder (comma-separated) |
| cover_capture_position | number | 0.5 | Position used to generate a cover image (fraction of playback duration) |
| seek_seconds | number | 10 | Seconds to skip with arrow keys during playback |
Not every key is listed here. All of them are defined in the JSON Schema described later, so a supporting editor can list them via completion.
fields — Field Definitions
An array that declares metadata file fields. Both GRFR’s built-in fields (performers, maker, and so on) and user-added fields are managed in this fields array. Each element defines one field.
"fields": [
{
"key": "maker",
"name": "Maker",
"type": "string",
"icon": "building.2",
"sidebar": true,
"order": 2
}
]
| Property | Type | Required | Description |
|---|---|---|---|
| key | string | yes | Key name in the metadata file. Snake case. Cannot use the system-reserved keys $schema / schema / title / cover / product_id / release_date / description / rating / part / created_at / updated_at, or the built-in filter field names duration / file_size / resolution_class / watched / missing |
| name | string | yes | Display name in the UI. For the six built-in fields (performers / maker / label / series / tags / director) kept at their default names, only the on-screen label is localized to the UI language (the value in the file does not change) |
| type | string | yes | Value type. string / list (array of strings) / number / date (YYYY-MM-DD) / boolean |
| icon | string | no | SF Symbol name for the sidebar row. If omitted, a type-based default is used |
| sidebar | boolean | no | When true, shown as an entity item in the sidebar. When false or omitted, shown only in the inspector |
| order | integer | no | Display order in the sidebar, ascending. If omitted, follows the definition array order |
If a catalog has no fields key at all, GRFR writes the six default fields (performers / maker / label / series / tags / director) with sidebar: true. You may delete ones you do not need. As long as the key remains (even as an empty array), GRFR will not write them again.
filters — Saved Filters
Named filter conditions you can call up from the app’s sidebar. Each filter has id (UUID string), name (display name), match, and rules. If match is "all", every condition must match; if "any", any one matching condition is enough.
Each rule in rules has a field to inspect (field), a comparison (op), and a comparison value (value). Allowed op and value values by field:
| field | op | value |
|---|---|---|
| title / maker / label / series / director | contains / not_contains / is / is_not / begins_with / ends_with | string |
| performers / tags | contains / not_contains | string |
| rating | is / is_not / > / >= / < / <= / between | number 1–5, or null (unrated) |
| duration (seconds) / file_size (bytes) | > / >= / < / <= / between | number |
| release_date / created_at | on_or_after / on_or_before / between | "YYYY-MM-DD" string |
| resolution_class | is / is_not | "sd" / "hd" / "fhd" / "uhd4k" |
| watched / missing | is | true / false |
For between, value is a two-element array [lower, upper]. Custom fields work the same way according to their declared type (string like title, list like tags, number like duration, date like release_date, boolean like watched).
If a catalog has no filters key at all, GRFR writes a few common filters such as “Unwatched” and “★4+”. You may delete ones you do not need. As long as the key remains (even as an empty array), GRFR will not write them again.
Filters that are hand-edited with wrong types or field names are not silently ignored; they appear on screen with a warning, and selecting them yields zero results.
Adding Your Own Fields
Top-level keys not defined in the metadata file specification are treated as “unknown keys” and are preserved when GRFR saves. It is safe for your scripts or external tools to add their own keys.
If you also declare a key’s name and type in the catalog’s fields array, that key appears as a “field” in GRFR’s UI and can be edited, searched, and filtered. Setting sidebar to true also shows it in the sidebar for entity-style counts and filtering. The metadata file itself does not change shape; values are still read and written as ordinary top-level keys like other fields.
You can add fields from the Fields tab in Settings, or by editing the fields array directly in a text editor; either way, changes made in one place are reflected in the other.
At minimum, each field declaration needs the metadata key name (key), display name (name), and type (type). For sidebar display, also set sidebar: true and an icon name (icon). For example, to add a string field “Studio” and a number field “Broadcast year”:
"fields": [
{ "key": "studio", "name": "Studio", "type": "string", "sidebar": true, "icon": "film" },
{ "key": "broadcast_year", "name": "Broadcast year", "type": "number" }
]
Write key in lowercase with underscores, and choose a name different from the predefined metadata keys. type is one of five: string (string), array of strings (list), number (number), date in YYYY-MM-DD form (date), or boolean (boolean). With that declaration, the metadata file can look like:
{
"$schema": "https://grfr.shikakun.com/schema/v1/metadata.json",
"schema": "grfr-v1",
"title": "The Long Winter",
"studio": "Harborlight Studios",
"broadcast_year": 2026
}
Values written before a declaration still appear on screen once you add the declaration later. Removing a declaration does not delete the value; it simply becomes an unknown key again.
Editing by Hand and Reading/Writing from Other Tools
If you edit and save either file while GRFR is running, changes are reflected automatically. If that coincides with an operation inside GRFR, the later save wins. GRFR never overwrites a file that cannot be parsed as JSON, so even if a file is broken mid-edit, fixing and saving again is enough.
GRFR is tolerant of broken or incomplete files. Missing keys are filled with defaults, and values of the wrong type are replaced with defaults when reading. It does not delete or clobber a file just because it cannot be read, which protects user data. Compatible tools should behave the same way.
The following write procedure lets other tools coexist safely:
- If the file already exists, first read all keys
- Rewrite only the keys you manage, and leave every other key as is
- If
$schemais missing, add the public schema URL (do not touch it if present) - In metadata files, set
updated_atto the current datetime only when metadata actually changed - Write to a temporary file, then replace the original
Specification Versions
The v1 in the schema key is the specification version and corresponds to the schema URL path (/schema/v1/). The version increases only when a change breaks compatibility with older files, such as changing the type or meaning of an existing key. New keys may be added, but because readers must preserve unknown keys, older tools will not break the files.
JSON Schema
Machine-readable definitions of this specification are published as JSON Schema (2020-12).
- Metadata file:
https://grfr.shikakun.com/schema/v1/metadata.json - Catalog file:
https://grfr.shikakun.com/schema/v1/catalog.json
As noted above, files GRFR saves include these URLs in $schema by default, so supporting editors get completion and validation immediately. The catalog schema also defines every key under settings.
These schema definitions are intentionally permissive for validation (no required keys in metadata files; only schema and entries required in catalog files; both allow unknown keys). For the pretty-printed form GRFR writes, the “Common Rules” and “Editing by Hand and Reading/Writing from Other Tools” sections in this document are authoritative.