initial commit

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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