Skip to content

Commit a190e5d

Browse files
committed
Add a Click commandline interface
- Added commands for all currently available api methods
1 parent 54325ba commit a190e5d

File tree

3 files changed

+344
-3
lines changed

3 files changed

+344
-3
lines changed

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,18 @@ dependencies = [
2020
"aiohttp~=3.9"
2121
]
2222

23+
[project.optional-dependencies]
24+
cli = [
25+
"click",
26+
]
27+
2328
[project.urls]
2429
Documentation = "https://tr4nt0r.github.io/pyloadapi"
2530
Source = "https://github.com/tr4nt0r/pyloadapi"
2631

32+
[project.scripts]
33+
pyloadapi = "pyloadapi.cli:cli"
34+
2735
[tool.hatch.version]
2836
source = "regex_commit"
2937
commit_extra_args = ["-e"]

requirements_dev.txt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
aiohttp==3.9.5
2+
click==8.1.7
23
mypy==1.10.1
34
pre-commit==3.7.1
45
python-dotenv==1.0.1
56
ruff==0.5.0
6-
7-
8-

src/pyloadapi/cli.py

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
"""Commandline interface for pyloadapi."""
2+
3+
import asyncio
4+
import json
5+
import logging
6+
from pathlib import Path
7+
from typing import Any
8+
9+
import aiohttp
10+
import click
11+
12+
from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI
13+
14+
logging.basicConfig(level=logging.INFO)
15+
_LOGGER = logging.getLogger(__name__)
16+
17+
CONFIG_FILE_PATH = Path("~/.config/pyloadapi/.pyload_config.json").expanduser()
18+
19+
20+
def load_config() -> Any:
21+
"""Load the configuration from a JSON file."""
22+
if not CONFIG_FILE_PATH.is_file():
23+
return {}
24+
25+
with CONFIG_FILE_PATH.open(encoding="utf-8") as file:
26+
return json.load(file)
27+
28+
29+
def save_config(config: dict[str, Any]) -> None:
30+
"""Save the configuration to a JSON file."""
31+
CONFIG_FILE_PATH.parent.mkdir(exist_ok=True, parents=True)
32+
with CONFIG_FILE_PATH.open("w", encoding="utf-8") as file:
33+
json.dump(config, file, indent=4)
34+
35+
36+
async def init_api(
37+
session: aiohttp.ClientSession, api_url: str, username: str, password: str
38+
) -> PyLoadAPI:
39+
"""Initialize the PyLoadAPI."""
40+
41+
api = PyLoadAPI(
42+
session=session, api_url=api_url, username=username, password=password
43+
)
44+
await api.login()
45+
46+
return api
47+
48+
49+
def abort_if_false(ctx: click.Context, param: Any, value: Any) -> None:
50+
"""Abort operation."""
51+
52+
if not value:
53+
ctx.abort()
54+
55+
56+
@click.group()
57+
@click.option("--api-url", help="Base URL of pyLoad")
58+
@click.option("--username", help="Username for pyLoad")
59+
@click.option("--password", help="Password for pyLoad")
60+
@click.pass_context
61+
def cli(
62+
ctx: click.Context,
63+
api_url: str | None = None,
64+
username: str | None = None,
65+
password: str | None = None,
66+
) -> None:
67+
"""CLI for interacting with pyLoad."""
68+
69+
config = load_config()
70+
71+
if api_url:
72+
config["api_url"] = api_url
73+
74+
if username:
75+
config["username"] = username
76+
77+
if password:
78+
config["password"] = password
79+
80+
save_config(config)
81+
82+
ctx.ensure_object(dict)
83+
ctx.obj["api_url"] = config.get("api_url")
84+
ctx.obj["username"] = config.get("username")
85+
ctx.obj["password"] = config.get("password")
86+
87+
if not all([ctx.obj["api_url"], ctx.obj["username"], ctx.obj["password"]]):
88+
raise click.ClickException(
89+
"URL, username, and password must be provided either via command line or config file."
90+
)
91+
92+
93+
@cli.command()
94+
@click.pass_context
95+
def status(ctx: click.Context) -> None:
96+
"""Display the general status information of pyLoad."""
97+
98+
async def _status() -> None:
99+
try:
100+
async with aiohttp.ClientSession() as session:
101+
api = await init_api(
102+
session,
103+
ctx.obj["api_url"],
104+
ctx.obj["username"],
105+
ctx.obj["password"],
106+
)
107+
stat = await api.get_status()
108+
free_space = await api.free_space()
109+
click.echo(
110+
f"Status:\n"
111+
f" - Active Downloads: {stat['active']}\n"
112+
f" - Items in Queue: {stat['queue']}\n"
113+
f" - Finished Downloads: {stat['total']}\n"
114+
f" - Download Speed: {(stat['speed'] * 8) / 1000000 } Mbit/s\n"
115+
f" - Free space: {round(free_space / (1024 ** 3), 2)} GiB\n"
116+
f" - Reconnect: {'Enabled' if stat['reconnect'] else 'Disabled'}\n"
117+
f" - Queue : {'Paused' if stat['pause'] else 'Running'}\n"
118+
)
119+
except CannotConnect as e:
120+
raise click.ClickException("Error: Unable to connect to pyLoad") from e
121+
except InvalidAuth as e:
122+
raise click.ClickException(
123+
"Error: Authentication failed, verify username and password"
124+
) from e
125+
except ParserError as e:
126+
raise click.ClickException(
127+
"Error: Unable to parse response from pyLoad"
128+
) from e
129+
130+
asyncio.run(_status())
131+
132+
133+
@cli.command()
134+
@click.option("-p", "--pause", is_flag=True, help="Pause pyLoads download queue")
135+
@click.option("-r", "--resume", is_flag=True, help="Resume pyLoads download queue")
136+
@click.pass_context
137+
def queue(ctx: click.Context, pause: bool, resume: bool) -> None:
138+
"""Toggle, pause or resume the download queue in pyLoad."""
139+
140+
async def _queue(pause: bool, resume: bool) -> None:
141+
try:
142+
async with aiohttp.ClientSession() as session:
143+
api = await init_api(
144+
session,
145+
ctx.obj["api_url"],
146+
ctx.obj["username"],
147+
ctx.obj["password"],
148+
)
149+
if pause:
150+
await api.pause()
151+
elif resume:
152+
await api.unpause()
153+
else:
154+
await api.toggle_pause()
155+
156+
s = await api.get_status()
157+
if s.get("pause") is True:
158+
click.echo("Paused download queue.")
159+
else:
160+
click.echo("Resumed download queue.")
161+
162+
except CannotConnect as e:
163+
raise click.ClickException("Error: Unable to connect to pyLoad") from e
164+
except InvalidAuth as e:
165+
raise click.ClickException(
166+
"Error: Authentication failed, verify username and password"
167+
) from e
168+
except ParserError as e:
169+
raise click.ClickException(
170+
"Error: Unable to parse response from pyLoad"
171+
) from e
172+
173+
asyncio.run(_queue(pause, resume))
174+
175+
176+
@cli.command()
177+
@click.pass_context
178+
def stop_all(ctx: click.Context) -> None:
179+
"""Abort all currently running downloads in pyLoad."""
180+
181+
async def _stop_all() -> None:
182+
try:
183+
async with aiohttp.ClientSession() as session:
184+
api = await init_api(
185+
session,
186+
ctx.obj["api_url"],
187+
ctx.obj["username"],
188+
ctx.obj["password"],
189+
)
190+
191+
await api.stop_all_downloads()
192+
click.echo("Aborted all running downloads.")
193+
except CannotConnect as e:
194+
raise click.ClickException("Error: Unable to connect to pyLoad") from e
195+
except InvalidAuth as e:
196+
raise click.ClickException(
197+
"Error: Authentication failed, verify username and password"
198+
) from e
199+
except ParserError as e:
200+
raise click.ClickException(
201+
"Error: Unable to parse response from pyLoad"
202+
) from e
203+
204+
asyncio.run(_stop_all())
205+
206+
207+
@cli.command()
208+
@click.pass_context
209+
def retry(ctx: click.Context) -> None:
210+
"""Retry all failed downloads in pyLoad."""
211+
212+
async def _retry() -> None:
213+
try:
214+
async with aiohttp.ClientSession() as session:
215+
api = await init_api(
216+
session,
217+
ctx.obj["api_url"],
218+
ctx.obj["username"],
219+
ctx.obj["password"],
220+
)
221+
222+
await api.restart_failed()
223+
click.echo("Retrying failed downloads.")
224+
except CannotConnect as e:
225+
raise click.ClickException("Error: Unable to connect to pyLoad") from e
226+
except InvalidAuth as e:
227+
raise click.ClickException(
228+
"Error: Authentication failed, verify username and password"
229+
) from e
230+
except ParserError as e:
231+
raise click.ClickException(
232+
"Error: Unable to parse response from pyLoad"
233+
) from e
234+
235+
asyncio.run(_retry())
236+
237+
238+
@cli.command()
239+
@click.pass_context
240+
def delete_finished(ctx: click.Context) -> None:
241+
"""Delete all finished files and packages from pyLoad."""
242+
243+
async def _delete_finished() -> None:
244+
try:
245+
async with aiohttp.ClientSession() as session:
246+
api = await init_api(
247+
session,
248+
ctx.obj["api_url"],
249+
ctx.obj["username"],
250+
ctx.obj["password"],
251+
)
252+
253+
await api.delete_finished()
254+
click.echo("Deleted finished files and packages.")
255+
except CannotConnect as e:
256+
raise click.ClickException("Error: Unable to connect to pyLoad") from e
257+
except InvalidAuth as e:
258+
raise click.ClickException(
259+
"Error: Authentication failed, verify username and password"
260+
) from e
261+
except ParserError as e:
262+
raise click.ClickException(
263+
"Error: Unable to parse response from pyLoad"
264+
) from e
265+
266+
asyncio.run(_delete_finished())
267+
268+
269+
@cli.command()
270+
@click.pass_context
271+
def restart(ctx: click.Context) -> None:
272+
"""Restart the pyLoad service."""
273+
274+
async def _restart() -> None:
275+
try:
276+
async with aiohttp.ClientSession() as session:
277+
api = await init_api(
278+
session,
279+
ctx.obj["api_url"],
280+
ctx.obj["username"],
281+
ctx.obj["password"],
282+
)
283+
284+
await api.restart()
285+
click.echo("Restarting pyLoad...")
286+
except CannotConnect as e:
287+
raise click.ClickException("Error: Unable to connect to pyLoad") from e
288+
except InvalidAuth as e:
289+
raise click.ClickException(
290+
"Error: Authentication failed, verify username and password"
291+
) from e
292+
except ParserError as e:
293+
raise click.ClickException(
294+
"Error: Unable to parse response from pyLoad"
295+
) from e
296+
297+
asyncio.run(_restart())
298+
299+
300+
@cli.command()
301+
@click.pass_context
302+
def toggle_reconnect(ctx: click.Context) -> None:
303+
"""Toggle the state of the auto-reconnect function of pyLoad."""
304+
305+
async def _toggle_reconnect() -> None:
306+
try:
307+
async with aiohttp.ClientSession() as session:
308+
api = await init_api(
309+
session,
310+
ctx.obj["api_url"],
311+
ctx.obj["username"],
312+
ctx.obj["password"],
313+
)
314+
315+
await api.toggle_reconnect()
316+
s = await api.get_status()
317+
click.echo(
318+
f"{"Enabled" if s.get("reconnect") else "Disabled"} auto-reconnect"
319+
)
320+
except CannotConnect as e:
321+
raise click.ClickException("Error: Unable to connect to pyLoad") from e
322+
except InvalidAuth as e:
323+
raise click.ClickException(
324+
"Error: Authentication failed, verify username and password"
325+
) from e
326+
except ParserError as e:
327+
raise click.ClickException(
328+
"Error: Unable to parse response from pyLoad"
329+
) from e
330+
331+
asyncio.run(_toggle_reconnect())
332+
333+
334+
if __name__ == "__main__":
335+
cli()

0 commit comments

Comments
 (0)