Features: - FPS counter in conversion status showing real-time encoding speed - Job queue now displays FPS, encoding speed (e.g., "1.2x"), and ETA for running conversions - Copy Comparison button exports side-by-side metadata comparison report - Auto-compare checkbox in Convert module - automatically loads Compare view after conversion - Convert Now properly adds job to queue and displays in Job Queue with live stats - Module badge colors in job queue now match main menu tile colors - Fixed fullscreen compare window sizing (reduced player dimensions to prevent overflow) Bug Fixes: - Fixed queue state management - only one job runs at a time (prevents multiple jobs showing "running") - Fixed Compare module slot assignment - single video drops now fill empty slot instead of overwriting - Fixed job queue scroll rubber banding (no longer jumps back to top) - Enhanced crop detection validation for WMV/AVI formats with dimension clamping and bounds checking Documentation: - VT_Player integration notes with API requirements for keyframing and trim features - LosslessCut feature analysis for Trim module inspiration - Video metadata guide covering MP4/MKV custom fields and NFO generation - Trim module design specification - Compare fullscreen mode documentation - Updated VIDEO_PLAYER_FORK.md to mark fork as completed Technical Changes: - Added state tracking for FPS, speed, and ETA (main.go:197-199) - Enhanced queue processJobs() to check for running jobs before starting new ones - Improved Compare module drag-and-drop logic with smart slot assignment (both code paths) - Added deferred scroll position restoration to prevent UI jumping - Job queue Config map now carries conversion stats for display
17 KiB
Video Metadata Guide for VideoTools
Overview
This guide covers adding custom metadata fields to video files, NFO generation, and integration with VideoTools modules.
📦 Container Format Metadata Capabilities
MP4 / MOV (MPEG-4)
Metadata storage: Atoms in moov container
Standard iTunes-compatible tags:
©nam - Title
©ART - Artist
©alb - Album
©day - Year
©gen - Genre
©cmt - Comment
desc - Description
©too - Encoding tool
©enc - Encoded by
cprt - Copyright
Custom tags (with proper keys):
----:com.apple.iTunes:DIRECTOR - Director
----:com.apple.iTunes:PERFORMERS - Performers
----:com.apple.iTunes:STUDIO - Studio/Production
----:com.apple.iTunes:SERIES - Series name
----:com.apple.iTunes:SCENE - Scene number
----:com.apple.iTunes:CATEGORIES - Categories/Tags
Setting metadata with FFmpeg:
ffmpeg -i input.mp4 -c copy \
-metadata title="Scene Title" \
-metadata artist="Performer Name" \
-metadata album="Series Name" \
-metadata date="2025" \
-metadata genre="Category" \
-metadata comment="Scene description" \
-metadata description="Full scene info" \
output.mp4
Custom fields:
ffmpeg -i input.mp4 -c copy \
-metadata:s:v:0 custom_field="Custom Value" \
output.mp4
MKV (Matroska)
Metadata storage: Tags element (XML-based)
Built-in tag support:
<Tags>
<Tag>
<Simple>
<Name>TITLE</Name>
<String>Scene Title</String>
</Simple>
<Simple>
<Name>ARTIST</Name>
<String>Performer Name</String>
</Simple>
<Simple>
<Name>DIRECTOR</Name>
<String>Director Name</String>
</Simple>
<Simple>
<Name>STUDIO</Name>
<String>Production Studio</String>
</Simple>
<!-- Arbitrary custom tags -->
<Simple>
<Name>PERFORMERS</Name>
<String>Performer 1, Performer 2</String>
</Simple>
<Simple>
<Name>SCENE_NUMBER</Name>
<String>EP042</String>
</Simple>
<Simple>
<Name>CATEGORIES</Name>
<String>Cat1, Cat2, Cat3</String>
</Simple>
</Tag>
</Tags>
Setting metadata with FFmpeg:
ffmpeg -i input.mkv -c copy \
-metadata title="Scene Title" \
-metadata artist="Performer Name" \
-metadata director="Director" \
-metadata studio="Studio Name" \
output.mkv
Advantages of MKV:
- Unlimited custom tags (any key-value pairs)
- Can attach files (NFO, images, scripts)
- Hierarchical metadata structure
- Best for archival/preservation
MOV (QuickTime)
Same as MP4 (both use MPEG-4 structure), but QuickTime supports additional proprietary tags.
📄 NFO File Format
NFO (Info) files are plain text/XML files that contain detailed metadata. Common in media libraries (Kodi, Plex, etc.).
NFO Format for Movies:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<movie>
<title>Scene Title</title>
<originaltitle>Original Title</originaltitle>
<sorttitle>Sort Title</sorttitle>
<year>2025</year>
<releasedate>2025-12-04</releasedate>
<plot>Scene description and plot summary</plot>
<runtime>45</runtime> <!-- minutes -->
<studio>Production Studio</studio>
<director>Director Name</director>
<actor>
<name>Performer 1</name>
<role>Role 1</role>
<thumb>path/to/performer1.jpg</thumb>
</actor>
<actor>
<name>Performer 2</name>
<role>Role 2</role>
</actor>
<genre>Category 1</genre>
<genre>Category 2</genre>
<tag>Tag1</tag>
<tag>Tag2</tag>
<rating>8.5</rating>
<userrating>9.0</userrating>
<fileinfo>
<streamdetails>
<video>
<codec>h264</codec>
<width>1920</width>
<height>1080</height>
<durationinseconds>2700</durationinseconds>
<aspect>1.777778</aspect>
</video>
<audio>
<codec>aac</codec>
<channels>2</channels>
</audio>
</streamdetails>
</fileinfo>
<!-- Custom fields -->
<series>Series Name</series>
<episode>42</episode>
<scene_number>EP042</scene_number>
</movie>
NFO Format for TV Episodes:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<episodedetails>
<title>Episode Title</title>
<showtitle>Series Name</showtitle>
<season>1</season>
<episode>5</episode>
<aired>2025-12-04</aired>
<plot>Episode description</plot>
<runtime>30</runtime>
<director>Director Name</director>
<actor>
<name>Performer 1</name>
<role>Character</role>
</actor>
<studio>Production Studio</studio>
<rating>8.0</rating>
</episodedetails>
🛠️ VideoTools Integration Plan
Module: Metadata Editor (New Module)
Purpose: Edit video metadata and generate NFO files
Features:
- Load video → Extract existing metadata
- Edit fields → Standard + custom fields
- NFO generation → Auto-generate from metadata
- Embed metadata → Write back to video file (lossless remux)
- Batch metadata → Apply same metadata to multiple files
- Templates → Save/load metadata templates
UI Layout:
┌─────────────────────────────────────────────────┐
│ < METADATA │ ← Purple header
├─────────────────────────────────────────────────┤
│ │
│ File: scene_042.mp4 │
│ │
│ ┌─ Basic Info ──────────────────────────────┐ │
│ │ Title: [________________] │ │
│ │ Studio: [________________] │ │
│ │ Series: [________________] │ │
│ │ Scene #: [____] │ │
│ │ Date: [2025-12-04] │ │
│ │ Duration: 45:23 (auto) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌─ Performers ────────────────────────────────┐ │
│ │ Performer 1: [________________] [X] │ │
│ │ Performer 2: [________________] [X] │ │
│ │ [+ Add Performer] │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌─ Categories/Tags ──────────────────────────┐ │
│ │ [Tag1] [Tag2] [Tag3] [+ Add] │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌─ Description ────────────────────────────────┐ │
│ │ [Multiline text area for plot/description] │ │
│ │ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌─ Custom Fields ────────────────────────────┐ │
│ │ Director: [________________] │ │
│ │ IMDB ID: [________________] │ │
│ │ Custom 1: [________________] │ │
│ │ [+ Add Field] │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ [Generate NFO] [Embed in Video] [Save Template]│
│ │
└─────────────────────────────────────────────────┘
🔧 Implementation Details
1. Reading Metadata
Using FFprobe:
ffprobe -v quiet -print_format json -show_format input.mp4
# Output includes:
{
"format": {
"filename": "input.mp4",
"tags": {
"title": "Scene Title",
"artist": "Performer Name",
"album": "Series Name",
"date": "2025",
"genre": "Category",
"comment": "Description"
}
}
}
Go implementation:
type VideoMetadata struct {
Title string
Studio string
Series string
SceneNumber string
Date string
Performers []string
Director string
Categories []string
Description string
CustomFields map[string]string
}
func probeMetadata(path string) (*VideoMetadata, error) {
cmd := exec.Command("ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_format",
path,
)
output, err := cmd.Output()
if err != nil {
return nil, err
}
var result struct {
Format struct {
Tags map[string]string `json:"tags"`
} `json:"format"`
}
json.Unmarshal(output, &result)
metadata := &VideoMetadata{
Title: result.Format.Tags["title"],
Studio: result.Format.Tags["studio"],
Series: result.Format.Tags["album"],
Date: result.Format.Tags["date"],
Categories: strings.Split(result.Format.Tags["genre"], ", "),
Description: result.Format.Tags["comment"],
CustomFields: make(map[string]string),
}
return metadata, nil
}
2. Writing Metadata
Using FFmpeg (lossless remux):
func embedMetadata(inputPath string, metadata *VideoMetadata, outputPath string) error {
args := []string{
"-i", inputPath,
"-c", "copy", // Lossless copy
}
// Add standard tags
if metadata.Title != "" {
args = append(args, "-metadata", fmt.Sprintf("title=%s", metadata.Title))
}
if metadata.Studio != "" {
args = append(args, "-metadata", fmt.Sprintf("studio=%s", metadata.Studio))
}
if metadata.Series != "" {
args = append(args, "-metadata", fmt.Sprintf("album=%s", metadata.Series))
}
if metadata.Date != "" {
args = append(args, "-metadata", fmt.Sprintf("date=%s", metadata.Date))
}
if len(metadata.Categories) > 0 {
args = append(args, "-metadata", fmt.Sprintf("genre=%s", strings.Join(metadata.Categories, ", ")))
}
if metadata.Description != "" {
args = append(args, "-metadata", fmt.Sprintf("comment=%s", metadata.Description))
}
// Add custom fields
for key, value := range metadata.CustomFields {
args = append(args, "-metadata", fmt.Sprintf("%s=%s", key, value))
}
args = append(args, outputPath)
cmd := exec.Command("ffmpeg", args...)
return cmd.Run()
}
3. Generating NFO
func generateNFO(metadata *VideoMetadata, videoPath string) (string, error) {
nfo := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<movie>
<title>` + escapeXML(metadata.Title) + `</title>
<studio>` + escapeXML(metadata.Studio) + `</studio>
<series>` + escapeXML(metadata.Series) + `</series>
<year>` + metadata.Date + `</year>
<plot>` + escapeXML(metadata.Description) + `</plot>
`
// Add performers
for _, performer := range metadata.Performers {
nfo += ` <actor>
<name>` + escapeXML(performer) + `</name>
</actor>
`
}
// Add categories/genres
for _, category := range metadata.Categories {
nfo += ` <genre>` + escapeXML(category) + `</genre>
`
}
// Add custom fields
for key, value := range metadata.CustomFields {
nfo += ` <` + key + `>` + escapeXML(value) + `</` + key + `>
`
}
nfo += `</movie>`
// Save to file (same name as video + .nfo extension)
nfoPath := strings.TrimSuffix(videoPath, filepath.Ext(videoPath)) + ".nfo"
return nfoPath, os.WriteFile(nfoPath, []byte(nfo), 0644)
}
func escapeXML(s string) string {
s = strings.ReplaceAll(s, "&", "&")
s = strings.ReplaceAll(s, "<", "<")
s = strings.ReplaceAll(s, ">", ">")
s = strings.ReplaceAll(s, "\"", """)
s = strings.ReplaceAll(s, "'", "'")
return s
}
4. Attaching NFO to MKV
MKV supports embedded attachments (like NFO files):
# Attach NFO file to MKV
mkvpropedit video.mkv --add-attachment scene_info.nfo --attachment-mime-type text/plain --attachment-name "scene_info.nfo"
# Or with FFmpeg (re-mux required)
ffmpeg -i input.mkv -i scene_info.nfo -c copy \
-attach scene_info.nfo -metadata:s:t:0 mimetype=text/plain \
output.mkv
Go implementation:
func attachNFOtoMKV(mkvPath string, nfoPath string) error {
cmd := exec.Command("mkvpropedit", mkvPath,
"--add-attachment", nfoPath,
"--attachment-mime-type", "text/plain",
"--attachment-name", filepath.Base(nfoPath),
)
return cmd.Run()
}
📋 Metadata Templates
Allow users to save metadata templates for batch processing.
Template JSON:
{
"name": "Studio XYZ Default Template",
"fields": {
"studio": "Studio XYZ",
"series": "Series Name",
"categories": ["Category1", "Category2"],
"custom_fields": {
"director": "John Doe",
"producer": "Jane Smith"
}
}
}
Usage:
- User creates template with common studio/series info
- Load template when editing new video
- Only fill in unique fields (title, performers, date, scene #)
- Batch apply template to multiple files
🎯 Use Cases
1. Adult Content Library
Title: "Scene Title"
Studio: "Production Studio"
Series: "Series Name - Season 2"
Scene Number: "EP042"
Performers: ["Performer A", "Performer B"]
Director: "Director Name"
Categories: ["Category1", "Category2", "Category3"]
Date: "2025-12-04"
Description: "Full scene description and plot"
2. Personal Video Archive
Title: "Birthday Party 2025"
Event: "John's 30th Birthday"
Location: "Los Angeles, CA"
People: ["John", "Sarah", "Mike", "Emily"]
Date: "2025-06-15"
Description: "John's surprise birthday party"
3. Movie Collection
Title: "Movie Title"
Original Title: "原題"
Director: "Christopher Nolan"
Year: "2024"
IMDB ID: "tt1234567"
Actors: ["Actor 1", "Actor 2"]
Genre: ["Sci-Fi", "Thriller"]
Rating: "8.5/10"
🔌 Integration with Existing Modules
Convert Module
- Checkbox: "Preserve metadata" (default: on)
- Checkbox: "Copy metadata from source" (default: on)
- Allow adding/editing metadata before conversion
Inspect Module
- Add tab: "Metadata" to view/edit metadata
- Show both standard and custom fields
- Quick edit without re-encoding
Compare Module
- Add: "Compare Metadata" button
- Show metadata diff between two files
- Highlight differences
🚀 Implementation Roadmap
Phase 1: Basic Metadata Support (Week 1)
- Read metadata with ffprobe
- Display in Inspect module
- Edit basic fields (title, artist, date, comment)
- Write metadata with FFmpeg (lossless)
Phase 2: NFO Generation (Week 2)
- NFO file generation
- Save alongside video file
- Load NFO and populate fields
- Template system
Phase 3: Advanced Metadata (Week 3)
- Custom fields support
- Performers list
- Categories/tags
- Metadata Editor module UI
Phase 4: Batch & Templates (Week 4)
- Metadata templates
- Batch apply to multiple files
- MKV attachment support (embed NFO)
📚 References
FFmpeg Metadata Documentation
- https://ffmpeg.org/ffmpeg-formats.html#Metadata
- https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
NFO Format Standards
- Kodi NFO: https://kodi.wiki/view/NFO_files
- Plex Agents: https://support.plex.tv/articles/
Matroska Tags
✅ Summary
Yes, you can absolutely store custom metadata in video files!
Best format for rich metadata: MKV (unlimited custom tags + file attachments)
Most compatible: MP4/MOV (iTunes tags work in QuickTime, VLC, etc.)
Recommended approach for VideoTools:
- Support both embedded metadata (in video file) AND sidecar NFO files
- MKV: Embed NFO as attachment + metadata tags
- MP4: Metadata tags + separate .nfo file
- Allow users to choose what metadata to embed
- Generate NFO for media center compatibility (Kodi, Plex, Jellyfin)
Next steps:
- Add basic metadata reading to
probeVideo()function - Add metadata display to Inspect module
- Create Metadata Editor module
- Implement NFO generation
- Add metadata templates
This would be a killer feature for VideoTools! 🚀