Skip to content
Snippets Groups Projects
Verified Commit b947f834 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Added playlists ls, create and rm

parent 162fd96c
No related branches found
No related tags found
No related merge requests found
Pipeline #4564 passed
...@@ -45,6 +45,7 @@ except ImportError: ...@@ -45,6 +45,7 @@ except ImportError:
else: else:
SSL_PROTOCOLS = (*SSL_PROTOCOLS, uvloop.loop.SSLProtocol) SSL_PROTOCOLS = (*SSL_PROTOCOLS, uvloop.loop.SSLProtocol)
def ignore_aiohttp_ssl_eror(loop): def ignore_aiohttp_ssl_eror(loop):
"""Ignore aiohttp #3535 / cpython #13548 issue with SSL data after close """Ignore aiohttp #3535 / cpython #13548 issue with SSL data after close
...@@ -73,15 +74,15 @@ def ignore_aiohttp_ssl_eror(loop): ...@@ -73,15 +74,15 @@ def ignore_aiohttp_ssl_eror(loop):
"Fatal error on transport", "Fatal error on transport",
}: }:
# validate we have the right exception, transport and protocol # validate we have the right exception, transport and protocol
exception = context.get('exception') exception = context.get("exception")
protocol = context.get('protocol') protocol = context.get("protocol")
if ( if (
isinstance(exception, ssl.SSLError) isinstance(exception, ssl.SSLError)
and exception.reason == 'KRB5_S_INIT' and exception.reason == "KRB5_S_INIT"
and isinstance(protocol, SSL_PROTOCOLS) and isinstance(protocol, SSL_PROTOCOLS)
): ):
if loop.get_debug(): if loop.get_debug():
asyncio.log.logger.debug('Ignoring asyncio SSL KRB5_S_INIT error') asyncio.log.logger.debug("Ignoring asyncio SSL KRB5_S_INIT error")
return return
if orig_handler is not None: if orig_handler is not None:
orig_handler(loop, context) orig_handler(loop, context)
...@@ -129,8 +130,10 @@ def async_command(f): ...@@ -129,8 +130,10 @@ def async_command(f):
if _async_reraise: if _async_reraise:
raise raise
message = str(e) message = str(e)
if hasattr(e, 'status') and e.status == 401: if hasattr(e, "status") and e.status == 401:
message = "Remote answered with {}, ensure your are logged in first".format(e.status) message = "Remote answered with {}, ensure your are logged in first".format(
e.status
)
raise click.ClickException(message) raise click.ClickException(message)
except (exceptions.FunkwhaleError) as e: except (exceptions.FunkwhaleError) as e:
if _async_reraise: if _async_reraise:
...@@ -161,11 +164,13 @@ RAW_DECORATOR = click.option( ...@@ -161,11 +164,13 @@ RAW_DECORATOR = click.option(
"--raw", is_flag=True, help="Directly output JSON returned by the happy" "--raw", is_flag=True, help="Directly output JSON returned by the happy"
) )
class lazy_credential():
class lazy_credential:
""" """
A proxy object to request access to the proxy object at the later possible point, A proxy object to request access to the proxy object at the later possible point,
cf #4 cf #4
""" """
def __init__(self, *args): def __init__(self, *args):
self.args = args self.args = args
self._cached_value = None self._cached_value = None
...@@ -177,9 +182,15 @@ class lazy_credential(): ...@@ -177,9 +182,15 @@ class lazy_credential():
try: try:
v = keyring.get_password(*self.args) v = keyring.get_password(*self.args)
except ValueError as e: except ValueError as e:
raise click.ClickException("Error while retrieving password from keyring: {}. Your password may be incorrect.".format(e.args[0])) raise click.ClickException(
"Error while retrieving password from keyring: {}. Your password may be incorrect.".format(
e.args[0]
)
)
except Exception as e: except Exception as e:
raise click.ClickException("Error while retrieving password from keyring: {}".format(e.args[0])) raise click.ClickException(
"Error while retrieving password from keyring: {}".format(e.args[0])
)
self._cached_value = v self._cached_value = v
return v return v
...@@ -265,9 +276,15 @@ async def login(ctx, username, password): ...@@ -265,9 +276,15 @@ async def login(ctx, username, password):
try: try:
keyring.set_password(ctx.obj["SERVER_URL"], "_", token) keyring.set_password(ctx.obj["SERVER_URL"], "_", token)
except ValueError as e: except ValueError as e:
raise click.ClickException("Error while retrieving password from keyring: {}. Your password may be incorrect.".format(e.args[0])) raise click.ClickException(
"Error while retrieving password from keyring: {}. Your password may be incorrect.".format(
e.args[0]
)
)
except Exception as e: except Exception as e:
raise click.ClickException("Error while retrieving password from keyring: {}".format(e.args[0])) raise click.ClickException(
"Error while retrieving password from keyring: {}".format(e.args[0])
)
click.echo("Login successfull!") click.echo("Login successfull!")
...@@ -443,7 +460,7 @@ def get_ls_command(group, endpoint, output_conf): ...@@ -443,7 +460,7 @@ def get_ls_command(group, endpoint, output_conf):
result = await ctx.obj["remote"].request("get", url, params=params) result = await ctx.obj["remote"].request("get", url, params=params)
result.raise_for_status() result.raise_for_status()
payload = await result.json() payload = await result.json()
next_page_url = payload['next'] next_page_url = payload["next"]
page_count += 1 page_count += 1
if raw: if raw:
click.echo(json.dumps(payload, sort_keys=True, indent=4)) click.echo(json.dumps(payload, sort_keys=True, indent=4))
...@@ -766,7 +783,9 @@ def flatten(d, parent_key="", sep="_"): ...@@ -766,7 +783,9 @@ def flatten(d, parent_key="", sep="_"):
) )
@click.pass_context @click.pass_context
@async_command @async_command
async def track_download(ctx, id, format, directory, template, overwrite, ignore_errors, skip_existing): async def track_download(
ctx, id, format, directory, template, overwrite, ignore_errors, skip_existing
):
async with ctx.obj["remote"]: async with ctx.obj["remote"]:
progressbar = tqdm.tqdm(id, unit="Files") progressbar = tqdm.tqdm(id, unit="Files")
for i in progressbar: for i in progressbar:
...@@ -776,8 +795,8 @@ async def track_download(ctx, id, format, directory, template, overwrite, ignore ...@@ -776,8 +795,8 @@ async def track_download(ctx, id, format, directory, template, overwrite, ignore
logs.logger.info("Downloading from {}".format(download_url)) logs.logger.info("Downloading from {}".format(download_url))
filename_params = flatten(track_data) filename_params = flatten(track_data)
filename_params["album"] = filename_params['album_title'] filename_params["album"] = filename_params["album_title"]
filename_params["artist"] = filename_params['artist_name'] filename_params["artist"] = filename_params["artist_name"]
filename_params["extension"] = format filename_params["extension"] = format
filename_params["year"] = ( filename_params["year"] = (
filename_params["album_release_date"][:4] filename_params["album_release_date"][:4]
...@@ -792,7 +811,11 @@ async def track_download(ctx, id, format, directory, template, overwrite, ignore ...@@ -792,7 +811,11 @@ async def track_download(ctx, id, format, directory, template, overwrite, ignore
full_path = os.path.join(directory, filename) full_path = os.path.join(directory, filename)
existing = os.path.exists(full_path) existing = os.path.exists(full_path)
if skip_existing and existing: if skip_existing and existing:
logs.logger.info("'{}' already exists on disk, skipping download".format(full_path)) logs.logger.info(
"'{}' already exists on disk, skipping download".format(
full_path
)
)
continue continue
elif not overwrite and existing: elif not overwrite and existing:
raise click.ClickException( raise click.ClickException(
...@@ -801,15 +824,25 @@ async def track_download(ctx, id, format, directory, template, overwrite, ignore ...@@ -801,15 +824,25 @@ async def track_download(ctx, id, format, directory, template, overwrite, ignore
) )
) )
async with ctx.obj["remote"].request("get", download_url, timeout=0) as response: async with ctx.obj["remote"].request(
"get", download_url, timeout=0
) as response:
try: try:
response.raise_for_status() response.raise_for_status()
except aiohttp.ClientResponseError as e: except aiohttp.ClientResponseError as e:
if response.status in ignore_errors: if response.status in ignore_errors:
logs.logger.warning("Remote answered with {} for url {}, skipping".format(response.status, download_url)) logs.logger.warning(
"Remote answered with {} for url {}, skipping".format(
response.status, download_url
)
)
continue continue
else: else:
raise click.ClickException("Remote answered with {} for url {}, exiting".format(response.status, download_url)) raise click.ClickException(
"Remote answered with {} for url {}, exiting".format(
response.status, download_url
)
)
if directory: if directory:
final_directory = os.path.dirname(full_path) final_directory = os.path.dirname(full_path)
pathlib.Path(final_directory).mkdir(parents=True, exist_ok=True) pathlib.Path(final_directory).mkdir(parents=True, exist_ok=True)
...@@ -930,7 +963,7 @@ async def favorites_tracks_create(ctx, id): ...@@ -930,7 +963,7 @@ async def favorites_tracks_create(ctx, id):
async with ctx.obj["remote"]: async with ctx.obj["remote"]:
for i in id: for i in id:
data = {'track': i} data = {"track": i}
async with ctx.obj["remote"].request( async with ctx.obj["remote"].request(
"post", "api/v1/favorites/tracks/", data=data "post", "api/v1/favorites/tracks/", data=data
) as response: ) as response:
...@@ -947,7 +980,7 @@ async def favorites_tracks_rm(ctx, id): ...@@ -947,7 +980,7 @@ async def favorites_tracks_rm(ctx, id):
async with ctx.obj["remote"]: async with ctx.obj["remote"]:
for i in id: for i in id:
data = {'track': i} data = {"track": i}
async with ctx.obj["remote"].request( async with ctx.obj["remote"].request(
"delete", "api/v1/favorites/tracks/remove/", data=data "delete", "api/v1/favorites/tracks/remove/", data=data
) as response: ) as response:
...@@ -965,6 +998,60 @@ favorites_tracks_ls = get_ls_command( # noqa ...@@ -965,6 +998,60 @@ favorites_tracks_ls = get_ls_command( # noqa
) )
@cli.group()
@click.pass_context
def playlists(ctx):
pass
playlists_ls = get_ls_command(
playlists,
"api/v1/playlists/",
output_conf={
"labels": [
"ID",
"Name",
"Visibility",
"Tracks Count",
"User",
"Created",
"Modified",
],
"type": "PLAYLIST",
},
)
playlists_rm = get_delete_command(playlists, "api/v1/playlists/{}/")
@playlists.command("create")
@click.option("--name", prompt=True)
@click.option(
"--visibility",
type=click.Choice(["me", "instance", "everyone"]),
default="me",
prompt=True,
)
@click.option("--raw", is_flag=True)
@click.pass_context
@async_command
async def playlists_create(ctx, raw, name, visibility):
async with ctx.obj["remote"]:
result = await ctx.obj["remote"].request(
"post", "api/v1/playlists/", data={"name": name, "visibility": visibility}
)
result.raise_for_status()
payload = await result.json()
if raw:
click.echo(json.dumps(payload, sort_keys=True, indent=4))
else:
click.echo("Playlist created:")
click.echo(
output.table(
[payload], ["ID", "Name", "Visibility", "Tracks Count"], type="PLAYLIST"
)
)
@cli.group() @cli.group()
@click.pass_context @click.pass_context
......
...@@ -37,7 +37,6 @@ FIELDS = { ...@@ -37,7 +37,6 @@ FIELDS = {
}, },
"LIBRARY": { "LIBRARY": {
"Name": {"field": "name"}, "Name": {"field": "name"},
"Visibility": {"field": "privacy_level"},
"Description": {"field": "description"}, "Description": {"field": "description"},
"Uploads": {"field": "uploads_count"}, "Uploads": {"field": "uploads_count"},
}, },
...@@ -47,6 +46,10 @@ FIELDS = { ...@@ -47,6 +46,10 @@ FIELDS = {
"Artist": {"field": "track.artist.name"}, "Artist": {"field": "track.artist.name"},
"Favorite Date": {"field": "creation_date"}, "Favorite Date": {"field": "creation_date"},
}, },
"PLAYLIST": {
"Tracks Count": {"field": "tracks_count"},
"User": {"field": "user.username"},
},
"USER": { "USER": {
"Username": {"field": "username"}, "Username": {"field": "username"},
"Federation ID": {"field": "full_username"}, "Federation ID": {"field": "full_username"},
......
...@@ -288,3 +288,52 @@ def test_albums_ls(cli_ctx, session, responses, get_requests): ...@@ -288,3 +288,52 @@ def test_albums_ls(cli_ctx, session, responses, get_requests):
requests = get_requests("get", url) requests = get_requests("get", url)
assert len(requests) == 1 assert len(requests) == 1
def test_playlists_create(cli_ctx, session, responses, get_requests):
command = cli.playlists_create
url = "https://test.funkwhale/api/v1/playlists/"
responses.post(url)
command.callback(name="test", visibility="public", raw=False, _async_reraise=True)
requests = get_requests("post", url)
assert len(requests) == 1
assert requests[0].kwargs["data"] == {"name": "test", "visibility": "public"}
def test_playlists_ls(cli_ctx, session, responses, get_requests):
command = cli.playlists_ls
url = "https://test.funkwhale/api/v1/playlists/?ordering=-creation_date&page=1&page_size=5&q=hello"
responses.get(
url, payload={"results": [], "next": None, "previous": None, "count": 0}
)
command.callback(
raw=False,
page=1,
page_size=5,
ordering="-creation_date",
filter="favorites=true",
query=["hello"],
column=None,
format=None,
no_headers=False,
ids=False,
limit=1,
)
requests = get_requests("get", url)
assert len(requests) == 1
def test_playlists_rm(cli_ctx, session, responses, get_requests):
command = cli.playlists_rm
url = "https://test.funkwhale/api/v1/playlists/"
responses.delete(url + "1/")
responses.delete(url + "42/")
command.callback(id=[1, 42], raw=False, no_input=True, _async_reraise=True)
assert len(get_requests("delete", url + "1/")) == 1
assert len(get_requests("delete", url + "42/")) == 1
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment