makemkvcon stdout to json

Discussion of advanced MakeMKV functionality, expert mode, conversion profiles
Post Reply
flojo
Posts: 58
Joined: Thu Jun 22, 2023 4:27 am
Location: El Paso

makemkvcon stdout to json

Post by flojo »

This might help someone. There was a few keynames I couldn't determine because I don't know what the values are but, I do believe I was able to correctly name most of the main ones (check the DVD example).

Below are the makemkvcon options used (not sure if --profile matters):
-r --minlength=0 --profile=:.mmcp.xml info

Usage:

Code: Select all

mmc2json.py -p
mmc2json.py -p "file.json"
Help:

Code: Select all

Version 1, 2023-11-12
JSON from the stdout of 'makemkvcon' using the below:
  makemkvcon -r --minlength=0 --profile=:.mmcp.xml info disc:0
 -b, --binary  : set a binary, default is path: "makemkvcon"
 -d, --device  : set a device, default is: "disc:0"
 -j, --JSON    : print JSON in 1 line to stdout
 -p, --pretty  : print JSON line by line to stdout with 2 space indent
                 * can be followed by a filename to write to, ie. -p "mkv.json"
 -l, --line    : print JSON line by line to stdout without 2 space indent
 -g, --guess   : guess the main title number, but guesses poorly.
                 * does not support playlist obfustication
                 it guesses by greatest $byte_size + $most_chapters
                 if no chapters, guess by $byte_size + $length_in_seconds
                 if that fails, guess solely by the greatest $byte_size
 -v, --version : print script version
 -h, --help    : print this help
The script:

Code: Select all

#!/usr/bin/env python3

import json
import subprocess
import sys

binary = "makemkvcon"
device = "disc:0"

# does "--profile" work with makemkvcon?
options = ["-r", "--minlength=0", "--profile=:.mmcp.xml", "info"]

# OK is not used but it is set to true on msg 5011 in msg_parse()
# MSG:5011,0,0,"Operation successfully completed","Operation successfully completed"
OK = False

json_dict = {
  "script_version": 1,
  "script_version_date": "2023-11-12",
  "binary": binary,
  "options": options,
  "device": device,
  "version": "",
  "profile": False,
  "mode": "",
  "access": "",
  "disc": {},
  "msg": [],
  "tcount": 0,
  "drive": [],
  "drv": [],
  "dropped": [],
  "titles": [],
  "skipped": [],
  "dupes": [],
  "info": [],
  "missed_key_numbers": []
  }

def clean_string(arg: list | str, individually: bool = True) -> list | str | int:
  if not isinstance(arg, list) and not isinstance(arg, str):
    raise TypeError("Argument to be cleaned was not a string or list, type was:", str(type(arg)))
  ret = None
  if individually:
    if isinstance(arg, list):
      ret = []
      for i in arg:
        tmp = i.strip()
        if tmp[0] == '"':
          tmp = tmp[1:]
        if tmp[-1] == '"':
          tmp = tmp[:-1]
        ret.append( tmp )
    elif isinstance(arg, str):
        ret = arg.strip()
        if ret[0] == '"':
          ret = ret[1:]
        if ret[-1] == '"':
          ret = ret[:-1]
  else:
    if isinstance(arg, list):
      ret = []
      for i in arg:
        tmp = i.strip()
        if tmp[0] + tmp[-1] == '""':
          tmp = tmp[1:-1]
        ret.append( tmp )
    elif isinstance(arg, str):
        ret = arg.strip()
        if ret[0] + ret[-1] == '""':
          ret = ret[1:-1]
  return ret

def segments_parse(segments: str) -> list | int:
  # csv deliminated string eg. 1-22,23,24,25,26-46,47
  if not isinstance(segments, str):
    raise TypeError("Supplied segments argument was not a string, type was:", str(type(segments)))
  tmp = segments.split(",")
  ret = []
  if len(tmp) == 1 and tmp[0].isdigit():
    ret.append(int(tmp[0]))
    return ret
  elif len(tmp) > 1:
    while len(tmp) > 0:
      if "-" in tmp[0]:
        ranges = tmp[0].split("-")
        while len(ranges) > 0:
          range_start = -1
          range_end   = -1
          if ranges[0].isdigit():
            range_start = int(ranges[0])
          else:
            ranges.pop(0)
            continue

          if len(ranges) > 1 and ranges[1].isdigit():
            range_end = int(ranges[1])

          if range_start > -1 and range_end > -1:
            ret.append(range_start)
            if range_end > range_start:
              for _ in range(range_end - range_start):
                ret.append(ret[len(ret) - 1] + 1)
          ranges.pop(0)
      else:
        if tmp[0].isdigit():
          ret.append(int(tmp[0]))
      tmp.pop(0)
  return ret

def guess_main_title() -> int:
  # TODO: Find a real way to determine main title.
  #       Wouldn't it be marked in a mpls or some
  #       file on the disc or in the Java stuff?
  #       Doing this by length of time, or
  #       size + bytes, or whatever is very poor
  
  # time based only
  # longest = [-1, -1]
  # for i in range(len(json_dict["info"])):
  #   if "seconds" in json_dict["info"][i]:
  #     print(json_dict["info"][i]["seconds"], json_dict["info"][i]["title"])
  #     if json_dict["info"][i]["seconds"] > longest[0]:
  #       longest[0] = json_dict["info"][i]["seconds"]
  #       longest[1] = json_dict["info"][i]["title"]
  # if longest[0] > -1:
  #   print(longest[1])
  #
  results = []
  chapters_in_at_least_one_title = False
  for i in range(len(json_dict["info"])):
    tbytes = -1
    tchaps = -1
    tsecs  = -1
    if "title" in json_dict["info"][i]:
      if json_dict["info"][i]["title"] >= 0:
        if "bytes" in json_dict["info"][i]:
          tbytes = json_dict["info"][i]["bytes"]
        if "chapters" in json_dict["info"][i]:
          chapters_in_at_least_one_title = True
          tchaps = json_dict["info"][i]["chapters"]
        if "seconds" in json_dict["info"][i]:
          tsecs = json_dict["info"][i]["seconds"]

        if chapters_in_at_least_one_title:
          results.append([ json_dict["info"][i]["title"], tbytes, tsecs, tchaps ])
        else:
          results.append([ json_dict["info"][i]["title"], tbytes, tsecs ])

  results.sort(key=lambda el: el[1], reverse=True)
  cands = [ -1, -1, -1, -1]
  biggest = [ -1, -1 ] # biggest = [ title number, bytes ]
  for i in results:
    # ([ title number, bytes] if bytes >, new biggest byte value)
    biggest = [ i[0], i[1] ] if i[1] > biggest[1] else biggest
    if chapters_in_at_least_one_title:
      #  more bytes with 1+ chapters    or   bytes equal and more chapters
      if (i[1] > cands[1] and i[3] > 0) or (i[1] == cands[1] and i[3] > cands[3]):
        cands[0] = i[0]
        cands[1] = i[1]
        cands[2] = i[2]
        cands[3] = i[3]
    else:
      #  more bytes      or       bytes equal and longer in seconds
      if i[1] > cands[1] or (i[1] == cands[1] and i[2] > cands[2]):
        cands[0] = i[0]
        cands[1] = i[1]
        cands[2] = i[2]
  
  if cands[0] < 1:
    cands[0] = biggest[0]

  print(cands[0])
  return 0

def get_seconds(timecode: str) -> int:
  # only works with 3 segment string, eg. 0:02:32
  if not isinstance(timecode, str):
    raise TypeError("Supplied timecode was not a string")
  vals = timecode.strip().split(":")
  if len(vals) == 3 and vals[0].isdigit() and vals[1].isdigit() and vals[2].isdigit():
     return (int(vals[0]) * 60 * 60) + (int(vals[1]) * 60) + int(vals[2])
  raise AssertionError("Could not parse seconds from timecode, timecode was:", str(timecode))

def get_title_number(line: str) -> int:
  # eg. TINFO:0,8,0,"2"
  if not isinstance(line, str):
    raise TypeError("Supplied line with title number was not a string, type was:", str(type(line)))
  toks = line.strip().split(",")
  if len(toks) > 3:
    title_num = toks[0].split(":") # get the 0 in TINFO:0,...
    if len(title_num) == 2 and title_num[1].isdigit():
      return int(title_num[1])
  raise AssertionError("Could not determin title number from line, line was:", str(line))

def assign_title(title_num: int) -> int:
  if not isinstance(title_num, int):
    raise TypeError("Supplied title number was not a int, type was:", str(type(title_num)))
  index = None
  for i in range(len(json_dict["info"])):
    if "title" in json_dict["info"][i]:
      if title_num == json_dict["info"][i]["title"]:
        return i
  index = len(json_dict["info"])
  json_dict["info"].append( { "title": title_num } )
  return index

def assign_stream(idx_title: int, stream_num: int) -> int:
  if not isinstance(idx_title, int) and not isinstance(stream_num, int):
    raise TypeError("Supplied title index or stream index were not integers, title index:", str(type(idx_title)), ", stream index:", str(type(stream_num)))
  if "sinfo" in json_dict["info"][idx_title]:
    for i in range(len(json_dict["info"][idx_title]["sinfo"])):
      if json_dict["info"][idx_title]["sinfo"][i]["stream"] == stream_num:
        return i
  else:
    json_dict["info"][idx_title]["sinfo"] = [];
  json_dict["info"][idx_title]["sinfo"].append({
                    "stream": stream_num })
  return (len(json_dict["info"][idx_title]["sinfo"]) - 1)

def msg_parse(line: str) -> int:
  # eg. MSG:1005,0,1,"MakeMKV v1.17.5 linux(x64-release) started","%1 started","MakeMKV v1.17.5 linux(x64-release)"
  if not isinstance(line, str):
    raise TypeError("Supplied msg argument was not a string, type:", str(type(line)))
  toks = clean_string( line.split(",") )
  if not len(toks) >= 5: # at least 5 array elements
    raise AssertionError("Splitting of msg line was not >= 5 tokens, len(toks):", str(len(toks)))
  num = toks[0].split(":") # split "MSG:1005"
  if len(num) != 2:
    raise AssertionError("Parsing of msg number failed because toks[0].split(\":\") didn't result in 2 elements, toks[0]:", str(toks[0]))
  if not num[1].isdigit():
    raise AssertionError("Value for msg number failed isdigit(), value:", str(num[1]))
  num = int(num[1]) # message number 1005
  if num < 0 or num > 65535:
    raise AssertionError("Parsing of message number failed because it was < 0 or > 65535, value:", str(num))

  if num == 3028 and len(toks) == 10:
  # MSG:3028,16777216,3,"Title #5 was added (2 cell(s), 0:01:44)","Title #%1 was added (%2 cell(s), %3)","5","2","0:01:44"
    seconds = get_seconds(toks[9])
    if not seconds >= 0 or not toks[7].isdigit() or not toks[8].isdigit():
      raise AssertionError("Parsing of title seconds, cells, or number failed, seconds >= 0:", str(seconds), ", cells.isdigit():", str(toks[8]), ", number.isdigit():", str(toks[7]))
    json_dict["titles"].append( {
      "seconds": seconds,
        "cells": int(toks[8]),
        "title": int(toks[7])
    })
  elif num == 3307 and len(toks) == 7:
  # MSG:3307,0,2,"File 00241.mpls was added as title #2","File %1 was added as title #%2","00241.mpls","2"
    if not toks[6].isdigit() or not len(toks[5]) > 0:
      raise AssertionError("Parsing of title resulted in an array with too few elements or title number failed .isdigit(): len(toks[5]):", str(len(toks[5])), ", toks[6].isdigit():", str(toks[6]))
    json_dict["titles"].append( {
        "title": int(toks[6]),
         "file": toks[5]
      })
  elif num == 3309 and len(toks) == 7:
  # MSG:3309,16777216,2,"Title 00005.mpls is equal to title 00105.mpls and was skipped","Title %1 is equal to title %2 and was skipped","00005.mpls","00105.mpls"
    if len(toks[6]) > 0 and len(toks[5]) > 0:
      json_dict["dupes"].append( {
        "source": toks[6],
          "dupe": toks[5]
      })
  elif num == 1005 and len(toks) == 6:
  # MSG:1005,0,1,"MakeMKV v1.17.5 linux(x64-release) started","%1 started","MakeMKV v1.17.5 linux(x64-release)"
    json_dict["version"] = ",".join(toks[5:])
  elif num == 1009:
  # MSG:1009,0,1,"Profile parsing error: default profile missing, using builtin default","Profile parsing error: %1","default profile missing, using builtin default"
    if "profile missing" in line:
      json_dict["profile"] = False
  elif num == 1011 and len(toks) == 6:
  # MSG:1011,0,1,"Using LibreDrive mode (v06.3 id=96929CF53B04)","%1","Using LibreDrive mode (v06.3 id=96929CF53B04)"
    json_dict["mode"] = toks[5]
  elif num == 3007 and len(toks) == 5:
  # MSG:3007,0,0,"Using direct disc access mode","Using direct disc access mode"
    if toks[4] == 'Using direct disc access mode':
      json_dict["access"] = "direct"
    else:
      json_dict["access"] = toks[4]
  elif num == 3025:
    # MSG:3025,16777216,3,"Title #00295.m2ts has length of 29 seconds which is less than minimum \
    # title length of 5000 seconds and was therefore skipped","Title #%1 has length of %2 seconds \
    # which is less than minimum title length of %3 seconds and was therefore skipped","00295.m2ts","29","5000"
    json_dict["skipped"].append([toks[-3], line])
  elif num == 5011:
  # MSG:5011,0,0,"Operation successfully completed","Operation successfully completed"
    global OK
    OK = True
  return 0

def cinfo_keyname(num: int) -> str:
  if not isinstance(num, int):
    return ""
  if num == 1:
    return "media"
  if num == 2:
    return "title"
  if num == 28:
    return "lang"
  if num == 29:
    return "language"
  if num == 30:
    return "title_1"
  if num == 31:
    return "p_cinfo_31"
  if num == 32:
    return "volume"
  if num == 33:
    return "cinfo_33"
  if num == 49:
    return "cinfo_49"
  return ""

def cinfo_parse(line: str) -> int:
  # eg. CINFO:1,6209,"Blu-ray disc"
  if not isinstance(line, str):
    raise TypeError("Supplied cinfo line argument was not a string, type was:", str(type(line)))
  toks = clean_string( line.split(",") )
  if not len(toks) >= 3: # at least 3
    raise AssertionError("Spliting of line did not result in >= 3 elements, line.split(\",\"):", str(len(toks)))
  num = toks[0].split(":")
  if len(num) != 2:
    raise AssertionError("Splitting for finding cinfo number != 2 elements, length:", str(len(num)))
  if not num[1].isdigit():
    raise AssertionError("Value for cinfo number failed isdigit(), value:", str(num[1]))

  key = cinfo_keyname(int(num[1]))
  if key:
    temp = clean_string( ",".join(toks[2:]) )
    if key == "media":
      if temp == "Blu-ray disc":
        json_dict["disc"]["p_media"] = temp
        temp = 1
      elif temp == "DVD disc":
        json_dict["disc"]["p_media"] = temp
        temp = 2
    json_dict["disc"][key] = temp
  else:
    json_dict["missed_key_numbers"].append(["cinfo", int(num[1]), line])

  return 0

def tinfo_keyname(num: int) -> str:
  if not isinstance(num, int):
    return ""
  if num == 2:
    return "name"
  elif num == 8:
    return "chapters"
  elif num == 9:
    return "p_timecode"
  elif num == 10:
    return "p_size"
  elif num == 11:
    return "bytes"
  elif num == 16:
    return "file"
  elif num == 24:
    return "tinfo_24"
  elif num == 25:
    return "segment_count"
  elif num == 26:
    return "p_segments"
  elif num == 27:
    return "mkv_file"
  elif num == 28:
    return "lang"
  elif num == 29:
    return "language"
  elif num == 30:
    return "p_summary"
  elif num == 31:
    return "p_tinfo_31"
  elif num == 33:
    return "tinfo_33"
  elif num == 49:
    return "tinfo_49"
  return ""

def tinfo_parse(line: str) -> int:
  # eg. TINFO:0,8,0,"2"
  if not isinstance(line, str):
    raise TypeError("Supplied tinfo line argument was not a string, type was:", str(type(line)))
  title_num = get_title_number(line)
  if title_num < 0:
    raise AssertionError("Failed to parse tinfo title number")
  toks = clean_string( line.split(",") )
  if not len(toks) >= 4 or not toks[1].isdigit():
    raise AssertionError("Array was not >= 4, len(toks):", str(len(toks)))
  if not toks[1].isdigit():
    raise AssertionError("Value for tinfo number failed isdigit(), value:", str(toks[1]))
  
  
  val = ','.join(toks[3:])
  key = tinfo_keyname(int(toks[1])) # toks[1] ie. the 8 in TINFO:0,8,...
  if key:
    if key == "p_timecode": # TODO: change key name to "p_timecode"
      seconds = get_seconds(val)
      if seconds >= 0:
        json_dict["info"][assign_title(title_num)]["seconds"] = seconds
    elif key == "p_segments":
      segments = segments_parse(",".join(toks[3:]))
      if len(segments) > 0:
        json_dict["info"][assign_title(title_num)]["segments"] = segments
    elif key == "segment_count" or key == "bytes" or key == "chapters":
      if val.isdigit():
        val = int(val)
    json_dict["info"][assign_title(title_num)][key] = val
  else:
    json_dict["missed_key_numbers"].append(["tinfo", int(toks[1]), line])

  return 0
  
def sinfo_keyname(num: int) -> str:
  if not isinstance(num, int):
    return ""
  if num == 1:
    return "type"
  elif num == 2:
    return "p_channels_long"
  elif num == 3:
    return "lang"
  elif num == 4:
    return "language"
  elif num == 5:
    return "mime"
  elif num == 6:
    return "p_name_short"
  elif num == 7:
    return "p_name_medium"
  elif num == 13:
    return "p_bitrate"
  elif num == 14:
    return "channels"
  elif num == 17:
    return "sample_rate"
  elif num == 18:
    return "bits_per_sample"
  elif num == 19:
    return "p_resolution"
  elif num == 20:
    return "aspect"
  elif num == 21:
    return "p_framerate"
  elif num == 22:
    return "sinfo_22"
  elif num == 28:
    return "lang_1"
  elif num == 29:
    return "language_1"
  elif num == 30:
    return "p_name_long"
  elif num == 31:
    return "p_sinfo_31"
  elif num == 33:
    return "sinfo_33"
  elif num == 34:
    return "sinfo_34"
  elif num == 38:
    return "sinfo_38"
  elif num == 39:
    return "priority"
  elif num == 40:
    return "p_channels_short"
  elif num == 41:
    return "sinfo_41"
  elif num == 42:
    return "sinfo_42"
  return ""

def sinfo_parse(line: str) -> int:
  # eg. SINFO:0,0,1,6201,"Video"
  if not isinstance(line, str):
    raise TypeError("Supplied sinfo line argument was not a string, type was:", str(type(line)))
  title_num = get_title_number(line)
  if title_num > -1:
    toks = clean_string( line.split(",") )
    if not len(toks) >= 5:
      raise AssertionError("Splitting of sinfo line was not >= 5 elements, length:", str(len(toks)))
    if not toks[1].isdigit() or not toks[2].isdigit():
      raise AssertionError("Value for title number or stream number failed isdigit(), title:", str(toks[1], ", stream:", str(toks[2])))
    
    idx_title  = assign_title(title_num)
    idx_stream = assign_stream(idx_title, int(toks[1]))
    val = ",".join(toks[4:])
    key = sinfo_keyname(int(toks[2]))
    if key:
      if key == "channels" or key == "bits_per_sample" or key == "sample_rate":
        if val.isdigit():
          val = int(val)
      elif key == "p_resolution":
        res = val.split("x")
        if res[0].isdigit() and res[1].isdigit():
          json_dict["info"][idx_title]["sinfo"][idx_stream]["vres"] = int(res[0])
          json_dict["info"][idx_title]["sinfo"][idx_stream]["hres"] = int(res[1])
      elif key == "p_framerate":
        tmp = clean_string( val.split(" ") )
        if tmp[0].replace('.', '', 1).isdigit():
          json_dict["info"][idx_title]["sinfo"][idx_stream]["framerate"] = float(tmp[0])
      json_dict["info"][idx_title]["sinfo"][idx_stream][key] = val
    else:
      json_dict["missed_key_numbers"].append(["sinfo", int(toks[2]), line])

  return 0

def parse_line(line: str, guess_title: bool = False) -> int:
  # eg. MSG:1005,0,1,"MakeMKV v1.17.5 linux(x64-release) started","%1 started","MakeMKV v1.17.5 linux(x64-release)"
  if not isinstance(line, str):
    raise TypeError("Supplied line argument was not a string, type was:", str(type(line)))
  if guess_title:
    if line[:4] == "TINF":
      tinfo_parse(line)
    return 0
  if line[:4] == "SINF":
  # SINFO:0,0,1,6201,"Video"
    sinfo_parse(line)
  elif line[:4] == "TINF":
  # TINFO:0,16,0,"00105.mpls"
    tinfo_parse(line)
  elif line[:4] == "MSG:":
  # MSG:3307,0,2,"File 00241.mpls was added as title #2","File %1 was added as title #%2","00241.mpls","2"
    msg_parse(line)
    json_dict["msg"].append(line)
  elif line[:4] == "DRV:":
  # DRV:0,2,999,12,"BD-RE ASUS BW-16D1HT 3.02 KL2J5FB2412","GLADIATOR_2000","/dev/sr0"
    drive = clean_string( line.split(",") )
    if len(drive) >= 7 and drive[4] and drive[5] and drive[6] \
      and drive[4] != "" and drive[5] != "" and drive[6] != "":
      json_dict["drive"].append( {
          "model": drive[4],
         "volume": drive[5],
          "mount": drive[6]
      })
    json_dict["drv"].append(line)
  elif line[:4] == "CINF":
  # CINFO:1,6209,"Blu-ray disc"
    cinfo_parse(line)
  elif line[:4] == "TCOU":
  # TCOUNT:63
    tcount = line.strip().split(":")
    if len(tcount) == 2 and tcount[1].isdigit():
      json_dict["tcount"] = int(tcount[1])
  else:
    json_dict["dropped"].append(line)
  return 0

def run_proc(command: list, guess_title: bool) -> int:
  if not isinstance(command, list):
    raise TypeError("Supplied command argument was not a list, type was:", str(type(command)))
  if not isinstance(guess_title, bool):
    raise TypeError("Supplied guess title argument was not a bool, type was:", str(type(guess_title)))
  proc = None
  try:
    proc = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, encoding='utf-8', universal_newlines=True);
  except (OSError, IOError) as e:
    raise e

  while proc and proc.stdout.readable():
      line = proc.stdout.readline()
      if not line:
          break
      # exit on any line with errors
      parse_line(line, guess_title)

  proc.wait()
  return proc.returncode

def print_version() -> None:
  print("Version " + str(json_dict["script_version"]) + ", " + str(json_dict["script_version_date"]))

def help() -> None:
  print_version()
  print("JSON from the stdout of 'makemkvcon' using the below:")
  print(" ", json_dict["binary"], " ".join(json_dict["options"]), json_dict["device"])
  print(" -b, --binary  : set a binary, default is path: \"makemkvcon\"")
  print(" -d, --device  : set a device, default is: \"disc:0\"")
  print(" -j, --JSON    : print JSON in 1 line to stdout")
  print(" -p, --pretty  : print JSON line by line to stdout with 2 space indent")
  print("                 * can be followed by a filename to write to, ie. -p \"mkv.json\"")
  print(" -l, --line    : print JSON line by line to stdout without 2 space indent")
  print(" -g, --guess   : guess the main title number, but guesses poorly.")
  print("                 * does _NOT_ support playlist obfustication")
  print("                 it guesses by greatest $byte_size + $most_chapters")
  print("                 if no chapters, guess by $byte_size + $length_in_seconds")
  print("                 if that fails, guess solely by the greatest $byte_size")
  print(" -v, --version : print script version")
  print(" -h, --help    : print this help")

def json_file_write(filename: str, json: str) -> None:
  if not isinstance(filename, str) or not isinstance(json, str):
    raise TypeError("Filename or JSON was not a string, filename:", str(type(filename)), ", JSON:", str(type(json)))
  try:
    with open(filename, "w") as fp:
      fp.write( json );
      fp.flush();
  except (OSError, IOError) as e:
    raise e

def run() -> int:

  if len(sys.argv) > 1:

    binary = None
    if "-b" in sys.argv:
      idx = sys.argv.index("-b")
      binary = sys.argv[idx + 1]
      del sys.argv[idx]
      del sys.argv[idx]
    if "--binary" in sys.argv:
      idx = sys.argv.index("--binary")
      binary = sys.argv[idx + 1]
      del sys.argv[idx]
      del sys.argv[idx]
    if binary:
      json_dict["binary"] = binary
      
    device = None
    if "-d" in sys.argv:
      idx = sys.argv.index("-d")
      device = sys.argv[idx + 1]
      del sys.argv[idx]
      del sys.argv[idx]
    if "--device" in sys.argv:
      idx = sys.argv.index("--device")
      device = sys.argv[idx + 1]
      del sys.argv[idx]
      del sys.argv[idx]
    if device:
      json_dict["device"] = device
    
    if sys.argv[1] == '-v' or sys.argv[1] == "--version":
      print_version()
      return 0
    elif sys.argv[1] == '-h' or sys.argv[1] == "--help":
      help()
      return 0

    if sys.argv[1] not in [ 
      "-g", "--guess",
      "-j", "--json",
      "-p", "--pretty",
      "-l", "--line" ]:
      raise AssertionError("Unrecognized command(s):", str(" ".join(sys.argv)))

    if not json_dict["device"]:
      raise AssertionError("Device argument (-d or --device) is required.")
    if not json_dict["binary"]:
      raise AssertionError("Binary argument (-b or --binary) is required.")
    
    json_dict["options"].insert(0, json_dict["binary"])
    json_dict["options"].append(json_dict["device"])

    if sys.argv[1] == '-g' or sys.argv[1] == "--guess":
      run_proc(json_dict["options"], True)
      guess_main_title()
    elif sys.argv[1] == '-j' or sys.argv[1] == "--json":
      run_proc(json_dict["options"], False)
      print(json.dumps(json_dict, sort_keys=True))
    elif sys.argv[1] == '-p' or sys.argv[1] == "--pretty":
      run_proc(json_dict["options"], False)
      if len(sys.argv) == 2:
        print(json.dumps(json_dict, indent=2, sort_keys=True))
      elif len(sys.argv) == 3:
        json_file_write( sys.argv[2], json.dumps(json_dict, indent=2, sort_keys=True) )
    elif sys.argv[1] == '-l' or sys.argv[1] == "--line":
      run_proc(json_dict["options"], False)
      print(json.dumps(json_dict, indent=0, sort_keys=True))
  else:
    help()
  return 0

def all_exceptions(exctype, value, traceback) -> None:
  print(exctype, value)
  sys.__excepthook__(exctype, value, traceback)
  sys.exit(1)

if __name__ == "__main__":
  sys.excepthook = all_exceptions;
  run()
  sys.exit(0)
  
DVD example:

Code: Select all

{
  "access": "direct",
  "binary": "makemkvcon",
  "device": "disc:0",
  "disc": {
    "cinfo_33": "0",
    "media": 2,
    "p_cinfo_31": "<b>Source information</b><br>",
    "p_media": "DVD disc",
    "title": "WONDERFUL_LIFE",
    "title_1": "WONDERFUL_LIFE",
    "volume": "WONDERFUL_LIFE"
  },
  "drive": [
    {
      "model": "DVD+R-DL HL-DT-ST DVDRW  GX40N RQ00 KZHC6S05805",
      "mount": "/dev/sr0",
      "volume": "WONDERFUL_LIFE"
    }
  ],
  "dropped": [],
  "drv": [
    "DRV:0,2,999,1,\"DVD+R-DL HL-DT-ST DVDRW  GX40N RQ00 KZHC6S05805\",\"WONDERFUL_LIFE\",\"/dev/sr0\"\n",
    "DRV:1,256,999,0,\"\",\"\",\"\"\n",
    "DRV:2,256,999,0,\"\",\"\",\"\"\n",
    "DRV:3,256,999,0,\"\",\"\",\"\"\n",
    "DRV:4,256,999,0,\"\",\"\",\"\"\n",
    "DRV:5,256,999,0,\"\",\"\",\"\"\n",
    "DRV:6,256,999,0,\"\",\"\",\"\"\n",
    "DRV:7,256,999,0,\"\",\"\",\"\"\n",
    "DRV:8,256,999,0,\"\",\"\",\"\"\n",
    "DRV:9,256,999,0,\"\",\"\",\"\"\n",
    "DRV:10,256,999,0,\"\",\"\",\"\"\n",
    "DRV:11,256,999,0,\"\",\"\",\"\"\n",
    "DRV:12,256,999,0,\"\",\"\",\"\"\n",
    "DRV:13,256,999,0,\"\",\"\",\"\"\n",
    "DRV:14,256,999,0,\"\",\"\",\"\"\n",
    "DRV:15,256,999,0,\"\",\"\",\"\"\n"
  ],
  "dupes": [],
  "info": [
    {
      "bytes": 6576545792,
      "chapters": 28,
      "mkv_file": "B1_t00.mkv",
      "p_segments": "1-21,22-46",
      "p_size": "6.1 GB",
      "p_summary": "28 chapter(s),6.1 GB (B1)",
      "p_timecode": "2:10:10",
      "p_tinfo_31": "<b>Title information</b><br>",
      "seconds": 7810,
      "segment_count": 2,
      "segments": [
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8,
        9,
        10,
        11,
        12,
        13,
        14,
        15,
        16,
        17,
        18,
        19,
        20,
        21,
        22,
        23,
        24,
        25,
        26,
        27,
        28,
        29,
        30,
        31,
        32,
        33,
        34,
        35,
        36,
        37,
        38,
        39,
        40,
        41,
        42,
        43,
        44,
        45,
        46
      ],
      "sinfo": [
        {
          "aspect": "4:3",
          "framerate": 29.97,
          "hres": 480,
          "mime": "V_MPEG2",
          "p_bitrate": "9.8 Mb/s",
          "p_framerate": "29.97 (30000/1001)",
          "p_name_long": "Mpeg2",
          "p_name_medium": "Mpeg2",
          "p_name_short": "Mpeg2",
          "p_resolution": "720x480",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "0",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 0,
          "type": "Video",
          "vres": 720
        },
        {
          "channels": 2,
          "lang": "eng",
          "language": "English",
          "mime": "A_AC3",
          "p_bitrate": "192 Kb/s",
          "p_channels_long": "Stereo",
          "p_channels_short": "stereo",
          "p_name_long": "DD Stereo English",
          "p_name_medium": "Dolby Digital",
          "p_name_short": "DD",
          "p_sinfo_31": "<b>Track information</b><br>",
          "priority": "Default",
          "sample_rate": 48000,
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "d",
          "sinfo_42": "( Lossless conversion )",
          "stream": 1,
          "type": "Audio"
        },
        {
          "channels": 2,
          "lang": "fre",
          "language": "French",
          "mime": "A_AC3",
          "p_bitrate": "192 Kb/s",
          "p_channels_long": "Stereo",
          "p_channels_short": "stereo",
          "p_name_long": "DD Stereo French",
          "p_name_medium": "Dolby Digital",
          "p_name_short": "DD",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sample_rate": 48000,
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 2,
          "type": "Audio"
        },
        {
          "lang": "eng",
          "language": "English",
          "mime": "S_VOBSUB",
          "p_name_long": " English",
          "p_name_medium": "Dvd Subtitles",
          "p_name_short": "",
          "p_sinfo_31": "<b>Track information</b><br>",
          "priority": "Default",
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "d",
          "sinfo_42": "( Lossless conversion )",
          "stream": 3,
          "type": "Subtitles"
        },
        {
          "lang": "fre",
          "language": "French",
          "mime": "S_VOBSUB",
          "p_name_long": " French",
          "p_name_medium": "Dvd Subtitles",
          "p_name_short": "",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 4,
          "type": "Subtitles"
        },
        {
          "lang": "eng",
          "language": "English",
          "mime": "S_CC608/DVD",
          "p_bitrate": "9.8 Mb/s",
          "p_name_long": "CC\u2192Text English ( Lossy conversion )",
          "p_name_medium": "Closed Captions",
          "p_name_short": "CC",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_34": "Text subtitles ( Lossy conversion )",
          "sinfo_38": "",
          "sinfo_41": "Text",
          "sinfo_42": "( Lossy conversion )",
          "stream": 5,
          "type": "Subtitles"
        }
      ],
      "tinfo_24": "01",
      "tinfo_33": "0",
      "tinfo_49": "B1",
      "title": 0
    },
    {
      "bytes": 99321856,
      "chapters": 2,
      "mkv_file": "A1_t01.mkv",
      "p_segments": "1-2,3",
      "p_size": "94.7 MB",
      "p_summary": "2 chapter(s),94.7 MB (A1)",
      "p_timecode": "0:02:32",
      "p_tinfo_31": "<b>Title information</b><br>",
      "seconds": 152,
      "segment_count": 2,
      "segments": [
        1,
        2,
        3
      ],
      "sinfo": [
        {
          "aspect": "4:3",
          "framerate": 29.97,
          "hres": 480,
          "mime": "V_MPEG2",
          "p_bitrate": "9.8 Mb/s",
          "p_framerate": "29.97 (30000/1001)",
          "p_name_long": "Mpeg2",
          "p_name_medium": "Mpeg2",
          "p_name_short": "Mpeg2",
          "p_resolution": "720x480",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "0",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 0,
          "type": "Video",
          "vres": 720
        },
        {
          "channels": 2,
          "lang": "eng",
          "language": "English",
          "mime": "A_AC3",
          "p_bitrate": "192 Kb/s",
          "p_channels_long": "Stereo",
          "p_channels_short": "stereo",
          "p_name_long": "DD Stereo English",
          "p_name_medium": "Dolby Digital",
          "p_name_short": "DD",
          "p_sinfo_31": "<b>Track information</b><br>",
          "priority": "Default",
          "sample_rate": 48000,
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "d",
          "sinfo_42": "( Lossless conversion )",
          "stream": 1,
          "type": "Audio"
        }
      ],
      "tinfo_24": "02",
      "tinfo_33": "0",
      "tinfo_49": "A1",
      "title": 1
    },
    {
      "bytes": 914821120,
      "chapters": 2,
      "mkv_file": "C1_t02.mkv",
      "p_segments": "1-3,4",
      "p_size": "872.4 MB",
      "p_summary": "2 chapter(s),872.4 MB (C1)",
      "p_timecode": "0:22:42",
      "p_tinfo_31": "<b>Title information</b><br>",
      "seconds": 1362,
      "segment_count": 2,
      "segments": [
        1,
        2,
        3,
        4
      ],
      "sinfo": [
        {
          "aspect": "4:3",
          "framerate": 29.97,
          "hres": 480,
          "mime": "V_MPEG2",
          "p_bitrate": "9.8 Mb/s",
          "p_framerate": "29.97 (30000/1001)",
          "p_name_long": "Mpeg2",
          "p_name_medium": "Mpeg2",
          "p_name_short": "Mpeg2",
          "p_resolution": "720x480",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "0",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 0,
          "type": "Video",
          "vres": 720
        },
        {
          "channels": 2,
          "lang": "eng",
          "language": "English",
          "mime": "A_AC3",
          "p_bitrate": "192 Kb/s",
          "p_channels_long": "Stereo",
          "p_channels_short": "stereo",
          "p_name_long": "DD Stereo English",
          "p_name_medium": "Dolby Digital",
          "p_name_short": "DD",
          "p_sinfo_31": "<b>Track information</b><br>",
          "priority": "Default",
          "sample_rate": 48000,
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "d",
          "sinfo_42": "( Lossless conversion )",
          "stream": 1,
          "type": "Audio"
        },
        {
          "lang": "eng",
          "language": "English",
          "mime": "S_VOBSUB",
          "p_name_long": " English",
          "p_name_medium": "Dvd Subtitles",
          "p_name_short": "",
          "p_sinfo_31": "<b>Track information</b><br>",
          "priority": "Default",
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "d",
          "sinfo_42": "( Lossless conversion )",
          "stream": 2,
          "type": "Subtitles"
        },
        {
          "lang": "fre",
          "language": "French",
          "mime": "S_VOBSUB",
          "p_name_long": " French",
          "p_name_medium": "Dvd Subtitles",
          "p_name_short": "",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 3,
          "type": "Subtitles"
        },
        {
          "lang": "eng",
          "language": "English",
          "mime": "S_CC608/DVD",
          "p_bitrate": "9.8 Mb/s",
          "p_name_long": "CC\u2192Text English ( Lossy conversion )",
          "p_name_medium": "Closed Captions",
          "p_name_short": "CC",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_34": "Text subtitles ( Lossy conversion )",
          "sinfo_38": "",
          "sinfo_41": "Text",
          "sinfo_42": "( Lossy conversion )",
          "stream": 4,
          "type": "Subtitles"
        }
      ],
      "tinfo_24": "03",
      "tinfo_33": "0",
      "tinfo_49": "C1",
      "title": 2
    },
    {
      "bytes": 569491456,
      "chapters": 2,
      "mkv_file": "C2_t03.mkv",
      "p_segments": "1-2,3",
      "p_size": "543.1 MB",
      "p_summary": "2 chapter(s),543.1 MB (C2)",
      "p_timecode": "0:14:04",
      "p_tinfo_31": "<b>Title information</b><br>",
      "seconds": 844,
      "segment_count": 2,
      "segments": [
        1,
        2,
        3
      ],
      "sinfo": [
        {
          "aspect": "4:3",
          "framerate": 29.97,
          "hres": 480,
          "mime": "V_MPEG2",
          "p_bitrate": "9.8 Mb/s",
          "p_framerate": "29.97 (30000/1001)",
          "p_name_long": "Mpeg2",
          "p_name_medium": "Mpeg2",
          "p_name_short": "Mpeg2",
          "p_resolution": "720x480",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "0",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 0,
          "type": "Video",
          "vres": 720
        },
        {
          "channels": 2,
          "lang": "eng",
          "language": "English",
          "mime": "A_AC3",
          "p_bitrate": "192 Kb/s",
          "p_channels_long": "Stereo",
          "p_channels_short": "stereo",
          "p_name_long": "DD Stereo English",
          "p_name_medium": "Dolby Digital",
          "p_name_short": "DD",
          "p_sinfo_31": "<b>Track information</b><br>",
          "priority": "Default",
          "sample_rate": 48000,
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "d",
          "sinfo_42": "( Lossless conversion )",
          "stream": 1,
          "type": "Audio"
        },
        {
          "lang": "eng",
          "language": "English",
          "mime": "S_VOBSUB",
          "p_name_long": " English",
          "p_name_medium": "Dvd Subtitles",
          "p_name_short": "",
          "p_sinfo_31": "<b>Track information</b><br>",
          "priority": "Default",
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "d",
          "sinfo_42": "( Lossless conversion )",
          "stream": 2,
          "type": "Subtitles"
        },
        {
          "lang": "fre",
          "language": "French",
          "mime": "S_VOBSUB",
          "p_name_long": " French",
          "p_name_medium": "Dvd Subtitles",
          "p_name_short": "",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 3,
          "type": "Subtitles"
        },
        {
          "lang": "eng",
          "language": "English",
          "mime": "S_CC608/DVD",
          "p_bitrate": "9.8 Mb/s",
          "p_name_long": "CC\u2192Text English ( Lossy conversion )",
          "p_name_medium": "Closed Captions",
          "p_name_short": "CC",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_34": "Text subtitles ( Lossy conversion )",
          "sinfo_38": "",
          "sinfo_41": "Text",
          "sinfo_42": "( Lossy conversion )",
          "stream": 4,
          "type": "Subtitles"
        }
      ],
      "tinfo_24": "04",
      "tinfo_33": "0",
      "tinfo_49": "C2",
      "title": 3
    },
    {
      "bytes": 69318656,
      "chapters": 2,
      "mkv_file": "C3_t04.mkv",
      "p_segments": "1,2",
      "p_size": "66.1 MB",
      "p_summary": "2 chapter(s),66.1 MB (C3)",
      "p_timecode": "0:01:44",
      "p_tinfo_31": "<b>Title information</b><br>",
      "seconds": 104,
      "segment_count": 2,
      "segments": [
        1,
        2
      ],
      "sinfo": [
        {
          "aspect": "4:3",
          "framerate": 29.97,
          "hres": 480,
          "mime": "V_MPEG2",
          "p_bitrate": "9.8 Mb/s",
          "p_framerate": "29.97 (30000/1001)",
          "p_name_long": "Mpeg2",
          "p_name_medium": "Mpeg2",
          "p_name_short": "Mpeg2",
          "p_resolution": "720x480",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "0",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 0,
          "type": "Video",
          "vres": 720
        },
        {
          "channels": 2,
          "lang": "eng",
          "language": "English",
          "mime": "A_AC3",
          "p_bitrate": "192 Kb/s",
          "p_channels_long": "Stereo",
          "p_channels_short": "stereo",
          "p_name_long": "DD Stereo English",
          "p_name_medium": "Dolby Digital",
          "p_name_short": "DD",
          "p_sinfo_31": "<b>Track information</b><br>",
          "priority": "Default",
          "sample_rate": 48000,
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "d",
          "sinfo_42": "( Lossless conversion )",
          "stream": 1,
          "type": "Audio"
        }
      ],
      "tinfo_24": "05",
      "tinfo_33": "0",
      "tinfo_49": "C3",
      "title": 4
    },
    {
      "bytes": 99321856,
      "chapters": 2,
      "mkv_file": "C4_t05.mkv",
      "p_segments": "1-2,3",
      "p_size": "94.7 MB",
      "p_summary": "2 chapter(s),94.7 MB (C4)",
      "p_timecode": "0:02:32",
      "p_tinfo_31": "<b>Title information</b><br>",
      "seconds": 152,
      "segment_count": 2,
      "segments": [
        1,
        2,
        3
      ],
      "sinfo": [
        {
          "aspect": "4:3",
          "framerate": 29.97,
          "hres": 480,
          "mime": "V_MPEG2",
          "p_bitrate": "9.8 Mb/s",
          "p_framerate": "29.97 (30000/1001)",
          "p_name_long": "Mpeg2",
          "p_name_medium": "Mpeg2",
          "p_name_short": "Mpeg2",
          "p_resolution": "720x480",
          "p_sinfo_31": "<b>Track information</b><br>",
          "sinfo_22": "0",
          "sinfo_33": "0",
          "sinfo_38": "",
          "sinfo_42": "( Lossless conversion )",
          "stream": 0,
          "type": "Video",
          "vres": 720
        },
        {
          "channels": 2,
          "lang": "eng",
          "language": "English",
          "mime": "A_AC3",
          "p_bitrate": "192 Kb/s",
          "p_channels_long": "Stereo",
          "p_channels_short": "stereo",
          "p_name_long": "DD Stereo English",
          "p_name_medium": "Dolby Digital",
          "p_name_short": "DD",
          "p_sinfo_31": "<b>Track information</b><br>",
          "priority": "Default",
          "sample_rate": 48000,
          "sinfo_22": "0",
          "sinfo_33": "90",
          "sinfo_38": "d",
          "sinfo_42": "( Lossless conversion )",
          "stream": 1,
          "type": "Audio"
        }
      ],
      "tinfo_24": "06",
      "tinfo_33": "0",
      "tinfo_49": "C4",
      "title": 5
    }
  ],
  "missed_key_numbers": [],
  "mode": "",
  "msg": [
    "MSG:1005,0,1,\"MakeMKV v1.17.5 linux(x64-release) started\",\"%1 started\",\"MakeMKV v1.17.5 linux(x64-release)\"\n",
    "MSG:3007,0,0,\"Using direct disc access mode\",\"Using direct disc access mode\"\n",
    "MSG:3028,16777216,3,\"Title #1 was added (46 cell(s), 2:10:10)\",\"Title #%1 was added (%2 cell(s), %3)\",\"1\",\"46\",\"2:10:10\"\n",
    "MSG:3028,0,3,\"Title #2 was added (3 cell(s), 0:02:32)\",\"Title #%1 was added (%2 cell(s), %3)\",\"2\",\"3\",\"0:02:32\"\n",
    "MSG:3028,0,3,\"Title #3 was added (4 cell(s), 0:22:42)\",\"Title #%1 was added (%2 cell(s), %3)\",\"3\",\"4\",\"0:22:42\"\n",
    "MSG:3028,16777216,3,\"Title #4 was added (3 cell(s), 0:14:04)\",\"Title #%1 was added (%2 cell(s), %3)\",\"4\",\"3\",\"0:14:04\"\n",
    "MSG:3028,16777216,3,\"Title #5 was added (2 cell(s), 0:01:44)\",\"Title #%1 was added (%2 cell(s), %3)\",\"5\",\"2\",\"0:01:44\"\n",
    "MSG:3028,0,3,\"Title #6 was added (3 cell(s), 0:02:32)\",\"Title #%1 was added (%2 cell(s), %3)\",\"6\",\"3\",\"0:02:32\"\n",
    "MSG:5011,0,0,\"Operation successfully completed\",\"Operation successfully completed\"\n"
  ],
  "options": [
    "makemkvcon",
    "-r",
    "--minlength=0",
    "--profile=:.mmcp.xml",
    "info",
    "disc:0"
  ],
  "profile": false,
  "script_version": 1,
  "script_version_date": "2023-11-12",
  "skipped": [],
  "tcount": 6,
  "titles": [
    {
      "cells": 46,
      "seconds": 7810,
      "title": 1
    },
    {
      "cells": 3,
      "seconds": 152,
      "title": 2
    },
    {
      "cells": 4,
      "seconds": 1362,
      "title": 3
    },
    {
      "cells": 3,
      "seconds": 844,
      "title": 4
    },
    {
      "cells": 2,
      "seconds": 104,
      "title": 5
    },
    {
      "cells": 3,
      "seconds": 152,
      "title": 6
    }
  ],
  "version": "MakeMKV v1.17.5 linux(x64-release)"
}
The standard Blu-Ray and 4K Blu-Ray examples are too big to post, but they're nearly identical.

.
Post Reply