From d6c60983c41dc4e122974e4f019550ea8c9c8728 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 9 Jul 2019 23:28:47 +0200
Subject: [PATCH] Added playlist track listing

---
 funkwhale_cli/cli.py    | 144 ++++++++++++++++++++++++++++++----------
 funkwhale_cli/output.py |   6 ++
 tests/test_cli.py       |  25 +++++++
 3 files changed, 139 insertions(+), 36 deletions(-)

diff --git a/funkwhale_cli/cli.py b/funkwhale_cli/cli.py
index 2feefef..39eb281 100644
--- a/funkwhale_cli/cli.py
+++ b/funkwhale_cli/cli.py
@@ -92,6 +92,10 @@ def ignore_aiohttp_ssl_eror(loop):
     loop.set_exception_handler(ignore_ssl_error)
 
 
+def noop_decorator(f):
+    return f
+
+
 def URL(v):
     if v is NOOP:
         raise click.ClickException(
@@ -385,25 +389,57 @@ def get_pagination_data(payload):
     return data
 
 
-def get_ls_command(group, endpoint, output_conf):
+def get_ls_command(
+    group,
+    endpoint,
+    output_conf,
+    pagination=True,
+    filter=True,
+    ordering=True,
+    with_id=False,
+    name="ls",
+):
 
     available_fields = sorted(
         set(output_conf["labels"]) | set(output.FIELDS["*"].keys())
     )
+    id_decorator = click.argument("id") if with_id else noop_decorator
+    page_decorator = (
+        click.option("--page", "-p", type=click.INT, default=1)
+        if pagination
+        else noop_decorator
+    )
+    page_size_decorator = (
+        click.option("--page-size", "-s", type=click.INT, default=None)
+        if pagination
+        else noop_decorator
+    )
+    limit_decorator = (
+        click.option("--limit", "-l", type=click.INT, default=1)
+        if pagination
+        else noop_decorator
+    )
+    ordering_decorator = (
+        click.option("--ordering", "-o", default=None) if ordering else noop_decorator
+    )
+    filter_decorator = (
+        click.option("--filter", "-f", multiple=True) if filter else noop_decorator
+    )
 
-    @group.command("ls")
+    @group.command(name)
+    @id_decorator
     @click.argument("query", nargs=-1)
     @RAW_DECORATOR
     @click.option(
         "--format", "-t", type=click.Choice(output.TABLE_FORMATS), default="simple"
     )
     @click.option("--no-headers", "-h", is_flag=True, default=False)
-    @click.option("--page", "-p", type=click.INT, default=1)
-    @click.option("--page-size", "-s", type=click.INT, default=None)
-    @click.option("--ordering", "-o", default=None)
-    @click.option("--filter", "-f", multiple=True)
     @click.option("--ids", "-i", is_flag=True)
-    @click.option("--limit", "-l", type=click.INT, default=1)
+    @page_decorator
+    @page_size_decorator
+    @ordering_decorator
+    @filter_decorator
+    @limit_decorator
     @click.option(
         "--column",
         "-c",
@@ -414,33 +450,32 @@ def get_ls_command(group, endpoint, output_conf):
     )
     @click.pass_context
     @async_command
-    async def ls(
-        ctx,
-        raw,
-        page,
-        page_size,
-        ordering,
-        filter,
-        query,
-        column,
-        format,
-        no_headers,
-        ids,
-        limit,
-    ):
+    async def ls(ctx, raw, column, format, no_headers, ids, **kwargs):
+        id = kwargs.get("id")
+        limit = kwargs.get("limit")
+        page = kwargs.get("page")
+        page_size = kwargs.get("page_size")
+        ordering = kwargs.get("ordering")
+        filter = kwargs.get("filter")
+        query = kwargs.get("query")
         if ids:
             no_headers = True
             column = [output_conf.get("id_field", "UUID")]
             format = "plain"
+        base_url = endpoint
+        if with_id:
+            base_url = base_url.format(id)
         next_page_url = None
         page_count = 0
         while True:
             if limit and page_count >= limit:
                 break
             async with ctx.obj["remote"]:
-                if page_count == 0:
-                    url = endpoint
-                    params = {"page": page}
+                if not pagination or page_count == 0:
+                    url = base_url
+                    params = {}
+                    if page:
+                        params["page"] = page
                     if page_size:
                         params["page_size"] = page_size
                     if ordering:
@@ -460,7 +495,7 @@ def get_ls_command(group, endpoint, output_conf):
                 result = await ctx.obj["remote"].request("get", url, params=params)
                 result.raise_for_status()
                 payload = await result.json()
-                next_page_url = payload["next"]
+                next_page_url = payload.get("next")
                 page_count += 1
             if raw:
                 click.echo(json.dumps(payload, sort_keys=True, indent=4))
@@ -474,6 +509,8 @@ def get_ls_command(group, endpoint, output_conf):
                         headers=not no_headers,
                     )
                 )
+                if not pagination:
+                    break
                 pagination_data = get_pagination_data(payload)
                 if pagination_data["page_size"]:
                     start = (
@@ -499,17 +536,20 @@ def get_ls_command(group, endpoint, output_conf):
     return ls
 
 
-def get_show_command(group, url_template, output_conf, name='show', force_id=None):
+def get_show_command(group, url_template, output_conf, name="show", force_id=None):
 
     available_fields = sorted(
         set(output_conf["labels"]) | set(output.FIELDS["*"].keys())
     )
     if force_id:
+
         def id_decorator(f):
             @functools.wraps(f)
             def inner(raw, column, format):
                 return f(raw, force_id, column, format)
+
             return inner
+
     else:
         id_decorator = click.argument("id")
 
@@ -529,16 +569,12 @@ def get_show_command(group, url_template, output_conf, name='show', force_id=Non
     )
     @click.pass_context
     @async_command
-    async def show(
-        ctx,
-        raw,
-        id,
-        column,
-        format,
-    ):
+    async def show(ctx, raw, id, column, format):
 
         async with ctx.obj["remote"]:
-            async with ctx.obj["remote"].request("get", url_template.format(id)) as result:
+            async with ctx.obj["remote"].request(
+                "get", url_template.format(id)
+            ) as result:
                 result.raise_for_status()
                 payload = await result.json()
         if raw:
@@ -1052,6 +1088,42 @@ async def playlists_create(ctx, raw, name, visibility):
         )
 
 
+@playlists.command("tracks-add")
+@click.argument("id")
+@click.argument("track", nargs=-1)
+@click.option(
+    "--no-duplicates",
+    is_flag=True,
+    default=False,
+    help="Prevent insertion of tracks that already exist in the playlist",
+)
+@click.pass_context
+@async_command
+async def playlists_tracks_add(ctx, id, track, no_duplicates):
+    async with ctx.obj["remote"]:
+        async with ctx.obj["remote"].request(
+            "post",
+            "api/v1/playlists/{}/".format(id),
+            data={"tracks": track, "allow_duplicates": not no_duplicates},
+        ) as response:
+            response.raise_for_status()
+
+
+playlists_tracks = get_ls_command(
+    playlists,
+    "api/v1/playlists/{}/tracks/",
+    name="tracks",
+    with_id=True,
+    pagination=False,
+    ordering=False,
+    filter=False,
+    output_conf={
+        "labels": ["Position", "ID", "Title", "Artist", "Album", "Created"],
+        "id_field": "ID",
+        "type": "PLAYLIST_TRACK",
+    },
+)
+
 
 @cli.group()
 @click.pass_context
@@ -1077,8 +1149,8 @@ users_me = get_show_command(
         ],
         "type": "USER",
     },
-    force_id='me',
-    name='me',
+    force_id="me",
+    name="me",
 )
 
 
diff --git a/funkwhale_cli/output.py b/funkwhale_cli/output.py
index 21ab2d5..0616153 100644
--- a/funkwhale_cli/output.py
+++ b/funkwhale_cli/output.py
@@ -50,6 +50,12 @@ FIELDS = {
         "Tracks Count": {"field": "tracks_count"},
         "User": {"field": "user.username"},
     },
+    "PLAYLIST_TRACK": {
+        "Position": {"field": "index"},
+        "Title": {"field": "track.title"},
+        "Artist": {"field": "track.artist.name"},
+        "Album": {"field": "track.album.title"},
+    },
     "USER": {
         "Username": {"field": "username"},
         "Federation ID": {"field": "full_username"},
diff --git a/tests/test_cli.py b/tests/test_cli.py
index bcf8764..4f0a1f9 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -337,3 +337,28 @@ def test_playlists_rm(cli_ctx, session, responses, get_requests):
 
     assert len(get_requests("delete", url + "1/")) == 1
     assert len(get_requests("delete", url + "42/")) == 1
+
+
+def test_playlists_tracks_add(cli_ctx, session, responses, get_requests):
+    command = cli.playlists_tracks_add
+    url = "https://test.funkwhale/api/v1/playlists/66/"
+    responses.post(url)
+
+    command.callback(id=66, track=[1, 42], no_duplicates=True, _async_reraise=True)
+
+    requests = get_requests("post", url)
+    assert len(requests) == 1
+    assert requests[0].kwargs["data"] == {"tracks": [1, 42], "allow_duplicates": False}
+
+
+def test_playlists_tracks(cli_ctx, session, responses, get_requests):
+    command = cli.playlists_tracks
+    url = "https://test.funkwhale/api/v1/playlists/66/tracks/"
+    responses.get(url, payload={"results": [], "count": 0})
+
+    command.callback(
+        id=66, raw=False, column=None, format=None, no_headers=False, ids=False
+    )
+
+    requests = get_requests("get", url)
+    assert len(requests) == 1
-- 
GitLab