Merge pull request #67 from TerryGeng/master

Enhanced the web interface
This commit is contained in:
azlux
2020-02-08 12:14:11 +01:00
committed by GitHub
12 changed files with 5259 additions and 309 deletions
+9
View File
@@ -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 = !
+204 -66
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+4
View File
@@ -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
View File
@@ -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 }}/&nbsp;</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>&nbsp;{{ 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>
+55
View File
@@ -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 %}
+3 -2
View File
@@ -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