diff options
Diffstat (limited to 'mediabrowser/__init__.py')
-rw-r--r-- | mediabrowser/__init__.py | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/mediabrowser/__init__.py b/mediabrowser/__init__.py new file mode 100644 index 0000000..0bdc21d --- /dev/null +++ b/mediabrowser/__init__.py @@ -0,0 +1,196 @@ +import os +import logging +import mimetypes +from datetime import datetime +from functools import partial + +from flask import Blueprint, render_template, abort, \ + url_for, Response, request + +from . import ffmpeg + + +class cached(object): + """ + @cached(cache=cache, keyfunc=lambda path: path) + def function(path): + ... + """ + + def __init__(self, cache=None, keyfunc=None): + assert cache, 'cache parameter must be set' + assert keyfunc, 'keyfunc parameter must be set' + self.cache = cache + self.keyfunc = keyfunc + + def __call__(self, func): + def wrapped_func(*args, **kwargs): + key = self.keyfunc(*args, **kwargs) + cached_value = self.cache.get(key) + if cached_value is not None: + return cached_value + else: + value = func(*args, **kwargs) + self.cache.set(key, value) + return value + + return wrapped_func + + +def build(root_directory, cache): + blueprint = Blueprint('mediabrowser', __name__, static_folder='assets', + template_folder='templates') + + @cached(cache=cache, keyfunc=lambda ospath: ospath) + def splittimes_cached(ospath): + return list(ffmpeg.calculate_splittimes(ospath, 50)) + + @cached(cache=cache, keyfunc=lambda path: "ffprobe_{}".format(path)) + def ffprobe(path): + try: + data = ffmpeg.ffprobe_data(path) + if 'format' not in data or \ + 'duration' not in data['format']: + logging.warning('analysis failed for %s: Incomplete data', path) + return None + else: + return data + except: + logging.warning('ffprobe failed for %s', path) + return None + + @cached(cache=cache, keyfunc=lambda path: "is_video_{}".format(path)) + def get_video_mime_type(path): + """ + :return: mime type if path is video file or None otherwise + """ + fallback = { + '.mkv': 'video/x-matroska', + '.avi': 'video/avi', + '.webm': 'video/webm', + '.flv': 'video/x-flv', + '.mp4': 'video/mp4', + '.mpg': 'video/mp2t'} + + (filetype, encoding) = mimetypes.guess_type(path) + if filetype is None: + _, extension = os.path.splitext(path) + if extension in fallback.keys(): + return fallback[extension] + else: + return None + else: + if filetype.startswith('video/'): + return filetype + else: + return None + + @blueprint.route('/assets/<path:filename>') + def assets(filename): + return blueprint.send_static_file(filename) + + @blueprint.route('/<path:path>/stream/<float:ss>_<float:t>') + def stream(ss, t, path): + path = os.path.normpath(path) + ospath = os.path.join(root_directory, path) + # cut at next key frame after given time 'ss' + new_ss = ffmpeg.find_next_keyframe(ospath, ss, t / 2) + # find next key frame after given time 't' + new_t = ffmpeg.find_next_keyframe(ospath, ss + t, t / 2) - new_ss + process = ffmpeg.stream(ospath, new_ss, new_t) + return Response(process.stdout, mimetype='video/MP2T') + + @blueprint.route('/<path:path>/m3u8') + def m3u8(path): + path = os.path.normpath(path) + ospath = os.path.join(root_directory, path) + + max_chunk_duration = 60 + splittimes = splittimes_cached(ospath) + + buf = '#EXTM3U\n' + buf += '#EXT-X-VERSION:3\n' + buf += '#EXT-X-TARGETDURATION:{}\n'.format(max_chunk_duration) + buf += '#EXT-X-MEDIA-SEQUENCE:0\n' + + for (pos, chunk_duration) in splittimes: + buf += "#EXTINF:{},\n".format(chunk_duration) + buf += "stream/{}_{}\n".format(pos, chunk_duration) + + buf += '#EXT-X-ENDLIST\n' + return Response(buf, mimetype='application/x-mpegurl') + + @blueprint.route('/<path:path>/thumbnail') + def thumbnail(path): + path = os.path.normpath(path) + ospath = os.path.join(root_directory, path) + client_mtime = request.if_modified_since + mtime = datetime.fromtimestamp(os.stat(ospath).st_mtime) + if client_mtime is not None and mtime <= client_mtime: + return Response(status=304) + else: + process = ffmpeg.thumbnail_png(ospath, 64) + r = Response(process.stdout, mimetype="image/png") + r.last_modified = mtime + return r + + @blueprint.route('/<path:path>/download/inline') + def download_inline(path): + return download(path, inline=True) + + @blueprint.route('/<path:path>/download') + def download(path, inline=False): + path = os.path.normpath(path) + ospath = os.path.join(root_directory, path) + filename = os.path.basename(path) + mime_type = get_video_mime_type(ospath) + if not mime_type: + return Response(status=501, response=b'Not a video file') + + r = Response(open(ospath, 'rb'), mimetype=mime_type) + if inline: + r.headers['Content-Disposition'] = "inline; filename=\"{}\"".format(filename) + else: + r.headers['Content-Disposition'] = "attachment; filename=\"{}\"".format(filename) + + return r + + @blueprint.route('/<path:path>/watch') + def watch(path): + path = os.path.normpath(path) + filename = os.path.basename(path) + return render_template('watch.html', + path=path, filename=filename) + + @blueprint.route('/', defaults={'path': ''}) + @blueprint.route('/<path:path>/list') + def listdir(path): + def gather_fileinfo(path, ospath, filename): + osfilepath = os.path.join(ospath, filename) + if os.path.isdir(osfilepath) and not filename.startswith('.'): + return {'type': 'directory', 'filename': filename, + 'link': url_for('mediabrowser.listdir', + path=os.path.join(path, filename))} + else: + if not get_video_mime_type(osfilepath): + return None + else: + return { + 'type': 'file', 'filename': filename, + 'fullpath': os.path.join(path, filename)} + + try: + path = os.path.normpath(path) + ospath = os.path.join(root_directory, path) + files = list( + map(partial(gather_fileinfo, path, ospath), os.listdir(ospath))) + files = list(filter(lambda file: file is not None, files)) + files.sort(key=lambda i: (i['type'] == 'file' and '1' or '0') + i['filename'].lower()) + return render_template('listdir.html', + files=files, + parent=os.path.dirname(path), + path=path) + except FileNotFoundError: + abort(404) + + return blueprint |