initial commit

This commit is contained in:
Grizouille
2025-11-06 22:42:49 +01:00
parent 72dfd1e21e
commit 6399ab4af2
50 changed files with 4044 additions and 233 deletions

View File

@@ -0,0 +1,59 @@
;;; template configuration for deezer-downlaoder
;;; you need to adjust: deezer.cookie_arl
[mpd]
; if you set this to True, the backend will connect to mpd (localhost:6600) and update
; the music database after a completed download
use_mpd = False
host = localhost
port = 6600
music_dir_root = /tmp/deezer-downloader
[download_dirs]
base = /tmp/deezer-downloader
songs = %(base)s/songs
albums = %(base)s/albums
zips = %(base)s/zips
playlists = %(base)s/playlists
youtubedl = %(base)s/youtube-dl
[debug]
; debug output used for /debug
command = journalctl -u deezer-downloader -n 100 --output cat
[http]
; web backend options
host = 127.0.0.1
port = 5000
; if used behind a proxy, specify base url prefix
; url_prefix = /deezer
url_prefix =
api_root = %(url_prefix)s
static_root = %(url_prefix)s/static
[proxy]
; server:
; - https://user:pass@host:port
; - socks5://127.0.0.1:9050
; - socks5h://127.0.0.1:9050 (DNS goes also over proxy)
server =
[threadpool]
; number of workers in thread pool, this specifies the maximum number of parallel downloads
workers = 4
[deezer]
; valid arl cookie value
; login manually using your web browser and take the arl cookie
cookie_arl = [a-f0-9]{192}
; mp3 or flac - flac needs premium subscription
quality = mp3
[youtubedl]
; you are responsible for keeping yt-dlp up-to-date (https://github.com/yt-dlp/yt-dlp)
; command = /home/kmille/projects/deezer-downloader/app/venv/bin/yt-dlp
command = /usr/bin/yt-dlp
; vim: syntax=dosini

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
import sys
import argparse
from pathlib import Path
import waitress
def get_version():
from importlib.metadata import version
v = version("deezer_downloader")
return f"v{v}"
def run_backend():
from deezer_downloader.configuration import config
from deezer_downloader.web.app import app
print(f"Listening on {config['http']['host']}:{config['http'].getint('port')} (version {get_version()})")
if __name__ == '__main__':
app.run(debug=True,
host=config['http']['host'],
port=config['http'].getint('port'))
else:
listen = f"{config['http']['host']}:{config['http'].getint('port')}"
waitress.serve(app, listen=listen)
def main():
parser = argparse.ArgumentParser(prog='deezer-downloader',
description="Download music from Deezer and Spotify with a simple web frontend, through a local-hosted service written in Python.",
epilog="More info at https://github.com/kmille/deezer-downloader.")
parser.add_argument("-v", "--version", action='store_true', help="show version and exit")
parser.add_argument("-t", "--show-config-template", action='store_true', help="show config template - you have to provide the ARL cookie at least")
parser.add_argument("-c", "--config", help="config file - if not supplied, the following directories are considered looking for deezer-downloader.ini: current working directory, XDG_CONFIG_HOME environment variable, ~/.config, /etc)")
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
if args.version:
print(sys.argv[0], get_version())
sys.exit(0)
if args.show_config_template:
print((Path(__file__).parent / Path("deezer-downloader.ini.template")).read_text(), end="")
sys.exit(0)
from deezer_downloader.configuration import load_config
load_config(args.config)
run_backend()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,53 @@
import sys
import os
from pathlib import Path
from configparser import ConfigParser
config = None
def load_config(config_abs):
global config
if not os.path.exists(config_abs):
print(f"Could not find config file: {config_abs}")
sys.exit(1)
config = ConfigParser()
config.read(config_abs)
assert list(config.keys()) == ['DEFAULT', 'mpd', 'download_dirs', 'debug', 'http', 'proxy', 'threadpool', 'deezer', 'youtubedl'], f"Validating config file failed. Check {config_abs}"
if config['mpd'].getboolean('use_mpd'):
if not config['mpd']['music_dir_root'].startswith(config['download_dirs']['base']):
print("ERROR: base download dir must be a subdirectory of the mpd music_dir_root")
sys.exit(1)
if not Path(config['youtubedl']['command']).exists():
print(f"ERROR: yt-dlp not found at {config['youtubedl']['command']}")
sys.exit(1)
proxy_server = config['proxy']['server']
if len(proxy_server) > 0:
if not proxy_server.startswith("https://") and \
not proxy_server.startswith("socks5"): # there is also socks5h
print(f"ERROR: invalid proxy server address: {config['proxy']['server']}")
sys.exit(1)
if "DEEZER_COOKIE_ARL" in os.environ.keys():
config["deezer"]["cookie_arl"] = os.environ["DEEZER_COOKIE_ARL"]
if len(config["deezer"]["cookie_arl"].strip()) == 0:
print("ERROR: cookie_arl must not be empty")
sys.exit(1)
if "DEEZER_QUALITY" in os.environ.keys():
config["deezer"]["quality"] = os.environ["DEEZER_QUALITY"]
if "quality" in config['deezer']:
if config['deezer']["quality"] not in ("mp3", "flac"):
print("ERROR: quality must be mp3 or flac in config file")
sys.exit(1)
else:
print("Warning: quality not set in config file. Using mp3")
config["deezer"]["quality"] = "mp3"

491
deezer_downloader/deezer.py Normal file
View File

@@ -0,0 +1,491 @@
import sys
import re
import json
from typing import Optional, Sequence
from deezer_downloader.configuration import config
from Crypto.Hash import MD5
from Crypto.Cipher import Blowfish
import urllib.parse
import html.parser
import requests
from binascii import a2b_hex, b2a_hex
from mutagen.flac import FLAC, Picture
from mutagen.mp3 import MP3
from mutagen.id3 import PictureType, TIT2, TALB, TPE1, TRCK, TDRC, TPOS, APIC, TPE2
from mutagen import MutagenError
# BEGIN TYPES
TYPE_TRACK = "track"
TYPE_ALBUM = "album"
TYPE_PLAYLIST = "playlist"
TYPE_ARTIST = "artist"
TYPE_ALBUM_TRACK = "album_track" # used for listing songs of an album
TYPE_ARTIST_ALBUM = "artist_album" # used for listing albums of an artist
TYPE_ARTIST_TOP = "artist_top" # used for listing top tracks of an artist
# END TYPES
session = None
license_token = {}
sound_format = ""
USER_AGENT = "Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"
def get_user_data() -> tuple[str, str]:
try:
user_data = session.get('https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=')
user_data_json = user_data.json()['results']
options = user_data_json['USER']['OPTIONS']
license_token = options['license_token']
web_sound_quality = options['web_sound_quality']
return license_token, web_sound_quality
except (requests.exceptions.RequestException, KeyError) as e:
print(f"ERROR: Could not get license token: {e}")
# quality_config comes from config file
# web_sound_quality is a dict coming from Deezer API and depends on ARL cookie (premium subscription)
def set_song_quality(quality_config: str, web_sound_quality: dict):
global sound_format
flac_supported = web_sound_quality['lossless'] is True
if flac_supported:
if quality_config == "flac":
sound_format = "FLAC"
else:
sound_format = "MP3_320"
else:
if quality_config == "flac":
print("WARNING: flac quality is configured in config file but not supported (no premium subscription?). Falling back to mp3")
sound_format = "MP3_128"
def get_file_extension() -> str:
return "flac" if sound_format == "FLAC" else "mp3"
# quality is mp3 or flac
def init_deezer_session(proxy_server: str, quality: str) -> None:
global session, license_token, web_sound_quality
header = {
'Pragma': 'no-cache',
'Origin': 'https://www.deezer.com',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
'User-Agent': USER_AGENT,
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Accept': '*/*',
'Cache-Control': 'no-cache',
'X-Requested-With': 'XMLHttpRequest',
'Connection': 'keep-alive',
'Referer': 'https://www.deezer.com/login',
'DNT': '1',
}
session = requests.session()
session.headers.update(header)
session.cookies.update({'arl': config['deezer']['cookie_arl'], 'comeback': '1'})
if len(proxy_server.strip()) > 0:
print(f"Using proxy {proxy_server}")
session.proxies.update({"https": proxy_server})
license_token, web_sound_quality = get_user_data()
set_song_quality(quality, web_sound_quality)
class Deezer404Exception(Exception):
pass
class Deezer403Exception(Exception):
pass
class DeezerApiException(Exception):
pass
class ScriptExtractor(html.parser.HTMLParser):
""" extract <script> tag contents from a html page """
def __init__(self):
html.parser.HTMLParser.__init__(self)
self.scripts = []
self.curtag = None
def handle_starttag(self, tag, attrs):
self.curtag = tag.lower()
def handle_data(self, data):
if self.curtag == "script":
self.scripts.append(data)
def handle_endtag(self, tag):
self.curtag = None
def md5hex(data):
""" return hex string of md5 of the given string """
# type(data): bytes
# returns: bytes
h = MD5.new()
h.update(data)
return b2a_hex(h.digest())
def calcbfkey(songid):
""" Calculate the Blowfish decrypt key for a given songid """
key = b"g4el58wc0zvf9na1"
songid_md5 = md5hex(songid.encode())
xor_op = lambda i: chr(songid_md5[i] ^ songid_md5[i + 16] ^ key[i])
decrypt_key = "".join([xor_op(i) for i in range(16)])
return decrypt_key
def blowfishDecrypt(data, key):
iv = a2b_hex("0001020304050607")
c = Blowfish.new(key.encode(), Blowfish.MODE_CBC, iv)
return c.decrypt(data)
def decryptfile(fh, key, fo):
"""
Decrypt data from file <fh>, and write to file <fo>.
decrypt using blowfish with <key>.
Only every third 2048 byte block is encrypted.
"""
blockSize = 2048
i = 0
for data in fh.iter_content(blockSize):
if not data:
break
isEncrypted = ((i % 3) == 0)
isWholeBlock = len(data) == blockSize
if isEncrypted and isWholeBlock:
data = blowfishDecrypt(data, key)
fo.write(data)
i += 1
def downloadpicture(pic_idid):
setting_domain_img = "https://e-cdns-images.dzcdn.net/images"
url = setting_domain_img + "/cover/" + pic_idid + "/1200x1200.jpg"
resp = session.get(url)
resp.raise_for_status()
return resp.content
def get_song_url(track_token: str, quality: int = 3) -> str:
try:
response = requests.post(
"https://media.deezer.com/v1/get_url",
json={
'license_token': license_token,
'media': [{
'type': "FULL",
"formats": [
{"cipher": "BF_CBC_STRIPE", "format": sound_format}]
}],
'track_tokens': [track_token,]
},
headers={"User-Agent": USER_AGENT},
)
response.raise_for_status()
data = response.json()
except requests.exceptions.RequestException as e:
raise DeezerApiException(f"Could not retrieve song URL: {e}")
if not data.get('data') or 'errors' in data['data'][0]:
raise DeezerApiException(f"Could not get download url from API: {data['data'][0]['errors'][0]['message']}")
if len(data["data"][0]["media"]) == 0:
raise DeezerApiException(f"Could not get download url from API. There was no API error, but also no song information. API response: {data}")
url = data['data'][0]['media'][0]['sources'][0]['url']
return url
def download_song(song: dict, output_file: str) -> None:
# downloads and decrypts the song from Deezer. Adds ID3 and art cover
# song: dict with information of the song (grabbed from Deezer.com)
# output_file: absolute file name of the output file
assert type(song) is dict, "song must be a dict"
assert type(output_file) is str, "output_file must be a str"
try:
url = get_song_url(song["TRACK_TOKEN"])
except Exception as e:
print(f"Could not download song (https://www.deezer.com/us/track/{song['SNG_ID']}). Maybe it's not available anymore or at least not in your country. {e}")
if "FALLBACK" in song:
song = song["FALLBACK"]
print(f"Trying fallback song https://www.deezer.com/us/track/{song['SNG_ID']}")
try:
url = get_song_url(song["TRACK_TOKEN"])
except Exception:
pass
else:
print("Fallback song seems to work")
else:
raise
key = calcbfkey(song["SNG_ID"])
is_flac = get_file_extension() == "flac"
try:
with session.get(url, stream=True) as response:
response.raise_for_status()
with open(output_file, "w+b") as fo:
decryptfile(response, key, fo)
write_song_metadata(output_file, song, is_flac)
except MutagenError as e:
print(f"Warning: Could not write metadata to file: {e}")
except Exception as e:
raise DeezerApiException(f"Could not write song to disk: {e}") from e
print("Download finished: {}".format(output_file))
def write_song_metadata(output_file: str, song: dict, is_flac: bool) -> None:
def set_metadata(audio, key, value):
if not value:
return
elif isinstance(audio, MP3):
if key == 'artist':
audio['TPE1'] = TPE1(encoding=3, text=value)
elif key == 'albumartist':
audio['TPE2'] = TPE2(encoding=3, text=value)
elif key == 'title':
audio['TIT2'] = TIT2(encoding=3, text=value)
elif key == 'album':
audio['TALB'] = TALB(encoding=3, text=value)
elif key == 'discnumber':
audio['TPOS'] = TPOS(encoding=3, text=value)
elif key == 'tracknumber':
audio['TRCK'] = TRCK(encoding=3, text=value)
elif key == 'date':
audio['TDRC'] = TDRC(encoding=3, text=value)
elif key == 'picture':
audio['APIC'] = APIC(encoding=3, mime='image/jpeg', type=PictureType.COVER_FRONT, desc='Cover', data=value)
else:
if key == 'picture':
pic = Picture()
pic.mime = u'image/jpeg'
pic.type = PictureType.COVER_FRONT
pic.desc = 'Cover'
pic.data = value
audio.add_picture(pic)
else:
audio[key] = value
if is_flac:
audio = FLAC(output_file)
else:
audio = MP3(output_file)
set_metadata(audio, "artist", song.get("ART_NAME", None))
set_metadata(audio, "title", song.get("SNG_TITLE", None))
set_metadata(audio, "album", song.get("ALB_TITLE", None))
set_metadata(audio, 'tracknumber', song.get("TRACK_NUMBER", None))
set_metadata(audio, "discnumber", song.get("DISK_NUMBER", None))
if "album_Data" in globals() and "PHYSICAL_RELEASE_DATE" in album_Data:
set_metadata(audio, "date", album_Data.get("PHYSICAL_RELEASE_DATE")[:4])
set_metadata(audio, "picture", downloadpicture(song["ALB_PICTURE"]))
set_metadata(audio, "albumartist", song.get('ALB_ART_NAME', song.get('ART_NAME', None)))
audio.save()
def get_song_infos_from_deezer_website(search_type, id):
# search_type: either one of the constants: TYPE_TRACK|TYPE_ALBUM|TYPE_PLAYLIST
# id: deezer_id of the song/album/playlist (like https://www.deezer.com/de/track/823267272)
# return: if TYPE_TRACK => song (dict grabbed from the website with information about a song)
# return: if TYPE_ALBUM|TYPE_PLAYLIST => list of songs
# raises
# Deezer404Exception if
# 1. open playlist https://www.deezer.com/de/playlist/1180748301 and click on song Honey from Moby in a new tab:
# 2. Deezer gives you a 404: https://www.deezer.com/de/track/68925038
# Deezer403Exception if we are not logged in
url = "https://www.deezer.com/us/{}/{}".format(search_type, id)
resp = session.get(url)
if resp.status_code == 404:
raise Deezer404Exception("ERROR: Got a 404 for {} from Deezer".format(url))
if "MD5_ORIGIN" not in resp.text:
raise Deezer403Exception("ERROR: we are not logged in on deezer.com. Please update the cookie")
parser = ScriptExtractor()
parser.feed(resp.text)
parser.close()
songs = []
for script in parser.scripts:
regex = re.search(r'{"DATA":.*', script)
if regex:
DZR_APP_STATE = json.loads(regex.group())
global album_Data
album_Data = DZR_APP_STATE.get("DATA")
if DZR_APP_STATE['DATA']['__TYPE__'] == 'playlist' or DZR_APP_STATE['DATA']['__TYPE__'] == 'album':
# songs if you searched for album/playlist
for song in DZR_APP_STATE['SONGS']['data']:
songs.append(song)
elif DZR_APP_STATE['DATA']['__TYPE__'] == 'song':
# just one song on that page
songs.append(DZR_APP_STATE['DATA'])
return songs[0] if search_type == TYPE_TRACK else songs
def deezer_search(search, search_type):
# search: string (What are you looking for?)
# search_type: either one of the constants: TYPE_TRACK|TYPE_ALBUM|TYPE_ALBUM_TRACK (TYPE_PLAYLIST is not supported)
# return: list of dicts (keys depend on search_type)
if search_type not in [TYPE_TRACK, TYPE_ALBUM, TYPE_ARTIST, TYPE_ALBUM_TRACK, TYPE_ARTIST_ALBUM, TYPE_ARTIST_TOP]:
print("ERROR: search_type is wrong: {}".format(search_type))
return []
search = urllib.parse.quote_plus(search)
if search_type == TYPE_ALBUM_TRACK:
url = f"https://api.deezer.com//album/{search}"
elif search_type == TYPE_ARTIST_TOP:
url = f"https://api.deezer.com/artist/{search}/top?limit=20"
elif search_type == TYPE_ARTIST_ALBUM:
url = f"https://api.deezer.com/artist/{search}/albums"
else:
url = f"https://api.deezer.com/search/{search_type}?q={search}"
try:
resp = session.get(url)
resp.raise_for_status()
data = resp.json()
if search_type == TYPE_ALBUM_TRACK:
data = data["tracks"]['data']
else:
data = data['data']
except (requests.exceptions.RequestException, KeyError) as e:
print(f"ERROR: Could not search for music: {e}")
return []
return_nice = []
for item in data:
i = {}
i['id'] = str(item['id'])
if search_type in (TYPE_ALBUM, TYPE_ARTIST_ALBUM):
i['id_type'] = TYPE_ALBUM
i['album'] = item['title']
i['album_id'] = item['id']
i['img_url'] = item['cover_small']
i['title'] = ''
i['preview_url'] = ''
i['artist'] = ''
if search_type == TYPE_ALBUM:
# strange API design? artist is not there when asking for ARTIST_ALBUMs
i['artist'] = item['artist']['name']
elif search_type in (TYPE_TRACK, TYPE_ARTIST_TOP, TYPE_ALBUM_TRACK):
i['id_type'] = TYPE_TRACK
i['title'] = item['title']
i['img_url'] = item['album']['cover_small']
i['album'] = item['album']['title']
i['album_id'] = item['album']['id']
i['artist'] = item['artist']['name']
i['preview_url'] = item['preview']
elif search_type == TYPE_ARTIST:
i['id_type'] = TYPE_ARTIST
i['title'] = ''
i['img_url'] = item['picture_small']
i['album'] = ''
i['album_id'] = ''
i['artist'] = item['name']
i['artist_id'] = item['id']
i['preview_url'] = ''
return_nice.append(i)
return return_nice
def parse_deezer_playlist(playlist_id):
# playlist_id: id of the playlist or the url of it
# e.g. https://www.deezer.com/de/playlist/6046721604 or 6046721604
# return (playlist_name, list of songs) (song is a dict with information about the song)
# raises DeezerApiException if something with the Deezer API is broken
try:
playlist_id = re.search(r'\d+', playlist_id).group(0)
except AttributeError:
raise DeezerApiException("ERROR: Regex (\\d+) for playlist_id failed. You gave me '{}'".format(playlist_id))
url_get_csrf_token = "https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token="
req = session.post(url_get_csrf_token)
csrf_token = req.json()['results']['checkForm']
url_get_playlist_songs = "https://www.deezer.com/ajax/gw-light.php?method=deezer.pagePlaylist&input=3&api_version=1.0&api_token={}".format(csrf_token)
data = {'playlist_id': int(playlist_id),
'start': 0,
'tab': 0,
'header': True,
'lang': 'de',
'nb': 500}
req = session.post(url_get_playlist_songs, json=data)
json = req.json()
if len(json['error']) > 0:
raise DeezerApiException("ERROR: deezer api said {}".format(json['error']))
json_data = json['results']
playlist_name = json_data['DATA']['TITLE']
number_songs = json_data['DATA']['NB_SONG']
print("Playlist '{}' has {} songs".format(playlist_name, number_songs))
print("Got {} songs from API".format(json_data['SONGS']['count']))
return playlist_name, json_data['SONGS']['data']
def get_deezer_favorites(user_id: str) -> Optional[Sequence[int]]:
if not user_id.isnumeric():
raise Exception(f"User id '{user_id}' must be numeric")
resp = session.get(f"https://api.deezer.com/user/{user_id}/tracks?limit=10000000000")
assert resp.status_code == 200, f"got invalid status asking for favorite song\n{resp.text}s"
resp_json = resp.json()
if "error" in resp_json.keys():
raise Exception(f"Upstream api error getting favorite songs for user {user_id}:\n{resp_json['error']}")
# check is set next
while "next" in resp_json.keys():
resp = session.get(resp_json["next"])
assert resp.status_code == 200, f"got invalid status asking for favorite song\n{resp.text}s"
resp_json_next = resp.json()
if "error" in resp_json_next.keys():
raise Exception(f"Upstream api error getting favorite songs for user {user_id}:\n{resp_json_next['error']}")
resp_json["data"] += resp_json_next["data"]
if "next" in resp_json_next.keys():
resp_json["next"] = resp_json_next["next"]
else:
del resp_json["next"]
print(f"Got {resp_json['total']} favorite songs for user {user_id} from the api")
songs = [song['id'] for song in resp_json['data']]
return songs
def test_deezer_login():
print("Let's check if the deezer login is still working")
try:
song = get_song_infos_from_deezer_website(TYPE_TRACK, "917265")
except (Deezer403Exception, Deezer404Exception) as msg:
print(msg)
print("Login is not working anymore.")
return False
if song:
print("Login is still working.")
return True
else:
print("Login is not working anymore.")
return False
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == "check-login":
test_deezer_login()

Binary file not shown.

View File

@@ -0,0 +1,189 @@
import re
import base64
import pyotp
from time import sleep
from urllib.parse import urlparse, parse_qs
from typing import Tuple
import requests
token_url = 'https://open.spotify.com/api/token'
server_time_url = 'https://open.spotify.com/api/server-time'
playlist_base_url = 'https://api.spotify.com/v1/playlists/{}/tracks?limit=100&additional_types=track' # todo figure out market
track_base_url = 'https://api.spotify.com/v1/tracks/{}'
album_base_url = 'https://api.spotify.com/v1/albums/{}/tracks'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'Referer': 'https://open.spotify.com/',
'Origin': 'https://open.spotify.com'
}
class SpotifyInvalidUrlException(Exception):
pass
class SpotifyWebsiteParserException(Exception):
pass
def get_secrets() -> Tuple[int, list[int]]:
# please read https://github.com/librespot-org/librespot/discussions/1562#discussioncomment-14659870
# sudo docker run --rm misiektoja/spotify-secrets-grabber --secretbytes
return ("61", [44, 55, 47, 42, 70, 40, 34, 114, 76, 74, 50, 111, 120, 97, 75, 76, 94, 102, 43, 69, 49, 120, 118, 80, 64, 78])
def generate_totp(
timestamp_seconds: int,
secret: bytes,
) -> str:
transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret)]
joined = "".join(str(num) for num in transformed)
hex_str = joined.encode().hex()
secret32 = base64.b32encode(bytes.fromhex(hex_str)).decode().rstrip("=")
return pyotp.TOTP(secret32, digits=6, interval=30).at(timestamp_seconds)
def get_server_time() -> int:
response = requests.get(server_time_url)
response.raise_for_status()
return response.json()["serverTime"]
def parse_uri(uri):
u = urlparse(uri)
if u.netloc == "embed.spotify.com":
if not u.query:
raise SpotifyInvalidUrlException("ERROR: url {} is not supported".format(uri))
qs = parse_qs(u.query)
return parse_uri(qs['uri'][0])
# backwards compatibility
if not u.scheme and not u.netloc:
return {"type": "playlist", "id": u.path}
if u.scheme == "spotify":
parts = uri.split(":")
else:
if u.netloc != "open.spotify.com" and u.netloc != "play.spotify.com":
raise SpotifyInvalidUrlException("ERROR: url {} is not supported".format(uri))
parts = u.path.split("/")
if parts[1] == "embed":
parts = parts[1:]
l = len(parts)
if l == 3 and parts[1] in ["album", "track", "playlist"]:
return {"type": parts[1], "id": parts[2]}
if l == 5 and parts[3] == "playlist":
return {"type": parts[3], "id": parts[4]}
# todo add support for other types; artists, searches, users
raise SpotifyInvalidUrlException("ERROR: unable to determine Spotify URL type or type is unsupported.")
def get_songs_from_spotify_website(playlist, proxy=None):
# parses Spotify Playlist from Spotify website
# playlist: playlist url or playlist id as string
# proxy: https/socks5 proxy (e. g. socks5://user:pass@127.0.0.1:1080/)
# e.g. https://open.spotify.com/playlist/0wl9Q3oedquNlBAJ4MGZtS
# e.g. https://open.spotify.com/embed/0wl9Q3oedquNlBAJ4MGZtS
# e.g. 0wl9Q3oedquNlBAJ4MGZtS
# return: list of songs (song: artist - title)
# raises SpotifyWebsiteParserException if parsing the website goes wrong
return_data = []
url_info = parse_uri(playlist)
timestamp = get_server_time()
version, secret_string = get_secrets()
totp = generate_totp(timestamp, secret_string)
params = {
"reason": "init",
"productType": "web-player",
"totp": totp,
"totpVer": version,
"ts": timestamp,
}
req = requests.get(token_url, headers=headers, params=params, proxies={"https": proxy})
if req.status_code != 200:
raise SpotifyWebsiteParserException(
"ERROR: {} gave us not a 200 (version {}, totp {}). Instead: {}".format(token_url, version, totp, req.status_code))
token = req.json()
if url_info['type'] == "playlist":
url = playlist_base_url.format(url_info["id"])
while True:
resp = get_json_from_api(url, token["accessToken"], proxy)
if resp is None: # try again in case of rate limit
resp = get_json_from_api(url, token["accessToken"], proxy)
if resp is None:
break
for track in resp['items']:
return_data.append(parse_track(track["track"]))
if resp['next'] is None:
break
url = resp['next']
elif url_info["type"] == "track":
resp = get_json_from_api(track_base_url.format(url_info["id"]), token["accessToken"], proxy)
if resp is None: # try again in case of rate limit
resp = get_json_from_api(track_base_url.format(url_info["id"]), token["accessToken"], proxy)
return_data.append(parse_track(resp))
elif url_info["type"] == "album":
resp = get_json_from_api(album_base_url.format(url_info["id"]), token["accessToken"], proxy)
if resp is None: # try again in case of rate limit
resp = get_json_from_api(album_base_url.format(url_info["id"]), token["accessToken"], proxy)
for track in resp['items']:
return_data.append(parse_track(track))
return [track for track in return_data if track]
def parse_track(track):
artist = track['artists'][0]['name']
song = track['name']
full = "{} {}".format(artist, song)
# remove everything in brackets to get better search results later on Deezer
# e.g. (Radio Version) or (Remastered)
return re.sub(r'\([^)]*\)', '', full)
def get_json_from_api(api_url, access_token, proxy):
headers.update({'Authorization': 'Bearer {}'.format(access_token)})
req = requests.get(api_url, headers=headers, proxies={"https": proxy}, timeout=10)
if req.status_code == 429:
seconds = int(req.headers.get("Retry-After", "5")) + 1
print("INFO: rate limited! Sleeping for {} seconds".format(seconds))
sleep(seconds)
return None
if req.status_code != 200:
raise SpotifyWebsiteParserException("ERROR: {} gave us not a 200. Instead: {}".format(api_url, req.status_code))
return req.json()
if __name__ == '__main__':
# playlist = "21wZXvtrERELL0bVtKtuUh"
#playlist = "0wl9Q3oedquNlBAJ4MGZtS"
playlist = "76bsnIYWIZOCjCpSJB876p"
#album = "spotify:album:7zCODUHkfuRxsUjtuzNqbd"
#song = "https://open.spotify.com/track/6piFKF6WvM6ZZLmi2Vz8Vt"
#print(get_songs_from_spotify_website(playlist))
print(get_songs_from_spotify_website(playlist))

View File

@@ -0,0 +1,100 @@
import threading
import time
from queue import Queue
import traceback
local_obj = threading.local()
class ThreadpoolScheduler:
def __init__(self):
self.task_queue = Queue() # threadsafe queue where we put/get QueuedTask objects
self.worker_threads = [] # list of WorkerThread objects
# self.commands: {'function_name': function_pointer_to_the_function}
# {'download_deezer_song_and_queue': <function download_deezer_song_and_queue at 0x7ff81d739280>}
self.commands = {}
# list of all QueuedTask objects we processed during runtime (used by /queue)
self.all_tasks = []
def run_workers(self, num_workers):
for i in range(num_workers):
t = WorkerThread(i, self.task_queue)
t.start()
self.worker_threads.append(t)
def enqueue_task(self, description, command, **kwargs):
q = QueuedTask(description, command, self.commands[command], **kwargs)
self.task_queue.put(q)
self.all_tasks.append(q)
return q
def register_command(self):
def decorator(fun):
self.commands[fun.__name__] = fun
return fun
return decorator
def stop_workers(self):
for i in range(len(self.worker_threads)):
self.task_queue.put(False)
for worker in self.worker_threads:
worker.join()
# print("All workers stopped")
class WorkerThread(threading.Thread):
def __init__(self, index, task_queue):
super().__init__(daemon=True)
self.index = index # just an id per Worker
self.task_queue = task_queue # shared between all WorkerThreads
def run(self):
while True:
# print(f"Worker {self.index} is waiting for a task")
task = self.task_queue.get(block=True)
if not task:
# print(f"Worker {self.index} is exiting")
return
# print(f"Worker {self.index} is now working on task: {task.kwargs}")
task.state = "active"
self.ts_started = time.time()
task.worker_index = self.index
local_obj.current_task = task
try:
task.result = task.exec()
task.state = "mission accomplished"
except Exception as ex:
print(traceback.format_exc())
print(f"Task {task.fn_name} failed with parameters '{task.kwargs}'\nReason: {ex}")
task.state = "failed"
task.exception = ex
self.ts_finished = time.time()
# print(f"worker {self.index} is done with task: {task.kwargs} (state={task.state})")
class QueuedTask:
def __init__(self, description, fn_name, fn, **kwargs):
self.description = description
self.fn_name = fn_name
self.fn = fn
self.kwargs = kwargs
self.state = "waiting"
self.exception = None
self.result = None
self.progress = 0
self.progress_maximum = 0
self.ts_queued = time.time()
self.ts_started = 0
self.ts_finished = 0
def exec(self):
return self.fn(**self.kwargs)
def report_progress(value, maximum):
local_obj.current_task.progress = value
local_obj.current_task.progress_maximum = maximum

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python3
import os
from subprocess import Popen, PIPE
from functools import wraps
import requests
import atexit
from flask import Flask, render_template, request, jsonify
from markupsafe import escape
from flask_autoindex import AutoIndex
import warnings
import giphypop
from deezer_downloader.configuration import config
from deezer_downloader.web.music_backend import sched
from deezer_downloader.deezer import deezer_search, init_deezer_session
app = Flask(__name__)
auto_index = AutoIndex(app, config["download_dirs"]["base"], add_url_rules=False)
auto_index.add_icon_rule('music.png', ext='m3u8')
warnings.filterwarnings("ignore", message="You are using the giphy public api key")
giphy = giphypop.Giphy()
def init():
sched.run_workers(config.getint('threadpool', 'workers'))
init_deezer_session(config['proxy']['server'],
config['deezer']['quality'])
@atexit.register
def stop_workers():
sched.stop_workers()
init()
# user input validation
def validate_schema(*parameters_to_check):
def decorator(f):
@wraps(f)
def wrapper(*args, **kw):
j = request.get_json(force=True)
print("User request: {} with {}".format(request.path, j))
# check if all parameters are supplied by the user
if set(j.keys()) != set(parameters_to_check):
return jsonify({"error": 'parameters missing, required fields: {}'.format(parameters_to_check)}), 400
if "type" in j.keys():
if j['type'] not in ["album", "track", "artist", "album_track", "artist_album", "artist_top"]:
return jsonify({"error": "type must be album, track, artist, album_track, artist_album or artist_top"}), 400
if "music_id" in j.keys():
if type(j['music_id']) is not int:
return jsonify({"error": "music_id must be a integer"}), 400
if "add_to_playlist" in j.keys():
if type(j['add_to_playlist']) is not bool:
return jsonify({"error": "add_to_playlist must be a boolean"}), 400
if "create_zip" in j.keys():
if type(j['create_zip']) is not bool:
return jsonify({"error": "create_zip must be a boolean"}), 400
if "query" in j.keys():
if type(j['query']) is not str:
return jsonify({"error": "query is not a string"}), 400
if j['query'] == "":
return jsonify({"error": "query is empty"}), 400
if "url" in j.keys():
if (type(j['url']) is not str) or (not j['url'].startswith("http")):
return jsonify({"error": "url is not a url. http... only"}), 400
if "playlist_url" in j.keys():
if type(j['playlist_url']) is not str:
return jsonify({"error": "playlist_url is not a string"}), 400
if len(j['playlist_url'].strip()) == 0:
return jsonify({"error": "playlist_url is empty"}), 400
if "playlist_name" in j.keys():
if type(j['playlist_name']) is not str:
return jsonify({"error": "playlist_name is not a string"}), 400
if len(j['playlist_name'].strip()) == 0:
return jsonify({"error": "playlist_name is empty"}), 400
if "user_id" in j.keys():
if type(j['user_id']) is not str or not j['user_id'].isnumeric():
return jsonify({"error": "user_id must be a numeric string"}), 400
return f(*args, **kw)
return wrapper
return decorator
@app.route("/")
def index():
return render_template("index.html",
api_root=config["http"]["api_root"],
static_root=config["http"]["static_root"],
use_mpd=str(config['mpd'].getboolean('use_mpd')).lower())
@app.route("/debug")
def show_debug():
if "LOG_FILE" in os.environ:
# check env LOG_FILE in Dockerfile
# overwriting config value when using Docker
cmd = f"tail -n 100 {os.environ['LOG_FILE']}"
else:
cmd = config["debug"]["command"]
p = Popen(cmd, shell=True, stdout=PIPE)
p.wait()
stdout, __ = p.communicate()
return jsonify({'debug_msg': stdout.decode()})
@app.route("/downloads/")
@app.route("/downloads/<path:path>")
def autoindex(path="."):
# directory index - flask version (let the user download mp3/zip in the browser)
try:
gif = giphy.random_gif(tag="cat")
media_url = gif.media_url
except requests.exceptions.HTTPError:
# the api is rate-limited. Fallback:
media_url = "https://cataas.com/cat"
template_context = {'gif_url': media_url}
return auto_index.render_autoindex(path, template_context=template_context)
@app.route('/queue', methods=['GET'])
def show_queue():
"""
shows queued tasks
return:
json: [ { tasks } ]
"""
results = [
{'id': id(task),
'description': escape(task.description),
#'command': task.fn_name,
'args': escape(task.kwargs),
'state': escape(task.state),
'result': escape(task.result),
'exception': escape(str(task.exception)),
'progress': [task.progress, task.progress_maximum]
} for task in sched.all_tasks
]
return jsonify(results)
@app.route('/search', methods=['POST'])
@validate_schema("type", "query")
def search():
"""
searches for available music in the Deezer library
para:
type: track|album|album_track
query: search query
return:
json: [ { artist, id, (title|album) } ]
"""
user_input = request.get_json(force=True)
results = deezer_search(user_input['query'], user_input['type'])
return jsonify(results)
@app.route('/download', methods=['POST'])
@validate_schema("type", "music_id", "add_to_playlist", "create_zip")
def deezer_download_song_or_album():
"""
downloads a song or an album from Deezer to the dir specified in settings.py
para:
type: album|track
music_id: id of the album or track (int)
add_to_playlist: True|False (add to mpd playlist)
create_zip: True|False (create a zip for the album)
"""
user_input = request.get_json(force=True)
desc = "Downloading {}".format(user_input['type'])
if user_input['type'] == "track":
task = sched.enqueue_task(desc, "download_deezer_song_and_queue",
track_id=user_input['music_id'],
add_to_playlist=user_input['add_to_playlist'])
else:
task = sched.enqueue_task(desc, "download_deezer_album_and_queue_and_zip",
album_id=user_input['music_id'],
add_to_playlist=user_input['add_to_playlist'],
create_zip=user_input['create_zip'])
return jsonify({"task_id": id(task), })
@app.route('/youtubedl', methods=['POST'])
@validate_schema("url", "add_to_playlist")
def youtubedl_download():
"""
takes an url and tries to download it via youtuble-dl
para:
url: link to youtube (or something youtube-dl supports)
add_to_playlist: True|False (add to mpd playlist)
"""
user_input = request.get_json(force=True)
desc = "Downloading via youtube-dl"
task = sched.enqueue_task(desc, "download_youtubedl_and_queue",
video_url=user_input['url'],
add_to_playlist=user_input['add_to_playlist'])
return jsonify({"task_id": id(task), })
@app.route('/playlist/deezer', methods=['POST'])
@validate_schema("playlist_url", "add_to_playlist", "create_zip")
def deezer_playlist_download():
"""
downloads songs of a public Deezer playlist.
A directory with the name of the playlist will be created.
para:
playlist_url: link to a public Deezer playlist (the id of the playlist works too)
add_to_playlist: True|False (add to mpd playlist)
create_zip: True|False (create a zip for the playlist)
"""
user_input = request.get_json(force=True)
desc = "Downloading Deezer playlist"
task = sched.enqueue_task(desc, "download_deezer_playlist_and_queue_and_zip",
playlist_id=user_input['playlist_url'],
add_to_playlist=user_input['add_to_playlist'],
create_zip=user_input['create_zip'])
return jsonify({"task_id": id(task), })
@app.route('/playlist/spotify', methods=['POST'])
@validate_schema("playlist_name", "playlist_url", "add_to_playlist", "create_zip")
def spotify_playlist_download():
"""
1. /GET and parse the Spotify playlist (html)
2. search every single song on Deezer. Use the first hit
3. download the song from Deezer
para:
playlist_name: name of the playlist (used for the subfolder)
playlist_url: link to Spotify playlist or just the id of it
add_to_playlist: True|False (add to mpd playlist)
create_zip: True|False (create a zip for the playlist)
"""
user_input = request.get_json(force=True)
desc = "Downloading Spotify playlist"
task = sched.enqueue_task(desc, "download_spotify_playlist_and_queue_and_zip",
playlist_name=user_input['playlist_name'],
playlist_id=user_input['playlist_url'],
add_to_playlist=user_input['add_to_playlist'],
create_zip=user_input['create_zip'])
return jsonify({"task_id": id(task), })
@app.route('/favorites/deezer', methods=['POST'])
@validate_schema("user_id", "add_to_playlist", "create_zip")
def deezer_favorites_download():
"""
downloads favorite songs of a Deezer user (looks like this in the brwoser:
https://www.deezer.com/us/profile/%%user_id%%/loved)
a subdirecotry with the name of the user_id will be created.
para:
user_id: deezer user_id
add_to_playlist: True|False (add to mpd playlist)
create_zip: True|False (create a zip for the playlist)
"""
user_input = request.get_json(force=True)
desc = "Downloading Deezer favorites"
task = sched.enqueue_task(desc, "download_deezer_favorites",
user_id=user_input['user_id'],
add_to_playlist=user_input['add_to_playlist'],
create_zip=user_input['create_zip'])
return jsonify({"task_id": id(task), })

View File

@@ -0,0 +1,256 @@
import time
import os.path
from os.path import basename
import mpd
import platform
from zipfile import ZipFile, ZIP_DEFLATED
from deezer_downloader.configuration import config
from deezer_downloader.youtubedl import youtubedl_download
from deezer_downloader.spotify import get_songs_from_spotify_website
from deezer_downloader.deezer import TYPE_TRACK, TYPE_ALBUM, TYPE_PLAYLIST, get_song_infos_from_deezer_website, download_song, parse_deezer_playlist, deezer_search, get_deezer_favorites
from deezer_downloader.deezer import Deezer403Exception, Deezer404Exception, DeezerApiException
from deezer_downloader.deezer import get_file_extension
from deezer_downloader.threadpool_queue import ThreadpoolScheduler, report_progress
sched = ThreadpoolScheduler()
def check_download_dirs_exist():
for directory in [config["download_dirs"]["songs"], config["download_dirs"]["zips"], config["download_dirs"]["albums"],
config["download_dirs"]["playlists"], config["download_dirs"]["youtubedl"]]:
os.makedirs(directory, exist_ok=True)
check_download_dirs_exist()
def make_song_paths_relative_to_mpd_root(songs, prefix=""):
# ensure last slash
config["mpd"]["music_dir_root"] = os.path.join(config["mpd"]["music_dir_root"], '')
songs_paths_relative_to_mpd_root = []
for song in songs:
songs_paths_relative_to_mpd_root.append(prefix + song[len(config["mpd"]["music_dir_root"]):])
return songs_paths_relative_to_mpd_root
def update_mpd_db(songs, add_to_playlist):
# songs: list of music files or just a string (file path)
if not config["mpd"].getboolean("use_mpd"):
return
print("Updating mpd database")
timeout_counter = 0
mpd_client = mpd.MPDClient(use_unicode=True)
try:
mpd_client.connect(config["mpd"]["host"], config["mpd"].getint("port"))
except ConnectionRefusedError as e:
print("ERROR connecting to MPD ({}:{}): {}".format(config["mpd"]["host"], config["mpd"]["port"], e))
return
mpd_client.update()
if add_to_playlist:
songs = [songs] if type(songs) is not list else songs
songs = make_song_paths_relative_to_mpd_root(songs)
while len(mpd_client.search("file", songs[0])) == 0:
# c.update() does not block so wait for it
if timeout_counter == 10:
print("Tried it {} times. Give up now.".format(timeout_counter))
return
print("'{}' not found in the music db. Let's wait for it".format(songs[0]))
timeout_counter += 1
time.sleep(2)
for song in songs:
try:
mpd_client.add(song)
print("Added to mpd playlist: '{}'".format(song))
except mpd.base.CommandError as mpd_error:
print("ERROR adding '{}' to playlist: {}".format(song, mpd_error))
def clean_filename(path):
path = path.replace("\t", " ")
if any(platform.win32_ver()):
path.replace("\"", "'")
array_of_special_characters = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
else:
array_of_special_characters = ['/', ':', '"', '?']
return ''.join([c for c in path if c not in array_of_special_characters])
def download_song_and_get_absolute_filename(search_type, song, playlist_name=None):
file_extension = get_file_extension()
if search_type == TYPE_ALBUM:
song_filename = "{:02d} - {} {}.{}".format(int(song['TRACK_NUMBER']),
song['ART_NAME'],
song['SNG_TITLE'],
file_extension)
else:
song_filename = "{} - {}.{}".format(song['ART_NAME'],
song['SNG_TITLE'],
file_extension)
song_filename = clean_filename(song_filename)
if search_type == TYPE_TRACK:
absolute_filename = os.path.join(config["download_dirs"]["songs"], song_filename)
elif search_type == TYPE_ALBUM:
album_name = "{} - {}".format(song['ART_NAME'], song['ALB_TITLE'])
album_name = clean_filename(album_name)
album_dir = os.path.join(config["download_dirs"]["albums"], album_name)
if not os.path.exists(album_dir):
os.mkdir(album_dir)
absolute_filename = os.path.join(album_dir, song_filename)
elif search_type == TYPE_PLAYLIST:
assert type(playlist_name) is str
playlist_name = clean_filename(playlist_name)
playlist_dir = os.path.join(config["download_dirs"]["playlists"], playlist_name)
if not os.path.exists(playlist_dir):
os.mkdir(playlist_dir)
absolute_filename = os.path.join(playlist_dir, song_filename)
if os.path.exists(absolute_filename):
print("Skipping song '{}'. Already exists.".format(absolute_filename))
else:
print("Downloading '{}'".format(song_filename))
download_song(song, absolute_filename)
return absolute_filename
def create_zip_file(songs_absolute_location):
# take first song in list and take the parent dir (name of album/playlist")
parent_dir = basename(os.path.dirname(songs_absolute_location[0]))
location_zip_file = os.path.join(config["download_dirs"]["zips"], "{}.zip".format(parent_dir))
print("Creating zip file '{}'".format(location_zip_file))
with ZipFile(location_zip_file, 'w', compression=ZIP_DEFLATED) as zip:
for song_location in songs_absolute_location:
try:
print("Adding song {}".format(song_location))
zip.write(song_location, arcname=os.path.join(parent_dir, basename(song_location)))
except FileNotFoundError:
print("Could not find file '{}'".format(song_location))
print("Done with the zip")
return location_zip_file
def create_m3u8_file(songs_absolute_location):
playlist_directory, __ = os.path.split(songs_absolute_location[0])
# 00 as prefix => will be shown as first in dir listing
m3u8_filename = "00 {}.m3u8".format(os.path.basename(playlist_directory))
print("Creating m3u8 file: '{}'".format(m3u8_filename))
m3u8_file_abs = os.path.join(playlist_directory, m3u8_filename)
with open(m3u8_file_abs, "w", encoding="utf-8") as f:
for song in songs_absolute_location:
if os.path.exists(song):
f.write(basename(song) + "\n")
# add m3u8_file so that will be zipped to
songs_absolute_location.append(m3u8_file_abs)
return songs_absolute_location
@sched.register_command()
def download_deezer_song_and_queue(track_id, add_to_playlist):
song = get_song_infos_from_deezer_website(TYPE_TRACK, track_id)
try:
absolute_filename = download_song_and_get_absolute_filename(TYPE_TRACK, song)
update_mpd_db(absolute_filename, add_to_playlist)
return make_song_paths_relative_to_mpd_root([absolute_filename])
except DeezerApiException:
# warning is printed in download_song_and_get_absolute_filename
pass
@sched.register_command()
def download_deezer_album_and_queue_and_zip(album_id, add_to_playlist, create_zip):
songs = get_song_infos_from_deezer_website(TYPE_ALBUM, album_id)
songs_absolute_location = []
for i, song in enumerate(songs):
report_progress(i, len(songs))
assert type(song) is dict
try:
absolute_filename = download_song_and_get_absolute_filename(TYPE_ALBUM, song)
songs_absolute_location.append(absolute_filename)
except Exception as e:
print(f"Warning: {e}. Continuing with album...")
update_mpd_db(songs_absolute_location, add_to_playlist)
if create_zip:
return [create_zip_file(songs_absolute_location)]
return make_song_paths_relative_to_mpd_root(songs_absolute_location)
@sched.register_command()
def download_deezer_playlist_and_queue_and_zip(playlist_id, add_to_playlist, create_zip):
playlist_name, songs = parse_deezer_playlist(playlist_id)
songs_absolute_location = []
for i, song in enumerate(songs):
report_progress(i, len(songs))
try:
absolute_filename = download_song_and_get_absolute_filename(TYPE_PLAYLIST, song, playlist_name)
songs_absolute_location.append(absolute_filename)
except Exception as e:
print(f"Warning: {e}. Continuing with playlist...")
update_mpd_db(songs_absolute_location, add_to_playlist)
songs_with_m3u8_file = create_m3u8_file(songs_absolute_location)
if create_zip:
return [create_zip_file(songs_with_m3u8_file)]
return make_song_paths_relative_to_mpd_root(songs_absolute_location)
@sched.register_command()
def download_spotify_playlist_and_queue_and_zip(playlist_name, playlist_id, add_to_playlist, create_zip):
songs = get_songs_from_spotify_website(playlist_id,
config["proxy"]["server"])
songs_absolute_location = []
print(f"We got {len(songs)} songs from the Spotify playlist")
for i, song_of_playlist in enumerate(songs):
report_progress(i, len(songs))
# song_of_playlist: string (artist - song)
try:
track_id = deezer_search(song_of_playlist, TYPE_TRACK)[0]['id'] #[0] can throw IndexError
song = get_song_infos_from_deezer_website(TYPE_TRACK, track_id)
absolute_filename = download_song_and_get_absolute_filename(TYPE_PLAYLIST, song, playlist_name)
songs_absolute_location.append(absolute_filename)
except Exception as e:
print(f"Warning: Could not download Spotify song ({song_of_playlist}) on Deezer: {e}")
update_mpd_db(songs_absolute_location, add_to_playlist)
songs_with_m3u8_file = create_m3u8_file(songs_absolute_location)
if create_zip:
return [create_zip_file(songs_with_m3u8_file)]
return make_song_paths_relative_to_mpd_root(songs_absolute_location)
@sched.register_command()
def download_youtubedl_and_queue(video_url, add_to_playlist):
filename_absolute = youtubedl_download(video_url,
config["download_dirs"]["youtubedl"],
config["proxy"]["server"])
update_mpd_db(filename_absolute, add_to_playlist)
return make_song_paths_relative_to_mpd_root([filename_absolute])
@sched.register_command()
def download_deezer_favorites(user_id: str, add_to_playlist: bool, create_zip: bool):
songs_absolute_location = []
output_directory = f"favorites_{user_id}"
favorite_songs = get_deezer_favorites(user_id)
for i, fav_song in enumerate(favorite_songs):
report_progress(i, len(favorite_songs))
try:
song = get_song_infos_from_deezer_website(TYPE_TRACK, fav_song)
try:
absolute_filename = download_song_and_get_absolute_filename(TYPE_PLAYLIST, song, output_directory)
songs_absolute_location.append(absolute_filename)
except Exception as e:
print(f"Warning: {e}. Continuing with favorties...")
except (IndexError, Deezer403Exception, Deezer404Exception) as msg:
print(msg)
print(f"Could not find song ({fav_song}) on Deezer?")
update_mpd_db(songs_absolute_location, add_to_playlist)
songs_with_m3u8_file = create_m3u8_file(songs_absolute_location)
if create_zip:
return [create_zip_file(songs_with_m3u8_file)]
return make_song_paths_relative_to_mpd_root(songs_absolute_location)
if __name__ == '__main__':
pass
#download_spotify_playlist_and_queue_and_zip("test", '21wZXvtrERELL0bVtKtuUh', False, False)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,47 @@
.search-container {
display: flex;
height: 38px;
border: 1px solid #ced4da;
border-radius: 7px;
padding: 0 8px;
}
input.search {
flex: 1;
height: 100%;
font-size: 14px;
border: none;
outline: none;
background-color: transparent;
color: #495057;
}
button.search {
height: 100%;
color: #495057;
font-size: 14px;
border: none;
background-color: transparent;
}
.tabs-category {
display: flex;
/* border-bottom: 1px solid #ced4da; */
}
.btn-category {
margin-top: 10px;
padding: 8px 16px;
border: none;
border-bottom: 3px solid transparent;
background-color: transparent;
cursor: pointer;
font-size: 14px;
color: #6a6a6a;
outline: none !important;
}
.btn-category.active {
border-bottom: 3px solid #242424;
color: #242424;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.jGrowl{z-index:9999;color:#fff;font-size:12px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;position:fixed}.jGrowl.top-left{left:0;top:0}.jGrowl.top-right{right:0;top:0}.jGrowl.bottom-left{left:0;bottom:0}.jGrowl.bottom-right{right:0;bottom:0}.jGrowl.center{top:0;width:50%;left:25%}.jGrowl.center .jGrowl-closer,.jGrowl.center .jGrowl-notification{margin-left:auto;margin-right:auto}.jGrowl-notification{background-color:#000;opacity:.9;-ms-filter:alpha(90);filter:alpha(90);zoom:1;width:250px;padding:10px;margin:10px;text-align:left;display:none;border-radius:5px;min-height:40px}.jGrowl-notification .ui-state-highlight,.jGrowl-notification .ui-widget-content .ui-state-highlight,.jGrowl-notification .ui-widget-header .ui-state-highlight{border:1px solid #000;background:#000;color:#fff}.jGrowl-notification .jGrowl-header{font-weight:700;font-size:.85em}.jGrowl-notification .jGrowl-close{background-color:transparent;color:inherit;border:none;z-index:99;float:right;font-weight:700;font-size:1em;cursor:pointer}.jGrowl-closer{background-color:#000;opacity:.9;-ms-filter:alpha(90);filter:alpha(90);zoom:1;width:250px;padding:10px;margin:10px;display:none;border-radius:5px;padding-top:4px;padding-bottom:4px;cursor:pointer;font-size:.9em;font-weight:700;text-align:center}.jGrowl-closer .ui-state-highlight,.jGrowl-closer .ui-widget-content .ui-state-highlight,.jGrowl-closer .ui-widget-header .ui-state-highlight{border:1px solid #000;background:#000;color:#fff}@media print{.jGrowl{display:none}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,358 @@
function deezer_download(music_id, type, add_to_playlist, create_zip) {
$.post(deezer_downloader_api_root + '/download',
JSON.stringify({ type: type, music_id: parseInt(music_id), add_to_playlist: add_to_playlist, create_zip: create_zip}),
function(data) {
if(create_zip == true) {
text = "You like being offline? You will get a zip file!";
}
else if(type == "album") {
if(add_to_playlist == true) {
text = "Good choice! The album will be downloaded and queued to the playlist";
} else {
text = "Good choice! The album will be downloaded.";
}
} else {
if(add_to_playlist == true) {
text = "Good choice! The song will be downloaded and queued to the playlist";
} else {
text = "Good choice! The song will be downloaded.";
}
}
$.jGrowl(text, { life: 4000 });
console.log(data);
});
}
function play_preview(src) {
$("#audio_tag").attr("src", src)[0].play();
}
$(document).ready(function() {
if(!show_mpd_features) {
$("#yt_download_play").hide()
$("#spotify_download_play").hide()
$("#deezer_playlist_download_play").hide()
$("#deezer_favorites_download_play").hide()
};
function youtubedl_download(add_to_playlist) {
$.post(deezer_downloader_api_root + '/youtubedl',
JSON.stringify({ url: $('#youtubedl-query').val(), add_to_playlist: add_to_playlist }),
function(data) {
console.log(data);
$.jGrowl("As you wish", { life: 4000 });
});
}
function spotify_playlist_download(add_to_playlist, create_zip) {
$.post(deezer_downloader_api_root + '/playlist/spotify',
JSON.stringify({ playlist_name: $('#spotify-playlist-name').val(),
playlist_url: $('#spotify-playlist-url').val(),
add_to_playlist: add_to_playlist,
create_zip: create_zip}),
function(data) {
console.log(data);
$.jGrowl("As you wish", { life: 4000 });
});
}
function deezer_playlist_download(add_to_playlist, create_zip) {
$.post(deezer_downloader_api_root + '/playlist/deezer',
JSON.stringify({ playlist_url: $('#deezer-playlist-url').val(),
add_to_playlist: add_to_playlist,
create_zip: create_zip}),
function(data) {
console.log(data);
$.jGrowl("As you wish", { life: 4000 });
});
}
function deezer_favorites_download(add_to_playlist, create_zip) {
$.post(deezer_downloader_api_root + '/favorites/deezer',
JSON.stringify({ user_id: $('#deezer-favorites-userid').val(),
add_to_playlist: add_to_playlist,
create_zip: create_zip}),
function(data) {
console.log(data);
$.jGrowl("As you wish", { life: 4000 });
});
}
function search(type) {
const query = $('#songs-albums-query').val();
if (!query.length) return ;
deezer_load_list(type, query);
}
function deezer_load_list(type, query) {
$.post(deezer_downloader_api_root + '/search',
JSON.stringify({ type: type, query: query.toString() }),
function(data) {
$("#results > tbody").html("");
for (var i = 0; i < data.length; i++) {
drawTableEntry(data[i], type);
}
});
}
function drawTableEntry(rowData, mtype) {
var row = $("<tr>");
$("#results").append(row);
var button_col = $("<td style='text-align: end'>");
if (mtype === "track" || mtype === "album_track" || mtype === "artist_top") {
$("#col-title").show();
$("#col-album").show();
$("#col-artist").show();
if (mtype !== "album_track") {
$("#col-cover").show();
row.append($("<td><img src='"+rowData.img_url+"' style='cursor: pointer; border-radius: 3px'></td>")
.click(() => play_preview(rowData.preview_url)));
} else {
$("#col-cover").hide();
}
row.append($("<td>" + rowData.artist + "</td>"));
row.append($("<td>" + rowData.title + "</td>"));
row.append($("<td>" + rowData.album + "</td>"));
if (rowData.preview_url) {
button_col.append($('<button class="btn btn-default"> <i class="fa fa-headphones fa-lg" title="listen preview in browser" ></i> </button>')
.click(() => play_preview(rowData.preview_url)));
}
} else if (mtype === "album" || mtype === "artist_album") {
$("#col-cover").show();
$("#col-title").hide();
$("#col-album").show();
$("#col-artist").show();
row.append($("<td><img src='"+rowData.img_url+"' style='cursor: pointer; border-radius: 3px'></td>")
.click(() => deezer_load_list("album_track", rowData.album_id)));
row.append($("<td>" + rowData.artist + "</td>"));
row.append($("<td>" + rowData.album + "</td>"));
button_col.append($('<button class="btn btn-default"> <i class="fa fa-list fa-lg" title="list album songs" ></i> </button>')
.click(() => deezer_load_list("album_track", rowData.album_id)));
} else if (mtype === "artist") {
$("#col-cover").show();
$("#col-artist").show();
$("#col-album").hide();
$("#col-title").hide();
row.append($("<td><img src='"+rowData.img_url+"' style='cursor: pointer; border-radius: 29px'></td>")
.click(() => deezer_load_list("artist_album", rowData.artist_id)));
row.append($("<td>" + rowData.artist + "</td>"));
button_col.append($('<button class="btn btn-default"> <i class="fa fa-arrow-up fa-lg" title="list artist top songs" ></i> </button>')
.click(() => deezer_load_list("artist_top", rowData.artist_id)));
button_col.append($('<button class="btn btn-default"> <i class="fa fa-list fa-lg" title="list artist albums" ></i> </button>')
.click(() => deezer_load_list("artist_album", rowData.artist_id)));
}
if (mtype !== "artist") {
if (show_mpd_features) {
button_col.append($('<button class="btn btn-default"> <i class="fa fa-play-circle fa-lg" title="download and queue to mpd" ></i> </button>')
.click(() => deezer_download(rowData.id, rowData.id_type, true, false)));
}
button_col.append($('<button class="btn btn-default" > <i class="fa fa-download fa-lg" title="download" ></i> </button>')
.click(() => deezer_download(rowData.id, rowData.id_type, false, false)));
}
if(rowData.id_type == "album") {
button_col.append($('<button class="btn btn-default"> <i class="fa fa-file-archive-o fa-lg" title="download as zip file" ></i> </button>')
.click(() => deezer_download(rowData.id, rowData.id_type, false, true)));
}
row.append(button_col);
}
function show_debug_log() {
$.get(deezer_downloader_api_root + '/debug', function(data) {
var debug_log_textarea = $("#ta-debug-log");
debug_log_textarea.val(data.debug_msg);
if(debug_log_textarea.length) {
debug_log_textarea.scrollTop(debug_log_textarea[0].scrollHeight - debug_log_textarea.height());
}
});
}
function show_task_queue() {
$.get(deezer_downloader_api_root + '/queue', function(data) {
var queue_table = $("#task-list tbody");
queue_table.html("");
for (var i = data.length - 1; i >= 0; i--) {
var html="<tr><td>"+data[i].description+"</td><td>"+JSON.stringify(data[i].args)+"</td>"+
"<td>"+data[i].state+"</td></tr>";
$(html).appendTo(queue_table);
switch (data[i].state) {
case "active":
$("<tr><td colspan=4><progress value="+data[i].progress[0]+" max="+data[i].progress[1]+" style='width:100%'/></td></tr>").appendTo(queue_table);
case "failed":
$("<tr><td colspan=4 style='color:red'>"+data[i].exception+"</td></tr>").appendTo(queue_table);
}
}
if ($("#nav-task-queue").hasClass("active")) {
setTimeout(show_task_queue, 1000);
}
});
}
let search_type = "track";
$("#search_deezer").click(function() {
search(search_type);
});
$("#deezer-search-track").click(function() {
if (search_type == "track") return;
search_type = "track";
$("#deezer-search-track").addClass("active");
$("#deezer-search-album").removeClass("active");
$("#deezer-search-artist").removeClass("active");
search(search_type);
});
$("#deezer-search-album").click(function() {
if (search_type == "album") return;
search_type = "album";
$("#deezer-search-album").addClass("active");
$("#deezer-search-track").removeClass("active");
$("#deezer-search-artist").removeClass("active");
search(search_type);
});
$("#deezer-search-artist").click(function() {
if (search_type == "artist") return;
search_type = "artist";
$("#deezer-search-artist").addClass("active");
$("#deezer-search-track").removeClass("active");
$("#deezer-search-album").removeClass("active");
search(search_type);
});
$("#yt_download").click(function() {
youtubedl_download(false);
});
$("#yt_download_play").click(function() {
youtubedl_download(true);
});
$("#nav-debug-log").click(function() {
show_debug_log();
});
$("#nav-task-queue").click(function() {
show_task_queue();
});
// BEGIN SPOTIFY
$("#spotify_download_play").click(function() {
spotify_playlist_download(true, false);
});
$("#spotify_download").click(function() {
spotify_playlist_download(false, false);
});
$("#spotify_zip").click(function() {
spotify_playlist_download(false, true);
});
// END SPOTIFY
// BEGIN DEEZER PLAYLIST
$("#deezer_playlist_download_play").click(function() {
deezer_playlist_download(true, false);
});
$("#deezer_playlist_download").click(function() {
deezer_playlist_download(false, false);
});
$("#deezer_playlist_zip").click(function() {
deezer_playlist_download(false, true);
});
// END DEEZER PLAYLIST
// BEGIN DEEZER FAVORITE SONGS
$("#deezer_favorites_download_play").click(function() {
deezer_favorites_download(true, false);
});
$("#deezer_favorites_download").click(function() {
deezer_favorites_download(false, false);
});
$("#deezer_favorites_zip").click(function() {
deezer_favorites_download(false, true);
});
// END DEEZER FAVORITE SONGS
function show_tab(id_nav, id_content) {
// nav
$(".nav-link").removeClass("active")
//$("#btn-show-debug").addClass("active")
$("#" + id_nav).addClass("active")
// content
$(".container .tab-pane").removeClass("active show")
//$("#youtubedl").addClass("active show")
$("#" + id_content).addClass("active show")
}
var bbody = document.getElementById('body');
bbody.onkeydown = function (event) {
if (event.key !== undefined) {
if (event.key === 'Enter' ) {
search(search_type);
} else if (event.key === 'm' && event.ctrlKey) {
$("#songs-albums-query")[0].value = "";
$("#songs-albums-query")[0].focus();
}
if (event.ctrlKey && event.shiftKey) {
console.log("pressed ctrl + shift + ..");
if(event.key === '!') {
id_nav = "nav-songs-albums";
id_content = "songs_albums";
}
if(event.key === '"') {
id_nav = "nav-youtubedl";
id_content = "youtubedl";
}
if(event.key === '§') {
id_nav = "nav-spotify-playlists";
id_content = "spotify-playlists";
}
if(event.key === '$') {
id_nav = "nav-deezer-playlists";
id_content = "deezer-playlists";
}
if(event.key === '%') {
id_nav = "nav-songs-albums";
id_content = "songs_albums";
window.open('/downloads/', '_blank');
}
if(event.key === "&") {
id_nav = "nav-debug-log";
id_content = "debug";
show_debug_log();
}
if(event.key === '/') {
id_nav = "nav-task-queue";
id_content = "queue";
}
if(typeof id_nav !== 'undefined') {
show_tab(id_nav, id_content);
}
}
}
};
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
{% extends '__autoindex__/autoindex.html' %}
{% block footer %}
<br>
<div style="text-align: center" class="img-container">
<img height=400px; src="{{ gif_url }}" />
</div>
{% endblock %}

View File

@@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Good Music - Good Feeling</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ static_root }}/css/bootstrap.min.css">
<script>
window.deezer_downloader_api_root = '{{ api_root }}';
</script>
<script src="{{ static_root }}/js/jquery.min.js"></script>
<script src="{{ static_root }}/js/popper.min.js"></script>
<script src="{{ static_root }}/js/bootstrap.min.js"></script>
<script>
var show_mpd_features = {{ use_mpd }};
</script>
<script src="{{ static_root }}/js/custom.js"></script>
<script src="{{ static_root }}/js/jquery.jgrowl.min.js"></script>
<link rel="stylesheet" href="{{ static_root }}/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="{{ static_root }}/css/jquery.jgrowl.min.css" />
<link rel="stylesheet" type="text/css" href="{{ static_root }}/css/custom.css" />
<style>
.footer {
position: fixed;
bottom: 0;
width: 80%;
left: 10%;
height: 40px;
background-color: #f5f5f5;
text-align: center;
}
</style>
</head>
<body id="body" >
<div class="container">
<br>
<div class="row">
<ul id="navigation" class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="nav-songs-albums" data-toggle="tab" href="#songs_albums">Songs/Albums (1)</a>
</li>
<li class="nav-item">
<a class="nav-link" id="nav-youtubedl" data-toggle="tab" href="#youtubedl">Youtube-dl (2)</a>
</li>
<li class="nav-item">
<a class="nav-link" id="nav-spotify-playlists" data-toggle="tab" href="#spotify-playlists">Spotify Playlists (3)</a>
</li>
<li class="nav-item">
<a class="nav-link" id="nav-deezer" data-toggle="tab" href="#deezer">Deezer (4)</a>
</li>
<li class="nav-item">
<a class="nav-link" id="nav-file-downloads" href="downloads/" target="_blank" >Files (5)</a>
</li>
<li class="nav-item">
<a class="nav-link" id="nav-debug-log" data-toggle="tab" href="#debug">Debug (6)</a>
</li>
<li class="nav-item">
<a class="nav-link" id="nav-task-queue" data-toggle="tab" href="#queue">Queue (7)</a>
</li>
</ul>
</div> <!-- end row -->
<!-- Tab panes -->
<div class="tab-content">
<div id="songs_albums" class="container tab-pane active">
<br>
<audio src="" controls style="float:right" id="audio_tag"></audio>
<h3>Download songs and albums</h3>
<div class="input-group">
<div class="search-container col-md-6 col-lg-5 col-xs-12">
<input type="text" class="search" id="songs-albums-query" placeholder="Search for ..." >
<button type="button" class="search" onclick="$('#songs-albums-query').val('')" class="search">
<i class="fa fa-times"></i>
</button>
<button type="button" class="search" id="search_deezer">
<i class="fa fa-search"></i>
</button>
</div>
</div>
<div class="tabs-category">
<button id="deezer-search-track" class="btn-category active" href="#">Tracks</button>
<button id="deezer-search-album" class="btn-category" href="#">Albums</button>
<button id="deezer-search-artist" class="btn-category" href="#">Artists</button>
</div>
<table id="results" class="table">
<thead>
<tr>
<th id="col-cover" style="width: 56px"></th>
<th id="col-artist">Artist</th>
<th id="col-title">Title</th>
<th id="col-album">Album</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div> <!-- end div tab songs/albums -->
<div id="youtubedl" class="container tab-pane fade">
<br>
<h3>Download stuff via youtube-dl</h3>
<div class="input-group">
<input type="text" class="form-control" id="youtubedl-query" placeholder="Download audio from YouTube, Invidious, Vimeo, Soundcloud, YouPorn, ... " />
&nbsp;
</div>
<br>
<span class="input-group-btn">
<button type="button" class="btn btn-info" id="yt_download_play">Download & Play</button>
<button type="button" class="btn btn-info" id="yt_download">Download</button>
<button type="button" class="btn btn-info" onclick="$('#youtubedl-query').val('')" >Clear</button>
</span>
</div> <!-- end div tab youtube-dl -->
<div id="spotify-playlists" class="container tab-pane fade">
<br>
<h3>Download Spotify playlist</h3>
<div class="input-group">
<input type="text" class="form-control" id="spotify-playlist-name" placeholder="name of spotify playlist" />
&nbsp;
<input type="text" class="form-control" id="spotify-playlist-url" placeholder="url or id of the playlist" />
&nbsp;
</div>
<br>
<div class="input-group">
<span class="input-group-btn">
<button type="button" class="btn btn-info" id="spotify_download_play">Download & Play</button>
<button type="button" class="btn btn-info" id="spotify_download">Download</button>
<button type="button" class="btn btn-info" id="spotify_zip">Give me a zip</button>
<button type="button" class="btn btn-info" onclick="$('input[id^=\'spotify\']').val('')" >Clear</button>
</span>
</div>
</div> <!-- end div tab spotify playlists -->
<div id="deezer" class="container tab-pane fade">
<br>
<h3>Download Deezer playlist</h3>
<div class="input-group">
<input type="text" class="form-control" id="deezer-playlist-url" placeholder="url or id of the playlist" />
&nbsp;
</div>
<br>
<div class="input-group">
<span class="input-group-btn">
<button type="button" class="btn btn-info" id="deezer_playlist_download_play">Download & Play</button>
<button type="button" class="btn btn-info" id="deezer_playlist_download">Download</button>
<button type="button" class="btn btn-info" id="deezer_playlist_zip">Give me a zip</button>
<button type="button" class="btn btn-info" onclick="$('#deezer-playlist-url').val('')" >Clear</button>
</span>
</div>
<br>
<h3>Download Deezer favorite songs</h3>
<div class="input-group">
<input type="text" class="form-control" id="deezer-favorites-userid" placeholder="user id of Deezer user"songs />
&nbsp;
</div>
<br>
<div class="input-group">
<span class="input-group-btn">
<button type="button" class="btn btn-info" id="deezer_favorites_download_play">Download & Play</button>
<button type="button" class="btn btn-info" id="deezer_favorites_download">Download</button>
<button type="button" class="btn btn-info" id="deezer_favorites_zip">Give me a zip</button>
<button type="button" class="btn btn-info" onclick="$('#deezer-favorites-userid').val('')" >Clear</button>
</span>
</div>
</div> <!-- end div tab deezer playlists -->
<div id="debug" class="container tab-pane fade">
<br>
<h3>Debug</h3>
<div class="form-group">
<textarea readonly class="form-control" id="ta-debug-log"></textarea>
</div> <!-- end div textares -->
</div> <!-- end div tab debug -->
<div id="queue" class="container tab-pane fade">
<br>
<h3>Queue</h3>
<table id="task-list" class="table">
<thead>
<tr>
<th>Description</th>
<th>Args</th>
<th>State</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div> <!-- end div tab queue -->
<footer class="footer">
<div class="container">
<i class="fa fa-angellist" ></i> |
<span class="text-muted">
ctrl+m: focus search bar |
Enter: search songs |
Ctrl+Shift+[1-7]: navigation |
</span>
<a id="werbung" href="https://www.effektiv-spenden.org/spenden-tipps/warum-und-wie-ich-spende-gastbeitrag-christoph-hartmann/"><i class="fa fas fa-heart" title="ja, klick drauf." ></i> </a>
</div>
</footer>
<style>
html, body, .container, .tab-content, .tab-pane .form-group {
height: 95%;
}
textarea.form-control {
height: 95%;
}
#werbung:link, #werbung:visited, #werbung:hover, #werbung:active { color: red }
</style>
</div> <!-- end div container -->
</body>
</html>

View File

@@ -0,0 +1,60 @@
import re
from shlex import quote
from subprocess import Popen, PIPE
from deezer_downloader.configuration import config
class YoutubeDLFailedException(Exception):
pass
class DownloadedFileNotFoundException(Exception):
pass
def execute(cmd):
print('Executing "{}"'.format(cmd))
p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
p.wait()
stdout, stderr = p.communicate()
print(stdout.decode())
if p.returncode != 0:
print(stderr.decode())
raise YoutubeDLFailedException("ERROR: youtube-dl exited with non-zero: \n{}\nYou may have to update it!".format(stderr.decode()))
return get_absolute_filename(stdout.decode(), stderr.decode())
def get_absolute_filename(stdout, stderr):
regex_foo = re.search(r'Destination:\s(.*mp3)', stdout)
if not regex_foo:
raise DownloadedFileNotFoundException("ERROR: Can not extract output file via regex. \nstderr: {}\nstdout: {}".format(stderr, stdout))
return regex_foo.group(1)
def youtubedl_download(url, destination_dir, proxy=None):
# url, e.g. https://www.youtube.com/watch?v=ZbZSe6N_BXs
# destination_dir: /tmp/
# proxy: https/socks5 proxy (e. g. socks5://user:pass@127.0.0.1:1080/)
# returns: absolute filename of the downloaded file
# raises
# YoutubeDLFailedException if youtube-dl exits with non-zero
# DownloadedFileNotFoundException if we cannot get the converted output file from youtube-dl with a regex
proxy_command = f" --proxy {proxy}" if proxy else ""
youtube_dl_cmd = config["youtubedl"]["command"] + \
proxy_command + \
" -x --audio-format mp3 " + \
"--audio-quality 0 " + \
f"-o '{destination_dir}/%(title)s.%(ext)s' " + \
"--embed-metadata " + \
"--no-embed-chapters " + \
quote(url)
filename_absolute = execute(youtube_dl_cmd)
return filename_absolute
if __name__ == '__main__':
video_url = "https://www.invidio.us/watch?v=ZbZSe6N_BXs"
youtubedl_download(video_url, "/tmp/music/deezer/youtube-dl")