[GH-ISSUE #11] Using Votify as a library in Python projects #10

Closed
opened 2026-03-04 14:58:34 +03:00 by kerem · 5 comments
Owner

Originally created by @vijaykrpp on GitHub (Feb 25, 2026).
Original GitHub issue: https://github.com/GladistonXD/votify-fix/issues/11

Hi,

Thanks for keeping votify up & running. I want simple code snippet to use it in my projects. Right not I can only use it via command lines.

Can you provide the code snippet for the same?

(Like glomatico's gamdl):

import asyncio

from gamdl.api import AppleMusicApi, ItunesApi
from gamdl.downloader import (
    AppleMusicBaseDownloader,
    AppleMusicDownloader,
    AppleMusicMusicVideoDownloader,
    AppleMusicSongDownloader,
    AppleMusicUploadedVideoDownloader,
)
from gamdl.interface import (
    AppleMusicInterface,
    AppleMusicMusicVideoInterface,
    AppleMusicSongInterface,
    AppleMusicUploadedVideoInterface,
)

async def main():
    # Create AppleMusicApi instance (from cookies or wrapper)
    apple_music_api = await AppleMusicApi.create_from_netscape_cookies(
        cookies_path="cookies.txt",
    )
    itunes_api = ItunesApi(
        apple_music_api.storefront,
        apple_music_api.language,
    )

    # Check subscription
    assert apple_music_api.active_subscription

    # Set up interfaces
    interface = AppleMusicInterface(apple_music_api, itunes_api)
    song_interface = AppleMusicSongInterface(interface)
    music_video_interface = AppleMusicMusicVideoInterface(interface)
    uploaded_video_interface = AppleMusicUploadedVideoInterface(interface)

    # Set up base downloader and specialized downloaders
    base_downloader = AppleMusicBaseDownloader()
    song_downloader = AppleMusicSongDownloader(
        base_downloader=base_downloader,
        interface=song_interface,
    )
    music_video_downloader = AppleMusicMusicVideoDownloader(
        base_downloader=base_downloader,
        interface=music_video_interface,
    )
    uploaded_video_downloader = AppleMusicUploadedVideoDownloader(
        base_downloader=base_downloader,
        interface=uploaded_video_interface,
    )

    # Main downloader
    downloader = AppleMusicDownloader(
        interface=interface,
        base_downloader=base_downloader,
        song_downloader=song_downloader,
        music_video_downloader=music_video_downloader,
        uploaded_video_downloader=uploaded_video_downloader,
    )

    # Download a song
    url = "https://music.apple.com/us/album/never-gonna-give-you-up-2022-remaster/1624945511?i=1624945512"
    url_info = downloader.get_url_info(url)
    if url_info:
        download_queue = await downloader.get_download_queue(url_info)
        if download_queue:
            for download_item in download_queue:
                await downloader.download(download_item)


if __name__ == "__main__":
    asyncio.run(main())
Originally created by @vijaykrpp on GitHub (Feb 25, 2026). Original GitHub issue: https://github.com/GladistonXD/votify-fix/issues/11 Hi, Thanks for keeping votify up & running. I want simple code snippet to use it in my projects. Right not I can only use it via command lines. Can you provide the code snippet for the same? (Like [glomatico's gamdl](https://github.com/glomatico/gamdl)): ``` import asyncio from gamdl.api import AppleMusicApi, ItunesApi from gamdl.downloader import ( AppleMusicBaseDownloader, AppleMusicDownloader, AppleMusicMusicVideoDownloader, AppleMusicSongDownloader, AppleMusicUploadedVideoDownloader, ) from gamdl.interface import ( AppleMusicInterface, AppleMusicMusicVideoInterface, AppleMusicSongInterface, AppleMusicUploadedVideoInterface, ) async def main(): # Create AppleMusicApi instance (from cookies or wrapper) apple_music_api = await AppleMusicApi.create_from_netscape_cookies( cookies_path="cookies.txt", ) itunes_api = ItunesApi( apple_music_api.storefront, apple_music_api.language, ) # Check subscription assert apple_music_api.active_subscription # Set up interfaces interface = AppleMusicInterface(apple_music_api, itunes_api) song_interface = AppleMusicSongInterface(interface) music_video_interface = AppleMusicMusicVideoInterface(interface) uploaded_video_interface = AppleMusicUploadedVideoInterface(interface) # Set up base downloader and specialized downloaders base_downloader = AppleMusicBaseDownloader() song_downloader = AppleMusicSongDownloader( base_downloader=base_downloader, interface=song_interface, ) music_video_downloader = AppleMusicMusicVideoDownloader( base_downloader=base_downloader, interface=music_video_interface, ) uploaded_video_downloader = AppleMusicUploadedVideoDownloader( base_downloader=base_downloader, interface=uploaded_video_interface, ) # Main downloader downloader = AppleMusicDownloader( interface=interface, base_downloader=base_downloader, song_downloader=song_downloader, music_video_downloader=music_video_downloader, uploaded_video_downloader=uploaded_video_downloader, ) # Download a song url = "https://music.apple.com/us/album/never-gonna-give-you-up-2022-remaster/1624945511?i=1624945512" url_info = downloader.get_url_info(url) if url_info: download_queue = await downloader.get_download_queue(url_info) if download_queue: for download_item in download_queue: await downloader.download(download_item) if __name__ == "__main__": asyncio.run(main()) ```
kerem closed this issue 2026-03-04 14:58:34 +03:00
Author
Owner

@GladistonXD commented on GitHub (Feb 25, 2026):

Do you want to use it as an API? You can use this as a base:

import sys
import logging
from pathlib import Path

import colorama
from votify.custom_logger_formatter import CustomLoggerFormatter

from votify.spotify_api import SpotifyApi
from votify.downloader import Downloader
from votify.downloader_audio import DownloaderAudio
from votify.downloader_song import DownloaderSong
from votify.enums import AudioQuality


def setup_logger():
    colorama.just_fix_windows_console()

    logger = logging.getLogger("votify")
    logger.setLevel(logging.INFO)

    if not logger.handlers:
        stream_handler = logging.StreamHandler()
        stream_handler.setFormatter(CustomLoggerFormatter())
        logger.addHandler(stream_handler)

    return logger


def main():
    logger = setup_logger()
    logger.info("Starting Votify")

    url = 'https://open.spotify.com/playlist/07okaLkrjXDozkKvISrAlq?si=b8a422f15e5246a6'
    customize_name = 'file'

    if len(sys.argv) > 1:
        url = sys.argv[1]
    if len(sys.argv) > 2:
        customize_name = sys.argv[2].replace(".m4a", "")

    cookies_path = Path("cookies.txt")
    if not cookies_path.exists():
        logger.critical(f"Arquivo '{cookies_path.absolute()}' não encontrado. Crie e coloque seus cookies.")
        return

    secrets_url = "https://code.thetadev.de/ThetaDev/spotify-secrets/raw/branch/main/secrets/secretDict.json"
    spotify_api = SpotifyApi.from_cookies_file(cookies_path, secrets_url=secrets_url)

    wvd_path = Path.home() / ".votify" / "device.wvd"

    template_pasta = "" if customize_name else "{artist}/{album}"
    template_arquivo = customize_name if customize_name else "{track_number} {title}"

    downloader = Downloader(
        spotify_api=spotify_api,
        output_path=Path("Spotify"),
        temp_path=Path("temp"),
        wvd_path=wvd_path,

        template_folder_album=template_pasta,
        template_folder_compilation=template_pasta,
        template_folder_episode=template_pasta,
        template_folder_music_video=template_pasta,
        template_file_single_disc=template_arquivo,
        template_file_multi_disc=template_arquivo,
        template_file_episode=template_arquivo,
        template_file_music_video=template_arquivo
    )

    try:
        downloader.set_cdm()
    except Exception:
        pass

    downloader_audio = DownloaderAudio(
        downloader=downloader,
        audio_quality=AudioQuality.AAC_MEDIUM
    )

    downloader_song = DownloaderSong(
        downloader_audio=downloader_audio,
        lrc_only=False,
        no_lrc=False
    )

    error_count = 0
    try:
        logger.info(f'(URL 1/1) Checking "{url}"')
        url_info = downloader.get_url_info(url)

        if url_info.type == "artist":
            download_queue = downloader.get_download_queue_from_artist(url_info.id)
        else:
            download_queue = downloader.get_download_queue(url_info.type, url_info.id)


        for index, item in enumerate(download_queue, start=1):

            if len(download_queue) > 1 and customize_name:
                template_numerado = f"{index:02d} - {customize_name}"
                downloader.template_file_single_disc = template_numerado
                downloader.template_file_multi_disc = template_numerado
                downloader.template_file_episode = template_numerado
                downloader.template_file_music_video = template_numerado

            media_metadata = item.get("media_metadata", item) if isinstance(item, dict) else getattr(item,"media_metadata",item)
            if not isinstance(media_metadata, dict):
                media_metadata = getattr(media_metadata, "__dict__", {})

            if isinstance(media_metadata, dict) and 'track' in media_metadata and isinstance(media_metadata['track'],dict):
                clean_track_metadata = media_metadata['track']
            else:
                clean_track_metadata = media_metadata

            try:
                track_name = media_metadata["data"]["trackUnion"]["name"]
            except (KeyError, TypeError):
                try:
                    track_name = media_metadata["data"]["episodeUnionV2"]["name"]
                except (KeyError, TypeError):
                    track_name = clean_track_metadata.get("name", "Unknown Track")

            try:
                media_id = downloader.get_media_id(clean_track_metadata)
            except ValueError:
                if isinstance(item, dict) and 'id' in item:
                    media_id = item['id']
                else:
                    raise

            media_type = "track"
            try:
                target_uri = media_metadata['data']['trackUnion']['uri']
            except (KeyError, TypeError):
                target_uri = clean_track_metadata.get('uri', '')

            if "episode" in target_uri:
                media_type = "episode"
            elif "track" in target_uri:
                media_type = "track"
            else:
                try:
                    media_type = media_metadata['data']['trackUnion']['__typename'].lower()
                except (KeyError, TypeError, AttributeError):
                    pass

            logger.info(f'(Track {index}/{len(download_queue)} from URL 1/1) Downloading "{track_name}"')

            gid_metadata = downloader.get_gid_metadata(
                media_id,
                media_type,
                spotify_api.user_profile,
                track_name,
                False,
                False
            )

            safe_album_metadata = getattr(item, "album_metadata", None) if not isinstance(item, dict) else item.get("album_metadata")
            safe_playlist_metadata = getattr(item, "playlist_metadata", None) if not isinstance(item,dict) else item.get("playlist_metadata")

            downloader_song.download(
                track_id=media_id,
                track_metadata=media_metadata,
                album_metadata=safe_album_metadata,
                gid_metadata=gid_metadata,
                playlist_metadata=safe_playlist_metadata,
                product_name=spotify_api.user_profile,
                playlist_track=index
            )

    except Exception as e:
        error_count += 1
        logger.error(f'Failed to check "{url}"', exc_info=False)

    logger.info(f"Done ({error_count} error(s))")


if __name__ == "__main__":
    main()
<!-- gh-comment-id:3960618196 --> @GladistonXD commented on GitHub (Feb 25, 2026): Do you want to use it as an API? You can use this as a base: ```python import sys import logging from pathlib import Path import colorama from votify.custom_logger_formatter import CustomLoggerFormatter from votify.spotify_api import SpotifyApi from votify.downloader import Downloader from votify.downloader_audio import DownloaderAudio from votify.downloader_song import DownloaderSong from votify.enums import AudioQuality def setup_logger(): colorama.just_fix_windows_console() logger = logging.getLogger("votify") logger.setLevel(logging.INFO) if not logger.handlers: stream_handler = logging.StreamHandler() stream_handler.setFormatter(CustomLoggerFormatter()) logger.addHandler(stream_handler) return logger def main(): logger = setup_logger() logger.info("Starting Votify") url = 'https://open.spotify.com/playlist/07okaLkrjXDozkKvISrAlq?si=b8a422f15e5246a6' customize_name = 'file' if len(sys.argv) > 1: url = sys.argv[1] if len(sys.argv) > 2: customize_name = sys.argv[2].replace(".m4a", "") cookies_path = Path("cookies.txt") if not cookies_path.exists(): logger.critical(f"Arquivo '{cookies_path.absolute()}' não encontrado. Crie e coloque seus cookies.") return secrets_url = "https://code.thetadev.de/ThetaDev/spotify-secrets/raw/branch/main/secrets/secretDict.json" spotify_api = SpotifyApi.from_cookies_file(cookies_path, secrets_url=secrets_url) wvd_path = Path.home() / ".votify" / "device.wvd" template_pasta = "" if customize_name else "{artist}/{album}" template_arquivo = customize_name if customize_name else "{track_number} {title}" downloader = Downloader( spotify_api=spotify_api, output_path=Path("Spotify"), temp_path=Path("temp"), wvd_path=wvd_path, template_folder_album=template_pasta, template_folder_compilation=template_pasta, template_folder_episode=template_pasta, template_folder_music_video=template_pasta, template_file_single_disc=template_arquivo, template_file_multi_disc=template_arquivo, template_file_episode=template_arquivo, template_file_music_video=template_arquivo ) try: downloader.set_cdm() except Exception: pass downloader_audio = DownloaderAudio( downloader=downloader, audio_quality=AudioQuality.AAC_MEDIUM ) downloader_song = DownloaderSong( downloader_audio=downloader_audio, lrc_only=False, no_lrc=False ) error_count = 0 try: logger.info(f'(URL 1/1) Checking "{url}"') url_info = downloader.get_url_info(url) if url_info.type == "artist": download_queue = downloader.get_download_queue_from_artist(url_info.id) else: download_queue = downloader.get_download_queue(url_info.type, url_info.id) for index, item in enumerate(download_queue, start=1): if len(download_queue) > 1 and customize_name: template_numerado = f"{index:02d} - {customize_name}" downloader.template_file_single_disc = template_numerado downloader.template_file_multi_disc = template_numerado downloader.template_file_episode = template_numerado downloader.template_file_music_video = template_numerado media_metadata = item.get("media_metadata", item) if isinstance(item, dict) else getattr(item,"media_metadata",item) if not isinstance(media_metadata, dict): media_metadata = getattr(media_metadata, "__dict__", {}) if isinstance(media_metadata, dict) and 'track' in media_metadata and isinstance(media_metadata['track'],dict): clean_track_metadata = media_metadata['track'] else: clean_track_metadata = media_metadata try: track_name = media_metadata["data"]["trackUnion"]["name"] except (KeyError, TypeError): try: track_name = media_metadata["data"]["episodeUnionV2"]["name"] except (KeyError, TypeError): track_name = clean_track_metadata.get("name", "Unknown Track") try: media_id = downloader.get_media_id(clean_track_metadata) except ValueError: if isinstance(item, dict) and 'id' in item: media_id = item['id'] else: raise media_type = "track" try: target_uri = media_metadata['data']['trackUnion']['uri'] except (KeyError, TypeError): target_uri = clean_track_metadata.get('uri', '') if "episode" in target_uri: media_type = "episode" elif "track" in target_uri: media_type = "track" else: try: media_type = media_metadata['data']['trackUnion']['__typename'].lower() except (KeyError, TypeError, AttributeError): pass logger.info(f'(Track {index}/{len(download_queue)} from URL 1/1) Downloading "{track_name}"') gid_metadata = downloader.get_gid_metadata( media_id, media_type, spotify_api.user_profile, track_name, False, False ) safe_album_metadata = getattr(item, "album_metadata", None) if not isinstance(item, dict) else item.get("album_metadata") safe_playlist_metadata = getattr(item, "playlist_metadata", None) if not isinstance(item,dict) else item.get("playlist_metadata") downloader_song.download( track_id=media_id, track_metadata=media_metadata, album_metadata=safe_album_metadata, gid_metadata=gid_metadata, playlist_metadata=safe_playlist_metadata, product_name=spotify_api.user_profile, playlist_track=index ) except Exception as e: error_count += 1 logger.error(f'Failed to check "{url}"', exc_info=False) logger.info(f"Done ({error_count} error(s))") if __name__ == "__main__": main() ```
Author
Owner

@cynthia2006 commented on GitHub (Feb 26, 2026):

@vijaykrpp You can checkout votifast, which has a very friendly API to work with. Here's a minimal example to get you started.

import asyncio
import aiofiles
import httpx
import os

from pywidevine import Cdm, Device
from votifast.api import SpotifyApi

SECRETS_URL = 'https://code.thetadev.de/ThetaDev/spotify-secrets/raw/branch/main/secrets/secretDict.json'

async def main():
  client = httpx.AsyncClient(timeout=False, follow_redirects=True)
  cdm = Cdm.from_device(Device.load('path/to/wvd'))
  secrets = (await client.get(SECRETS_URL)).json()

  spotify = SpotifyApi.with_cookies(client, 'path/to/cookies', cdm, secrets)
  await spotify.initialize()

  # NOTE: Track is a dataclass, not an unstructured dict as is in Votify.
  # IMPORTANT: Replace <track-id> placeholder with your track ID.
  track = await spotify.get_track('<track-id>')
  source = track.hq_source(spotify.is_premium)
  interim = f'{src.file_id}.bin'
  final_path = f'{track.artist_line} - {track.name}.m4a'
  
  async with (
    aiofiles.open(interim, 'wb') as file, 
    client.stream('GET', source.cdns[0]) as r
  ):
    async for chunk in r.aiter_bytes():
      file.write(chunk)
  
  key = await self.spotify.get_widevine_key(source.file_id)
  if key:
    hex_key = key.key.hex()
  else:
    return

  ffmpeg = await asyncio.create_subprocess_exec(
        'ffmpeg',
        '-hide_banner',
        '-y',
        '-loglevel', 'error',
        '-decryption_key', hex_key,
        '-i', interim,
        '-c', 'copy',
        final_path
  )
  await ffmpeg.communicate(None)
  
if __name__ == '__main__':
  main()
<!-- gh-comment-id:3963863174 --> @cynthia2006 commented on GitHub (Feb 26, 2026): @vijaykrpp You can checkout [votifast](https://github.com/cynthia2006/votifast), which has a very friendly API to work with. Here's a minimal example to get you started. ```py import asyncio import aiofiles import httpx import os from pywidevine import Cdm, Device from votifast.api import SpotifyApi SECRETS_URL = 'https://code.thetadev.de/ThetaDev/spotify-secrets/raw/branch/main/secrets/secretDict.json' async def main(): client = httpx.AsyncClient(timeout=False, follow_redirects=True) cdm = Cdm.from_device(Device.load('path/to/wvd')) secrets = (await client.get(SECRETS_URL)).json() spotify = SpotifyApi.with_cookies(client, 'path/to/cookies', cdm, secrets) await spotify.initialize() # NOTE: Track is a dataclass, not an unstructured dict as is in Votify. # IMPORTANT: Replace <track-id> placeholder with your track ID. track = await spotify.get_track('<track-id>') source = track.hq_source(spotify.is_premium) interim = f'{src.file_id}.bin' final_path = f'{track.artist_line} - {track.name}.m4a' async with ( aiofiles.open(interim, 'wb') as file, client.stream('GET', source.cdns[0]) as r ): async for chunk in r.aiter_bytes(): file.write(chunk) key = await self.spotify.get_widevine_key(source.file_id) if key: hex_key = key.key.hex() else: return ffmpeg = await asyncio.create_subprocess_exec( 'ffmpeg', '-hide_banner', '-y', '-loglevel', 'error', '-decryption_key', hex_key, '-i', interim, '-c', 'copy', final_path ) await ffmpeg.communicate(None) if __name__ == '__main__': main() ```
Author
Owner

@vijaykrpp commented on GitHub (Feb 26, 2026):

Thanks a lot, @GladistonXD and @cynthia2006. Much appreciated.

<!-- gh-comment-id:3964154218 --> @vijaykrpp commented on GitHub (Feb 26, 2026): Thanks a lot, @GladistonXD and @cynthia2006. Much appreciated.
Author
Owner

@vijaykrpp commented on GitHub (Feb 27, 2026):

@GladistonXD how do I manipulate the downloaded file name and the directory? It defaults to download at Spotify folder with subfolders names taken from artist name and album name. I want to keep it simple, to download at /path/to/download/file.m4a ?

<!-- gh-comment-id:3970640421 --> @vijaykrpp commented on GitHub (Feb 27, 2026): @GladistonXD how do I manipulate the downloaded file name and the directory? It defaults to download at ```Spotify``` folder with subfolders names taken from artist name and album name. I want to keep it simple, to download at ```/path/to/download/file.m4a``` ?
Author
Owner

@GladistonXD commented on GitHub (Feb 28, 2026):

I edited the code above; in this part here you can customize the output:

customize_name = 'file'

<!-- gh-comment-id:3975830072 --> @GladistonXD commented on GitHub (Feb 28, 2026): I edited the code above; in this part here you can customize the output: customize_name = 'file'
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/votify-fix#10
No description provided.