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.
Files changed (159) hide show
  1. {library-3.2.4 → library-3.2.5}/.github/README.md +12 -2
  2. {library-3.2.4 → library-3.2.5}/PKG-INFO +13 -3
  3. {library-3.2.4 → library-3.2.5}/library/__main__.py +3 -2
  4. {library-3.2.4 → library-3.2.5}/library/fsdb/disk_usage.py +11 -6
  5. {library-3.2.4 → library-3.2.5}/library/mediafiles/torrents_start.py +77 -13
  6. {library-3.2.4 → library-3.2.5}/library/usage.py +11 -1
  7. {library-3.2.4 → library-3.2.5}/library/utils/arggroups.py +6 -0
  8. {library-3.2.4 → library-3.2.5}/library/utils/filter_engine.py +2 -2
  9. {library-3.2.4 → library-3.2.5}/library/utils/processes.py +4 -4
  10. {library-3.2.4 → library-3.2.5}/library/utils/web.py +74 -13
  11. {library-3.2.4 → library-3.2.5}/pyproject.toml +1 -1
  12. {library-3.2.4 → library-3.2.5}/LICENSE +0 -0
  13. {library-3.2.4 → library-3.2.5}/library/__init__.py +0 -0
  14. {library-3.2.4 → library-3.2.5}/library/assets/__init__.py +0 -0
  15. {library-3.2.4 → library-3.2.5}/library/assets/calibre.css +0 -0
  16. {library-3.2.4 → library-3.2.5}/library/assets/kotobago.png +0 -0
  17. {library-3.2.4 → library-3.2.5}/library/createdb/__init__.py +0 -0
  18. {library-3.2.4 → library-3.2.5}/library/createdb/av.py +0 -0
  19. {library-3.2.4 → library-3.2.5}/library/createdb/computer_info.py +0 -0
  20. {library-3.2.4 → library-3.2.5}/library/createdb/computers_add.py +0 -0
  21. {library-3.2.4 → library-3.2.5}/library/createdb/fs_add.py +0 -0
  22. {library-3.2.4 → library-3.2.5}/library/createdb/fs_add_metadata.py +0 -0
  23. {library-3.2.4 → library-3.2.5}/library/createdb/gallery_add.py +0 -0
  24. {library-3.2.4 → library-3.2.5}/library/createdb/gallery_backend.py +0 -0
  25. {library-3.2.4 → library-3.2.5}/library/createdb/getty_add.py +0 -0
  26. {library-3.2.4 → library-3.2.5}/library/createdb/hn_add.py +0 -0
  27. {library-3.2.4 → library-3.2.5}/library/createdb/links_add.py +0 -0
  28. {library-3.2.4 → library-3.2.5}/library/createdb/nicotine_import.py +0 -0
  29. {library-3.2.4 → library-3.2.5}/library/createdb/places_import.py +0 -0
  30. {library-3.2.4 → library-3.2.5}/library/createdb/reddit_add.py +0 -0
  31. {library-3.2.4 → library-3.2.5}/library/createdb/row_add.py +0 -0
  32. {library-3.2.4 → library-3.2.5}/library/createdb/site_add.py +0 -0
  33. {library-3.2.4 → library-3.2.5}/library/createdb/substack.py +0 -0
  34. {library-3.2.4 → library-3.2.5}/library/createdb/subtitle.py +0 -0
  35. {library-3.2.4 → library-3.2.5}/library/createdb/tables_add.py +0 -0
  36. {library-3.2.4 → library-3.2.5}/library/createdb/tabs_add.py +0 -0
  37. {library-3.2.4 → library-3.2.5}/library/createdb/tildes.py +0 -0
  38. {library-3.2.4 → library-3.2.5}/library/createdb/torrents_add.py +0 -0
  39. {library-3.2.4 → library-3.2.5}/library/createdb/tube_add.py +0 -0
  40. {library-3.2.4 → library-3.2.5}/library/createdb/tube_backend.py +0 -0
  41. {library-3.2.4 → library-3.2.5}/library/createdb/web_add.py +0 -0
  42. {library-3.2.4 → library-3.2.5}/library/data/__init__.py +0 -0
  43. {library-3.2.4 → library-3.2.5}/library/data/dictionary.pkl +0 -0
  44. {library-3.2.4 → library-3.2.5}/library/data/ffmpeg_errors.py +0 -0
  45. {library-3.2.4 → library-3.2.5}/library/data/http_errors.py +0 -0
  46. {library-3.2.4 → library-3.2.5}/library/data/imagemagick_errors.py +0 -0
  47. {library-3.2.4 → library-3.2.5}/library/data/unar_errors.py +0 -0
  48. {library-3.2.4 → library-3.2.5}/library/data/wordbank.py +0 -0
  49. {library-3.2.4 → library-3.2.5}/library/data/yt_dlp_errors.py +0 -0
  50. {library-3.2.4 → library-3.2.5}/library/editdb/__init__.py +0 -0
  51. {library-3.2.4 → library-3.2.5}/library/editdb/dedupe_db.py +0 -0
  52. {library-3.2.4 → library-3.2.5}/library/editdb/dedupe_media.py +0 -0
  53. {library-3.2.4 → library-3.2.5}/library/editdb/merge_online_local.py +0 -0
  54. {library-3.2.4 → library-3.2.5}/library/editdb/mpv_watchlater.py +0 -0
  55. {library-3.2.4 → library-3.2.5}/library/editdb/pushshift.py +0 -0
  56. {library-3.2.4 → library-3.2.5}/library/editdb/reddit_selftext.py +0 -0
  57. {library-3.2.4 → library-3.2.5}/library/files/__init__.py +0 -0
  58. {library-3.2.4 → library-3.2.5}/library/files/christen.py +0 -0
  59. {library-3.2.4 → library-3.2.5}/library/files/sample_compare.py +0 -0
  60. {library-3.2.4 → library-3.2.5}/library/files/sample_hash.py +0 -0
  61. {library-3.2.4 → library-3.2.5}/library/files/similar_files.py +0 -0
  62. {library-3.2.4 → library-3.2.5}/library/folders/__init__.py +0 -0
  63. {library-3.2.4 → library-3.2.5}/library/folders/big_dirs.py +0 -0
  64. {library-3.2.4 → library-3.2.5}/library/folders/filter_src.py +0 -0
  65. {library-3.2.4 → library-3.2.5}/library/folders/merge_mv.py +0 -0
  66. {library-3.2.4 → library-3.2.5}/library/folders/mergerfs_cp.py +0 -0
  67. {library-3.2.4 → library-3.2.5}/library/folders/mount_stats.py +0 -0
  68. {library-3.2.4 → library-3.2.5}/library/folders/move_list.py +0 -0
  69. {library-3.2.4 → library-3.2.5}/library/folders/scatter.py +0 -0
  70. {library-3.2.4 → library-3.2.5}/library/folders/similar_folders.py +0 -0
  71. {library-3.2.4 → library-3.2.5}/library/fsdb/__init__.py +0 -0
  72. {library-3.2.4 → library-3.2.5}/library/fsdb/filesystem.py +0 -0
  73. {library-3.2.4 → library-3.2.5}/library/fsdb/folder_stats.py +0 -0
  74. {library-3.2.4 → library-3.2.5}/library/fsdb/search_db.py +0 -0
  75. {library-3.2.4 → library-3.2.5}/library/lb.py +0 -0
  76. {library-3.2.4 → library-3.2.5}/library/mediadb/__init__.py +0 -0
  77. {library-3.2.4 → library-3.2.5}/library/mediadb/block.py +0 -0
  78. {library-3.2.4 → library-3.2.5}/library/mediadb/db_history.py +0 -0
  79. {library-3.2.4 → library-3.2.5}/library/mediadb/db_media.py +0 -0
  80. {library-3.2.4 → library-3.2.5}/library/mediadb/db_playlists.py +0 -0
  81. {library-3.2.4 → library-3.2.5}/library/mediadb/download.py +0 -0
  82. {library-3.2.4 → library-3.2.5}/library/mediadb/download_status.py +0 -0
  83. {library-3.2.4 → library-3.2.5}/library/mediadb/history.py +0 -0
  84. {library-3.2.4 → library-3.2.5}/library/mediadb/history_add.py +0 -0
  85. {library-3.2.4 → library-3.2.5}/library/mediadb/optimize_db.py +0 -0
  86. {library-3.2.4 → library-3.2.5}/library/mediadb/playlists.py +0 -0
  87. {library-3.2.4 → library-3.2.5}/library/mediadb/redownload.py +0 -0
  88. {library-3.2.4 → library-3.2.5}/library/mediadb/search.py +0 -0
  89. {library-3.2.4 → library-3.2.5}/library/mediadb/stats.py +0 -0
  90. {library-3.2.4 → library-3.2.5}/library/mediafiles/__init__.py +0 -0
  91. {library-3.2.4 → library-3.2.5}/library/mediafiles/images_to_pdf.py +0 -0
  92. {library-3.2.4 → library-3.2.5}/library/mediafiles/media_check.py +0 -0
  93. {library-3.2.4 → library-3.2.5}/library/mediafiles/pdf_edit.py +0 -0
  94. {library-3.2.4 → library-3.2.5}/library/mediafiles/process_ffmpeg.py +0 -0
  95. {library-3.2.4 → library-3.2.5}/library/mediafiles/process_image.py +0 -0
  96. {library-3.2.4 → library-3.2.5}/library/mediafiles/process_media.py +0 -0
  97. {library-3.2.4 → library-3.2.5}/library/mediafiles/process_text.py +0 -0
  98. {library-3.2.4 → library-3.2.5}/library/mediafiles/torrents_dump.py +0 -0
  99. {library-3.2.4 → library-3.2.5}/library/mediafiles/unardel.py +0 -0
  100. {library-3.2.4 → library-3.2.5}/library/misc/__init__.py +0 -0
  101. {library-3.2.4 → library-3.2.5}/library/misc/dedupe_czkawka.py +0 -0
  102. {library-3.2.4 → library-3.2.5}/library/misc/export_text.py +0 -0
  103. {library-3.2.4 → library-3.2.5}/library/misc/search_help.py +0 -0
  104. {library-3.2.4 → library-3.2.5}/library/multidb/__init__.py +0 -0
  105. {library-3.2.4 → library-3.2.5}/library/multidb/allocate_torrents.py +0 -0
  106. {library-3.2.4 → library-3.2.5}/library/multidb/copy_play_counts.py +0 -0
  107. {library-3.2.4 → library-3.2.5}/library/multidb/merge_dbs.py +0 -0
  108. {library-3.2.4 → library-3.2.5}/library/playback/__init__.py +0 -0
  109. {library-3.2.4 → library-3.2.5}/library/playback/links_open.py +0 -0
  110. {library-3.2.4 → library-3.2.5}/library/playback/media_player.py +0 -0
  111. {library-3.2.4 → library-3.2.5}/library/playback/media_printer.py +0 -0
  112. {library-3.2.4 → library-3.2.5}/library/playback/play_actions.py +0 -0
  113. {library-3.2.4 → library-3.2.5}/library/playback/playback_control.py +0 -0
  114. {library-3.2.4 → library-3.2.5}/library/playback/post_actions.py +0 -0
  115. {library-3.2.4 → library-3.2.5}/library/playback/surf.py +0 -0
  116. {library-3.2.4 → library-3.2.5}/library/playback/tabs_open.py +0 -0
  117. {library-3.2.4 → library-3.2.5}/library/playback/torrents_info.py +0 -0
  118. {library-3.2.4 → library-3.2.5}/library/playback/torrents_remaining.py +0 -0
  119. {library-3.2.4 → library-3.2.5}/library/readme.py +0 -0
  120. {library-3.2.4 → library-3.2.5}/library/tablefiles/__init__.py +0 -0
  121. {library-3.2.4 → library-3.2.5}/library/tablefiles/columns.py +0 -0
  122. {library-3.2.4 → library-3.2.5}/library/tablefiles/eda.py +0 -0
  123. {library-3.2.4 → library-3.2.5}/library/tablefiles/incremental_diff.py +0 -0
  124. {library-3.2.4 → library-3.2.5}/library/tablefiles/markdown_tables.py +0 -0
  125. {library-3.2.4 → library-3.2.5}/library/tablefiles/mcda.py +0 -0
  126. {library-3.2.4 → library-3.2.5}/library/tablefiles/plot.py +0 -0
  127. {library-3.2.4 → library-3.2.5}/library/text/__init__.py +0 -0
  128. {library-3.2.4 → library-3.2.5}/library/text/cluster_sort.py +0 -0
  129. {library-3.2.4 → library-3.2.5}/library/text/combinations.py +0 -0
  130. {library-3.2.4 → library-3.2.5}/library/text/expand_links.py +0 -0
  131. {library-3.2.4 → library-3.2.5}/library/text/extract_links.py +0 -0
  132. {library-3.2.4 → library-3.2.5}/library/text/extract_text.py +0 -0
  133. {library-3.2.4 → library-3.2.5}/library/text/json_keys_rename.py +0 -0
  134. {library-3.2.4 → library-3.2.5}/library/text/markdown_links.py +0 -0
  135. {library-3.2.4 → library-3.2.5}/library/text/nouns.py +0 -0
  136. {library-3.2.4 → library-3.2.5}/library/text/regex_sort.py +0 -0
  137. {library-3.2.4 → library-3.2.5}/library/text/timestamps.py +0 -0
  138. {library-3.2.4 → library-3.2.5}/library/utils/__init__.py +0 -0
  139. {library-3.2.4 → library-3.2.5}/library/utils/arg_utils.py +0 -0
  140. {library-3.2.4 → library-3.2.5}/library/utils/argparse_utils.py +0 -0
  141. {library-3.2.4 → library-3.2.5}/library/utils/consts.py +0 -0
  142. {library-3.2.4 → library-3.2.5}/library/utils/date_utils.py +0 -0
  143. {library-3.2.4 → library-3.2.5}/library/utils/db_utils.py +0 -0
  144. {library-3.2.4 → library-3.2.5}/library/utils/devices.py +0 -0
  145. {library-3.2.4 → library-3.2.5}/library/utils/file_utils.py +0 -0
  146. {library-3.2.4 → library-3.2.5}/library/utils/gui.py +0 -0
  147. {library-3.2.4 → library-3.2.5}/library/utils/iterables.py +0 -0
  148. {library-3.2.4 → library-3.2.5}/library/utils/log_utils.py +0 -0
  149. {library-3.2.4 → library-3.2.5}/library/utils/mpv_utils.py +0 -0
  150. {library-3.2.4 → library-3.2.5}/library/utils/nums.py +0 -0
  151. {library-3.2.4 → library-3.2.5}/library/utils/objects.py +0 -0
  152. {library-3.2.4 → library-3.2.5}/library/utils/path_utils.py +0 -0
  153. {library-3.2.4 → library-3.2.5}/library/utils/pd_utils.py +0 -0
  154. {library-3.2.4 → library-3.2.5}/library/utils/printing.py +0 -0
  155. {library-3.2.4 → library-3.2.5}/library/utils/remote_processes.py +0 -0
  156. {library-3.2.4 → library-3.2.5}/library/utils/shell_utils.py +0 -0
  157. {library-3.2.4 → library-3.2.5}/library/utils/sql_utils.py +0 -0
  158. {library-3.2.4 → library-3.2.5}/library/utils/sqlgroups.py +0 -0
  159. {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.004; 102 subcommands)
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.4
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.004; 102 subcommands)
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.004"
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
- if args.sort_groups_by:
55
- key = args.sort_groups_by.replace(" desc", "")
56
- if key != "priority":
57
- return lambda x: (x.get(key) or 0, objects.Reverser(x.get("path")))
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([{"path": k, **v} for k, v in d.items()], key=sort_by(args), reverse=reverse)
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
- temp_prefix = Path(args.temp_drive)
159
+ temp_drive = Path(args.temp_drive)
112
160
  else:
113
- temp_prefix = Path(args.download_drive)
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
- for path in args.paths:
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
- temp_path = temp_prefix
122
- if args.tracker_dirnames:
123
- tracker = get_tracker_domain(torrent)
124
- if tracker:
125
- download_path /= tracker
126
- temp_path /= tracker
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["time_created"] > 0 and args.time_created(d["time_created"])] # type: ignore
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["time_modified"] > 0 and args.time_modified(d["time_modified"])] # type: ignore
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
- return_code = os.spawnvpe(os.P_WAIT, command[0], command, os.environ)
249
+ result = subprocess.run(command, check=False)
250
250
 
251
- if return_code != 0:
252
- msg = f"[{shlex.join(command)}] exited {return_code}"
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 subprocess.CompletedProcess(command, return_code)
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 args.user_data_dir:
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 _profile:
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
- options.add_experimental_option(
410
- "prefs",
411
- {
412
- "credentials_enable_service": False,
413
- "profile.password_manager_enabled": False,
414
- "profile.default_content_setting_values.notifications": 2,
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 args.user_data_dir:
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()
@@ -35,7 +35,7 @@ dynamic = []
35
35
  name = "library"
36
36
  readme = ".github/README.md"
37
37
  requires-python = ">=3.12"
38
- version = "3.2.4"
38
+ version = "3.2.5"
39
39
 
40
40
  [project.license]
41
41
  file = "LICENSE"
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