]> git.0d.be Git - botaradio.git/blob - interface.py
c2257dcd0a607658f1808d8256350d2ee50d291a
[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_from_scrap, 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     id_tags_lookup = var.music_db.query_tags_by_ids(list(var.cache.file_id_lookup.values()))
122
123     path_tags_lookup = {}
124     for path, id in var.cache.file_id_lookup.items():
125         path_tags_lookup[path] = id_tags_lookup[id]
126
127     return path_tags_lookup
128
129 def recur_dir(dirobj):
130     for name, dir in dirobj.get_subdirs().items():
131         print(dirobj.fullpath + "/" + name)
132         recur_dir(dir)
133
134 @web.route("/", methods=['GET'])
135 @requires_auth
136 def index():
137     while var.cache.dir_lock.locked():
138         time.sleep(0.1)
139
140     tags_color_lookup = build_tags_color_lookup()
141     path_tags_lookup = build_path_tags_lookup()
142
143     return render_template('index.html',
144                            all_files=var.cache.files,
145                            tags_lookup=path_tags_lookup,
146                            tags_color_lookup=tags_color_lookup,
147                            music_library=var.cache.dir,
148                            os=os,
149                            playlist=var.playlist,
150                            user=var.user,
151                            paused=var.bot.is_pause,
152                            )
153
154 @web.route("/playlist", methods=['GET'])
155 @requires_auth
156 def playlist():
157     if len(var.playlist) == 0:
158         return jsonify({'items': [render_template('playlist.html',
159                                m=False,
160                                index=-1
161                                )]
162                         })
163
164     tags_color_lookup = build_tags_color_lookup()
165     items = []
166
167     for index, item_wrapper in enumerate(var.playlist):
168          items.append(render_template('playlist.html',
169                                      index=index,
170                                      tags_color_lookup=tags_color_lookup,
171                                      m=item_wrapper.item(),
172                                      playlist=var.playlist
173                                      )
174                      )
175
176     return jsonify({ 'items': items })
177
178 def status():
179     if len(var.playlist) > 0:
180         return jsonify({'ver': var.playlist.version,
181                         'empty': False,
182                         'play': not var.bot.is_pause,
183                         'mode': var.playlist.mode})
184     else:
185         return jsonify({'ver': var.playlist.version,
186                         'empty': True,
187                         'play': False,
188                         'mode': var.playlist.mode})
189
190
191 @web.route("/post", methods=['POST'])
192 @requires_auth
193 def post():
194     global log
195
196     if request.method == 'POST':
197         if request.form:
198             log.debug("web: Post request from %s: %s" % ( request.remote_addr, str(request.form)))
199         if 'add_file_bottom' in request.form and ".." not in request.form['add_file_bottom']:
200             path = var.music_folder + request.form['add_file_bottom']
201             if os.path.isfile(path):
202                 music_wrapper = get_item_wrapper_by_id(var.bot, var.cache.file_id_lookup[request.form['add_file_bottom']], user)
203
204                 var.playlist.append(music_wrapper)
205                 log.info('web: add to playlist(bottom): ' + music_wrapper.format_debug_string())
206
207         elif 'add_file_next' in request.form and ".." not in request.form['add_file_next']:
208             path = var.music_folder + request.form['add_file_next']
209             if os.path.isfile(path):
210                 music_wrapper = get_item_wrapper_by_id(var.bot, var.cache.file_id_lookup[request.form['add_file_next']], user)
211                 var.playlist.insert(var.playlist.current_index + 1, music_wrapper)
212                 log.info('web: add to playlist(next): ' + music_wrapper.format_debug_string())
213
214         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']):
215             try:
216                 folder = request.form['add_folder']
217             except:
218                 folder = request.form['add_folder_recursively']
219
220             if not folder.endswith('/'):
221                 folder += '/'
222
223             if os.path.isdir(var.music_folder + folder):
224                 dir = var.cache.dir
225                 if 'add_folder_recursively' in request.form:
226                     files = dir.get_files_recursively(folder)
227                 else:
228                     files = dir.get_files(folder)
229
230                 music_wrappers = list(map(
231                     lambda file:
232                     get_item_wrapper_by_id(var.bot, var.cache.file_id_lookup[folder + file], user),
233                 files))
234
235                 var.playlist.extend(music_wrappers)
236
237                 for music_wrapper in music_wrappers:
238                     log.info('web: add to playlist: ' + music_wrapper.format_debug_string())
239
240
241         elif 'add_url' in request.form:
242             music_wrapper = get_item_wrapper_from_scrap(var.bot, type='url', url=request.form['add_url'], user=user)
243             var.playlist.append(music_wrapper)
244
245             log.info("web: add to playlist: " + music_wrapper.format_debug_string())
246             if len(var.playlist) == 2:
247                 # If I am the second item on the playlist. (I am the next one!)
248                 var.bot.async_download_next()
249
250         elif 'add_radio' in request.form:
251             url = request.form['add_radio']
252             music_wrapper = get_item_wrapper_from_scrap(var.bot, type='radio', url=url, user=user)
253             var.playlist.append(music_wrapper)
254
255             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
256
257         elif 'delete_music' in request.form:
258             music_wrapper = var.playlist[int(request.form['delete_music'])]
259             log.info("web: delete from playlist: " + music_wrapper.format_debug_string())
260
261             if len(var.playlist) >= int(request.form['delete_music']):
262                 index = int(request.form['delete_music'])
263
264                 if index == var.playlist.current_index:
265                     var.playlist.remove(index)
266
267                     if index < len(var.playlist):
268                         if not var.bot.is_pause:
269                             var.bot.interrupt()
270                             var.playlist.current_index -= 1
271                             # then the bot will move to next item
272
273                     else:  # if item deleted is the last item of the queue
274                         var.playlist.current_index -= 1
275                         if not var.bot.is_pause:
276                             var.bot.interrupt()
277                 else:
278                     var.playlist.remove(index)
279
280
281         elif 'play_music' in request.form:
282             music_wrapper = var.playlist[int(request.form['play_music'])]
283             log.info("web: jump to: " + music_wrapper.format_debug_string())
284
285             if len(var.playlist) >= int(request.form['play_music']):
286                 var.playlist.point_to(int(request.form['play_music']) - 1)
287                 var.bot.interrupt()
288                 time.sleep(0.1)
289
290         elif 'delete_music_file' in request.form and ".." not in request.form['delete_music_file']:
291             path = var.music_folder + request.form['delete_music_file']
292             if os.path.isfile(path):
293                 log.info("web: delete file " + path)
294                 os.remove(path)
295
296         elif 'delete_folder' in request.form and ".." not in request.form['delete_folder']:
297             path = var.music_folder + request.form['delete_folder']
298             if os.path.isdir(path):
299                 log.info("web: delete folder " + path)
300                 shutil.rmtree(path)
301                 time.sleep(0.1)
302
303         elif 'add_tag' in request.form:
304             music_wrappers = get_item_wrappers_by_tags(var.bot, [request.form['add_tag']], user)
305             for music_wrapper in music_wrappers:
306                 log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
307             var.playlist.extend(music_wrappers)
308
309         elif 'action' in request.form:
310             action = request.form['action']
311             if action == "randomize":
312                 if var.playlist.mode != "random":
313                     var.playlist = media.playlist.get_playlist("random", var.playlist)
314                 else:
315                     var.playlist.randomize()
316                 var.bot.interrupt()
317                 var.db.set('playlist', 'playback_mode', "random")
318                 log.info("web: playback mode changed to random.")
319             if action == "one-shot":
320                 var.playlist = media.playlist.get_playlist("one-shot", var.playlist)
321                 var.db.set('playlist', 'playback_mode', "one-shot")
322                 log.info("web: playback mode changed to one-shot.")
323             if action == "repeat":
324                 var.playlist = media.playlist.get_playlist("repeat", var.playlist)
325                 var.db.set('playlist', 'playback_mode', "repeat")
326                 log.info("web: playback mode changed to repeat.")
327             if action == "autoplay":
328                 var.playlist = media.playlist.get_playlist("autoplay", var.playlist)
329                 var.db.set('playlist', 'playback_mode', "autoplay")
330                 log.info("web: playback mode changed to autoplay.")
331             if action == "rescan":
332                 var.cache.build_dir_cache(var.bot)
333                 log.info("web: Local file cache refreshed.")
334             elif action == "stop":
335                 var.bot.stop()
336             elif action == "pause":
337                 var.bot.pause()
338             elif action == "resume":
339                 var.bot.resume()
340             elif action == "clear":
341                 var.bot.clear()
342             elif action == "volume_up":
343                 if var.bot.volume_set + 0.03 < 1.0:
344                     var.bot.volume_set = var.bot.volume_set + 0.03
345                 else:
346                     var.bot.volume_set = 1.0
347                 var.db.set('bot', 'volume', str(var.bot.volume_set))
348                 log.info("web: volume up to %d" % (var.bot.volume_set * 100))
349             elif action == "volume_down":
350                 if var.bot.volume_set - 0.03 > 0:
351                     var.bot.volume_set = var.bot.volume_set - 0.03
352                 else:
353                     var.bot.volume_set = 0
354                 var.db.set('bot', 'volume', str(var.bot.volume_set))
355                 log.info("web: volume up to %d" % (var.bot.volume_set * 100))
356
357     return status()
358
359 @web.route('/upload', methods=["POST"])
360 def upload():
361     global log
362
363     files = request.files.getlist("file[]")
364     if not files:
365         return redirect("./", code=406)
366
367     #filename = secure_filename(file.filename).strip()
368     for file in files:
369         filename = file.filename
370         if filename == '':
371             return redirect("./", code=406)
372
373         targetdir = request.form['targetdir'].strip()
374         if targetdir == '':
375             targetdir = 'uploads/'
376         elif '../' in targetdir:
377             return redirect("./", code=406)
378
379         log.info('web: Uploading file from %s:' % request.remote_addr)
380         log.info('web: - filename: ' + filename)
381         log.info('web: - targetdir: ' + targetdir)
382         log.info('web:  - mimetype: ' + file.mimetype)
383
384         if "audio" in file.mimetype:
385             storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir))
386             print('storagepath:',storagepath)
387             if not storagepath.startswith(os.path.abspath(var.music_folder)):
388                 return redirect("./", code=406)
389
390             try:
391                 os.makedirs(storagepath)
392             except OSError as ee:
393                 if ee.errno != errno.EEXIST:
394                     return redirect("./", code=500)
395
396             filepath = os.path.join(storagepath, filename)
397             log.info(' - filepath: ' + filepath)
398             if os.path.exists(filepath):
399                 return redirect("./", code=406)
400
401             file.save(filepath)
402         else:
403             return redirect("./", code=409)
404
405     return redirect("./", code=302)
406
407
408 @web.route('/download', methods=["GET"])
409 def download():
410     global log
411
412     if 'file' in request.args:
413         requested_file = request.args['file']
414         log.info('web: Download of file %s requested from %s:' % (requested_file, request.remote_addr))
415         if '../' not in requested_file:
416             folder_path = var.music_folder
417             files = var.cache.files
418
419             if requested_file in files:
420                 filepath = os.path.join(folder_path, requested_file)
421                 try:
422                     return send_file(filepath, as_attachment=True)
423                 except Exception as e:
424                     log.exception(e)
425                     abort(404)
426     elif 'directory' in request.args:
427         requested_dir = request.args['directory']
428         folder_path = var.music_folder
429         requested_dir_fullpath = os.path.abspath(os.path.join(folder_path, requested_dir)) + '/'
430         if requested_dir_fullpath.startswith(folder_path):
431             if os.path.samefile(requested_dir_fullpath, folder_path):
432                 prefix = 'all'
433             else:
434                 prefix = secure_filename(os.path.relpath(requested_dir_fullpath, folder_path))
435             zipfile = util.zipdir(requested_dir_fullpath, prefix)
436             try:
437                 return send_file(zipfile, as_attachment=True)
438             except Exception as e:
439                 log.exception(e)
440                 abort(404)
441
442     return redirect("./", code=400)
443
444
445 if __name__ == '__main__':
446     web.run(port=8181, host="127.0.0.1")