library 3.2.5__tar.gz → 3.2.7__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 (160) hide show
  1. {library-3.2.5 → library-3.2.7}/.github/README.md +27 -2
  2. {library-3.2.5 → library-3.2.7}/PKG-INFO +28 -3
  3. {library-3.2.5 → library-3.2.7}/library/__main__.py +1 -1
  4. library-3.2.7/library/mediafiles/torrents_dump.py +89 -0
  5. {library-3.2.5 → library-3.2.7}/library/playback/play_actions.py +2 -1
  6. {library-3.2.5 → library-3.2.7}/library/playback/playback_control.py +2 -2
  7. {library-3.2.5 → library-3.2.7}/library/playback/torrents_info.py +77 -26
  8. {library-3.2.5 → library-3.2.7}/library/usage.py +26 -1
  9. {library-3.2.5 → library-3.2.7}/library/utils/arggroups.py +25 -1
  10. {library-3.2.5 → library-3.2.7}/library/utils/devices.py +12 -0
  11. {library-3.2.5 → library-3.2.7}/pyproject.toml +1 -1
  12. library-3.2.5/library/mediafiles/torrents_dump.py +0 -67
  13. {library-3.2.5 → library-3.2.7}/LICENSE +0 -0
  14. {library-3.2.5 → library-3.2.7}/library/__init__.py +0 -0
  15. {library-3.2.5 → library-3.2.7}/library/assets/__init__.py +0 -0
  16. {library-3.2.5 → library-3.2.7}/library/assets/calibre.css +0 -0
  17. {library-3.2.5 → library-3.2.7}/library/assets/kotobago.png +0 -0
  18. {library-3.2.5 → library-3.2.7}/library/createdb/__init__.py +0 -0
  19. {library-3.2.5 → library-3.2.7}/library/createdb/av.py +0 -0
  20. {library-3.2.5 → library-3.2.7}/library/createdb/computer_info.py +0 -0
  21. {library-3.2.5 → library-3.2.7}/library/createdb/computers_add.py +0 -0
  22. {library-3.2.5 → library-3.2.7}/library/createdb/fs_add.py +0 -0
  23. {library-3.2.5 → library-3.2.7}/library/createdb/fs_add_metadata.py +0 -0
  24. {library-3.2.5 → library-3.2.7}/library/createdb/gallery_add.py +0 -0
  25. {library-3.2.5 → library-3.2.7}/library/createdb/gallery_backend.py +0 -0
  26. {library-3.2.5 → library-3.2.7}/library/createdb/getty_add.py +0 -0
  27. {library-3.2.5 → library-3.2.7}/library/createdb/hn_add.py +0 -0
  28. {library-3.2.5 → library-3.2.7}/library/createdb/links_add.py +0 -0
  29. {library-3.2.5 → library-3.2.7}/library/createdb/nicotine_import.py +0 -0
  30. {library-3.2.5 → library-3.2.7}/library/createdb/places_import.py +0 -0
  31. {library-3.2.5 → library-3.2.7}/library/createdb/reddit_add.py +0 -0
  32. {library-3.2.5 → library-3.2.7}/library/createdb/row_add.py +0 -0
  33. {library-3.2.5 → library-3.2.7}/library/createdb/site_add.py +0 -0
  34. {library-3.2.5 → library-3.2.7}/library/createdb/substack.py +0 -0
  35. {library-3.2.5 → library-3.2.7}/library/createdb/subtitle.py +0 -0
  36. {library-3.2.5 → library-3.2.7}/library/createdb/tables_add.py +0 -0
  37. {library-3.2.5 → library-3.2.7}/library/createdb/tabs_add.py +0 -0
  38. {library-3.2.5 → library-3.2.7}/library/createdb/tildes.py +0 -0
  39. {library-3.2.5 → library-3.2.7}/library/createdb/torrents_add.py +0 -0
  40. {library-3.2.5 → library-3.2.7}/library/createdb/tube_add.py +0 -0
  41. {library-3.2.5 → library-3.2.7}/library/createdb/tube_backend.py +0 -0
  42. {library-3.2.5 → library-3.2.7}/library/createdb/web_add.py +0 -0
  43. {library-3.2.5 → library-3.2.7}/library/data/__init__.py +0 -0
  44. {library-3.2.5 → library-3.2.7}/library/data/dictionary.pkl +0 -0
  45. {library-3.2.5 → library-3.2.7}/library/data/ffmpeg_errors.py +0 -0
  46. {library-3.2.5 → library-3.2.7}/library/data/http_errors.py +0 -0
  47. {library-3.2.5 → library-3.2.7}/library/data/imagemagick_errors.py +0 -0
  48. {library-3.2.5 → library-3.2.7}/library/data/unar_errors.py +0 -0
  49. {library-3.2.5 → library-3.2.7}/library/data/wordbank.py +0 -0
  50. {library-3.2.5 → library-3.2.7}/library/data/yt_dlp_errors.py +0 -0
  51. {library-3.2.5 → library-3.2.7}/library/editdb/__init__.py +0 -0
  52. {library-3.2.5 → library-3.2.7}/library/editdb/dedupe_db.py +0 -0
  53. {library-3.2.5 → library-3.2.7}/library/editdb/dedupe_media.py +0 -0
  54. {library-3.2.5 → library-3.2.7}/library/editdb/merge_online_local.py +0 -0
  55. {library-3.2.5 → library-3.2.7}/library/editdb/mpv_watchlater.py +0 -0
  56. {library-3.2.5 → library-3.2.7}/library/editdb/pushshift.py +0 -0
  57. {library-3.2.5 → library-3.2.7}/library/editdb/reddit_selftext.py +0 -0
  58. {library-3.2.5 → library-3.2.7}/library/files/__init__.py +0 -0
  59. {library-3.2.5 → library-3.2.7}/library/files/christen.py +0 -0
  60. {library-3.2.5 → library-3.2.7}/library/files/sample_compare.py +0 -0
  61. {library-3.2.5 → library-3.2.7}/library/files/sample_hash.py +0 -0
  62. {library-3.2.5 → library-3.2.7}/library/files/similar_files.py +0 -0
  63. {library-3.2.5 → library-3.2.7}/library/folders/__init__.py +0 -0
  64. {library-3.2.5 → library-3.2.7}/library/folders/big_dirs.py +0 -0
  65. {library-3.2.5 → library-3.2.7}/library/folders/filter_src.py +0 -0
  66. {library-3.2.5 → library-3.2.7}/library/folders/merge_mv.py +0 -0
  67. {library-3.2.5 → library-3.2.7}/library/folders/mergerfs_cp.py +0 -0
  68. {library-3.2.5 → library-3.2.7}/library/folders/mount_stats.py +0 -0
  69. {library-3.2.5 → library-3.2.7}/library/folders/move_list.py +0 -0
  70. {library-3.2.5 → library-3.2.7}/library/folders/scatter.py +0 -0
  71. {library-3.2.5 → library-3.2.7}/library/folders/similar_folders.py +0 -0
  72. {library-3.2.5 → library-3.2.7}/library/fsdb/__init__.py +0 -0
  73. {library-3.2.5 → library-3.2.7}/library/fsdb/disk_usage.py +0 -0
  74. {library-3.2.5 → library-3.2.7}/library/fsdb/filesystem.py +0 -0
  75. {library-3.2.5 → library-3.2.7}/library/fsdb/folder_stats.py +0 -0
  76. {library-3.2.5 → library-3.2.7}/library/fsdb/search_db.py +0 -0
  77. {library-3.2.5 → library-3.2.7}/library/lb.py +0 -0
  78. {library-3.2.5 → library-3.2.7}/library/mediadb/__init__.py +0 -0
  79. {library-3.2.5 → library-3.2.7}/library/mediadb/block.py +0 -0
  80. {library-3.2.5 → library-3.2.7}/library/mediadb/db_history.py +0 -0
  81. {library-3.2.5 → library-3.2.7}/library/mediadb/db_media.py +0 -0
  82. {library-3.2.5 → library-3.2.7}/library/mediadb/db_playlists.py +0 -0
  83. {library-3.2.5 → library-3.2.7}/library/mediadb/download.py +0 -0
  84. {library-3.2.5 → library-3.2.7}/library/mediadb/download_status.py +0 -0
  85. {library-3.2.5 → library-3.2.7}/library/mediadb/history.py +0 -0
  86. {library-3.2.5 → library-3.2.7}/library/mediadb/history_add.py +0 -0
  87. {library-3.2.5 → library-3.2.7}/library/mediadb/optimize_db.py +0 -0
  88. {library-3.2.5 → library-3.2.7}/library/mediadb/playlists.py +0 -0
  89. {library-3.2.5 → library-3.2.7}/library/mediadb/redownload.py +0 -0
  90. {library-3.2.5 → library-3.2.7}/library/mediadb/search.py +0 -0
  91. {library-3.2.5 → library-3.2.7}/library/mediadb/stats.py +0 -0
  92. {library-3.2.5 → library-3.2.7}/library/mediafiles/__init__.py +0 -0
  93. {library-3.2.5 → library-3.2.7}/library/mediafiles/images_to_pdf.py +0 -0
  94. {library-3.2.5 → library-3.2.7}/library/mediafiles/media_check.py +0 -0
  95. {library-3.2.5 → library-3.2.7}/library/mediafiles/pdf_edit.py +0 -0
  96. {library-3.2.5 → library-3.2.7}/library/mediafiles/process_ffmpeg.py +0 -0
  97. {library-3.2.5 → library-3.2.7}/library/mediafiles/process_image.py +0 -0
  98. {library-3.2.5 → library-3.2.7}/library/mediafiles/process_media.py +0 -0
  99. {library-3.2.5 → library-3.2.7}/library/mediafiles/process_text.py +0 -0
  100. {library-3.2.5 → library-3.2.7}/library/mediafiles/torrents_start.py +0 -0
  101. {library-3.2.5 → library-3.2.7}/library/mediafiles/unardel.py +0 -0
  102. {library-3.2.5 → library-3.2.7}/library/misc/__init__.py +0 -0
  103. {library-3.2.5 → library-3.2.7}/library/misc/dedupe_czkawka.py +0 -0
  104. {library-3.2.5 → library-3.2.7}/library/misc/export_text.py +0 -0
  105. {library-3.2.5 → library-3.2.7}/library/misc/search_help.py +0 -0
  106. {library-3.2.5 → library-3.2.7}/library/multidb/__init__.py +0 -0
  107. {library-3.2.5 → library-3.2.7}/library/multidb/allocate_torrents.py +0 -0
  108. {library-3.2.5 → library-3.2.7}/library/multidb/copy_play_counts.py +0 -0
  109. {library-3.2.5 → library-3.2.7}/library/multidb/merge_dbs.py +0 -0
  110. {library-3.2.5 → library-3.2.7}/library/playback/__init__.py +0 -0
  111. {library-3.2.5 → library-3.2.7}/library/playback/links_open.py +0 -0
  112. {library-3.2.5 → library-3.2.7}/library/playback/media_player.py +0 -0
  113. {library-3.2.5 → library-3.2.7}/library/playback/media_printer.py +0 -0
  114. {library-3.2.5 → library-3.2.7}/library/playback/post_actions.py +0 -0
  115. {library-3.2.5 → library-3.2.7}/library/playback/surf.py +0 -0
  116. {library-3.2.5 → library-3.2.7}/library/playback/tabs_open.py +0 -0
  117. {library-3.2.5 → library-3.2.7}/library/playback/torrents_remaining.py +0 -0
  118. {library-3.2.5 → library-3.2.7}/library/readme.py +0 -0
  119. {library-3.2.5 → library-3.2.7}/library/tablefiles/__init__.py +0 -0
  120. {library-3.2.5 → library-3.2.7}/library/tablefiles/columns.py +0 -0
  121. {library-3.2.5 → library-3.2.7}/library/tablefiles/eda.py +0 -0
  122. {library-3.2.5 → library-3.2.7}/library/tablefiles/incremental_diff.py +0 -0
  123. {library-3.2.5 → library-3.2.7}/library/tablefiles/markdown_tables.py +0 -0
  124. {library-3.2.5 → library-3.2.7}/library/tablefiles/mcda.py +0 -0
  125. {library-3.2.5 → library-3.2.7}/library/tablefiles/plot.py +0 -0
  126. {library-3.2.5 → library-3.2.7}/library/text/__init__.py +0 -0
  127. {library-3.2.5 → library-3.2.7}/library/text/cluster_sort.py +0 -0
  128. {library-3.2.5 → library-3.2.7}/library/text/combinations.py +0 -0
  129. {library-3.2.5 → library-3.2.7}/library/text/expand_links.py +0 -0
  130. {library-3.2.5 → library-3.2.7}/library/text/extract_links.py +0 -0
  131. {library-3.2.5 → library-3.2.7}/library/text/extract_text.py +0 -0
  132. {library-3.2.5 → library-3.2.7}/library/text/json_keys_rename.py +0 -0
  133. {library-3.2.5 → library-3.2.7}/library/text/markdown_links.py +0 -0
  134. {library-3.2.5 → library-3.2.7}/library/text/nouns.py +0 -0
  135. {library-3.2.5 → library-3.2.7}/library/text/regex_sort.py +0 -0
  136. {library-3.2.5 → library-3.2.7}/library/text/timestamps.py +0 -0
  137. {library-3.2.5 → library-3.2.7}/library/utils/__init__.py +0 -0
  138. {library-3.2.5 → library-3.2.7}/library/utils/arg_utils.py +0 -0
  139. {library-3.2.5 → library-3.2.7}/library/utils/argparse_utils.py +0 -0
  140. {library-3.2.5 → library-3.2.7}/library/utils/consts.py +0 -0
  141. {library-3.2.5 → library-3.2.7}/library/utils/date_utils.py +0 -0
  142. {library-3.2.5 → library-3.2.7}/library/utils/db_utils.py +0 -0
  143. {library-3.2.5 → library-3.2.7}/library/utils/file_utils.py +0 -0
  144. {library-3.2.5 → library-3.2.7}/library/utils/filter_engine.py +0 -0
  145. {library-3.2.5 → library-3.2.7}/library/utils/gui.py +0 -0
  146. {library-3.2.5 → library-3.2.7}/library/utils/iterables.py +0 -0
  147. {library-3.2.5 → library-3.2.7}/library/utils/log_utils.py +0 -0
  148. {library-3.2.5 → library-3.2.7}/library/utils/mpv_utils.py +0 -0
  149. {library-3.2.5 → library-3.2.7}/library/utils/nums.py +0 -0
  150. {library-3.2.5 → library-3.2.7}/library/utils/objects.py +0 -0
  151. {library-3.2.5 → library-3.2.7}/library/utils/path_utils.py +0 -0
  152. {library-3.2.5 → library-3.2.7}/library/utils/pd_utils.py +0 -0
  153. {library-3.2.5 → library-3.2.7}/library/utils/printing.py +0 -0
  154. {library-3.2.5 → library-3.2.7}/library/utils/processes.py +0 -0
  155. {library-3.2.5 → library-3.2.7}/library/utils/remote_processes.py +0 -0
  156. {library-3.2.5 → library-3.2.7}/library/utils/shell_utils.py +0 -0
  157. {library-3.2.5 → library-3.2.7}/library/utils/sql_utils.py +0 -0
  158. {library-3.2.5 → library-3.2.7}/library/utils/sqlgroups.py +0 -0
  159. {library-3.2.5 → library-3.2.7}/library/utils/strings.py +0 -0
  160. {library-3.2.5 → library-3.2.7}/library/utils/web.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.005; 102 subcommands)
102
+ library (v3.2.007; 102 subcommands)
103
103
 
104
104
  Create database subcommands:
105
105
  ╭─────────────────┬──────────────────────────────────────────╮
@@ -2178,10 +2178,14 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2178
2178
  <details><summary>Print torrent file info</summary>
2179
2179
 
2180
2180
  $ library torrents-dump -h
2181
- usage: library torrents-dump PATH ...
2181
+ usage: library torrents-dump [--trackers] PATH ...
2182
2182
 
2183
2183
  View torrent file metadata via libtorrent
2184
2184
 
2185
+ Aggregate torrent metadata by tracker domain:
2186
+
2187
+ library torrents-dump --trackers PATH ...
2188
+
2185
2189
 
2186
2190
  </details>
2187
2191
 
@@ -3042,6 +3046,22 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
3042
3046
 
3043
3047
  library torrents query
3044
3048
 
3049
+ Positional torrent searches use the path aligned with torrent status by default.
3050
+ Search a specific path field or both fields with --path-search:
3051
+
3052
+ library torrents --dl --path-search save /mnt/d4/
3053
+ library torrents --dl --path-search both /mnt/d4/
3054
+
3055
+ Find incomplete torrents whose temp and final paths use different drives.
3056
+ --different-drives searches both path fields unless --path-search is specified:
3057
+
3058
+ library torrents --dl --different-drives
3059
+ library torrents --dl --different-drives /mnt/d4/
3060
+
3061
+ Find incomplete torrents whose temp and final paths use the same drive:
3062
+
3063
+ library torrents --dl --same-drives
3064
+
3045
3065
  Aggregate torrent info (torrents-status)
3046
3066
 
3047
3067
  library torrents -pa
@@ -3058,6 +3078,10 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
3058
3078
 
3059
3079
  library torrents --dl --progress=+10% --force-start
3060
3080
 
3081
+ Find torrents with a recorded completion timestamp
3082
+
3083
+ library torrents --completed
3084
+
3061
3085
  Stop completed downloads
3062
3086
 
3063
3087
  library torrents -S+5MiB --seeders=+5 --time-stalled=+45days --time-active=+180days --stop
@@ -3082,6 +3106,7 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
3082
3106
  When --delete-rows is provided, the metadata is removed from qBittorrent
3083
3107
  When --delete-files is provided, the downloaded files are deleted
3084
3108
  When --delete-incomplete 80% is provided, any files that were downloaded less than 80% are deleted
3109
+ When --reset-progress is provided, the downloaded files are deleted and progress is reset without removing the torrents
3085
3110
 
3086
3111
 
3087
3112
  </details>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: library
3
- Version: 3.2.5
3
+ Version: 3.2.7
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.005; 102 subcommands)
210
+ library (v3.2.007; 102 subcommands)
211
211
 
212
212
  Create database subcommands:
213
213
  ╭─────────────────┬──────────────────────────────────────────╮
@@ -2286,10 +2286,14 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2286
2286
  <details><summary>Print torrent file info</summary>
2287
2287
 
2288
2288
  $ library torrents-dump -h
2289
- usage: library torrents-dump PATH ...
2289
+ usage: library torrents-dump [--trackers] PATH ...
2290
2290
 
2291
2291
  View torrent file metadata via libtorrent
2292
2292
 
2293
+ Aggregate torrent metadata by tracker domain:
2294
+
2295
+ library torrents-dump --trackers PATH ...
2296
+
2293
2297
 
2294
2298
  </details>
2295
2299
 
@@ -3150,6 +3154,22 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
3150
3154
 
3151
3155
  library torrents query
3152
3156
 
3157
+ Positional torrent searches use the path aligned with torrent status by default.
3158
+ Search a specific path field or both fields with --path-search:
3159
+
3160
+ library torrents --dl --path-search save /mnt/d4/
3161
+ library torrents --dl --path-search both /mnt/d4/
3162
+
3163
+ Find incomplete torrents whose temp and final paths use different drives.
3164
+ --different-drives searches both path fields unless --path-search is specified:
3165
+
3166
+ library torrents --dl --different-drives
3167
+ library torrents --dl --different-drives /mnt/d4/
3168
+
3169
+ Find incomplete torrents whose temp and final paths use the same drive:
3170
+
3171
+ library torrents --dl --same-drives
3172
+
3153
3173
  Aggregate torrent info (torrents-status)
3154
3174
 
3155
3175
  library torrents -pa
@@ -3166,6 +3186,10 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
3166
3186
 
3167
3187
  library torrents --dl --progress=+10% --force-start
3168
3188
 
3189
+ Find torrents with a recorded completion timestamp
3190
+
3191
+ library torrents --completed
3192
+
3169
3193
  Stop completed downloads
3170
3194
 
3171
3195
  library torrents -S+5MiB --seeders=+5 --time-stalled=+45days --time-active=+180days --stop
@@ -3190,6 +3214,7 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
3190
3214
  When --delete-rows is provided, the metadata is removed from qBittorrent
3191
3215
  When --delete-files is provided, the downloaded files are deleted
3192
3216
  When --delete-incomplete 80% is provided, any files that were downloaded less than 80% are deleted
3217
+ When --reset-progress is provided, the downloaded files are deleted and progress is reset without removing the torrents
3193
3218
 
3194
3219
 
3195
3220
  </details>
@@ -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.005"
8
+ __version__ = "3.2.007"
9
9
 
10
10
  progs = {
11
11
  "Create database subcommands": {
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/python3
2
+
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ from pathlib import Path
5
+
6
+ from library import usage
7
+ from library.createdb import torrents_add
8
+ from library.utils import arggroups, argparse_utils, printing, strings
9
+
10
+
11
+ def parse_args():
12
+ parser = argparse_utils.ArgumentParser(usage=usage.torrents_dump)
13
+ arggroups.debug(parser)
14
+
15
+ parser.add_argument("--trackers", action="store_true", help="Aggregate torrent metadata by tracker")
16
+ arggroups.paths_or_stdin(parser)
17
+ args = parser.parse_args()
18
+ arggroups.args_post(args, parser)
19
+ return args
20
+
21
+
22
+ def gen_torrents(l):
23
+ for s in l:
24
+ p = Path(s)
25
+ if p.is_dir():
26
+ for file in p.rglob("*.torrent"):
27
+ if not file.is_dir():
28
+ yield file
29
+ elif p.is_file() and p.suffix == ".torrent":
30
+ yield p
31
+
32
+
33
+ def aggregate_trackers(torrents):
34
+ trackers = {}
35
+ for _, torrent in torrents:
36
+ tracker = torrent["tracker"]
37
+ stats = trackers.setdefault(tracker, {"tracker": tracker, "count": 0, "size": 0})
38
+ stats["count"] += 1
39
+ stats["size"] += torrent["size"]
40
+
41
+ return [
42
+ {**stats, "size": strings.file_size(stats["size"])}
43
+ for stats in sorted(trackers.values(), key=lambda d: (d["size"], d["tracker"] or ""))
44
+ ]
45
+
46
+
47
+ def torrents_dump():
48
+ args = parse_args()
49
+
50
+ torrent_files = list(gen_torrents(args.paths))
51
+ with ThreadPoolExecutor() as executor:
52
+ metadata_results = executor.map(torrents_add.extract_metadata, torrent_files)
53
+ torrents = list(zip(torrent_files, metadata_results))
54
+
55
+ if args.trackers:
56
+ tracker_stats = aggregate_trackers(torrents)
57
+ if tracker_stats:
58
+ printing.table(tracker_stats)
59
+ return
60
+
61
+ rows = []
62
+ for torrent_path, d in torrents:
63
+ torrent = torrents_add.torrent_decode(torrent_path)
64
+ trackers = [t for t in torrent.trackers()]
65
+ announce_urls = []
66
+ for t in sorted(trackers, key=lambda t: (t.source, t.tier)):
67
+ try:
68
+ if url := t.url:
69
+ announce_urls.append(url)
70
+ except UnicodeDecodeError:
71
+ continue
72
+
73
+ rows.append(
74
+ d
75
+ | {
76
+ "size": strings.file_size(d["size"]),
77
+ "size_avg": strings.file_size(d["size_avg"]),
78
+ "size_median": strings.file_size(d["size_median"]),
79
+ "time_uploaded": strings.relative_datetime(d["time_uploaded"]),
80
+ "time_created": strings.relative_datetime(d["time_created"]),
81
+ "time_modified": strings.relative_datetime(d["time_modified"]),
82
+ "time_created": strings.relative_datetime(d["time_created"]),
83
+ "files": d["files"][0]["path"] + ", ...",
84
+ "announce_urls": " ".join(announce_urls),
85
+ "web_seeds": " ".join(d["web_seeds"]),
86
+ }
87
+ )
88
+
89
+ printing.table(rows)
@@ -252,6 +252,7 @@ If you don't know the exact name of your chromecast group run `catt scan`
252
252
  if args.chromecast:
253
253
  from catt.api import CattDevice
254
254
 
255
+ args.chromecast_device = args.chromecast_device or devices.catt_default_device()
255
256
  args.cc = CattDevice(args.chromecast_device, lazy=True)
256
257
  args.cc_ip = devices.get_ip_of_chromecast(args.chromecast_device)
257
258
 
@@ -502,7 +503,7 @@ def watch() -> None:
502
503
 
503
504
 
504
505
  def listen() -> None:
505
- args = parse_args(SC.listen, default_chromecast="Xylo and Orchestra")
506
+ args = parse_args(SC.listen) # no --cast-to means catt's own default speaker
506
507
  args.profiles = [DBType.audio]
507
508
  process_playqueue(args)
508
509
 
@@ -288,14 +288,14 @@ def catt_stop(args) -> None:
288
288
  catt_device = []
289
289
  if args.chromecast_device:
290
290
  catt_device = ["-d", args.chromecast_device]
291
- processes.cmd("catt", *catt_device, "stop")
291
+ processes.cmd("catt", *catt_device, "stop", strict=False)
292
292
 
293
293
 
294
294
  def catt_pause(args) -> None:
295
295
  catt_device = []
296
296
  if args.chromecast_device:
297
297
  catt_device = ["-d", args.chromecast_device]
298
- processes.cmd("catt", *catt_device, "play_toggle")
298
+ processes.cmd("catt", *catt_device, "play_toggle", strict=False)
299
299
 
300
300
 
301
301
  def kill_process(name) -> None:
@@ -47,6 +47,12 @@ def parse_args():
47
47
  const="73%",
48
48
  help="Delete incomplete files from matching torrents",
49
49
  )
50
+ parser.add_argument(
51
+ "--reset-progress",
52
+ "--reset",
53
+ action="store_true",
54
+ help="Delete matching torrent files and recheck progress without removing the torrent",
55
+ )
50
56
  parser.add_argument("--move", type=Path, help="Directory to move folders/files")
51
57
  arggroups.mmv_folders(parser)
52
58
  parser.add_argument(
@@ -188,6 +194,8 @@ def filter_torrents_by_activity(args, torrents):
188
194
 
189
195
  if args.complete:
190
196
  torrents = [t for t in torrents if t.state_enum.is_complete]
197
+ if args.completed:
198
+ torrents = [t for t in torrents if t.completion_on > 0]
191
199
  if args.incomplete:
192
200
  torrents = [t for t in torrents if not t.state_enum.is_complete]
193
201
 
@@ -297,6 +305,10 @@ def filter_torrents_by_criteria(args, torrents):
297
305
 
298
306
  if args.private is not None:
299
307
  torrents = [t for t in torrents if args.private is t.private]
308
+ if args.different_drives is not None:
309
+ torrents = [
310
+ t for t in torrents if torrent_paths_on_different_drives(t) is args.different_drives
311
+ ]
300
312
  if args.no_tagged:
301
313
  tags = set(args.no_tagged)
302
314
  torrents = [t for t in torrents if tags.isdisjoint(t.tags.split(", "))]
@@ -309,7 +321,7 @@ def filter_torrents_by_criteria(args, torrents):
309
321
  for t in torrents
310
322
  if strings.glob_match_all(
311
323
  args.torrent_search,
312
- [t.name, t.comment, t.save_path if t.state_enum.is_complete else t.download_path, t.hash],
324
+ [t.name, t.comment, *torrent_paths_for_search(args, t), t.hash],
313
325
  )
314
326
  ]
315
327
  if args.torrent_exclude:
@@ -318,7 +330,7 @@ def filter_torrents_by_criteria(args, torrents):
318
330
  for t in torrents
319
331
  if not strings.glob_match_any(
320
332
  args.torrent_exclude,
321
- [t.name, t.comment, t.save_path if t.state_enum.is_complete else t.download_path, t.hash],
333
+ [t.name, t.comment, *torrent_paths_for_search(args, t), t.hash],
322
334
  )
323
335
  ]
324
336
  if args.torrent_include:
@@ -327,7 +339,7 @@ def filter_torrents_by_criteria(args, torrents):
327
339
  for t in torrents
328
340
  if strings.glob_match_any(
329
341
  args.torrent_include,
330
- [t.name, t.comment, t.save_path if t.state_enum.is_complete else t.download_path, t.hash],
342
+ [t.name, t.comment, *torrent_paths_for_search(args, t), t.hash],
331
343
  )
332
344
  ]
333
345
  if args.file_search:
@@ -502,6 +514,58 @@ def map_value_status(t, status):
502
514
  return MAP_VALUE.get(status, t.state == status)
503
515
 
504
516
 
517
+ def torrent_paths_for_search(args, t):
518
+ path_search = getattr(args, "path_search", None)
519
+ if path_search is None and getattr(args, "different_drives", None) is not None:
520
+ path_search = "both"
521
+ if path_search is None:
522
+ path_search = "status"
523
+ if path_search == "download":
524
+ return [t.download_path]
525
+ if path_search == "save":
526
+ return [t.save_path]
527
+ if path_search == "both":
528
+ return [t.download_path, t.save_path]
529
+ return [t.save_path if t.state_enum.is_complete else t.download_path]
530
+
531
+
532
+ def torrent_paths_on_different_drives(t):
533
+ if not t.download_path or not t.save_path:
534
+ return False
535
+ return path_utils.mountpoint(t.download_path) != path_utils.mountpoint(t.save_path)
536
+
537
+
538
+ def reset_torrent_progress(args, qbt_client, torrent):
539
+ originally_stopped = bool(torrent.state_enum.is_stopped)
540
+ qbt_client.torrents_stop(torrent_hashes=[torrent.hash])
541
+
542
+ paths = set()
543
+ for file in torrent_files(torrent):
544
+ for base_path in (torrent.download_path, torrent.save_path):
545
+ if base_path:
546
+ paths.add(Path(base_path) / file.name)
547
+
548
+ for file_path in paths:
549
+ if file_path.exists() and not file_path.is_dir():
550
+ log.info("Deleting file: %s", file_path)
551
+ file_path.unlink(missing_ok=True)
552
+
553
+ qbt_client.torrents_recheck(torrent_hashes=[torrent.hash])
554
+ if not originally_stopped:
555
+ qbt_client.torrents_start(torrent_hashes=[torrent.hash])
556
+
557
+
558
+ def set_torrent_paths(qbt_client, t, temp_path, download_path):
559
+ save_path = download_path if download_path is not None else t.save_path
560
+ if save_path:
561
+ print(" ", t.save_path, "==>", save_path)
562
+ qbt_client.torrents_set_location(str(save_path), torrent_hashes=[t.hash])
563
+
564
+ if temp_path is not None:
565
+ print(" ", t.download_path, "-->", temp_path)
566
+ qbt_client.torrents_set_download_path(str(temp_path), torrent_hashes=[t.hash])
567
+
568
+
505
569
  def torrents_info():
506
570
  args = parse_args()
507
571
 
@@ -910,25 +974,15 @@ def torrents_info():
910
974
  log.warning("Keeping %s incomplete file: %s", strings.percent(1 - file.progress), file_path)
911
975
  break # Stop after deleting first valid path
912
976
 
977
+ if args.reset_progress:
978
+ print("Resetting progress of", len(torrents))
979
+ for t in torrents:
980
+ reset_torrent_progress(args, qbt_client, t)
981
+
913
982
  alt_move_syntax = any(
914
983
  k not in args.defaults for k in ["temp_drive", "temp_prefix", "download_drive", "download_prefix"]
915
984
  )
916
985
  if args.move or alt_move_syntax:
917
-
918
- def set_temp_path(t, temp_path):
919
- if temp_path is None:
920
- return
921
-
922
- print(" ", t.download_path, "-->", temp_path)
923
- qbt_client.torrents_set_download_path(str(temp_path), torrent_hashes=[t.hash])
924
-
925
- def set_download_path(t, download_path):
926
- if download_path is None:
927
- return
928
-
929
- print(" ", t.save_path, "==>", download_path)
930
- qbt_client.torrents_set_save_path(str(download_path), torrent_hashes=[t.hash])
931
-
932
986
  for idx, t in enumerate(torrents):
933
987
  print("Moving", idx + 1, "of", len(torrents))
934
988
 
@@ -993,28 +1047,25 @@ def torrents_info():
993
1047
  log.debug("download_path %s", download_path)
994
1048
 
995
1049
  new_path = download_path if t.state_enum.is_complete else temp_path
1050
+ if new_path is None:
1051
+ new_path = t.save_path if t.state_enum.is_complete else t.download_path
996
1052
  if args.simulate:
997
1053
  print("Moving", t.content_path, "to", new_path)
998
1054
  continue
999
1055
 
1000
1056
  content_path = Path(t.content_path) if t.content_path else None
1001
- if content_path and content_path.exists():
1057
+ if content_path and content_path.exists() and new_path is not None:
1002
1058
  if content_path.is_file():
1003
1059
  print("Moving file", content_path, "to", new_path)
1004
1060
  merge_mv.move(args, [str(content_path)], str(new_path))
1005
1061
  else:
1006
- new_path /= content_path.name
1062
+ new_path = Path(new_path) / content_path.name
1007
1063
  print("Moving folder", content_path, "to", new_path)
1008
1064
  merge_mv.move(args, [str(content_path)], str(new_path))
1009
1065
 
1010
1066
  if not (args.delete_files or args.delete_rows):
1011
1067
  # update metadata
1012
- if t.state_enum.is_complete: # temp path first
1013
- set_temp_path(t, temp_path)
1014
- set_download_path(t, download_path)
1015
- else: # download path first
1016
- set_download_path(t, download_path)
1017
- set_temp_path(t, temp_path)
1068
+ set_torrent_paths(qbt_client, t, temp_path, download_path)
1018
1069
 
1019
1070
  if not originally_stopped:
1020
1071
  qbt_client.torrents_start(torrent_hashes=[t.hash])
@@ -1976,9 +1976,13 @@ torrents_start = """library torrents-start [--prefix /mnt/d/] [--scan] PATH ...
1976
1976
  --scan overrides the --tracker-dirnames layout when choosing save paths.
1977
1977
  """
1978
1978
 
1979
- torrents_dump = """library torrents-dump PATH ...
1979
+ torrents_dump = """library torrents-dump [--trackers] PATH ...
1980
1980
 
1981
1981
  View torrent file metadata via libtorrent
1982
+
1983
+ Aggregate torrent metadata by tracker domain:
1984
+
1985
+ library torrents-dump --trackers PATH ...
1982
1986
  """
1983
1987
 
1984
1988
  torrents_info = """library torrents-info
@@ -1991,6 +1995,22 @@ torrents_info = """library torrents-info
1991
1995
 
1992
1996
  library torrents query
1993
1997
 
1998
+ Positional torrent searches use the path aligned with torrent status by default.
1999
+ Search a specific path field or both fields with --path-search:
2000
+
2001
+ library torrents --dl --path-search save /mnt/d4/
2002
+ library torrents --dl --path-search both /mnt/d4/
2003
+
2004
+ Find incomplete torrents whose temp and final paths use different drives.
2005
+ --different-drives searches both path fields unless --path-search is specified:
2006
+
2007
+ library torrents --dl --different-drives
2008
+ library torrents --dl --different-drives /mnt/d4/
2009
+
2010
+ Find incomplete torrents whose temp and final paths use the same drive:
2011
+
2012
+ library torrents --dl --same-drives
2013
+
1994
2014
  Aggregate torrent info (torrents-status)
1995
2015
 
1996
2016
  library torrents -pa
@@ -2007,6 +2027,10 @@ torrents_info = """library torrents-info
2007
2027
 
2008
2028
  library torrents --dl --progress=+10% --force-start
2009
2029
 
2030
+ Find torrents with a recorded completion timestamp
2031
+
2032
+ library torrents --completed
2033
+
2010
2034
  Stop completed downloads
2011
2035
 
2012
2036
  library torrents -S+5MiB --seeders=+5 --time-stalled=+45days --time-active=+180days --stop
@@ -2031,6 +2055,7 @@ torrents_info = """library torrents-info
2031
2055
  When --delete-rows is provided, the metadata is removed from qBittorrent
2032
2056
  When --delete-files is provided, the downloaded files are deleted
2033
2057
  When --delete-incomplete 80% is provided, any files that were downloaded less than 80% are deleted
2058
+ When --reset-progress is provided, the downloaded files are deleted and progress is reset without removing the torrents
2034
2059
  """
2035
2060
 
2036
2061
  torrents_remaining = """library torrents-remaining
@@ -2764,7 +2764,6 @@ def qBittorrent_torrents(parent_parser):
2764
2764
 
2765
2765
  parser.add_argument(
2766
2766
  "--complete",
2767
- "--completed",
2768
2767
  "--uploading",
2769
2768
  "--upload",
2770
2769
  "--up",
@@ -2773,6 +2772,11 @@ def qBittorrent_torrents(parent_parser):
2773
2772
  action="store_true",
2774
2773
  help="Include completed torrents",
2775
2774
  )
2775
+ parser.add_argument(
2776
+ "--completed",
2777
+ action="store_true",
2778
+ help="Include torrents with a recorded completion timestamp",
2779
+ )
2776
2780
  parser.add_argument(
2777
2781
  "--incomplete",
2778
2782
  "--downloading",
@@ -2795,6 +2799,26 @@ def qBittorrent_torrents(parent_parser):
2795
2799
  help="Include torrents with files currently in use by processes",
2796
2800
  )
2797
2801
  parser.add_argument("--private", action=argparse.BooleanOptionalAction, help="Match only private torrents")
2802
+ drive_filter = parser.add_mutually_exclusive_group()
2803
+ drive_filter.add_argument(
2804
+ "--different-drives",
2805
+ dest="different_drives",
2806
+ action="store_true",
2807
+ help="Include torrents whose incomplete and final paths are on different mountpoints",
2808
+ )
2809
+ drive_filter.add_argument(
2810
+ "--no-different-drives",
2811
+ "--same-drives",
2812
+ dest="different_drives",
2813
+ action="store_false",
2814
+ help="Include torrents whose incomplete and final paths are on the same mountpoint",
2815
+ )
2816
+ parser.set_defaults(different_drives=None)
2817
+ parser.add_argument(
2818
+ "--path-search",
2819
+ choices=["status", "download", "save", "both"],
2820
+ help="Path used by positional torrent searches: status, download, save, or both",
2821
+ )
2798
2822
 
2799
2823
  parser.add_argument(
2800
2824
  "--sizes",
@@ -8,6 +8,18 @@ from library.utils.log_utils import log
8
8
  webbrowser.register("termux-open-url '%s'", None)
9
9
 
10
10
 
11
+ def catt_default_device() -> str:
12
+ """catt's own configured speaker, i.e. options.device in ~/.config/catt/catt.cfg."""
13
+ config = Path.home() / ".config" / "catt" / "catt.cfg"
14
+ if not config.exists():
15
+ return ""
16
+ for line in config.read_text().splitlines():
17
+ key, _, value = line.partition("=")
18
+ if key.strip() == "device":
19
+ return value.strip()
20
+ return ""
21
+
22
+
11
23
  def get_ip_of_chromecast(device_name) -> str:
12
24
  from pychromecast import discovery
13
25
 
@@ -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.5"
38
+ version = "3.2.7"
39
39
 
40
40
  [project.license]
41
41
  file = "LICENSE"
@@ -1,67 +0,0 @@
1
- #!/usr/bin/python3
2
-
3
- from concurrent.futures import ThreadPoolExecutor
4
- from pathlib import Path
5
-
6
- from library import usage
7
- from library.createdb import torrents_add
8
- from library.utils import arggroups, argparse_utils, printing, strings
9
-
10
-
11
- def parse_args():
12
- parser = argparse_utils.ArgumentParser(usage=usage.torrents_dump)
13
- arggroups.debug(parser)
14
-
15
- arggroups.paths_or_stdin(parser)
16
- args = parser.parse_args()
17
- arggroups.args_post(args, parser)
18
- return args
19
-
20
-
21
- def gen_torrents(l):
22
- for s in l:
23
- p = Path(s)
24
- if p.is_dir():
25
- for file in p.rglob("*.torrent"):
26
- if not file.is_dir():
27
- yield file
28
- elif p.is_file() and p.suffix == ".torrent":
29
- yield p
30
-
31
-
32
- def torrents_dump():
33
- args = parse_args()
34
-
35
- torrent_files = list(gen_torrents(args.paths))
36
- with ThreadPoolExecutor() as executor:
37
- metadata_results = executor.map(torrents_add.extract_metadata, torrent_files)
38
- torrents = list(zip(torrent_files, metadata_results))
39
-
40
- for i, (torrent_path, d) in enumerate(torrents):
41
- torrent = torrents_add.torrent_decode(torrent_path)
42
- trackers = [t for t in torrent.trackers()]
43
- announce_urls = []
44
- for t in sorted(trackers, key=lambda t: (t.source, t.tier)):
45
- try:
46
- if url := t.url:
47
- announce_urls.append(url)
48
- except UnicodeDecodeError:
49
- continue
50
-
51
- printing.table(
52
- [
53
- d
54
- | {
55
- "size": strings.file_size(d["size"]),
56
- "size_avg": strings.file_size(d["size_avg"]),
57
- "size_median": strings.file_size(d["size_median"]),
58
- "time_uploaded": strings.relative_datetime(d["time_uploaded"]),
59
- "time_created": strings.relative_datetime(d["time_created"]),
60
- "time_modified": strings.relative_datetime(d["time_modified"]),
61
- "time_created": strings.relative_datetime(d["time_created"]),
62
- "files": d["files"][0]["path"] + ", ...",
63
- "announce_urls": " ".join(announce_urls),
64
- "web_seeds": " ".join(d["web_seeds"]),
65
- }
66
- ]
67
- )
File without changes
File without changes
File without changes