diff --git a/app.py b/app.py index 0b6203a..b11322e 100644 --- a/app.py +++ b/app.py @@ -28,6 +28,7 @@ import tjaf from functools import wraps from flask import Flask, g, jsonify, render_template, request, abort, redirect, session, flash, make_response, send_from_directory +import mimetypes from flask_caching import Cache from flask_session import Session from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError @@ -786,7 +787,33 @@ def send_assets(ref): @app.route(basedir + "songs/") def send_songs(ref): - return cache_wrap(flask.send_from_directory("public/songs", ref), 604800) + path = os.path.normpath(os.path.join("public/songs", ref)) + if not os.path.isfile(path): + return abort(404) + rng = flask.request.headers.get("Range") + if rng and re.match(r"^bytes=\d+-\d*$", rng): + size = os.path.getsize(path) + m = re.match(r"bytes=(\d+)-(\d*)", rng) + start = int(m.group(1)) + end = int(m.group(2) or size - 1) + start = max(0, start) + end = min(size - 1, end) + with open(path, "rb") as f: + f.seek(start) + data = f.read(end - start + 1) + mime = mimetypes.guess_type(path)[0] or "application/octet-stream" + resp = flask.Response(data, 206, mimetype=mime) + resp.headers["Content-Range"] = f"bytes {start}-{end}/{size}" + resp.headers["Accept-Ranges"] = "bytes" + resp.headers["Cache-Control"] = "public, max-age=604800, s-maxage=604800" + resp.headers["CDN-Cache-Control"] = "max-age=604800" + return resp + resp = flask.send_from_directory("public/songs", ref) + res = flask.make_response(resp) + res.headers["Accept-Ranges"] = "bytes" + res.headers["Cache-Control"] = "public, max-age=604800, s-maxage=604800" + res.headers["CDN-Cache-Control"] = "max-age=604800" + return res @app.route(basedir + "manifest.json") def send_manifest(): diff --git a/public/src/js/abstractfile.js b/public/src/js/abstractfile.js index 62694f5..ba0fbb4 100644 --- a/public/src/js/abstractfile.js +++ b/public/src/js/abstractfile.js @@ -51,6 +51,55 @@ class RemoteFile{ request.responseType = "arraybuffer" }) } + arrayBufferFast(threads){ + var t = threads || 4 + var head = new XMLHttpRequest() + head.open("HEAD", this.url) + var headPromise = pageEvents.load(head).then(() => { + if(head.status !== 200){ + return Promise.reject() + } + var len = parseInt(head.getResponseHeader("Content-Length")) + if(!len || len < 262144){ + return this.arrayBuffer() + } + var chunk = Math.ceil(len / t) + var ranges = [] + for(var i = 0; i < t; i++){ + var start = i * chunk + var end = Math.min(len - 1, (i + 1) * chunk - 1) + if(start > end){ break } + ranges.push([start, end]) + } + var promises = ranges.map(r => { + var req = new XMLHttpRequest() + req.open("GET", this.url) + req.responseType = "arraybuffer" + req.setRequestHeader("Range", "bytes=" + r[0] + "-" + r[1]) + return pageEvents.load(req).then(() => { + if(req.status !== 206 && req.status !== 200){ + return Promise.reject() + } + return req.response + }) + }) + return Promise.all(promises).then(parts => { + var total = 0 + for(var i = 0; i < parts.length; i++){ + total += parts[i].byteLength + } + var out = new Uint8Array(total) + var offset = 0 + for(var i = 0; i < parts.length; i++){ + out.set(new Uint8Array(parts[i]), offset) + offset += parts[i].byteLength + } + return out.buffer + }) + }) + head.send() + return headPromise.catch(() => this.arrayBuffer()) + } read(encoding){ if(encoding){ return this.blob().then(blob => readFile(blob, false, encoding)) diff --git a/public/src/js/soundbuffer.js b/public/src/js/soundbuffer.js index fee3d23..30dc0c6 100644 --- a/public/src/js/soundbuffer.js +++ b/public/src/js/soundbuffer.js @@ -1,4 +1,4 @@ -class SoundBuffer{ +class SoundBuffer{ constructor(...args){ this.init(...args) } @@ -12,7 +12,8 @@ } load(file, gain){ var decoder = file.name.endsWith(".ogg") ? this.oggDecoder : this.audioDecoder - return file.arrayBuffer().then(response => { + var promise = typeof file.arrayBufferFast === "function" ? file.arrayBufferFast(4) : file.arrayBuffer() + return promise.then(response => { return new Promise((resolve, reject) => { return decoder(response, resolve, reject) }).catch(error => Promise.reject([error, file.url]))