Technology · PRACTICAL GUIDE
Audit HandBrake's queued subtitle flags without starting an encode
Separate a subtitle search request from explicit tracks in a saved queue report, keeping true, false and unrecorded values distinct.

A batch queue can contain a foreign-audio search request without any explicit subtitle tracks. Treating those as the same thing makes a quick check misleading: requesting a scan does not confirm which subtitle will be selected or appear in the output.
HandBrake already provides subtitle selection behaviour in saved presets and per-job queue editing. This guide adds a limited offline CSV check of selected settings in your own supported export. It does not repair a native bug, change the queue, open media or start an encode.
Start with the right saved export
Configure and save HandBrake's subtitle selection behaviour in your preset before adding a batch. Presets store selection behaviour, rather than a fixed list of manually chosen tracks. For jobs already queued, use the native queue's Edit facility and add the revised job back; reviewing each job remains a valid built-in method.
For this recipe, keep a separate copy named queue.json of a supported Windows queue export for CLI use. Current Windows source builds an array of objects containing Job, with Source, Destination and Subtitle objects. Its ordinary GUI queue backup uses a different shape and is rejected. The CLI export excludes completed jobs, but this report does not show queue status or claim that every exported job is pending.
This is a saved snapshot, not a live queue monitor. The recipe requires UTF-8 JSON, SubtitleList as an array, and selected fields with supported types. Missing Search is accepted as unrecorded; an explicit null Search object, legacy SRT structure or unsupported shape is rejected. Other platforms' GUI exports and future formats are outside this recipe.
Create the report without overwriting files
Use Python 3 in a Linux Bash terminal in the folder containing your queue.json copy. Copy the following command as one line, preserving straight quotes and backslashes. It refuses an existing subtitles.csv. This exact command was executed on Linux Bash only; it was not run on Windows or macOS.
The converter parses and validates all report rows before creating the CSV. It rejects duplicate JSON keys, nonstandard numbers, invalid selected types and malformed supported structures. It opens neither source media nor imported subtitle files. Unknown settings are not audited: this is a report of seventeen selected fields, not validation of the complete encode configuration.
python3 -c 'exec("import csv, json, sys\nfrom pathlib import Path\ndef pairs(items):\n d = {}\n for k, v in items:\n if k in d:\n raise ValueError('"'"'Duplicate JSON key'"'"')\n d[k] = v\n return d\ndef bad(value):\n raise ValueError('"'"'Nonstandard JSON number'"'"')\ndef obj(v):\n if not isinstance(v, dict):\n raise ValueError('"'"'Expected object'"'"')\n return v\ndef text(v):\n if v is None: return '"'"''"'"'\n if not isinstance(v, str): raise ValueError('"'"'Expected string'"'"')\n v.encode('"'"'utf-8'"'"')\n return v\ndef integer(v):\n if v is None: return '"'"''"'"'\n if type(v) is not int: raise ValueError('"'"'Expected integer'"'"')\n return str(v)\ndef flag(v):\n if v is None: return '"'"''"'"'\n if type(v) is not bool: raise ValueError('"'"'Expected Boolean'"'"')\n return '"'"'true'"'"' if v else '"'"'false'"'"'\nsrc, dst = map(Path, sys.argv[1:])\njobs = json.loads(src.read_bytes().decode('"'"'utf-8-sig'"'"'), object_pairs_hook=pairs, parse_constant=bad)\nif not isinstance(jobs, list): raise ValueError('"'"'Expected CLI queue array'"'"')\nrows = []\nfor n, wrapper in enumerate(jobs, 1):\n job = obj(obj(wrapper)['"'"'Job'"'"'])\n source, dest, sub = obj(job['"'"'Source'"'"']), obj(job['"'"'Destination'"'"']), obj(job['"'"'Subtitle'"'"'])\n title = source['"'"'Title'"'"']\n if type(title) is not int or title < 1: raise ValueError('"'"'Expected positive title index'"'"')\n base = [str(n), text(source['"'"'Path'"'"']), text(dest['"'"'File'"'"']), str(title)]\n search = obj(sub.get('"'"'Search'"'"', {}))\n rows.append(base + ['"'"'search'"'"', '"'"''"'"', '"'"''"'"', flag(search.get('"'"'Enable'"'"'))] + [flag(search.get(k)) for k in ['"'"'Burn'"'"', '"'"'Default'"'"', '"'"'Forced'"'"']] + ['"'"''"'"'] * 6)\n tracks = sub['"'"'SubtitleList'"'"']\n if not isinstance(tracks, list): raise ValueError('"'"'Expected subtitle list'"'"')\n for i, item in enumerate(tracks, 1):\n item = obj(item)\n if '"'"'SRT'"'"' in item: raise ValueError('"'"'Legacy SRT shape unsupported'"'"')\n imp = item.get('"'"'Import'"'"')\n if imp is None:\n track = item['"'"'Track'"'"']\n if type(track) is not int or track < 0: raise ValueError('"'"'Expected embedded track index'"'"')\n kind, imp = '"'"'embedded'"'"', {}\n else:\n kind, imp = '"'"'import'"'"', obj(imp)\n if not text(imp['"'"'Filename'"'"']): raise ValueError('"'"'Import filename missing'"'"')\n rows.append(base + [kind, str(i), integer(item.get('"'"'Track'"'"')), '"'"''"'"'] + [flag(item.get(k)) for k in ['"'"'Burn'"'"', '"'"'Default'"'"', '"'"'Forced'"'"']] + [integer(item.get('"'"'Offset'"'"'))] + [text(imp.get(k)) for k in ['"'"'Filename'"'"', '"'"'Language'"'"', '"'"'Codeset'"'"', '"'"'Format'"'"']] + [text(item.get('"'"'Name'"'"'))])\nheader = ['"'"'job'"'"', '"'"'source'"'"', '"'"'destination'"'"', '"'"'title'"'"', '"'"'row_kind'"'"', '"'"'entry'"'"', '"'"'track'"'"', '"'"'enable'"'"', '"'"'burn'"'"', '"'"'default'"'"', '"'"'forced'"'"', '"'"'offset_ms'"'"', '"'"'import_file'"'"', '"'"'import_language'"'"', '"'"'import_codeset'"'"', '"'"'import_format'"'"', '"'"'name'"'"']\nwith dst.open('"'"'x'"'"', encoding='"'"'utf-8-sig'"'"', newline='"'"''"'"') as f:\n w = csv.writer(f); w.writerow(header); w.writerows(rows)\nwith dst.open(encoding='"'"'utf-8-sig'"'"', newline='"'"''"'"') as f:\n if list(csv.reader(f)) != [header] + rows: raise ValueError('"'"'Readback mismatch; discard partial CSV'"'"')\nprint(len(jobs), '"'"'saved jobs and'"'"', len(rows), '"'"'report rows checked; not an encode result'"'"')\n")' "queue.json" "subtitles.csv"
Read search and explicit track rows separately
The columns are job, source, destination, title, row_kind, entry, track, enable, burn, default, forced, offset_ms, import_file, import_language, import_codeset, import_format and name. Job and explicit entry ordinals start at 1 in saved order; repeated jobs are retained. Source, destination and names are parsed strings, without path expansion or file-existence checks.
Each job has one row_kind search row carrying its Search flags, followed by rows for explicit SubtitleList entries. A true enable flag requests a search; it is not a confirmed subtitle selection. An empty explicit list with search enabled is therefore different from no configured search. Booleans print true or false; missing or null selected field values become blank, meaning no recorded value, never an inferred false. Track rows leave enable blank because it is a search field.
Embedded Track indices are zero-based: 0 is a valid first track, not an absent value. Native imported entries use Track -1 and a separate Import object; their raw filename, language, codeset and format are reported without inspecting the subtitle file. Offset is an integer in milliseconds, including negative values, not seconds. Neither flags nor offsets establish that a file exists, matches the intended language or will play correctly in the output container.
Verify the snapshot before approving the batch
The command reads the CSV back and compares its header and every field before reporting job and report-row counts. Check exactly one search row per exported job, then compare known embedded and imported entries with the native queue. Count CSV records, not physical text lines: a quoted field may contain a newline. An empty export creates only a header. Retain the original JSON and CSV master; edit or filter only a working report copy.
In LibreOffice Calc import UTF-8 using comma separators and straight double quotes as the text delimiter. Set all seventeen columns to Text; disable Merge delimiters, Trim spaces and Evaluate formulae. Enable Edit filter settings if the dialog is hidden. We did not execute Calc's interface. Change jobs in HandBrake, export a new snapshot and create a newly named report; editing this CSV does not update the queue and it is not an import file.
We executed 26 synthetic cases: seven valid cases covered empty lists, search without explicit tracks, Track 0, imported Track -1, negative offsets, missing/null fields, Unicode, quotes, newlines and repeated job order; nineteen invalid cases stopped before CSV creation. Seven additional overwrite attempts were refused, with input and existing output bytes unchanged. No real HandBrake GUI, queue export or encode was tested. If writing or readback fails, discard a partial CSV and retry to a new name. Separately verify an owned short test encode and its playback before relying on an overnight batch; the report cannot prove scan results or output subtitles.
