科技 · 實用指南
不啟動編碼,檢查 HandBrake 佇列的字幕旗標
在已儲存佇列的檢查表中區分字幕搜尋請求與明確軌道,並保留 true、false 和未記錄值的差別。

批次佇列可能只有外語音訊字幕搜尋請求,沒有任何明確字幕軌道。把兩者視為同一回事會誤導檢查:請求掃描,不代表已經確認選中了哪條字幕,也不代表它會出現在輸出中。
HandBrake 已支援在儲存的預設中設定字幕選擇行為,也支援逐項編輯佇列。本指南補充對自己持有的受支援匯出副本進行有限的離線 CSV 檢查,不修復所謂原生缺陷、不改佇列、不開啟媒體,也不啟動編碼。
先取得正確的已儲存匯出
加入批次工作前,在預設中設定並儲存 HandBrake 的字幕選擇行為。預設儲存的是選擇行為,而非一組手動指定的固定軌道。已有佇列工作可用原生 Edit 功能修改,再將修改後的工作加入佇列;逐項檢查也是有效的內建方法。
本方法需要一份另外儲存為 queue.json 的、受支援的 Windows 佇列 CLI 用途匯出副本。目前 Windows 原始碼產生包含 Job 的物件陣列,其中有 Source、Destination 和 Subtitle 物件。一般 GUI 佇列備份的結構不同,會被拒絕。CLI 匯出排除已完成工作,但本表不顯示工作狀態,也不聲稱每個匯出工作都在等待執行。
這是儲存時的快照,不是即時佇列監視器。要求 UTF-8 JSON、陣列型別的 SubtitleList,以及型別符合支援範圍的選定欄位。缺少 Search 時按未記錄處理;Search 明確為 null、舊式 SRT 結構或其他不支援的結構會被拒絕。其他平台的 GUI 匯出與未來格式不在支援範圍內。
建立檢查表,避免覆寫檔案
在有 Python 3 的 Linux Bash 終端機中進入 queue.json 副本所在目錄。將下方命令作為一整行複製,保留直引號和反斜線。已存在 subtitles.csv 時會拒絕覆寫。此精確命令僅在 Linux Bash 執行過,沒有在 Windows 或 macOS 執行。
轉換器先解析並驗證全部檢查列,再建立 CSV。它拒絕重複 JSON 鍵、非標準數值、選定欄位的錯誤型別,以及受支援結構中的無效形態。它不開啟來源媒體或匯入字幕檔,也不檢查未選入表中的設定:這是十七個選定欄位的報告,不是完整編碼設定驗證。
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"
分開閱讀搜尋列和明確軌道列
欄名是 job、source、destination、title、row_kind、entry、track、enable、burn、default、forced、offset_ms、import_file、import_language、import_codeset、import_format 和 name。工作序號與明確軌道項目序號按儲存順序從 1 開始;重複工作保留。來源路徑、目標路徑和名稱是解析後的字串,不展開路徑或檢查檔案是否存在。
每個工作先有一列 row_kind 為 search 的列,記錄 Search 旗標,後面是 SubtitleList 明確項目的列。enable 為 true 表示請求搜尋,不是已經確認的字幕選擇。因此,啟用搜尋但明確清單為空,與沒有設定搜尋不同。布林值輸出為 true 或 false;選定欄位缺失或為 null 時留空,表示沒有記錄值,絕不推斷為 false。軌道列的 enable 留空,因為它是搜尋欄位。
內嵌 Track 索引從 0 開始:0 是有效的第一條軌道,不是缺失值。原生匯入項目使用 Track -1 和獨立的 Import 物件;檢查表保留其原始檔名、語言、字元編碼及格式,不讀取字幕檔。Offset 是毫秒單位的整數,可以為負數,不是秒。旗標或位移值不能證明檔案存在、語言符合預期,或字幕在輸出容器中能正常播放。
核准批次工作前核對快照
命令在報告工作數與檢查列數前,重新讀取 CSV 並比較表頭及每個欄位。確認每個匯出工作恰有一列搜尋列,再將已知內嵌與匯入項目和原生佇列對照。統計 CSV 記錄,不要統計文字實體行:引號包圍的欄位可能含換行。空匯出僅產生表頭。保留原始 JSON 和 CSV 母本,只在工作副本中編輯或篩選。
在 LibreOffice Calc 中按 UTF-8、逗號分隔及直雙引號文字限定符匯入。將十七欄全部設為文字,關閉合併分隔符、刪除空格和計算公式。若對話框隱藏,啟用編輯篩選器設定。我們沒有執行 Calc 介面。要修改工作,應在 HandBrake 中修改、重新匯出快照,再用新檔名產生報告;編輯 CSV 不會更新佇列,它也不是匯入檔。
此命令實際執行了 26 個合成用例:七個有效用例涵蓋空清單、沒有明確軌道的搜尋、Track 0、匯入 Track -1、負位移、缺失或 null 欄位、Unicode、引號、換行及重複工作順序;十九個無效用例在建立 CSV 前停止。另外七次覆寫嘗試被拒絕,輸入與既有輸出的位元組維持不變。沒有測試真實 HandBrake GUI、佇列匯出或編碼。寫入或回讀失敗時,捨棄部分 CSV,換新檔名重試。依賴整夜批次編碼前,應另行驗證自己有權使用的短片試編碼及播放;本表不能證明掃描結果或輸出字幕。
