VideoHelp Forum
+ Reply to Thread
Results 1 to 22 of 22
Thread
  1. Psychology student CrymanChen's Avatar
    Join Date
    Apr 2022
    Location
    Mainland China
    Search Comp PM
    Hi folks,
    rencently i've been watching this podcast from Spotify
    Code:
    https://open.spotify.com/episode/0EBTCiYZl535zGYjbC6LXo?si=sNytZqqnTjes6MKP1wrSpA&nd=1
    Image
    [Attachment 67013 - Click to enlarge]


    It is protected, but I couldn't find any mpd or m3u8 file.
    Could you please help me find the so-called playback file? It must have a list in which each video segment is listed.
    Thank you~
    twitter @Cryman_Chen
    email crymanchen@gmail.com
    Quote Quote  
  2. Psychology student CrymanChen's Avatar
    Join Date
    Apr 2022
    Location
    Mainland China
    Search Comp PM
    Interestingly, I've got the key but I don't know how to download this.
    twitter @Cryman_Chen
    email crymanchen@gmail.com
    Quote Quote  
  3. I couldn't find an mpd or m3u8 either, few months ago when I needed, so I resorted to sort of a manual way of downloading the init, the chunks and then using type command in Windows to concatenate them.
    Quote Quote  
  4. Originally Posted by CrymanChen View Post
    Interestingly, I've got the key but I don't know how to download this.
    How'd you get the key?

    I couldn't find the PSSH... or the MPD. Loads of webm files that ffmpeg doesn't like.
    Quote Quote  
  5. Psychology student CrymanChen's Avatar
    Join Date
    Apr 2022
    Location
    Mainland China
    Search Comp PM
    Originally Posted by genericeddie View Post

    How'd you get the key?

    I couldn't find the PSSH... or the MPD. Loads of webm files that ffmpeg doesn't like.
    Spotify is pretty easy, at least easier than those such as YouTube, NowE, etc.
    twitter @Cryman_Chen
    email crymanchen@gmail.com
    Quote Quote  
  6. The webm podcasts are more tricky than the music tracks.

    You can get the pssh by searching for 'drm' in the network tab and finding the pssh in the response (choose the widevine one). I suspect that response also describes the step size for the webm parts to download and the podcast duration.

    The webm parts are numbered something like (this is from memory and probably not completely accurate, apart from the numbers):
    https://****0web.webm*****
    https://****4web.webm*****
    https://****8web.webm*****

    I guess that's a step size of 4.

    Someone who knows how to use yt-dlp properly will probably know how to download all the parts with a single yt-dlp command, otherwise a script might be needed.

    However...
    1. I got an error using N_m3u8DL-RE with the keys and the webm urls (I didn't try hard, I could have just made a mistake on the command line, so this might still be the right way to go for webm)
    2. yt-dlp downloaded the webm sections I asked it to without any issues, but mp4decrypt probably isn't the right tool for decrypting them, because it didn't decrypt them successfully.

    Summary: the webm podcasts are more tricky than the music tracks.
    Quote Quote  
  7. Psychology student CrymanChen's Avatar
    Join Date
    Apr 2022
    Location
    Mainland China
    Search Comp PM
    By the way, just now I checked that link I posted in last September, and found that "We couldn't find the page you want".

    Is it because I changed my account region (US -> Pakistan) so Podcast videos are not available in the current location, or is it because that video is not availavle anymore??
    Image
    [Attachment 70636 - Click to enlarge]
    twitter @Cryman_Chen
    email crymanchen@gmail.com
    Quote Quote  
  8. Originally Posted by genericeddie View Post
    Originally Posted by CrymanChen View Post
    Interestingly, I've got the key but I don't know how to download this.
    How'd you get the key?

    I couldn't find the PSSH... or the MPD. Loads of webm files that ffmpeg doesn't like.
    There is no MPD with Spotify, and the PSSH is found inside 'supports_drm' or with EME logger extension. In 'supports_drm', you will also find a manifest of sorts where you can see all quality profiles. Not all podcast videos are encrypted, though, so the PSSH might be missing for a reason.

    As for avoiding the thousands of files, I found that the best way is to use something like wget or curl to feed all the tiny files directly into two big video.mp4 and audio.mp4 and then merge it. FFmpeg do not like it when there are literally thousands of tiny files. This will also make it easier to use mp4decrypt on those videos that need it.
    Quote Quote  
  9. Member
    Join Date
    Jun 2023
    Location
    Manchester
    Search PM
    I checked the link to the podcast you shared, and it appears to be a protected episode on Spotify. Unfortunately, finding a specific playback file or video clips may not be possible because Spotify uses its own streaming infrastructure. However, if you're looking for ways to access the content, I'd suggest contacting the creator of the podcast or Spotify support for help. They may be able to offer alternatives or clarify any restrictions on accessing content. Or you could try buying Spotify promo packages at songlifty.com to access quality broadcasts. I hope you find a solution, and happy listening to podcasts!
    Last edited by iannzhjab; 13th Jun 2023 at 03:03.
    Quote Quote  
  10. Feels Good Man 2nHxWW6GkN1l916N3ayz8HQoi's Avatar
    Join Date
    Jan 2024
    Location
    Pepe Island
    Search Comp PM
    Here is the downloader for Spotify Video Podcasts. Only free podcasts. Email and password are needed as well as CDM in wvd format (only for DRM content).

    Code:
    import base64
    import json
    import os
    from urllib.parse import urlparse
    
    import requests
    from librespot import util
    from librespot.core import Session
    from librespot.metadata import EpisodeId
    from pywidevine.cdm import Cdm
    from pywidevine.device import Device
    from pywidevine.pssh import PSSH
    
    EMAIL = "YOUR_EMAIL"
    PASSWORD = "YOUR_PASSWORD"
    WVD_FILE = "device_wvd_file.wvd"
    OUTPUT_FOLDER = os.path.join(".", "spotify_output")
    
    APP_RESOLVE_URL = 'https://apresolve.spotify.com/'
    MIME_INCLUDE = ["video/mp4", "audio/mp4"]
    
    
    def get_spotify_base_url():
        t = "spclient"
        return json.loads(requests.get(
            APP_RESOLVE_URL, params={'type': [t]}
        ).content.decode())[t][0].split(":")[0]
    
    
    BASE_URL = get_spotify_base_url()
    MANIFEST_URL = 'https://' + BASE_URL + '/manifests/v7/json/sources/{manifest_id}/options/supports_drm'
    LICENSE_URL = f'https://{BASE_URL}/widevine-license/v1/video/license'
    
    
    def get_access_token():
        session = Session.Builder().user_pass(
            EMAIL, PASSWORD
        ).create()
        return session, session.tokens().get("playlist-read")
    
    
    SESSION, ACCESS_TOKEN = get_access_token()
    
    
    def get_pssh_from_init(init_url):
        content = requests.get(init_url).content
        offsets = []
        offset = 0
    
        while True:
            offset = content.find(b'pssh', offset)
            if offset == -1:
                break
    
            size = int.from_bytes(content[offset - 4:offset], byteorder='big')
            pssh_offset = offset - 4
    
            offsets.append(content[pssh_offset:pssh_offset + size])
            offset += size
    
        pssh_list = [base64.b64encode(wv_offset).decode() for wv_offset in offsets]
        for pssh in pssh_list:
            if 70 < len(pssh) < 190:
                return pssh
        return None
    
    
    def get_keys(pssh_value):
        if pssh_value is None:
            return []
        try:
            device = Device.load(WVD_FILE)
        except:
            return []
    
        pssh_value = PSSH(pssh_value)
        cdm = Cdm.from_device(device)
        cdm_session_id = cdm.open()
    
        challenge = cdm.get_license_challenge(cdm_session_id, pssh_value)
        licence = requests.post(
            LICENSE_URL, data=challenge,
            headers={'authorization': f'Bearer {ACCESS_TOKEN}'}
        )
        licence.raise_for_status()
        cdm.parse_license(cdm_session_id, licence.content)
    
        keys = []
        for key in cdm.get_keys(cdm_session_id):
            if "CONTENT" in key.type:
                keys += [f"{key.kid.hex}:{key.key.hex()}"]
        cdm.close(cdm_session_id)
        return keys
    
    
    def get_base_url(base_urls, test_url):
        for base_url in base_urls:
            if 200 <= requests.get(f"{base_url}{test_url}").status_code < 300:
                return base_url
    
    
    def generate_segm_m3u8(output_path, content, profile, manifest):
        init_url = manifest["initialization_template"]
        init_url = init_url.replace("{{profile_id}}", str(profile["id"]))
        init_url = init_url.replace("{{file_type}}", str(profile["file_type"]))
        segm_url = manifest["segment_template"]
        segm_url = segm_url.replace("{{profile_id}}", str(profile["id"]))
        segm_url = segm_url.replace("{{file_type}}", str(profile["file_type"]))
    
        base_url = get_base_url(manifest["base_urls"], init_url)
        if base_url is None:
            return None
        init_url = f"{base_url}{init_url}"
        segm_url = f"{base_url}{segm_url}"
    
        segm_incr = content["segment_length"]
        last_segm = int(content["end_time_millis"] / 1000)
        m3u8_content = "#EXTM3U\n#EXT-X-VERSION:3\n\n"
        m3u8_content += f"#EXT-X-MAP:URI=\"{init_url}\"\n"
    
        for i in range(0, last_segm, segm_incr):
            segment = segm_url.replace("{{segment_timestamp}}", str(i))
            m3u8_content += f"#EXTINF:{segm_incr:.3f},\n{segment}\n"
    
        m3u8_content += "#EXT-X-ENDLIST\n"
        with open(output_path, "w") as f:
            f.write(m3u8_content)
        return init_url
    
    
    def generate_master_m3u8(name, manifest):
        output_path = os.path.join(OUTPUT_FOLDER, name)
        if not os.path.exists(output_path):
            os.makedirs(output_path)
    
        m3u8_content = "#EXTM3U\n#EXT-X-VERSION:3\n\n"
        pssh = None
    
        for content in manifest["contents"]:
            max_res = max([p["video_resolution"] for p in content["profiles"] if "video_bitrate" in p])
            for profile in content["profiles"]:
                p_id = profile["id"]
                mime_type = profile["mime_type"]
                if mime_type not in MIME_INCLUDE:
                    star = True
                    for check in ["video", "audio"]:
                        if f"{check}/*" not in MIME_INCLUDE and f"{check}/" in mime_type:
                            star = False
                            break
                    if not star:
                        continue
    
                title = f"video_{p_id}.m3u8" if "video_bitrate" in profile else f"audio_{p_id}.m3u8"
                init_url = generate_segm_m3u8(os.path.join(output_path, title), content, profile, manifest)
                if init_url is None:
                    continue
    
                if "video_bitrate" in profile:
                    if profile["video_resolution"] == max_res and pssh is None:
                        pssh = get_pssh_from_init(init_url)
    
                    video_bitrate = profile["video_bitrate"]
                    video_width = profile["video_width"]
                    video_height = profile["video_height"]
                    video_codec = profile["video_codec"]
    
                    m3u8_content += f"#EXT-X-STREAM-INF:BANDWIDTH={video_bitrate},RESOLUTION={video_width}x{video_height},CODECS=\"{video_codec}\",TYPE=VIDEO,MIME-TYPE=\"{mime_type}\",AUDIO=\"Audio\"\n"
                    m3u8_content += f"{title}\n"
                elif "audio_bitrate" in profile:
                    audio_bitrate = profile["audio_bitrate"]
                    audio_codec = profile["audio_codec"]
    
                    m3u8_content += f'#EXT-X-MEDIA:TYPE=AUDIO,AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",GROUP-ID="Audio",URI="{title}\"\n'
                    # m3u8_content += f"#EXT-X-STREAM-INF:BANDWIDTH={audio_bitrate},CODECS=\"{audio_codec}\",TYPE=AUDIO,MIME-TYPE=\"{mime_type}\"\n"
                    # m3u8_content += f"{title}\n"
    
        for srt_code in manifest.get("subtitle_language_codes", []):
            srt_url = manifest["subtitle_template"].replace("{{language_code}}", srt_code)
            base_url = get_base_url(manifest["subtitle_base_urls"], srt_url)
            srt_url = f"{base_url}{srt_url}"
    
            _, srt_ext = os.path.splitext(os.path.basename(urlparse(srt_url).path))
            srt_path = os.path.join(output_path, f"subtitle_{srt_code}{srt_ext}")
            with open(srt_path, 'w') as f:
                f.write(requests.get(srt_url).content.decode())
    
        output_path = os.path.join(output_path, "master.m3u8")
        with open(output_path, "w") as f:
            f.write(m3u8_content)
        return output_path, pssh
    
    
    def get_video_data(episode_id):
        content_uri = f'spotify:episode:{episode_id}'
        episode = EpisodeId.from_uri(content_uri)
        manifest_id = util.bytes_to_hex(
            SESSION.api().get_metadata_4_episode(episode).video[0].file_id
        )
    
        manifest = json.loads(requests.get(
            MANIFEST_URL.format(manifest_id=manifest_id),
            headers={'authorization': f'Bearer {ACCESS_TOKEN}'}
        ).content.decode())
        return generate_master_m3u8(episode_id, manifest)
    
    
    def get_download_command(source_url):
        name = source_url.split("/")[-1]
        download_path, pssh = get_video_data(name)
        keys = get_keys(pssh)
    
        if len(keys) == 0:
            if pssh is not None:
                return f"Need local CDM (in WVD format) for {name}"
            return f'N_m3u8DL-RE.exe "{download_path}" -M format=mkv --save-name "{name}"'
        return f'N_m3u8DL-RE.exe "{download_path}" {" ".join([f"--key {k}" for k in keys])} -M format=mkv --save-name "{name}"'
    
    
    SOURCE_URLS = [
        "https://open.spotify.com/episode/4dbjxVECWt1p2CcGrs7Wen",
        "https://open.spotify.com/episode/0OzuSfwPTCoPcjhnGsXOJa",
        "https://open.spotify.com/episode/3ppOLVKTWPsZeTk6r0l9Gg",
        "https://open.spotify.com/episode/0EBTCiYZl535zGYjbC6LXo",
    ]
    
    for s in SOURCE_URLS:
        print(get_download_command(s))
    Output:
    Code:
    N_m3u8DL-RE.exe ".\spotify_output\4dbjxVECWt1p2CcGrs7Wen\master.m3u8" -M format=mkv --save-name "4dbjxVECWt1p2CcGrs7Wen"
    N_m3u8DL-RE.exe ".\spotify_output\0OzuSfwPTCoPcjhnGsXOJa\master.m3u8" -M format=mkv --save-name "0OzuSfwPTCoPcjhnGsXOJa"
    N_m3u8DL-RE.exe ".\spotify_output\3ppOLVKTWPsZeTk6r0l9Gg\master.m3u8" --key 2dc54356f02f0aad15b5837eac4bad8b:6f08b5c8499e8f9328e5ca8266d7c949 -M format=mkv --save-name "3ppOLVKTWPsZeTk6r0l9Gg"
    N_m3u8DL-RE.exe ".\spotify_output\0EBTCiYZl535zGYjbC6LXo\master.m3u8" --key 690e36993c307d3cbec9b7863a6304ff:4b6c623daa65807ce0bbb31db8417b95 -M format=mkv --save-name "0EBTCiYZl535zGYjbC6LXo"
    Image
    [Attachment 78167 - Click to enlarge]


    Image
    [Attachment 78020 - Click to enlarge]


    It is meant only for podcasts that are videos with or without DRM. Audio podcasts do not work with this script. Initially, I wanted to add support for them as well, but this has already been done and there are already existing tools for this type of content (DRM isn't even relevant for the audio ones because it can be skipped). But I haven't seen tools for video. If any subtitles are found, those are downloaded as well in the m3u8 folder and can be remuxed later manually.

    An interesting problem was encountered. There is no m3u8 or MPD for this content. Spotify API returns a JSON that contains the necessary information to build one, but no m3u8/mpd. So the easy solution was to build an m3u8 and use existing tools on it. The generated m3u8 may show some weird information about bitrate, but at least the resolution is perfect and you can deduce which one is audio/video. Be sure to download it right away because the URL fragments contain query parameters that can expire. I've chosen m3u8 because it is easier to generate it by following a text based specification, rather than XML. Don't forget to delete the output folder at the end.

    Edit: Changed the m3u8 to properly separate the audio and video in the N_m3u8 media selection. Thanks @snake for explaining the m3u8 format to achieve this.

    Edit2: Modified the m3u8 format to auto select the audio track using n_m3u8. Thanks again @snake
    Last edited by 2nHxWW6GkN1l916N3ayz8HQoi; 6th Apr 2024 at 04:45.
    Quote Quote  
  11. Originally Posted by 2nHxWW6GkN1l916N3ayz8HQoi View Post
    Here is the downloader for Spotify Video Podcasts. Only free podcasts. Email and password are needed as well as CDM in wvd format (only for DRM content).

    Code:
    import base64
    import json
    import os
    from urllib.parse import urlparse
    
    import requests
    from librespot import util
    from librespot.core import Session
    from librespot.metadata import EpisodeId
    from pywidevine.cdm import Cdm
    from pywidevine.device import Device
    from pywidevine.pssh import PSSH
    
    EMAIL = "YOUR_EMAIL"
    PASSWORD = "YOUR_PASSWORD"
    WVD_FILE = "device_wvd_file.wvd"
    OUTPUT_FOLDER = os.path.join(".", "spotify_output")
    
    APP_RESOLVE_URL = 'https://apresolve.spotify.com/'
    MIME_INCLUDE = ["video/mp4", "audio/mp4"]
    
    
    def get_spotify_base_url():
        t = "spclient"
        return json.loads(requests.get(
            APP_RESOLVE_URL, params={'type': [t]}
        ).content.decode())[t][0].split(":")[0]
    
    
    BASE_URL = get_spotify_base_url()
    MANIFEST_URL = 'https://' + BASE_URL + '/manifests/v7/json/sources/{manifest_id}/options/supports_drm'
    LICENSE_URL = f'https://{BASE_URL}/widevine-license/v1/video/license'
    
    
    def get_access_token():
        session = Session.Builder().user_pass(
            EMAIL, PASSWORD
        ).create()
        return session, session.tokens().get("playlist-read")
    
    
    SESSION, ACCESS_TOKEN = get_access_token()
    
    
    def get_pssh_from_init(init_url):
        content = requests.get(init_url).content
        offsets = []
        offset = 0
    
        while True:
            offset = content.find(b'pssh', offset)
            if offset == -1:
                break
    
            size = int.from_bytes(content[offset - 4:offset], byteorder='big')
            pssh_offset = offset - 4
    
            offsets.append(content[pssh_offset:pssh_offset + size])
            offset += size
    
        pssh_list = [base64.b64encode(wv_offset).decode() for wv_offset in offsets]
        for pssh in pssh_list:
            if 70 < len(pssh) < 190:
                return pssh
        return None
    
    
    def get_keys(pssh_value):
        if pssh_value is None:
            return []
        try:
            device = Device.load(WVD_FILE)
        except:
            return []
    
        pssh_value = PSSH(pssh_value)
        cdm = Cdm.from_device(device)
        cdm_session_id = cdm.open()
    
        challenge = cdm.get_license_challenge(cdm_session_id, pssh_value)
        licence = requests.post(
            LICENSE_URL, data=challenge,
            headers={'authorization': f'Bearer {ACCESS_TOKEN}'}
        )
        licence.raise_for_status()
        cdm.parse_license(cdm_session_id, licence.content)
    
        keys = []
        for key in cdm.get_keys(cdm_session_id):
            if "CONTENT" in key.type:
                keys += [f"{key.kid.hex}:{key.key.hex()}"]
        cdm.close(cdm_session_id)
        return keys
    
    
    def get_base_url(base_urls, test_url):
        for base_url in base_urls:
            if 200 <= requests.get(f"{base_url}{test_url}").status_code < 300:
                return base_url
    
    
    def generate_segm_m3u8(output_path, content, profile, manifest):
        init_url = manifest["initialization_template"]
        init_url = init_url.replace("{{profile_id}}", str(profile["id"]))
        init_url = init_url.replace("{{file_type}}", str(profile["file_type"]))
        segm_url = manifest["segment_template"]
        segm_url = segm_url.replace("{{profile_id}}", str(profile["id"]))
        segm_url = segm_url.replace("{{file_type}}", str(profile["file_type"]))
    
        base_url = get_base_url(manifest["base_urls"], init_url)
        if base_url is None:
            return None
        init_url = f"{base_url}{init_url}"
        segm_url = f"{base_url}{segm_url}"
    
        segm_incr = content["segment_length"]
        last_segm = int(content["end_time_millis"] / 1000)
        m3u8_content = "#EXTM3U\n#EXT-X-VERSION:3\n\n"
        m3u8_content += f"#EXT-X-MAP:URI=\"{init_url}\"\n"
    
        for i in range(0, last_segm, segm_incr):
            segment = segm_url.replace("{{segment_timestamp}}", str(i))
            m3u8_content += f"#EXTINF:{segm_incr:.3f},\n{segment}\n"
    
        m3u8_content += "#EXT-X-ENDLIST\n"
        with open(output_path, "w") as f:
            f.write(m3u8_content)
        return init_url
    
    
    def generate_master_m3u8(name, manifest):
        output_path = os.path.join(OUTPUT_FOLDER, name)
        if not os.path.exists(output_path):
            os.makedirs(output_path)
    
        m3u8_content = "#EXTM3U\n#EXT-X-VERSION:3\n\n"
        pssh = None
    
        for content in manifest["contents"]:
            max_res = max([p["video_resolution"] for p in content["profiles"] if "video_bitrate" in p])
            for profile in content["profiles"]:
                p_id = profile["id"]
                mime_type = profile["mime_type"]
                if mime_type not in MIME_INCLUDE:
                    star = True
                    for check in ["video", "audio"]:
                        if f"{check}/*" not in MIME_INCLUDE and f"{check}/" in mime_type:
                            star = False
                            break
                    if not star:
                        continue
    
                title = f"video_{p_id}.m3u8" if "video_bitrate" in profile else f"audio_{p_id}.m3u8"
                init_url = generate_segm_m3u8(os.path.join(output_path, title), content, profile, manifest)
                if init_url is None:
                    continue
    
                if "video_bitrate" in profile:
                    if profile["video_resolution"] == max_res and pssh is None:
                        pssh = get_pssh_from_init(init_url)
    
                    video_bitrate = profile["video_bitrate"]
                    video_width = profile["video_width"]
                    video_height = profile["video_height"]
                    video_codec = profile["video_codec"]
    
                    m3u8_content += f"#EXT-X-STREAM-INF:BANDWIDTH={video_bitrate},RESOLUTION={video_width}x{video_height},CODECS=\"{video_codec}\",TYPE=VIDEO,MIME-TYPE=\"{mime_type}\"\n"
                    m3u8_content += f"{title}\n"
                elif "audio_bitrate" in profile:
                    audio_bitrate = profile["audio_bitrate"]
                    audio_codec = profile["audio_codec"]
    
                    m3u8_content += f"#EXT-X-STREAM-INF:BANDWIDTH={audio_bitrate},CODECS=\"{audio_codec}\",TYPE=AUDIO,MIME-TYPE=\"{mime_type}\"\n"
                    m3u8_content += f"{title}\n"
    
        for srt_code in manifest.get("subtitle_language_codes", []):
            srt_url = manifest["subtitle_template"].replace("{{language_code}}", srt_code)
            base_url = get_base_url(manifest["subtitle_base_urls"], srt_url)
            srt_url = f"{base_url}{srt_url}"
    
            _, srt_ext = os.path.splitext(os.path.basename(urlparse(srt_url).path))
            srt_path = os.path.join(output_path, f"subtitle_{srt_code}{srt_ext}")
            with open(srt_path, 'w') as f:
                f.write(requests.get(srt_url).content.decode())
    
        output_path = os.path.join(output_path, "master.m3u8")
        with open(output_path, "w") as f:
            f.write(m3u8_content)
        return output_path, pssh
    
    
    def get_video_data(episode_id):
        content_uri = f'spotify:episode:{episode_id}'
        episode = EpisodeId.from_uri(content_uri)
        manifest_id = util.bytes_to_hex(
            SESSION.api().get_metadata_4_episode(episode).video[0].file_id
        )
    
        manifest = json.loads(requests.get(
            MANIFEST_URL.format(manifest_id=manifest_id),
            headers={'authorization': f'Bearer {ACCESS_TOKEN}'}
        ).content.decode())
        return generate_master_m3u8(episode_id, manifest)
    
    
    def get_download_command(source_url):
        name = source_url.split("/")[-1]
        download_path, pssh = get_video_data(name)
        keys = get_keys(pssh)
    
        if len(keys) == 0:
            if pssh is not None:
                return f"Need local CDM (in WVD format) for {name}"
            return f'N_m3u8DL-RE.exe "{download_path}" -M format=mkv --save-name "{name}"'
        return f'N_m3u8DL-RE.exe "{download_path}" {" ".join([f"--key {k}" for k in keys])} -M format=mkv --save-name "{name}"'
    
    
    SOURCE_URLS = [
        "https://open.spotify.com/episode/4dbjxVECWt1p2CcGrs7Wen",
        "https://open.spotify.com/episode/0OzuSfwPTCoPcjhnGsXOJa",
        "https://open.spotify.com/episode/3ppOLVKTWPsZeTk6r0l9Gg",
        "https://open.spotify.com/episode/0EBTCiYZl535zGYjbC6LXo",
    ]
    
    for s in SOURCE_URLS:
        print(get_download_command(s))
    Output:
    Code:
    N_m3u8DL-RE.exe ".\spotify_output\4dbjxVECWt1p2CcGrs7Wen\master.m3u8" -M format=mkv --save-name "4dbjxVECWt1p2CcGrs7Wen"
    N_m3u8DL-RE.exe ".\spotify_output\0OzuSfwPTCoPcjhnGsXOJa\master.m3u8" -M format=mkv --save-name "0OzuSfwPTCoPcjhnGsXOJa"
    N_m3u8DL-RE.exe ".\spotify_output\3ppOLVKTWPsZeTk6r0l9Gg\master.m3u8" --key 2dc54356f02f0aad15b5837eac4bad8b:6f08b5c8499e8f9328e5ca8266d7c949 -M format=mkv --save-name "3ppOLVKTWPsZeTk6r0l9Gg"
    N_m3u8DL-RE.exe ".\spotify_output\0EBTCiYZl535zGYjbC6LXo\master.m3u8" --key 690e36993c307d3cbec9b7863a6304ff:4b6c623daa65807ce0bbb31db8417b95 -M format=mkv --save-name "0EBTCiYZl535zGYjbC6LXo"
    Image
    [Attachment 78019 - Click to enlarge]


    Image
    [Attachment 78020 - Click to enlarge]


    It is meant only for podcasts that are videos with or without DRM. Audio podcasts do not work with this script. Initially, I wanted to add support for them as well, but this has already been done and there are already existing tools for this type of content (DRM isn't even relevant for the audio ones because it can be skipped). But I haven't seen tools for video. If any subtitles are found, those are downloaded as well in the m3u8 folder and can be remuxed later manually.

    An interesting problem was encountered. There is no m3u8 or MPD for this content. Spotify API returns a JSON that contains the necessary information to build one, but no m3u8/mpd. So the easy solution was to build an m3u8 and use existing tools on it. The generated m3u8 may show some weird information about bitrate, but at least the resolution is perfect and you can deduce which one is audio/video. Be sure to download it right away because the URL fragments contain query parameters that can expire. I've chosen m3u8 because it is easier to generate it by following a text based specification, rather than XML. Don't forget to delete the output folder at the end.
    great work thanks
    Quote Quote  
  12. Originally Posted by 2nHxWW6GkN1l916N3ayz8HQoi View Post
    Here is the downloader for Spotify Video Podcasts. Only free podcasts. Email and password are needed as well as CDM in wvd format (only for DRM content).
    Well done and... thanks I sometimes shamelessly send people I barely know, to other people for help...
    Quote Quote  
  13. Psychology student CrymanChen's Avatar
    Join Date
    Apr 2022
    Location
    Mainland China
    Search Comp PM
    Originally Posted by 2nHxWW6GkN1l916N3ayz8HQoi View Post
    Here is the downloader for Spotify Video Podcasts. Only free podcasts. Email and password are needed as well as CDM in wvd format (only for DRM content).
    Thank you Sir.
    twitter @Cryman_Chen
    email crymanchen@gmail.com
    Quote Quote  
  14. Feels Good Man 2nHxWW6GkN1l916N3ayz8HQoi's Avatar
    Join Date
    Jan 2024
    Location
    Pepe Island
    Search Comp PM
    Originally Posted by Silv3r View Post
    great work thanks
    Originally Posted by CrymanChen View Post
    Thank you Sir.
    You're welcome

    Originally Posted by [ss]vegeta View Post
    Well done and... thanks I sometimes shamelessly send people I barely know, to other people for help...
    No problem As I said to that person, I never solve private problems however I'm always willing to help directly (or indirectly) people I learned a lot from. I believe all problems and solutions should be publicly available so people can learn since this is a forum. And if I get more people interested in coding, that's a bonus.
    Quote Quote  
  15. Forgive my ignorance.
    but how am I to use this code? looks like python but ofc I'm missing modules? Ive not used pywidevine before but i did install it using pip.
    (Thus far I've been able to get keys I need through CDRM-Project and then download with from mpd + keys with N_m3u8DL-RE)
    However for spotify, I just don't understand how they've parsed the files at all. If I attempt to grab what I believe to be manifest I get errored or a json file lol
    indeed a baby at all of this I most sincerely appreciate any/all guidance.
    Quote Quote  
  16. give us your video link for test
    Quote Quote  
  17. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    Originally Posted by 2nHxWW6GkN1l916N3ayz8HQoi View Post

    .... I never solve private problems however I'm always willing to help directly (or indirectly) people I learned a lot from. I believe all problems and solutions should be publicly available so people can learn since this is a forum. And if I get more people interested in coding, that's a bonus.
    Nice sentiment! I've been trying to get people coding since forever; you could generously say it has been slow progress.
    Do not PM me please. Just ask your question here!
    https://forum.videohelp.com/threads/413534-From-My-Postbox
    Quote Quote  
  18. Originally Posted by lomero View Post
    give us your video link for test

    Hi so right now I'm just trying to use as-is with his example urls

    Code:
    SOURCE_URLS = [
        "https://open.spotify.com/episode/4dbjxVECWt1p2CcGrs7Wen",
        "https://open.spotify.com/episode/0OzuSfwPTCoPcjhnGsXOJa",
        "https://open.spotify.com/episode/3ppOLVKTWPsZeTk6r0l9Gg",
        "https://open.spotify.com/episode/0EBTCiYZl535zGYjbC6LXo",
    ]
    My question though is how is this whole code executed. I've trying throwing this in .py file calling it with either python3 or pywidevine, no-go if make an executable doesnt work either I'm just not sure where this code goes, and how you run it..
    Quote Quote  
  19. Originally Posted by u-ink View Post
    Originally Posted by lomero View Post
    give us your video link for test

    Hi so right now I'm just trying to use as-is with his example urls

    Code:
    SOURCE_URLS = [
        "https://open.spotify.com/episode/4dbjxVECWt1p2CcGrs7Wen",
        "https://open.spotify.com/episode/0OzuSfwPTCoPcjhnGsXOJa",
        "https://open.spotify.com/episode/3ppOLVKTWPsZeTk6r0l9Gg",
        "https://open.spotify.com/episode/0EBTCiYZl535zGYjbC6LXo",
    ]
    My question though is how is this whole code executed. I've trying throwing this in .py file calling it with either python3 or pywidevine, no-go if make an executable doesnt work either I'm just not sure where this code goes, and how you run it..
    If you pasted the kind of error you got when you ran "python thisscript.py", that would definitely help us help you.
    Quote Quote  
  20. Originally Posted by white_snake View Post
    If you pasted the kind of error you got when you ran "python thisscript.py", that would definitely help us help you.
    Okay, I'm an idiot. Thank you.
    I should've known better to try again once rested.

    Initially I had caught the error: " ModuleNotFoundError: No module named 'librespot' "
    ofc that just a matter of installing. But what happened is I had installed pywidevine afterwards which caused it to break. Now reading logs I can see that librespot "requires protobuf==3.20.1" & "requires requests==2.30.0" which conflicts with pywidevine which "requires protobuf<5.0.0,>=4.25.1" & "requires requests<3.0.0,>=2.31.0" so I just had to downgrade my requests and protobuf modules to match librespot requirements and it works as intended.

    Ofc this means I can't utilize pywidevine, which in turn means I wouldn't be able to get keys for anything DRM protected. Right now it isn't much of an issue for me as it seems what the couple podcast episodes I was after weren't protected. My guess then is I'd just have to downgrade pywidevine to a lower version that is compatible with the current requirements by librespot.

    Sorry I wasted peoples time.
    I'm trying to use these tools the best I can, ofc I should always be mindful that more often than not the weakest link is me and sometimes a bit of rest and some coffee does wonders to the obvious solutions to trivial problems. Oh well, bound to make many such mistakes as I learn I suppose best I get comfortable becoming the fool
    Quote Quote  
  21. You didn't waste anyone's time, no worries. I had that same inconvenience when I first installed librespot, which is where python virtual environments come in handy: https://forum.videohelp.com/threads/411862-Beyond-WKS-KEYS
    Quote Quote  
  22. Ofc why didnt I think of virtual envs. This is where pipx would come in handy then (I knew I'd eventually have a need for this- good bookmark) https://github.com/pypa/pipx for anyone interested if I understand correctly, this will be simpler way to manage my 'environments' without the need to call on the activation every time.

    Awesome community, thanks for your help white_snake!
    Quote Quote  



Similar Threads

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