Skip to content

Commit

Permalink
2.3.1 (#56)
Browse files Browse the repository at this point in the history
* Update README
  * Changed description
  * Added "quit qBittorrent" note
  * Added Docker `BT_backup` path
* Added support for `qBt-downloadPath` (#59 ) (#58 )
* Fixed `bt-backup-path` CLI (#60 ) (#57 )
  • Loading branch information
jslay88 authored Jun 3, 2022
1 parent 703394d commit d8df9a8
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 11 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# qBt Migrate
![Python](https://github.com/jslay88/qbt_migrate/actions/workflows/python.yml/badge.svg)

This tool changes the root paths of existing torrents in qBittorrent.
This tool changes the paths of existing torrents in qBittorrent in a bulk fashion.
It can also convert slashes when migrating between Windows and Linux/Mac.

![Demo](demo.gif)

## Usage
**ALWAYS** ensure qBittorrent is closed before running `qbt_migrate`.
Either quit through `File` -> `Exit`, task tray icon, or task manager for your system.

Install from PyPi using `pip`, or jump to [Examples](#Examples) for Docker

Expand Down Expand Up @@ -38,6 +40,7 @@ Override `BT_backup` path if needed.
Default BT_backup paths:
* Windows: `%LOCALAPPDATA%/qBittorrent/BT_backup`
* Linux/Mac: `$HOME/.local/share/data/qBittorrent/BT_backup`
* Docker: `/config/qBittorrent/BT_backup`

A backup zip archive is automatically created in the `BT_backup` directory.

Expand Down
2 changes: 1 addition & 1 deletion qbt_migrate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
from .methods import convert_slashes, discover_bt_backup_path


__version__ = "2.3.0" + os.getenv("VERSION_TAG", "")
__version__ = "2.3.1" + os.getenv("VERSION_TAG", "")

logging.getLogger(__name__).addHandler(logging.NullHandler())
23 changes: 22 additions & 1 deletion qbt_migrate/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ def save_path(self) -> Optional[str]:
if "save_path" in self._data:
return self._data["save_path"]

@property
def qbt_download_path(self) -> Optional[str]:
if "qBt-downloadPath" in self._data:
return self._data["qBt-downloadPath"]

@property
def qbt_save_path(self) -> Optional[str]:
if "qBt-savePath" in self._data:
Expand All @@ -177,7 +182,7 @@ def set_save_path(
save_file: bool = True,
create_backup: bool = True,
):
if key not in ["save_path", "qBt-savePath"]:
if key not in ["save_path", "qBt-savePath", "qBt-downloadPath"]:
raise KeyError("When setting a save path, key must be `save_path` or `qBt-savePath`. " f"Received {key}")
if create_backup:
self.save(self.backup_filename)
Expand All @@ -192,6 +197,7 @@ def set_save_paths(
self,
path: str,
qbt_path: Optional[str] = None,
qbt_download_path: Optional[str] = None,
target_os: Optional[TargetOS] = None,
save_file: bool = True,
create_backup: bool = True,
Expand All @@ -204,6 +210,14 @@ def set_save_paths(
qbt_path = path if qbt_path is None else qbt_path
if qbt_path:
self.set_save_path(qbt_path, key="qBt-savePath", target_os=target_os, save_file=False, create_backup=False)
if qbt_download_path:
self.set_save_path(
qbt_download_path, key="qBt-downloadPath", target_os=target_os, save_file=False, create_backup=False
)
elif self.qbt_download_path:
self.set_save_path(
self.qbt_save_path, key="qBt-downloadPath", target_os=target_os, save_file=False, create_backup=False
)
if self.mapped_files is not None and target_os is not None:
self.logger.debug("Converting Slashes for mapped_files...")
self._data["mapped_files"] = [convert_slashes(path, target_os) for path in self.mapped_files]
Expand All @@ -229,20 +243,26 @@ def replace_paths(
if regex_path:
new_save_path = None
new_qbt_save_path = None
new_qbt_download_path = None
pattern = re.compile(existing_path)
if self.save_path:
new_save_path = pattern.sub(new_path, self.save_path)
if self.qbt_save_path:
new_qbt_save_path = pattern.sub(new_path, self.qbt_save_path)
if not self.save_path:
new_save_path = new_qbt_save_path
if self.qbt_download_path:
new_qbt_download_path = pattern.sub(new_path, self.qbt_download_path)
if self.mapped_files:
self._data["mapped_files"] = [pattern.sub(new_path, path) for path in self.mapped_files]
else:
new_save_path = self.save_path.replace(existing_path, new_path) if self.save_path is not None else None
new_qbt_save_path = (
self.qbt_save_path.replace(existing_path, new_path) if self.qbt_save_path is not None else None
)
new_qbt_download_path = (
self.qbt_download_path.replace(existing_path, new_path) if self.qbt_download_path is not None else None
)
if not self.save_path:
new_save_path = new_qbt_save_path
if self.mapped_files:
Expand All @@ -253,6 +273,7 @@ def replace_paths(
self.set_save_paths(
path=str(new_save_path),
qbt_path=new_qbt_save_path,
qbt_download_path=new_qbt_download_path,
target_os=target_os,
save_file=save_file,
create_backup=create_backup,
Expand Down
11 changes: 8 additions & 3 deletions qbt_migrate/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import argparse
import logging
import sys
from pathlib import Path

from . import QBTBatchMove, __version__, discover_bt_backup_path
from .enums import TargetOS
Expand Down Expand Up @@ -72,19 +73,23 @@ def main():
return
qbm = QBTBatchMove()
if args.bt_backup_path is not None:
qbm.bt_backup_path = args.bt_backup_path
qbm.bt_backup_path = Path(args.bt_backup_path.strip())
else:
bt_backup_path = input(f"BT_backup Path {qbm.bt_backup_path}: ")
if bt_backup_path.strip():
qbm.bt_backup_path = bt_backup_path
qbm.bt_backup_path = Path(bt_backup_path.strip())
if args.existing_path is None:
args.existing_path = input("Existing Path: ")
if args.new_path is None:
args.new_path = input("New Path: ")

# Get Valid Regex Input
if args.regex is None:
while (answer := input("Regex Paths with Capture Groups [y/N]: ").lower().strip()) not in (
while (
answer := input("Existing and New paths are regex patterns (capture groups recommended)? [y/N]: ")
.lower()
.strip()
) not in (
"y",
"yes",
"n",
Expand Down
15 changes: 14 additions & 1 deletion tests/test_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ def test_fastresume_properties(temp_dir):
assert fast_resume.save_path == fast_resume._data["save_path"]
if "qBt-savePath" in fast_resume._data:
assert fast_resume.qbt_save_path == fast_resume._data["qBt-savePath"]
if "qBt-downloadPath" in fast_resume._data:
assert fast_resume.qbt_download_path == fast_resume._data["qBt-downloadPath"]
assert (
fast_resume.mapped_files == fast_resume._data["mapped_files"]
if "mapped_files" in fast_resume._data
Expand Down Expand Up @@ -228,8 +230,10 @@ def test_fastresume_set_save_path(monkeypatch):
# Explicitly test setting of key
fast_resume.set_save_path("/this/is/a/test", save_file=False, create_backup=False)
fast_resume.set_save_path("/this/is/a/test", key="qBt-savePath", save_file=False, create_backup=False)
fast_resume.set_save_path("/this/is/a/test", key="qBt-downloadPath", save_file=False, create_backup=False)
assert fast_resume.save_path == "/this/is/a/test"
assert fast_resume.qbt_save_path == "/this/is/a/test"
assert fast_resume.qbt_download_path == "/this/is/a/test"

mock.reset()
# Test target_os None
Expand Down Expand Up @@ -273,10 +277,12 @@ def test_fastresume_set_save_paths(monkeypatch):
fast_resume.set_save_paths("/this/is/a/path")
assert fast_resume.save_path == "/this/is/a/path"
assert fast_resume.qbt_save_path == "/this/is/a/path"
assert fast_resume.qbt_download_path == "/this/is/a/path"
# Test with differing qBt-savePath
fast_resume.set_save_paths("/this/is/a/path", "/this/is/a/qbt/path")
fast_resume.set_save_paths("/this/is/a/path", "/this/is/a/qbt/path", "/this/is/a/qbt/download/path")
assert fast_resume.save_path == "/this/is/a/path"
assert fast_resume.qbt_save_path == "/this/is/a/qbt/path"
assert fast_resume.qbt_download_path == "/this/is/a/qbt/download/path"

# Test mapped_files convert slashes (replacing of paths happens with `replace_paths`)
fast_resume.set_save_paths("C:/this/is/a/path", target_os=TargetOS.WINDOWS)
Expand All @@ -290,6 +296,11 @@ def test_fastresume_set_save_paths(monkeypatch):
assert fast_resume.save_path == "/this/is/a/path"
assert fast_resume.qbt_save_path == "/this/is/a/path"

# Test missing qBt-downloadPath key, stays missing
del fast_resume._data["qBt-downloadPath"]
fast_resume.set_save_paths("/this/is/a/path")
assert fast_resume.qbt_download_path is None


def test_fastresume_save(monkeypatch):
fast_resume = FastResume("./tests/test_files/good.fastresume")
Expand Down Expand Up @@ -317,6 +328,7 @@ def test_fastresume_replace_paths(monkeypatch):
fast_resume.replace_paths("/some/test", "/a/new/test", save_file=False, create_backup=False)
assert fast_resume.save_path == "/a/new/test/path"
assert fast_resume.qbt_save_path == "/a/new/test/path"
assert fast_resume.qbt_download_path == "/a/new/test/path"
assert len(fast_resume.mapped_files) > 0
for mapped_file in fast_resume.mapped_files:
assert mapped_file.startswith("/a/new/test/path")
Expand All @@ -327,6 +339,7 @@ def test_fastresume_replace_paths_regex(monkeypatch):
fast_resume.replace_paths(r"/some/(\w+)/.*$", r"/\1/regex", True, save_file=False, create_backup=False)
assert fast_resume.save_path == "/test/regex"
assert fast_resume.qbt_save_path == "/test/regex"
assert fast_resume.qbt_download_path == "/test/regex"
assert len(fast_resume.mapped_files) > 0
for mapped_file in fast_resume.mapped_files:
assert mapped_file.startswith("/test/regex")
Expand Down
7 changes: 4 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
from unittest.mock import patch

import pytest
Expand Down Expand Up @@ -121,7 +122,7 @@ def test_main_with_inputs(monkeypatch):
monkeypatch.setattr("qbt_migrate.cli.QBTBatchMove", MockQBTBatchMove)
monkeypatch.setattr("sys.argv", ["qbt_migrate"])
main()
assert MockQBTBatchMove.instance.bt_backup_path == "bt-backup-path"
assert MockQBTBatchMove.instance.bt_backup_path == Path("bt-backup-path")
assert len(MockQBTBatchMove.run_call[0]) == 6
assert MockQBTBatchMove.run_call[0][0] == "existing-path"
assert MockQBTBatchMove.run_call[0][1] == "new-path"
Expand Down Expand Up @@ -150,7 +151,7 @@ def test_main_with_args(monkeypatch):
],
)
main()
assert MockQBTBatchMove.instance.bt_backup_path == "different-bt-backup-path"
assert MockQBTBatchMove.instance.bt_backup_path == Path("different-bt-backup-path")
assert len(MockQBTBatchMove.run_call[0]) == 6
assert MockQBTBatchMove.run_call[0][0] == "different-existing-path"
assert MockQBTBatchMove.run_call[0][1] == "different-new-path"
Expand All @@ -166,7 +167,7 @@ def test_main_invalid_input_loops(monkeypatch):
mock_user_input = MockUserInput(["not-valid", "yes", "not-valid", "windows"])
monkeypatch.setattr("builtins.input", lambda _: mock_user_input.next())
main()
assert MockQBTBatchMove.instance.bt_backup_path == "backup-path"
assert MockQBTBatchMove.instance.bt_backup_path == Path("backup-path")
assert len(MockQBTBatchMove.run_call[0]) == 6
assert MockQBTBatchMove.run_call[0][0] == "e-path"
assert MockQBTBatchMove.run_call[0][1] == "n-path"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_files/good.fastresume
Original file line number Diff line number Diff line change
@@ -1 +1 @@
d11:active_timei90e10:added_timei1644727394e10:allocation6:sparse15:apply_ip_filteri1e12:auto_managedi0e12:banned_peers6:��dT�13:banned_peers60:14:completed_timei1644727428e11:disable_dhti0e11:disable_lsdi0e11:disable_pexi0e19:download_rate_limiti-1e11:file-format22:libtorrent resume file12:file-versioni1e13:file_priorityli1ee13:finished_timei57e9:httpseedsle9:info-hash20:ɫ�>6yY<ؙ'�R�R��Nn13:last_downloadi204407e18:last_seen_completei1644727466e11:last_uploadi0e18:libtorrent-version8:1.2.14.012:mapped_filesl29:/some/test/path/mapped_file_129:/some/test/path/mapped_file_2e15:max_connectionsi100e11:max_uploadsi4e12:num_completei1043e14:num_downloadedi16777215e14:num_incompletei10e6:pausedi1e5:peers0:6:peers60:6:pieces0:12:qBt-category0:17:qBt-contentLayout8:Original26:qBt-firstLastPiecePriorityi0e8:qBt-name0:14:qBt-ratioLimiti-2000e12:qBt-savePath15:/some/test/path14:qBt-seedStatusi1e20:qBt-seedingTimeLimiti-2e8:qBt-tagsle9:save_path15:/some/test/path9:seed_modei0e12:seeding_timei57e19:sequential_downloadi0e10:share_modei0e15:stop_when_readyi0e13:super_seedingi0e16:total_downloadedi1287360228e14:total_uploadedi0e8:trackersll35:https://torrent.ubuntu.com/announceel40:https://ipv6.torrent.ubuntu.com/announceee11:upload_modei0e17:upload_rate_limiti-1e8:url-listlee
d11:active_timei90e10:added_timei1644727394e10:allocation6:sparse15:apply_ip_filteri1e12:auto_managedi0e12:banned_peers6:��dT�13:banned_peers60:14:completed_timei1644727428e11:disable_dhti0e11:disable_lsdi0e11:disable_pexi0e19:download_rate_limiti-1e11:file-format22:libtorrent resume file12:file-versioni1e13:file_priorityli1ee13:finished_timei57e9:httpseedsle9:info-hash20:ɫ�>6yY<ؙ'�R�R��Nn13:last_downloadi204407e18:last_seen_completei1644727466e11:last_uploadi0e18:libtorrent-version8:1.2.14.012:mapped_filesl29:/some/test/path/mapped_file_129:/some/test/path/mapped_file_2e15:max_connectionsi100e11:max_uploadsi4e12:num_completei1043e14:num_downloadedi16777215e14:num_incompletei10e6:pausedi1e5:peers0:6:peers60:6:pieces0:12:qBt-category0:17:qBt-contentLayout8:Original16:qBt-downloadPath15:/some/test/path26:qBt-firstLastPiecePriorityi0e8:qBt-name0:14:qBt-ratioLimiti-2000e12:qBt-savePath15:/some/test/path14:qBt-seedStatusi1e20:qBt-seedingTimeLimiti-2e8:qBt-tagsle9:save_path15:/some/test/path9:seed_modei0e12:seeding_timei57e19:sequential_downloadi0e10:share_modei0e15:stop_when_readyi0e13:super_seedingi0e16:total_downloadedi1287360228e14:total_uploadedi0e8:trackersll35:https://torrent.ubuntu.com/announceel40:https://ipv6.torrent.ubuntu.com/announceee11:upload_modei0e17:upload_rate_limiti-1e8:url-listlee
Expand Down

0 comments on commit d8df9a8

Please sign in to comment.