initial commit
This commit is contained in:
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>
|
||||
|
||||
Reference in New Issue
Block a user