Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for qBittorrent 4.3.4.1 #51

Merged
merged 2 commits into from
Apr 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
env:
LATEST_PYTHON_VERSION: 3.9
LATEST_QBT_VERSION: 4.3.4.1
QBT_ALWAYS_TEST: 4.3.4.1, 4.3.3, 4.3.2
QBT_ALWAYS_TEST: 4.3.4.1, 4.3.3, 4.3.2, 4.3.1
SUBMIT_COVERAGE_VERSIONS: 2.7, 3.9
COMPREHENSIVE_TESTS_BRANCH: comprehensive_tests
PYTHON_QBITTORRENTAPI_HOST: localhost:8080
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
Version 2021.4.20 (11 apr 2021)
- Add support for ratio limit and seeding time limit when adding torrents

Version 2021.4.19 (8 apr 2021)
- Update license in setup to match gpl->mit license change on github

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import qbittorrentapi
qbt_client = qbittorrentapi.Client(host='localhost', port=8080, username='admin', password='adminadmin')

# the Client will automatically acquire/maintain a logged in state in line with any request.
# therefore, this is not necessary; however, you many want to test the provided login credentials.
# therefore, this is not necessary; however, you may want to test the provided login credentials.
try:
qbt_client.auth_log_in()
except qbittorrentapi.LoginFailed as e:
Expand Down
61 changes: 55 additions & 6 deletions qbittorrentapi/torrents.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,10 @@ def add(
use_auto_torrent_management=None,
is_sequential_download=None,
is_first_last_piece_priority=None,
tags=None,
content_layout=None,
ratio_limit=None,
seeding_time_limit=None,
**kwargs
):
return self._client.torrents_add(
Expand All @@ -603,6 +607,10 @@ def add(
is_sequential_download=is_sequential_download,
use_auto_torrent_management=use_auto_torrent_management,
is_first_last_piece_priority=is_first_last_piece_priority,
tags=tags,
content_layout=content_layout,
ratio_limit=ratio_limit,
seeding_time_limit=seeding_time_limit,
**kwargs
)

Expand Down Expand Up @@ -1022,6 +1030,8 @@ def torrents_add(
is_first_last_piece_priority=None,
tags=None,
content_layout=None,
ratio_limit=None,
seeding_time_limit=None,
**kwargs
):
"""
Expand Down Expand Up @@ -1056,9 +1066,28 @@ def torrents_add(
:param is_first_last_piece_priority: True or False for first and last piece download priority
:param tags: tag(s) to assign to torrent(s) (added in Web API v2.6.2)
:param content_layout: Original, Subfolder, or NoSubfolder to control filesystem structure for content (added in Web API v2.7)
:param ratio_limit: share limit as ratio of upload amt over download amt; e.g. 0.5 or 2.0 (added in Web API v2.8.1)
:param seeding_time_limit: number of minutes to seed torrent (added in Web API v2.8.1)
:return: "Ok." for success and "Fails." for failure
"""

# convert pre-v2.7 params to post-v2.7 params...or post-v2.7 to pre-v.2.7
api_version = self.app_web_api_version()
if (
content_layout is None
and is_root_folder is not None
and self._is_version_less_than("2.7", api_version, lteq=True)
):
content_layout = "Original" if is_root_folder else "NoSubfolder"
is_root_folder = None
elif (
content_layout is not None
and is_root_folder is None
and self._is_version_less_than(api_version, "2.7", lteq=False)
):
is_root_folder = content_layout in {"Subfolder", "Original"}
content_layout = None

data = {
"urls": (None, self._list2string(urls, "\n")),
"savepath": (None, save_path),
Expand All @@ -1072,6 +1101,8 @@ def torrents_add(
"rename": (None, rename),
"upLimit": (None, upload_limit),
"dlLimit": (None, download_limit),
"ratioLimit": (None, ratio_limit),
"seedingTimeLimit": (None, seeding_time_limit),
"autoTMM": (None, use_auto_torrent_management),
"sequentialDownload": (None, is_sequential_download),
"firstLastPiecePrio": (None, is_first_last_piece_priority),
Expand Down Expand Up @@ -1398,23 +1429,41 @@ def torrents_rename_file(
"""
torrent_hash = torrent_hash

# convert pre-v2.7 params to post-v2.7 params if a newer qBittorrent is being used
# convert pre-v2.7 params to post-v2.7...or post-v2.7 to pre-v2.7
# HACK: v4.3.2 and v4.3.3 both use web api v2.7 but old/new_path were introduced in v4.3.3
if (
old_path is None
and new_path is None
and isinstance(file_id, int)
and self._is_version_less_than("v4.3.3", self.app.version, lteq=True)
and file_id is not None
and self._is_version_less_than("v4.3.3", self.app_version(), lteq=True)
):
try:
old_path = self.torrents_files(torrent_hash=torrent_hash)[file_id].name
except (IndexError, AttributeError):
except (IndexError, AttributeError, TypeError):
logger.debug(
"ERROR: File ID '%s' isn't valid...'oldPath' cannot be determined."
% file_id
"ERROR: File ID '%s' isn't valid...'oldPath' cannot be determined.",
file_id,
)
old_path = ""
new_path = new_file_name or ""
elif (
old_path is not None
and new_path is not None
and file_id is None
and self._is_version_less_than(self.app_version(), "v4.3.3", lteq=False)
):
# previous only allowed renaming the file...not also moving it
new_file_name = new_path.split("/")[-1]
for file in self.torrents_files(torrent_hash=torrent_hash):
if file.name == old_path:
file_id = file.id
break
else:
logger.debug(
"ERROR: old_path '%s' isn't valid...'file_id' cannot be determined.",
old_path,
)
file_id = ""

data = {
"hash": torrent_hash,
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="qbittorrent-api",
version="2021.4.19",
version="2021.4.20",
packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
include_package_data=True,
install_requires=[
Expand Down
41 changes: 28 additions & 13 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from os import environ
from os import path
from os import path as os_path
from sys import path as sys_path
from time import sleep

import pytest
import six

from qbittorrentapi import APIConnectionError
from qbittorrentapi import Client
from qbittorrentapi.exceptions import APIError
from qbittorrentapi.request import Request

qbt_version = "v" + environ["QBT_VER"]
Expand Down Expand Up @@ -45,6 +45,11 @@
)
_orig_torrent_hash = "d1101a2b9d202811a05e8c57c557a20bf974dc8a"

with open(
os_path.join(sys_path[0], "tests", "kubuntu-20.04.2.0-desktop-amd64.iso.torrent"),
mode="rb",
) as f:
torrent1_file = f.read()
torrent1_url = "http://cdimage.ubuntu.com/kubuntu/releases/20.04.1/release/kubuntu-20.04.2.0-desktop-amd64.iso.torrent"
torrent1_filename = torrent1_url.split("/")[-1]
torrent1_hash = "2ea1327a1758400827fe091a9bb2a35dee9ea5e8"
Expand All @@ -53,6 +58,10 @@
torrent2_filename = torrent2_url.split("/")[-1]
torrent2_hash = "3d75247029ffa408e52714d371b6c0f15a63ff41"

with open(os_path.join(sys_path[0], "tests", "root_folder.torrent"), mode="rb") as f:
root_folder_torrent_file = f.read()
root_folder_torrent_hash = "a14553bd936a6d496402082454a70ea7a9521adc"

is_version_less_than = Request._is_version_less_than
suppress_context = Request._suppress_context

Expand Down Expand Up @@ -201,23 +210,29 @@ def orig_torrent(client, orig_torrent_hash):
@pytest.fixture
def new_torrent(client):
"""Torrent that is added on demand to qBittorrent and then removed"""
yield next(new_torrent_standalone(client))


def new_torrent_standalone(client, torrent_hash=torrent1_hash, **kwargs):
def add_test_torrent(client):
for attempt in range(_check_limit):
client.torrents.add(
urls=torrent1_url,
save_path=path.expanduser("~/test_download/"),
category="test_category",
is_paused=True,
upload_limit=1024,
download_limit=2048,
is_sequential_download=True,
is_first_last_piece_priority=True,
)
if kwargs:
client.torrents.add(**kwargs)
else:
client.torrents.add(
torrent_files=torrent1_file,
save_path=os_path.expanduser("~/test_download/"),
category="test_category",
is_paused=True,
upload_limit=1024,
download_limit=2048,
is_sequential_download=True,
is_first_last_piece_priority=True,
)
try:
# not all versions of torrents_info() support passing a hash
return list(
filter(lambda t: t.hash == torrent1_hash, client.torrents_info())
filter(lambda t: t.hash == torrent_hash, client.torrents_info())
)[0]
except:
if attempt >= _check_limit - 1:
Expand Down
Binary file not shown.
1 change: 1 addition & 0 deletions tests/root_folder.torrent
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
d10:created by18:qBittorrent v4.3.013:creation datei1618171953e4:infod5:filesld6:lengthi15e4:pathl8:file.txteee4:name11:root_folder12:piece lengthi16384e6:pieces20:�m�rR���-v?J���|7:privatei1eee
90 changes: 78 additions & 12 deletions tests/test_torrents.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from qbittorrentapi.exceptions import Forbidden403Error
from qbittorrentapi.exceptions import Conflict409Error
from qbittorrentapi.exceptions import InvalidRequest400Error
from qbittorrentapi.exceptions import MissingRequiredParameters400Error
from qbittorrentapi.exceptions import TorrentFileError
from qbittorrentapi.exceptions import TorrentFileNotFoundError
from qbittorrentapi.exceptions import TorrentFilePermissionError
Expand All @@ -31,7 +32,10 @@

from tests.conftest import (
check,
new_torrent_standalone,
retry,
root_folder_torrent_hash,
root_folder_torrent_file,
torrent1_filename,
torrent2_filename,
torrent1_hash,
Expand Down Expand Up @@ -208,20 +212,78 @@ def fake_open(*arg, **kwargs):
client.torrents_add(torrent_files="/etc/hosts")


def test_add_options(api_version, new_torrent):
check(lambda: new_torrent.category, "test_category")
@pytest.mark.parametrize("keep_root_folder", (True, False, None))
@pytest.mark.parametrize(
"content_layout", (None, "Original", "Subfolder", "NoSubfolder")
)
def test_add_options(client, api_version, keep_root_folder, content_layout):
client.torrents_delete(torrent_hashes=root_folder_torrent_hash, delete_files=True)
if is_version_less_than("2.3.0", api_version, lteq=True):
client.torrents_create_tags("option-tag")
torrent = next(
new_torrent_standalone(
torrent_hash=root_folder_torrent_hash,
client=client,
torrent_files=root_folder_torrent_file,
save_path=path.expanduser("~/test_download/"),
category="test_category",
is_paused=True,
upload_limit=1024,
download_limit=2048,
is_sequential_download=True,
is_first_last_piece_priority=True,
is_root_folder=keep_root_folder,
rename="this is a new name for the torrent",
use_auto_torrent_management=False,
tags="option-tag",
content_layout=content_layout,
ratio_limit=2,
seeding_time_limit=120,
)
)
check(lambda: torrent.category, "test_category")
check(
lambda: new_torrent.state,
lambda: torrent.state,
("pausedDL", "checkingResumeData"),
reverse=True,
any=True,
)
check(lambda: new_torrent.save_path, path.expanduser("~/test_download/"))
check(lambda: new_torrent.up_limit, 1024)
check(lambda: new_torrent.dl_limit, 2048)
check(lambda: new_torrent.seq_dl, True)
if is_version_less_than("2.0.0", api_version, lteq=False):
check(lambda: new_torrent.f_l_piece_prio, True)
check(lambda: torrent.save_path, path.expanduser("~/test_download/"))
check(lambda: torrent.up_limit, 1024)
check(lambda: torrent.dl_limit, 2048)
check(lambda: torrent.seq_dl, True)
if is_version_less_than("2.0.1", api_version, lteq=True):
check(lambda: torrent.f_l_piece_prio, True)
if content_layout is None:
check(
lambda: torrent.files[0]["name"].startswith("root_folder"),
keep_root_folder in {True, None},
)
check(lambda: torrent.name, "this is a new name for the torrent")
check(lambda: torrent.auto_tmm, False)
if is_version_less_than("2.6.2", api_version, lteq=True):
check(lambda: torrent.tags, "option-tag")

if is_version_less_than("2.7", api_version, lteq=True):
# after web api v2.7...root dir is driven by content_layout
if content_layout is None:
should_root_dir_exists = keep_root_folder in {None, True}
else:
should_root_dir_exists = content_layout in {"Original", "Subfolder"}
else:
# before web api v2.7...it is driven by is_root_folder
if content_layout is not None and keep_root_folder is None:
should_root_dir_exists = content_layout in {"Original", "Subfolder"}
else:
should_root_dir_exists = keep_root_folder in {None, True}
check(
lambda: any(f["name"].startswith("root_folder") for f in torrent.files),
should_root_dir_exists,
)

if is_version_less_than("2.8.1", api_version, lteq=True):
check(lambda: torrent.ratio_limit, 2)
check(lambda: torrent.seeding_time_limit, 120)


def test_properties(client, orig_torrent):
Expand Down Expand Up @@ -353,24 +415,28 @@ def test_rename_file(
torrent_hash=new_torrent.hash, file_id=0, new_file_name=new_name
)
else:
# pre-v4.3.3 rename_file signature
getattr(client, client_func)(
torrent_hash=new_torrent.hash, file_id=0, new_file_name=new_name
)
check(lambda: new_torrent.files[0].name.replace("+", " "), new_name)

# test invalid file ID is rejected
with pytest.raises(Conflict409Error):
getattr(client, client_func)(
torrent_hash=new_torrent.hash, file_id=10, new_file_name=new_name
)

if is_version_less_than("v4.3.2", app_version, lteq=False):
# post-v4.3.3 rename_file signature
getattr(client, client_func)(
torrent_hash=new_torrent.hash,
old_path=new_torrent.files[0].name,
new_path=new_name + "_new",
)
check(lambda: new_torrent.files[0].name.replace("+", " "), new_name + "_new")
# test invalid old_path is rejected
with pytest.raises(Conflict409Error):
getattr(client, client_func)(
torrent_hash=new_torrent.hash, old_path="asdf", new_path="xcvb"
)


@pytest.mark.parametrize("new_name", ("asdf zxcv", "asdf_zxcv"))
Expand Down