11 KiB
Redis Metadata Storage Architecture
This document describes how MetaMesh stores file metadata in Redis, the key structure, plugin metadata contributions, and special cases like FFmpeg stream handling.
Overview
MetaMesh uses Redis as its primary metadata storage, storing file metadata as Redis Hashes with flattened key-value pairs. This design enables:
- Efficient nested object storage without JSON serialization overhead
- Individual field updates without full document rewrites
- Cross-service access via Redis pub/sub notifications
- Fast VFS lookups using content-based hash IDs
Key Structure
Hash-Based Storage
Each file's metadata is stored as a Redis Hash:
Hash Key: file:{hashId}
Hash Fields: property/path (e.g., "fileinfo/duration", "titles/eng")
Hash Values: String values
Example Redis commands:
# Store file metadata
HMSET file:bafkr4ih5kapbjzq... \
"cid_midhash256" "bafkr4ih5kapbjzq..." \
"filePath" "/files/watch/movie.mkv" \
"fileinfo/duration" "7200.5" \
"fileinfo/formatName" "matroska,webm"
# Retrieve all metadata
HGETALL file:bafkr4ih5kapbjzq...
# Get single field
HGET file:bafkr4ih5kapbjzq... "fileinfo/duration"
File Index
A Redis Set tracks all known file hash IDs:
Key: file:__index__
Type: Set
Value: {hashId1, hashId2, hashId3, ...}
This enables fast bulk queries like "get all files" without scanning keys.
Primary Hash ID
The primary identifier for each file is cid_midhash256 - an IPFS-compatible CID generated from:
- SHA-256 hash of the middle 1MB of the file
- File size in bytes
- IPFS CID v1 encoding (multicodec 0x1000)
Example: bafkr4ih5kapbjzqvylwj7kx7zmkpxn6qj5xw...
This provides:
- Content-based addressing (same file = same ID)
- Fast computation (only reads 1MB)
- IPFS compatibility for future distributed storage
Collision Handling
If two different files produce the same midhash (rare), the system falls back to full SHA-256:
// Normal case
hashId = midHash256; // "bafkr4ih5kapbjzq..."
// Collision detected (different filePath with same midhash)
hashId = metadata['cid_sha2-256']; // Full hash fallback
Metadata Flattening
Nested objects are flattened into path-based keys for efficient Redis storage.
Flattening Rules
// Input (nested object)
{
title: "Inception",
video: {
codec: "h265",
width: "1920"
},
streams: [
{ language: "eng" },
{ language: "jpn" }
]
}
// Output (flattened for Redis)
title = "Inception"
video/codec = "h265"
video/width = "1920"
streams/0/language = "eng"
streams/1/language = "jpn"
Key rules:
- Nested objects use
/as path separator - Arrays use numeric indices (0, 1, 2...)
- All values stored as strings
null/undefinedstored as empty strings- Numbers and booleans converted via
String()
Reconstruction
When reading, the flattened structure is reconstructed into nested objects:
const metadata = await kvClient.getMetadataFlat(hashId);
// Returns: { title: "Inception", video: { codec: "h265", width: "1920" }, ... }
Plugin Metadata Structure
Each plugin writes metadata using a prefixed key pattern:
{plugin-id}/{field} # Simple field
{plugin-id}/{group}/{field} # Grouped field
{plugin-id}/{group}/{index}/{field} # Indexed field (for arrays)
Plugin Contribution Flow
File Discovered
↓
[FileProcessorPiscina]
├─ Compute midhash256
├─ Store preliminary metadata
└─ Dispatch plugin tasks
↓
[Plugin Processing]
├─ file-info → fileType, mimeType, sizeByte
├─ ffmpeg → fileinfo/duration, fileinfo/streamdetails/...
├─ filename-parser → titles/eng, season, episode, videoType
├─ anime-detector → anime, titles/jpn, titles/jpl
├─ tmdb → tmdbid, plot/eng, genres
└─ full-hash → cid_sha2-256, cid_sha1
↓
[Plugin Callbacks]
└─ Merge metadata into Redis via HMSET
Core Metadata Types
HashMeta - Content Identification
cid_crc32 = "bafkr..." # CRC32 (multicodec 0x0132)
cid_md5 = "bafkr..." # MD5 (multicodec 0xd5)
cid_sha1 = "bafkr..." # SHA-1 (multicodec 0x11)
cid_sha2-256 = "bafkr..." # SHA-256 (multicodec 0x12)
cid_midhash256 = "bafkr..." # Middle-hash (PRIMARY ID, multicodec 0x1000)
FileStatMeta - File Properties
fileType = "video" # video, audio, image, text
mimeType = "video/x-matroska" # MIME type
sizeByte = "4294967296" # File size in bytes
VideoMeta - Classification
videoType = "movie" # movie, episode, unknown
originalTitle = "Inception"
titles/eng = "Inception" # Localized titles
titles/jpn = "インセプション"
season = "1"
episode = "5"
movieYear = "2010"
AnimeMeta - Anime Detection
anime = "true" # "true" or "false"
titles/jpn = "進撃の巨人" # Japanese title
titles/jpl = "Shingeki no Kyojin" # Romaji
SubtitleMeta - Subtitle References
subtitles/eng/0 = "bafkr..." # Reference to subtitle file CID
subtitles/eng/1 = "bafks..."
subtitles/jpn/0 = "bafkt..."
JellyfinMeta - Comprehensive Metadata
imdbid = "tt1375666"
tmdbid = "27205"
anidbid = "12345"
plot/eng = "A movie about..."
genre/0 = "Action"
genre/1 = "Sci-Fi"
rating = "8.8"
mpaa = "PG-13"
releasedate = "2010-07-16"
art/poster = "bafkr..." # Reference to poster image CID
art/fanart = "bafks..." # Reference to backdrop image CID
FFmpeg Plugin - Special Case
The FFmpeg plugin extracts detailed stream information with a specific nested structure.
Stream Details Structure
fileinfo/duration = "7200.5"
fileinfo/formatName = "matroska,webm"
fileinfo/streamdetails/video/0/codec = "h264"
fileinfo/streamdetails/video/0/width = "1920"
fileinfo/streamdetails/video/0/height = "1080"
fileinfo/streamdetails/video/0/aspect = "16:9"
fileinfo/streamdetails/video/0/bitrate = "8000000"
fileinfo/streamdetails/audio/0/codec = "aac"
fileinfo/streamdetails/audio/0/language = "eng"
fileinfo/streamdetails/audio/0/channels = "6"
fileinfo/streamdetails/audio/1/codec = "dts"
fileinfo/streamdetails/audio/1/language = "fra"
fileinfo/streamdetails/audio/1/channels = "6"
fileinfo/streamdetails/subtitle/0/language = "eng"
fileinfo/streamdetails/subtitle/0/format = "subrip"
fileinfo/streamdetails/subtitle/1/language = "jpn"
fileinfo/streamdetails/subtitle/1/format = "ass"
fileinfo/streamdetails/embeddedimage/0/type = "cover"
Stream Type Keys
| Stream Type | Key Pattern | Description |
|---|---|---|
| Video | fileinfo/streamdetails/video/{n}/... |
Video streams (0, 1, 2...) |
| Audio | fileinfo/streamdetails/audio/{n}/... |
Audio tracks |
| Subtitle | fileinfo/streamdetails/subtitle/{n}/... |
Embedded subtitles |
| Embedded Image | fileinfo/streamdetails/embeddedimage/{n}/... |
Cover art, thumbnails |
Common Stream Fields
Video streams:
codec- h264, hevc, av1, vp9, etc.width,height- Resolutionaspect- Aspect ratiobitrate- Video bitrateprofile- Codec profile (main, high, etc.)level- Codec level
Audio streams:
codec- aac, dts, ac3, flac, etc.language- ISO 639-2 code (eng, jpn, fra)channels- Channel count (2, 6, 8)bitrate- Audio bitratesamplerate- Sample rate
Subtitle streams:
language- ISO 639-2 codeformat- subrip, ass, pgs, vobsub
Redis Pub/Sub Notifications
MetaMesh uses Redis pub/sub to notify other services of metadata changes.
Channels
meta-sort:file:batch # Batch file updates (every 5 seconds)
meta-sort:scan:reset # Full rescan notification
Batch Update Message Format
{
"timestamp": 1704067200000,
"changes": [
{ "action": "add", "hashId": "bafkr4ih5..." },
{ "action": "update", "hashId": "bafkr4ih5..." },
{ "action": "remove", "hashId": "bafkr4ih5..." }
]
}
This enables meta-fuse to update its VFS in near real-time without polling.
Complete Example
Here's a complete metadata example for a movie file:
# Query all metadata
redis-cli HGETALL file:bafkr4ih5kapbjzq...
# Result (reconstructed as nested object)
{
# Content identification
"cid_midhash256": "bafkr4ih5kapbjzq...",
"cid_sha2-256": "bafks7ij2kbpck...",
"filePath": "/files/watch/movies/Inception.2010.1080p.BluRay.x264.mkv",
# File properties
"fileType": "video",
"mimeType": "video/x-matroska",
"sizeByte": "4294967296",
# Classification
"videoType": "movie",
"originalTitle": "Inception",
"movieYear": "2010",
# Localized titles
"titles": {
"eng": "Inception",
"fra": "Inception",
"jpn": "インセプション"
},
# FFmpeg stream details
"fileinfo": {
"duration": "8878.5",
"formatName": "matroska,webm",
"streamdetails": {
"video": {
"0": {
"codec": "h264",
"width": "1920",
"height": "1080",
"aspect": "16:9",
"bitrate": "8000000"
}
},
"audio": {
"0": { "codec": "dts", "language": "eng", "channels": "6" },
"1": { "codec": "ac3", "language": "fra", "channels": "6" }
},
"subtitle": {
"0": { "language": "eng", "format": "subrip" },
"1": { "language": "fra", "format": "subrip" }
}
}
},
# External metadata (TMDB)
"tmdbid": "27205",
"imdbid": "tt1375666",
"plot": {
"eng": "A thief who steals corporate secrets through dream-sharing technology..."
},
"genre": ["Action", "Science Fiction", "Adventure"],
"rating": "8.4",
"releasedate": "2010-07-16",
# Media assets
"art": {
"poster": "bafkr4poster...",
"fanart": "bafkr4fanart..."
},
# Detection flags
"anime": "false"
}
Key Implementation Files
| Component | File Path |
|---|---|
| Redis Client | packages/meta-sort/packages/meta-sort-core/src/kv/RedisClient.ts |
| Metadata Utils | packages/meta-sort/packages/meta-sort-core/src/kv/MetadataUtils.ts |
| KV Interface | packages/meta-sort/packages/meta-sort-core/src/kv/IKVClient.ts |
| File Processor | packages/meta-sort/packages/meta-sort-core/src/logic/fileProcessor/FileProcessorPiscina.ts |
| Pub/Sub Pipeline | packages/meta-sort/packages/meta-sort-core/src/logic/pipeline/StreamingPipeline.ts |
| Type Definitions | packages/meta-interface/src/lib/metadata-type/*.ts |
Debugging
# List all file keys
docker exec meta-sort-dev redis-cli KEYS 'file:*' | head -20
# Get all metadata for a file
docker exec meta-sort-dev redis-cli HGETALL 'file:bafkr4ih5...'
# Get specific field
docker exec meta-sort-dev redis-cli HGET 'file:bafkr4ih5...' 'fileinfo/duration'
# Count total files
docker exec meta-sort-dev redis-cli SCARD 'file:__index__'
# Monitor metadata changes in real-time
docker exec meta-sort-dev redis-cli SUBSCRIBE 'meta-sort:file:batch'