summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYves Fischer <yvesf-git@xapek.org>2016-01-02 15:05:53 +0100
committerYves Fischer <yvesf-git@xapek.org>2016-01-08 20:38:18 +0100
commit478b2feb875e4ef5a458531c0d24f0b2117f5490 (patch)
tree799ff53e65e3992424e190ee4201500a8862eb71
parentd63cdea1618f3e493a966ce43fb735b0afc098b6 (diff)
downloadflask-mediabrowser-478b2feb875e4ef5a458531c0d24f0b2117f5490.tar.gz
flask-mediabrowser-478b2feb875e4ef5a458531c0d24f0b2117f5490.zip
better thumbs, better caching (with filesystem)
-rwxr-xr-xflask-mediabrowser10
-rw-r--r--mediabrowser/__init__.py75
-rw-r--r--mediabrowser/assets/directory.pngbin3167 -> 2227 bytes
-rw-r--r--mediabrowser/assets/parent.pngbin2556 -> 3205 bytes
-rw-r--r--mediabrowser/assets/spinner.gifbin0 -> 915 bytes
-rw-r--r--mediabrowser/assets/style.css11
-rw-r--r--mediabrowser/ffmpeg.py2
-rw-r--r--mediabrowser/templates/listdir.html42
-rw-r--r--mediabrowser/templates/watch.html4
-rw-r--r--mediabrowser/wsgi.py9
10 files changed, 106 insertions, 47 deletions
diff --git a/flask-mediabrowser b/flask-mediabrowser
index d1c0f8a..584d7bb 100755
--- a/flask-mediabrowser
+++ b/flask-mediabrowser
@@ -1,10 +1,11 @@
#!/usr/bin/env python3.4
from flask import Flask
-from werkzeug.contrib.cache import SimpleCache
+from werkzeug.contrib.cache import FileSystemCache
import mediabrowser
import os
+import tempfile
import logging
from argparse import ArgumentParser
@@ -19,7 +20,12 @@ if __name__ == "__main__":
args = parser.parse_args()
- cache = SimpleCache(threshold=5000, default_timeout=60*60*5)
+ cache_dir = os.path.join(tempfile.gettempdir(),
+ "mediabrowser-{}".format(os.geteuid()))
+ if not os.path.exists(cache_dir):
+ os.makedirs(cache_dir)
+ # default_timeout=0 doesn't work with FileSystemCache
+ cache = FileSystemCache(cache_dir, default_timeout=9999999999, threshold=5000)
app = Flask("mediabrowser-demo")
app.register_blueprint(mediabrowser.build(args.root, cache))
diff --git a/mediabrowser/__init__.py b/mediabrowser/__init__.py
index 62aeb48..dc7aa16 100644
--- a/mediabrowser/__init__.py
+++ b/mediabrowser/__init__.py
@@ -1,4 +1,5 @@
import os
+import io
import logging
import mimetypes
from datetime import datetime
@@ -36,6 +37,58 @@ class cached(object):
return wrapped_func
+class cached_stream(object):
+ """decorator to apply SavingIoWrapper"""
+ def __init__(self, cache, keyfunc):
+ 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 io.BytesIO(cached_value)
+ else:
+ value = func(*args, **kwargs)
+ return SavingIoWrapper(value, key, self.cache)
+
+ return wrapped_func
+
+
+class SavingIoWrapper(io.RawIOBase):
+ """Wraps a read-only io stream and buffers all read-ed data.
+ on close() that data is written to the specified cache"""
+ def __init__(self, stream, key, cache):
+ self.stream = stream
+ self.key = key
+ self.cache = cache
+ self.buf = b""
+ self.finished = False
+
+ def close(self):
+ if self.finished:
+ self.cache.set(self.key, self.buf)
+ logging.info("Saved iostream after close to key {} with"
+ " length={}".format(self.key, len(self.buf)))
+ self.stream.close()
+
+ @property
+ def closed(self):
+ return self.stream.closed
+
+ def readable(self):
+ return self.stream.readable()
+
+ def seekable(self):
+ return False
+
+ def read(self, size=-1):
+ b = self.stream.read(size)
+ self.buf += b
+ if b == b'':
+ self.finished = True
+ return b
+
def build(root_directory, cache):
blueprint = Blueprint('mediabrowser', __name__, static_folder='assets',
@@ -85,6 +138,16 @@ def build(root_directory, cache):
else:
return None
+ @cached_stream(cache=cache, keyfunc=lambda ospath: "thumb_video_{}".format(ospath))
+ def ffmpeg_thumbnail_video(ospath):
+ process = ffmpeg.thumbnail_video(ospath, 100, 60)
+ return process.stdout
+
+ @cached_stream(cache=cache, keyfunc=lambda ospath: "thumb_poster_{}".format(ospath))
+ def ffmpeg_thumbnail_poster(ospath):
+ process = ffmpeg.thumbnail(ospath, 852, 480)
+ return process.stdout
+
@blueprint.route('/assets/<path:filename>')
def assets(filename):
return blueprint.send_static_file(filename)
@@ -131,8 +194,8 @@ def build(root_directory, cache):
buf += '#EXT-X-ENDLIST\n'
return Response(buf, mimetype='application/x-mpegurl')
- @blueprint.route('/<path:path>/thumbnail')
- def thumbnail(path):
+ @blueprint.route('/<path:path>/poster')
+ def poster(path):
path = os.path.normpath(path)
ospath = os.path.join(root_directory, path)
client_mtime = request.if_modified_since
@@ -140,8 +203,8 @@ def build(root_directory, cache):
if client_mtime is not None and mtime <= client_mtime:
return Response(status=304)
else:
- process = ffmpeg.thumbnail(ospath, 90, 50)
- r = Response(process.stdout, mimetype="image/jpeg")
+ stream = ffmpeg_thumbnail_poster(ospath)
+ r = Response(stream, mimetype="image/jpeg")
r.last_modified = mtime
return r
@@ -154,8 +217,8 @@ def build(root_directory, cache):
if client_mtime is not None and mtime <= client_mtime:
return Response(status=304)
else:
- process = ffmpeg.thumbnail_video(ospath, 90, 50)
- r = Response(process.stdout, mimetype="video/webm")
+ stream = ffmpeg_thumbnail_video(ospath)
+ r = Response(stream, mimetype="video/webm")
r.last_modified = mtime
return r
diff --git a/mediabrowser/assets/directory.png b/mediabrowser/assets/directory.png
index 008a956..bd1ffb8 100644
--- a/mediabrowser/assets/directory.png
+++ b/mediabrowser/assets/directory.png
Binary files differ
diff --git a/mediabrowser/assets/parent.png b/mediabrowser/assets/parent.png
index c1919b8..7212962 100644
--- a/mediabrowser/assets/parent.png
+++ b/mediabrowser/assets/parent.png
Binary files differ
diff --git a/mediabrowser/assets/spinner.gif b/mediabrowser/assets/spinner.gif
new file mode 100644
index 0000000..6347748
--- /dev/null
+++ b/mediabrowser/assets/spinner.gif
Binary files differ
diff --git a/mediabrowser/assets/style.css b/mediabrowser/assets/style.css
index 04e56a7..b5d7689 100644
--- a/mediabrowser/assets/style.css
+++ b/mediabrowser/assets/style.css
@@ -50,10 +50,17 @@ body.list {
font-size: 36px;
}
+body.list a {
+ vertical-align: super;
+}
+
+body.list video {
+ display: inline;
+}
+
body.list > div {
- vertical-align: middle;
overflow: hidden;
- height: 64px;
+ height: 60px;
white-space: nowrap;
}
diff --git a/mediabrowser/ffmpeg.py b/mediabrowser/ffmpeg.py
index cd10ae4..d132b24 100644
--- a/mediabrowser/ffmpeg.py
+++ b/mediabrowser/ffmpeg.py
@@ -116,7 +116,7 @@ def calculate_splittimes(ospath, chunk_duration):
def thumbnail(ospath, width, height):
process = LoggedPopen(shlex.split("ffmpeg -v fatal -noaccurate_seek -ss 25.0 -i") + [ospath] +
- shlex.split("-frames:v 10 -map 0:v"
+ shlex.split("-frames:v 1 -map 0:v"
" -filter:v \"scale='w=trunc(oh*a/2)*2:h={}', crop='min({},iw):min({},ih)'\""
" -f singlejpeg pipe:".format(height+(height/10), width, height)),
stdout=PIPE)
diff --git a/mediabrowser/templates/listdir.html b/mediabrowser/templates/listdir.html
index cfa46f7..024822d 100644
--- a/mediabrowser/templates/listdir.html
+++ b/mediabrowser/templates/listdir.html
@@ -3,37 +3,13 @@
<head>
<title>Directory Browser - {{ path }}</title>
<link rel="stylesheet" href="{{ url_for('mediabrowser.assets', filename='style.css') }}" />
- <script>
- function convertOneThumbnailToVideo() {
- var els = document.getElementsByClassName("thumbnail");
- if (els.length > 0) {
- var el = els[0];
- console.log("convert",el);
-
- var videoEl = document.createElement("video");
- videoEl.addEventListener("error", convertOneThumbnailToVideo, true);
- videoEl.addEventListener("canplaythrough", convertOneThumbnailToVideo, true);
- videoEl.setAttribute("width", 90);
- videoEl.setAttribute("height", 50);
- videoEl.setAttribute("autoplay", "");
- videoEl.setAttribute("loop", true);
- videoEl.setAttribute("src", el.getAttribute("data-video-src"));
- el.parentNode.replaceChild(videoEl, el);
- }
- }
- window.onload = function() {
- convertOneThumbnailToVideo();
- }
- </script>
</head>
<body class="list">
- {% if parent != path %}
+ {% if path != '.' %}
<div>
+ <img src="{{ url_for('mediabrowser.assets', filename='parent.png') }}" />
<a href="{{ url_for('mediabrowser.listdir', path=parent) }}">
- <div style="width: 100%">
- <img src="{{ url_for('mediabrowser.assets', filename='parent.png') }}" />
- ..
- </div>
+ Parent Directory
</a>
</div>
{% endif %}
@@ -41,8 +17,10 @@
{% for file in files %}
{% if file['type'] == 'file' %}
<div>
- <img class="thumbnail" src="{{ url_for('mediabrowser.thumbnail', path=file['fullpath']) }}"
- data-video-src="{{ url_for('mediabrowser.thumbnail_video', path=file['fullpath']) }}"/>
+ <video src="{{ url_for('mediabrowser.thumbnail_video', path=file['fullpath']) }}"
+ poster="{{ url_for('mediabrowser.assets', filename='spinner.gif') }}"
+ autoplay="" loop="true" width="100" height="60">
+ </video>
<a href="{{ url_for('mediabrowser.watch', path=file['fullpath']) }}">
{{ file['filename'] }}
</a>
@@ -51,11 +29,9 @@
{% if file['type'] == 'directory' %}
<div>
+ <img src="{{ url_for('mediabrowser.assets', filename='directory.png') }}" />
<a href="{{ file['link'] }}">
- <div style="width: 100%">
- <img src="{{ url_for('mediabrowser.assets', filename='directory.png') }}" />
- {{ file['filename'] }}
- </div>
+ {{ file['filename'] }}
</a>
</div>
{% endif %}
diff --git a/mediabrowser/templates/watch.html b/mediabrowser/templates/watch.html
index 555c0e9..7e3c9fd 100644
--- a/mediabrowser/templates/watch.html
+++ b/mediabrowser/templates/watch.html
@@ -60,7 +60,7 @@
var config = {
debug: logger,
maxBufferLength: 500,
- manifestLoadingTimeOut: 120000,
+ manifestLoadingTimeOut: 20000,
levelLoadingTimeOut: 20000,
fragLoadingTimeOut: 50000
};
@@ -69,6 +69,8 @@
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
video.play();
+ video.setAttribute('poster',
+ "{{ url_for('mediabrowser.poster', path=path) }}");
});
hls.on(Hls.Events.ERROR, function (event, data) {
if (data.fatal) {
diff --git a/mediabrowser/wsgi.py b/mediabrowser/wsgi.py
index 038ca5b..4091b7b 100644
--- a/mediabrowser/wsgi.py
+++ b/mediabrowser/wsgi.py
@@ -1,7 +1,7 @@
import mediabrowser
from flask import Flask
-from werkzeug.contrib.cache import SimpleCache
+from werkzeug.contrib.cache import FileSystemCache
import os
import logging
@@ -10,7 +10,12 @@ logging.basicConfig(level=logging.INFO)
root = os.getenv("MEDIABROWSER_ROOT")
if not root:
raise Exception('Must set MEDIABROWSER_ROOT variable')
-cache = SimpleCache()
+cache_dir = os.getenv("MEDIABROWSER_CACHEDIR")
+if not cache_dir:
+ raise Exception('Must set MEDIABROWSER_CACHEDIR variable')
+
+# default_timeout=0 doesn't work with FileSystemCache
+cache = FileSystemCache(cache_dir, default_timeout=9999999999, threshold=5000)
application = Flask("mediabrowser-demo")
application.register_blueprint(mediabrowser.build(root, cache))