diff --git a/api/obs/api/routes/tracks.py b/api/obs/api/routes/tracks.py index b004c3e..076d45e 100644 --- a/api/obs/api/routes/tracks.py +++ b/api/obs/api/routes/tracks.py @@ -1,16 +1,17 @@ import logging +import queue import re +import tarfile from json import load as jsonload from os.path import join, exists, isfile +from sanic.exceptions import InvalidUsage, NotFound, Forbidden +from sanic.response import file_stream, empty from sqlalchemy import select, func, and_ from sqlalchemy.orm import joinedload -from obs.api.db import Track, User, Comment, DuplicateTrackFileError from obs.api.app import api, require_auth, read_api_key, json - -from sanic.response import file_stream, empty -from sanic.exceptions import InvalidUsage, NotFound, Forbidden +from obs.api.db import Track, Comment, DuplicateTrackFileError log = logging.getLogger(__name__) @@ -58,6 +59,39 @@ async def _return_tracks(req, extend_query, limit, offset, order_by=None): }, ) +class StreamerHelper: + def __init__(self, response): + self.response = response + self.towrite = queue.Queue() + + def write(self, data): + self.towrite.put(data) + + async def send_all(self): + while True: + try: + tosend = self.towrite.get(block=False) + await self.response.send(tosend) + except queue.Empty: + break + + +async def tar_of_tracks(req, files): + + response = await req.respond(content_type="application/x-gtar", headers={'Content-Disposition': 'attachment; filename="tracks.tar.bz2"'}) + + helper = StreamerHelper(response) + + tar = tarfile.open(name=None, fileobj=helper, mode="w|bz2", bufsize=256 * 512) + for fname in files: + logging.info(f"sending {fname}") + with open(fname, "rb") as fobj: + tar.addfile(tar.gettarinfo(fname),fobj) + await helper.send_all() + tar.close() + await helper.send_all() + + await response.eof() @api.get("/tracks") async def get_tracks(req): @@ -135,13 +169,15 @@ async def tracks_bulk_action(req): action = body["action"] track_slugs = body["tracks"] - if action not in ("delete", "makePublic", "makePrivate", "reprocess"): + if action not in ("delete", "makePublic", "makePrivate", "reprocess", "download"): raise InvalidUsage("invalid action") query = select(Track).where( and_(Track.author_id == req.ctx.user.id, Track.slug.in_(track_slugs)) ) + files = set() + for track in (await req.ctx.db.execute(query)).scalars(): if action == "delete": await req.ctx.db.delete(track) @@ -155,9 +191,15 @@ async def tracks_bulk_action(req): track.public = False elif action == "reprocess": track.queue_processing() + elif action == "download": + files.add(track.get_original_file_path(req.app.config)) await req.ctx.db.commit() + if action == "download": + await tar_of_tracks(req, files) + return + return empty() diff --git a/frontend/src/pages/MyTracksPage.tsx b/frontend/src/pages/MyTracksPage.tsx index 48b9f4e..b336282 100644 --- a/frontend/src/pages/MyTracksPage.tsx +++ b/frontend/src/pages/MyTracksPage.tsx @@ -27,6 +27,8 @@ import { Page, FormattedDate, Visibility } from "components"; import api from "api"; import { useCallbackRef, formatDistance, formatDuration } from "utils"; +import download from "downloadjs"; + const COLOR_BY_STATUS: Record = { error: "red", complete: "green", @@ -233,12 +235,16 @@ function TracksTable({ title }) { }; const bulkAction = async (action: string) => { - await api.post("/tracks/bulk", { + const data = await api.post("/tracks/bulk", { body: { action, tracks: Object.keys(selectedTracks), }, + returnResponse: true }); + if (action === "download") { + download(await data.blob(), "tracks.tar.bz2", "application/x-gtar"); + } setShowBulkDelete(false); setSelectedTracks({}); @@ -263,6 +269,9 @@ function TracksTable({ title }) { bulkAction("reprocess")}> Reprocess + bulkAction("download")}> + Download + setShowBulkDelete(true)}> Delete