VideoHelp Forum




+ Reply to Thread
Results 1 to 2 of 2
  1. Hi guys.
    I wasn't able to find a working solution for bunny cdn drm protected videos (used by many course platforms) so i created my own.
    It works for both protected / unprotected videos.
    It uses playwright to emulate firefox browser and N_M3U8DL-RE for download.

    You can pass it a single url of a file with list of urls.


    Code:
    https://github.com/nonab/bunny-cdn-downloader
    Quote Quote  
  2. Originally Posted by monk87 View Post
    Hi guys.
    I wasn't able to find a working solution for bunny cdn drm protected videos (used by many course platforms) so i created my own.
    It works for both protected / unprotected videos.
    It uses playwright to emulate firefox browser and N_M3U8DL-RE for download.

    You can pass it a single url of a file with list of urls.


    Code:
    https://github.com/nonab/bunny-cdn-downloader
    # Vidéo unique
    python bunny_key_grabber.py https://site.com/video-page

    # Fichier local
    python bunny_key_grabber.py urls.txt

    # Fichier distant (GitHub raw, S3, etc.)
    python bunny_key_grabber.py https://raw.githubusercontent.com/user/repo/main/urls.txt

    # Mode impression uniquement
    python bunny_key_grabber.py --print-only https://raw.githubusercontent.com/user/repo/main/urls.txt
    Code:
    import sys
    import re
    import subprocess
    import shutil
    from pathlib import Path
    import requests
    from playwright.sync_api import sync_playwright
    
    
    def load_urls(file):
        path = Path(file)
        if not path.exists():
            print(f"Error: File '{file}' not found.")
            sys.exit(1)
        return [line.strip() for line in path.read_text().splitlines() if line.strip()]
    
    
    def load_urls_from_remote(url):
        try:
            r = requests.get(url, timeout=15)
            r.raise_for_status()
            return [line.strip() for line in r.text.splitlines() if line.strip()]
        except Exception as e:
            print(f"Error loading remote URL list: {e}")
            sys.exit(1)
    
    
    def sanitize_filename(title):
        return re.sub(r'[\\/:"*?<>|]+', '_', title)
    
    
    # ----------------- CLI FLAGS -----------------
    
    print_only_mode = "--print-only" in sys.argv
    if print_only_mode:
        sys.argv.remove("--print-only")
    
    if len(sys.argv) < 2:
        print("Usage: python bunny_key_grabber.py [--print-only] <url | file | remote_txt_url>")
        sys.exit(1)
    
    if not print_only_mode and not shutil.which("N_M3U8DL-RE"):
        print(
            "N_M3U8DL-RE not found.\n"
            "Download: https://github.com/nilaoda/N_m3u8DL-RE/releases\n"
            "Add it to PATH and retry."
        )
        sys.exit(1)
    
    input_arg = sys.argv[1]
    urls = []
    
    # ----------------- INPUT MODE DETECTION -----------------
    
    if input_arg.lower().startswith(("http://", "https://")):
        if input_arg.lower().endswith(".txt"):
            print(f"Detected remote URL list: {input_arg}")
            urls = load_urls_from_remote(input_arg)
        else:
            urls = [input_arg]
    else:
        print(f"Detected local file mode: {input_arg}")
        urls = load_urls(input_arg)
    
    # ----------------- PLAYWRIGHT JS -----------------
    
    get_media_playlist_js = """
    async (masterPlaylistUrl) => {
        const response = await fetch(masterPlaylistUrl);
        const masterPlaylistContent = await response.text();
    
        let mediaPlaylistUrl = null;
        for (const line of masterPlaylistContent.trim().split('\\n')) {
            if (line.trim() && !line.startsWith("#")) {
                mediaPlaylistUrl = new URL(line.trim(), masterPlaylistUrl).href;
                break;
            }
        }
    
        const mediaResponse = await fetch(mediaPlaylistUrl);
        return await mediaResponse.text();
    }
    """
    
    # ----------------- PROCESSING -----------------
    
    commands_list = []
    
    print("\n--- Starting URL Processing ---")
    
    with sync_playwright() as p:
        browser = p.firefox.launch(
            headless=True,
            firefox_user_prefs={"media.volume_scale": "0.0"}
        )
        context = browser.new_context()
    
        for page_url in urls:
            print("\n==============================")
            print("Processing:", page_url)
    
            page = context.new_page()
            state = {"hex_key": None, "playlist_url": None, "title": None}
    
            def on_response(response):
                if "b-cdn.net" in response.url and "/key/" in response.url:
                    try:
                        state["hex_key"] = response.body().hex()
                    except Exception:
                        pass
    
            def on_request(request):
                if "b-cdn.net" in request.url and request.url.endswith("playlist.m3u8"):
                    state["playlist_url"] = request.url
    
            page.on("response", on_response)
            page.on("request", on_request)
    
            try:
                page.goto(
                    page_url,
                    wait_until="domcontentloaded",
                    timeout=60000,
                    referer="https://iframe.mediadelivery.net/"
                )
                title = page.title()
                if title:
                    state["title"] = sanitize_filename(title)
            except Exception as e:
                print(f"Error loading page: {e}")
                page.close()
                continue
    
            max_wait, elapsed = 15, 0
            while not state["playlist_url"] and elapsed < max_wait:
                page.wait_for_timeout(500)
                elapsed += 0.5
    
            if not state["playlist_url"]:
                print("No master playlist found.")
                page.close()
                continue
    
            try:
                print("Master playlist found, checking DRM...")
                media_playlist_content = page.evaluate(
                    get_media_playlist_js, state["playlist_url"]
                )
            except Exception as e:
                print(f"Failed to read playlist: {e}")
                page.close()
                continue
    
            is_drm = "#EXT-X-KEY" in media_playlist_content
            cmd_parts = ["N_M3U8DL-RE", "-sv", "best"]
    
            if state["title"]:
                cmd_parts.append(f'--save-name "{state["title"]}"')
    
            if is_drm:
                print("DRM detected, waiting for key...")
                key_wait, waited = 5, 0
                while not state["hex_key"] and waited < key_wait:
                    page.wait_for_timeout(500)
                    waited += 0.5
    
                if not state["hex_key"]:
                    print("Failed to capture DRM key.")
                    page.close()
                    continue
    
                print("Encryption key captured.")
                cmd_parts.append(f'--custom-hls-key "{state["hex_key"]}"')
    
            cmd_parts.append(f'"{state["playlist_url"]}"')
            cmd_parts.append('--header "Referer: https://iframe.mediadelivery.net/"')
    
            cmd = " ".join(cmd_parts)
            commands_list.append(cmd)
            print("Command generated.")
    
            page.close()
    
        browser.close()
    
    print("\n--- URL Processing Finished ---")
    
    # ----------------- EXECUTION -----------------
    
    if not commands_list:
        print("No downloadable videos found.")
        sys.exit(0)
    
    print("\n==============================")
    
    if print_only_mode:
        print("--- Print Only Mode ---")
        for c in commands_list:
            print(c)
    else:
        print(f"Found {len(commands_list)} video(s).")
        for i, c in enumerate(commands_list, 1):
            print(f"\n--- Download {i}/{len(commands_list)} ---")
            print(c)
            try:
                subprocess.run(c, shell=True, check=True)
                print("Download completed.")
            except subprocess.CalledProcessError as e:
                print(f"Download error: {e}")
    Quote Quote  



Similar Threads

Visit our sponsor! Try DVDFab and backup Blu-rays!