Skip to content

Commit

Permalink
Add download options and position from CLI
Browse files Browse the repository at this point in the history
Issue #33: #33
PR #93: #93
Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me>
  • Loading branch information
jonnieey committed Apr 9, 2021
1 parent ff35d2b commit 3ec3673
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 15 deletions.
25 changes: 18 additions & 7 deletions src/aria2p/api.py
Expand Up @@ -44,7 +44,12 @@ def __init__(self, client: Optional[Client] = None) -> None:
def __repr__(self) -> str:
return f"API({self.client!r})"

def add(self, uri: str) -> List[Download]: # noqa: WPS231 (not that complex)
def add(
self,
uri: str,
options: OptionsType = None,
position: int = None,
) -> List[Download]: # noqa: WPS231 (not that complex)
"""
Add a download (guess its type).
Expand All @@ -53,6 +58,9 @@ def add(self, uri: str) -> List[Download]: # noqa: WPS231 (not that complex)
Arguments:
uri: The URI or file-path to add.
options: An instance of the [`Options`][aria2p.options.Options] class or a dictionary
containing aria2c options to create the download with.
position: The position where to insert the new download in the queue. Start at 0 (top).
Returns:
The created downloads.
Expand All @@ -69,17 +77,20 @@ def add(self, uri: str) -> List[Download]: # noqa: WPS231 (not that complex)

if path_exists:
if path.suffix == ".torrent":
new_downloads.append(self.add_torrent(path))
new_downloads.append(self.add_torrent(path, options=options, position=position))
elif path.suffix == ".metalink":
new_downloads.extend(self.add_metalink(path))
new_downloads.extend(self.add_metalink(path, options=options, position=position))
else:
for uris, options in self.parse_input_file(path):
new_downloads.append(self.add_uris(uris, options=options))
for uris, download_options in self.parse_input_file(path):
# Add batch downloads in specified position in queue.
new_downloads.append(self.add_uris(uris, options=download_options, position=position))
if position is not None:
position += 1

elif uri.startswith("magnet:?"):
new_downloads.append(self.add_magnet(uri))
new_downloads.append(self.add_magnet(uri, options=options, position=position))
else:
new_downloads.append(self.add_uris([uri]))
new_downloads.append(self.add_uris([uri], options=options, position=position))

return new_downloads

Expand Down
101 changes: 93 additions & 8 deletions src/aria2p/cli.py
Expand Up @@ -253,6 +253,26 @@ def subparser(command: str, text: str, **kwargs) -> argparse.ArgumentParser: #
subparser("top", "Launch the top-like interactive interface.")
listen_parser = subparser("listen", "Listen to notifications.")

# ========= REUSABLE OPTIONS ========= #
def add_options_argument(parser):
parser.add_argument(
"-o",
"--options",
dest="options",
type=parse_options_string,
help="Options for the new download(s), separated by semi-colons. "
f"Example: 'dir=~/aria2_downloads;max-download-limit=100K'",
)

def add_position_argument(parser):
parser.add_argument(
"-p",
"--position",
dest="position",
type=int,
help="Position at which to add the new download(s) in the queue. Starts at 0 (top).",
)

# ========= CALL PARSER ========= #
call_parser.add_argument(
"method",
Expand Down Expand Up @@ -280,18 +300,26 @@ def subparser(command: str, text: str, **kwargs) -> argparse.ArgumentParser: #
# ========= ADD PARSER ========= #
add_parser.add_argument("uris", nargs="*", help="The URIs/file-paths to add.")
add_parser.add_argument("-f", "--from-file", dest="from_file", help="Load URIs from a file.")
add_options_argument(add_parser)
add_position_argument(add_parser)

# ========= ADD MAGNET PARSER ========= #
add_magnets_parser.add_argument("uris", nargs="*", help="The magnet URIs to add.")
add_magnets_parser.add_argument("-f", "--from-file", dest="from_file", help="Load URIs from a file.")
add_options_argument(add_magnets_parser)
add_position_argument(add_magnets_parser)

# ========= ADD TORRENT PARSER ========= #
add_torrents_parser.add_argument("torrent_files", nargs="*", help="The paths to the torrent files.")
add_torrents_parser.add_argument("-f", "--from-file", dest="from_file", help="Load file paths from a file.")
add_options_argument(add_torrents_parser)
add_position_argument(add_torrents_parser)

# ========= ADD METALINK PARSER ========= #
add_metalinks_parser.add_argument("metalink_files", nargs="*", help="The paths to the metalink files.")
add_metalinks_parser.add_argument("-f", "--from-file", dest="from_file", help="Load file paths from a file.")
add_options_argument(add_metalinks_parser)
add_position_argument(add_metalinks_parser)

# ========= PAUSE PARSER ========= #
pause_parser.add_argument("gids", nargs="*", help="The GIDs of the downloads to pause.")
Expand Down Expand Up @@ -456,7 +484,35 @@ def get_method(name: str) -> Optional[str]:
return methods.get(name)


def subcommand_add(api: API, uris: List[str] = None, from_file: str = None) -> int:
def parse_options_string(options_string: str = None) -> dict:
"""
Parse string of options.
Arguments:
options_string: String of aria2c options.
Returns:
Dictionary containing aria2c options.
"""
try:
return {
opt.strip(): val.strip()
for opt, val in (download_option.split("=", 1) for download_option in options_string.split(";"))
}
except ValueError:
raise argparse.ArgumentTypeError(
"Options strings must follow this format:\nopt-name=opt-value;opt-name2=opt-value2"
)


def subcommand_add(
api: API,
uris: List[str] = None,
from_file: str = None,
options: dict = None,
position: int = None,
) -> int:
"""
Add magnet subcommand.
Expand All @@ -466,6 +522,8 @@ def subcommand_add(api: API, uris: List[str] = None, from_file: str = None) -> i
from_file: Path to the file to read uris from.
Deprecated: Every URI that is a valid file-path
and is not a torrent or a metalink is now read as an input file.
options: String of aria2c options to add to download.
position: Position to add new download in the queue.
Returns:
int: 0 if OK else 1.
Expand All @@ -481,7 +539,10 @@ def subcommand_add(api: API, uris: List[str] = None, from_file: str = None) -> i
new_downloads = []

for uri in uris:
new_downloads.extend(api.add(uri))
created_downloads = api.add(uri, options=options, position=position)
new_downloads.extend(created_downloads)
if position is not None:
position += len(created_downloads)

if new_downloads:
for new_download in new_downloads:
Expand All @@ -492,14 +553,22 @@ def subcommand_add(api: API, uris: List[str] = None, from_file: str = None) -> i
return 1


def subcommand_add_magnets(api: API, uris: List[str] = None, from_file: str = None) -> int:
def subcommand_add_magnets(
api: API,
uris: List[str] = None,
from_file: str = None,
options: dict = None,
position: int = None,
) -> int:
"""
Add magnet subcommand.
Arguments:
api: The API instance to use.
uris: The URIs of the magnets.
from_file: Path to the file to read uris from.
options: String of aria2c options to add to download.
position: Position to add new download in the queue.
Returns:
int: Always 0.
Expand All @@ -517,20 +586,28 @@ def subcommand_add_magnets(api: API, uris: List[str] = None, from_file: str = No
ok = False

for uri in uris:
new_download = api.add_magnet(uri)
new_download = api.add_magnet(uri, options=options, position=position)
print(f"Created download {new_download.gid}")

return 0 if ok else 1


def subcommand_add_torrents(api: API, torrent_files: List[str] = None, from_file: str = None) -> int:
def subcommand_add_torrents(
api: API,
torrent_files: List[str] = None,
from_file: str = None,
options: dict = None,
position: int = None,
) -> int:
"""
Add torrent subcommand.
Arguments:
api: The API instance to use.
torrent_files: The paths to the torrent files.
from_file: Path to the file to read torrent files paths from.
options: String of aria2c options to add to download.
position: Position to add new download in the queue.
Returns:
int: Always 0.
Expand All @@ -548,20 +625,28 @@ def subcommand_add_torrents(api: API, torrent_files: List[str] = None, from_file
ok = False

for torrent_file in torrent_files:
new_download = api.add_torrent(torrent_file)
new_download = api.add_torrent(torrent_file, options=options, position=position)
print(f"Created download {new_download.gid}")

return 0 if ok else 1


def subcommand_add_metalinks(api: API, metalink_files: List[str] = None, from_file: str = None) -> int:
def subcommand_add_metalinks(
api: API,
metalink_files: List[str] = None,
from_file: str = None,
options: dict = None,
position: int = None,
) -> int:
"""
Add metalink subcommand.
Arguments:
api: The API instance to use.
metalink_files: The paths to the metalink files.
from_file: Path to the file to metalink files paths from.
options: String of aria2c options to add to download.
position: Position to add new download in the queue.
Returns:
int: 0 if OK else 1.
Expand All @@ -579,7 +664,7 @@ def subcommand_add_metalinks(api: API, metalink_files: List[str] = None, from_fi
ok = False

for metalink_file in metalink_files:
new_downloads = api.add_metalink(metalink_file)
new_downloads = api.add_metalink(metalink_file, options=options, position=position)
for download in new_downloads:
print(f"Created download {download.gid}")

Expand Down
15 changes: 15 additions & 0 deletions tests/test_cli.py
Expand Up @@ -232,3 +232,18 @@ def thread_target():
captured = capsys.readouterr()
assert captured.err == ""
assert captured.out == "started 0000000000000001\nstarted 0000000000000002\n"


@pytest.mark.parametrize("command", ["add", "add-magnet", "add-torrent", "add-metalink"])
def test_parse_valid_options(command):
parser = cli.get_parser()
opts = parser.parse_args([command, "/some/file/on/the/disk", "-o", "opt1=val;opt2=val2, val3=val4"])
assert opts.options == {"opt1": "val", "opt2": "val2, val3=val4"}


@pytest.mark.parametrize("command", ["add", "add-magnet", "add-torrent", "add-metalink"])
def test_parse_invalid_options(command, capsys):
parser = cli.get_parser()
with pytest.raises(SystemExit):
opts = parser.parse_args([command, "http://example.com", "-o", "opt1"])
assert "Options strings must follow this format" in capsys.readouterr().err

0 comments on commit 3ec3673

Please sign in to comment.