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