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