]> git.0d.be Git - botaradio.git/blob - interface.py
fix: typo in web interface
[botaradio.git] / interface.py
1 #!/usr/bin/python3
2
3 from functools import wraps
4 from flask import Flask, render_template, request, redirect, send_file, Response, jsonify, abort
5 import variables as var
6 import util
7 import os
8 import os.path
9 import shutil
10 from werkzeug.utils import secure_filename
11 import errno
12 import media
13 from media.playlist import get_item_wrapper, get_item_wrapper_by_id, get_item_wrappers_by_tags
14 import logging
15 import time
16
17
18 class ReverseProxied(object):
19     '''Wrap the application in this middleware and configure the
20     front-end server to add these headers, to let you quietly bind
21     this to a URL other than / and to an HTTP scheme that is
22     different than what is used locally.
23
24     In nginx:
25     location /myprefix {
26         proxy_pass http://192.168.0.1:5001;
27         proxy_set_header Host $host;
28         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
29         proxy_set_header X-Scheme $scheme;
30         proxy_set_header X-Script-Name /myprefix;
31         }
32
33     :param app: the WSGI application
34     '''
35
36     def __init__(self, app):
37         self.app = app
38
39     def __call__(self, environ, start_response):
40         script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
41         if script_name:
42             environ['SCRIPT_NAME'] = script_name
43             path_info = environ['PATH_INFO']
44             if path_info.startswith(script_name):
45                 environ['PATH_INFO'] = path_info[len(script_name):]
46
47         scheme = environ.get('HTTP_X_SCHEME', '')
48         if scheme:
49             environ['wsgi.url_scheme'] = scheme
50         real_ip = environ.get('HTTP_X_REAL_IP', '')
51         if real_ip:
52             environ['REMOTE_ADDR'] = real_ip
53         return self.app(environ, start_response)
54
55
56 web = Flask(__name__)
57 log = logging.getLogger("bot")
58 user = 'Remote Control'
59
60 def init_proxy():
61     global web
62     if var.is_proxified:
63         web.wsgi_app = ReverseProxied(web.wsgi_app)
64
65 # https://stackoverflow.com/questions/29725217/password-protect-one-webpage-in-flask-app
66
67 def check_auth(username, password):
68     """This function is called to check if a username /
69     password combination is valid.
70     """
71     return username == var.config.get("webinterface", "user") and password == var.config.get("webinterface", "password")
72
73 def authenticate():
74     """Sends a 401 response that enables basic auth"""
75     global log
76     return Response(
77     'Could not verify your access level for that URL.\n'
78     'You have to login with proper credentials', 401,
79     {'WWW-Authenticate': 'Basic realm="Login Required"'})
80
81 def requires_auth(f):
82     @wraps(f)
83     def decorated(*args, **kwargs):
84         global log
85         auth = request.authorization
86         if var.config.getboolean("webinterface", "require_auth") and (not auth or not check_auth(auth.username, auth.password)):
87             if auth:
88                 log.info("web: Failed login attempt, user: %s" % auth.username)
89             return authenticate()
90         return f(*args, **kwargs)
91     return decorated
92
93 def tag_color(tag):
94     num = hash(tag) % 8
95     if num == 0:
96         return "primary"
97     elif num == 1:
98         return "secondary"
99     elif num == 2:
100         return "success"
101     elif num == 3:
102         return "danger"
103     elif num == 4:
104         return "warning"
105     elif num == 5:
106         return "info"
107     elif num == 6:
108         return "light"
109     elif num == 7:
110         return "dark"
111
112 def build_tags_color_lookup():
113     color_lookup = {}
114     for tag in var.music_db.query_all_tags():
115         color_lookup[tag] = tag_color(tag)
116
117
118     return color_lookup
119
120 def build_path_tags_lookup():
121     path_lookup = {}
122     items = var.cache.file_id_lookup.items()
123     for path, id in items:
124         path_lookup[path] = var.music_db.query_tags_by_id(id)
125
126     return path_lookup
127
128 def recur_dir(dirobj):
129     for name, dir in dirobj.get_subdirs().items():
130         print(dirobj.fullpath + "/" + name)
131         recur_dir(dir)
132
133 @web.route("/", methods=['GET'])
134 @requires_auth
135 def index():
136     while var.cache.dir_lock.locked():
137         time.sleep(0.1)
138
139     tags_color_lookup = build_tags_color_lookup()
140     path_tags_lookup = build_path_tags_lookup()
141
142     return render_template('index.html',
143                            all_files=var.cache.files,
144                            tags_lookup=path_tags_lookup,
145                            tags_color_lookup=tags_color_lookup,
146                            music_library=var.cache.dir,
147                            os=os,
148                            playlist=var.playlist,
149                            user=var.user,
150                            paused=var.bot.is_pause,
151                            )
152
153 @web.route("/playlist", methods=['GET'])
154 @requires_auth
155 def playlist():
156     if len(var.playlist) == 0:
157         return jsonify({'items': [render_template('playlist.html',
158                                m=False,
159                                index=-1
160                                )]
161                         })
162
163     tags_color_lookup = build_tags_color_lookup()
164     items = []
165
166     for index, item_wrapper in enumerate(var.playlist):
167          items.append(render_template('playlist.html',
168                                      index=index,
169                                      tags_color_lookup=tags_color_lookup,
170                                      m=item_wrapper.item(),
171                                      playlist=var.playlist
172                                      )
173                      )
174
175     return jsonify({ 'items': items })
176
177 def status():
178     if len(var.playlist) > 0:
179         return jsonify({'ver': var.playlist.version,
180                         'empty': False,
181                         'play': not var.bot.is_pause,
182                         'mode': var.playlist.mode})
183     else:
184         return jsonify({'ver': var.playlist.version,
185                         'empty': True,
186                         'play': False,
187                         'mode': var.playlist.mode})
188
189
190 @web.route("/post", methods=['POST'])
191 @requires_auth
192 def post():
193     global log
194
195     if request.method == 'POST':
196         if request.form:
197             log.debug("web: Post request from %s: %s" % ( request.remote_addr, str(request.form)))
198         if 'add_file_bottom' in request.form and ".." not in request.form['add_file_bottom']:
199             path = var.music_folder + request.form['add_file_bottom']
200             if os.path.isfile(path):
201                 music_wrapper = get_item_wrapper_by_id(var.bot, var.cache.file_id_lookup[request.form['add_file_bottom']], user)
202
203                 var.playlist.append(music_wrapper)
204                 log.info('web: add to playlist(bottom): ' + music_wrapper.format_debug_string())
205
206         elif 'add_file_next' in request.form and ".." not in request.form['add_file_next']:
207             path = var.music_folder + request.form['add_file_next']
208             if os.path.isfile(path):
209                 music_wrapper = get_item_wrapper_by_id(var.bot, var.cache.file_id_lookup[request.form['add_file_next']], user)
210                 var.playlist.insert(var.playlist.current_index + 1, music_wrapper)
211                 log.info('web: add to playlist(next): ' + music_wrapper.format_debug_string())
212
213         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']):
214             try:
215                 folder = request.form['add_folder']
216             except:
217                 folder = request.form['add_folder_recursively']
218
219             if not folder.endswith('/'):
220                 folder += '/'
221
222             if os.path.isdir(var.music_folder + folder):
223                 dir = var.cache.dir
224                 if 'add_folder_recursively' in request.form:
225                     files = dir.get_files_recursively(folder)
226                 else:
227                     files = dir.get_files(folder)
228
229                 music_wrappers = list(map(
230                     lambda file:
231                     get_item_wrapper_by_id(var.bot, var.cache.file_id_lookup[folder + file], user),
232                 files))
233
234                 var.playlist.extend(music_wrappers)
235
236                 for music_wrapper in music_wrappers:
237                     log.info('web: add to playlist: ' + music_wrapper.format_debug_string())
238
239
240         elif 'add_url' in request.form:
241             music_wrapper = get_item_wrapper(var.bot, type='url', url=request.form['add_url'], user=user)
242             var.playlist.append(music_wrapper)
243
244             log.info("web: add to playlist: " + music_wrapper.format_debug_string())
245             if len(var.playlist) == 2:
246                 # If I am the second item on the playlist. (I am the next one!)
247                 var.bot.async_download_next()
248
249         elif 'add_radio' in request.form:
250             url = request.form['add_radio']
251             music_wrapper = get_item_wrapper(var.bot, type='radio', url=url, user=user)
252             var.playlist.append(music_wrapper)
253
254             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
255
256         elif 'delete_music' in request.form:
257             music_wrapper = var.playlist[int(request.form['delete_music'])]
258             log.info("web: delete from playlist: " + music_wrapper.format_debug_string())
259
260             if len(var.playlist) >= int(request.form['delete_music']):
261                 index = int(request.form['delete_music'])
262
263                 if index == var.playlist.current_index:
264                     var.playlist.remove(index)
265
266                     if index < len(var.playlist):
267                         if not var.bot.is_pause:
268                             var.bot.interrupt()
269                             var.playlist.current_index -= 1
270                             # then the bot will move to next item
271
272                     else:  # if item deleted is the last item of the queue
273                         var.playlist.current_index -= 1
274                         if not var.bot.is_pause:
275                             var.bot.interrupt()
276                 else:
277                     var.playlist.remove(index)
278
279
280         elif 'play_music' in request.form:
281             music_wrapper = var.playlist[int(request.form['play_music'])]
282             log.info("web: jump to: " + music_wrapper.format_debug_string())
283
284             if len(var.playlist) >= int(request.form['play_music']):
285                 var.playlist.point_to(int(request.form['play_music']) - 1)
286                 var.bot.interrupt()
287                 time.sleep(0.1)
288
289         elif 'delete_music_file' in request.form and ".." not in request.form['delete_music_file']:
290             path = var.music_folder + request.form['delete_music_file']
291             if os.path.isfile(path):
292                 log.info("web: delete file " + path)
293                 os.remove(path)
294
295         elif 'delete_folder' in request.form and ".." not in request.form['delete_folder']:
296             path = var.music_folder + request.form['delete_folder']
297             if os.path.isdir(path):
298                 log.info("web: delete folder " + path)
299                 shutil.rmtree(path)
300                 time.sleep(0.1)
301
302         elif 'add_tag' in request.form:
303             music_wrappers = get_item_wrappers_by_tags(var.bot, [request.form['add_tag']], user)
304             for music_wrapper in music_wrappers:
305                 log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
306             var.playlist.extend(music_wrappers)
307
308         elif 'action' in request.form:
309             action = request.form['action']
310             if action == "randomize":
311                 if var.playlist.mode != "random":
312                     var.playlist = media.playlist.get_playlist("random", var.playlist)
313                 else:
314                     var.playlist.randomize()
315                 var.bot.interrupt()
316                 var.db.set('playlist', 'playback_mode', "random")
317                 log.info("web: playback mode changed to random.")
318             if action == "one-shot":
319                 var.playlist = media.playlist.get_playlist("one-shot", var.playlist)
320                 var.db.set('playlist', 'playback_mode', "one-shot")
321                 log.info("web: playback mode changed to one-shot.")
322             if action == "repeat":
323                 var.playlist = media.playlist.get_playlist("repeat", var.playlist)
324                 var.db.set('playlist', 'playback_mode', "repeat")
325                 log.info("web: playback mode changed to repeat.")
326             if action == "autoplay":
327                 var.playlist = media.playlist.get_playlist("autoplay", var.playlist)
328                 var.db.set('playlist', 'playback_mode', "autoplay")
329                 log.info("web: playback mode changed to autoplay.")
330             if action == "rescan":
331                 var.cache.build_dir_cache(var.bot)
332                 log.info("web: Local file cache refreshed.")
333             elif action == "stop":
334                 var.bot.stop()
335             elif action == "pause":
336                 var.bot.pause()
337             elif action == "resume":
338                 var.bot.resume()
339             elif action == "clear":
340                 var.bot.clear()
341             elif action == "volume_up":
342                 if var.bot.volume_set + 0.03 < 1.0:
343                     var.bot.volume_set = var.bot.volume_set + 0.03
344                 else:
345                     var.bot.volume_set = 1.0
346                 var.db.set('bot', 'volume', str(var.bot.volume_set))
347                 log.info("web: volume up to %d" % (var.bot.volume_set * 100))
348             elif action == "volume_down":
349                 if var.bot.volume_set - 0.03 > 0:
350                     var.bot.volume_set = var.bot.volume_set - 0.03
351                 else:
352                     var.bot.volume_set = 0
353                 var.db.set('bot', 'volume', str(var.bot.volume_set))
354                 log.info("web: volume up to %d" % (var.bot.volume_set * 100))
355
356     return status()
357
358 @web.route('/upload', methods=["POST"])
359 def upload():
360     global log
361
362     files = request.files.getlist("file[]")
363     if not files:
364         return redirect("./", code=406)
365
366     #filename = secure_filename(file.filename).strip()
367     for file in files:
368         filename = file.filename
369         if filename == '':
370             return redirect("./", code=406)
371
372         targetdir = request.form['targetdir'].strip()
373         if targetdir == '':
374             targetdir = 'uploads/'
375         elif '../' in targetdir:
376             return redirect("./", code=406)
377
378         log.info('web: Uploading file from %s:' % request.remote_addr)
379         log.info('web: - filename: ' + filename)
380         log.info('web: - targetdir: ' + targetdir)
381         log.info('web:  - mimetype: ' + file.mimetype)
382
383         if "audio" in file.mimetype:
384             storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir))
385             print('storagepath:',storagepath)
386             if not storagepath.startswith(os.path.abspath(var.music_folder)):
387                 return redirect("./", code=406)
388
389             try:
390                 os.makedirs(storagepath)
391             except OSError as ee:
392                 if ee.errno != errno.EEXIST:
393                     return redirect("./", code=500)
394
395             filepath = os.path.join(storagepath, filename)
396             log.info(' - filepath: ' + filepath)
397             if os.path.exists(filepath):
398                 return redirect("./", code=406)
399
400             file.save(filepath)
401         else:
402             return redirect("./", code=409)
403
404     return redirect("./", code=302)
405
406
407 @web.route('/download', methods=["GET"])
408 def download():
409     global log
410
411     if 'file' in request.args:
412         requested_file = request.args['file']
413         log.info('web: Download of file %s requested from %s:' % (requested_file, request.remote_addr))
414         if '../' not in requested_file:
415             folder_path = var.music_folder
416             files = var.cache.files
417
418             if requested_file in files:
419                 filepath = os.path.join(folder_path, requested_file)
420                 try:
421                     return send_file(filepath, as_attachment=True)
422                 except Exception as e:
423                     log.exception(e)
424                     abort(404)
425     elif 'directory' in request.args:
426         requested_dir = request.args['directory']
427         folder_path = var.music_folder
428         requested_dir_fullpath = os.path.abspath(os.path.join(folder_path, requested_dir)) + '/'
429         if requested_dir_fullpath.startswith(folder_path):
430             if os.path.samefile(requested_dir_fullpath, folder_path):
431                 prefix = 'all'
432             else:
433                 prefix = secure_filename(os.path.relpath(requested_dir_fullpath, folder_path))
434             zipfile = util.zipdir(requested_dir_fullpath, prefix)
435             try:
436                 return send_file(zipfile, as_attachment=True)
437             except Exception as e:
438                 log.exception(e)
439                 abort(404)
440
441     return redirect("./", code=400)
442
443
444 if __name__ == '__main__':
445     web.run(port=8181, host="127.0.0.1")