diff --git a/funkwhale_cli/cli/uploads.py b/funkwhale_cli/cli/uploads.py index 5e803b8f56d815ff9075c1f03dff0cb943b70b05..2e0a06775456da12477133cbb1296fbd3cc24bc0 100644 --- a/funkwhale_cli/cli/uploads.py +++ b/funkwhale_cli/cli/uploads.py @@ -1,4 +1,5 @@ import datetime +import json import os import asyncio @@ -37,29 +38,106 @@ def track_read(file_obj, name, progress): setattr(file_obj, "read", patched_read) -async def upload(path, size, remote, ref, library_id, semaphore, global_progress): +def get_valid_fields(metadata): + data = {} + for field in ["title", "position", "tags", "description"]: + if field not in metadata: + continue + if field == "description": + data[field] = metadata[field]["text"] + else: + data[field] = metadata[field] + + return data + + +async def upload( + path, + size, + remote, + ref, + container_type, + draft, + album, + license, + library_or_channel_id, + semaphore, + global_progress, +): async with semaphore: filename = os.path.basename(path) data = { - "library": library_id, + container_type: library_or_channel_id, "import_reference": ref, "source": "upload://{}".format(filename), "audio_file": open(path, "rb"), } + if container_type == "channel": + # needed to set proper metadata in the file later on before publication + data["import_status"] = "draft" + data["import_metadata"] = json.dumps( + {"title": filename, "album": album, "license": license} + ) + track_read(data["audio_file"], filename, global_progress) response = await remote.request("post", "api/v1/uploads/", data=data, timeout=0) - response.raise_for_status() + upload = await base.check_status(response) + upload = await response.json() + if container_type == "channel": + metadata_response = await remote.request( + "get", "api/v1/uploads/{}/audio-file-metadata/".format(upload["uuid"]) + ) + metadata_response.raise_for_status() + metadata = await metadata_response.json() + new_data = {"import_metadata": upload["import_metadata"]} + new_data["import_metadata"].update(get_valid_fields(metadata)) + if not draft: + new_data["import_status"] = "pending" + patch_response = await remote.request( + "patch", "api/v1/uploads/{}/".format(upload["uuid"]), json=new_data + ) + print(await patch_response.json()) + patch_response.raise_for_status() + return response @uploads.command("create") -@click.argument("library_id") +@click.argument("library_or_channel_id") @click.argument("paths", nargs=-1) @click.option("-r", "--ref", default=None) +@click.option( + "-a", + "--album", + default=None, + help="Album to associate with the uploads. Only used when --channel is provided", +) +@click.option( + "-l", + "--license", + default=None, + help="License to associate with the uploads. Only used when --channel is provided", +) +@click.option( + "-c", + "--channel", + is_flag=True, + default=False, + help="Provide this flag if you're uploading to a channel", +) +@click.option( + "-d", + "--draft", + is_flag=True, + default=False, + help="Provide this flag if you want to upload in draft and publish later", +) @click.option("-p", "--parallel", type=click.INT, default=1) @click.pass_context @base.async_command -async def uploads_create(ctx, library_id, paths, ref, parallel): +async def uploads_create( + ctx, library_or_channel_id, paths, ref, parallel, draft, channel, album, license +): logs.logger.info("Uploading {} files…".format(len(paths))) paths = sorted(set(paths)) if not paths: @@ -68,23 +146,39 @@ async def uploads_create(ctx, library_id, paths, ref, parallel): sizes = {path: os.path.getsize(path) for path in paths} async with ctx.obj["remote"]: - logs.logger.info("Checking library {} existence…".format(library_id)) - library_data = await ctx.obj["remote"].request( - "get", "api/v1/libraries/{}/".format(library_id) - ) - library_data.raise_for_status() + if channel: + logs.logger.info( + "Checking channel {} existence…".format(library_or_channel_id) + ) + channel_data = await ctx.obj["remote"].request( + "get", "api/v1/channels/{}/".format(library_or_channel_id) + ) + channel_data.raise_for_status() + else: + logs.logger.info( + "Checking library {} existence…".format(library_or_channel_id) + ) + library_data = await ctx.obj["remote"].request( + "get", "api/v1/libraries/{}/".format(library_or_channel_id) + ) + library_data.raise_for_status() sem = asyncio.Semaphore(parallel) progressbar = tqdm.tqdm( total=sum(sizes.values()), unit="B", unit_scale=True, unit_divisor=1024 ) + tasks = [ upload( path=path, ref=ref, size=sizes[path], + draft=draft, + album=album, + license=license, global_progress=progressbar, remote=ctx.obj["remote"], - library_id=library_id, + library_or_channel_id=library_or_channel_id, + container_type="channel" if channel else "library", semaphore=sem, ) for path in paths diff --git a/tests/test_cli.py b/tests/test_cli.py index 4b20b557e09affb1a407f186eb1b05c634be0125..fb071b1a08a6c15a438c6269cde6e3b1766e5bda 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,4 @@ +import json import uuid import pytest @@ -175,10 +176,14 @@ def test_uploads_create(cli_ctx, session, responses, get_requests, tmpdir): responses.get("https://test.funkwhale/api/v1/libraries/{}/".format(library_id)) command.callback( - library_id=library_id, + library_or_channel_id=library_id, paths=[str(tmp_file)], parallel=1, + channel=False, ref="test-import", + draft=False, + album=None, + license=None, _async_reraise=True, ) expected_data = { @@ -198,6 +203,59 @@ def test_uploads_create(cli_ctx, session, responses, get_requests, tmpdir): assert len(libraries_requests) == 1 +def test_uploads_create_channel(cli_ctx, session, responses, get_requests, tmpdir): + tmp_file = tmpdir.join("test.mp3") + tmp_file.write_text("content", "ascii") + channel_id = str(uuid.uuid4()) + upload_id = str(uuid.uuid4()) + command = cli.uploads.uploads_create + upload_data = { + "uuid": upload_id, + "import_metadata": {"title": "test.mp3", "album": 12, "license": "cc-by-sa-4.0"} + } + responses.post("https://test.funkwhale/api/v1/uploads/", payload=upload_data) + responses.get("https://test.funkwhale/api/v1/channels/{}/".format(channel_id)) + responses.get("https://test.funkwhale/api/v1/uploads/{}/audio-file-metadata/".format(upload_id), payload={"title": 'test title'}) + responses.patch("https://test.funkwhale/api/v1/uploads/{}/".format(upload_id)) + + command.callback( + library_or_channel_id=channel_id, + channel=True, + paths=[str(tmp_file)], + parallel=1, + ref="test-import", + draft=False, + album=12, + license="cc-by-sa-4.0", + _async_reraise=True, + ) + expected_data = { + "channel": channel_id, + "import_reference": "test-import", + "source": "upload://test.mp3", + "import_status": "draft", + "import_metadata": json.dumps({"title": "test.mp3", "album": 12, "license": "cc-by-sa-4.0"}), + } + upload_requests = get_requests("post", "https://test.funkwhale/api/v1/uploads/") + assert len(upload_requests) == 1 + data = upload_requests[0].kwargs["data"] + audio_file = data.pop("audio_file") + assert data == expected_data + assert audio_file.name == str(tmp_file) + channels_requests = get_requests( + "get", "https://test.funkwhale/api/v1/channels/{}/".format(channel_id) + ) + assert len(channels_requests) == 1 + publish_requests = get_requests("patch", "https://test.funkwhale/api/v1/uploads/{}/".format(upload_id)) + assert len(publish_requests) == 1 + data = publish_requests[0].kwargs["json"] + expected_data = { + "import_status": "pending", + "import_metadata": {"title": "test title", "album": 12, "license": "cc-by-sa-4.0"}, + } + assert data == expected_data + + def test_uploads_ls(cli_ctx, session, responses, get_requests): command = cli.uploads.uploads_ls url = "https://test.funkwhale/api/v1/uploads/?ordering=-creation_date&page=1&page_size=5&q=hello"