From 4ae9c1cdb77f20fb20ef157ad7baa27afcaa9848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Fri, 18 May 2018 17:46:21 +0200 Subject: [PATCH 01/30] wip --- configuration.ini | 4 ++-- interface.py | 2 +- mumbleBot.py | 11 +++++------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/configuration.ini b/configuration.ini index 894a72f..fbb3549 100644 --- a/configuration.ini +++ b/configuration.ini @@ -1,8 +1,8 @@ [bot] comment = Coucou, Je suis née du savoir du Azlux, accès au https://azlux.fr/bot volume = 0.1 -admin = Azlux;AzMobile -music_folder = /home/dmichel/botamusique/music/ +admin = BafTac +music_folder = /home/fabian/music/mumblebot/ tmp_folder = /tmp/ is_proxified = True diff --git a/interface.py b/interface.py index 8307595..db8e303 100644 --- a/interface.py +++ b/interface.py @@ -113,4 +113,4 @@ def download(): if __name__ == '__main__': - web.run(port=8181, host="0.0.0.0") + web.run(port=8181, host="127.0.0.1") diff --git a/mumbleBot.py b/mumbleBot.py index 66f7637..13a35bf 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -109,11 +109,10 @@ class MumbleBot: print(command + ' - ' + parameter + ' by ' + self.mumble.users[text.actor]['name']) if command == self.config.get('command', 'play_file') and parameter: - path = self.config.get('bot', 'music_folder') + parameter - if "/" in parameter: - self.mumble.users[text.actor].send_message(self.config.get('strings', 'bad_file')) - elif os.path.isfile(path): - var.playlist.append(["file", path]) + music_folder = self.config.get('bot', 'music_folder') + path = os.path.abspath(os.path.join(music_folder, parameter)) + if path.startswith(music_folder) and os.path.isfile(path): + self.launch_play_file(path) else: self.mumble.users[text.actor].send_message(self.config.get('strings', 'bad_file')) @@ -275,7 +274,7 @@ class MumbleBot: def start_web_interface(): - interface.web.run(port=8181, host="0.0.0.0") + interface.web.run(port=8181, host="127.0.0.1") if __name__ == '__main__': From 7910f23d0d6c8f3bab0c14c47e1c04da4b2de469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Fri, 18 May 2018 21:31:52 +0200 Subject: [PATCH 02/30] Support playback of local files - Recursive listing of available files - Protected against path traversal attacks - Does not follow symlinks - Possibility to ignore directories or files with specific names - If no exact match for the requested track can be found, the bot plays the track with the best match - If multiple tracks match the user gets a message listing all candidates --- configuration.ini | 3 +++ mumbleBot.py | 49 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/configuration.ini b/configuration.ini index fbb3549..0d43ea7 100644 --- a/configuration.ini +++ b/configuration.ini @@ -4,6 +4,8 @@ volume = 0.1 admin = BafTac music_folder = /home/fabian/music/mumblebot/ tmp_folder = /tmp/ +ignored_folders = tmp +ignored_files = Thumbs.db is_proxified = True [debug] @@ -35,6 +37,7 @@ not_playing = Aucun stream en lecture bad_file = Bad file asked no_file = Not file here bad_url = Bad URL asked +multiple_matches = Track not found! Possible candidates: help = Command available:
!play_file diff --git a/mumbleBot.py b/mumbleBot.py index 13a35bf..8ee49ea 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -110,9 +110,20 @@ class MumbleBot: if command == self.config.get('command', 'play_file') and parameter: music_folder = self.config.get('bot', 'music_folder') + # sanitize "../" and so on path = os.path.abspath(os.path.join(music_folder, parameter)) - if path.startswith(music_folder) and os.path.isfile(path): - self.launch_play_file(path) + if path.startswith(music_folder): + if os.path.isfile(path): + self.launch_play_file(path) + else: + # try to do a partial match + matches = [file for file in self.__get_recursive_filelist_sorted(music_folder) if parameter in file] + if len(matches) == 1: + self.launch_play_file(music_folder + matches[0]) + else: + msg = self.config.get('strings', 'multiple_matches') + '
' + msg += '
'.join(matches) + self.mumble.users[text.actor].send_message(msg) else: self.mumble.users[text.actor].send_message(self.config.get('strings', 'bad_file')) @@ -164,9 +175,27 @@ class MumbleBot: var.current_music = var.playlist[0] var.playlist.pop(0) self.launch_next() + elif command == self.config.get('command', 'list'): + folder_path = self.config.get('bot', 'music_folder') + + files = self.__get_recursive_filelist_sorted(folder_path) + if files : + self.mumble.users[text.actor].send_message('
'.join(files)) + else : + self.mumble.users[text.actor].send_message(self.config.get('strings', 'no_file')) else: self.mumble.users[text.actor].send_message(self.config.get('strings', 'bad_command')) + def launch_play_file(self, path): + self.stop() + if self.config.getboolean('debug', 'ffmpeg'): + ffmpeg_debug = "debug" + else: + ffmpeg_debug = "warning" + command = ["ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', path, '-ac', '1', '-f', 's16le', '-ar', '48000', '-'] + self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480) + self.playing = True + def is_admin(self, user): username = self.mumble.users[user]['name'] list_admin = self.config.get('bot', 'admin').split(';') @@ -272,6 +301,22 @@ class MumbleBot: channel = self.mumble.channels[self.mumble.users.myself['channel_id']] channel.send_text_message(msg) + def __get_recursive_filelist_sorted(self, path): + filelist = [] + for root, dirs, files in os.walk(path): + relroot = root.replace(path, '') + if relroot in self.config.get('bot', 'ignored_folders'): + continue + if len(relroot): + relroot += '/' + for file in files: + if file in self.config.get('bot', 'ignored_files'): + continue + filelist.append(relroot + file) + + filelist.sort() + return filelist + def start_web_interface(): interface.web.run(port=8181, host="127.0.0.1") From 3c2bc37a5d04d56b661a8e515e4288ad81821d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Fri, 18 May 2018 21:36:14 +0200 Subject: [PATCH 03/30] Fix help message --- configuration.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration.ini b/configuration.ini index 0d43ea7..141a2a6 100644 --- a/configuration.ini +++ b/configuration.ini @@ -45,6 +45,6 @@ help = Command available:
!play_radio - url of a stream
!next - jump to the next music of the playlist
!stop - stop and clear the playlist -
!play_file - stop + Go to default channel +
!oust - stop + Go to default channel
!v - get or change the volume (in %)
!joinme From 64988847e44985768030320baaa9447795bf7fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Fri, 18 May 2018 21:39:04 +0200 Subject: [PATCH 04/30] Add requirements.txt --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..62d44ae --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +opuslib==1.1.0 +protobuf==3.1.0 +flask +youtube-dl From dd01c8fc106b5b141fe806c59b253e4d1bf5dd00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Fri, 18 May 2018 22:51:55 +0200 Subject: [PATCH 05/30] Strip leading and trailing whitespace from commands --- mumbleBot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mumbleBot.py b/mumbleBot.py index 8ee49ea..0bf2d87 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -95,7 +95,7 @@ class MumbleBot: self.nb_exit += 1 def message_received(self, text): - message = text.message + message = text.message.strip() if message[0] == '!': message = message[1:].split(' ', 1) if len(message) > 0: From 53b35df4e4b9b0156c6e2161d21bf11631a61e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Fri, 18 May 2018 22:52:40 +0200 Subject: [PATCH 06/30] Put files in queue, match tracks case insensitive --- mumbleBot.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mumbleBot.py b/mumbleBot.py index 0bf2d87..4cb40d5 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -114,12 +114,17 @@ class MumbleBot: path = os.path.abspath(os.path.join(music_folder, parameter)) if path.startswith(music_folder): if os.path.isfile(path): - self.launch_play_file(path) + #self.launch_play_file(path) + filename = path.replace(music_folder, '') + var.playlist.append(["file", filename]) else: # try to do a partial match - matches = [file for file in self.__get_recursive_filelist_sorted(music_folder) if parameter in file] - if len(matches) == 1: - self.launch_play_file(music_folder + matches[0]) + matches = [file for file in self.__get_recursive_filelist_sorted(music_folder) if parameter.lower() in file.lower()] + if len(matches) == 0: + self.mumble.users[text.actor].send_message(self.config.get('strings', 'no_file')) + elif len(matches) == 1: + #self.launch_play_file(music_folder + matches[0]) + var.playlist.append(["file", matches[0]]) else: msg = self.config.get('strings', 'multiple_matches') + '
' msg += '
'.join(matches) From fa5495a3416997006901034cbd45061bfce8cc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Fri, 18 May 2018 23:05:38 +0200 Subject: [PATCH 07/30] Add !queue command to show next items in the queue --- configuration.ini | 5 +++++ mumbleBot.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/configuration.ini b/configuration.ini index 141a2a6..215138a 100644 --- a/configuration.ini +++ b/configuration.ini @@ -26,6 +26,7 @@ volume = v kill = kill stop_and_getout = oust joinme = joinme +queue = queue [strings] @@ -38,11 +39,15 @@ bad_file = Bad file asked no_file = Not file here bad_url = Bad URL asked multiple_matches = Track not found! Possible candidates: +queue_contents = The next items in the queue are: +queue_empty = The queue is empty! help = Command available:
!play_file
!play_url - youtube or soundcloud
!play_radio - url of a stream +
!list - display list of available tracks +
!queue - display items in queue
!next - jump to the next music of the playlist
!stop - stop and clear the playlist
!oust - stop + Go to default channel diff --git a/mumbleBot.py b/mumbleBot.py index 4cb40d5..d2aeb53 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -188,6 +188,16 @@ class MumbleBot: self.mumble.users[text.actor].send_message('
'.join(files)) else : self.mumble.users[text.actor].send_message(self.config.get('strings', 'no_file')) + + elif command == self.config.get('command', 'queue'): + if len(var.playlist) == 0: + msg = self.config.get('strings', 'queue_empty') + else: + msg = self.config.get('strings', 'queue_contents') + '
' + for (type, path) in var.playlist: + msg += '({}) {}
'.format(type, path) + + self.send_msg_channel(msg) else: self.mumble.users[text.actor].send_message(self.config.get('strings', 'bad_command')) From 85f90e4e44173b6c4eae8d97b20a2700f2cc3267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sat, 19 May 2018 22:34:56 +0200 Subject: [PATCH 08/30] Improve web interface (+ other small changes) - Web Interface shows folders recursively - Add command line options for web interface (--wi-port, --wi-addr) - Move command line option parsing to main - Move shared code to utils.py - Fix some issues with web interface - Fix other small things --- interface.py | 39 ++++++++++++++------ mumbleBot.py | 67 +++++++++++++++------------------- templates/index.html | 42 ++++++++++++++------- util.py | 87 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 63 deletions(-) create mode 100644 util.py diff --git a/interface.py b/interface.py index db8e303..85878b2 100644 --- a/interface.py +++ b/interface.py @@ -2,12 +2,12 @@ from flask import Flask, render_template, request, redirect import variables as var +import util import os.path from os import listdir import random from werkzeug.utils import secure_filename - class ReverseProxied(object): '''Wrap the application in this middleware and configure the front-end server to add these headers, to let you quietly bind @@ -58,17 +58,33 @@ def init_proxy(): @web.route("/", methods=['GET', 'POST']) def index(): folder_path = var.music_folder - files = {} - dirs = [f for f in listdir(folder_path) if os.path.isdir(os.path.join(folder_path, f))] - for director in dirs: - files[director] = [f for f in listdir(folder_path + director) if os.path.isfile(os.path.join(folder_path + director, f))] + 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 'add_file' in request.form and ".." not in request.form['add_music']: - var.playlist.append((request.form['type'], request.form['add_music'])) - if 'add_folder' in request.form and ".." not in request.form['add_folder']: - dir_files = [("file", request.form['add_folder'] + '/' + i) for i in files[request.form['add_folder']]] - var.playlist.extend(dir_files) + print(request.form) + if 'add_file' in request.form and ".." not in request.form['add_file']: + item = ('file', request.form['add_file']) + var.playlist.append(item) + if ('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: + folder = request.form['add_folder'] + except: + folder = request.form['add_folder_recursively'] + + if not folder.endswith('/'): + folder += '/' + + print('folder:', folder) + if 'add_folder_recursively' in request.form: + files = music_library.get_files_recursively(folder) + else: + files = music_library.get_files(folder) + files = list(map(lambda file: ('file', folder + '/' + file), files)) + print('Adding to playlist: ', files) + var.playlist.extend(files) elif 'delete_music' in request.form: try: var.playlist.remove("file", request.form['delete_music']) @@ -87,7 +103,8 @@ def index(): current_music=current_music, user=var.user, playlist=var.playlist, - all_files=files) + all_files=files, + music_library=music_library) @web.route('/download', methods=["POST"]) diff --git a/mumbleBot.py b/mumbleBot.py index d2aeb53..fc86c35 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -18,23 +18,17 @@ import variables as var import hashlib import youtube_dl import media +import util class MumbleBot: - def __init__(self): + def __init__(self, args): signal.signal(signal.SIGINT, self.ctrl_caught) self.config = configparser.ConfigParser(interpolation=None) self.config.read("configuration.ini", encoding='latin-1') - parser = argparse.ArgumentParser(description='Bot for playing radio stream on Mumble') - parser.add_argument("-s", "--server", dest="host", type=str, required=True, help="The server's hostame of a mumble server") - parser.add_argument("-u", "--user", dest="user", type=str, required=True, help="Username you wish, Default=abot") - parser.add_argument("-P", "--password", dest="password", type=str, default="", help="Password if server requires one") - parser.add_argument("-p", "--port", dest="port", type=int, default=64738, help="Port for the mumble server") - parser.add_argument("-c", "--channel", dest="channel", type=str, default="", help="Default chanel for the bot") - args = parser.parse_args() self.volume = self.config.getfloat('bot', 'volume') self.channel = args.channel @@ -64,11 +58,11 @@ class MumbleBot: self.nb_exit = 0 self.thread = None - interface.init_proxy() - - # t = threading.Thread(target=start_web_interface) - # t.daemon = True - # t.start() + if args.wi_addr: + interface.init_proxy() + tt = threading.Thread(target=start_web_interface, args=(args.wi_addr, args.wi_port)) + tt.daemon = True + tt.start() self.mumble = pymumble.Mumble(args.host, user=args.user, port=args.port, password=args.password, debug=self.config.getboolean('debug', 'mumbleConnection')) @@ -114,16 +108,14 @@ class MumbleBot: path = os.path.abspath(os.path.join(music_folder, parameter)) if path.startswith(music_folder): if os.path.isfile(path): - #self.launch_play_file(path) filename = path.replace(music_folder, '') var.playlist.append(["file", filename]) else: # try to do a partial match - matches = [file for file in self.__get_recursive_filelist_sorted(music_folder) if parameter.lower() in file.lower()] + matches = [file for file in util.get_recursive_filelist_sorted(music_folder) if parameter.lower() in file.lower()] if len(matches) == 0: self.mumble.users[text.actor].send_message(self.config.get('strings', 'no_file')) elif len(matches) == 1: - #self.launch_play_file(music_folder + matches[0]) var.playlist.append(["file", matches[0]]) else: msg = self.config.get('strings', 'multiple_matches') + '
' @@ -183,7 +175,7 @@ class MumbleBot: elif command == self.config.get('command', 'list'): folder_path = self.config.get('bot', 'music_folder') - files = self.__get_recursive_filelist_sorted(folder_path) + files = util.get_recursive_filelist_sorted(folder_path) if files : self.mumble.users[text.actor].send_message('
'.join(files)) else : @@ -248,8 +240,8 @@ class MumbleBot: command = ["ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', path, '-ac', '1', '-f', 's16le', '-ar', '48000', '-'] self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480) - var.current_music.append(title) - var.current_music.append(path) + #var.current_music[2] = title + #var.current_music[3] = path def download_music(self, url): url_hash = hashlib.md5(url.encode()).hexdigest() @@ -316,26 +308,25 @@ class MumbleBot: channel = self.mumble.channels[self.mumble.users.myself['channel_id']] channel.send_text_message(msg) - def __get_recursive_filelist_sorted(self, path): - filelist = [] - for root, dirs, files in os.walk(path): - relroot = root.replace(path, '') - if relroot in self.config.get('bot', 'ignored_folders'): - continue - if len(relroot): - relroot += '/' - for file in files: - if file in self.config.get('bot', 'ignored_files'): - continue - filelist.append(relroot + file) - filelist.sort() - return filelist - - -def start_web_interface(): - interface.web.run(port=8181, host="127.0.0.1") +def start_web_interface(addr, port): + print('Starting web interface on {}:{}'.format(addr, port)) + interface.web.run(port=port, host=addr) if __name__ == '__main__': - botamusique = MumbleBot() + parser = argparse.ArgumentParser(description='Bot for playing radio stream on Mumble') + + # Mumble arguments + parser.add_argument("-s", "--server", dest="host", type=str, required=True, help="The server's hostame of a mumble server") + parser.add_argument("-u", "--user", dest="user", type=str, required=True, help="Username you wish, Default=abot") + parser.add_argument("-P", "--password", dest="password", type=str, default="", help="Password if server requires one") + parser.add_argument("-p", "--port", dest="port", type=int, default=64738, help="Port for the mumble server") + parser.add_argument("-c", "--channel", dest="channel", type=str, default="", help="Default chanel for the bot") + + # web interface arguments + parser.add_argument('--wi-port', dest='wi_port', type=int, default=8181, help='Listening port of the web interface') + parser.add_argument('--wi-addr', dest='wi_addr', type=str, default=None, help='Listening address of the web interface') + + args = parser.parse_args() + botamusique = MumbleBot(args) diff --git a/templates/index.html b/templates/index.html index 2550d46..e180217 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,3 +1,28 @@ +{% macro dirlisting(path='') -%} +
    + {% for subdirname in music_library.get_subdirs(path) %} + {%- set subdirpath = path + subdirname + '/' %} +
  • {{ subdirname }}/
    +
  • + {%- set subdirs = music_library.get_subdirs(subdirpath) %} + {%- if subdirs %} + {%- for subdir in subdirs %} + {{- dirlisting(subdirpath) -}} + {%- endfor %} + {%- endif %} +
      + {%- set files = music_library.get_files(subdirpath) %} + {%- if files %} + {% for file in files %} + +
    • + {% endfor %} + {%- endif %} +
    + {% endfor %} +
+{%- endmacro %} + @@ -38,19 +63,8 @@ {% endfor %} -
- {% for dir in all_files %} - {{ dir }} -
-
-
    - {% for m in all_files[dir] %} -
  • -
    -
  • - {% endfor %} -
- {% endfor %} +

Music library:

+ {{ dirlisting() }} @@ -61,4 +75,4 @@ - \ No newline at end of file + diff --git a/util.py b/util.py new file mode 100644 index 0000000..2fbcac1 --- /dev/null +++ b/util.py @@ -0,0 +1,87 @@ +#!/usr/bin/python3 + +import configparser +import os +import variables as var + +__CONFIG = configparser.ConfigParser(interpolation=None) +__CONFIG.read("configuration.ini", encoding='latin-1') + +def get_recursive_filelist_sorted(path): + filelist = [] + for root, dirs, files in os.walk(path): + relroot = root.replace(path, '') + if relroot != '' and relroot in __CONFIG.get('bot', 'ignored_folders'): + continue + if len(relroot): + relroot += '/' + for file in files: + if file in __CONFIG.get('bot', 'ignored_files'): + continue + filelist.append(relroot + file) + + filelist.sort() + return filelist + +class Dir(object): + def __init__(self, name): + self.name = name + self.subdirs = {} + self.files = [] + + def add_file(self, file): + if file.startswith(self.name + '/'): + file = file.replace(self.name + '/', '') + + if '/' in file: + # This file is in a subdir + subdir = file.split('/')[0] + if subdir in self.subdirs: + self.subdirs[subdir].add_file(file) + else: + self.subdirs[subdir] = Dir(subdir) + self.subdirs[subdir].add_file(file) + else: + self.files.append(file) + return True + + def get_subdirs(self, path=None): + if path and path != '': + subdir = path.split('/')[0] + if subdir in self.subdirs: + searchpath = '/'.join(path.split('/')[1::]) + return self.subdirs[subdir].get_subdirs(searchpath) + else: + return self.subdirs + + + def get_files(self, path=None): + if path and path != '': + subdir = path.split('/')[0] + if subdir in self.subdirs: + searchpath = '/'.join(path.split('/')[1::]) + return self.subdirs[subdir].get_files(searchpath) + else: + return self.files + + def get_files_recursively(self, path=None): + print('in get_files_recursively', path) + if path and path != '': + subdir = path.split('/')[0] + if subdir in self.subdirs: + searchpath = '/'.join(path.split('/')[1::]) + return self.subdirs[subdir].get_files_recursively(searchpath) + else: + files = self.files + + for key, val in self.subdirs.items(): + files.extend(map(lambda file: key + '/' + file,val.get_files_recursively())) + + return files + + def render_text(self, ident=0): + print('{}{}/'.format(' ' * (ident * 4), self.name)) + for key, val in self.subdirs.items(): + val.render_text(ident+1) + for file in self.files: + print('{}{}'.format(' ' * ((ident + 1)) * 4, file)) From db51c7c4c78e64a8fbb1dedb262bfea49ddd1813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sat, 19 May 2018 22:48:48 +0200 Subject: [PATCH 09/30] interface: Don't show download/upload section; Make tracks searchable --- templates/index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/index.html b/templates/index.html index e180217..e08bd4c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -15,7 +15,7 @@ {%- if files %} {% for file in files %} -
  • +
  •   {{ file }}
  • {% endfor %} {%- endif %} @@ -34,6 +34,7 @@
    Refresh

    +{% if False %}
    @@ -45,6 +46,7 @@
    +{% endif %}
    Current Playing : {% if current_music %} From 36027414d8da18698fdc938a0f34ffca2864820a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sun, 20 May 2018 13:25:51 +0200 Subject: [PATCH 10/30] Fix variable referencing bug in Dir() --- util.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/util.py b/util.py index 2fbcac1..85a4331 100644 --- a/util.py +++ b/util.py @@ -46,31 +46,36 @@ class Dir(object): return True def get_subdirs(self, path=None): + subdirs = [] if path and path != '': subdir = path.split('/')[0] if subdir in self.subdirs: searchpath = '/'.join(path.split('/')[1::]) - return self.subdirs[subdir].get_subdirs(searchpath) + subdirs = self.subdirs[subdir].get_subdirs(searchpath) else: - return self.subdirs + subdirs = self.subdirs + return subdirs def get_files(self, path=None): + files = [] if path and path != '': subdir = path.split('/')[0] if subdir in self.subdirs: searchpath = '/'.join(path.split('/')[1::]) - return self.subdirs[subdir].get_files(searchpath) + files = self.subdirs[subdir].get_files(searchpath) else: - return self.files + files = self.files + + return files def get_files_recursively(self, path=None): - print('in get_files_recursively', path) + files = [] if path and path != '': subdir = path.split('/')[0] if subdir in self.subdirs: searchpath = '/'.join(path.split('/')[1::]) - return self.subdirs[subdir].get_files_recursively(searchpath) + files = self.subdirs[subdir].get_files_recursively(searchpath) else: files = self.files From c770b013707b9145132ac38ac1cfd1d85d64b35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sun, 20 May 2018 13:37:51 +0200 Subject: [PATCH 11/30] Fix formatting issues in web interface --- templates/index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/index.html b/templates/index.html index e08bd4c..09009b2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,8 +2,8 @@
      {% for subdirname in music_library.get_subdirs(path) %} {%- set subdirpath = path + subdirname + '/' %} -
    • {{ subdirname }}/
      -
    • +
    • {{ subdirname }}/
      +
    • {%- set subdirs = music_library.get_subdirs(subdirpath) %} {%- if subdirs %} {%- for subdir in subdirs %} @@ -15,7 +15,7 @@ {%- if files %} {% for file in files %} -
    •   {{ file }}
    • +
    •   {{ file }}
    • {% endfor %} {%- endif %}
    @@ -56,12 +56,12 @@ {% endif %}
    Playlist : -
    +
      {% for m in playlist %}
    • {{ m }} -
      +
    • {% endfor %}
    From f3813777b7860092fd8326db09b88f4055068b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sun, 20 May 2018 16:05:41 +0200 Subject: [PATCH 12/30] Allow uploading files to existing/new folders --- interface.py | 47 ++++++++++++++++++++++++++++++++------------ templates/index.html | 24 ++++++++++++---------- util.py | 16 +++++++++++++++ 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/interface.py b/interface.py index 85878b2..76b9624 100644 --- a/interface.py +++ b/interface.py @@ -7,6 +7,7 @@ import os.path from os import listdir import random from werkzeug.utils import secure_filename +import errno class ReverseProxied(object): '''Wrap the application in this middleware and configure the @@ -107,23 +108,43 @@ def index(): music_library=music_library) -@web.route('/download', methods=["POST"]) -def download(): - print(request.form) - - file = request.files['music_file'] +@web.route('/upload', methods=["POST"]) +def upload(): + file = request.files['file'] if not file: return redirect("./", code=406) - elif file.filename == '': - return redirect("./", code=406) - elif '..' in request.form['directory']: + + filename = secure_filename(file.filename).strip() + if filename == '': return redirect("./", code=406) - if file.name == "music_file" and "audio" in file.headers.to_list()[1][1]: - web.config['UPLOAD_FOLDER'] = var.music_folder + request.form['directory'] - filename = secure_filename(file.filename) - print(filename) - file.save(os.path.join(web.config['UPLOAD_FOLDER'], 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)) + if not storagepath.startswith(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) + if os.path.exists(filepath): + return redirect("./", code=406) + + file.save(filepath) return redirect("./", code=302) else: return redirect("./", code=409) diff --git a/templates/index.html b/templates/index.html index 09009b2..72858fd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -34,19 +34,23 @@
    Refresh

    -{% if False %} -
    -
    - - + Upload into + + + {% for dir in music_library.get_subdirs_recursively() %} +
    -{% endif %} + +
    +
    Current Playing : {% if current_music %} @@ -54,7 +58,7 @@ {% else %} No music {% endif %} -
    +
    Playlist :
    diff --git a/util.py b/util.py index 85a4331..8ed6f37 100644 --- a/util.py +++ b/util.py @@ -57,6 +57,22 @@ class Dir(object): return subdirs + def get_subdirs_recursively(self, path=None): + subdirs = [] + if path and path != '': + subdir = path.split('/')[0] + if subdir in self.subdirs: + searchpath = '/'.join(path.split('/')[1::]) + subdirs = self.subdirs[subdir].get_subdirs_recursively(searchpath) + else: + subdirs = list(self.subdirs.keys()) + + for key, val in self.subdirs.items(): + subdirs.extend(map(lambda subdir: key + '/' + subdir,val.get_subdirs_recursively())) + + subdirs.sort() + return subdirs + def get_files(self, path=None): files = [] if path and path != '': From de96175c18990b21bd5365200cbbbe5100d1b7b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sun, 20 May 2018 17:02:41 +0200 Subject: [PATCH 13/30] Add download button for each file - This commit also introduces some CSS to place the buttons/forms next to each other --- static/index.css | 23 +++++++++++++++++++++++ templates/index.html | 13 +++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/static/index.css b/static/index.css index e69de29..77ab01a 100644 --- a/static/index.css +++ b/static/index.css @@ -0,0 +1,23 @@ +/* necessary to place both forms/buttons next to each other */ +li.file { + clear: both; + list-style-position: outside; +} + +/* necessary to place both forms/buttons next to each other */ +form.file { + /* Float both forms to the left */ + float: left; +} + +form.file.file_add { + margin-left: 5px; + margin-right: 5px; +} + +/* necessary to place both forms/buttons next to each other */ +form.file.file_download { + clear: right; + /* with some space to the left of the second form */ + margin-right: 20px; +} diff --git a/templates/index.html b/templates/index.html index 72858fd..27712ac 100644 --- a/templates/index.html +++ b/templates/index.html @@ -14,8 +14,17 @@ {%- set files = music_library.get_files(subdirpath) %} {%- if files %} {% for file in files %} - -
  •   {{ file }}
  • +
  • +
    + + +
    +
    + + +  {{ file }} +
    +
  • {% endfor %} {%- endif %} From 74ae51f65cd3f12c4c82fa1b771288be0a3b6cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sun, 20 May 2018 20:32:05 +0200 Subject: [PATCH 14/30] Add download buttons to subdirectory entries --- static/index.css | 30 ++++++++++++++++++++++++++++++ templates/index.html | 17 +++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/static/index.css b/static/index.css index 77ab01a..011d00c 100644 --- a/static/index.css +++ b/static/index.css @@ -21,3 +21,33 @@ form.file.file_download { /* with some space to the left of the second form */ margin-right: 20px; } + +/* necessary to place all forms/buttons of the directory entries next to each other */ +li.directory { + clear: both; + list-style-position: outside; + margin-top: 15px; + margin-bottom: 15px; +} + +li.directory span { + float: left; +} + +form.directory { + float: left; +} + +form.directory.form1 { + margin-left: 5px; + margin-right: 5px; +} + +form.directory.form2 { + margin-right: 5px; +} + +form.directory.form3 { + clear: right; + margin-right: 5px; +} diff --git a/templates/index.html b/templates/index.html index 27712ac..149c3e0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,8 +2,21 @@
      {% for subdirname in music_library.get_subdirs(path) %} {%- set subdirpath = path + subdirname + '/' %} -
    • {{ subdirname }}/
      -
    • +
    • + {{ subdirname }}/  +
      + + +
      +
      + + +
      +
      + + +
      +
    • {%- set subdirs = music_library.get_subdirs(subdirpath) %} {%- if subdirs %} {%- for subdir in subdirs %} From b0137b2db667b82c81ffcd10b7543b741bbe20bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sun, 20 May 2018 22:41:35 +0200 Subject: [PATCH 15/30] Implement download functionality - directories are served as a zip-file --- interface.py | 32 +++++++++++++++++++++++++++++++- templates/index.html | 4 ++-- util.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/interface.py b/interface.py index 76b9624..f79a083 100644 --- a/interface.py +++ b/interface.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -from flask import Flask, render_template, request, redirect +from flask import Flask, render_template, request, redirect, send_file import variables as var import util import os.path @@ -149,6 +149,36 @@ def upload(): else: return redirect("./", code=409) +@web.route('/download', methods=["GET"]) +def download(): + if 'file' in request.args: + requested_file = request.args['file'] + if '../' not in requested_file: + folder_path = var.music_folder + files = util.get_recursive_filelist_sorted(var.music_folder) + + if requested_file in files: + filepath = os.path.join(folder_path, requested_file) + try: + return send_file(filepath, as_attachment=True) + except Exception as e: + self.log.exception(e) + self.Error(400) + elif 'directory' in request.args: + requested_dir = request.args['directory'] + folder_path = var.music_folder + requested_dir_fullpath = os.path.abspath(os.path.join(folder_path, requested_dir)) + '/' + if requested_dir_fullpath.startswith(folder_path): + prefix = secure_filename(os.path.relpath(requested_dir_fullpath, folder_path)) + zipfile = util.zipdir(requested_dir_fullpath, requested_dir) + try: + return send_file(zipfile, as_attachment=True) + except Exception as e: + self.log.exception(e) + self.Error(400) + + return redirect("./", code=400) + if __name__ == '__main__': web.run(port=8181, host="127.0.0.1") diff --git a/templates/index.html b/templates/index.html index 149c3e0..75b9b0e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -29,11 +29,11 @@ {% for file in files %}
    • - +
      - +  {{ file }}
      diff --git a/util.py b/util.py index 8ed6f37..e2868c6 100644 --- a/util.py +++ b/util.py @@ -1,8 +1,10 @@ #!/usr/bin/python3 import configparser +import hashlib import os import variables as var +import zipfile __CONFIG = configparser.ConfigParser(interpolation=None) __CONFIG.read("configuration.ini", encoding='latin-1') @@ -23,6 +25,36 @@ def get_recursive_filelist_sorted(path): filelist.sort() return filelist +# - zips all files of the given zippath (must be a directory) +# - returns the absolute path of the created zip file +# - zip file will be in the applications tmp folder (according to configuration) +# - format of the filename itself = prefix_hash.zip +# - prefix can be controlled by the caller +# - hash is a sha1 of the string representation of the directories' contents (which are +# zipped) +def zipdir(zippath, zipname_prefix=None): + zipname = __CONFIG.get('bot', 'tmp_folder') + if zipname_prefix and '../' not in zipname_prefix: + zipname += zipname_prefix.strip().replace('/', '_') + '_' + + files = get_recursive_filelist_sorted(zippath) + hash = hashlib.sha1((str(files).encode())).hexdigest() + zipname += hash + '.zip' + + if os.path.exists(zipname): + return zipname + + zipf = zipfile.ZipFile(zipname, 'w', zipfile.ZIP_DEFLATED) + + for file in files: + filepath = os.path.dirname(file) + file_to_add = os.path.join(zippath, file) + add_file_as = os.path.relpath(os.path.join(zippath, file), os.path.join(zippath, '..')) + zipf.write(file_to_add, add_file_as) + + zipf.close() + return zipname + class Dir(object): def __init__(self, name): self.name = name From 1e80ab2c0c2268602dcbdedc21c0198914941b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sun, 20 May 2018 22:52:08 +0200 Subject: [PATCH 16/30] Add button to download entire music library --- interface.py | 7 +++++-- templates/index.html | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/interface.py b/interface.py index f79a083..28ae96a 100644 --- a/interface.py +++ b/interface.py @@ -169,8 +169,11 @@ def download(): folder_path = var.music_folder requested_dir_fullpath = os.path.abspath(os.path.join(folder_path, requested_dir)) + '/' if requested_dir_fullpath.startswith(folder_path): - prefix = secure_filename(os.path.relpath(requested_dir_fullpath, folder_path)) - zipfile = util.zipdir(requested_dir_fullpath, requested_dir) + if os.path.samefile(requested_dir_fullpath, folder_path): + prefix = 'all' + else: + prefix = secure_filename(os.path.relpath(requested_dir_fullpath, folder_path)) + zipfile = util.zipdir(requested_dir_fullpath, prefix) try: return send_file(zipfile, as_attachment=True) except Exception as e: diff --git a/templates/index.html b/templates/index.html index 75b9b0e..87e1b3d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -92,6 +92,11 @@ {% endfor %}

    Music library:

    +
    + + +
    +
    {{ dirlisting() }} From 983d3b5a110790ae121eb55358e56773ea138235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20W=C3=BCrfl?= Date: Sun, 20 May 2018 23:03:08 +0200 Subject: [PATCH 17/30] Small cleanup changes --- interface.py | 10 +++++----- templates/index.html | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/interface.py b/interface.py index 28ae96a..7ce7fc1 100644 --- a/interface.py +++ b/interface.py @@ -121,13 +121,13 @@ def upload(): targetdir = request.form['targetdir'].strip() if targetdir == '': targetdir = 'uploads/' - elif '..' in targetdir: + elif '../' in targetdir: return redirect("./", code=406) - print('Uploading file:') - print('filename:', filename) - print('targetdir:', targetdir) - print('mimetype:', file.mimetype) + #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)) diff --git a/templates/index.html b/templates/index.html index 87e1b3d..1a68822 100644 --- a/templates/index.html +++ b/templates/index.html @@ -61,8 +61,9 @@
    Upload into - + +