library 3.2.7__tar.gz → 3.2.11__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 (162) hide show
  1. {library-3.2.7 → library-3.2.11}/.github/README.md +74 -4
  2. {library-3.2.7 → library-3.2.11}/PKG-INFO +126 -54
  3. {library-3.2.7 → library-3.2.11}/library/__main__.py +1 -2
  4. library-3.2.11/library/_version.py +25 -0
  5. {library-3.2.7 → library-3.2.11}/library/createdb/gallery_backend.py +37 -35
  6. {library-3.2.7 → library-3.2.11}/library/createdb/links_add.py +11 -2
  7. {library-3.2.7 → library-3.2.11}/library/mediadb/download.py +11 -3
  8. {library-3.2.7 → library-3.2.11}/library/mediafiles/images_to_pdf.py +1 -1
  9. {library-3.2.7 → library-3.2.11}/library/mediafiles/process_image.py +1 -1
  10. {library-3.2.7 → library-3.2.11}/library/mediafiles/unardel.py +39 -9
  11. {library-3.2.7 → library-3.2.11}/library/readme.py +28 -2
  12. library-3.2.11/library/text/extract_links.py +422 -0
  13. {library-3.2.7 → library-3.2.11}/library/usage.py +45 -1
  14. {library-3.2.7 → library-3.2.11}/library/utils/arggroups.py +84 -11
  15. {library-3.2.7 → library-3.2.11}/library/utils/consts.py +24 -0
  16. {library-3.2.7 → library-3.2.11}/library/utils/devices.py +1 -1
  17. {library-3.2.7 → library-3.2.11}/library/utils/printing.py +3 -6
  18. {library-3.2.7 → library-3.2.11}/library/utils/processes.py +9 -6
  19. {library-3.2.7 → library-3.2.11}/library/utils/shell_utils.py +12 -4
  20. {library-3.2.7 → library-3.2.11}/library/utils/strings.py +10 -6
  21. {library-3.2.7 → library-3.2.11}/library/utils/web.py +279 -11
  22. {library-3.2.7 → library-3.2.11}/pyproject.toml +23 -37
  23. library-3.2.11/pyproject.toml.orig +207 -0
  24. library-3.2.7/library/text/extract_links.py +0 -274
  25. {library-3.2.7 → library-3.2.11}/LICENSE +0 -0
  26. {library-3.2.7 → library-3.2.11}/library/__init__.py +0 -0
  27. {library-3.2.7 → library-3.2.11}/library/assets/__init__.py +0 -0
  28. {library-3.2.7 → library-3.2.11}/library/assets/calibre.css +0 -0
  29. {library-3.2.7 → library-3.2.11}/library/assets/kotobago.png +0 -0
  30. {library-3.2.7 → library-3.2.11}/library/createdb/__init__.py +0 -0
  31. {library-3.2.7 → library-3.2.11}/library/createdb/av.py +0 -0
  32. {library-3.2.7 → library-3.2.11}/library/createdb/computer_info.py +0 -0
  33. {library-3.2.7 → library-3.2.11}/library/createdb/computers_add.py +0 -0
  34. {library-3.2.7 → library-3.2.11}/library/createdb/fs_add.py +0 -0
  35. {library-3.2.7 → library-3.2.11}/library/createdb/fs_add_metadata.py +0 -0
  36. {library-3.2.7 → library-3.2.11}/library/createdb/gallery_add.py +0 -0
  37. {library-3.2.7 → library-3.2.11}/library/createdb/getty_add.py +0 -0
  38. {library-3.2.7 → library-3.2.11}/library/createdb/hn_add.py +0 -0
  39. {library-3.2.7 → library-3.2.11}/library/createdb/nicotine_import.py +0 -0
  40. {library-3.2.7 → library-3.2.11}/library/createdb/places_import.py +0 -0
  41. {library-3.2.7 → library-3.2.11}/library/createdb/reddit_add.py +0 -0
  42. {library-3.2.7 → library-3.2.11}/library/createdb/row_add.py +0 -0
  43. {library-3.2.7 → library-3.2.11}/library/createdb/site_add.py +0 -0
  44. {library-3.2.7 → library-3.2.11}/library/createdb/substack.py +0 -0
  45. {library-3.2.7 → library-3.2.11}/library/createdb/subtitle.py +0 -0
  46. {library-3.2.7 → library-3.2.11}/library/createdb/tables_add.py +0 -0
  47. {library-3.2.7 → library-3.2.11}/library/createdb/tabs_add.py +0 -0
  48. {library-3.2.7 → library-3.2.11}/library/createdb/tildes.py +0 -0
  49. {library-3.2.7 → library-3.2.11}/library/createdb/torrents_add.py +0 -0
  50. {library-3.2.7 → library-3.2.11}/library/createdb/tube_add.py +0 -0
  51. {library-3.2.7 → library-3.2.11}/library/createdb/tube_backend.py +0 -0
  52. {library-3.2.7 → library-3.2.11}/library/createdb/web_add.py +0 -0
  53. {library-3.2.7 → library-3.2.11}/library/data/__init__.py +0 -0
  54. {library-3.2.7 → library-3.2.11}/library/data/dictionary.pkl +0 -0
  55. {library-3.2.7 → library-3.2.11}/library/data/ffmpeg_errors.py +0 -0
  56. {library-3.2.7 → library-3.2.11}/library/data/http_errors.py +0 -0
  57. {library-3.2.7 → library-3.2.11}/library/data/imagemagick_errors.py +0 -0
  58. {library-3.2.7 → library-3.2.11}/library/data/unar_errors.py +0 -0
  59. {library-3.2.7 → library-3.2.11}/library/data/wordbank.py +0 -0
  60. {library-3.2.7 → library-3.2.11}/library/data/yt_dlp_errors.py +0 -0
  61. {library-3.2.7 → library-3.2.11}/library/editdb/__init__.py +0 -0
  62. {library-3.2.7 → library-3.2.11}/library/editdb/dedupe_db.py +0 -0
  63. {library-3.2.7 → library-3.2.11}/library/editdb/dedupe_media.py +0 -0
  64. {library-3.2.7 → library-3.2.11}/library/editdb/merge_online_local.py +0 -0
  65. {library-3.2.7 → library-3.2.11}/library/editdb/mpv_watchlater.py +0 -0
  66. {library-3.2.7 → library-3.2.11}/library/editdb/pushshift.py +0 -0
  67. {library-3.2.7 → library-3.2.11}/library/editdb/reddit_selftext.py +0 -0
  68. {library-3.2.7 → library-3.2.11}/library/files/__init__.py +0 -0
  69. {library-3.2.7 → library-3.2.11}/library/files/christen.py +0 -0
  70. {library-3.2.7 → library-3.2.11}/library/files/sample_compare.py +0 -0
  71. {library-3.2.7 → library-3.2.11}/library/files/sample_hash.py +0 -0
  72. {library-3.2.7 → library-3.2.11}/library/files/similar_files.py +0 -0
  73. {library-3.2.7 → library-3.2.11}/library/folders/__init__.py +0 -0
  74. {library-3.2.7 → library-3.2.11}/library/folders/big_dirs.py +0 -0
  75. {library-3.2.7 → library-3.2.11}/library/folders/filter_src.py +0 -0
  76. {library-3.2.7 → library-3.2.11}/library/folders/merge_mv.py +0 -0
  77. {library-3.2.7 → library-3.2.11}/library/folders/mergerfs_cp.py +0 -0
  78. {library-3.2.7 → library-3.2.11}/library/folders/mount_stats.py +0 -0
  79. {library-3.2.7 → library-3.2.11}/library/folders/move_list.py +0 -0
  80. {library-3.2.7 → library-3.2.11}/library/folders/scatter.py +0 -0
  81. {library-3.2.7 → library-3.2.11}/library/folders/similar_folders.py +0 -0
  82. {library-3.2.7 → library-3.2.11}/library/fsdb/__init__.py +0 -0
  83. {library-3.2.7 → library-3.2.11}/library/fsdb/disk_usage.py +0 -0
  84. {library-3.2.7 → library-3.2.11}/library/fsdb/filesystem.py +0 -0
  85. {library-3.2.7 → library-3.2.11}/library/fsdb/folder_stats.py +0 -0
  86. {library-3.2.7 → library-3.2.11}/library/fsdb/search_db.py +0 -0
  87. {library-3.2.7 → library-3.2.11}/library/lb.py +0 -0
  88. {library-3.2.7 → library-3.2.11}/library/mediadb/__init__.py +0 -0
  89. {library-3.2.7 → library-3.2.11}/library/mediadb/block.py +0 -0
  90. {library-3.2.7 → library-3.2.11}/library/mediadb/db_history.py +0 -0
  91. {library-3.2.7 → library-3.2.11}/library/mediadb/db_media.py +0 -0
  92. {library-3.2.7 → library-3.2.11}/library/mediadb/db_playlists.py +0 -0
  93. {library-3.2.7 → library-3.2.11}/library/mediadb/download_status.py +0 -0
  94. {library-3.2.7 → library-3.2.11}/library/mediadb/history.py +0 -0
  95. {library-3.2.7 → library-3.2.11}/library/mediadb/history_add.py +0 -0
  96. {library-3.2.7 → library-3.2.11}/library/mediadb/optimize_db.py +0 -0
  97. {library-3.2.7 → library-3.2.11}/library/mediadb/playlists.py +0 -0
  98. {library-3.2.7 → library-3.2.11}/library/mediadb/redownload.py +0 -0
  99. {library-3.2.7 → library-3.2.11}/library/mediadb/search.py +0 -0
  100. {library-3.2.7 → library-3.2.11}/library/mediadb/stats.py +0 -0
  101. {library-3.2.7 → library-3.2.11}/library/mediafiles/__init__.py +0 -0
  102. {library-3.2.7 → library-3.2.11}/library/mediafiles/media_check.py +0 -0
  103. {library-3.2.7 → library-3.2.11}/library/mediafiles/pdf_edit.py +0 -0
  104. {library-3.2.7 → library-3.2.11}/library/mediafiles/process_ffmpeg.py +0 -0
  105. {library-3.2.7 → library-3.2.11}/library/mediafiles/process_media.py +0 -0
  106. {library-3.2.7 → library-3.2.11}/library/mediafiles/process_text.py +0 -0
  107. {library-3.2.7 → library-3.2.11}/library/mediafiles/torrents_dump.py +0 -0
  108. {library-3.2.7 → library-3.2.11}/library/mediafiles/torrents_start.py +0 -0
  109. {library-3.2.7 → library-3.2.11}/library/misc/__init__.py +0 -0
  110. {library-3.2.7 → library-3.2.11}/library/misc/dedupe_czkawka.py +0 -0
  111. {library-3.2.7 → library-3.2.11}/library/misc/export_text.py +0 -0
  112. {library-3.2.7 → library-3.2.11}/library/misc/search_help.py +0 -0
  113. {library-3.2.7 → library-3.2.11}/library/multidb/__init__.py +0 -0
  114. {library-3.2.7 → library-3.2.11}/library/multidb/allocate_torrents.py +0 -0
  115. {library-3.2.7 → library-3.2.11}/library/multidb/copy_play_counts.py +0 -0
  116. {library-3.2.7 → library-3.2.11}/library/multidb/merge_dbs.py +0 -0
  117. {library-3.2.7 → library-3.2.11}/library/playback/__init__.py +0 -0
  118. {library-3.2.7 → library-3.2.11}/library/playback/links_open.py +0 -0
  119. {library-3.2.7 → library-3.2.11}/library/playback/media_player.py +0 -0
  120. {library-3.2.7 → library-3.2.11}/library/playback/media_printer.py +0 -0
  121. {library-3.2.7 → library-3.2.11}/library/playback/play_actions.py +0 -0
  122. {library-3.2.7 → library-3.2.11}/library/playback/playback_control.py +0 -0
  123. {library-3.2.7 → library-3.2.11}/library/playback/post_actions.py +0 -0
  124. {library-3.2.7 → library-3.2.11}/library/playback/surf.py +0 -0
  125. {library-3.2.7 → library-3.2.11}/library/playback/tabs_open.py +0 -0
  126. {library-3.2.7 → library-3.2.11}/library/playback/torrents_info.py +0 -0
  127. {library-3.2.7 → library-3.2.11}/library/playback/torrents_remaining.py +0 -0
  128. {library-3.2.7 → library-3.2.11}/library/tablefiles/__init__.py +0 -0
  129. {library-3.2.7 → library-3.2.11}/library/tablefiles/columns.py +0 -0
  130. {library-3.2.7 → library-3.2.11}/library/tablefiles/eda.py +0 -0
  131. {library-3.2.7 → library-3.2.11}/library/tablefiles/incremental_diff.py +0 -0
  132. {library-3.2.7 → library-3.2.11}/library/tablefiles/markdown_tables.py +0 -0
  133. {library-3.2.7 → library-3.2.11}/library/tablefiles/mcda.py +0 -0
  134. {library-3.2.7 → library-3.2.11}/library/tablefiles/plot.py +0 -0
  135. {library-3.2.7 → library-3.2.11}/library/text/__init__.py +0 -0
  136. {library-3.2.7 → library-3.2.11}/library/text/cluster_sort.py +0 -0
  137. {library-3.2.7 → library-3.2.11}/library/text/combinations.py +0 -0
  138. {library-3.2.7 → library-3.2.11}/library/text/expand_links.py +0 -0
  139. {library-3.2.7 → library-3.2.11}/library/text/extract_text.py +0 -0
  140. {library-3.2.7 → library-3.2.11}/library/text/json_keys_rename.py +0 -0
  141. {library-3.2.7 → library-3.2.11}/library/text/markdown_links.py +0 -0
  142. {library-3.2.7 → library-3.2.11}/library/text/nouns.py +0 -0
  143. {library-3.2.7 → library-3.2.11}/library/text/regex_sort.py +0 -0
  144. {library-3.2.7 → library-3.2.11}/library/text/timestamps.py +0 -0
  145. {library-3.2.7 → library-3.2.11}/library/utils/__init__.py +0 -0
  146. {library-3.2.7 → library-3.2.11}/library/utils/arg_utils.py +0 -0
  147. {library-3.2.7 → library-3.2.11}/library/utils/argparse_utils.py +0 -0
  148. {library-3.2.7 → library-3.2.11}/library/utils/date_utils.py +0 -0
  149. {library-3.2.7 → library-3.2.11}/library/utils/db_utils.py +0 -0
  150. {library-3.2.7 → library-3.2.11}/library/utils/file_utils.py +0 -0
  151. {library-3.2.7 → library-3.2.11}/library/utils/filter_engine.py +0 -0
  152. {library-3.2.7 → library-3.2.11}/library/utils/gui.py +0 -0
  153. {library-3.2.7 → library-3.2.11}/library/utils/iterables.py +0 -0
  154. {library-3.2.7 → library-3.2.11}/library/utils/log_utils.py +0 -0
  155. {library-3.2.7 → library-3.2.11}/library/utils/mpv_utils.py +0 -0
  156. {library-3.2.7 → library-3.2.11}/library/utils/nums.py +0 -0
  157. {library-3.2.7 → library-3.2.11}/library/utils/objects.py +0 -0
  158. {library-3.2.7 → library-3.2.11}/library/utils/path_utils.py +0 -0
  159. {library-3.2.7 → library-3.2.11}/library/utils/pd_utils.py +0 -0
  160. {library-3.2.7 → library-3.2.11}/library/utils/remote_processes.py +0 -0
  161. {library-3.2.7 → library-3.2.11}/library/utils/sql_utils.py +0 -0
  162. {library-3.2.7 → library-3.2.11}/library/utils/sqlgroups.py +0 -0
@@ -10,7 +10,9 @@ tracking online video playlists (eg. YouTube subscriptions) and scheduling brows
10
10
 
11
11
  [![Downloads](https://static.pepy.tech/badge/library)](https://pepy.tech/project/library)
12
12
 
13
- ## Install
13
+ ## Getting started
14
+
15
+ <details><summary>Install</summary>
14
16
 
15
17
  Linux recommended but [Windows setup instructions](./Windows.md) available.
16
18
 
@@ -24,7 +26,31 @@ Required: `ffmpeg`
24
26
 
25
27
  Some features work better with: `mpv`, `fd-find`, `fish`
26
28
 
27
- ## Getting started
29
+ Optional Python extras are grouped under `library[deluxe]`. This includes
30
+ `gallery-dl` (used by `gallery-add`), `internetarchive` (used for Internet
31
+ Archive downloads), and `cdx_toolkit` (used by the `--webcache`/`--wayback`
32
+ CDX extractor):
33
+
34
+ pip install "library[deluxe]"
35
+
36
+ Torrent metadata commands (`torrents-add` and `torrents-dump`) require the
37
+ `libtorrent` Python bindings. They are included by `library[deluxe]` when using
38
+ Python 3.12 or 3.13; Python 3.14 wheels are not available yet, so use Python
39
+ 3.13 for torrent support.
40
+
41
+ If the PyPI wheel is unavailable for your Python or platform, these system
42
+ packages provide the bindings:
43
+
44
+ * Debian/Ubuntu: `sudo apt install python3-libtorrent`
45
+ * Fedora: `sudo dnf install rb_libtorrent-python3`
46
+ * macOS/Homebrew: `brew install libtorrent-rasterbar`
47
+
48
+ The Debian, Fedora, and Homebrew packages are built for their system or
49
+ Homebrew Python and may not be importable from a separate uv virtualenv. There
50
+ is no official Scoop package for the Python bindings; on Windows, use Python
51
+ 3.13 and `pip install "library[deluxe]"`.
52
+
53
+ </details>
28
54
 
29
55
  <details><summary>Local media</summary>
30
56
 
@@ -99,7 +125,7 @@ To stop playing press Ctrl+C in either the terminal or mpv
99
125
  <details><summary>List all subcommands</summary>
100
126
 
101
127
  $ library
102
- library (v3.2.007; 102 subcommands)
128
+ library (v3.2.011; 102 subcommands)
103
129
 
104
130
  Create database subcommands:
105
131
  ╭─────────────────┬──────────────────────────────────────────╮
@@ -797,6 +823,14 @@ BTW, for some cols like time_deleted you'll need to specify a where clause so th
797
823
  --page-key start --page-start 0 --page-step 50 --fixed-pages 14 --stop-pages-no-match 1 \
798
824
  plab.db https://plab/forum/tracker.php?o=(string replace ' ' \n -- 1 4 7 10 15)&s=2&tm=-1&f=(string replace ' ' \n -- 1670 1768 60 1671 1644 1672 1111 508 555 1112 1718 1143 1717 1851 1713 1712 1775 1674 902 1675 36 1830 1803 1831 1741 1676 1677 1780 1110 1124 1784 1769 1793 1797 1804 1819 1825 1836 1842 1846 1857 1861 1867 1451 1788 1789 1792 1798 1805 1820 1826 1837 1843 1847 1856 1862 1868 284 1853 1823 1800 1801 1719 997 1818 1849 1711 1791 1762)
799
825
 
826
+ Recursively add every link on a site (glob excludes prune subtrees from the crawl)
827
+
828
+ library links-add links.db --recursive https://example.com/docs/ --path-exclude '*/tags/*'
829
+
830
+ Add every archived link for a site from the Internet Archive CDX index
831
+
832
+ library links-add links.db --webcache https://example.com/docs/
833
+
800
834
 
801
835
  </details>
802
836
 
@@ -1130,6 +1164,31 @@ BTW, for some cols like time_deleted you'll need to specify a where clause so th
1130
1164
  library links https://en.wikipedia.org/wiki/List_of_bacon_dishes --path-include https://en.wikipedia.org/wiki/ --after-include famous
1131
1165
  https://en.wikipedia.org/wiki/Omelette
1132
1166
 
1167
+ Download the filtered links. Internet Archive details pages use the Internet Archive downloader and preserve all original files:
1168
+
1169
+ library extract-links --download https://readrare.com/
1170
+
1171
+ Mirror a site recursively. Only links within the same domain and parent directory as the given
1172
+ path are followed; links pointing elsewhere are downloaded but not followed. Includes/excludes
1173
+ accept globs, so you can prune whole subtrees while crawling:
1174
+
1175
+ library links --recursive --download https://example.com/docs/
1176
+ library links -R --download https://example.com/docs/ --path-exclude '*/tag/*' '*/tags/*'
1177
+
1178
+ Extract links from a web archive CDX index instead of crawling live pages. Internet Archive
1179
+ Wayback snapshots are emitted by default. Wayback URLs are collapsed to their original
1180
+ archived URL while crawling: sibling pages that only exist under a different timestamp are
1181
+ still followed, but multiple captures of the same page are collapsed so only one copy of
1182
+ each page is downloaded. Use --webcache-from/--webcache-to to target a time period:
1183
+
1184
+ library links --webcache https://example.com/docs/
1185
+ library links --webcache --recursive --download https://example.com/docs/
1186
+ library links --webcache --webcache-source cc https://example.com/docs/
1187
+ library links --webcache --webcache-from 2020 --webcache-to 2021 https://example.com/docs/
1188
+
1189
+ Common tracking and analytics URLs, including Google Tag Manager and Google Analytics,
1190
+ are excluded by default. Use --path-exclude to replace the default exclusions when needed.
1191
+
1133
1192
  Read from local clipboard and filter out links based on nearby plain text
1134
1193
 
1135
1194
  library links --local-html (cb -t text/html | psub) --after-exclude paranormal spooky horror podcast tech fantasy supernatural lecture sport
@@ -2320,7 +2379,7 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2320
2379
  | /home/xk/github/xk/lb/.ruff_cache/ | 19.5 kB | 100 |
2321
2380
  | /home/xk/github/xk/lb/.gitattributes | 119 Bytes | |
2322
2381
  | /home/xk/github/xk/lb/.mypy_cache/ | 280 Bytes | 4 |
2323
- | /home/xk/github/xk/lb/.pdm-python | 15 Bytes | |
2382
+ | /home/xk/github/xk/lb/.venv | 15 Bytes | |
2324
2383
 
2325
2384
  Only include files with a specific depth
2326
2385
 
@@ -2493,6 +2552,17 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2493
2552
  library download dl.db --include https://www.youtube.com/watch?v=YE7VzlLtp-4
2494
2553
  library download dl.db -s https://www.youtube.com/watch?v=YE7VzlLtp-4 # equivalent
2495
2554
 
2555
+ Recursively download a site (filesystem profile). Only links within the same domain and
2556
+ parent directory as the seed are followed; other links are downloaded but not followed.
2557
+
2558
+ library download links.db --filesystem --recursive --prefix ~/mirror/ https://example.com/docs/
2559
+
2560
+ Download a site's archived pages from the Internet Archive Wayback Machine using the CDX API.
2561
+ `--webcache` implies `--links`; add `--recursive` to also parse the archived pages for
2562
+ further links:
2563
+
2564
+ library download links.db --filesystem --webcache --prefix ~/mirror/ https://example.com/docs/
2565
+
2496
2566
  Maximizing the variety of subdomains
2497
2567
 
2498
2568
  library download photos.db --photos --image --sort "ROW_NUMBER() OVER ( PARTITION BY SUBSTR(m.path, INSTR(m.path, '//') + 2, INSTR( SUBSTR(m.path, INSTR(m.path, '//') + 2), '/') - 1) )"
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: library
3
- Version: 3.2.7
3
+ Version: 3.2.11
4
4
  Summary: xk media library
5
- Author-Email: Jacob Chapman <7908073+chapmanjacobd@users.noreply.github.com>
5
+ Author: Jacob Chapman
6
+ Author-email: Jacob Chapman <7908073+chapmanjacobd@users.noreply.github.com>
6
7
  License: BSD 3-Clause License
7
8
 
8
9
  Copyright (c) 2024, Jacob Chapman
@@ -31,16 +32,10 @@ License: BSD 3-Clause License
31
32
  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
33
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
34
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
-
35
- Project-URL: documentation, https://github.com/chapmanjacobd/library#usage
36
- Project-URL: homepage, https://github.com/chapmanjacobd/library#readme
37
- Project-URL: repository, https://github.com/chapmanjacobd/library/
38
- Requires-Python: >=3.12
39
35
  Requires-Dist: beautifulsoup4>=4.10.0
40
36
  Requires-Dist: sqlite-utils>=3.38
41
37
  Requires-Dist: ipython>=9.0.0
42
38
  Requires-Dist: ffmpeg-python
43
- Requires-Dist: gallery-dl
44
39
  Requires-Dist: yt-dlp[default]
45
40
  Requires-Dist: natsort
46
41
  Requires-Dist: charset-normalizer
@@ -61,49 +56,56 @@ Requires-Dist: screeninfo
61
56
  Requires-Dist: tabulate
62
57
  Requires-Dist: tinytag
63
58
  Requires-Dist: tldextract
64
- Requires-Dist: pyreadline3; platform_system == "Windows"
65
- Provides-Extra: dev
66
- Requires-Dist: black; extra == "dev"
67
- Requires-Dist: isort; extra == "dev"
68
- Requires-Dist: ssort; extra == "dev"
69
- Provides-Extra: test
70
- Requires-Dist: ruff; extra == "test"
71
- Requires-Dist: pytest; extra == "test"
72
- Requires-Dist: pytest-regressions; extra == "test"
73
- Requires-Dist: freezegun; extra == "test"
74
- Requires-Dist: pandas; extra == "test"
75
- Requires-Dist: pyfakefs; extra == "test"
59
+ Requires-Dist: pyreadline3 ; sys_platform == 'win32'
60
+ Requires-Dist: aiohttp ; extra == 'deluxe'
61
+ Requires-Dist: annoy ; extra == 'deluxe'
62
+ Requires-Dist: catt ; extra == 'deluxe'
63
+ Requires-Dist: cdx-toolkit ; extra == 'deluxe'
64
+ Requires-Dist: curl-cffi ; extra == 'deluxe'
65
+ Requires-Dist: gallery-dl ; extra == 'deluxe'
66
+ Requires-Dist: geopandas ; extra == 'deluxe'
67
+ Requires-Dist: img2pdf ; extra == 'deluxe'
68
+ Requires-Dist: internetarchive ; extra == 'deluxe'
69
+ Requires-Dist: ocrmypdf ; extra == 'deluxe'
70
+ Requires-Dist: openpyxl ; extra == 'deluxe'
71
+ Requires-Dist: paramiko ; extra == 'deluxe'
72
+ Requires-Dist: pdf2image ; extra == 'deluxe'
73
+ Requires-Dist: pillow ; extra == 'deluxe'
74
+ Requires-Dist: pyexiftool ; extra == 'deluxe'
75
+ Requires-Dist: pymcdm ; extra == 'deluxe'
76
+ Requires-Dist: pyvirtualdisplay ; extra == 'deluxe'
77
+ Requires-Dist: qbittorrent-api ; extra == 'deluxe'
78
+ Requires-Dist: scikit-learn ; extra == 'deluxe'
79
+ Requires-Dist: selenium ; extra == 'deluxe'
80
+ Requires-Dist: libtorrent ; python_full_version < '3.14' and extra == 'deluxe'
81
+ Requires-Dist: tqdm ; extra == 'deluxe'
82
+ Requires-Dist: wordllama>=0.2.7.post0 ; extra == 'deluxe'
83
+ Requires-Dist: xattr ; sys_platform != 'win32' and extra == 'deluxe'
84
+ Requires-Dist: black ; extra == 'dev'
85
+ Requires-Dist: isort ; extra == 'dev'
86
+ Requires-Dist: ssort ; extra == 'dev'
87
+ Requires-Dist: brotab ; extra == 'fat'
88
+ Requires-Dist: camelot-py>=0.11.0 ; extra == 'fat'
89
+ Requires-Dist: ghostscript ; extra == 'fat'
90
+ Requires-Dist: opencv-python ; extra == 'fat'
91
+ Requires-Dist: orjson ; extra == 'fat'
92
+ Requires-Dist: selenium-wire-lw ; extra == 'fat'
93
+ Requires-Dist: subliminal ; extra == 'fat'
94
+ Requires-Dist: textract-py3==2.0.1 ; extra == 'fat'
95
+ Requires-Dist: ruff ; extra == 'test'
96
+ Requires-Dist: pytest ; extra == 'test'
97
+ Requires-Dist: pytest-regressions ; extra == 'test'
98
+ Requires-Dist: freezegun ; extra == 'test'
99
+ Requires-Dist: pandas ; extra == 'test'
100
+ Requires-Dist: pyfakefs ; extra == 'test'
101
+ Requires-Python: >=3.12
102
+ Project-URL: documentation, https://github.com/chapmanjacobd/library#usage
103
+ Project-URL: homepage, https://github.com/chapmanjacobd/library#readme
104
+ Project-URL: repository, https://github.com/chapmanjacobd/library/
76
105
  Provides-Extra: deluxe
77
- Requires-Dist: aiohttp; extra == "deluxe"
78
- Requires-Dist: annoy; extra == "deluxe"
79
- Requires-Dist: catt; extra == "deluxe"
80
- Requires-Dist: curl-cffi; extra == "deluxe"
81
- Requires-Dist: geopandas; extra == "deluxe"
82
- Requires-Dist: img2pdf; extra == "deluxe"
83
- Requires-Dist: ocrmypdf; extra == "deluxe"
84
- Requires-Dist: openpyxl; extra == "deluxe"
85
- Requires-Dist: paramiko; extra == "deluxe"
86
- Requires-Dist: pdf2image; extra == "deluxe"
87
- Requires-Dist: pillow; extra == "deluxe"
88
- Requires-Dist: PyExifTool; extra == "deluxe"
89
- Requires-Dist: pymcdm; extra == "deluxe"
90
- Requires-Dist: pyvirtualdisplay; extra == "deluxe"
91
- Requires-Dist: qbittorrent-api; extra == "deluxe"
92
- Requires-Dist: scikit-learn; extra == "deluxe"
93
- Requires-Dist: selenium; extra == "deluxe"
94
- Requires-Dist: libtorrent; extra == "deluxe"
95
- Requires-Dist: tqdm; extra == "deluxe"
96
- Requires-Dist: wordllama>=0.2.7.post0; extra == "deluxe"
97
- Requires-Dist: xattr; extra == "deluxe"
106
+ Provides-Extra: dev
98
107
  Provides-Extra: fat
99
- Requires-Dist: brotab; extra == "fat"
100
- Requires-Dist: camelot-py>=0.11.0; extra == "fat"
101
- Requires-Dist: ghostscript; extra == "fat"
102
- Requires-Dist: opencv-python; extra == "fat"
103
- Requires-Dist: orjson; extra == "fat"
104
- Requires-Dist: selenium-wire-lw; extra == "fat"
105
- Requires-Dist: subliminal; extra == "fat"
106
- Requires-Dist: textract-py3==2.0.1; extra == "fat"
108
+ Provides-Extra: test
107
109
  Description-Content-Type: text/markdown
108
110
 
109
111
  # library (media toolkit)
@@ -118,7 +120,9 @@ tracking online video playlists (eg. YouTube subscriptions) and scheduling brows
118
120
 
119
121
  [![Downloads](https://static.pepy.tech/badge/library)](https://pepy.tech/project/library)
120
122
 
121
- ## Install
123
+ ## Getting started
124
+
125
+ <details><summary>Install</summary>
122
126
 
123
127
  Linux recommended but [Windows setup instructions](./Windows.md) available.
124
128
 
@@ -132,7 +136,31 @@ Required: `ffmpeg`
132
136
 
133
137
  Some features work better with: `mpv`, `fd-find`, `fish`
134
138
 
135
- ## Getting started
139
+ Optional Python extras are grouped under `library[deluxe]`. This includes
140
+ `gallery-dl` (used by `gallery-add`), `internetarchive` (used for Internet
141
+ Archive downloads), and `cdx_toolkit` (used by the `--webcache`/`--wayback`
142
+ CDX extractor):
143
+
144
+ pip install "library[deluxe]"
145
+
146
+ Torrent metadata commands (`torrents-add` and `torrents-dump`) require the
147
+ `libtorrent` Python bindings. They are included by `library[deluxe]` when using
148
+ Python 3.12 or 3.13; Python 3.14 wheels are not available yet, so use Python
149
+ 3.13 for torrent support.
150
+
151
+ If the PyPI wheel is unavailable for your Python or platform, these system
152
+ packages provide the bindings:
153
+
154
+ * Debian/Ubuntu: `sudo apt install python3-libtorrent`
155
+ * Fedora: `sudo dnf install rb_libtorrent-python3`
156
+ * macOS/Homebrew: `brew install libtorrent-rasterbar`
157
+
158
+ The Debian, Fedora, and Homebrew packages are built for their system or
159
+ Homebrew Python and may not be importable from a separate uv virtualenv. There
160
+ is no official Scoop package for the Python bindings; on Windows, use Python
161
+ 3.13 and `pip install "library[deluxe]"`.
162
+
163
+ </details>
136
164
 
137
165
  <details><summary>Local media</summary>
138
166
 
@@ -207,7 +235,7 @@ To stop playing press Ctrl+C in either the terminal or mpv
207
235
  <details><summary>List all subcommands</summary>
208
236
 
209
237
  $ library
210
- library (v3.2.007; 102 subcommands)
238
+ library (v3.2.011; 102 subcommands)
211
239
 
212
240
  Create database subcommands:
213
241
  ╭─────────────────┬──────────────────────────────────────────╮
@@ -905,6 +933,14 @@ BTW, for some cols like time_deleted you'll need to specify a where clause so th
905
933
  --page-key start --page-start 0 --page-step 50 --fixed-pages 14 --stop-pages-no-match 1 \
906
934
  plab.db https://plab/forum/tracker.php?o=(string replace ' ' \n -- 1 4 7 10 15)&s=2&tm=-1&f=(string replace ' ' \n -- 1670 1768 60 1671 1644 1672 1111 508 555 1112 1718 1143 1717 1851 1713 1712 1775 1674 902 1675 36 1830 1803 1831 1741 1676 1677 1780 1110 1124 1784 1769 1793 1797 1804 1819 1825 1836 1842 1846 1857 1861 1867 1451 1788 1789 1792 1798 1805 1820 1826 1837 1843 1847 1856 1862 1868 284 1853 1823 1800 1801 1719 997 1818 1849 1711 1791 1762)
907
935
 
936
+ Recursively add every link on a site (glob excludes prune subtrees from the crawl)
937
+
938
+ library links-add links.db --recursive https://example.com/docs/ --path-exclude '*/tags/*'
939
+
940
+ Add every archived link for a site from the Internet Archive CDX index
941
+
942
+ library links-add links.db --webcache https://example.com/docs/
943
+
908
944
 
909
945
  </details>
910
946
 
@@ -1238,6 +1274,31 @@ BTW, for some cols like time_deleted you'll need to specify a where clause so th
1238
1274
  library links https://en.wikipedia.org/wiki/List_of_bacon_dishes --path-include https://en.wikipedia.org/wiki/ --after-include famous
1239
1275
  https://en.wikipedia.org/wiki/Omelette
1240
1276
 
1277
+ Download the filtered links. Internet Archive details pages use the Internet Archive downloader and preserve all original files:
1278
+
1279
+ library extract-links --download https://readrare.com/
1280
+
1281
+ Mirror a site recursively. Only links within the same domain and parent directory as the given
1282
+ path are followed; links pointing elsewhere are downloaded but not followed. Includes/excludes
1283
+ accept globs, so you can prune whole subtrees while crawling:
1284
+
1285
+ library links --recursive --download https://example.com/docs/
1286
+ library links -R --download https://example.com/docs/ --path-exclude '*/tag/*' '*/tags/*'
1287
+
1288
+ Extract links from a web archive CDX index instead of crawling live pages. Internet Archive
1289
+ Wayback snapshots are emitted by default. Wayback URLs are collapsed to their original
1290
+ archived URL while crawling: sibling pages that only exist under a different timestamp are
1291
+ still followed, but multiple captures of the same page are collapsed so only one copy of
1292
+ each page is downloaded. Use --webcache-from/--webcache-to to target a time period:
1293
+
1294
+ library links --webcache https://example.com/docs/
1295
+ library links --webcache --recursive --download https://example.com/docs/
1296
+ library links --webcache --webcache-source cc https://example.com/docs/
1297
+ library links --webcache --webcache-from 2020 --webcache-to 2021 https://example.com/docs/
1298
+
1299
+ Common tracking and analytics URLs, including Google Tag Manager and Google Analytics,
1300
+ are excluded by default. Use --path-exclude to replace the default exclusions when needed.
1301
+
1241
1302
  Read from local clipboard and filter out links based on nearby plain text
1242
1303
 
1243
1304
  library links --local-html (cb -t text/html | psub) --after-exclude paranormal spooky horror podcast tech fantasy supernatural lecture sport
@@ -2428,7 +2489,7 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2428
2489
  | /home/xk/github/xk/lb/.ruff_cache/ | 19.5 kB | 100 |
2429
2490
  | /home/xk/github/xk/lb/.gitattributes | 119 Bytes | |
2430
2491
  | /home/xk/github/xk/lb/.mypy_cache/ | 280 Bytes | 4 |
2431
- | /home/xk/github/xk/lb/.pdm-python | 15 Bytes | |
2492
+ | /home/xk/github/xk/lb/.venv | 15 Bytes | |
2432
2493
 
2433
2494
  Only include files with a specific depth
2434
2495
 
@@ -2601,6 +2662,17 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2601
2662
  library download dl.db --include https://www.youtube.com/watch?v=YE7VzlLtp-4
2602
2663
  library download dl.db -s https://www.youtube.com/watch?v=YE7VzlLtp-4 # equivalent
2603
2664
 
2665
+ Recursively download a site (filesystem profile). Only links within the same domain and
2666
+ parent directory as the seed are followed; other links are downloaded but not followed.
2667
+
2668
+ library download links.db --filesystem --recursive --prefix ~/mirror/ https://example.com/docs/
2669
+
2670
+ Download a site's archived pages from the Internet Archive Wayback Machine using the CDX API.
2671
+ `--webcache` implies `--links`; add `--recursive` to also parse the archived pages for
2672
+ further links:
2673
+
2674
+ library download links.db --filesystem --webcache --prefix ~/mirror/ https://example.com/docs/
2675
+
2604
2676
  Maximizing the variety of subdomains
2605
2677
 
2606
2678
  library download photos.db --photos --image --sort "ROW_NUMBER() OVER ( PARTITION BY SUBSTR(m.path, INSTR(m.path, '//') + 2, INSTR( SUBSTR(m.path, INSTR(m.path, '//') + 2), '/') - 1) )"
@@ -2,11 +2,10 @@ import argparse, importlib, os, sys, textwrap
2
2
 
3
3
  from tabulate import tabulate
4
4
 
5
+ from library._version import __version__
5
6
  from library.utils import argparse_utils, iterables
6
7
  from library.utils.log_utils import log
7
8
 
8
- __version__ = "3.2.007"
9
-
10
9
  progs = {
11
10
  "Create database subcommands": {
12
11
  "fs_add": "Add local media",
@@ -0,0 +1,25 @@
1
+ # Copyright (c) 2026, Jacob Chapman
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version as installed_version
5
+ from pathlib import Path
6
+ import tomllib
7
+
8
+
9
+ def get_version() -> str:
10
+ pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml"
11
+ if pyproject_path.is_file():
12
+ with pyproject_path.open("rb") as pyproject_file:
13
+ project_version = tomllib.load(pyproject_file)["project"].get("version")
14
+ if isinstance(project_version, str):
15
+ return project_version
16
+ message = f"Missing string project version in {pyproject_path}"
17
+ raise RuntimeError(message)
18
+
19
+ try:
20
+ return installed_version("library")
21
+ except PackageNotFoundError as error:
22
+ raise RuntimeError("Unable to determine the library version") from error
23
+
24
+
25
+ __version__ = get_version()
@@ -2,9 +2,6 @@ import os
2
2
  from pathlib import Path
3
3
  from types import ModuleType
4
4
 
5
- import gallery_dl
6
- from gallery_dl.job import Job
7
-
8
5
  from library.mediadb import db_media, db_playlists
9
6
  from library.utils import consts, printing, strings
10
7
  from library.utils.log_utils import log
@@ -142,52 +139,57 @@ def download(args, m):
142
139
  )
143
140
 
144
141
 
145
- class UrlJob(Job):
146
- resolve = 1 # depth of queue resolution
142
+ def url_job_class(gallery_dl):
143
+ # gallery_dl is an optional (deluxe) dependency, so the Job base class
144
+ # can only be resolved once gallery_dl has been imported
145
+ class UrlJob(gallery_dl.job.Job):
146
+ resolve = 1 # depth of queue resolution
147
147
 
148
- def __init__(self, url, parent=None, resolve=None):
149
- super().__init__(url, parent)
150
- self.results = []
151
- self.visited = set()
152
- if resolve is not None:
153
- self.resolve = resolve
148
+ def __init__(self, url, parent=None, resolve=None):
149
+ super().__init__(url, parent)
150
+ self.results = []
151
+ self.visited = set()
152
+ if resolve is not None:
153
+ self.resolve = resolve
154
154
 
155
- if self.resolve > 0:
156
- self.handle_queue = self.handle_queue_resolve
155
+ if self.resolve > 0:
156
+ self.handle_queue = self.handle_queue_resolve
157
157
 
158
- def handle_url(self, url, kwdict):
159
- self.results.append((url, kwdict))
158
+ def handle_url(self, url, kwdict):
159
+ self.results.append((url, kwdict))
160
160
 
161
- def handle_queue(self, url, kwdict):
162
- # unresolved queue entry
163
- self.results.append((url, kwdict))
161
+ def handle_queue(self, url, kwdict):
162
+ # unresolved queue entry
163
+ self.results.append((url, kwdict))
164
164
 
165
- def handle_queue_resolve(self, url, kwdict):
166
- if url in self.visited:
167
- return
168
- self.visited.add(url)
165
+ def handle_queue_resolve(self, url, kwdict):
166
+ if url in self.visited:
167
+ return
168
+ self.visited.add(url)
169
169
 
170
- cls = kwdict.get("_extractor")
171
- if cls:
172
- extr = cls.from_url(url)
173
- else:
174
- extr = self.extractor.find(url)
170
+ cls = kwdict.get("_extractor")
171
+ if cls:
172
+ extr = cls.from_url(url)
173
+ else:
174
+ extr = self.extractor.find(url)
175
175
 
176
- if not extr:
177
- self.results.append((url, kwdict))
178
- return
176
+ if not extr:
177
+ self.results.append((url, kwdict))
178
+ return
179
179
 
180
- job = self.__class__(extr, self, self.resolve - 1)
181
- job.results = self.results # shared accumulator
182
- job.visited = self.visited # shared visited set
183
- job.run()
180
+ job = self.__class__(extr, self, self.resolve - 1)
181
+ job.results = self.results # shared accumulator
182
+ job.visited = self.visited # shared visited set
183
+ job.run()
184
+
185
+ return UrlJob
184
186
 
185
187
 
186
188
  def get_playlist_metadata(args, playlist_path):
187
189
  gallery_dl = load_module_level_gallery_dl(args)
188
190
 
189
191
  added_media_count = 0
190
- job = UrlJob(playlist_path)
192
+ job = url_job_class(gallery_dl)(playlist_path)
191
193
  job.run()
192
194
 
193
195
  is_playlist = len(job.results) > 1
@@ -130,6 +130,7 @@ To compensate for this the script will only continue fetching pages until there
130
130
  )
131
131
 
132
132
  arggroups.filter_links(parser)
133
+ arggroups.spider(parser)
133
134
 
134
135
  arggroups.requests(parser)
135
136
  arggroups.selenium(parser)
@@ -254,6 +255,8 @@ def extractor(args, playlist_path):
254
255
  new_media = set()
255
256
  end_of_playlist = False
256
257
  page_limit = args.backfill_pages or args.fixed_pages or args.max_pages
258
+ if args.recursive or args.webcache:
259
+ page_limit = 1
257
260
 
258
261
  if args.confirm_ready:
259
262
  if args.selenium:
@@ -283,8 +286,14 @@ def extractor(args, playlist_path):
283
286
  page_known = set()
284
287
  page_new = {}
285
288
  try:
286
- for link_dict in extract_links.get_inner_urls(args, page_path):
289
+ link_iter = (
290
+ extract_links.crawl(args, [page_path])
291
+ if args.recursive
292
+ else extract_links.iter_links(args, page_path)
293
+ )
294
+ for link_dict in link_iter:
287
295
  link = link_dict.pop("link")
296
+ link_dict.pop("mime", None)
288
297
 
289
298
  if link == args.stop_link:
290
299
  end_of_playlist = True
@@ -302,7 +311,7 @@ def extractor(args, playlist_path):
302
311
  printing.print_overwrite(f"Page {page_count} link scan: {len(page_new)} new [{len(page_known)} known]")
303
312
  print(file=sys.stderr)
304
313
 
305
- if not (args.backfill_pages or args.fixed_pages):
314
+ if not args.recursive and not (args.backfill_pages or args.fixed_pages):
306
315
  if (args.stop_known and len(page_known) > args.stop_known) or (
307
316
  args.stop_new and args.stop_new >= len(page_new)
308
317
  ):
@@ -35,6 +35,7 @@ def parse_args():
35
35
  arggroups.requests(parser)
36
36
  arggroups.selenium(parser)
37
37
  arggroups.filter_links(parser)
38
+ arggroups.spider(parser)
38
39
 
39
40
  parser.add_argument("--same-domain", action="store_true", help="Choose a random domain to focus on")
40
41
 
@@ -88,6 +89,9 @@ def download(args=None) -> None:
88
89
  db_playlists.create(args)
89
90
  db_media.create(args)
90
91
 
92
+ if args.recursive or args.webcache:
93
+ args.links = True
94
+
91
95
  m_columns = db_utils.columns(args, "media")
92
96
 
93
97
  if "limit" in args.defaults and "media" in args.db.table_names() and "webpath" in m_columns:
@@ -118,7 +122,7 @@ def download(args=None) -> None:
118
122
  media_printer.media_printer(args, media)
119
123
  return
120
124
 
121
- get_inner_urls = iterables.return_unique(extract_links.get_inner_urls, lambda d: d["link"])
125
+ get_inner_urls = iterables.return_unique(extract_links.iter_links, lambda d: d["link"])
122
126
  if args.safe and args.profile == DBType.image:
123
127
  gallery_backend.load_module_level_gallery_dl(args)
124
128
  for m in media:
@@ -180,7 +184,11 @@ def download(args=None) -> None:
180
184
  if args.links:
181
185
  dl_paths = []
182
186
  try:
183
- for link_dict in get_inner_urls(args, original_path):
187
+ if args.recursive:
188
+ link_iter = extract_links.crawl(args, [original_path])
189
+ else:
190
+ link_iter = get_inner_urls(args, original_path)
191
+ for link_dict in link_iter:
184
192
  dl_paths.append(link_dict["link"])
185
193
  except requests.HTTPError as excinfo:
186
194
  log.warning(
@@ -207,7 +215,7 @@ def download(args=None) -> None:
207
215
  for i, dl_path in enumerate(dl_paths):
208
216
  error = None
209
217
  try:
210
- local_path = web.download_url(args, dl_path)
218
+ local_path = web.download_link(args, dl_path)
211
219
  except RuntimeError as excinfo:
212
220
  local_path = None
213
221
  error = str(excinfo)
@@ -112,7 +112,7 @@ def gen_arg_groups(args):
112
112
  else:
113
113
  log.warning("No images found in %s", path)
114
114
  elif path_utils.ext(path) in consts.ARCHIVE_EXTENSIONS:
115
- archive_dir = processes.unar_delete(path)
115
+ archive_dir, _ = processes.unar_delete(path)
116
116
  archive_image_paths = shell_utils.rglob(str(archive_dir), DEFAULT_EXTENSIONS, quiet=True)[0]
117
117
  if archive_image_paths:
118
118
  yield archive_image_paths
@@ -52,7 +52,7 @@ def process_path(args, path) -> str | None:
52
52
  if args.simulate:
53
53
  log.info("Extracting images %s", path)
54
54
  else:
55
- archive_dir = processes.unar_delete(path)
55
+ archive_dir, _ = processes.unar_delete(path)
56
56
  image_paths = shell_utils.rglob(str(archive_dir), consts.IMAGE_EXTENSIONS, quiet=True)[0]
57
57
  for p in image_paths:
58
58
  process_path(args, p)