library 3.2.4__tar.gz → 3.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {library-3.2.4 → library-3.2.5}/.github/README.md +12 -2
- {library-3.2.4 → library-3.2.5}/PKG-INFO +13 -3
- {library-3.2.4 → library-3.2.5}/library/__main__.py +3 -2
- {library-3.2.4 → library-3.2.5}/library/fsdb/disk_usage.py +11 -6
- {library-3.2.4 → library-3.2.5}/library/mediafiles/torrents_start.py +77 -13
- {library-3.2.4 → library-3.2.5}/library/usage.py +11 -1
- {library-3.2.4 → library-3.2.5}/library/utils/arggroups.py +6 -0
- {library-3.2.4 → library-3.2.5}/library/utils/filter_engine.py +2 -2
- {library-3.2.4 → library-3.2.5}/library/utils/processes.py +4 -4
- {library-3.2.4 → library-3.2.5}/library/utils/web.py +74 -13
- {library-3.2.4 → library-3.2.5}/pyproject.toml +1 -1
- {library-3.2.4 → library-3.2.5}/LICENSE +0 -0
- {library-3.2.4 → library-3.2.5}/library/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/assets/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/assets/calibre.css +0 -0
- {library-3.2.4 → library-3.2.5}/library/assets/kotobago.png +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/av.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/computer_info.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/computers_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/fs_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/fs_add_metadata.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/gallery_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/gallery_backend.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/getty_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/hn_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/links_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/nicotine_import.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/places_import.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/reddit_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/row_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/site_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/substack.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/subtitle.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/tables_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/tabs_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/tildes.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/torrents_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/tube_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/tube_backend.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/createdb/web_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/data/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/data/dictionary.pkl +0 -0
- {library-3.2.4 → library-3.2.5}/library/data/ffmpeg_errors.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/data/http_errors.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/data/imagemagick_errors.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/data/unar_errors.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/data/wordbank.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/data/yt_dlp_errors.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/editdb/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/editdb/dedupe_db.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/editdb/dedupe_media.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/editdb/merge_online_local.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/editdb/mpv_watchlater.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/editdb/pushshift.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/editdb/reddit_selftext.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/files/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/files/christen.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/files/sample_compare.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/files/sample_hash.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/files/similar_files.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/folders/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/folders/big_dirs.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/folders/filter_src.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/folders/merge_mv.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/folders/mergerfs_cp.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/folders/mount_stats.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/folders/move_list.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/folders/scatter.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/folders/similar_folders.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/fsdb/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/fsdb/filesystem.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/fsdb/folder_stats.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/fsdb/search_db.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/lb.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/block.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/db_history.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/db_media.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/db_playlists.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/download.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/download_status.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/history.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/history_add.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/optimize_db.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/playlists.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/redownload.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/search.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediadb/stats.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/images_to_pdf.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/media_check.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/pdf_edit.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/process_ffmpeg.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/process_image.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/process_media.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/process_text.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/torrents_dump.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/mediafiles/unardel.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/misc/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/misc/dedupe_czkawka.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/misc/export_text.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/misc/search_help.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/multidb/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/multidb/allocate_torrents.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/multidb/copy_play_counts.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/multidb/merge_dbs.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/links_open.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/media_player.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/media_printer.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/play_actions.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/playback_control.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/post_actions.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/surf.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/tabs_open.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/torrents_info.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/playback/torrents_remaining.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/readme.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/tablefiles/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/tablefiles/columns.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/tablefiles/eda.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/tablefiles/incremental_diff.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/tablefiles/markdown_tables.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/tablefiles/mcda.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/tablefiles/plot.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/cluster_sort.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/combinations.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/expand_links.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/extract_links.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/extract_text.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/json_keys_rename.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/markdown_links.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/nouns.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/regex_sort.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/text/timestamps.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/__init__.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/arg_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/argparse_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/consts.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/date_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/db_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/devices.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/file_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/gui.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/iterables.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/log_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/mpv_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/nums.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/objects.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/path_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/pd_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/printing.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/remote_processes.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/shell_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/sql_utils.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/sqlgroups.py +0 -0
- {library-3.2.4 → library-3.2.5}/library/utils/strings.py +0 -0
|
@@ -99,7 +99,7 @@ To stop playing press Ctrl+C in either the terminal or mpv
|
|
|
99
99
|
<details><summary>List all subcommands</summary>
|
|
100
100
|
|
|
101
101
|
$ library
|
|
102
|
-
library (v3.2.
|
|
102
|
+
library (v3.2.005; 102 subcommands)
|
|
103
103
|
|
|
104
104
|
Create database subcommands:
|
|
105
105
|
╭─────────────────┬──────────────────────────────────────────╮
|
|
@@ -2190,10 +2190,20 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
|
|
|
2190
2190
|
<details><summary>Start torrents (qBittorrent-nox)</summary>
|
|
2191
2191
|
|
|
2192
2192
|
$ library torrents-start -h
|
|
2193
|
-
usage: library torrents-start [--prefix /mnt/d/] PATH ...
|
|
2193
|
+
usage: library torrents-start [--prefix /mnt/d/] [--scan] PATH ...
|
|
2194
2194
|
|
|
2195
2195
|
Start torrent files in qBittorrent-nox
|
|
2196
2196
|
|
|
2197
|
+
By default the temp/download paths are derived from the configured drives and
|
|
2198
|
+
prefixes (and optional tracker subdirectories). With --scan, the temp and download
|
|
2199
|
+
drive prefixes are searched to locate each torrent's files on disk. If
|
|
2200
|
+
--tracker-dirnames is also set, the tracker subdirectories of each prefix are
|
|
2201
|
+
searched as well. Files found under the temp/downloading prefix are used as the
|
|
2202
|
+
download_path (temp), and files under the download/seeding prefix as the save_path,
|
|
2203
|
+
so qBittorrent hash-scans and moves any incomplete data into place. Torrents with
|
|
2204
|
+
no file matches are logged, skipped, and their .torrent files are left in place.
|
|
2205
|
+
--scan overrides the --tracker-dirnames layout when choosing save paths.
|
|
2206
|
+
|
|
2197
2207
|
|
|
2198
2208
|
</details>
|
|
2199
2209
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: library
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.5
|
|
4
4
|
Summary: xk media library
|
|
5
5
|
Author-Email: Jacob Chapman <7908073+chapmanjacobd@users.noreply.github.com>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -207,7 +207,7 @@ To stop playing press Ctrl+C in either the terminal or mpv
|
|
|
207
207
|
<details><summary>List all subcommands</summary>
|
|
208
208
|
|
|
209
209
|
$ library
|
|
210
|
-
library (v3.2.
|
|
210
|
+
library (v3.2.005; 102 subcommands)
|
|
211
211
|
|
|
212
212
|
Create database subcommands:
|
|
213
213
|
╭─────────────────┬──────────────────────────────────────────╮
|
|
@@ -2298,10 +2298,20 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
|
|
|
2298
2298
|
<details><summary>Start torrents (qBittorrent-nox)</summary>
|
|
2299
2299
|
|
|
2300
2300
|
$ library torrents-start -h
|
|
2301
|
-
usage: library torrents-start [--prefix /mnt/d/] PATH ...
|
|
2301
|
+
usage: library torrents-start [--prefix /mnt/d/] [--scan] PATH ...
|
|
2302
2302
|
|
|
2303
2303
|
Start torrent files in qBittorrent-nox
|
|
2304
2304
|
|
|
2305
|
+
By default the temp/download paths are derived from the configured drives and
|
|
2306
|
+
prefixes (and optional tracker subdirectories). With --scan, the temp and download
|
|
2307
|
+
drive prefixes are searched to locate each torrent's files on disk. If
|
|
2308
|
+
--tracker-dirnames is also set, the tracker subdirectories of each prefix are
|
|
2309
|
+
searched as well. Files found under the temp/downloading prefix are used as the
|
|
2310
|
+
download_path (temp), and files under the download/seeding prefix as the save_path,
|
|
2311
|
+
so qBittorrent hash-scans and moves any incomplete data into place. Torrents with
|
|
2312
|
+
no file matches are logged, skipped, and their .torrent files are left in place.
|
|
2313
|
+
--scan overrides the --tracker-dirnames layout when choosing save paths.
|
|
2314
|
+
|
|
2305
2315
|
|
|
2306
2316
|
</details>
|
|
2307
2317
|
|
|
@@ -5,7 +5,7 @@ from tabulate import tabulate
|
|
|
5
5
|
from library.utils import argparse_utils, iterables
|
|
6
6
|
from library.utils.log_utils import log
|
|
7
7
|
|
|
8
|
-
__version__ = "3.2.
|
|
8
|
+
__version__ = "3.2.005"
|
|
9
9
|
|
|
10
10
|
progs = {
|
|
11
11
|
"Create database subcommands": {
|
|
@@ -367,12 +367,13 @@ def library(args=None) -> None:
|
|
|
367
367
|
log.debug(sys.argv)
|
|
368
368
|
|
|
369
369
|
if hasattr(args, "func"):
|
|
370
|
+
subcommand = sys.argv[1] if len(sys.argv) >= 2 else ""
|
|
370
371
|
if len(sys.argv) >= 2:
|
|
371
372
|
del sys.argv[1]
|
|
372
373
|
try:
|
|
373
374
|
return args.func()
|
|
374
375
|
except Exception:
|
|
375
|
-
print("Bug found in", " ".join(sys.argv), file=sys.stderr)
|
|
376
|
+
print("Bug found in", subcommand, " ".join(sys.argv), file=sys.stderr)
|
|
376
377
|
raise
|
|
377
378
|
else:
|
|
378
379
|
try:
|
|
@@ -50,11 +50,12 @@ def parse_args(defaults_override=None):
|
|
|
50
50
|
return args
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def sort_by(args):
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
def sort_by(args, default_sort="priority"):
|
|
54
|
+
sort_groups_by = args.sort_groups_by or default_sort
|
|
55
|
+
if sort_groups_by:
|
|
56
|
+
keys = [key.replace(" desc", "") for key in sort_groups_by.split(",")]
|
|
57
|
+
if keys != ["priority"]:
|
|
58
|
+
return lambda x: (*[x.get(key) or 0 for key in keys], objects.Reverser(x.get("path")))
|
|
58
59
|
|
|
59
60
|
# priority sort
|
|
60
61
|
return lambda x: (
|
|
@@ -170,7 +171,11 @@ def get_subset_group_by_extensions(args) -> list[dict]:
|
|
|
170
171
|
if args.sort_groups_by and " desc" in args.sort_groups_by:
|
|
171
172
|
reverse = False
|
|
172
173
|
|
|
173
|
-
return sorted(
|
|
174
|
+
return sorted(
|
|
175
|
+
[{"path": k, **v} for k, v in d.items()],
|
|
176
|
+
key=sort_by(args, default_sort="size,count"),
|
|
177
|
+
reverse=reverse,
|
|
178
|
+
)
|
|
174
179
|
|
|
175
180
|
|
|
176
181
|
def get_subset_group_by_mimetypes(args) -> list[dict]:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import getpass, shutil, time
|
|
1
|
+
import getpass, os, shutil, time
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from time import sleep
|
|
4
4
|
|
|
@@ -102,28 +102,92 @@ def start_qBittorrent(args):
|
|
|
102
102
|
return qbt_client
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def scan_torrent_location(torrent, roots, max_depth=3):
|
|
106
|
+
if torrent.num_files() == 0:
|
|
107
|
+
return None, None
|
|
108
|
+
first_file = Path(next(iter(torrent.files())).path)
|
|
109
|
+
for root in roots:
|
|
110
|
+
root = Path(root)
|
|
111
|
+
if not root.is_dir():
|
|
112
|
+
continue
|
|
113
|
+
for dirpath, dirnames, _filenames in os.walk(root):
|
|
114
|
+
depth = len(Path(dirpath).relative_to(root).parts)
|
|
115
|
+
if depth > max_depth:
|
|
116
|
+
dirnames[:] = []
|
|
117
|
+
continue
|
|
118
|
+
if (Path(dirpath) / first_file).exists():
|
|
119
|
+
return root, Path(dirpath)
|
|
120
|
+
return None, None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def torrent_paths(args, torrent, temp_prefix, download_prefix):
|
|
124
|
+
if args.scan:
|
|
125
|
+
roots = [temp_prefix, download_prefix]
|
|
126
|
+
if args.tracker_dirnames:
|
|
127
|
+
tracker = get_tracker_domain(torrent)
|
|
128
|
+
if tracker:
|
|
129
|
+
roots += [temp_prefix / tracker, download_prefix / tracker]
|
|
130
|
+
root, location = scan_torrent_location(torrent, roots)
|
|
131
|
+
if not location:
|
|
132
|
+
return None, None
|
|
133
|
+
log.debug("Found files for %s at %s (root %s)", torrent.name(), location, root)
|
|
134
|
+
|
|
135
|
+
if root == temp_prefix or temp_prefix in root.parents:
|
|
136
|
+
# files live under the temp/downloading prefix: keep them as the temp path,
|
|
137
|
+
# and let the download prefix be the final save path
|
|
138
|
+
return download_prefix, location
|
|
139
|
+
# files live under the download/seeding prefix: use them as the final save path,
|
|
140
|
+
# and the temp prefix as where any incomplete data would go
|
|
141
|
+
return location, temp_prefix
|
|
142
|
+
|
|
143
|
+
download_path = download_prefix
|
|
144
|
+
temp_path = temp_prefix
|
|
145
|
+
if args.tracker_dirnames:
|
|
146
|
+
tracker = get_tracker_domain(torrent)
|
|
147
|
+
if tracker:
|
|
148
|
+
download_path /= tracker
|
|
149
|
+
temp_path /= tracker
|
|
150
|
+
return download_path, temp_path
|
|
151
|
+
|
|
152
|
+
|
|
105
153
|
def torrents_start():
|
|
106
154
|
args = parse_args()
|
|
107
155
|
|
|
108
156
|
qbt_client = start_qBittorrent(args)
|
|
109
157
|
|
|
110
158
|
if args.temp_drive and Path(args.temp_drive).is_absolute():
|
|
111
|
-
|
|
159
|
+
temp_drive = Path(args.temp_drive)
|
|
112
160
|
else:
|
|
113
|
-
|
|
114
|
-
temp_prefix /= args.temp_prefix
|
|
115
|
-
download_prefix = Path(args.download_drive) / args.download_prefix
|
|
161
|
+
temp_drive = Path(args.download_drive)
|
|
116
162
|
|
|
117
|
-
|
|
163
|
+
if "download_drive" in args.defaults and args.temp_drive:
|
|
164
|
+
# --temp-drive set but not --download-drive: use the same drive for both prefixes
|
|
165
|
+
download_drive = temp_drive
|
|
166
|
+
else:
|
|
167
|
+
download_drive = Path(args.download_drive)
|
|
168
|
+
temp_prefix = temp_drive / args.temp_prefix
|
|
169
|
+
download_prefix = download_drive / args.download_prefix
|
|
170
|
+
|
|
171
|
+
paths = []
|
|
172
|
+
for p in args.paths:
|
|
173
|
+
p = Path(p)
|
|
174
|
+
if p.is_dir():
|
|
175
|
+
paths.extend(p.glob("*.torrent"))
|
|
176
|
+
else:
|
|
177
|
+
paths.append(p)
|
|
178
|
+
|
|
179
|
+
for path in paths:
|
|
118
180
|
torrent = torrent_decode(path)
|
|
119
181
|
|
|
120
|
-
download_path = download_prefix
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
182
|
+
download_path, temp_path = torrent_paths(args, torrent, temp_prefix, download_prefix)
|
|
183
|
+
if download_path is None:
|
|
184
|
+
log.warning(
|
|
185
|
+
"Skipping %s: no file matches under %s or %s (torrent file left in place)",
|
|
186
|
+
path,
|
|
187
|
+
temp_prefix,
|
|
188
|
+
download_prefix,
|
|
189
|
+
)
|
|
190
|
+
continue
|
|
127
191
|
|
|
128
192
|
qbt_client.torrents_add(
|
|
129
193
|
torrent_files=path,
|
|
@@ -1961,9 +1961,19 @@ torrents_add = """library torrents-add DATABASE PATH ...
|
|
|
1961
1961
|
"""
|
|
1962
1962
|
|
|
1963
1963
|
|
|
1964
|
-
torrents_start = """library torrents-start [--prefix /mnt/d/] PATH ...
|
|
1964
|
+
torrents_start = """library torrents-start [--prefix /mnt/d/] [--scan] PATH ...
|
|
1965
1965
|
|
|
1966
1966
|
Start torrent files in qBittorrent-nox
|
|
1967
|
+
|
|
1968
|
+
By default the temp/download paths are derived from the configured drives and
|
|
1969
|
+
prefixes (and optional tracker subdirectories). With --scan, the temp and download
|
|
1970
|
+
drive prefixes are searched to locate each torrent's files on disk. If
|
|
1971
|
+
--tracker-dirnames is also set, the tracker subdirectories of each prefix are
|
|
1972
|
+
searched as well. Files found under the temp/downloading prefix are used as the
|
|
1973
|
+
download_path (temp), and files under the download/seeding prefix as the save_path,
|
|
1974
|
+
so qBittorrent hash-scans and moves any incomplete data into place. Torrents with
|
|
1975
|
+
no file matches are logged, skipped, and their .torrent files are left in place.
|
|
1976
|
+
--scan overrides the --tracker-dirnames layout when choosing save paths.
|
|
1967
1977
|
"""
|
|
1968
1978
|
|
|
1969
1979
|
torrents_dump = """library torrents-dump PATH ...
|
|
@@ -2717,6 +2717,12 @@ def torrents_start(parser):
|
|
|
2717
2717
|
parser.add_argument(
|
|
2718
2718
|
"--delete-torrent", action=argparse.BooleanOptionalAction, default=True, help="Delete torrent file after adding"
|
|
2719
2719
|
)
|
|
2720
|
+
parser.add_argument(
|
|
2721
|
+
"--scan",
|
|
2722
|
+
action="store_true",
|
|
2723
|
+
help="Scan the temp/download drives (and their prefixes) to find where each torrent's files are, "
|
|
2724
|
+
"and set both the temp and download paths to that location (overrides --tracker-dirnames)",
|
|
2725
|
+
)
|
|
2720
2726
|
|
|
2721
2727
|
|
|
2722
2728
|
def qBittorrent_torrents(parent_parser):
|
|
@@ -427,10 +427,10 @@ def filter_items_by_criteria(args, items):
|
|
|
427
427
|
|
|
428
428
|
if getattr(args, "time_created", []):
|
|
429
429
|
items = [d if "time_created" in d else file_utils.get_file_stats(d) for d in items]
|
|
430
|
-
items = [d for d in items if d
|
|
430
|
+
items = [d for d in items if d.get("time_created", 0) > 0 and args.time_created(d["time_created"])] # type: ignore
|
|
431
431
|
if getattr(args, "time_modified", []):
|
|
432
432
|
items = [d if "time_modified" in d else file_utils.get_file_stats(d) for d in items]
|
|
433
|
-
items = [d for d in items if d
|
|
433
|
+
items = [d for d in items if d.get("time_modified", 0) > 0 and args.time_modified(d["time_modified"])] # type: ignore
|
|
434
434
|
|
|
435
435
|
if getattr(args, "to_json", False):
|
|
436
436
|
items = [d if "size" in d else file_utils.get_file_stats(d) for d in items]
|
|
@@ -246,16 +246,16 @@ def cmd_detach(*command, **kwargs) -> subprocess.CompletedProcess:
|
|
|
246
246
|
|
|
247
247
|
|
|
248
248
|
def cmd_interactive(*command, strict=True) -> subprocess.CompletedProcess:
|
|
249
|
-
|
|
249
|
+
result = subprocess.run(command, check=False)
|
|
250
250
|
|
|
251
|
-
if
|
|
252
|
-
msg = f"[{shlex.join(command)}] exited {
|
|
251
|
+
if result.returncode != 0:
|
|
252
|
+
msg = f"[{shlex.join(command)}] exited {result.returncode}"
|
|
253
253
|
if strict:
|
|
254
254
|
raise RuntimeError(msg)
|
|
255
255
|
else:
|
|
256
256
|
log.info(msg)
|
|
257
257
|
|
|
258
|
-
return
|
|
258
|
+
return result
|
|
259
259
|
|
|
260
260
|
|
|
261
261
|
def Pclose(process) -> subprocess.CompletedProcess: # noqa: N802
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import argparse, datetime, functools, http.cookiejar, os, pathlib, random, re, socket, tempfile, time, urllib.error, urllib.parse, urllib.request, warnings
|
|
1
|
+
import argparse, datetime, functools, http.cookiejar, os, pathlib, random, re, shutil, socket, tempfile, time, urllib.error, urllib.parse, urllib.request, warnings
|
|
2
2
|
from contextlib import suppress
|
|
3
3
|
from email.message import Message
|
|
4
4
|
from pathlib import Path
|
|
@@ -322,6 +322,35 @@ def _firefox_default_profile(root):
|
|
|
322
322
|
return next((p for p in (profiles.get(s, "Path", fallback=None) for s in sections) if p), None)
|
|
323
323
|
|
|
324
324
|
|
|
325
|
+
def copy_firefox_profile_for_selenium(source_profile, selenium_profile):
|
|
326
|
+
selenium_profile.mkdir(parents=True, exist_ok=True)
|
|
327
|
+
source_cookies = source_profile / "cookies.sqlite"
|
|
328
|
+
if source_cookies.exists():
|
|
329
|
+
for suffix in ("", "-wal", "-shm"):
|
|
330
|
+
src_file = source_cookies.with_suffix(source_cookies.suffix + suffix)
|
|
331
|
+
if src_file.exists():
|
|
332
|
+
shutil.copy2(src_file, selenium_profile / src_file.name)
|
|
333
|
+
for name in ("logins.json", "key4.db", "cert9.db", "permissions.sqlite"):
|
|
334
|
+
src_file = source_profile / name
|
|
335
|
+
if src_file.exists():
|
|
336
|
+
shutil.copy2(src_file, selenium_profile / name)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
SKIP_CHROMIUM_DIRS = {"Cache", "Code Cache", "GPUCache", "Service Worker", "Service Worker/CacheStorage", "Session Storage", "Sessions", "databases", "blob_storage", "File System", "IndexedDB", "WebStorage", "GamepadMappings", "safe_browsing", "SafetyTips", "Subresource Filter", "BudgetDatabase", "CELPuzzles", "Crowd Deny", "MEIPreload", "Segmentation Platform", "TranslateRanker", "TranslateScripts", "VideoDecodeStats"}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def copy_chromium_profile_for_selenium(source_profile, selenium_profile):
|
|
343
|
+
source_profile = Path(source_profile)
|
|
344
|
+
selenium_profile = Path(selenium_profile)
|
|
345
|
+
for item in source_profile.iterdir():
|
|
346
|
+
if item.name in SKIP_CHROMIUM_DIRS or item.name in {"LOCK", "LOG", "LOG.old"} or item.name.startswith(".org.chromium."):
|
|
347
|
+
continue
|
|
348
|
+
if item.is_dir():
|
|
349
|
+
shutil.copytree(item, selenium_profile / item.name, ignore=shutil.ignore_patterns("*.lock", "LOCK"))
|
|
350
|
+
else:
|
|
351
|
+
shutil.copy2(item, selenium_profile / item.name)
|
|
352
|
+
|
|
353
|
+
|
|
325
354
|
def load_selenium(args, wire=False):
|
|
326
355
|
if getattr(args, "driver", False):
|
|
327
356
|
return
|
|
@@ -334,6 +363,7 @@ def load_selenium(args, wire=False):
|
|
|
334
363
|
resolved = get_browser_profile_path(browser_name, profile)
|
|
335
364
|
if resolved:
|
|
336
365
|
args.user_data_dir = resolved
|
|
366
|
+
args.profile_directory = profile
|
|
337
367
|
_profile = profile
|
|
338
368
|
log.info("Using browser profile: %s", resolved)
|
|
339
369
|
if browser_name == "firefox" and not getattr(args, "chrome", False):
|
|
@@ -370,6 +400,16 @@ def load_selenium(args, wire=False):
|
|
|
370
400
|
|
|
371
401
|
service = Service(log_path=tempfile.mktemp(".geckodriver.log"))
|
|
372
402
|
options = Options()
|
|
403
|
+
|
|
404
|
+
_using_real_profile = bool(args.user_data_dir)
|
|
405
|
+
if _using_real_profile:
|
|
406
|
+
_real_profile = Path(args.user_data_dir)
|
|
407
|
+
_selenium_profile = Path(tempfile.mkdtemp(prefix="ff_selenium_")) / "profile"
|
|
408
|
+
copy_firefox_profile_for_selenium(_real_profile, _selenium_profile)
|
|
409
|
+
args.selenium_profile_dir = _selenium_profile
|
|
410
|
+
args.user_data_dir = str(_selenium_profile)
|
|
411
|
+
log.info("Selenium profile: %s (cookies from %s)", _selenium_profile, _real_profile)
|
|
412
|
+
|
|
373
413
|
if args.user_data_dir:
|
|
374
414
|
options.add_argument("--profile")
|
|
375
415
|
options.add_argument(args.user_data_dir)
|
|
@@ -380,7 +420,7 @@ def load_selenium(args, wire=False):
|
|
|
380
420
|
|
|
381
421
|
args.driver = webdriver.Firefox(service=service, options=options)
|
|
382
422
|
|
|
383
|
-
if not
|
|
423
|
+
if not _using_real_profile:
|
|
384
424
|
addons = [Path("~/.local/lib/ublock_origin.xpi").expanduser().resolve()]
|
|
385
425
|
if getattr(args, "auto_pager", False):
|
|
386
426
|
addons.append(Path("~/.local/lib/weautopagerize.xpi").expanduser().resolve())
|
|
@@ -401,25 +441,43 @@ def load_selenium(args, wire=False):
|
|
|
401
441
|
from selenium.webdriver.chrome.options import Options
|
|
402
442
|
|
|
403
443
|
options = Options()
|
|
444
|
+
|
|
445
|
+
_using_real_profile = bool(args.user_data_dir)
|
|
446
|
+
if _using_real_profile:
|
|
447
|
+
_real_profile = Path(args.user_data_dir)
|
|
448
|
+
_profile_name = _profile or "Default"
|
|
449
|
+
_real_profile_dir = _real_profile / _profile_name
|
|
450
|
+
_selenium_profile = Path(tempfile.mkdtemp(prefix="chrome_selenium_"))
|
|
451
|
+
copy_chromium_profile_for_selenium(_real_profile_dir, _selenium_profile / _profile_name)
|
|
452
|
+
for name in ("Local State",):
|
|
453
|
+
src_file = _real_profile / name
|
|
454
|
+
if src_file.exists():
|
|
455
|
+
shutil.copy2(src_file, _selenium_profile / name)
|
|
456
|
+
args.selenium_profile_dir = _selenium_profile
|
|
457
|
+
args.user_data_dir = str(_selenium_profile)
|
|
458
|
+
log.info("Selenium profile: %s (cookies from %s)", _selenium_profile, _real_profile_dir)
|
|
459
|
+
|
|
404
460
|
if args.user_data_dir:
|
|
405
461
|
options.add_argument(f"--user-data-dir={args.user_data_dir}")
|
|
406
|
-
if
|
|
462
|
+
if _using_real_profile:
|
|
463
|
+
options.add_argument(f"--profile-directory={_profile_name}")
|
|
464
|
+
elif _profile:
|
|
407
465
|
options.add_argument(f"--profile-directory={_profile}")
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
466
|
+
else:
|
|
467
|
+
options.add_experimental_option(
|
|
468
|
+
"prefs",
|
|
469
|
+
{
|
|
470
|
+
"credentials_enable_service": False,
|
|
471
|
+
"profile.password_manager_enabled": False,
|
|
472
|
+
"profile.default_content_setting_values.notifications": 2,
|
|
473
|
+
},
|
|
474
|
+
)
|
|
417
475
|
options.add_argument("--disable-notifications")
|
|
418
476
|
options.add_argument("--mute-audio")
|
|
419
477
|
if xvfb is False:
|
|
420
478
|
options.add_argument("--headless=new")
|
|
421
479
|
|
|
422
|
-
if not
|
|
480
|
+
if not _using_real_profile:
|
|
423
481
|
addons = [Path("~/.local/lib/ublock_origin.crx").expanduser().resolve()]
|
|
424
482
|
if getattr(args, "auto_pager", False):
|
|
425
483
|
addons.append(Path("~/.local/lib/autopager.crx").expanduser().resolve())
|
|
@@ -437,6 +495,9 @@ def load_selenium(args, wire=False):
|
|
|
437
495
|
|
|
438
496
|
def quit_selenium(args):
|
|
439
497
|
args.driver.quit()
|
|
498
|
+
_selenium_dir = getattr(args, "selenium_profile_dir", None)
|
|
499
|
+
if _selenium_dir:
|
|
500
|
+
shutil.rmtree(_selenium_dir, ignore_errors=True)
|
|
440
501
|
if args.verbose < consts.LOG_DEBUG and not getattr(args, "manual", False):
|
|
441
502
|
with suppress(Exception):
|
|
442
503
|
args.driver_display.stop()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|