diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index da86972..a5681ad 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,6 +5,7 @@ on: push: branches: - master + pull_request: jobs: build: @@ -20,6 +21,7 @@ jobs: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch + type=ref,event=pr - name: Set up QEMU uses: docker/setup-qemu-action@v2 diff --git a/README.md b/README.md index d44ee04..51fdbc8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Upstream -This is a maintained fork of the upstream project at https://gitlab.com/AlexKM/qbittools. +This is an opinionated fork of the upstream project at https://gitlab.com/AlexKM/qbittools. ## Description @@ -11,35 +11,17 @@ qbittools is a feature rich CLI for the management of torrents in qBittorrent. - [Upstream](#upstream) - [Description](#description) - [Table of contents](#table-of-contents) -- [Requirements](#requirements) - [Installation](#installation) - [Docker image](#docker-image) - - [Building binary manually with Docker (optional)](#building-binary-manually-with-docker-optional) - - [Run as a script (optional)](#run-as-a-script-optional) + - [Building](#building) - [Configuration](#configuration) - [Usage](#usage) - [Help](#help) - [Subcommands](#subcommands) - - [Add](#add) - - [Operating system limits](#operating-system-limits) - - [ruTorrent / AutoDL](#rutorrent--autodl) - - [Unpause](#unpause) - - [Automatic unpause in qBittorrent](#automatic-unpause-in-qbittorrent) - [Tagging](#tagging) - - [Automatic tagging with Cron](#automatic-tagging-with-cron) - [Reannounce](#reannounce) - - [Reannounce with systemd](#reannounce-with-systemd) - - [Update passkey](#update-passkey) - - [Export](#export) - - [Mover](#mover) - - [Automatic moving with Cron](#automatic-moving-with-cron) - [Orphaned](#orphaned) -## Requirements - -- Any usable Linux distribution (binary builds are built with musl and fully static starting from 0.4.0) -- ca-certificates (for connecting to https) - ## Installation ### Docker image @@ -50,9 +32,7 @@ Run a container with access to host network: docker run -it --rm --network host github.com/buroa/qbittools tagging --unregistered ``` -### Building binary manually with Docker (optional) - -
Click to expand +### Building ```bash # clone the repository @@ -63,25 +43,6 @@ docker build -t qbittools:latest --pull . docker run -it --rm --network host qbittools reannounce -p 12345 ``` -
- -### Run as a script (optional) - -
Click to expand - -```bash -# clone the repository -git clone https://github.com/buroa/qbittools.git && cd qbittools -# create and activate virtual environment -virtualenv -p python3 venv -source venv/bin/activate -# install dependencies -install -r requirements.txt -# use qbittools.py instead of the binary -``` - -
- ## Configuration qBittools doesn't have any configuration files currently. It parses host, port and username from the qBittorrent configuration file located by default at `~/.config/qBittorrent/qBittorrent.conf`, you can specify a different qBittorrent config with `-C` flag. @@ -94,108 +55,29 @@ You also can specify host, port and username manually without a configuration fi ### Help -All commands have extensive help with all available options: - -
Click to expand +All commands have extensive help with all available options. ```bash $ qbittools export -h -usage: qbittools export [-h] -p 12345 [-s 127.0.0.1] [-U username] [-P password] [-i ~/.local/share/qBittorrent/BT_backup] -o ~/export [-c mycategory] [-t [mytag ...]] +usage: qbittools.py reannounce [-h] [--pause-resume] [--process-seeding] + [-C ~/.config/qBittorrent/qBittorrent.conf] [-p 12345] [-s 127.0.0.1] [-U username] + [-P password] -optional arguments: +options: -h, --help show this help message and exit + --pause-resume Will pause/resume torrents that are invalid. + --process-seeding Will also process seeding torrents for reannouncements. + -C ~/.config/qBittorrent/qBittorrent.conf, --config ~/.config/qBittorrent/qBittorrent.conf -p 12345, --port 12345 port -s 127.0.0.1, --server 127.0.0.1 host -U username, --username username -P password, --password password - -i ~/.local/share/qBittorrent/BT_backup, --input ~/.local/share/qBittorrent/BT_backup - Path to qBittorrent .torrent files - -o ~/export, --output ~/export - Path to where to save exported torrents - -c mycategory, --category mycategory - Filter by category - -t [mytag ...], --tags [mytag ...] - Filter by tags ``` -
- ### Subcommands -#### Add - -
Click to expand - -Add a single torrent with custom category - -```bash -$ qbittools add /path/to/my.torrent -c mycategory -``` - -Add a folder of torrents and assign multiple tags - -```bash -$ qbittools add /path/to/folder -t mytag1 mytag2 -``` - -Add a torrent in paused state and skip hash checking - -```bash -$ qbittools add /path/to/my.torrent --add-paused --skip-checking -``` - -Don't add more torrents if there are more than 3 downloads active while ignoring downloads with speed under 1 MiB/s - -```bash -$ qbittools add /path/to/my.torrent --max-downloads 3 --max-downloads-speed-ignore-limit 1024 -``` - -Pause all active torrents temporarily and mark them with `temp_paused` tag while ignoring active uploads with speed under 10 MiB/s (**You have** to configure unpause command in qBittorrent if you want these torrents to be unpaused automatically) - -```bash -$ qbittools add /path/to/my.torrent --pause-active --pause-active-upspeed-ignore-limit 10240 -``` - -
- -##### Operating system limits - -If you encounter `too many open files` or `no file descriptors available` errors while adding a lot of torrents, you can try to bypass it with simple shell commands, this will add torrents one by one: - -```bash -IFS=$'\n' find /path/to/your/torrents/ -maxdepth 1 -type f -name "*.torrent" -exec qbittools add {} --skip-checking \; -``` - -##### ruTorrent / AutoDL - -Adding torrents from autodl-irssi to qBittorrent using ruTorrent: - -``` -Action = Run Program -Command = /usr/local/bin/qbittools -Arguments = add $(TorrentPathName) -c music -``` - -#### Unpause - -Only useful if you pause torrents automatically with `--pause-active` parameters from add command. - -Resume all torrents with `temp_paused` tag if there are no active downloads while ignoring slow downloads under 10 MiB/s - -```bash -$ qbittools unpause -d 10240 -``` - -##### Automatic unpause in qBittorrent - -Check `Run external program on torrent completion` in the settings and use tool with an absolute path: - -``` -/usr/local/bin/qbittools unpause -d 10240 -``` - #### Tagging Create useful tags to group torrents by tracker domains, not working trackers, unregistered torrents and duplicates @@ -204,19 +86,9 @@ Create useful tags to group torrents by tracker domains, not working trackers, u $ qbittools tagging --duplicates --unregistered --not-working --added-on --trackers ``` -##### Automatic tagging with Cron - -Execute every 10 minutes (`crontab -e` and add this entry) - -``` -*/10 * * * * /usr/local/bin/qbittools tagging --duplicates --unregistered --not-working --added-on --trackers -``` - #### Reannounce -Automatic reannounce on problematic trackers (run in screen/tmux to prevent it from closing when you end a ssh session): - -
Click to expand +Automatic reannounce on problematic trackers ```bash $ qbittools reannounce @@ -232,89 +104,30 @@ $ qbittools reannounce 07:41:35 PM [Movie.2020.2160p.WEB-DL.H264-GROUP] is active, progress: 11.1% ``` -
- -##### Reannounce with systemd - -Reannounce can be executed and restarted on problems automatically by systemd. Create a new service at `/etc/systemd/system/` with the following contents: - -
Click to expand - -```ini -[Unit] -Description=qbittools reannounce -After=qbittorrent@%i.service - -[Service] -User=%i -Group=%i -ExecStart=/usr/local/bin/qbittools reannounce - -Restart=always -RestartSec=3 - -[Install] -WantedBy=multi-user.target -``` - -
- -Restart the daemon with `systemctl daemon-reload` and start the service with `systemctl start qbittools-reannounce@username` by replacing username with the user you want to run it from. Check service logs with `journalctl -u qbittools-reannounce@username.service` if necessary. - -#### Update passkey - -Update passkey in all matching torrents (all tracker urls that match `--old` parameter): - -```bash -$ qbittools update_passkey --old 12345 --new v3rrjmnfxwq3gfrgs9m37dvnfkvdbqnqc -2021-01-08 21:38:45,301 INFO:Replaced [https://trackerurl.net/12345/announce] to [https://trackerurl.net/v3rrjmnfxwq3gfrgs9m37dvnfkvdbqnqc/announce] in 10 torrents -``` - -#### Export - -Export all matching .torrent files by category or tags: - -
Click to expand - -```bash -$ qbittools export -o ./export --category movies --tags tracker.org mytag -01:23:43 PM INFO:Matched 47 torrents -01:23:43 PM INFO:Exported [movies] Fatman.2020.BluRay.1080p.TrueHD.5.1.AVC.REMUX-FraMeSToR [fbef10dc89bf8dff21a401d9304f62b074ffd6af].torrent -01:23:43 PM INFO:Exported [movies] La.Haine.1995.UHD.BluRay.2160p.DTS-HD.MA.5.1.DV.HEVC.REMUX-FraMeSToR [ee5ff82613c7fcd2672e2b60fc64375486f976ba].torrent -01:23:43 PM INFO:Exported [movies] Ip.Man.3.2015.UHD.BluRay.2160p.TrueHD.Atmos.7.1.DV.HEVC.REMUX-FraMeSToR [07da008f9c64fe4927ee18ac5c94292f61098a69].torrent -01:23:43 PM INFO:Exported [movies] Brazil.1985.Director's.Cut.BluRay.1080p.FLAC.2.0.AVC.REMUX-FraMeSToR [988e8749a9d3f07e5d216001efc938b732579c16].torrent -``` - -
- -#### Mover - -Useful for those who want to move torrents to different categories over time. Combined with enabled Automatic Torrent Management this will move files from one folder to another. - -Move torrents inactive for more than 60 seconds and completed more than 60 minutes ago from categories `tracker1` and `tracker2` to category `lts` - -```bash -$ qbittools mover tracker1 tracker2 -d lts -``` - -Move torrents inactive for more than 600 seconds and completed more than 30 minutes ago from category `racing` to category `lts` +#### Orphaned -```bash -$ qbittools mover racing -d lts --completion-threshold 30 --active-threshold 600 -``` +Find files no longer associated with any torrent, but still present in download folders (default download folder and folders from all categories). This command will remove orphaned files if you pass the `--confirm` flag. -##### Automatic moving with Cron +This command is very opinionated on a certian directory structure so use with caution and make sure you run it without the `--confirm` flag to make sure it won't delete anything unintentional. -Execute every 10 minutes (`crontab -e` and add this entry) +This is how I have my paths laid out where `/downloads/qbittorrent/complete` is the default save path in qBittorrent and each folder under it is a category. `Default Torrent Management Mode: Automatic`, `When Torrent Category changed: Relocate`, `When Default Save Path changed: Relocate affected torrents` and `When Category Save Path changed: Relocate affected torrents` is also set. Also make sure you use an incomplete directory that is outside the `/downloads/qbittorrent/complete` directory. ``` -*/10 * * * * /usr/local/bin/qbittools mover racing -d lts +/downloads/qbittorrent +└── complete + ├── cross-seed + ├── hit-and-runs + ├── lidarr + ├── manual + ├── myanonamouse + ├── overlord + ├── prowlarr + ├── radarr + ├── redacted + ├── rifftrax + └── sonarr ``` -#### Orphaned - -Find files no longer associated with any torrent, but still present in download folders (default download folder and folders from all categories). This command will remove orphaned files if you confirm it and also clean up all empty folders. _Be careful while removing a lot of files if you use these folders from other torrent client._ - ```bash -$ qbittools orphaned +$ qbittools orphaned --ignore-pattern "*_unpackerred" --ignore-pattern "*/manual/*" ``` diff --git a/qbittools/commands/add.py b/qbittools/commands/add.py deleted file mode 100755 index a68cc26..0000000 --- a/qbittools/commands/add.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 - -import pathlib -import qbittools -import commands.utils as utils -from humanfriendly import format_size, parse_size - -def __init__(args, logger): - client = qbittools.qbit_client(args) - active = len(list(filter(lambda x: x.dlspeed > args.max_downloads_speed_ignore_limit * 1024 and x.state == 'downloading', client.torrents.info(status_filter="downloading")))) - - if args.max_downloads != 0 and active >= args.max_downloads: - logger.info(f"Reached max active downloads: {active}") - return - - to_add = [] - - if args.max_iowait: - current_iowait = utils.iowait(interval=1) - - if current_iowait > args.max_iowait: - logger.info(f"Max iowait reached: {current_iowait}, stopping") - return - - if args.min_free_space: - parsed_size = parse_size(args.min_free_space, binary=True) - - free = 0 - - if args.category and client.application.preferences.auto_tmm_enabled and args.tmm != False: - category = client.torrent_categories.categories.get(args.category) - - if category and category.savePath != '': - logger.info(f"Checking free space in {category.savePath}") - free = utils.free_space(category.savePath) - else: - free = utils.free_space(qbittools.config.save_path) - elif args.save_path: - logger.info(f"Checking free space in {args.save_path}") - free = utils.free_space(args.save_path) - else: - free = utils.free_space(qbittools.config.save_path) - - if free < parsed_size: - logger.info(f"Minimum free space reached: {format_size(free, binary=True)}, stopping") - return - - for t in args.torrents: - p = pathlib.Path(t).expanduser() - - if p.is_dir(): - contents = list(p.glob('*.torrent')) - to_add += list(map(lambda x: x, contents)) - elif p.is_file(): - to_add.append(p) - - if args.pause_active: - for t in client.torrents.info(status_filter="active"): - if t.state != 'uploading': - continue - - if t.upspeed > args.pause_active_upspeed_ignore_limit * 1024: - t.add_tags(['temp_paused']) - t.pause() - logger.info(f"Paused {t.name} in {t.state} state, upspeed: {t.upspeed / 1024} KiB/s") - continue - - resp = client.torrents_add( - torrent_files=to_add, - save_path=args.save_path, - cookie=args.cookie, - category=args.category, - is_skip_checking=args.skip_checking, - is_paused=args.add_paused, - is_root_folder=args.root_folder, - rename=args.rename, - download_limit=args.dl_limit * 1024, - upload_limit=args.up_limit * 1024, - use_auto_torrent_management=args.tmm, - is_sequential_download=args.sequential, - is_first_last_piece_priority=args.first_last_piece_prio, - tags=','.join(args.tags), - ratio_limit=args.ratio_limit, - seeding_time_limit=args.seeding_time_limit, - content_layout=args.content_layout - ) - - logger.info(f"Adding torrents: {resp}") - -def add_arguments(subparser): - parser = subparser.add_parser('add') - qbittools.add_default_args(parser) - parser.add_argument('torrents', nargs='+', metavar='my.torrent', help='torrents path') - parser.add_argument('-o', '--save-path', metavar='/home/user/downloads', help='Download folder', required=False) - parser.add_argument('--cookie', help='Cookie sent to download the .torrent file', required=False) - parser.add_argument('-c', '--category', metavar='mycategory', help='Category for the torrent', required=False) - parser.add_argument('-t', '--tags', nargs='*', metavar='mytag', default=[], help='Tags for the torrent, split by a whitespace, qBit 4.3.2+', required=False) - parser.add_argument('--skip-checking', action='store_true', help='Skip hash checking') - parser.add_argument('--add-paused', action='store_true', help='Add torrents in the paused state') - parser.add_argument('--content-layout', default=None, choices=["Original", "Subfolder", "NoSubfolder"], help='Control filesystem structure for content') - parser.add_argument('--root-folder', action='store_true', dest='root_folder', help='Create the root folder') - parser.add_argument('--no-root-folder', action='store_false', dest='root_folder', help='Don\'t create the root folder') - parser.add_argument('--rename', help='New name for torrent(s)', required=False) - parser.add_argument('--dl-limit', type=int, help='Download limit in KiB/s', default=0, required=False) - parser.add_argument('--up-limit', type=int, help='Upload limit in KiB/s', default=0, required=False) - parser.add_argument('--tmm', action='store_true', dest='tmm', help='Enable Automatic Torrent Management') - parser.add_argument('--no-tmm', action='store_false', dest='tmm', help='Disable Automatic Torrent Management') - parser.add_argument('--sequential', action='store_true', help='Enable sequential download') - parser.add_argument('--first-last-piece-prio', action='store_true', help='Prioritize download first last piece') - parser.add_argument('--ratio-limit', type=float, help='max ratio limit, qBit 4.3.4+', default=-2, required=False) - parser.add_argument('--seeding-time-limit', type=int, help='seeding time limit in minutes, qBit 4.3.4+', default=-2, required=False) - parser.add_argument('--max-downloads', type=int, help='Max downloads limit', default=0, required=False) - parser.add_argument('--max-downloads-speed-ignore-limit', type=int, help='Doesn\'t count downloads with download speed under specified KiB/s for max limit', default=0, required=False) - parser.add_argument('--pause-active', action='store_true', help='Pause active torrents temporarily') - parser.add_argument('--pause-active-upspeed-ignore-limit', type=int, help='Doesn\'t count active torrents with upload speed under specified KiB/s for pausing', default=0, required=False) - parser.add_argument('--max-iowait', type=int, help='Don\t add a torrent if iowait is higher than specified', required=False) - parser.add_argument('--min-free-space', help='Don\'t add a torrent if save path has less than specified free space', required=False) - parser.set_defaults(tmm=None, root_folder=None) diff --git a/qbittools/commands/export.py b/qbittools/commands/export.py deleted file mode 100755 index eea573f..0000000 --- a/qbittools/commands/export.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 - -import os, shutil, tldextract -from pathlib import Path -import qbittools - -def __init__(args, logger): - client = qbittools.qbit_client(args) - - if args.tags: - matches = list(filter(lambda x: any(y in x.tags for y in args.tags), client.torrents.info(category=args.category))) - torrents = list(map(lambda x: (x.hash, x.name, x.trackers), matches)) - else: - torrents = list(map(lambda x: (x.hash, x.name, x.trackers), client.torrents.info(category=args.category))) - - logger.info(f"Matched {len(torrents)} torrents") - Path(args.output).expanduser().mkdir(parents=True, exist_ok=True) - - for h, name, trackers in torrents: - from_path = Path(args.input, f"{h}.torrent").expanduser() - if not from_path.exists(): - logger.error(f"{os.fsencode(from_path).decode('utf8', 'replace')} doesn't exist!") - continue - - pattern = "" - - tracker_matches = list(filter(lambda x: len(tldextract.extract(x.url).registered_domain) > 0, trackers)) - - if len(tracker_matches) > 0: - domain = tldextract.extract(tracker_matches[0].url).registered_domain - pattern += f"[{domain}]" - - if args.category: - pattern += f" [{args.category}]" - - pattern += f" {os.fsencode(name).decode('utf8', 'replace')} [{h}].torrent" - to_path = Path(args.output, pattern.strip()).expanduser() - - shutil.copy2(from_path, to_path) - logger.info(f"Exported {os.fsencode(to_path).decode('utf8', 'replace')}") - logger.info('Done') - -def add_arguments(subparser): - parser = subparser.add_parser('export') - qbittools.add_default_args(parser) - parser.add_argument('-i', '--input', metavar='~/.local/share/qBittorrent/BT_backup', default='~/.local/share/qBittorrent/BT_backup', help='Path to qBittorrent .torrent files', required=False) - parser.add_argument('-o', '--output', metavar='~/export', help='Path to where to save exported torrents', required=True) - parser.add_argument('-c', '--category', metavar='mycategory', help='Filter by category', required=False) - parser.add_argument('-t', '--tags', nargs='*', metavar='mytag', help='Filter by tags', required=False) diff --git a/qbittools/commands/mover.py b/qbittools/commands/mover.py deleted file mode 100644 index 07caa25..0000000 --- a/qbittools/commands/mover.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 - -import collections -from datetime import datetime -from tqdm import tqdm -import qbittools -import qbittorrentapi -import sys - -def __init__(args, logger): - client = qbittools.qbit_client(args) - - try: - client.torrents_create_category(args.destination) - except qbittorrentapi.exceptions.Conflict409Error as e: - pass - - if not client.application.preferences.auto_tmm_enabled: - logger.warning('Automatic Torrent Management should be enabled in order to change paths while moving torrents to a different category.') - - torrent_hashes = collections.defaultdict(list) - logger.info('Collecting torrents...') - - for t in tqdm(client.torrents.info(filter='completed')): - if t.category in args.source: - completed_diff = datetime.today() - datetime.fromtimestamp(t.completion_on) - last_activity_diff = datetime.today() - datetime.fromtimestamp(t.last_activity) - - if completed_diff.total_seconds() >= args.completion_threshold * 60 and last_activity_diff.total_seconds() >= args.active_threshold: - torrent_hashes[t.category].append(t.hash) - - if len(torrent_hashes) == 0: - logger.info('Nothing to do, exiting') - sys.exit() - - logger.info('Changing categories...') - for cat in tqdm(torrent_hashes): - client.torrents_set_category(args.destination, torrent_hashes=torrent_hashes[cat]) - -def add_arguments(subparser): - parser = subparser.add_parser('mover') - parser.add_argument('source', nargs='+', metavar='category1 category2', help='A list of categories to move to another category') - parser.add_argument('-d', '--destination', metavar='mycategory', help='A category in which all torrents will be moved', required=True) - parser.add_argument('--active-threshold', type=int, help='Move only torrents with last activity more than N seconds ago', default=0, required=False) - parser.add_argument('--completion-threshold', type=int, help='Move only torrents completed more than N minutes ago', default=0, required=False) - qbittools.add_default_args(parser) diff --git a/qbittools/commands/orphaned.py b/qbittools/commands/orphaned.py index 6fc3aa8..d46fccd 100644 --- a/qbittools/commands/orphaned.py +++ b/qbittools/commands/orphaned.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -import qbittools import os import shutil from fnmatch import fnmatch +import qbittools + def __init__(args, logger): client = qbittools.qbit_client(args) completed_dir = str(qbittools.config.save_path) @@ -35,7 +36,7 @@ def __init__(args, logger): logger.info(f"Found {len(qbittorrent_items)} total items in qBittorrent") - logger.info(f"Get a list of all files and folders in the completed directory") + logger.info(f"Getting a list of all files and folders in {completed_dir}/$qbcategory") folders = [folder for folder in os.listdir(completed_dir) if os.path.isdir(os.path.join(completed_dir, folder))] for folder in folders: folder_path = os.path.join(completed_dir, folder) @@ -45,7 +46,7 @@ def __init__(args, logger): if not any(fnmatch(item, pattern) or fnmatch(item_path, pattern) for pattern in ignore_patterns): if item_path not in qbittorrent_items: if not args.confirm: - logger.info(f"Skipping deletion of {item_path}") + logger.info(f"Deleting item {item_path}") else: try: if os.path.isfile(item_path): diff --git a/qbittools/commands/reannounce.py b/qbittools/commands/reannounce.py index 95a53b5..1b1be4b 100755 --- a/qbittools/commands/reannounce.py +++ b/qbittools/commands/reannounce.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import time + import qbittools def __init__(args, logger): diff --git a/qbittools/commands/tagging.py b/qbittools/commands/tagging.py index c7ccb8f..df4ae14 100755 --- a/qbittools/commands/tagging.py +++ b/qbittools/commands/tagging.py @@ -4,10 +4,11 @@ from datetime import datetime import tldextract from tqdm import tqdm -import qbittools import qbittorrentapi import commands.utils as utils +import qbittools + def __init__(args, logger): client = qbittools.qbit_client(args) diff --git a/qbittools/commands/unpause.py b/qbittools/commands/unpause.py deleted file mode 100755 index 6ba0f78..0000000 --- a/qbittools/commands/unpause.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 - -import qbittools - -def __init__(args, logger): - client = qbittools.qbit_client(args) - - torrents = client.torrents.info(status_filter="downloading") - active = len(list(filter(lambda x: x.state == 'downloading' and x.dlspeed > args.dl_ignore_limit * 1024, torrents))) - - if active > 0: - logger.error(f"There are still {active} active downloads") - return - - for t in client.torrents.info(): - if 'temp_paused' in t.tags and (t.state == 'pausedUP' or t.state == 'pausedDL'): - t.resume() - t.remove_tags(['temp_paused']) - logger.info(f"Resumed {t.name}") - - client.torrents_delete_tags(tags=['temp_paused']) - -def add_arguments(subparser): - parser = subparser.add_parser('unpause') - parser.add_argument('-d', '--dl-ignore-limit', type=int, help='Doesn\'t count active torrents with download speed under specified KiB/s for unpausing', default=0, required=False) - qbittools.add_default_args(parser) diff --git a/qbittools/commands/update_passkey.py b/qbittools/commands/update_passkey.py deleted file mode 100755 index 6224f46..0000000 --- a/qbittools/commands/update_passkey.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 - -import collections -import qbittools - -def __init__(args, logger): - client = qbittools.qbit_client(args) - - trackers = collections.defaultdict(int) - - for t in client.torrents.info(): - matches = list(map(lambda x: x.url, filter(lambda s: args.old in s.url, t.trackers))) - found = len(matches) > 0 - - if not found: continue - - for url in matches: - trackers[url] += 1 - if not args.dry_run: t.edit_tracker(url, url.replace(args.old, args.new)) - - for url in trackers: - if args.dry_run: - logger.info(f"Would replace [{url}] to [{url.replace(args.old, args.new)}] in {trackers[url]} torrents") - else: - logger.info(f"Replaced [{url}] to [{url.replace(args.old, args.new)}] in {trackers[url]} torrents") - - if len(trackers) == 0: logger.error(f"Not found any torrents matching {args.old} passkey") - -def add_arguments(subparser): - parser = subparser.add_parser('update_passkey') - parser.add_argument('--old', metavar='oldpasskey', help='Old passkey', required=True) - parser.add_argument('--new', metavar='newpasskey', help='New passkey', required=True) - parser.add_argument('-d', '--dry-run', action='store_true') - qbittools.add_default_args(parser) diff --git a/qbittools/commands/utils.py b/qbittools/commands/utils.py index e18d390..7603416 100644 --- a/qbittools/commands/utils.py +++ b/qbittools/commands/utils.py @@ -1,6 +1,3 @@ -import os, time, stat -import pathlib3x as pathlib - def format_bytes(size): power = 2**10 n = 0 @@ -10,43 +7,3 @@ def format_bytes(size): n += 1 formatted = round(size, 2) return f"{formatted} {power_labels[n]}" - -def free_space(path): - st = os.statvfs(path) - return st.f_bavail * st.f_frsize - -def iowait(interval): - tick = os.sysconf(os.sysconf_names['SC_CLK_TCK']) - numcpu = os.cpu_count() - readstats = open('/proc/stat') - procstats = readstats.readlines()[0].split() - user, nice, sys, idle, iowait, irq, sirq = ( float(procstats[1]), float(procstats[2]), - float(procstats[3]), float(procstats[4]), - float(procstats[5]), float(procstats[6]), - float(procstats[7]) ) - readstats.close() - time.sleep(interval) - readstats = open('/proc/stat') - procstats = readstats.readlines()[0].split() - userd, niced, sysd, idled, iowaitd, irqd, sirqd = ( float(procstats[1]), float(procstats[2]), - float(procstats[3]), float(procstats[4]), - float(procstats[5]), float(procstats[6]), - float(procstats[7]) ) - readstats.close() - iowait = '{0:.1f}'.format(((iowaitd - iowait)* 100 / tick ) / numcpu / interval) - - return float(iowait) - -def is_linked(path): - path = pathlib.Path(path) - - if os.path.islink(path): - return True - - if os.path.isfile(path) and os.lstat(path).st_nlink > 1: - return True - - if os.path.isdir(path): - linked = [os.path.join(path, x) for path, subdirs, files in os.walk(path) for x in files if os.lstat(os.path.join(path, x)).st_nlink > 1 or os.path.islink(os.path.join(path, x))] - - return len(linked) > 0 diff --git a/qbittools/qbittools.py b/qbittools/qbittools.py index 5668689..117cecc 100755 --- a/qbittools/qbittools.py +++ b/qbittools/qbittools.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import argparse, logging, sys, pkgutil, collections, os, configparser +import argparse, logging, sys, os, configparser import ipaddress from typing import NamedTuple import pathlib3x as pathlib @@ -9,8 +9,7 @@ os.environ["PYOXIDIZER"] = "1" import qbittorrentapi -import commands.add, commands.export, commands.reannounce, commands.update_passkey, commands.tagging, commands.unpause, commands.mover, commands.orphaned - +import commands.orphaned, commands.reannounce, commands.tagging class QbitConfig(NamedTuple): host: str @@ -18,27 +17,16 @@ class QbitConfig(NamedTuple): username: str save_path: pathlib.Path - logger = logging.getLogger(__name__) config = None - def add_default_args(parser): - parser.add_argument( - "-C", - "--config", - metavar="~/.config/qBittorrent/qBittorrent.conf", - default="~/.config/qBittorrent/qBittorrent.conf", - required=False, - ) + parser.add_argument("-C", "--config", metavar="~/.config/qBittorrent/qBittorrent.conf", default="~/.config/qBittorrent/qBittorrent.conf", required=False) + parser.add_argument("-s", "--server", metavar="127.0.0.1", help="host", required=False) parser.add_argument("-p", "--port", metavar="12345", help="port", required=False) - parser.add_argument( - "-s", "--server", metavar="127.0.0.1", help="host", required=False - ) parser.add_argument("-U", "--username", metavar="username", required=False) parser.add_argument("-P", "--password", metavar="password", required=False) - def qbit_client(args): global config @@ -82,7 +70,6 @@ def qbit_client(args): logger.error(e) return client - def config_values(path): config = configparser.ConfigParser() config.read(pathlib.Path(path).expanduser()) @@ -109,11 +96,10 @@ def config_values(path): return QbitConfig(host, port, user, save_path) - def main(): global config - logging.getLogger("filelock").setLevel(logging.ERROR) # supress lock messages + logging.getLogger("filelock").setLevel(logging.ERROR) # supress lock messages logging.basicConfig( stream=sys.stdout, level=logging.INFO, @@ -124,16 +110,7 @@ def main(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") - for cmd in [ - "add", - "export", - "reannounce", - "update_passkey", - "tagging", - "unpause", - "mover", - "orphaned", - ]: + for cmd in ["reannounce", "tagging", "orphaned"]: mod = getattr(globals()["commands"], cmd) getattr(mod, "add_arguments")(subparsers) @@ -146,6 +123,5 @@ def main(): mod = getattr(globals()["commands"], args.command) cmd = getattr(mod, "__init__")(args, logger) - if __name__ == "__main__": main()