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