diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index a9636c02a..a22b10f6f 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -21,7 +21,7 @@ jobs: env: LATEST_PYTHON_VERSION: "3.10" LATEST_QBT_VERSION: 4.4.0rc1 - QBT_ALWAYS_TEST: 4.4.0rc1, 4.3.9, 4.3.8, 4.3.5, 4.3.4.1, 4.3.3, 4.3.2, 4.3.1, 4.3.0.1, 4.2.0 + QBT_ALWAYS_TEST: 4.4.0, 4.3.9, 4.3.8, 4.3.5, 4.3.4.1, 4.3.3, 4.3.2, 4.3.1, 4.3.0.1, 4.2.0 QT_USE_DEFAULT_PAA: 4.3.9, 4.3.8, 4.3.7, 4.3.6, 4.3.5, 4.3.4.1, 4.3.3, 4.3.2, 4.3.1, 4.3.0.1, 4.2.5, 4.2.0 SUBMIT_COVERAGE_VERSIONS: 2.7, 3.10 COMPREHENSIVE_TESTS_BRANCH: comprehensive_tests @@ -32,8 +32,8 @@ jobs: QBT_LEGACY_INSTALL: 4.2.5, 4.2.0 strategy: matrix: - QBT_VER: [4.4.0rc1, 4.3.9, 4.3.8, 4.3.7, 4.3.6, 4.3.5, 4.3.4.1, 4.3.3, 4.3.2, 4.3.1, 4.3.0.1, 4.2.5, 4.2.0] - # QBT_VER: [4.4.0beta3] + # QBT_VER: [4.4.0, 4.3.9, 4.3.8, 4.3.7, 4.3.6, 4.3.5, 4.3.4.1, 4.3.3, 4.3.2, 4.3.1, 4.3.0.1, 4.2.5, 4.2.0] + QBT_VER: [4.4.0] python-version: ["3.10", "3.9", "3.8", "3.7", "3.6", "2.7", "pypy2", "pypy3", "3.11.0-alpha.2"] # python-version: [3.9] @@ -129,6 +129,7 @@ jobs: mkdir -p "$SRC_DIR" && mkdir -p "$QBT_DIR" cd "$SRC_DIR" + rm -rf qBittorrent git clone https://github.com/qbittorrent/qBittorrent.git --branch release-${{ matrix.QBT_VER }} --depth 1 cd qBittorrent export libtorrent_CFLAGS="$LIBTOR_DIR/include/" && export libtorrent_LIBS="$LIBTOR_DIR/lib/libtorrent-rasterbar.so" @@ -149,6 +150,7 @@ jobs: mkdir -p "$SRC_DIR" && mkdir -p "$QBT_DIR" cd "$SRC_DIR" + rm -rf qBittorrent git clone https://github.com/qbittorrent/qBittorrent.git --branch release-${{ matrix.QBT_VER }} --depth 1 cd qBittorrent cmake -G "Ninja" -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$LIBTOR_DIR" -DVERBOSE_CONFIGURE=ON \ diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bba08c6e9..a4d280b50 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +Version 2022.1.27 (7 jan 2022) + - Support for qBittorrent v4.4.0 + - torrents/info results can now be filtered by a torrent tag with parameter tag + - Added new torrent state "Forced Metadata Downloading" + - Support per-torrent/per-category "download folder" + Version 2021.12.26 (11 dec 2021) - Stop sending Origin and Referer headers (Fixes #63) diff --git a/README.md b/README.md index ef194dc39..956659ca9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ qBittorrent Web API Client Python client implementation for qBittorrent Web API. Supports qBittorrent v4.1.0+ (i.e. Web API v2.0+). -Currently supports up to qBittorrent [v4.3.9](https://github.com/qbittorrent/qBittorrent/releases/tag/release-4.3.9) (Web API v2.8.2) released on Oct 31, 2021. +Currently supports up to qBittorrent [v4.4.0](https://github.com/qbittorrent/qBittorrent/releases/tag/release-4.4.0) (Web API v2.8.3) released on XXX XX, 2021. [Find the full documentation for this client on RTD.](https://qbittorrent-api.readthedocs.io/) diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index 76ea53adf..41a19a0ed 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -21,7 +21,7 @@ Introduction Python client implementation for qBittorrent Web API. -Currently supports up to qBittorrent `v4.3.9 `_ (Web API v2.8.2) released on Oct 31, 2021. +Currently supports up to qBittorrent `v4.4.0 `_ (Web API v2.8.3) released on XXX XX, 2021. The full qBittorrent Web API documentation is available on their `wiki `_. diff --git a/qbittorrentapi/definitions.py b/qbittorrentapi/definitions.py index d9d7aac7e..759c9f0a4 100644 --- a/qbittorrentapi/definitions.py +++ b/qbittorrentapi/definitions.py @@ -59,6 +59,7 @@ class TorrentStates(Enum): ALLOCATING = "allocating" DOWNLOADING = "downloading" METADATA_DOWNLOAD = "metaDL" + FORCED_METADATA_DOWNLOAD = "forcedMetaDL" PAUSED_DOWNLOAD = "pausedDL" QUEUED_DOWNLOAD = "queuedDL" FORCED_DOWNLOAD = "forcedDL" @@ -74,6 +75,7 @@ def is_downloading(self): return self in ( TorrentStates.DOWNLOADING, TorrentStates.METADATA_DOWNLOAD, + TorrentStates.FORCED_METADATA_DOWNLOAD, TorrentStates.STALLED_DOWNLOAD, TorrentStates.CHECKING_DOWNLOAD, TorrentStates.PAUSED_DOWNLOAD, diff --git a/qbittorrentapi/torrents.py b/qbittorrentapi/torrents.py index 420977496..817dedce6 100644 --- a/qbittorrentapi/torrents.py +++ b/qbittorrentapi/torrents.py @@ -638,6 +638,7 @@ def __call__( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -648,6 +649,7 @@ def __call__( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -659,6 +661,7 @@ def all( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -669,6 +672,7 @@ def all( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -680,6 +684,7 @@ def downloading( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -690,6 +695,7 @@ def downloading( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -701,6 +707,7 @@ def completed( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -711,6 +718,7 @@ def completed( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -722,6 +730,7 @@ def paused( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -732,6 +741,7 @@ def paused( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -743,6 +753,7 @@ def active( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -753,6 +764,7 @@ def active( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -764,6 +776,7 @@ def inactive( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -774,6 +787,7 @@ def inactive( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -785,6 +799,7 @@ def resumed( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -795,6 +810,7 @@ def resumed( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -806,6 +822,7 @@ def stalled( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -816,6 +833,7 @@ def stalled( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -827,6 +845,7 @@ def stalled_uploading( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -837,6 +856,7 @@ def stalled_uploading( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -848,6 +868,7 @@ def stalled_downloading( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): return self._client.torrents_info( @@ -858,6 +879,7 @@ def stalled_downloading( limit=limit, offset=offset, torrent_hashes=torrent_hashes, + tag=tag, **kwargs ) @@ -1544,6 +1566,7 @@ def torrents_info( limit=None, offset=None, torrent_hashes=None, + tag=None, **kwargs ): """ @@ -1558,6 +1581,7 @@ def torrents_info( :param limit: Limit length of list :param offset: Start of list (if < 0, offset from end of list) :param torrent_hashes: Filter list by hash (separate multiple hashes with a '|') + :param tag: Filter list by tag (empty string means "untagged"; no "tag" param means "any tag"; added in Web API v2.8.3) :return: :class:`TorrentInfoList` - https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list """ data = { @@ -1568,6 +1592,7 @@ def torrents_info( "limit": limit, "offset": offset, "hashes": self._list2string(torrent_hashes, "|"), + "tag": tag, } return self._post(_name=APINames.Torrents, _method="info", data=data, **kwargs) diff --git a/setup.py b/setup.py index 55b9c6285..890e46959 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="qbittorrent-api", - version="2021.12.26", + version="2022.1.27", packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), include_package_data=True, install_requires=[ diff --git a/tests/conftest.py b/tests/conftest.py index ed810300a..dbadc78aa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,47 +7,16 @@ from qbittorrentapi import APIConnectionError from qbittorrentapi import Client -from qbittorrentapi.request import Request -qbt_version = "v" + os.environ["QBT_VER"] +from .version_map import api_version_map -api_version_map = { - "v4.1.0": "2.0", - "v4.1.1": "2.0.1", - "v4.1.2": "2.0.2", - "v4.1.3": "2.1", - "v4.1.4": "2.1.1", - "v4.1.5": "2.2", - "v4.1.6": "2.2", - "v4.1.7": "2.2", - "v4.1.8": "2.2", - "v4.1.9": "2.2.1", - "v4.1.9.1": "2.2.1", - "v4.2.0": "2.3", - "v4.2.1": "2.4", - "v4.2.2": "2.4.1", - "v4.2.3": "2.4.1", - "v4.2.4": "2.5", - "v4.2.5": "2.5.1", - "v4.3.0": "2.6", - "v4.3.0.1": "2.6", - "v4.3.1": "2.6.1", - "v4.3.2": "2.7", - "v4.3.3": "2.7", - "v4.3.4.1": "2.8.1", - "v4.3.5": "2.8.2", - "v4.3.6": "2.8.2", - "v4.3.7": "2.8.2", - "v4.3.8": "2.8.2", - "v4.3.9": "2.8.2", - "v4.4.0rc1": "2.8.3", -} +qbt_version = "v" + os.environ["QBT_VER"] BASE_PATH = sys_path[0] _check_limit = 10 _orig_torrent_url = ( - "http://releases.ubuntu.com/21.04/ubuntu-21.04-desktop-amd64.iso.torrent" + "https://releases.ubuntu.com/21.04/ubuntu-21.04-desktop-amd64.iso.torrent" ) _orig_torrent_hash = "64a980abe6e448226bb930ba061592e44c3781a1" @@ -58,11 +27,11 @@ mode="rb", ) as f: torrent1_file = f.read() -torrent1_url = "http://cdimage.ubuntu.com/kubuntu/releases/21.04/release/kubuntu-21.04-desktop-amd64.iso.torrent" +torrent1_url = "https://cdimage.ubuntu.com/kubuntu/releases/21.04/release/kubuntu-21.04-desktop-amd64.iso.torrent" torrent1_filename = torrent1_url.split("/")[-1] torrent1_hash = "d65d07329264aecb2d2be7a6c0e86b6613b2a600" -torrent2_url = "http://cdimage.ubuntu.com/xubuntu/releases/21.04/release/xubuntu-21.04-desktop-amd64.iso.torrent" +torrent2_url = "https://cdimage.ubuntu.com/xubuntu/releases/21.04/release/xubuntu-21.04-desktop-amd64.iso.torrent" torrent2_filename = torrent2_url.split("/")[-1] torrent2_hash = "80d773cbf111e906608077967683a0ffcc3a7668" diff --git a/tests/test_definitions.py b/tests/test_definitions.py index 262e7ef74..a53e65c9e 100644 --- a/tests/test_definitions.py +++ b/tests/test_definitions.py @@ -18,6 +18,7 @@ "allocating", "downloading", "metaDL", + "forcedMetaDL", "pausedDL", "queuedDL", "forcedDL", @@ -31,6 +32,7 @@ downloading_states = ( "downloading", "metaDL", + "forcedMetaDL", "stalledDL", "checkingDL", "pausedDL", diff --git a/tests/test_torrent.py b/tests/test_torrent.py index 535101708..e9f15dbd9 100644 --- a/tests/test_torrent.py +++ b/tests/test_torrent.py @@ -182,7 +182,12 @@ def test_set_location(api_version, new_torrent, client_func): try: loc = path.expanduser("~/Downloads/3/") getattr(new_torrent, client_func)(loc) - check(lambda: new_torrent.info.save_path, loc) + # qBittorrent may return trailing separators depending on version.... + check( + lambda: new_torrent.info.save_path, + (loc, loc[: len(loc) - 1]), + any=True, + ) break except AssertionError as e: exp = e diff --git a/tests/test_torrents.py b/tests/test_torrents.py index aea69a4ce..39a825f98 100644 --- a/tests/test_torrents.py +++ b/tests/test_torrents.py @@ -1,6 +1,6 @@ import errno import logging -from os import path +from os import path, stat from pkg_resources import parse_version as v import platform from sys import version_info @@ -517,6 +517,18 @@ def test_torrents_info(client, api_version, orig_torrent_hash, client_func): get_func(client, client_func)(torrent_hashes=orig_torrent_hash) +@pytest.mark.parametrize("client_func", ("torrents_info", "torrents.info")) +def test_torrents_info_tag(client, api_version, new_torrent, client_func): + if v(api_version) < v("2.8.3"): + return + tag_name = "tag_filter_name" + client.torrents_add_tags(tags=tag_name, torrent_hashes=new_torrent.hash) + torrents = get_func(client, client_func)( + torrent_hashes=new_torrent.hash, tag=tag_name + ) + assert new_torrent.hash in {t.hash for t in torrents} + + @pytest.mark.parametrize( "client_func", (("torrents_pause", "torrents_resume"), ("torrents.pause", "torrents.resume")), @@ -746,18 +758,15 @@ def test_set_share_limits(client, api_version, client_func, orig_torrent): ) def test_set_location(client, api_version, client_func, new_torrent): if v(api_version) > v("2.0.1"): - home = path.expanduser("~") - # whether the location is writable is only checked after version 2.0.1 - if v(api_version) > v("2.0.1"): - with pytest.raises(Forbidden403Error): - get_func(client, client_func)( - location="/etc/", torrent_hashes=new_torrent.hash - ) + with pytest.raises(Forbidden403Error): + get_func(client, client_func)( + location="/etc/", torrent_hashes=new_torrent.hash + ) - get_func(client, client_func)( - location="%s/Downloads/1/" % home, torrent_hashes=new_torrent.hash - ) - check(lambda: new_torrent.info.save_path, "%s/Downloads/1/" % home) + loc = path.expanduser("~/Downloads/1/") + get_func(client, client_func)(location=loc, torrent_hashes=new_torrent.hash) + # qBittorrent may return trailing separators depending on version.... + check(lambda: new_torrent.info.save_path, (loc, loc[: len(loc) - 1]), any=True) @pytest.mark.parametrize( @@ -901,6 +910,14 @@ def test_torrents_add_peers(client, api_version, orig_torrent, client_func, peer assert isinstance(p, TorrentsAddPeersDictionary) +def _categories_save_path_key(api_version): + """ + With qBittorrent 4.4.0 (Web API 2.8.4), the key in the category + definition returned changed from savePath to save_path.... + """ + return "save_path" if v(api_version) >= v("2.8.4") else "savePath" + + def test_categories1(client, api_version): if v(api_version) < v("2.1.1"): with pytest.raises(NotImplementedError): @@ -914,11 +931,12 @@ def test_categories2(client, api_version): with pytest.raises(NotImplementedError): client.torrent_categories.categories else: + save_path_key = _categories_save_path_key(api_version) name = "new_category" - client.torrent_categories.categories = {"name": name, "savePath": "/tmp"} + client.torrent_categories.categories = {"name": name, save_path_key: "/tmp"} assert name in client.torrent_categories.categories - client.torrent_categories.categories = {"name": name, "savePath": "/tmp/new"} - assert client.torrent_categories.categories[name]["savePath"] == "/tmp/new" + client.torrent_categories.categories = {"name": name, save_path_key: "/tmp/new"} + assert client.torrent_categories.categories[name][save_path_key] == "/tmp/new" client.torrents_remove_categories(categories=name) @@ -952,8 +970,12 @@ def test_create_categories( name, reverse=True, ) + save_path_key = _categories_save_path_key(api_version) + print(client.torrents_categories()) check( - lambda: (cat.savePath for cat in client.torrents_categories().values()), + lambda: [ + cat[save_path_key] for cat in client.torrents_categories().values() + ], save_path or "", reverse=True, ) @@ -986,8 +1008,11 @@ def test_edit_category(client, api_version, client_func, save_path, name): name, reverse=True, ) + save_path_key = _categories_save_path_key(api_version) check( - lambda: (cat.savePath for cat in client.torrents_categories().values()), + lambda: ( + cat[save_path_key] for cat in client.torrents_categories().values() + ), save_path or "", reverse=True, ) diff --git a/tests/version_map.py b/tests/version_map.py new file mode 100644 index 000000000..2761897c4 --- /dev/null +++ b/tests/version_map.py @@ -0,0 +1,33 @@ +# A map of qBittorrent versions to their respective API versions + +api_version_map = { + "v4.1.0": "2.0", + "v4.1.1": "2.0.1", + "v4.1.2": "2.0.2", + "v4.1.3": "2.1", + "v4.1.4": "2.1.1", + "v4.1.5": "2.2", + "v4.1.6": "2.2", + "v4.1.7": "2.2", + "v4.1.8": "2.2", + "v4.1.9": "2.2.1", + "v4.1.9.1": "2.2.1", + "v4.2.0": "2.3", + "v4.2.1": "2.4", + "v4.2.2": "2.4.1", + "v4.2.3": "2.4.1", + "v4.2.4": "2.5", + "v4.2.5": "2.5.1", + "v4.3.0": "2.6", + "v4.3.0.1": "2.6", + "v4.3.1": "2.6.1", + "v4.3.2": "2.7", + "v4.3.3": "2.7", + "v4.3.4.1": "2.8.1", + "v4.3.5": "2.8.2", + "v4.3.6": "2.8.2", + "v4.3.7": "2.8.2", + "v4.3.8": "2.8.2", + "v4.3.9": "2.8.2", + "v4.4.0": "2.8.4", +}