Skip to content

Instantly share code, notes, and snippets.

@jvuori
Forked from fwenzel/install_makemkv.sh
Last active August 19, 2026 17:27
Show Gist options
  • Select an option

  • Save jvuori/b595b201dbb6bfdbb95d632efaf6474a to your computer and use it in GitHub Desktop.

Select an option

Save jvuori/b595b201dbb6bfdbb95d632efaf6474a to your computer and use it in GitHub Desktop.
A convenient little script to install MakeMKV on Linux + Python wrapper which determines the version number automatically
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.14"
# dependencies = [
# "requests>=2.31",
# ]
# ///
"""Build and install the latest MakeMKV release from source.
Reads the current version and beta key off the official forum (makemkv.com
itself has been intermittently unreachable), then downloads, builds and
installs the "bin" and "oss" packages. If makemkv.com refuses the download,
falls back to the Wayback Machine's archived copy of the same file.
Usage:
uv run install_makemkv.py
uv reads the dependency block above and takes care of the rest.
"""
import argparse
import re
import subprocess
import sys
import tarfile
import tempfile
from pathlib import Path
import requests
FORUM_VERSION_PAGE = "https://forum.makemkv.com/forum/viewtopic.php?f=3&t=224"
FORUM_BETA_KEY_PAGE = "https://forum.makemkv.com/forum/viewtopic.php?f=5&t=1053"
DOWNLOAD_SITE = "https://www.makemkv.com/download"
WAYBACK_CDX_API = "https://web.archive.org/cdx/search/cdx"
CONFIG_FILE = Path.home() / ".MakeMKV" / "settings.conf"
APT_PACKAGES = [
"build-essential",
"pkg-config",
"libc6-dev",
"libssl-dev",
"libexpat1-dev",
"libavcodec-dev",
"libgl1-mesa-dev",
"qtbase5-dev",
"zlib1g-dev",
]
def find_latest_version() -> str:
"""Scrape the forum announcement thread for the current MakeMKV version."""
response = requests.get(FORUM_VERSION_PAGE, timeout=30)
# The server intermittently returns 508 Loop Detected but still serves a
# valid page, so don't treat it as fatal.
if response.status_code != 508:
response.raise_for_status()
match = re.search(
rf'a href="{re.escape(DOWNLOAD_SITE)}/makemkv-bin-(.*?)\.tar\.gz"', response.text
)
if match is None:
raise RuntimeError("No MakeMKV version found on the forum page.")
return match.group(1)
def find_beta_key() -> str:
"""Scrape the forum thread for the current free beta key."""
response = requests.get(FORUM_BETA_KEY_PAGE, timeout=30)
response.raise_for_status()
match = re.search(r"<code>(.*?)</code>", response.text)
if match is None:
raise RuntimeError("No beta key found on the forum page.")
return match.group(1)
def write_beta_key(beta_key: str) -> None:
"""Write the beta key into MakeMKV's settings.conf, replacing any existing key."""
if CONFIG_FILE.exists():
content = CONFIG_FILE.read_text()
new_content = re.sub(r'app_Key = ".*?"', f'app_Key = "{beta_key}"', content)
else:
content = ""
new_content = f'app_Key = "{beta_key}"\n'
if content == new_content:
return
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
CONFIG_FILE.write_text(new_content)
def _stream_download(url: str, dest: Path) -> None:
with requests.get(url, stream=True, timeout=120) as response:
response.raise_for_status()
with dest.open("wb") as fh:
for chunk in response.iter_content(chunk_size=1 << 16):
fh.write(chunk)
def _find_archived_copy(url: str) -> str | None:
"""Find the most recent Wayback Machine snapshot of `url` that actually
captured a successful (HTTP 200) response.
The plain "most recent snapshot" lookup can return a capture taken while
makemkv.com was already down, i.e. a recording of the error page itself
- so this filters the CDX index down to genuinely-successful captures.
"""
response = requests.get(
WAYBACK_CDX_API,
params={"url": url, "output": "json", "filter": "statuscode:200", "limit": -1},
timeout=60,
)
response.raise_for_status()
rows = response.json()
if len(rows) < 2: # first row is just the column header
return None
timestamp = rows[1][1]
# The "if_" modifier makes the Wayback Machine serve the raw file
# instead of an HTML page with an injected toolbar.
return f"https://web.archive.org/web/{timestamp}if_/{url}"
def download_package(name: str, version: str, dest_dir: Path) -> Path:
"""Download a MakeMKV package, falling back to the Wayback Machine on failure."""
filename = f"makemkv-{name}-{version}.tar.gz"
dest = dest_dir / filename
url = f"{DOWNLOAD_SITE}/{filename}"
try:
_stream_download(url, dest)
except requests.RequestException as direct_exc:
print(f"Direct download of {filename} failed ({direct_exc}); trying Wayback Machine.", file=sys.stderr)
try:
archive_url = _find_archived_copy(url)
if archive_url is None:
raise RuntimeError(f"No archived copy of {filename} found.")
_stream_download(archive_url, dest)
except (requests.RequestException, RuntimeError) as archive_exc:
raise RuntimeError(
f"Could not obtain {filename} from makemkv.com or the Wayback Machine."
) from archive_exc
if not tarfile.is_tarfile(dest):
raise RuntimeError(f"Downloaded file {dest} is not a valid tar archive.")
return dest
def extract_package(archive: Path, dest_dir: Path) -> Path:
with tarfile.open(archive) as tar:
top_level_dirs = {Path(member.name).parts[0] for member in tar.getmembers()}
if len(top_level_dirs) != 1:
raise RuntimeError(f"Unexpected archive layout in {archive}.")
tar.extractall(dest_dir)
return dest_dir / top_level_dirs.pop()
def build_and_install(package_dir: Path) -> None:
if (package_dir / "makefile.linux").exists():
# Pre-1.8.6 layout.
subprocess.run(["make", "-f", "makefile.linux"], cwd=package_dir, check=True)
subprocess.run(["sudo", "make", "-f", "makefile.linux", "install"], cwd=package_dir, check=True)
return
# Post-1.8.6 layout.
if (package_dir / "configure").exists():
subprocess.run(["./configure"], cwd=package_dir, check=True)
tmp_dir = package_dir / "tmp"
tmp_dir.mkdir(exist_ok=True)
(tmp_dir / "eula_accepted").write_text("accepted")
subprocess.run(["make"], cwd=package_dir, check=True)
subprocess.run(["sudo", "make", "install"], cwd=package_dir, check=True)
def missing_apt_packages() -> list[str]:
missing = []
for package in APT_PACKAGES:
result = subprocess.run(
["dpkg-query", "-W", "-f=${Status}", package],
capture_output=True,
text=True,
)
if "install ok installed" not in result.stdout:
missing.append(package)
return missing
def check_prerequisites() -> None:
"""Fail fast with install instructions rather than silently invoking sudo."""
missing = missing_apt_packages()
if not missing:
return
print("Missing required packages:", ", ".join(missing), file=sys.stderr)
print("Install them first, then re-run this script:", file=sys.stderr)
print(f" sudo apt-get install -y {' '.join(missing)}", file=sys.stderr)
raise SystemExit(1)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--version",
dest="version",
help="Install this MakeMKV version instead of scraping it off the forum "
"(useful if the forum page itself becomes unreachable).",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
check_prerequisites()
# Cache sudo credentials up front (needed later for `make install`) so
# failures happen before any download/build work is done.
subprocess.run(["sudo", "-v"], check=True)
if args.version:
version = args.version
print(f"Using pinned version: {version}")
else:
print(f"Getting version info from {FORUM_VERSION_PAGE}")
version = find_latest_version()
print(f"Found version: {version}")
print(f"Getting beta key from {FORUM_BETA_KEY_PAGE}")
beta_key = find_beta_key()
print(f"Found beta key: {beta_key}")
with tempfile.TemporaryDirectory(prefix="makemkv-") as tmp:
tmp_dir = Path(tmp)
for package_name in ("bin", "oss"):
print(f"Downloading makemkv-{package_name}-{version}...")
archive = download_package(package_name, version, tmp_dir)
print(f"Extracting {archive.name}...")
package_dir = extract_package(archive, tmp_dir)
print(f"Building and installing {package_dir.name}...")
build_and_install(package_dir)
print(f"Writing beta key to {CONFIG_FILE}")
write_beta_key(beta_key)
print("All done.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment