library 3.1.1__tar.gz → 3.2.2__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 (161) hide show
  1. {library-3.1.1 → library-3.2.2}/.github/README.md +32 -5
  2. {library-3.1.1 → library-3.2.2}/PKG-INFO +33 -6
  3. {library-3.1.1 → library-3.2.2}/library/__main__.py +1 -1
  4. {library-3.1.1 → library-3.2.2}/library/createdb/av.py +1 -1
  5. {library-3.1.1 → library-3.2.2}/library/createdb/fs_add.py +11 -9
  6. {library-3.1.1 → library-3.2.2}/library/createdb/gallery_add.py +4 -1
  7. {library-3.1.1 → library-3.2.2}/library/createdb/gallery_backend.py +15 -11
  8. {library-3.1.1 → library-3.2.2}/library/createdb/reddit_add.py +1 -1
  9. {library-3.1.1 → library-3.2.2}/library/createdb/tabs_add.py +1 -1
  10. {library-3.1.1 → library-3.2.2}/library/createdb/tube_backend.py +0 -1
  11. {library-3.1.1 → library-3.2.2}/library/files/llm_map.py +2 -0
  12. {library-3.1.1 → library-3.2.2}/library/files/sample_compare.py +9 -7
  13. {library-3.1.1 → library-3.2.2}/library/folders/merge_mv.py +4 -1
  14. {library-3.1.1 → library-3.2.2}/library/folders/scatter.py +6 -1
  15. {library-3.1.1 → library-3.2.2}/library/mediadb/block.py +6 -2
  16. {library-3.1.1 → library-3.2.2}/library/mediadb/db_media.py +1 -1
  17. {library-3.1.1 → library-3.2.2}/library/mediadb/db_playlists.py +35 -11
  18. {library-3.1.1 → library-3.2.2}/library/mediadb/download.py +3 -1
  19. {library-3.1.1 → library-3.2.2}/library/mediadb/history.py +12 -1
  20. {library-3.1.1 → library-3.2.2}/library/mediafiles/media_check.py +1 -1
  21. {library-3.1.1 → library-3.2.2}/library/mediafiles/process_ffmpeg.py +1 -1
  22. {library-3.1.1 → library-3.2.2}/library/mediafiles/process_image.py +1 -1
  23. {library-3.1.1 → library-3.2.2}/library/mediafiles/unardel.py +4 -5
  24. {library-3.1.1 → library-3.2.2}/library/playback/media_player.py +1 -1
  25. {library-3.1.1 → library-3.2.2}/library/playback/media_printer.py +4 -4
  26. library-3.2.2/library/tablefiles/plot.py +309 -0
  27. {library-3.1.1 → library-3.2.2}/library/text/nouns.py +1 -1
  28. {library-3.1.1 → library-3.2.2}/library/usage.py +31 -4
  29. {library-3.1.1 → library-3.2.2}/library/utils/arg_utils.py +6 -5
  30. {library-3.1.1 → library-3.2.2}/library/utils/arggroups.py +511 -29
  31. {library-3.1.1 → library-3.2.2}/library/utils/consts.py +3 -3
  32. {library-3.1.1 → library-3.2.2}/library/utils/filter_engine.py +2 -10
  33. {library-3.1.1 → library-3.2.2}/library/utils/iterables.py +2 -2
  34. {library-3.1.1 → library-3.2.2}/library/utils/nums.py +13 -2
  35. {library-3.1.1 → library-3.2.2}/library/utils/objects.py +20 -0
  36. {library-3.1.1 → library-3.2.2}/library/utils/processes.py +1 -1
  37. {library-3.1.1 → library-3.2.2}/library/utils/sqlgroups.py +1 -0
  38. {library-3.1.1 → library-3.2.2}/library/utils/strings.py +6 -6
  39. {library-3.1.1 → library-3.2.2}/library/utils/web.py +56 -9
  40. {library-3.1.1 → library-3.2.2}/pyproject.toml +1 -1
  41. library-3.1.1/library/tablefiles/plot.py +0 -94
  42. {library-3.1.1 → library-3.2.2}/LICENSE +0 -0
  43. {library-3.1.1 → library-3.2.2}/library/__init__.py +0 -0
  44. {library-3.1.1 → library-3.2.2}/library/assets/__init__.py +0 -0
  45. {library-3.1.1 → library-3.2.2}/library/assets/calibre.css +0 -0
  46. {library-3.1.1 → library-3.2.2}/library/assets/kotobago.png +0 -0
  47. {library-3.1.1 → library-3.2.2}/library/createdb/__init__.py +0 -0
  48. {library-3.1.1 → library-3.2.2}/library/createdb/computer_info.py +0 -0
  49. {library-3.1.1 → library-3.2.2}/library/createdb/computers_add.py +0 -0
  50. {library-3.1.1 → library-3.2.2}/library/createdb/fs_add_metadata.py +0 -0
  51. {library-3.1.1 → library-3.2.2}/library/createdb/getty_add.py +0 -0
  52. {library-3.1.1 → library-3.2.2}/library/createdb/hn_add.py +0 -0
  53. {library-3.1.1 → library-3.2.2}/library/createdb/links_add.py +0 -0
  54. {library-3.1.1 → library-3.2.2}/library/createdb/nicotine_import.py +0 -0
  55. {library-3.1.1 → library-3.2.2}/library/createdb/places_import.py +0 -0
  56. {library-3.1.1 → library-3.2.2}/library/createdb/row_add.py +0 -0
  57. {library-3.1.1 → library-3.2.2}/library/createdb/site_add.py +0 -0
  58. {library-3.1.1 → library-3.2.2}/library/createdb/substack.py +0 -0
  59. {library-3.1.1 → library-3.2.2}/library/createdb/subtitle.py +0 -0
  60. {library-3.1.1 → library-3.2.2}/library/createdb/tables_add.py +0 -0
  61. {library-3.1.1 → library-3.2.2}/library/createdb/tildes.py +0 -0
  62. {library-3.1.1 → library-3.2.2}/library/createdb/torrents_add.py +0 -0
  63. {library-3.1.1 → library-3.2.2}/library/createdb/tube_add.py +0 -0
  64. {library-3.1.1 → library-3.2.2}/library/createdb/web_add.py +0 -0
  65. {library-3.1.1 → library-3.2.2}/library/data/__init__.py +0 -0
  66. {library-3.1.1 → library-3.2.2}/library/data/dictionary.pkl +0 -0
  67. {library-3.1.1 → library-3.2.2}/library/data/ffmpeg_errors.py +0 -0
  68. {library-3.1.1 → library-3.2.2}/library/data/http_errors.py +0 -0
  69. {library-3.1.1 → library-3.2.2}/library/data/imagemagick_errors.py +0 -0
  70. {library-3.1.1 → library-3.2.2}/library/data/unar_errors.py +0 -0
  71. {library-3.1.1 → library-3.2.2}/library/data/wordbank.py +0 -0
  72. {library-3.1.1 → library-3.2.2}/library/data/yt_dlp_errors.py +0 -0
  73. {library-3.1.1 → library-3.2.2}/library/editdb/__init__.py +0 -0
  74. {library-3.1.1 → library-3.2.2}/library/editdb/dedupe_db.py +0 -0
  75. {library-3.1.1 → library-3.2.2}/library/editdb/dedupe_media.py +0 -0
  76. {library-3.1.1 → library-3.2.2}/library/editdb/merge_online_local.py +0 -0
  77. {library-3.1.1 → library-3.2.2}/library/editdb/mpv_watchlater.py +0 -0
  78. {library-3.1.1 → library-3.2.2}/library/editdb/pushshift.py +0 -0
  79. {library-3.1.1 → library-3.2.2}/library/editdb/reddit_selftext.py +0 -0
  80. {library-3.1.1 → library-3.2.2}/library/files/__init__.py +0 -0
  81. {library-3.1.1 → library-3.2.2}/library/files/christen.py +0 -0
  82. {library-3.1.1 → library-3.2.2}/library/files/sample_hash.py +0 -0
  83. {library-3.1.1 → library-3.2.2}/library/files/similar_files.py +0 -0
  84. {library-3.1.1 → library-3.2.2}/library/folders/__init__.py +0 -0
  85. {library-3.1.1 → library-3.2.2}/library/folders/big_dirs.py +0 -0
  86. {library-3.1.1 → library-3.2.2}/library/folders/filter_src.py +0 -0
  87. {library-3.1.1 → library-3.2.2}/library/folders/mergerfs_cp.py +0 -0
  88. {library-3.1.1 → library-3.2.2}/library/folders/mount_stats.py +0 -0
  89. {library-3.1.1 → library-3.2.2}/library/folders/move_list.py +0 -0
  90. {library-3.1.1 → library-3.2.2}/library/folders/similar_folders.py +0 -0
  91. {library-3.1.1 → library-3.2.2}/library/fsdb/__init__.py +0 -0
  92. {library-3.1.1 → library-3.2.2}/library/fsdb/disk_usage.py +0 -0
  93. {library-3.1.1 → library-3.2.2}/library/fsdb/filesystem.py +0 -0
  94. {library-3.1.1 → library-3.2.2}/library/fsdb/folder_stats.py +0 -0
  95. {library-3.1.1 → library-3.2.2}/library/fsdb/search_db.py +0 -0
  96. {library-3.1.1 → library-3.2.2}/library/lb.py +0 -0
  97. {library-3.1.1 → library-3.2.2}/library/mediadb/__init__.py +0 -0
  98. {library-3.1.1 → library-3.2.2}/library/mediadb/db_history.py +0 -0
  99. {library-3.1.1 → library-3.2.2}/library/mediadb/download_status.py +0 -0
  100. {library-3.1.1 → library-3.2.2}/library/mediadb/history_add.py +0 -0
  101. {library-3.1.1 → library-3.2.2}/library/mediadb/optimize_db.py +0 -0
  102. {library-3.1.1 → library-3.2.2}/library/mediadb/playlists.py +0 -0
  103. {library-3.1.1 → library-3.2.2}/library/mediadb/redownload.py +0 -0
  104. {library-3.1.1 → library-3.2.2}/library/mediadb/search.py +0 -0
  105. {library-3.1.1 → library-3.2.2}/library/mediadb/stats.py +0 -0
  106. {library-3.1.1 → library-3.2.2}/library/mediafiles/__init__.py +0 -0
  107. {library-3.1.1 → library-3.2.2}/library/mediafiles/images_to_pdf.py +0 -0
  108. {library-3.1.1 → library-3.2.2}/library/mediafiles/pdf_edit.py +0 -0
  109. {library-3.1.1 → library-3.2.2}/library/mediafiles/process_media.py +0 -0
  110. {library-3.1.1 → library-3.2.2}/library/mediafiles/process_text.py +0 -0
  111. {library-3.1.1 → library-3.2.2}/library/mediafiles/torrents_dump.py +0 -0
  112. {library-3.1.1 → library-3.2.2}/library/mediafiles/torrents_start.py +0 -0
  113. {library-3.1.1 → library-3.2.2}/library/misc/__init__.py +0 -0
  114. {library-3.1.1 → library-3.2.2}/library/misc/dedupe_czkawka.py +0 -0
  115. {library-3.1.1 → library-3.2.2}/library/misc/export_text.py +0 -0
  116. {library-3.1.1 → library-3.2.2}/library/misc/search_help.py +0 -0
  117. {library-3.1.1 → library-3.2.2}/library/multidb/__init__.py +0 -0
  118. {library-3.1.1 → library-3.2.2}/library/multidb/allocate_torrents.py +0 -0
  119. {library-3.1.1 → library-3.2.2}/library/multidb/copy_play_counts.py +0 -0
  120. {library-3.1.1 → library-3.2.2}/library/multidb/merge_dbs.py +0 -0
  121. {library-3.1.1 → library-3.2.2}/library/playback/__init__.py +0 -0
  122. {library-3.1.1 → library-3.2.2}/library/playback/links_open.py +0 -0
  123. {library-3.1.1 → library-3.2.2}/library/playback/play_actions.py +0 -0
  124. {library-3.1.1 → library-3.2.2}/library/playback/playback_control.py +0 -0
  125. {library-3.1.1 → library-3.2.2}/library/playback/post_actions.py +0 -0
  126. {library-3.1.1 → library-3.2.2}/library/playback/surf.py +0 -0
  127. {library-3.1.1 → library-3.2.2}/library/playback/tabs_open.py +0 -0
  128. {library-3.1.1 → library-3.2.2}/library/playback/torrents_info.py +0 -0
  129. {library-3.1.1 → library-3.2.2}/library/playback/torrents_remaining.py +0 -0
  130. {library-3.1.1 → library-3.2.2}/library/readme.py +0 -0
  131. {library-3.1.1 → library-3.2.2}/library/tablefiles/__init__.py +0 -0
  132. {library-3.1.1 → library-3.2.2}/library/tablefiles/columns.py +0 -0
  133. {library-3.1.1 → library-3.2.2}/library/tablefiles/eda.py +0 -0
  134. {library-3.1.1 → library-3.2.2}/library/tablefiles/incremental_diff.py +0 -0
  135. {library-3.1.1 → library-3.2.2}/library/tablefiles/markdown_tables.py +0 -0
  136. {library-3.1.1 → library-3.2.2}/library/tablefiles/mcda.py +0 -0
  137. {library-3.1.1 → library-3.2.2}/library/text/__init__.py +0 -0
  138. {library-3.1.1 → library-3.2.2}/library/text/cluster_sort.py +0 -0
  139. {library-3.1.1 → library-3.2.2}/library/text/combinations.py +0 -0
  140. {library-3.1.1 → library-3.2.2}/library/text/expand_links.py +0 -0
  141. {library-3.1.1 → library-3.2.2}/library/text/extract_links.py +0 -0
  142. {library-3.1.1 → library-3.2.2}/library/text/extract_text.py +0 -0
  143. {library-3.1.1 → library-3.2.2}/library/text/json_keys_rename.py +0 -0
  144. {library-3.1.1 → library-3.2.2}/library/text/markdown_links.py +0 -0
  145. {library-3.1.1 → library-3.2.2}/library/text/regex_sort.py +0 -0
  146. {library-3.1.1 → library-3.2.2}/library/text/timestamps.py +0 -0
  147. {library-3.1.1 → library-3.2.2}/library/utils/__init__.py +0 -0
  148. {library-3.1.1 → library-3.2.2}/library/utils/argparse_utils.py +0 -0
  149. {library-3.1.1 → library-3.2.2}/library/utils/date_utils.py +0 -0
  150. {library-3.1.1 → library-3.2.2}/library/utils/db_utils.py +0 -0
  151. {library-3.1.1 → library-3.2.2}/library/utils/devices.py +0 -0
  152. {library-3.1.1 → library-3.2.2}/library/utils/file_utils.py +0 -0
  153. {library-3.1.1 → library-3.2.2}/library/utils/gui.py +0 -0
  154. {library-3.1.1 → library-3.2.2}/library/utils/log_utils.py +0 -0
  155. {library-3.1.1 → library-3.2.2}/library/utils/mpv_utils.py +0 -0
  156. {library-3.1.1 → library-3.2.2}/library/utils/path_utils.py +0 -0
  157. {library-3.1.1 → library-3.2.2}/library/utils/pd_utils.py +0 -0
  158. {library-3.1.1 → library-3.2.2}/library/utils/printing.py +0 -0
  159. {library-3.1.1 → library-3.2.2}/library/utils/remote_processes.py +0 -0
  160. {library-3.1.1 → library-3.2.2}/library/utils/shell_utils.py +0 -0
  161. {library-3.1.1 → library-3.2.2}/library/utils/sql_utils.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.1.001; 103 subcommands)
102
+ library (v3.2.002; 103 subcommands)
103
103
 
104
104
  Create database subcommands:
105
105
  ╭─────────────────┬──────────────────────────────────────────╮
@@ -1859,12 +1859,35 @@ BTW, for some cols like time_deleted you'll need to specify a where clause so th
1859
1859
  <details><summary>Plot table-like files. A CLI interface to matplotlib</summary>
1860
1860
 
1861
1861
  $ library plot -h
1862
- usage: library plot PATH ... [--table STR] [--end-row INT]
1862
+ usage: library plot PATH ... [--table STR] [--end-row INT] -- [PLT.COMMAND ...]
1863
1863
 
1864
1864
  Plot one or more files
1865
1865
 
1866
1866
  Only 500,000 rows per file are loaded for performance purposes. Set `--end-row inf` to read all the rows and/or run out of RAM.
1867
1867
 
1868
+ With no commands after `--`, the table is analyzed like `library eda` and a set of figures is generated: histograms for numeric columns, bar charts of common values for low/medium-cardinality columns, time-series when a date column is detected, pairwise scatter plots of the most correlated numeric columns, a correlation heatmap, and a missing-values chart.
1869
+
1870
+ Histograms get a fitted PDF overlay, and each numeric column is drawn on a scale matched to its distribution (D'Agostino-Pearson test): normal data on a linear axis, log-normal or heavy right-skewed positive data on a log axis, data spanning orders of magnitude that includes zero/negative values on a symlog axis, and proportions in (0,1) on a logit axis. `--scale log|symlog|logit` forces that scale on all histograms; `--scale off` disables detection and draws plain linear histograms.
1871
+
1872
+ Pass a list of matplotlib.pyplot commands after `--` for manual control. Each command is a plt function name followed by column names (the first column is the x-axis, the rest are y-axes) and optional positional/keyword arguments. `index` refers to the row index. A single column is passed as a single dataset (e.g. for hist).
1873
+
1874
+ Axes are auto-labeled from the column names and units are guessed from common names (e.g. `size` -> bytes, `duration` -> seconds, `time_*` -> dates). A title is auto-added from the plotted columns (e.g. `scatter size duration` -> "Duration vs Size"). Pass an explicit `xlabel=` / `ylabel=` / `title=` to override.
1875
+
1876
+ A transformation pipeline can be applied before plotting: `--where` filters rows, `--classify` derives a `category` column from an expression or binning, and `--groupby` (with `--agg`) groups and aggregates rows. With `--groupby` the auto mode draws one bar chart per aggregated column (e.g. "Count per Category").
1877
+
1878
+ Examples:
1879
+
1880
+ lb plot data.csv
1881
+ lb plot data.csv -- plot x y
1882
+ lb plot data.csv -- scatter x y s=3 alpha=0.5 label=points
1883
+ lb plot data.csv -- hist x bins=50
1884
+ lb plot audio.db --table media --cols size,duration -- scatter size duration
1885
+ lb plot a.csv b.csv --join-tables -- sort -u time_created desc -- plot index time_created
1886
+ lb plot data.csv --where 'duration > 60'
1887
+ lb plot data.csv --classify 'size > 1GB' --groupby category
1888
+ lb plot data.csv --classify duration --bins 5 --groupby category --agg mean
1889
+ lb plot data.csv --groupby category --agg count --top 10
1890
+
1868
1891
 
1869
1892
  </details>
1870
1893
 
@@ -2591,7 +2614,7 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2591
2614
  <details><summary>Show and manage playback history</summary>
2592
2615
 
2593
2616
  $ library history -h
2594
- usage: library history [--frequency daily weekly (monthly) yearly] [--limit LIMIT] DATABASE [(all) watching watched created modified deleted]
2617
+ usage: library history [--frequency daily weekly (monthly) yearly] [--limit LIMIT] DATABASE [PATH ...] [(all) watching watched created modified deleted]
2595
2618
 
2596
2619
  View playback history
2597
2620
 
@@ -2617,8 +2640,12 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2617
2640
  Delete all history
2618
2641
  library history web_add.image.db -L inf --delete-rows
2619
2642
 
2620
- See also: library stats -h
2621
- library history-add -h
2643
+ Delete history for exact paths
2644
+ library history ~/mc/links.db --delete-rows https://example.com/article
2645
+ cb | library history ~/mc/links.db --delete-rows -
2646
+
2647
+ See also: library stats -h
2648
+ library history-add -h
2622
2649
 
2623
2650
 
2624
2651
  </details>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: library
3
- Version: 3.1.1
3
+ Version: 3.2.2
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.1.001; 103 subcommands)
210
+ library (v3.2.002; 103 subcommands)
211
211
 
212
212
  Create database subcommands:
213
213
  ╭─────────────────┬──────────────────────────────────────────╮
@@ -1967,12 +1967,35 @@ BTW, for some cols like time_deleted you'll need to specify a where clause so th
1967
1967
  <details><summary>Plot table-like files. A CLI interface to matplotlib</summary>
1968
1968
 
1969
1969
  $ library plot -h
1970
- usage: library plot PATH ... [--table STR] [--end-row INT]
1970
+ usage: library plot PATH ... [--table STR] [--end-row INT] -- [PLT.COMMAND ...]
1971
1971
 
1972
1972
  Plot one or more files
1973
1973
 
1974
1974
  Only 500,000 rows per file are loaded for performance purposes. Set `--end-row inf` to read all the rows and/or run out of RAM.
1975
1975
 
1976
+ With no commands after `--`, the table is analyzed like `library eda` and a set of figures is generated: histograms for numeric columns, bar charts of common values for low/medium-cardinality columns, time-series when a date column is detected, pairwise scatter plots of the most correlated numeric columns, a correlation heatmap, and a missing-values chart.
1977
+
1978
+ Histograms get a fitted PDF overlay, and each numeric column is drawn on a scale matched to its distribution (D'Agostino-Pearson test): normal data on a linear axis, log-normal or heavy right-skewed positive data on a log axis, data spanning orders of magnitude that includes zero/negative values on a symlog axis, and proportions in (0,1) on a logit axis. `--scale log|symlog|logit` forces that scale on all histograms; `--scale off` disables detection and draws plain linear histograms.
1979
+
1980
+ Pass a list of matplotlib.pyplot commands after `--` for manual control. Each command is a plt function name followed by column names (the first column is the x-axis, the rest are y-axes) and optional positional/keyword arguments. `index` refers to the row index. A single column is passed as a single dataset (e.g. for hist).
1981
+
1982
+ Axes are auto-labeled from the column names and units are guessed from common names (e.g. `size` -> bytes, `duration` -> seconds, `time_*` -> dates). A title is auto-added from the plotted columns (e.g. `scatter size duration` -> "Duration vs Size"). Pass an explicit `xlabel=` / `ylabel=` / `title=` to override.
1983
+
1984
+ A transformation pipeline can be applied before plotting: `--where` filters rows, `--classify` derives a `category` column from an expression or binning, and `--groupby` (with `--agg`) groups and aggregates rows. With `--groupby` the auto mode draws one bar chart per aggregated column (e.g. "Count per Category").
1985
+
1986
+ Examples:
1987
+
1988
+ lb plot data.csv
1989
+ lb plot data.csv -- plot x y
1990
+ lb plot data.csv -- scatter x y s=3 alpha=0.5 label=points
1991
+ lb plot data.csv -- hist x bins=50
1992
+ lb plot audio.db --table media --cols size,duration -- scatter size duration
1993
+ lb plot a.csv b.csv --join-tables -- sort -u time_created desc -- plot index time_created
1994
+ lb plot data.csv --where 'duration > 60'
1995
+ lb plot data.csv --classify 'size > 1GB' --groupby category
1996
+ lb plot data.csv --classify duration --bins 5 --groupby category --agg mean
1997
+ lb plot data.csv --groupby category --agg count --top 10
1998
+
1976
1999
 
1977
2000
  </details>
1978
2001
 
@@ -2699,7 +2722,7 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2699
2722
  <details><summary>Show and manage playback history</summary>
2700
2723
 
2701
2724
  $ library history -h
2702
- usage: library history [--frequency daily weekly (monthly) yearly] [--limit LIMIT] DATABASE [(all) watching watched created modified deleted]
2725
+ usage: library history [--frequency daily weekly (monthly) yearly] [--limit LIMIT] DATABASE [PATH ...] [(all) watching watched created modified deleted]
2703
2726
 
2704
2727
  View playback history
2705
2728
 
@@ -2725,8 +2748,12 @@ Inspired somewhat by https://nikkhokkho.sourceforge.io/?page=FileOptimizer
2725
2748
  Delete all history
2726
2749
  library history web_add.image.db -L inf --delete-rows
2727
2750
 
2728
- See also: library stats -h
2729
- library history-add -h
2751
+ Delete history for exact paths
2752
+ library history ~/mc/links.db --delete-rows https://example.com/article
2753
+ cb | library history ~/mc/links.db --delete-rows -
2754
+
2755
+ See also: library stats -h
2756
+ library history-add -h
2730
2757
 
2731
2758
 
2732
2759
  </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.1.001"
8
+ __version__ = "3.2.002"
9
9
 
10
10
  progs = {
11
11
  "Create database subcommands": {
@@ -213,7 +213,7 @@ def munge_av_tags(args, m) -> dict:
213
213
  args.delete_corrupt, corruption, duration
214
214
  ) and not file_utils.is_file_open(path):
215
215
  threshold_str = (
216
- strings.percent(args.delete_corrupt) if 0 < args.delete_corrupt < 1 else (args.delete_corrupt + "s")
216
+ strings.percent(args.delete_corrupt) if 0 < args.delete_corrupt < 1 else f"{args.delete_corrupt}s"
217
217
  )
218
218
  log.warning(
219
219
  "Deleting %s corruption %.1f%% exceeded threshold %s", path, corruption * 100, threshold_str
@@ -77,15 +77,17 @@ def extract_chunk(args, media) -> None:
77
77
  is_scan_all_files = getattr(args, "scan_all_files", False)
78
78
 
79
79
  if objects.is_profile(args, DBType.image):
80
- image_media = [
81
- m for m in media if m["path"].rsplit(".", 1)[-1].lower() in consts.IMAGE_EXTENSIONS or is_scan_all_files
82
- ]
80
+ if is_scan_all_files:
81
+ image_media = list(media)
82
+ other_media = []
83
+ else:
84
+ image_media, other_media = [], []
85
+ for m in media:
86
+ if m["path"].rsplit(".", 1)[-1].lower() in consts.IMAGE_EXTENSIONS:
87
+ image_media.append(m)
88
+ else:
89
+ other_media.append(m)
83
90
  if image_media:
84
- other_media = [
85
- m
86
- for m in media
87
- if m["path"].rsplit(".", 1)[-1].lower() not in consts.IMAGE_EXTENSIONS or is_scan_all_files
88
- ]
89
91
  image_media = extract_image_metadata_chunk(image_media)
90
92
  media = image_media + other_media
91
93
 
@@ -105,7 +107,7 @@ def extract_chunk(args, media) -> None:
105
107
  if description:
106
108
  tags += "\n" + description
107
109
  if tags:
108
- caption["captions_t0"] = {"time": 0, "text": tags}
110
+ caption["caption_t0"] = {"time": 0, "text": tags}
109
111
 
110
112
  captions.append(caption)
111
113
 
@@ -58,12 +58,15 @@ def gallery_add(args=None) -> None:
58
58
  if not args.force and len(paths) > 9:
59
59
  known_playlists = db_media.get_paths(args)
60
60
 
61
+ if args.safe:
62
+ gallery_backend.load_module_level_gallery_dl(args)
63
+
61
64
  for path in paths:
62
65
  if path in known_playlists:
63
66
  log.info("Known already. Skipping %s", path)
64
67
  continue
65
68
 
66
- if args.safe and not gallery_backend.is_supported(args, path):
69
+ if args.safe and not gallery_backend.is_supported(path):
67
70
  log.info("Skipping unsupported playlist (safe_mode) %s", path)
68
71
  continue
69
72
 
@@ -12,7 +12,7 @@ from library.utils.log_utils import log
12
12
  gallery_dl = None
13
13
 
14
14
 
15
- def load_module_level_gallery_dl(args) -> ModuleType:
15
+ def load_module_level_gallery_dl(args=None) -> ModuleType:
16
16
  global gallery_dl
17
17
 
18
18
  if gallery_dl is None:
@@ -20,12 +20,6 @@ def load_module_level_gallery_dl(args) -> ModuleType:
20
20
 
21
21
  gallery_dl.config.load()
22
22
 
23
- download_archive = Path(args.download_archive or "~/.local/share/gallerydl.sqlite3").expanduser().resolve()
24
- if download_archive.exists():
25
- gallery_dl.config.set(("extractor",), "archive", str(download_archive))
26
-
27
- if hasattr(args, "prefix"):
28
- gallery_dl.config.set(("extractor",), "base-directory", args.prefix)
29
23
  gallery_dl.config.set(("extractor",), "parent-directory", False)
30
24
  gallery_dl.config.set(
31
25
  ("extractor",),
@@ -47,13 +41,23 @@ def load_module_level_gallery_dl(args) -> ModuleType:
47
41
  if consts.PYTEST_RUNNING:
48
42
  gallery_dl.config.set(("extractor",), "download", False)
49
43
 
44
+ is_supported.extractors = gallery_dl.extractor.extractors()
45
+
46
+ if args is not None:
47
+ is_supported.configured = True
48
+ download_archive = Path(args.download_archive or "~/.local/share/gallerydl.sqlite3").expanduser().resolve()
49
+ if download_archive.exists():
50
+ gallery_dl.config.set(("extractor",), "archive", str(download_archive))
51
+
52
+ if hasattr(args, "prefix"):
53
+ gallery_dl.config.set(("extractor",), "base-directory", args.prefix)
54
+
50
55
  return gallery_dl
51
56
 
52
57
 
53
- def is_supported(args, url) -> bool:
54
- if getattr(is_supported, "extractors", None) is None:
55
- gallery_dl = load_module_level_gallery_dl(args)
56
- is_supported.extractors = gallery_dl.extractor.extractors()
58
+ def is_supported(url) -> bool:
59
+ if not getattr(is_supported, "configured", False):
60
+ raise RuntimeError("gallery_backend.is_supported() requires load_module_level_gallery_dl(args) to be called first")
57
61
 
58
62
  return any(ie.pattern.match(url) and ie.category != "generic" for ie in is_supported.extractors)
59
63
 
@@ -141,7 +141,7 @@ def slim_post_data(d: dict, subreddit=None) -> dict:
141
141
  skip_domains = ["linktr.ee", "twitter.com", "t.me", "patreon", "onlyfans", "fans.ly", "file-upload", "file-link"]
142
142
  url = d.get("url")
143
143
  if not url or any(domain in url for domain in skip_domains):
144
- d["url"] = d.get("url_overridden_by_dest")
144
+ url = d.get("url_overridden_by_dest")
145
145
 
146
146
  selftext = d.get("selftext")
147
147
  if selftext and selftext in (
@@ -29,7 +29,7 @@ def parse_args() -> argparse.Namespace:
29
29
 
30
30
 
31
31
  def get_days(frequency):
32
- d = {"weekly": 7, "monthly": 30, "quarterly": 89, "yearly": 364, "decadally": 3640}
32
+ d = {"weekly": 7, "monthly": 30, "quarterly": 91, "yearly": 364, "decadally": 3640}
33
33
  return d.get(frequency, 7)
34
34
 
35
35
 
@@ -316,7 +316,6 @@ def get_extra_metadata(args, playlist_path, playlist_dl_opts=None) -> list[dict]
316
316
  if len(captions) > 0:
317
317
  args.db["captions"].insert_all(captions, alter=True)
318
318
 
319
- entry["id"] = media_id
320
319
  entry["playlists_id"] = playlists_id
321
320
  entry["chapter_count"] = chapter_count
322
321
 
@@ -79,6 +79,7 @@ def llm_map():
79
79
  import pandas as pd
80
80
 
81
81
  results = []
82
+ base_llama_args = list(args.llama_args)
82
83
  for path in gen_paths(args):
83
84
  prompt = args.prompt
84
85
 
@@ -91,6 +92,7 @@ def llm_map():
91
92
  for k, v in replacements.items():
92
93
  prompt = prompt.replace(k, "\n" + v + "\n\n")
93
94
 
95
+ args.llama_args = list(base_llama_args)
94
96
  if args.image_model:
95
97
  args.llama_args += ["--image", str(Path(path).absolute())]
96
98
  elif args.text:
@@ -1,6 +1,5 @@
1
1
  import hashlib
2
2
  from concurrent.futures import ThreadPoolExecutor
3
- from contextlib import suppress
4
3
  from pathlib import Path
5
4
 
6
5
  from library import usage
@@ -45,13 +44,16 @@ def full_hash_compare(paths):
45
44
 
46
45
 
47
46
  def sample_cmp(*paths, threads=1, gap=0.1, chunk_size=None, ignore_holes=False, skip_full_hash=False):
48
- if len(paths) < 2:
49
- raise ValueError("Not enough paths. Include 2 or more paths to compare")
50
-
51
47
  path_stats = {}
48
+ existing_paths = []
52
49
  for path in paths:
53
- with suppress(FileNotFoundError):
50
+ try:
54
51
  path_stats[path] = Path(path).stat()
52
+ existing_paths.append(path)
53
+ except FileNotFoundError:
54
+ log.error("File not found %s", path)
55
+ if len(existing_paths) < 2:
56
+ raise ValueError("Not enough paths. Include 2 or more paths to compare")
55
57
 
56
58
  sizes = [stat_res.st_size for stat_res in path_stats.values()]
57
59
  if not all(size == sizes[0] for size in sizes):
@@ -71,7 +73,7 @@ def sample_cmp(*paths, threads=1, gap=0.1, chunk_size=None, ignore_holes=False,
71
73
  with ThreadPoolExecutor(max_workers=4) as pool:
72
74
  futures = {
73
75
  path: pool.submit(sample_hash.sample_hash_file, path, threads=threads, gap=gap, chunk_size=chunk_size)
74
- for path in paths
76
+ for path in existing_paths
75
77
  }
76
78
 
77
79
  paths_dict = {}
@@ -87,7 +89,7 @@ def sample_cmp(*paths, threads=1, gap=0.1, chunk_size=None, ignore_holes=False,
87
89
  if is_equal:
88
90
  if skip_full_hash:
89
91
  log.info("Files might be equal:\n%s", paths_str)
90
- elif full_hash_compare(paths):
92
+ elif full_hash_compare(existing_paths):
91
93
  log.info("Files are equal:\n%s", paths_str)
92
94
  else:
93
95
  log.info("Files are similar but NOT equal:\n%s", paths_str)
@@ -5,7 +5,7 @@ from collections.abc import Iterable
5
5
  from library import usage
6
6
  from library.folders import filter_src
7
7
  from library.folders.filter_src import process_time_filters, track_moved
8
- from library.utils import arggroups, argparse_utils, consts, devices, path_utils, shell_utils
8
+ from library.utils import arggroups, argparse_utils, consts, devices, path_utils, processes, shell_utils
9
9
  from library.utils.consts import DBType
10
10
  from library.utils.log_utils import log
11
11
  from library.utils.shell_utils import rglob_gen
@@ -265,6 +265,9 @@ def gen_src_dest(args, sources: Iterable[str], destination: str, shortcut_allowe
265
265
 
266
266
 
267
267
  def mmv_folders(args, mv_fn, sources, destination, shortcut_allowed=False):
268
+ if sources is None:
269
+ processes.exit_error("No paths passed in")
270
+
268
271
  destination = os.path.realpath(destination) + (os.sep if destination.endswith(os.sep) else "")
269
272
 
270
273
  sources = (
@@ -217,7 +217,12 @@ def get_rel_stats(parents, files) -> list[dict[str, float | str]]:
217
217
  mount_space.append([parent, used])
218
218
 
219
219
  return [
220
- {"mount": mount, "used": used / total_used, "free": used / total_used, "total": used / total_used}
220
+ {
221
+ "mount": mount,
222
+ "used": used / total_used,
223
+ "free": (total_used - used) / total_used,
224
+ "total": used / total_used,
225
+ }
221
226
  for mount, used in mount_space
222
227
  ]
223
228
 
@@ -227,7 +227,7 @@ def block(args=None) -> None:
227
227
  msg += f" {key}: {preview}\n"
228
228
  log.warning(msg)
229
229
  else:
230
- p[1] = data[args.match_column]
230
+ p.append(data[args.match_column])
231
231
  if p[1]:
232
232
  matching_media = list(
233
233
  args.db.query(
@@ -237,7 +237,11 @@ def block(args=None) -> None:
237
237
  )
238
238
 
239
239
  if not matching_media:
240
- unmatched_playlists.append(p)
240
+ if len(p) < 2:
241
+ unmatched_playlists.append(p)
242
+ continue
243
+ log.info("Blocking URL resolved by tube metadata: %s", p[1])
244
+ add_to_blocklist(args, p)
241
245
  continue
242
246
 
243
247
  if args.regex_sort:
@@ -29,7 +29,7 @@ def create(args):
29
29
  );
30
30
  """
31
31
  )
32
- if args.action in ("tube-add", "gallery-add", "web-add", "site-add"):
32
+ if args.action in ("tube-add", "gallery-add", "web-add", "site-add", "computers-add"):
33
33
  args.db.execute("DROP INDEX IF EXISTS idx_media_path;")
34
34
  try:
35
35
  args.db.execute("CREATE UNIQUE INDEX IF NOT EXISTS media_uniq_path_idx ON media (playlists_id, path);")
@@ -99,6 +99,8 @@ def _add(args, entry):
99
99
  playlists_id = get_id(args, entry["path"])
100
100
  if playlists_id:
101
101
  entry["id"] = playlists_id
102
+ entry["time_modified"] = consts.now()
103
+ entry["time_deleted"] = 0
102
104
  args.db["playlists"].upsert(entry, pk="id", alter=True)
103
105
  else:
104
106
  entry["time_created"] = consts.APPLICATION_START
@@ -123,8 +125,8 @@ def exists(args, playlist_path) -> bool:
123
125
  def get_parentpath_playlists_id(args, playlist_path) -> int | None:
124
126
  try:
125
127
  known = args.db.pop(
126
- "SELECT id FROM playlists WHERE ? LIKE path || '%' AND path != ?",
127
- [str(playlist_path), str(playlist_path)],
128
+ "SELECT id FROM playlists WHERE ? LIKE path || ? || '%' AND path != ?",
129
+ [str(playlist_path), os.sep, str(playlist_path)],
128
130
  )
129
131
  except sqlite3.OperationalError as excinfo:
130
132
  log.debug(excinfo)
@@ -174,17 +176,38 @@ def mark_media_deleted(args, playlist_paths) -> int:
174
176
  return deleted_playlist_count
175
177
 
176
178
 
177
- def delete_subpath_playlists(args, playlist_path) -> int | None:
179
+ def delete_subpath_playlists(args, playlist_path, replacement_playlist_id=None) -> int | None:
180
+ subpath_filter = """
181
+ COALESCE(time_deleted, 0)=0
182
+ AND path LIKE ?
183
+ AND path != ?
184
+ """
185
+ subpath_params = [str(playlist_path) + os.sep + "%", str(playlist_path)]
186
+
187
+ if replacement_playlist_id:
188
+ try:
189
+ with args.db.conn:
190
+ args.db.conn.execute(
191
+ f"""
192
+ UPDATE media
193
+ SET playlists_id = ?
194
+ WHERE playlists_id IN (
195
+ SELECT id FROM playlists WHERE {subpath_filter}
196
+ )
197
+ """,
198
+ [replacement_playlist_id, *subpath_params],
199
+ )
200
+ except sqlite3.OperationalError:
201
+ pass
202
+
178
203
  try:
179
204
  with args.db.conn:
180
205
  args.db.conn.execute(
181
- """
206
+ f"""
182
207
  DELETE from playlists
183
- WHERE COALESCE(time_deleted, 0)=0
184
- AND path LIKE ?
185
- AND path != ?
208
+ WHERE {subpath_filter}
186
209
  """,
187
- [str(playlist_path) + os.sep + "%", str(playlist_path)],
210
+ subpath_params,
188
211
  )
189
212
  except sqlite3.OperationalError:
190
213
  pass
@@ -196,15 +219,16 @@ def add(args, playlist_path: str, info: dict, check_subpath=False, extractor_key
196
219
  parentpath_playlist_id = get_parentpath_playlists_id(args, playlist_path)
197
220
  if parentpath_playlist_id:
198
221
  return parentpath_playlist_id
199
- else:
200
- delete_subpath_playlists(args, playlist_path)
201
222
 
202
223
  playlist = consolidate(args, objects.dumbcopy(info))
203
224
  if playlist_path:
204
225
  playlist = {**playlist, "path": playlist_path}
205
226
  if extractor_key:
206
227
  playlist["extractor_key"] = extractor_key
207
- return _add(args, objects.dict_filter_bool(playlist) or {})
228
+ playlists_id = _add(args, objects.dict_filter_bool(playlist) or {})
229
+ if playlist_path and check_subpath:
230
+ delete_subpath_playlists(args, playlist_path, replacement_playlist_id=playlists_id)
231
+ return playlists_id
208
232
 
209
233
 
210
234
  def media_exists(args, path, playlist_path) -> bool:
@@ -119,13 +119,15 @@ def download(args=None) -> None:
119
119
  return
120
120
 
121
121
  get_inner_urls = iterables.return_unique(extract_links.get_inner_urls, lambda d: d["link"])
122
+ if args.safe and args.profile == DBType.image:
123
+ gallery_backend.load_module_level_gallery_dl(args)
122
124
  for m in media:
123
125
  if args.blocklist_rules and sql_utils.is_blocked_dict_like_sql(m, args.blocklist_rules):
124
126
  continue
125
127
 
126
128
  if args.safe:
127
129
  if (args.profile in (DBType.audio, DBType.video) and not tube_backend.is_supported(m["path"])) or (
128
- args.profile in (DBType.image,) and not gallery_backend.is_supported(args, m["path"])
130
+ args.profile in (DBType.image,) and not gallery_backend.is_supported(m["path"])
129
131
  ):
130
132
  log.info("Skipping unsupported URL (safe_mode) %s", m["path"])
131
133
  continue
@@ -3,20 +3,31 @@ import argparse
3
3
  from library import usage
4
4
  from library.mediadb import db_history
5
5
  from library.playback import media_printer
6
- from library.utils import arggroups, argparse_utils, sqlgroups
6
+ from library.utils import arggroups, argparse_utils, sqlgroups, strings
7
7
 
8
8
 
9
9
  def parse_args() -> argparse.Namespace:
10
10
  parser = argparse_utils.ArgumentParser(usage=usage.history)
11
11
  arggroups.sql_fs(parser)
12
+ arggroups.frequency(parser)
12
13
  arggroups.history(parser)
13
14
  arggroups.debug(parser)
14
15
 
15
16
  arggroups.database(parser)
17
+ arggroups.paths_or_stdin(parser, required=False)
16
18
  args = parser.parse_intermixed_args()
17
19
  arggroups.args_post(args, parser)
20
+ arggroups.frequency_post(args)
18
21
 
22
+ args.paths = [strings.strip_enclosing_quotes(path) for path in args.paths or []]
19
23
  arggroups.sql_fs_post(args)
24
+ if args.paths:
25
+ path_bindings = []
26
+ for index, path in enumerate(args.paths):
27
+ binding = f"history_path_{index}"
28
+ path_bindings.append(f":{binding}")
29
+ args.filter_bindings[binding] = path
30
+ args.where.append(f"path IN ({', '.join(path_bindings)})")
20
31
 
21
32
  return args
22
33
 
@@ -223,7 +223,7 @@ def media_check() -> None:
223
223
  threshold_str = (
224
224
  strings.percent(args.delete_corrupt)
225
225
  if 0 < args.delete_corrupt < 1
226
- else (args.delete_corrupt + "s")
226
+ else f"{args.delete_corrupt}s"
227
227
  )
228
228
  log.warning(
229
229
  "Deleting %s corruption %.1f%% exceeded threshold %s", path, corruption * 100, threshold_str
@@ -137,7 +137,7 @@ def process_path(args, path, include_timecode=False, **kwargs) -> str | None:
137
137
  if not args.video_only and path_utils.ext(path) not in consts.IMAGE_ANIMATION_EXTENSIONS:
138
138
  log.warning("No audio stream found: %s", path)
139
139
 
140
- if video_stream and (video_stream.get("codec_name") or "") == "av1":
140
+ if video_stream and not args.audio_only and (video_stream.get("codec_name") or "") == "av1":
141
141
  log.info("Video is already AV1: %s", path)
142
142
  return str(path)
143
143
  elif (not video_stream or args.audio_only) and audio_stream and (audio_stream.get("codec_name") or "") == "opus":
@@ -104,7 +104,7 @@ def process_path(args, path) -> str | None:
104
104
  return None
105
105
 
106
106
  if not output_path.exists():
107
- return str(path) if path.exists else None
107
+ return str(path) if path.exists() else None
108
108
 
109
109
  output_stats = output_path.stat()
110
110
  if output_stats.st_size == 0 or (args.delete_larger and output_stats.st_size > original_stats.st_size):
@@ -145,11 +145,10 @@ def unardel() -> None:
145
145
  if not os.path.exists(m["path"]):
146
146
  log.error("[%s]: FileNotFoundError", m["path"])
147
147
  m["time_deleted"] = consts.APPLICATION_START
148
- continue
149
148
 
150
- if args.move and not m.get("time_deleted") and m.get("new_path"):
151
- dest = path_utils.relative_from_mountpoint(m["new_path"], args.move)
152
- shell_utils.rename_move_file(m["new_path"], dest)
153
- elif args.move_broken and not m.get("time_deleted") and os.path.exists(m["path"]):
149
+ if args.move and not m.get("time_deleted"):
150
+ dest = path_utils.relative_from_mountpoint(m["path"], args.move)
151
+ shell_utils.rename_move_file(m["path"], dest)
152
+ elif args.move_broken and m.get("time_deleted") and os.path.exists(m["path"]):
154
153
  dest = path_utils.relative_from_mountpoint(m["path"], args.move_broken)
155
154
  shell_utils.rename_move_file(m["path"], dest)
@@ -709,7 +709,7 @@ def play_list(args, media):
709
709
  playlist = MediaPrefetcher(args, media)
710
710
  playlist.fetch()
711
711
 
712
- if args.multiple_playback > 1:
712
+ if args.multiple_playback and args.multiple_playback != 1:
713
713
  multiple_player(args, playlist)
714
714
  else:
715
715
  while playlist.remaining:
@@ -1,4 +1,4 @@
1
- import csv, json, os, shlex, statistics, sys
1
+ import csv, json, os, statistics, sys
2
2
  from copy import deepcopy
3
3
  from io import StringIO
4
4
  from numbers import Number
@@ -77,12 +77,12 @@ def moved_media(args, moved_files: str | list, base_from, base_to) -> int:
77
77
  for chunk_paths in df_chunked:
78
78
  with args.db.conn:
79
79
  cursor = args.db.conn.execute(
80
- f"""UPDATE media
81
- SET path=REPLACE(path, '{shlex.quote(base_from)}', '{shlex.quote(base_to)}')
80
+ """UPDATE media
81
+ SET path=REPLACE(path, ?, ?)
82
82
  where path in ("""
83
83
  + ",".join(["?"] * len(chunk_paths))
84
84
  + ")",
85
- (*chunk_paths,),
85
+ (base_from, base_to, *chunk_paths),
86
86
  )
87
87
  modified_row_count += cursor.rowcount
88
88