initial commit
This commit is contained in:
59
deezer_downloader/cli/deezer-downloader.ini.template
Normal file
59
deezer_downloader/cli/deezer-downloader.ini.template
Normal 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
|
||||
56
deezer_downloader/cli/runner.py
Normal file
56
deezer_downloader/cli/runner.py
Normal 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()
|
||||
53
deezer_downloader/configuration.py
Normal file
53
deezer_downloader/configuration.py
Normal 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
491
deezer_downloader/deezer.py
Normal 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()
|
||||
BIN
deezer_downloader/requirements.txt
Normal file
BIN
deezer_downloader/requirements.txt
Normal file
Binary file not shown.
189
deezer_downloader/spotify.py
Normal file
189
deezer_downloader/spotify.py
Normal 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))
|
||||
100
deezer_downloader/threadpool_queue.py
Normal file
100
deezer_downloader/threadpool_queue.py
Normal 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
|
||||
263
deezer_downloader/web/app.py
Normal file
263
deezer_downloader/web/app.py
Normal 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), })
|
||||
256
deezer_downloader/web/music_backend.py
Normal file
256
deezer_downloader/web/music_backend.py
Normal 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)
|
||||
BIN
deezer_downloader/web/static/css/bootstrap-4.1.3-dist.zip
Normal file
BIN
deezer_downloader/web/static/css/bootstrap-4.1.3-dist.zip
Normal file
Binary file not shown.
7
deezer_downloader/web/static/css/bootstrap.bundle.min.js
vendored
Normal file
7
deezer_downloader/web/static/css/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
deezer_downloader/web/static/css/bootstrap.min.css
vendored
Normal file
7
deezer_downloader/web/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
47
deezer_downloader/web/static/css/custom.css
Normal file
47
deezer_downloader/web/static/css/custom.css
Normal 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;
|
||||
}
|
||||
4
deezer_downloader/web/static/css/font-awesome.min.css
vendored
Normal file
4
deezer_downloader/web/static/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deezer_downloader/web/static/css/jquery.jgrowl.min.css
vendored
Normal file
1
deezer_downloader/web/static/css/jquery.jgrowl.min.css
vendored
Normal 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}}
|
||||
BIN
deezer_downloader/web/static/favicon.ico
Normal file
BIN
deezer_downloader/web/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 327 B |
BIN
deezer_downloader/web/static/fonts/fontawesome-webfont.woff2
Normal file
BIN
deezer_downloader/web/static/fonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
7
deezer_downloader/web/static/js/bootstrap.min.js
vendored
Normal file
7
deezer_downloader/web/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
358
deezer_downloader/web/static/js/custom.js
Normal file
358
deezer_downloader/web/static/js/custom.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
2
deezer_downloader/web/static/js/jquery.jgrowl.min.js
vendored
Normal file
2
deezer_downloader/web/static/js/jquery.jgrowl.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
deezer_downloader/web/static/js/jquery.min.js
vendored
Normal file
2
deezer_downloader/web/static/js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
deezer_downloader/web/static/js/popper.min.js
vendored
Normal file
5
deezer_downloader/web/static/js/popper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
deezer_downloader/web/templates/autoindex.html
Normal file
11
deezer_downloader/web/templates/autoindex.html
Normal 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 %}
|
||||
|
||||
241
deezer_downloader/web/templates/index.html
Normal file
241
deezer_downloader/web/templates/index.html
Normal 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, ... " />
|
||||
|
||||
</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" />
|
||||
|
||||
<input type="text" class="form-control" id="spotify-playlist-url" placeholder="url or id of the playlist" />
|
||||
|
||||
</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" />
|
||||
|
||||
</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 />
|
||||
|
||||
</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>
|
||||
|
||||
60
deezer_downloader/youtubedl.py
Normal file
60
deezer_downloader/youtubedl.py
Normal 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")
|
||||
Reference in New Issue
Block a user