Merge pull request #67 from TerryGeng/master
Enhanced the web interface
This commit is contained in:
@@ -33,12 +33,21 @@ max_track_playlist = 20
|
|||||||
# Maximum music duration (minutes)
|
# Maximum music duration (minutes)
|
||||||
max_track_duration = 60
|
max_track_duration = 60
|
||||||
|
|
||||||
|
# If ducking is enabled, the bot will automatically attenuate its volume when someone is talking.
|
||||||
|
ducking = False
|
||||||
|
ducking_volume = 0.05
|
||||||
|
|
||||||
[webinterface]
|
[webinterface]
|
||||||
enabled = False
|
enabled = False
|
||||||
is_web_proxified = True
|
is_web_proxified = True
|
||||||
listening_addr = 127.0.0.1
|
listening_addr = 127.0.0.1
|
||||||
listening_port = 8181
|
listening_port = 8181
|
||||||
|
|
||||||
|
# Set this option to True to enable password protection for the web interface
|
||||||
|
require_auth = False
|
||||||
|
user =
|
||||||
|
password =
|
||||||
|
|
||||||
[command]
|
[command]
|
||||||
#This it the char (only on letter) the bot will recognize as a command
|
#This it the char (only on letter) the bot will recognize as a command
|
||||||
command_symbol = !
|
command_symbol = !
|
||||||
|
|||||||
+203
-65
@@ -1,14 +1,19 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from flask import Flask, render_template, request, redirect, send_file
|
from functools import wraps
|
||||||
|
from flask import Flask, render_template, request, redirect, send_file, Response, jsonify
|
||||||
import variables as var
|
import variables as var
|
||||||
import util
|
import util
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
import shutil
|
||||||
import random
|
import random
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
import errno
|
import errno
|
||||||
import media
|
import media
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
class ReverseProxied(object):
|
class ReverseProxied(object):
|
||||||
@@ -57,8 +62,36 @@ def init_proxy():
|
|||||||
if var.is_proxified:
|
if var.is_proxified:
|
||||||
web.wsgi_app = ReverseProxied(web.wsgi_app)
|
web.wsgi_app = ReverseProxied(web.wsgi_app)
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/29725217/password-protect-one-webpage-in-flask-app
|
||||||
|
|
||||||
@web.route("/", methods=['GET', 'POST'])
|
def check_auth(username, password):
|
||||||
|
"""This function is called to check if a username /
|
||||||
|
password combination is valid.
|
||||||
|
"""
|
||||||
|
return username == var.config.get("webinterface", "user") and password == var.config.get("webinterface", "password")
|
||||||
|
|
||||||
|
def authenticate():
|
||||||
|
"""Sends a 401 response that enables basic auth"""
|
||||||
|
logging.info("Web Interface login failed.")
|
||||||
|
return Response(
|
||||||
|
'Could not verify your access level for that URL.\n'
|
||||||
|
'You have to login with proper credentials', 401,
|
||||||
|
{'WWW-Authenticate': 'Basic realm="Login Required"'})
|
||||||
|
|
||||||
|
def requires_auth(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
auth = request.authorization
|
||||||
|
if var.config.getboolean("webinterface", "require_auth") and (not auth or not check_auth(auth.username, auth.password)):
|
||||||
|
if auth:
|
||||||
|
logging.info("Web Interface login attempt, user: %s" % auth.username)
|
||||||
|
return authenticate()
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
@web.route("/", methods=['GET'])
|
||||||
|
@requires_auth
|
||||||
def index():
|
def index():
|
||||||
folder_path = var.music_folder
|
folder_path = var.music_folder
|
||||||
files = util.get_recursive_filelist_sorted(var.music_folder)
|
files = util.get_recursive_filelist_sorted(var.music_folder)
|
||||||
@@ -66,13 +99,69 @@ def index():
|
|||||||
for file in files:
|
for file in files:
|
||||||
music_library.add_file(file)
|
music_library.add_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
return render_template('index.html',
|
||||||
|
all_files=files,
|
||||||
|
music_library=music_library,
|
||||||
|
os=os,
|
||||||
|
playlist=var.playlist,
|
||||||
|
user=var.user
|
||||||
|
)
|
||||||
|
|
||||||
|
@web.route("/playlist", methods=['GET'])
|
||||||
|
@requires_auth
|
||||||
|
def playlist():
|
||||||
|
if var.playlist.length() == 0:
|
||||||
|
return jsonify([render_template('playlist.html',
|
||||||
|
m=False,
|
||||||
|
index=-1
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for index, item in enumerate(var.playlist.playlist):
|
||||||
|
data.append(render_template('playlist.html',
|
||||||
|
index=index,
|
||||||
|
m=item,
|
||||||
|
playlist=var.playlist
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
@web.route("/post", methods=['POST'])
|
||||||
|
@requires_auth
|
||||||
|
def post():
|
||||||
|
folder_path = var.music_folder
|
||||||
|
files = util.get_recursive_filelist_sorted(var.music_folder)
|
||||||
|
music_library = util.Dir(folder_path)
|
||||||
|
for file in files:
|
||||||
|
music_library.add_file(file)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
print(request.form)
|
logging.debug("Post request: "+ str(request.form))
|
||||||
if 'add_file' in request.form and ".." not in request.form['add_file']:
|
if 'add_file_bottom' in request.form and ".." not in request.form['add_file_bottom']:
|
||||||
item = {'type': 'file',
|
path = var.config.get('bot', 'music_folder') + request.form['add_file_bottom']
|
||||||
'path' : request.form['add_file'],
|
if os.path.isfile(path):
|
||||||
'user' : 'Web'}
|
item = {'type': 'file',
|
||||||
var.playlist.append(item)
|
'path' : request.form['add_file_bottom'],
|
||||||
|
'title' : '',
|
||||||
|
'user' : 'Web'}
|
||||||
|
var.playlist.append(var.botamusique.get_music_tag_info(item, path))
|
||||||
|
logging.info('web: add to playlist(bottom): ' + item['path'])
|
||||||
|
|
||||||
|
elif 'add_file_next' in request.form and ".." not in request.form['add_file_next']:
|
||||||
|
path = var.config.get('bot', 'music_folder') + request.form['add_file_next']
|
||||||
|
if os.path.isfile(path):
|
||||||
|
item = {'type': 'file',
|
||||||
|
'path' : request.form['add_file_next'],
|
||||||
|
'title' : '',
|
||||||
|
'user' : 'Web'}
|
||||||
|
var.playlist.insert(
|
||||||
|
var.playlist.current_index + 1,
|
||||||
|
var.botamusique.get_music_tag_info(item, var.config.get('bot', 'music_folder') + item['path'])
|
||||||
|
)
|
||||||
|
logging.info('web: add to playlist(next): ' + item['path'])
|
||||||
|
|
||||||
elif ('add_folder' in request.form and ".." not in request.form['add_folder']) or ('add_folder_recursively' in request.form and ".." not in request.form['add_folder_recursively']):
|
elif ('add_folder' in request.form and ".." not in request.form['add_folder']) or ('add_folder_recursively' in request.form and ".." not in request.form['add_folder_recursively']):
|
||||||
try:
|
try:
|
||||||
@@ -84,86 +173,135 @@ def index():
|
|||||||
folder += '/'
|
folder += '/'
|
||||||
|
|
||||||
print('folder:', folder)
|
print('folder:', folder)
|
||||||
if 'add_folder_recursively' in request.form:
|
|
||||||
files = music_library.get_files_recursively(folder)
|
if os.path.isdir(var.config.get('bot', 'music_folder') + folder):
|
||||||
else:
|
if 'add_folder_recursively' in request.form:
|
||||||
files = music_library.get_files(folder)
|
files = music_library.get_files_recursively(folder)
|
||||||
files = list(map(lambda file: {'type':'file','path': os.path.join(folder, file), 'user':'Web'}, files))
|
else:
|
||||||
print('Adding to playlist: ', files)
|
files = music_library.get_files(folder)
|
||||||
var.playlist.extend(files)
|
|
||||||
|
files = list(map(lambda file: var.botamusique.get_music_tag_info({'type':'file','path': os.path.join(folder, file), 'user':'Web'}, \
|
||||||
|
var.config.get('bot', 'music_folder') + os.path.join(folder, file)), files))
|
||||||
|
|
||||||
|
logging.info("web: add to playlist: " + " ,".join([file['path'] for file in files]))
|
||||||
|
var.playlist.extend(files)
|
||||||
|
|
||||||
elif 'add_url' in request.form:
|
elif 'add_url' in request.form:
|
||||||
var.playlist.append({'type':'url',
|
var.playlist.append({'type':'url',
|
||||||
'url': request.form['add_url'],
|
'url': request.form['add_url'],
|
||||||
'user': 'Web',
|
'user': 'Web',
|
||||||
'ready': 'validation'})
|
'ready': 'validation'})
|
||||||
|
logging.info("web: add to playlist: " + request.form['add_url'])
|
||||||
media.url.get_url_info()
|
media.url.get_url_info()
|
||||||
var.playlist[-1]['ready'] = "no"
|
var.playlist.playlist[-1]['ready'] = "no"
|
||||||
|
|
||||||
elif 'add_radio' in request.form:
|
elif 'add_radio' in request.form:
|
||||||
var.playlist.append({'type': 'radio',
|
var.playlist.append({'type': 'radio',
|
||||||
'path': request.form['add_radio'],
|
'path': request.form['add_radio'],
|
||||||
'user': "Web"})
|
'user': "Web"})
|
||||||
|
logging.info("web: add to playlist: " + request.form['add_radio'])
|
||||||
|
|
||||||
elif 'delete_music' in request.form:
|
elif 'delete_music' in request.form:
|
||||||
if len(var.playlist) >= request.form['delete_music']:
|
music = var.playlist.playlist[int(request.form['delete_music'])]
|
||||||
var.playlist.pop(request.form['delete_music'])
|
logging.info("web: delete from playlist: " + str(music['path'] if 'path' in music else music['url']))
|
||||||
|
|
||||||
|
if len(var.playlist.playlist) >= int(request.form['delete_music']):
|
||||||
|
if var.playlist.current_index == int(request.form['delete_music']):
|
||||||
|
var.botamusique.pause()
|
||||||
|
var.playlist.remove(int(request.form['delete_music']))
|
||||||
|
var.botamusique.launch_music()
|
||||||
|
else:
|
||||||
|
var.playlist.remove(int(request.form['delete_music']))
|
||||||
|
|
||||||
|
elif 'play_music' in request.form:
|
||||||
|
music = var.playlist.playlist[int(request.form['play_music'])]
|
||||||
|
logging.info("web: jump to: " + str(music['path'] if 'path' in music else music['url']))
|
||||||
|
|
||||||
|
if len(var.playlist.playlist) >= int(request.form['play_music']):
|
||||||
|
var.botamusique.pause()
|
||||||
|
var.botamusique.launch_music(int(request.form['play_music']))
|
||||||
|
|
||||||
|
elif 'delete_music_file' in request.form and ".." not in request.form['delete_music_file']:
|
||||||
|
path = var.config.get('bot', 'music_folder') + request.form['delete_music_file']
|
||||||
|
if os.path.isfile(path):
|
||||||
|
logging.info("web: delete file " + path)
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
elif 'delete_folder' in request.form and ".." not in request.form['delete_folder']:
|
||||||
|
path = var.config.get('bot', 'music_folder') + request.form['delete_folder']
|
||||||
|
if os.path.isdir(path):
|
||||||
|
logging.info("web: delete folder " + path)
|
||||||
|
shutil.rmtree(path)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
elif 'action' in request.form:
|
elif 'action' in request.form:
|
||||||
action = request.form['action']
|
action = request.form['action']
|
||||||
if action == "randomize":
|
if action == "randomize":
|
||||||
random.shuffle(var.playlist)
|
var.playlist.randomize()
|
||||||
|
elif action == "stop":
|
||||||
return render_template('index.html',
|
var.botamusique.pause()
|
||||||
all_files=files,
|
elif action == "clear":
|
||||||
music_library=music_library,
|
var.botamusique.stop()
|
||||||
os=os,
|
elif action == "volume_up":
|
||||||
playlist=var.playlist,
|
if var.botamusique.volume_set + 0.03 < 1.0:
|
||||||
user=var.user)
|
var.botamusique.volume_set = var.botamusique.volume_set + 0.03
|
||||||
|
else:
|
||||||
|
var.botamusique.volume_set = 1.0
|
||||||
|
logging.info("web: volume up to %d" % (var.botamusique.volume_set * 100))
|
||||||
|
elif action == "volume_down":
|
||||||
|
if var.botamusique.volume_set - 0.03 > 0:
|
||||||
|
var.botamusique.volume_set = var.botamusique.volume_set - 0.03
|
||||||
|
else:
|
||||||
|
var.botamusique.volume_set = 0
|
||||||
|
logging.info("web: volume up to %d" % (var.botamusique.volume_set * 100))
|
||||||
|
|
||||||
|
return jsonify({'ver': var.playlist.version})
|
||||||
|
|
||||||
@web.route('/upload', methods=["POST"])
|
@web.route('/upload', methods=["POST"])
|
||||||
def upload():
|
def upload():
|
||||||
file = request.files['file']
|
files = request.files.getlist("file[]")
|
||||||
if not file:
|
if not files:
|
||||||
return redirect("./", code=406)
|
return redirect("./", code=406)
|
||||||
|
|
||||||
filename = secure_filename(file.filename).strip()
|
#filename = secure_filename(file.filename).strip()
|
||||||
if filename == '':
|
for file in files:
|
||||||
return redirect("./", code=406)
|
filename = file.filename
|
||||||
|
if filename == '':
|
||||||
targetdir = request.form['targetdir'].strip()
|
|
||||||
if targetdir == '':
|
|
||||||
targetdir = 'uploads/'
|
|
||||||
elif '../' in targetdir:
|
|
||||||
return redirect("./", code=406)
|
|
||||||
|
|
||||||
print('Uploading file:')
|
|
||||||
print('filename:', filename)
|
|
||||||
print('targetdir:', targetdir)
|
|
||||||
print('mimetype:', file.mimetype)
|
|
||||||
|
|
||||||
if "audio" in file.mimetype:
|
|
||||||
storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir))
|
|
||||||
print('storagepath:',storagepath)
|
|
||||||
if not storagepath.startswith(os.path.abspath(var.music_folder)):
|
|
||||||
return redirect("./", code=406)
|
return redirect("./", code=406)
|
||||||
|
|
||||||
try:
|
targetdir = request.form['targetdir'].strip()
|
||||||
os.makedirs(storagepath)
|
if targetdir == '':
|
||||||
except OSError as ee:
|
targetdir = 'uploads/'
|
||||||
if ee.errno != errno.EEXIST:
|
elif '../' in targetdir:
|
||||||
return redirect("./", code=500)
|
|
||||||
|
|
||||||
filepath = os.path.join(storagepath, filename)
|
|
||||||
print('filepath:',filepath)
|
|
||||||
if os.path.exists(filepath):
|
|
||||||
return redirect("./", code=406)
|
return redirect("./", code=406)
|
||||||
|
|
||||||
file.save(filepath)
|
logging.info('Uploading file:')
|
||||||
return redirect("./", code=302)
|
logging.info(' - filename: ' + filename)
|
||||||
else:
|
logging.info(' - targetdir: ' + targetdir)
|
||||||
return redirect("./", code=409)
|
logging.info(' - mimetype: ' + file.mimetype)
|
||||||
|
|
||||||
|
if "audio" in file.mimetype:
|
||||||
|
storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir))
|
||||||
|
print('storagepath:',storagepath)
|
||||||
|
if not storagepath.startswith(os.path.abspath(var.music_folder)):
|
||||||
|
return redirect("./", code=406)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(storagepath)
|
||||||
|
except OSError as ee:
|
||||||
|
if ee.errno != errno.EEXIST:
|
||||||
|
return redirect("./", code=500)
|
||||||
|
|
||||||
|
filepath = os.path.join(storagepath, filename)
|
||||||
|
logging.info(' - filepath: ' + filepath)
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
return redirect("./", code=406)
|
||||||
|
|
||||||
|
file.save(filepath)
|
||||||
|
else:
|
||||||
|
return redirect("./", code=409)
|
||||||
|
|
||||||
|
return redirect("./", code=302)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/download', methods=["GET"])
|
@web.route('/download', methods=["GET"])
|
||||||
|
|||||||
+101
-7
@@ -1,5 +1,99 @@
|
|||||||
import youtube_dl
|
import youtube_dl
|
||||||
import variables as var
|
import variables as var
|
||||||
|
import random
|
||||||
|
|
||||||
|
class PlayList:
|
||||||
|
playlist = []
|
||||||
|
current_index = 0
|
||||||
|
version = 0 # increase by one after each change
|
||||||
|
|
||||||
|
def append(self, item):
|
||||||
|
self.version += 1
|
||||||
|
self.playlist.append(item)
|
||||||
|
|
||||||
|
def insert(self, index, item):
|
||||||
|
self.version += 1
|
||||||
|
|
||||||
|
if index == -1:
|
||||||
|
index = self.current_index
|
||||||
|
|
||||||
|
self.playlist.insert(index, item)
|
||||||
|
|
||||||
|
if index <= self.current_index:
|
||||||
|
self.current_index += 1
|
||||||
|
|
||||||
|
def length(self):
|
||||||
|
return len(self.playlist)
|
||||||
|
|
||||||
|
def extend(self, items):
|
||||||
|
self.version += 1
|
||||||
|
self.playlist.extend(items)
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
self.version += 1
|
||||||
|
if len(self.playlist) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.current_index = self.next_index()
|
||||||
|
|
||||||
|
return self.playlist[self.current_index]
|
||||||
|
|
||||||
|
def update(self, item, index=-1):
|
||||||
|
self.version += 1
|
||||||
|
if index == -1:
|
||||||
|
index = self.current_index
|
||||||
|
self.playlist[index] = item
|
||||||
|
|
||||||
|
def remove(self, index=-1):
|
||||||
|
self.version += 1
|
||||||
|
if index > len(self.playlist) - 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if index == -1:
|
||||||
|
index = self.current_index
|
||||||
|
del self.playlist[index]
|
||||||
|
|
||||||
|
if self.current_index <= index:
|
||||||
|
self.next()
|
||||||
|
|
||||||
|
def current_item(self):
|
||||||
|
return self.playlist[self.current_index]
|
||||||
|
|
||||||
|
def next_index(self):
|
||||||
|
if len(self.playlist) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.current_index < len(self.playlist) - 1:
|
||||||
|
return self.current_index + 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def next_item(self):
|
||||||
|
if len(self.playlist) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.playlist[self.next_index()]
|
||||||
|
|
||||||
|
def jump(self, index):
|
||||||
|
self.version += 1
|
||||||
|
self.current_index = index
|
||||||
|
return self.playlist[index]
|
||||||
|
|
||||||
|
def randomize(self):
|
||||||
|
# current_index will lose track after shuffling, thus we take current music out before shuffling
|
||||||
|
current = self.current_item()
|
||||||
|
del self.playlist[self.current_index]
|
||||||
|
|
||||||
|
random.shuffle(self.playlist)
|
||||||
|
|
||||||
|
self.playlist.insert(0, current)
|
||||||
|
self.current_index = 0
|
||||||
|
self.version += 1
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.version += 1
|
||||||
|
self.playlist = []
|
||||||
|
self.current_index = 0
|
||||||
|
|
||||||
|
|
||||||
def get_playlist_info(url, start_index=0, user=""):
|
def get_playlist_info(url, start_index=0, user=""):
|
||||||
@@ -41,15 +135,15 @@ def get_music_info(index=0):
|
|||||||
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
try:
|
try:
|
||||||
info = ydl.extract_info(var.playlist[0]['url'], download=False)
|
info = ydl.extract_info(var.playlist.playlist[index]['url'], download=False)
|
||||||
# Check if the Duration is longer than the config
|
# Check if the Duration is longer than the config
|
||||||
if var.playlist[0]['current_index'] == index:
|
if var.playlist[index]['current_index'] == index:
|
||||||
var.playlist[0]['current_duration'] = info['entries'][0]['duration'] / 60
|
var.playlist[index]['current_duration'] = info['entries'][0]['duration'] / 60
|
||||||
var.playlist[0]['current_title'] = info['entries'][0]['title']
|
var.playlist[index]['current_title'] = info['entries'][0]['title']
|
||||||
# Check if the Duration of the next music is longer than the config (async download)
|
# Check if the Duration of the next music is longer than the config (async download)
|
||||||
elif var.playlist[0]['current_index'] == index - 1:
|
elif var.playlist[index]['current_index'] == index - 1:
|
||||||
var.playlist[0]['next_duration'] = info['entries'][0]['duration'] / 60
|
var.playlist[index]['next_duration'] = info['entries'][0]['duration'] / 60
|
||||||
var.playlist[0]['next_title'] = info['entries'][0]['title']
|
var.playlist[index]['next_title'] = info['entries'][0]['title']
|
||||||
except youtube_dl.utils.DownloadError:
|
except youtube_dl.utils.DownloadError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|||||||
+7
-7
@@ -2,22 +2,22 @@ import youtube_dl
|
|||||||
import variables as var
|
import variables as var
|
||||||
|
|
||||||
|
|
||||||
def get_url_info(index=-1):
|
def get_url_info(music):
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
'noplaylist': True
|
'noplaylist': True
|
||||||
}
|
}
|
||||||
var.playlist[index]['duration'] = 0
|
music['duration'] = 0
|
||||||
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
try:
|
try:
|
||||||
print(var.playlist)
|
print(var.playlist)
|
||||||
info = ydl.extract_info(var.playlist[index]['url'], download=False)
|
info = ydl.extract_info(music['url'], download=False)
|
||||||
var.playlist[index]['duration'] = info['duration'] / 60
|
music['duration'] = info['duration'] / 60
|
||||||
var.playlist[index]['title'] = info['title']
|
music['title'] = info['title']
|
||||||
except youtube_dl.utils.DownloadError:
|
except youtube_dl.utils.DownloadError:
|
||||||
pass
|
pass
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return True
|
return music
|
||||||
else:
|
else:
|
||||||
return True
|
return music
|
||||||
return False
|
return False
|
||||||
|
|||||||
+285
-138
@@ -4,6 +4,7 @@
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
import math
|
||||||
import signal
|
import signal
|
||||||
import configparser
|
import configparser
|
||||||
import audioop
|
import audioop
|
||||||
@@ -20,6 +21,7 @@ import util
|
|||||||
import base64
|
import base64
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
import mutagen
|
||||||
from mutagen.easyid3 import EasyID3
|
from mutagen.easyid3 import EasyID3
|
||||||
import re
|
import re
|
||||||
import media.url
|
import media.url
|
||||||
@@ -28,6 +30,7 @@ import media.playlist
|
|||||||
import media.radio
|
import media.radio
|
||||||
import media.system
|
import media.system
|
||||||
from librb import radiobrowser
|
from librb import radiobrowser
|
||||||
|
from media.playlist import PlayList
|
||||||
|
|
||||||
"""
|
"""
|
||||||
FORMAT OF A MUSIC INTO THE PLAYLIST
|
FORMAT OF A MUSIC INTO THE PLAYLIST
|
||||||
@@ -36,7 +39,8 @@ type : url
|
|||||||
title
|
title
|
||||||
path
|
path
|
||||||
duration
|
duration
|
||||||
thundnail
|
artist
|
||||||
|
thumbnail
|
||||||
user
|
user
|
||||||
ready (validation, no, downloading, yes)
|
ready (validation, no, downloading, yes)
|
||||||
from_playlist (yes,no)
|
from_playlist (yes,no)
|
||||||
@@ -52,7 +56,9 @@ type : radio
|
|||||||
type : file
|
type : file
|
||||||
path
|
path
|
||||||
title
|
title
|
||||||
|
artist
|
||||||
duration
|
duration
|
||||||
|
thumbnail
|
||||||
user
|
user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -62,29 +68,37 @@ version = 5
|
|||||||
class MumbleBot:
|
class MumbleBot:
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
signal.signal(signal.SIGINT, self.ctrl_caught)
|
signal.signal(signal.SIGINT, self.ctrl_caught)
|
||||||
self.volume = var.config.getfloat('bot', 'volume')
|
self.volume_set = var.config.getfloat('bot', 'volume')
|
||||||
if db.has_option('bot', 'volume'):
|
if db.has_option('bot', 'volume'):
|
||||||
self.volume = var.db.getfloat('bot', 'volume')
|
self.volume_set = var.db.getfloat('bot', 'volume')
|
||||||
|
|
||||||
|
self.volume = self.volume_set
|
||||||
|
|
||||||
self.channel = args.channel
|
self.channel = args.channel
|
||||||
|
|
||||||
# Set specific format for the log
|
root = logging.getLogger()
|
||||||
FORMAT = '%(asctime)s: %(message)s'
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
loglevel = logging.INFO
|
root.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
logfile = var.config.get('bot', 'logfile')
|
||||||
|
|
||||||
|
handler = None
|
||||||
|
if logfile:
|
||||||
|
handler = logging.FileHandler(logfile)
|
||||||
|
else:
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root.addHandler(handler)
|
||||||
|
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
loglevel = logging.DEBUG
|
root.setLevel(logging.DEBUG)
|
||||||
logging.debug("Starting in DEBUG loglevel")
|
logging.debug("Starting in DEBUG loglevel")
|
||||||
elif args.quiet:
|
elif args.quiet:
|
||||||
loglevel = logging.ERROR
|
root.setLevel(logging.ERROR)
|
||||||
logging.error("Starting in ERROR loglevel")
|
logging.error("Starting in ERROR loglevel")
|
||||||
logfile = var.config.get('bot', 'logfile')
|
|
||||||
if logfile:
|
|
||||||
logging.basicConfig(filename=logfile,format=FORMAT, level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S')
|
|
||||||
else:
|
|
||||||
logging.basicConfig(format=FORMAT, level=loglevel, datefmt='%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
# the playlist is... a list (Surprise !!)
|
var.playlist = PlayList()
|
||||||
var.playlist = []
|
|
||||||
|
|
||||||
var.user = args.user
|
var.user = args.user
|
||||||
var.music_folder = var.config.get('bot', 'music_folder')
|
var.music_folder = var.config.get('bot', 'music_folder')
|
||||||
@@ -94,6 +108,7 @@ class MumbleBot:
|
|||||||
self.nb_exit = 0
|
self.nb_exit = 0
|
||||||
self.thread = None
|
self.thread = None
|
||||||
self.is_playing = False
|
self.is_playing = False
|
||||||
|
self.is_pause = False
|
||||||
|
|
||||||
if var.config.getboolean("webinterface", "enabled"):
|
if var.config.getboolean("webinterface", "enabled"):
|
||||||
wi_addr = var.config.get("webinterface", "listening_addr")
|
wi_addr = var.config.get("webinterface", "listening_addr")
|
||||||
@@ -104,6 +119,7 @@ class MumbleBot:
|
|||||||
tt.daemon = True
|
tt.daemon = True
|
||||||
tt.start()
|
tt.start()
|
||||||
|
|
||||||
|
|
||||||
if args.host:
|
if args.host:
|
||||||
host = args.host
|
host = args.host
|
||||||
else:
|
else:
|
||||||
@@ -148,7 +164,13 @@ class MumbleBot:
|
|||||||
self.mumble.channels.find_by_name(self.channel).move_in()
|
self.mumble.channels.find_by_name(self.channel).move_in()
|
||||||
self.mumble.set_bandwidth(200000)
|
self.mumble.set_bandwidth(200000)
|
||||||
|
|
||||||
self.loop()
|
self.is_ducking = False
|
||||||
|
self.on_ducking = False
|
||||||
|
if var.config.getboolean("bot", "ducking"):
|
||||||
|
self.is_ducking = True
|
||||||
|
self.ducking_volume = var.config.getfloat("bot", "ducking_volume")
|
||||||
|
self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED, self.ducking_sound_received)
|
||||||
|
self.mumble.set_receive_sound(True)
|
||||||
|
|
||||||
# Set the CTRL+C shortcut
|
# Set the CTRL+C shortcut
|
||||||
def ctrl_caught(self, signal, frame):
|
def ctrl_caught(self, signal, frame):
|
||||||
@@ -163,7 +185,6 @@ class MumbleBot:
|
|||||||
|
|
||||||
# All text send to the chat is analysed by this function
|
# All text send to the chat is analysed by this function
|
||||||
def message_received(self, text):
|
def message_received(self, text):
|
||||||
|
|
||||||
message = text.message.strip()
|
message = text.message.strip()
|
||||||
user = self.mumble.users[text.actor]['name']
|
user = self.mumble.users[text.actor]['name']
|
||||||
|
|
||||||
@@ -185,7 +206,7 @@ class MumbleBot:
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.info(command + ' - ' + parameter + ' by ' + user)
|
logging.info('bot: received command ' + command + ' - ' + parameter + ' by ' + user)
|
||||||
|
|
||||||
if command == var.config.get('command', 'joinme'):
|
if command == var.config.get('command', 'joinme'):
|
||||||
self.mumble.users.myself.move_in(
|
self.mumble.users.myself.move_in(
|
||||||
@@ -278,6 +299,7 @@ class MumbleBot:
|
|||||||
music = {'type': 'file',
|
music = {'type': 'file',
|
||||||
'path': filename,
|
'path': filename,
|
||||||
'user': user}
|
'user': user}
|
||||||
|
logging.info("bot: add to playlist: " + filename)
|
||||||
var.playlist.append(music)
|
var.playlist.append(music)
|
||||||
else:
|
else:
|
||||||
# try to do a partial match
|
# try to do a partial match
|
||||||
@@ -290,6 +312,7 @@ class MumbleBot:
|
|||||||
music = {'type': 'file',
|
music = {'type': 'file',
|
||||||
'path': matches[0],
|
'path': matches[0],
|
||||||
'user': user}
|
'user': user}
|
||||||
|
logging.info("bot: add to playlist: " + matches[0])
|
||||||
var.playlist.append(music)
|
var.playlist.append(music)
|
||||||
else:
|
else:
|
||||||
msg = var.config.get(
|
msg = var.config.get(
|
||||||
@@ -306,27 +329,26 @@ class MumbleBot:
|
|||||||
'url': self.get_url_from_input(parameter),
|
'url': self.get_url_from_input(parameter),
|
||||||
'user': user,
|
'user': user,
|
||||||
'ready': 'validation'}
|
'ready': 'validation'}
|
||||||
var.playlist.append(music)
|
|
||||||
|
|
||||||
if media.url.get_url_info():
|
if media.url.get_url_info():
|
||||||
if var.playlist[-1]['duration'] > var.config.getint('bot', 'max_track_duration'):
|
if music['duration'] > var.config.getint('bot', 'max_track_duration'):
|
||||||
var.playlist.pop()
|
|
||||||
self.send_msg(var.config.get(
|
self.send_msg(var.config.get(
|
||||||
'strings', 'too_long'), text)
|
'strings', 'too_long'), text)
|
||||||
else:
|
else:
|
||||||
for i in var.db.options("url_ban"):
|
for i in var.db.options("url_ban"):
|
||||||
if var.playlist[-1]['url'] == i:
|
if music['url'] == i:
|
||||||
self.mumble.users[text.actor].send_text_message(
|
self.mumble.users[text.actor].send_text_message(
|
||||||
var.config.get('strings', 'url_ban'))
|
var.config.get('strings', 'url_ban'))
|
||||||
var.playlist.pop()
|
|
||||||
return
|
return
|
||||||
var.playlist[-1]['ready'] = "no"
|
music['ready'] = "no"
|
||||||
|
var.playlist.append(music)
|
||||||
|
logging.info("bot: add to playlist: " + music['url'])
|
||||||
self.async_download_next()
|
self.async_download_next()
|
||||||
else:
|
else:
|
||||||
var.playlist.pop()
|
|
||||||
self.send_msg(var.config.get(
|
self.send_msg(var.config.get(
|
||||||
'strings', 'unable_download'), text)
|
'strings', 'unable_download'), text)
|
||||||
|
|
||||||
|
|
||||||
elif command == var.config.get('command', 'play_playlist') and parameter:
|
elif command == var.config.get('command', 'play_playlist') and parameter:
|
||||||
offset = 1 # if you want to start the playlist at a specific index
|
offset = 1 # if you want to start the playlist at a specific index
|
||||||
try:
|
try:
|
||||||
@@ -357,24 +379,25 @@ class MumbleBot:
|
|||||||
'url': url,
|
'url': url,
|
||||||
'user': user}
|
'user': user}
|
||||||
var.playlist.append(music)
|
var.playlist.append(music)
|
||||||
|
logging.info("bot: add to playlist: " + music['url'])
|
||||||
self.async_download_next()
|
self.async_download_next()
|
||||||
else:
|
else:
|
||||||
self.send_msg(var.config.get('strings', 'bad_url'))
|
self.send_msg(var.config.get('strings', 'bad_url'))
|
||||||
# query http://www.radio-browser.info API for a radio station
|
# query http://www.radio-browser.info API for a radio station
|
||||||
elif command == var.config.get('command', 'rb_query'):
|
elif command == var.config.get('command', 'rb_query'):
|
||||||
logging.info('Querying radio stations')
|
logging.info('bot: Querying radio stations')
|
||||||
if not parameter:
|
if not parameter:
|
||||||
logging.debug('rbquery without parameter')
|
logging.debug('rbquery without parameter')
|
||||||
msg = var.config.get('strings', 'rb_query_empty')
|
msg = var.config.get('strings', 'rb_query_empty')
|
||||||
self.send_msg(msg, text)
|
self.send_msg(msg, text)
|
||||||
else:
|
else:
|
||||||
logging.debug('Found query parameter: ' + parameter)
|
logging.debug('bot: Found query parameter: ' + parameter)
|
||||||
# self.send_msg('Searching for stations - this may take some seconds...', text)
|
# self.send_msg('Searching for stations - this may take some seconds...', text)
|
||||||
rb_stations = radiobrowser.getstations_byname(parameter)
|
rb_stations = radiobrowser.getstations_byname(parameter)
|
||||||
msg = var.config.get('strings', 'rb_query_result') + " :"
|
msg = var.config.get('strings', 'rb_query_result') + " :"
|
||||||
msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th></tr>'
|
msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th></tr>'
|
||||||
if not rb_stations:
|
if not rb_stations:
|
||||||
logging.debug('No matches found for rbquery ' + parameter)
|
logging.debug('bot: No matches found for rbquery ' + parameter)
|
||||||
self.send_msg('Radio-Browser found no matches for ' + parameter, text)
|
self.send_msg('Radio-Browser found no matches for ' + parameter, text)
|
||||||
else:
|
else:
|
||||||
for s in rb_stations:
|
for s in rb_stations:
|
||||||
@@ -420,13 +443,13 @@ class MumbleBot:
|
|||||||
self.send_msg('Query result too long to post (> 5000 characters), please try another query.', text)
|
self.send_msg('Query result too long to post (> 5000 characters), please try another query.', text)
|
||||||
# Play a secific station (by id) from http://www.radio-browser.info API
|
# Play a secific station (by id) from http://www.radio-browser.info API
|
||||||
elif command == var.config.get('command', 'rb_play'):
|
elif command == var.config.get('command', 'rb_play'):
|
||||||
logging.debug('Play a station by ID')
|
logging.debug('bot: Play a station by ID')
|
||||||
if not parameter:
|
if not parameter:
|
||||||
logging.debug('rbplay without parameter')
|
logging.debug('rbplay without parameter')
|
||||||
msg = var.config.get('strings', 'rb_play_empty')
|
msg = var.config.get('strings', 'rb_play_empty')
|
||||||
self.send_msg(msg, text)
|
self.send_msg(msg, text)
|
||||||
else:
|
else:
|
||||||
logging.debug('Retreiving url for station ID ' + parameter)
|
logging.debug('bot: Retreiving url for station ID ' + parameter)
|
||||||
rstation = radiobrowser.getstationname_byid(parameter)
|
rstation = radiobrowser.getstationname_byid(parameter)
|
||||||
stationname = rstation[0]['name']
|
stationname = rstation[0]['name']
|
||||||
country = rstation[0]['country']
|
country = rstation[0]['country']
|
||||||
@@ -440,18 +463,21 @@ class MumbleBot:
|
|||||||
msg += '<table><tr><th>ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th><th>Homepage</th></tr>' + \
|
msg += '<table><tr><th>ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th><th>Homepage</th></tr>' + \
|
||||||
'<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td><td>%s</td></tr></table>' \
|
'<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td><td>%s</td></tr></table>' \
|
||||||
% (parameter, stationname, genre, codec, bitrate, country, homepage)
|
% (parameter, stationname, genre, codec, bitrate, country, homepage)
|
||||||
logging.debug('Added station to playlist %s' % stationname)
|
logging.debug('bot: Added station to playlist %s' % stationname)
|
||||||
self.send_msg(msg, text)
|
self.send_msg(msg, text)
|
||||||
url = radiobrowser.geturl_byid(parameter)
|
url = radiobrowser.geturl_byid(parameter)
|
||||||
if url != "-1":
|
if url != "-1":
|
||||||
logging.info('Found url: ' + url)
|
logging.info('bot: Found url: ' + url)
|
||||||
music = {'type': 'radio',
|
music = {'type': 'radio',
|
||||||
|
'title': stationname,
|
||||||
|
'artist': homepage,
|
||||||
'url': url,
|
'url': url,
|
||||||
'user': user}
|
'user': user}
|
||||||
var.playlist.append(music)
|
var.playlist.append(music)
|
||||||
|
logging.info("bot: add to playlist: " + music['url'])
|
||||||
self.async_download_next()
|
self.async_download_next()
|
||||||
else:
|
else:
|
||||||
logging.info('No playable url found.')
|
logging.info('bot: No playable url found.')
|
||||||
msg += "No playable url found for this station, please try another station."
|
msg += "No playable url found for this station, please try another station."
|
||||||
self.send_msg(msg, text)
|
self.send_msg(msg, text)
|
||||||
|
|
||||||
@@ -491,44 +517,50 @@ class MumbleBot:
|
|||||||
elif command == var.config.get('command', 'volume'):
|
elif command == var.config.get('command', 'volume'):
|
||||||
# The volume is a percentage
|
# The volume is a percentage
|
||||||
if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100:
|
if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100:
|
||||||
self.volume = float(float(parameter) / 100)
|
self.volume_set = float(float(parameter) / 100)
|
||||||
self.send_msg(var.config.get('strings', 'change_volume') % (
|
self.send_msg(var.config.get('strings', 'change_volume') % (
|
||||||
int(self.volume * 100), self.mumble.users[text.actor]['name']), text)
|
int(self.volume_set * 100), self.mumble.users[text.actor]['name']), text)
|
||||||
var.db.set('bot', 'volume', str(self.volume))
|
var.db.set('bot', 'volume', str(self.volume_set))
|
||||||
|
logging.info('bot: volume set to %d' % (self.volume_set * 100))
|
||||||
else:
|
else:
|
||||||
self.send_msg(var.config.get(
|
self.send_msg(var.config.get(
|
||||||
'strings', 'current_volume') % int(self.volume * 100), text)
|
'strings', 'current_volume') % int(self.volume_set * 100), text)
|
||||||
|
|
||||||
elif command == var.config.get('command', 'current_music'):
|
elif command == var.config.get('command', 'current_music'):
|
||||||
if len(var.playlist) > 0:
|
if len(var.playlist.playlist) > 0:
|
||||||
source = var.playlist[0]["type"]
|
current_music = var.playlist.current_item()
|
||||||
|
source = current_music["type"]
|
||||||
if source == "radio":
|
if source == "radio":
|
||||||
reply = "[radio] {title} on {url} by {user}".format(
|
reply = "[radio] {title} on {url} by {user}".format(
|
||||||
title=media.radio.get_radio_title(
|
title=media.radio.get_radio_title(
|
||||||
var.playlist[0]["url"]),
|
current_music["url"]),
|
||||||
url=var.playlist[0]["title"],
|
url=current_music["title"],
|
||||||
user=var.playlist[0]["user"]
|
user=current_music["user"]
|
||||||
)
|
)
|
||||||
elif source == "url" and 'from_playlist' in var.playlist[0]:
|
elif source == "url" and 'from_playlist' in current_music:
|
||||||
reply = "[playlist] {title} (from the playlist <a href=\"{url}\">{playlist}</a> by {user}".format(
|
reply = "[playlist] {title} (from the playlist <a href=\"{url}\">{playlist}</a> by {user}".format(
|
||||||
title=var.playlist[0]["title"],
|
title=current_music["title"],
|
||||||
url=var.playlist[0]["playlist_url"],
|
url=current_music["playlist_url"],
|
||||||
playlist=var.playlist[0]["playlist_title"],
|
playlist=current_music["playlist_title"],
|
||||||
user=var.playlist[0]["user"]
|
user=current_music["user"]
|
||||||
)
|
)
|
||||||
elif source == "url":
|
elif source == "url":
|
||||||
reply = "[url] {title} (<a href=\"{url}\">{url}</a>) by {user}".format(
|
reply = "[url] {title} (<a href=\"{url}\">{url}</a>) by {user}".format(
|
||||||
title=var.playlist[0]["title"],
|
title=current_music["title"],
|
||||||
url=var.playlist[0]["url"],
|
url=current_music["url"],
|
||||||
user=var.playlist[0]["user"]
|
user=current_music["user"]
|
||||||
)
|
)
|
||||||
elif source == "file":
|
elif source == "file":
|
||||||
reply = "[file] {title} by {user}".format(
|
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
|
||||||
title=var.playlist[0]["title"],
|
current_music['thumbnail'] + '"/>'
|
||||||
user=var.playlist[0]["user"])
|
reply = "[file] {title} by {user} <br> {thumb}".format(
|
||||||
|
title=current_music['artist'] + ' - ' + current_music['title'],
|
||||||
|
user=current_music["user"],
|
||||||
|
thumb=thumbnail_html
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
reply = "ERROR"
|
reply = "ERROR"
|
||||||
logging.error(var.playlist)
|
logging.error(current_music)
|
||||||
else:
|
else:
|
||||||
reply = var.config.get('strings', 'not_playing')
|
reply = var.config.get('strings', 'not_playing')
|
||||||
|
|
||||||
@@ -537,8 +569,8 @@ class MumbleBot:
|
|||||||
elif command == var.config.get('command', 'skip'):
|
elif command == var.config.get('command', 'skip'):
|
||||||
# Allow to remove specific music into the queue with a number
|
# Allow to remove specific music into the queue with a number
|
||||||
if parameter is not None and parameter.isdigit() and int(parameter) > 0:
|
if parameter is not None and parameter.isdigit() and int(parameter) > 0:
|
||||||
if int(parameter) < len(var.playlist):
|
if int(parameter) < len(var.playlist.playlist):
|
||||||
removed = var.playlist.pop(int(parameter))
|
removed = var.playlist.jump(int(parameter))
|
||||||
|
|
||||||
# the Title isn't here if the music wasn't downloaded
|
# the Title isn't here if the music wasn't downloaded
|
||||||
self.send_msg(var.config.get('strings', 'removing_item') % (
|
self.send_msg(var.config.get('strings', 'removing_item') % (
|
||||||
@@ -564,20 +596,25 @@ class MumbleBot:
|
|||||||
self.send_msg(var.config.get('strings', 'no_file'), text)
|
self.send_msg(var.config.get('strings', 'no_file'), text)
|
||||||
|
|
||||||
elif command == var.config.get('command', 'queue'):
|
elif command == var.config.get('command', 'queue'):
|
||||||
if len(var.playlist) <= 1:
|
if len(var.playlist.playlist) <= 1:
|
||||||
msg = var.config.get('strings', 'queue_empty')
|
msg = var.config.get('strings', 'queue_empty')
|
||||||
else:
|
else:
|
||||||
msg = var.config.get(
|
msg = var.config.get(
|
||||||
'strings', 'queue_contents') + '<br />'
|
'strings', 'queue_contents') + '<br />'
|
||||||
i = 1
|
i = 1
|
||||||
for value in var.playlist[1:]:
|
for value in var.playlist.playlist:
|
||||||
msg += '[{}] ({}) {}<br />'.format(i, value['type'], value['title'] if 'title' in value else value['url'])
|
msg += '[{}] ({}) {}<br />'.format(i, value['type'], value['title'] if 'title' in value else value['url'])
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
self.send_msg(msg, text)
|
self.send_msg(msg, text)
|
||||||
|
|
||||||
elif command == var.config.get('command', 'repeat'):
|
elif command == var.config.get('command', 'repeat'):
|
||||||
var.playlist.append(var.playlist[0])
|
var.playlist.append(var.playlist.current_item())
|
||||||
|
music = var.playlist.current_item()
|
||||||
|
if music['type'] == 'file':
|
||||||
|
logging.info("bot: add to playlist: " + music['path'])
|
||||||
|
else:
|
||||||
|
logging.info("bot: add to playlist: " + music['url'])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.mumble.users[text.actor].send_text_message(
|
self.mumble.users[text.actor].send_text_message(
|
||||||
@@ -593,72 +630,84 @@ class MumbleBot:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def next():
|
def next():
|
||||||
logging.debug("Next into the queue")
|
logging.debug("bot: Next into the queue")
|
||||||
if len(var.playlist) > 1:
|
return var.playlist.next()
|
||||||
var.playlist.pop(0)
|
|
||||||
return True
|
|
||||||
elif len(var.playlist) == 1:
|
|
||||||
var.playlist.pop(0)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def launch_music(self):
|
def launch_music(self, index=-1):
|
||||||
uri = ""
|
uri = ""
|
||||||
logging.debug("launch_music asked" + str(var.playlist[0]))
|
music = None
|
||||||
if var.playlist[0]["type"] == "url":
|
if var.playlist.length() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if index == -1:
|
||||||
|
music = var.playlist.current_item()
|
||||||
|
else:
|
||||||
|
music = var.playlist.jump(index)
|
||||||
|
|
||||||
|
logging.info("bot: play music " + str(music['path'] if 'path' in music else music['url']))
|
||||||
|
if music["type"] == "url":
|
||||||
# Delete older music is the tmp folder is too big
|
# Delete older music is the tmp folder is too big
|
||||||
media.system.clear_tmp_folder(var.config.get(
|
media.system.clear_tmp_folder(var.config.get(
|
||||||
'bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size'))
|
'bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size'))
|
||||||
|
|
||||||
# Check if the music is ready to be played
|
# Check if the music is ready to be played
|
||||||
if var.playlist[0]["ready"] == "downloading":
|
if music["ready"] == "downloading":
|
||||||
return
|
return
|
||||||
elif var.playlist[0]["ready"] != "yes":
|
elif music["ready"] != "yes":
|
||||||
logging.info("Current music wasn't ready, Downloading...")
|
logging.info("Current music wasn't ready, Downloading...")
|
||||||
self.download_music(index=0)
|
self.download_music(music)
|
||||||
|
if music == False:
|
||||||
|
var.playlist.remove()
|
||||||
|
return
|
||||||
|
|
||||||
# get the Path
|
if self.update_music_tag_info():
|
||||||
uri = var.playlist[0]['path']
|
music = var.playlist.current_item()
|
||||||
if os.path.isfile(uri):
|
|
||||||
audio = EasyID3(uri)
|
|
||||||
title = ""
|
|
||||||
if audio["title"]:
|
|
||||||
# take the title from the file tag
|
|
||||||
title = audio["title"][0]
|
|
||||||
|
|
||||||
# Remove .mp3 and add .jpg
|
thumbnail_html = ''
|
||||||
path_thumbnail = var.playlist[0]['path'][:-4] + '.jpg'
|
if 'thumbnail' in music:
|
||||||
thumbnail_html = ""
|
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
|
||||||
if os.path.isfile(path_thumbnail):
|
music['thumbnail'] + '"/>'
|
||||||
# Create the image message
|
display = ''
|
||||||
im = Image.open(path_thumbnail)
|
if 'artist' in music:
|
||||||
im.thumbnail((100, 100), Image.ANTIALIAS)
|
display = music['artist'] + ' - '
|
||||||
buffer = BytesIO()
|
if 'title' in music:
|
||||||
im.save(buffer, format="JPEG")
|
display += music['title']
|
||||||
thumbnail_base64 = base64.b64encode(buffer.getvalue())
|
|
||||||
thumbnail_html = '<img src="data:image/PNG;base64,' + \
|
|
||||||
thumbnail_base64.decode() + '"/>'
|
|
||||||
|
|
||||||
logging.debug("Thumbail data " + thumbnail_html)
|
|
||||||
if var.config.getboolean('bot', 'announce_current_music'):
|
if var.config.getboolean('bot', 'announce_current_music'):
|
||||||
self.send_msg(var.config.get(
|
self.send_msg(var.config.get(
|
||||||
'strings', 'now_playing') % (title, thumbnail_html))
|
'strings', 'now_playing') % (display, thumbnail_html))
|
||||||
else:
|
|
||||||
logging.error("Error with the path during launch_music")
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif var.playlist[0]["type"] == "file":
|
elif music["type"] == "file":
|
||||||
uri = var.config.get('bot', 'music_folder') + \
|
uri = var.config.get('bot', 'music_folder') + \
|
||||||
var.playlist[0]["path"]
|
var.playlist.current_item()["path"]
|
||||||
|
|
||||||
|
if self.update_music_tag_info(uri):
|
||||||
|
music = var.playlist.current_item()
|
||||||
|
|
||||||
|
thumbnail_html = ''
|
||||||
|
if 'thumbnail' in music:
|
||||||
|
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
|
||||||
|
music['thumbnail'] + '"/>'
|
||||||
|
display = ''
|
||||||
|
if 'artist' in music:
|
||||||
|
display = music['artist'] + ' - '
|
||||||
|
if 'title' in music:
|
||||||
|
display += music['title']
|
||||||
|
|
||||||
|
if var.config.getboolean('bot', 'announce_current_music'):
|
||||||
|
self.send_msg(var.config.get(
|
||||||
|
'strings', 'now_playing') % (display, thumbnail_html))
|
||||||
|
|
||||||
|
elif music["type"] == "radio":
|
||||||
|
uri = music["url"]
|
||||||
|
if 'title' not in music:
|
||||||
|
logging.info("bot: fetching radio server description")
|
||||||
|
title = media.radio.get_radio_server_description(uri)
|
||||||
|
music["title"] = title
|
||||||
|
|
||||||
elif var.playlist[0]["type"] == "radio":
|
|
||||||
uri = var.playlist[0]["url"]
|
|
||||||
title = media.radio.get_radio_server_description(uri)
|
|
||||||
var.playlist[0]["title"] = title
|
|
||||||
if var.config.getboolean('bot', 'announce_current_music'):
|
if var.config.getboolean('bot', 'announce_current_music'):
|
||||||
self.send_msg(var.config.get('strings', 'now_playing') %
|
self.send_msg(var.config.get('strings', 'now_playing') %
|
||||||
(title, "URL : " + uri))
|
(music["title"], "URL: " + uri))
|
||||||
|
|
||||||
if var.config.getboolean('debug', 'ffmpeg'):
|
if var.config.getboolean('debug', 'ffmpeg'):
|
||||||
ffmpeg_debug = "debug"
|
ffmpeg_debug = "debug"
|
||||||
@@ -667,41 +716,47 @@ class MumbleBot:
|
|||||||
|
|
||||||
command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i',
|
command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i',
|
||||||
uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-')
|
uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-')
|
||||||
logging.info("FFmpeg command : " + " ".join(command))
|
logging.info("bot: execute ffmpeg command: " + " ".join(command))
|
||||||
# The ffmpeg process is a thread
|
# The ffmpeg process is a thread
|
||||||
self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480)
|
self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480)
|
||||||
self.is_playing = True
|
self.is_playing = True
|
||||||
|
self.is_pause = False
|
||||||
|
self.last_volume_cycle_time = time.time()
|
||||||
|
|
||||||
def download_music(self, index):
|
def download_music(self, index=-1):
|
||||||
if var.playlist[index]['type'] == 'url' and var.playlist[index]['ready'] == "validation":
|
if index == -1:
|
||||||
if media.url.get_url_info(index=index):
|
index = var.playlist.current_index
|
||||||
if var.playlist[index]['duration'] > var.config.getint('bot', 'max_track_duration'):
|
|
||||||
|
music = var.playlist.playlist[index]
|
||||||
|
if music['type'] == 'url' and music['ready'] == "validation":
|
||||||
|
music = media.url.get_url_info(music)
|
||||||
|
if music:
|
||||||
|
if music['duration'] > var.config.getint('bot', 'max_track_duration'):
|
||||||
# Check the length, useful in case of playlist, it wasn't checked before)
|
# Check the length, useful in case of playlist, it wasn't checked before)
|
||||||
var.playlist.pop()
|
|
||||||
logging.info(
|
logging.info(
|
||||||
"the music " + var.playlist[index]["url"] + " has a duration of " + var.playlist[index]['duration'] + "s -- too long")
|
"the music " + music["url"] + " has a duration of " + music['duration'] + "s -- too long")
|
||||||
self.send_msg(var.config.get('strings', 'too_long'))
|
self.send_msg(var.config.get('strings', 'too_long'))
|
||||||
return
|
return False
|
||||||
else:
|
else:
|
||||||
var.playlist[index]['ready'] = "no"
|
music['ready'] = "no"
|
||||||
else:
|
else:
|
||||||
var.playlist.pop(index)
|
|
||||||
logging.error("Error while fetching info from the URL")
|
logging.error("Error while fetching info from the URL")
|
||||||
self.send_msg(var.config.get('strings', 'unable_download'))
|
self.send_msg(var.config.get('strings', 'unable_download'))
|
||||||
|
return False
|
||||||
|
|
||||||
if var.playlist[index]['type'] == 'url' and var.playlist[index]['ready'] == "no":
|
if music['type'] == 'url' and music['ready'] == "no":
|
||||||
# download the music
|
# download the music
|
||||||
var.playlist[index]['ready'] = "downloading"
|
music['ready'] = "downloading"
|
||||||
|
|
||||||
logging.debug("Download index:" + str(index))
|
url = music['url']
|
||||||
logging.debug(var.playlist[index])
|
|
||||||
|
|
||||||
url = var.playlist[index]['url']
|
|
||||||
url_hash = hashlib.md5(url.encode()).hexdigest()
|
url_hash = hashlib.md5(url.encode()).hexdigest()
|
||||||
|
|
||||||
|
logging.info("bot: Download url:" + url)
|
||||||
|
logging.debug(music)
|
||||||
|
|
||||||
path = var.config.get('bot', 'tmp_folder') + url_hash + ".%(ext)s"
|
path = var.config.get('bot', 'tmp_folder') + url_hash + ".%(ext)s"
|
||||||
mp3 = path.replace(".%(ext)s", ".mp3")
|
mp3 = path.replace(".%(ext)s", ".mp3")
|
||||||
var.playlist[index]['path'] = mp3
|
music['path'] = mp3
|
||||||
|
|
||||||
# if os.path.isfile(mp3):
|
# if os.path.isfile(mp3):
|
||||||
# audio = EasyID3(mp3)
|
# audio = EasyID3(mp3)
|
||||||
@@ -721,35 +776,115 @@ class MumbleBot:
|
|||||||
{'key': 'FFmpegMetadata'}]
|
{'key': 'FFmpegMetadata'}]
|
||||||
}
|
}
|
||||||
self.send_msg(var.config.get(
|
self.send_msg(var.config.get(
|
||||||
'strings', "download_in_progress") % var.playlist[index]['title'])
|
'strings', "download_in_progress") % music['title'])
|
||||||
|
|
||||||
logging.info("Information before start downloading :" +
|
logging.info("Information before start downloading: " +
|
||||||
str(var.playlist[index]))
|
str(music['title']))
|
||||||
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
||||||
for i in range(2): # Always try 2 times
|
for i in range(2): # Always try 2 times
|
||||||
try:
|
try:
|
||||||
ydl.extract_info(url)
|
ydl.extract_info(url)
|
||||||
if 'ready' in var.playlist[index] and var.playlist[index]['ready'] == "downloading":
|
if 'ready' in music and music['ready'] == "downloading":
|
||||||
var.playlist[index]['ready'] = "yes"
|
music['ready'] = "yes"
|
||||||
except youtube_dl.utils.DownloadError:
|
except youtube_dl.utils.DownloadError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
return
|
var.playlist.playlist[index] = music
|
||||||
|
|
||||||
|
def update_music_tag_info(self, uri=""):
|
||||||
|
music = var.playlist.current_item()
|
||||||
|
if not music['type'] == 'file' and not music['type'] == 'url':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# get the Path
|
||||||
|
if uri == "":
|
||||||
|
if 'path' in music:
|
||||||
|
uri = music['path']
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if os.path.isfile(uri):
|
||||||
|
music = self.get_music_tag_info(music, uri)
|
||||||
|
var.playlist.update(music)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_music_tag_info(self, music, uri=""):
|
||||||
|
if not uri:
|
||||||
|
uri = music['path']
|
||||||
|
|
||||||
|
if os.path.isfile(uri):
|
||||||
|
try:
|
||||||
|
audio = EasyID3(uri)
|
||||||
|
if 'title' in audio:
|
||||||
|
# take the title from the file tag
|
||||||
|
music['title'] = audio["title"][0]
|
||||||
|
|
||||||
|
if 'artist' in audio:
|
||||||
|
music['artist'] = ', '.join(audio["artist"])
|
||||||
|
|
||||||
|
path_thumbnail = uri[:-3] + "jpg"
|
||||||
|
im = None
|
||||||
|
|
||||||
|
if os.path.isfile(path_thumbnail):
|
||||||
|
im = Image.open(path_thumbnail)
|
||||||
|
|
||||||
|
# try to extract artwork from mp3 ID3 tag
|
||||||
|
elif uri[-3:] == "mp3":
|
||||||
|
tags = mutagen.File(uri)
|
||||||
|
if "APIC:" in tags:
|
||||||
|
im = Image.open(BytesIO(tags["APIC:"].data))
|
||||||
|
|
||||||
|
if im:
|
||||||
|
im.thumbnail((100, 100), Image.ANTIALIAS)
|
||||||
|
buffer = BytesIO()
|
||||||
|
im = im.convert('RGB')
|
||||||
|
im.save(buffer, format="JPEG")
|
||||||
|
music['thumbnail'] = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||||
|
|
||||||
|
return music
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# if nothing found
|
||||||
|
music['title'] = os.path.basename(uri)[:-4]
|
||||||
|
|
||||||
|
return music
|
||||||
|
|
||||||
|
|
||||||
def async_download_next(self):
|
def async_download_next(self):
|
||||||
# Function start if the next music isn't ready
|
# Function start if the next music isn't ready
|
||||||
# Do nothing in case the next music is already downloaded
|
# Do nothing in case the next music is already downloaded
|
||||||
logging.info("Async download next asked")
|
logging.info("bot: Async download next asked ")
|
||||||
if len(var.playlist) > 1 and var.playlist[1]['type'] == 'url' and var.playlist[1]['ready'] in ["no", "validation"]:
|
if len(var.playlist.playlist) > 1 and var.playlist.next_item()['type'] == 'url' \
|
||||||
|
and var.playlist.next_item()['ready'] in ["no", "validation"]:
|
||||||
th = threading.Thread(
|
th = threading.Thread(
|
||||||
target=self.download_music, kwargs={'index': 1})
|
target=self.download_music, kwargs={'index': var.playlist.next_index()})
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
logging.info("Start downloading next in thread")
|
logging.info("bot: Start downloading next in thread")
|
||||||
th.daemon = True
|
th.daemon = True
|
||||||
th.start()
|
th.start()
|
||||||
|
|
||||||
|
def volume_cycle(self):
|
||||||
|
delta = time.time() - self.last_volume_cycle_time
|
||||||
|
|
||||||
|
if delta > 0.001:
|
||||||
|
if self.is_ducking and self.on_ducking:
|
||||||
|
self.volume = (self.volume - self.ducking_volume) * math.exp(- delta / 0.2) + self.ducking_volume
|
||||||
|
if self.ducking_release > time.time():
|
||||||
|
self.on_ducking = False
|
||||||
|
else:
|
||||||
|
self.volume = self.volume_set - (self.volume_set - self.volume) * math.exp(- delta / 0.5)
|
||||||
|
|
||||||
|
self.last_volume_cycle_time = time.time()
|
||||||
|
|
||||||
|
def ducking_sound_received(self, user, sound):
|
||||||
|
self.on_ducking = True
|
||||||
|
self.ducking_release = time.time() + 1 # ducking release after 0.5s
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
# Parse the html from the message to get the URL
|
# Parse the html from the message to get the URL
|
||||||
def get_url_from_input(string):
|
def get_url_from_input(string):
|
||||||
@@ -775,6 +910,7 @@ class MumbleBot:
|
|||||||
raw_music = self.thread.stdout.read(480)
|
raw_music = self.thread.stdout.read(480)
|
||||||
if raw_music:
|
if raw_music:
|
||||||
# Adjust the volume and send it to mumble
|
# Adjust the volume and send it to mumble
|
||||||
|
self.volume_cycle()
|
||||||
self.mumble.sound_output.add_sound(
|
self.mumble.sound_output.add_sound(
|
||||||
audioop.mul(raw_music, 2, self.volume))
|
audioop.mul(raw_music, 2, self.volume))
|
||||||
else:
|
else:
|
||||||
@@ -788,9 +924,9 @@ class MumbleBot:
|
|||||||
# get next music
|
# get next music
|
||||||
self.is_playing = False
|
self.is_playing = False
|
||||||
self.next()
|
self.next()
|
||||||
if len(var.playlist) > 0:
|
if not self.is_pause and len(var.playlist.playlist) > 0:
|
||||||
if var.playlist[0]['type'] in ['radio', 'file'] \
|
if var.playlist.current_item()['type'] in ['radio', 'file'] \
|
||||||
or (var.playlist[0]['type'] == 'url' and var.playlist[0]['ready'] not in ['validation', 'downloading']):
|
or (var.playlist.current_item()['type'] == 'url' and var.playlist.current_item()['ready'] not in ['validation', 'downloading']):
|
||||||
# Check if the music can be start before launch the music
|
# Check if the music can be start before launch the music
|
||||||
self.launch_music()
|
self.launch_music()
|
||||||
self.async_download_next()
|
self.async_download_next()
|
||||||
@@ -809,8 +945,18 @@ class MumbleBot:
|
|||||||
if self.thread:
|
if self.thread:
|
||||||
self.thread.kill()
|
self.thread.kill()
|
||||||
self.thread = None
|
self.thread = None
|
||||||
var.playlist = []
|
var.playlist.clear()
|
||||||
self.is_playing = False
|
self.is_playing = False
|
||||||
|
logging.info("bot: music stopped. playlist trashed.")
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
# Kill the ffmpeg thread
|
||||||
|
if self.thread:
|
||||||
|
self.thread.kill()
|
||||||
|
self.thread = None
|
||||||
|
self.is_playing = False
|
||||||
|
self.is_pause = True
|
||||||
|
logging.info("bot: music paused.")
|
||||||
|
|
||||||
def set_comment(self):
|
def set_comment(self):
|
||||||
self.mumble.users.myself.comment(var.config.get('bot', 'comment'))
|
self.mumble.users.myself.comment(var.config.get('bot', 'comment'))
|
||||||
@@ -885,4 +1031,5 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
var.config = config
|
var.config = config
|
||||||
var.db = db
|
var.db = db
|
||||||
botamusique = MumbleBot(args)
|
var.botamusique = MumbleBot(args)
|
||||||
|
var.botamusique.loop()
|
||||||
|
|||||||
+1
-1
Submodule pymumble updated: 8ccfb0e7cf...437d2ebec6
@@ -0,0 +1,4 @@
|
|||||||
|
.bs-docs-section{margin-top:4em}
|
||||||
|
.btn-space{margin-right:5px}
|
||||||
|
.playlist-title{display:inline-block}
|
||||||
|
.playlist-artwork{display:inline-block; margin-left:5px;}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
File diff suppressed because one or more lines are too long
+203
-88
@@ -4,23 +4,43 @@
|
|||||||
{%- set subdirpath = os.path.relpath(subdirobj.fullpath, music_library.fullpath) %}
|
{%- set subdirpath = os.path.relpath(subdirobj.fullpath, music_library.fullpath) %}
|
||||||
{%- set subdirid = subdirpath.replace("/","-") %}
|
{%- set subdirid = subdirpath.replace("/","-") %}
|
||||||
<li class="directory list-group-item list-group-item-primary">
|
<li class="directory list-group-item list-group-item-primary">
|
||||||
<span>{{ subdirpath }}/ </span>
|
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<form method="post" class="directory">
|
<div class="btn-group" role="group">
|
||||||
<input type="text" value="{{ subdirpath }}" name="add_folder" hidden>
|
<button type="button" class="btn btn-success btn-sm"
|
||||||
<button type="submit" class="btn btn-secondary">Add all tracks from this folder</button>
|
onclick="request('/post', {add_folder : '{{ subdirpath }}'})">
|
||||||
</form>
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
<form method="post" class="directory">
|
</button>
|
||||||
<input type="text" value="{{ subdirpath }}" name="add_folder_recursively" hidden>
|
<div class="btn-group" role="group">
|
||||||
<button type="submit" class="btn btn-secondary">Add all tracks from this folder (recursively)</button>
|
<button id="btnGroupDrop2" type="button" class="btn btn-success btn-sm dropdown-toggle btn-space" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
|
||||||
</form>
|
<div class="dropdown-menu" aria-labelledby="btnGroupDrop2" style="">
|
||||||
|
<a class="dropdown-item"
|
||||||
|
onclick="request('/post', {add_folder : '{{ subdirpath }}'})">
|
||||||
|
<i class="fa fa-folder" aria-hidden="true"></i> Entire folder
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item"
|
||||||
|
onclick="request('/post', {add_folder_recursively : '{{ subdirpath }}'})">
|
||||||
|
<i class="fa fa-folder" aria-hidden="true"></i> Entire folder and sub-folders
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group lead"><div class="btn-space"><i class="fa fa-folder" aria-hidden="true"></i></div><a class="lead" data-toggle="collapse"
|
||||||
|
data-target="#multiCollapse-{{ subdirid }}" aria-expanded="true"
|
||||||
|
aria-controls="multiCollapse-{{ subdirid }}" href="#"> {{ subdirpath }}/</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group" style="float: right;">
|
||||||
<form action="./download" method="get" class="directory">
|
<form action="./download" method="get" class="directory">
|
||||||
<input type="text" value="{{ subdirpath }}" name="directory" hidden>
|
<input type="text" value="{{ subdirpath }}" name="directory" hidden>
|
||||||
<button type="submit" class="btn btn-secondary">Download entire directory</button>
|
<button type="submit" class="btn btn-primary btn-sm btn-space"><i class="fa fa-download" aria-hidden="true"></i></button>
|
||||||
|
</form>
|
||||||
|
<form method="post">
|
||||||
|
<input type="text" value="{{ subdirpath }}" name="delete_folder" hidden>
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm btn-space"><i class="fas fa-trash-alt"></i></button>
|
||||||
</form>
|
</form>
|
||||||
<button class="btn btn-primary" type="button" data-toggle="collapse"
|
|
||||||
data-target="#multiCollapse-{{ subdirid }}" aria-expanded="true"
|
|
||||||
aria-controls="multiCollapse-{{ subdirid }}">Toggle Collapse</button>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<div class="collapse multi-collapse" id="multiCollapse-{{ subdirid }}">
|
<div class="collapse multi-collapse" id="multiCollapse-{{ subdirid }}">
|
||||||
@@ -33,15 +53,39 @@
|
|||||||
{% set filepath = os.path.relpath(os.path.join(dir.fullpath, file), music_library.fullpath) %}
|
{% set filepath = os.path.relpath(os.path.join(dir.fullpath, file), music_library.fullpath) %}
|
||||||
<li class="file list-group-item">
|
<li class="file list-group-item">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<form method="post" class="file file_add">
|
<div class="btn-group" role="group">
|
||||||
<input type="text" value="{{ filepath }}" name="add_file" hidden>
|
<button type="button" class="btn btn-success btn-sm"
|
||||||
<button type="submit" class="btn btn-primary">Add</button>
|
onclick="request('/post', {add_file_bottom : '{{ filepath }}'})">
|
||||||
</form>
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button id="btnGroupDrop2" type="button" class="btn btn-success btn-sm dropdown-toggle btn-space" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="btnGroupDrop2" style="">
|
||||||
|
<a class="dropdown-item"
|
||||||
|
onclick="request('/post', {add_file_bottom : '{{ filepath }}'})">
|
||||||
|
<i class="fa fa-angle-down" aria-hidden="true"></i> To bottom of play list
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item"
|
||||||
|
onclick="request('/post', {add_file_next : '{{ filepath }}'})">
|
||||||
|
<i class="fa fa-angle-right" aria-hidden="true"></i> After current song
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="btn-group lead"><div class="btn-space"><i class="fa fa-music" aria-hidden="true"></i></div> {{ filepath }}</div>
|
||||||
|
|
||||||
|
<div class="btn-group" style="float: right;">
|
||||||
<form action="./download" method="get" class="file file_download">
|
<form action="./download" method="get" class="file file_download">
|
||||||
<input type="text" value="{{ filepath }}" name="file" hidden>
|
<input type="text" value="{{ filepath }}" name="file" hidden>
|
||||||
<button type="submit" class="btn btn-primary">Download</button>
|
<button type="submit" class="btn btn-primary btn-sm btn-space"><i class="fa fa-download" aria-hidden="true"></i></button>
|
||||||
</form>
|
</form>
|
||||||
</div> {{ filepath }}
|
<form method="post">
|
||||||
|
<input type="text" value="{{ filepath }}" name="delete_music_file" hidden>
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm btn-space"><i class="fas fa-trash-alt"></i></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@@ -54,98 +98,94 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>botamusique web interface</title>
|
<title>botamusique web interface</title>
|
||||||
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/custom.css">
|
||||||
<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
|
<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
|
||||||
<META HTTP-EQUIV="Expires" CONTENT="-1">
|
<META HTTP-EQUIV="Expires" CONTENT="-1">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="bs-docs-section">
|
||||||
|
<div class="page-header" id="banner">
|
||||||
|
<h1><i class="fa fa-music" aria-hidden="true"></i> botamusique Web Interface</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bs-docs-section">
|
||||||
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div id="playlist" class="card">
|
<div id="playlist" class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title">Play List</h2>
|
<h2 class="card-title"><i class="fa fa-list" aria-hidden="true"></i> Play List</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div>
|
<div class="btn-group" style="margin-bottom: 5px;">
|
||||||
Currently Playing :
|
<button type="button" class="btn btn-primary btn-space"
|
||||||
{% if playlist|length > 0 %}
|
onclick="request('/post', {action : 'randomize'})">
|
||||||
{% if 'url' in playlist[0] %}
|
<i class="fas fa-random" aria-hidden="true"></i>
|
||||||
<a href="{{ playlist[0]['url'] }}">{{ playlist[0]['url'] }}</a>
|
</button>
|
||||||
{% elif 'path' in playlist[0] %}
|
<button type="button" class="btn btn-danger btn-space"
|
||||||
{{ playlist[0]['path'] }}
|
onclick="request('/post', {action : 'stop'})">
|
||||||
{% endif %}
|
<i class="fas fa-stop" aria-hidden="true"></i>
|
||||||
{% else %}
|
</button>
|
||||||
No music
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
<div class="btn-group" style="float: right;">
|
||||||
|
<button type="button" class="btn btn-warning btn-space"
|
||||||
|
onclick="request('/post', {action : 'volume_down'})">
|
||||||
|
<i class="fa fa-volume-down" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning btn-space"
|
||||||
|
onclick="request('/post', {action : 'volume_up'})">
|
||||||
|
<i class="fa fa-volume-up" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<form method="post">
|
|
||||||
<input type="text" value="randomize" name="action" hidden>
|
|
||||||
<button type="submit" class="btn btn-primary">Randomize playlist</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">#</th>
|
<th scope="col">#</th>
|
||||||
<th scope="col">Type</th>
|
|
||||||
<th scope="col">Title</th>
|
<th scope="col">Title</th>
|
||||||
<th scope="col">Url/Path</th>
|
<th scope="col">Url/Path</th>
|
||||||
<th scope="col">Action</th>
|
<th scope="col">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="playlist-table">
|
||||||
{% for m in playlist[1:] %}
|
<tr class="table-dark">
|
||||||
<tr>
|
<td colspan="4" class="text-muted" style="text-align:center;"> Fetching playlist .... </td>
|
||||||
<th scope="row">{{ loop.index }}</th>
|
|
||||||
<td>{{ m['type'] }}</td>
|
|
||||||
<td>
|
|
||||||
{% if 'title' in m %}
|
|
||||||
({{ m['title'] }})
|
|
||||||
{% else %}
|
|
||||||
No title
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if 'url' in m %}
|
|
||||||
<a href="{{ m['url'] }}">{{ m['url'] }}</a>
|
|
||||||
{% elif 'path' in m %}
|
|
||||||
{{ m['path'] }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<form method="post">
|
|
||||||
<input type="text" value="{{ loop.index }}" name="delete_music" hidden>
|
|
||||||
<button type="submit" class="btn btn-primary">Remove</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-danger btn-space"
|
||||||
|
onclick="request('/post', {action : 'clear'})">
|
||||||
|
<i class="fas fa-trash-alt" aria-hidden="true"></i> Clear Playlist
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="bs-docs-section">
|
||||||
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div id="browser" class="card">
|
<div id="browser" class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title">Music library</h2>
|
<h2 class="card-title"><i class="fa fa-list" aria-hidden="true"></i> Music Library</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" style="margin-bottom: 5px;" role="group">
|
||||||
<form action="./download" method="get" class="directory form1">
|
<form action="./download" method="get" class="directory form1">
|
||||||
<input type="text" value="./" name="directory" hidden>
|
<input type="text" value="./" name="directory" hidden>
|
||||||
<button type="submit" class="btn btn-primary">Download entire music library</button>
|
<button type="submit" class="btn btn-secondary btn-space"><i class="fa fa-download" aria-hidden="true"></i> Download All</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" class="directory form3">
|
<form method="post" class="directory form3">
|
||||||
<input type="text" value="./" name="add_folder_recursively" hidden>
|
<input type="text" value="./" name="add_folder_recursively" hidden>
|
||||||
<button type="submit" class="btn btn-primary">Add all tracks from music library
|
<button type="submit" class="btn btn-secondary btn-space"><i class="fa fa-plus" aria-hidden="true"></i> Add All</button>
|
||||||
(recursively)</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -154,10 +194,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="upload" class="container">
|
<div id="upload" class="container">
|
||||||
<div class="row">
|
<div class="bs-docs-section">
|
||||||
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -165,24 +207,29 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form action="./upload" method="post" enctype="multipart/form-data">
|
<form action="./upload" method="post" enctype="multipart/form-data">
|
||||||
<div class="input-group">
|
<div class="row" style="margin-bottom: 5px;">
|
||||||
<div class="custom-file">
|
<div id="uploadBox" class="col-lg-8 input-group">
|
||||||
<input type="file" name="file" class="custom-file-input" id="uploadSelectFile"
|
<div id="uploadField" style="display: flex; width: 100%">
|
||||||
aria-describedby="uploadSubmit" value="Browse Music file" />
|
<div class="custom-file btn-space">
|
||||||
<label class="custom-file-label" for="uploadSelectFile">Choose file</label>
|
<input type="file" name="file[]" class="custom-file-input" id="uploadSelectFile"
|
||||||
|
aria-describedby="uploadSubmit" value="Browse Music file" multiple/>
|
||||||
|
<label class="custom-file-label" for="uploadSelectFile">Choose file</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group-append">
|
<div class="col-lg-4 input-group-append">
|
||||||
<span class="input-group-text">Upload To</span>
|
<span class="input-group-text">Upload To</span>
|
||||||
<input class="form-control" list="targetdirs" id="targetdir" name="targetdir"
|
<input class="form-control btn-space" list="targetdirs" id="targetdir" name="targetdir"
|
||||||
placeholder="uploads" />
|
placeholder="uploads" />
|
||||||
<datalist id="targetdirs">
|
<datalist id="targetdirs">
|
||||||
<option value="uploads">
|
<option value="uploads">
|
||||||
{% for dir in music_library.get_subdirs_recursively() %}
|
{% for dir in music_library.get_subdirs_recursively() %}
|
||||||
<option value="{{ dir }}">
|
<option value="{{ dir }}">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</datalist>
|
</datalist>
|
||||||
|
|
||||||
<button class="btn btn-outline-secondary" type="submit"
|
<button class="btn btn-outline-secondary" type="submit"
|
||||||
id="uploadSubmit">Upload</button>
|
id="uploadSubmit">Upload</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -190,7 +237,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
</div>
|
||||||
|
|
||||||
|
<div class="bs-docs-section">
|
||||||
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -198,9 +248,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<label>Add Youtube/Soundcloud URL :</label>
|
<label>Add Youtube/Soundcloud URL</label>
|
||||||
<input class="form-control" type="text" name="add_url">
|
<div class="input-group">
|
||||||
<button type="submit" class="btn btn-primary">Add URL</button>
|
<input class="form-control btn-space" type="text" name="add_url">
|
||||||
|
<button type="submit" class="btn btn-primary">Add URL</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,25 +264,88 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<label>Add Radio URL :</label>
|
<label>Add Radio URL</label>
|
||||||
<input class="form-control" type="text" name="add_radio">
|
<div class="input-group">
|
||||||
<button type="submit" class="btn btn-primary">Add Radio</button>
|
<input class="form-control btn-space" type="text" name="add_radio">
|
||||||
|
<button type="submit" class="btn btn-primary">Add Radio</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
|
<script src="/static/js/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
|
||||||
<script src="/static/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
<script src="/static/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||||
|
<script src="/static/js/fontawesome.all.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$('#uploadSelectFile').on('change', function () {
|
$('#uploadSelectFile').on('change', function () {
|
||||||
//get the file name
|
//get the file name
|
||||||
var fileName = $(this).val().replace('C:\\fakepath\\', " ");
|
var fileName = $(this).val().replace('C:\\fakepath\\', " ");
|
||||||
//replace the "Choose a file" label
|
//replace the "Choose a file" label
|
||||||
$(this).next('.custom-file-label').html(fileName);
|
$(this).next('.custom-file-label').html(fileName);
|
||||||
})
|
});
|
||||||
|
$('a.a-submit, button.btn-submit').on('click', function (event) {
|
||||||
|
$(event.target).closest('form').submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
var playlist_ver = 0;
|
||||||
|
|
||||||
|
function request(url, _data){
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/post',
|
||||||
|
data : _data,
|
||||||
|
statusCode : {
|
||||||
|
200 : function(data) {
|
||||||
|
if (data.ver > playlist_ver) {
|
||||||
|
updatePlaylist();
|
||||||
|
playlist_ver = data.ver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayPlaylist(data){
|
||||||
|
$("#playlist-table tr").remove();
|
||||||
|
$.each(data, function(index, item){
|
||||||
|
$("#playlist-table").append(item);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePlaylist(){
|
||||||
|
$.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
url: '/playlist',
|
||||||
|
statusCode : {
|
||||||
|
200 : displayPlaylist
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Check the version of playlist to see if update is needed.
|
||||||
|
setInterval(function(){
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url : '/post',
|
||||||
|
statusCode : {
|
||||||
|
200 : function(data){
|
||||||
|
if(data.ver > playlist_ver){
|
||||||
|
updatePlaylist();
|
||||||
|
playlist_ver = data.ver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} , 3000);
|
||||||
|
|
||||||
|
$(document).ready(updatePlaylist);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{% if index == -1 %}
|
||||||
|
<tr class="table-dark">
|
||||||
|
<td colspan="4" class="text-muted" style="text-align:center;"> Play list is empty. </td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
{% if index == playlist.current_index %}
|
||||||
|
<tr class="table-active">
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
{% endif %}
|
||||||
|
<th scope="row">{{ index + 1 }}</th>
|
||||||
|
<td>
|
||||||
|
<div class="playlist-title">
|
||||||
|
{% if 'thumbnail' in m %}
|
||||||
|
<img width="80" src="data:image/PNG;base64,{{ m['thumbnail'] }}"/>
|
||||||
|
{% else %}
|
||||||
|
<img width="80" src="/static/image/unknown-album.png"/>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="playlist-artwork">
|
||||||
|
{% if 'title' in m and m['title'].strip() %}
|
||||||
|
<b>{{ m['title']|truncate(45) }}</b>
|
||||||
|
{% elif 'url' in m %}
|
||||||
|
<b>{{ m['url']|truncate(45) }}</b>
|
||||||
|
{% endif %}
|
||||||
|
<span class="badge badge-secondary">{{ m['type'].capitalize() }}</span>
|
||||||
|
<br>
|
||||||
|
{% if 'artist' in m %}
|
||||||
|
{{ m['artist'] }}
|
||||||
|
{% else %}
|
||||||
|
Unknown Artist
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if 'url' in m %}
|
||||||
|
<small><a href="{{ m['url'] }}"><i>{{ m['url']|truncate(50) }}</i></a></small>
|
||||||
|
{% elif 'path' in m %}
|
||||||
|
<small>{{ m['path']|truncate(50) }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-success btn-sm btn-space"
|
||||||
|
onclick="request('/post', {play_music : '{{ index }}'})">
|
||||||
|
<i class="fa fa-play" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm btn-space"
|
||||||
|
onclick="request('/post', {delete_music : '{{ index }}'})">
|
||||||
|
<i class="fas fa-trash-alt" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
current_music = None
|
current_music = None
|
||||||
playlist = []
|
playlist = None
|
||||||
user = ""
|
user = ""
|
||||||
music_folder = ""
|
music_folder = ""
|
||||||
is_proxified = False
|
is_proxified = False
|
||||||
dbfile = None
|
dbfile = None
|
||||||
db = None
|
db = None
|
||||||
config = None
|
config = None
|
||||||
|
botamusique = None
|
||||||
Reference in New Issue
Block a user