GRFR

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:

  1. If the file already exists, first read all keys
  2. Rewrite only the keys you manage, and leave every other key as is
  3. If $schema is missing, add the public schema URL (do not touch it if present)
  4. In metadata files, set updated_at to the current datetime only when metadata actually changed
  5. 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.