reny 1.0.0__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.
- reny-1.0.0/LICENSE +11 -0
- reny-1.0.0/PKG-INFO +131 -0
- reny-1.0.0/README.md +114 -0
- reny-1.0.0/pyproject.toml +3 -0
- reny-1.0.0/reny/__init__.py +0 -0
- reny-1.0.0/reny/cli/__init__.py +0 -0
- reny-1.0.0/reny/cli/base/__init__.py +0 -0
- reny-1.0.0/reny/cli/base/bmp_dispatch.py +60 -0
- reny-1.0.0/reny/cli/base/bmp_options.py +342 -0
- reny-1.0.0/reny/cli/base/vchk.py +47 -0
- reny-1.0.0/reny/cli/renamer/__init__.py +0 -0
- reny-1.0.0/reny/cli/renamer/renamer_dispatch.py +144 -0
- reny-1.0.0/reny/cli/renamer/renamer_options.py +364 -0
- reny-1.0.0/reny/commons/__init__.py +0 -0
- reny-1.0.0/reny/commons/chainedhandler.py +102 -0
- reny-1.0.0/reny/commons/descriptors.py +173 -0
- reny-1.0.0/reny/commons/progressbar.py +154 -0
- reny-1.0.0/reny/commons/taskprocessor.py +149 -0
- reny-1.0.0/reny/commons/utils.py +204 -0
- reny-1.0.0/reny/fstools/__init__.py +0 -0
- reny-1.0.0/reny/fstools/builders/__init__.py +0 -0
- reny-1.0.0/reny/fstools/builders/fsb.py +221 -0
- reny-1.0.0/reny/fstools/builders/fsentry.py +60 -0
- reny-1.0.0/reny/fstools/builders/fsprms.py +372 -0
- reny-1.0.0/reny/fstools/dirtools.py +389 -0
- reny-1.0.0/reny/fstools/fsutils.py +272 -0
- reny-1.0.0/reny/fstools/rename.py +403 -0
- reny-1.0.0/reny/fstools/virtual_organizer.py +301 -0
- reny-1.0.0/reny/fstools/walker.py +79 -0
- reny-1.0.0/reny.egg-info/PKG-INFO +131 -0
- reny-1.0.0/reny.egg-info/SOURCES.txt +44 -0
- reny-1.0.0/reny.egg-info/dependency_links.txt +1 -0
- reny-1.0.0/reny.egg-info/entry_points.txt +2 -0
- reny-1.0.0/reny.egg-info/requires.txt +5 -0
- reny-1.0.0/reny.egg-info/top_level.txt +2 -0
- reny-1.0.0/setup.cfg +4 -0
- reny-1.0.0/setup.py +23 -0
- reny-1.0.0/tests/__init__.py +0 -0
- reny-1.0.0/tests/base/__init__.py +0 -0
- reny-1.0.0/tests/base/test_base.py +92 -0
- reny-1.0.0/tests/commons/__init__.py +0 -0
- reny-1.0.0/tests/commons/test_commons.py +141 -0
- reny-1.0.0/tests/fs/__init__.py +0 -0
- reny-1.0.0/tests/fs/test_fs_base.py +32 -0
- reny-1.0.0/tests/fs/test_fs_organize.py +763 -0
- reny-1.0.0/tests/fs/test_fsutils.py +251 -0
reny-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Copyright (c) 2026 Arseniy Kuznetsov
|
|
2
|
+
##
|
|
3
|
+
## This program is free software; you can redistribute it and/or
|
|
4
|
+
## modify it under the terms of the GNU General Public License
|
|
5
|
+
## as published by the Free Software Foundation; either version 2
|
|
6
|
+
## of the License, or (at your option) any later version.
|
|
7
|
+
##
|
|
8
|
+
## This program is distributed in the hope that it will be useful,
|
|
9
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
## GNU General Public License for more details.
|
reny-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reny
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight, powerful batch renaming and filesystem organizing CLI tool.
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: pygtrie
|
|
8
|
+
Provides-Extra: test
|
|
9
|
+
Requires-Dist: pytest; extra == "test"
|
|
10
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
11
|
+
Dynamic: description
|
|
12
|
+
Dynamic: description-content-type
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
Dynamic: provides-extra
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: summary
|
|
17
|
+
|
|
18
|
+
# Reny
|
|
19
|
+
A lightweight, fast, and safe batch renaming and filesystem organization tool.
|
|
20
|
+
|
|
21
|
+
## Background
|
|
22
|
+
`reny` was originally created as the `renamer` component inside the larger [`batchmp`](https://github.com/akpw/batch-mp-tools) (Batch Media Processing) suite. It was spun off into its own standalone package to provide for an ultra-lightweight, safe, pure-filesystem organizing tool without FFmpeg / Mutagen media dependencies.
|
|
23
|
+
|
|
24
|
+
If you need advanced media operations (like denoising, cover-art extraction, or format transcoding), check out the original [`batchmp`](https://github.com/akpw/batch-mp-tools) project. If you just need to safely organize your files with surgical precision, `reny` is all you need.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
You can install `reny` globally using pipx:
|
|
28
|
+
```bash
|
|
29
|
+
pipx install .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
- **Virtual Views:** Preview how a directory structure would look when reorganised by type, size, or date without moving or changing anything
|
|
34
|
+
- **Indexing:** Multi-level indexing across nested directories, supporting multiple indexing schemes
|
|
35
|
+
- **Padding:** Automatically pad existing numbers in filenames with leading zeros to fix sorting orders
|
|
36
|
+
- **Flattening:** Safely collapse nested directory structures into a single folder
|
|
37
|
+
- **Regex Replacement:** Powerful batch renaming using standard regular expressions
|
|
38
|
+
- **Dry-Run by Default:** `reny` will always visualize targeted changes and ask for confirmation before it ever touches your files
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
### Basic Operations
|
|
43
|
+
**Print current directory structure:**
|
|
44
|
+
```bash
|
|
45
|
+
reny
|
|
46
|
+
```
|
|
47
|
+
*(Without arguments, `reny` defaults to the `print` command)*
|
|
48
|
+
|
|
49
|
+
**Add a sequential index to all `.txt` files recursively:**
|
|
50
|
+
```bash
|
|
51
|
+
reny -r -in '*.txt' index
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Pad existing numbers with leading zeros (e.g., `2 kms.png` becomes `02 kms.png`):**
|
|
55
|
+
```bash
|
|
56
|
+
reny pad -md 2
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Regex Replace:**
|
|
60
|
+
Change spaces to underscores in all filenames:
|
|
61
|
+
```bash
|
|
62
|
+
reny replace -fs ' ' -rs '_'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Advanced Operations & Virtual Views
|
|
66
|
+
**Flattening nested directories:**
|
|
67
|
+
Collapse all sub-directories and bring their files up to the current folder level:
|
|
68
|
+
```bash
|
|
69
|
+
reny flatten
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Organize by File Type:**
|
|
73
|
+
Move files into sub-directories grouped by their file extension (e.g., `png/`, `pdf/`):
|
|
74
|
+
```bash
|
|
75
|
+
reny organize -b type
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Virtual Views (Dry-Run Preview):**
|
|
79
|
+
Preview how files would look if organized by year and month, *without actually moving any files on your drive*:
|
|
80
|
+
```bash
|
|
81
|
+
reny print -b date --date-format "%Y/%m"
|
|
82
|
+
```
|
|
83
|
+
```text
|
|
84
|
+
Virtual view by date:
|
|
85
|
+
~/Downloads
|
|
86
|
+
|- 2025/
|
|
87
|
+
|- 01/
|
|
88
|
+
|- document.pdf
|
|
89
|
+
|- 02/
|
|
90
|
+
|- image.png
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Documentation & Tutorials
|
|
94
|
+
Although `reny` is a standalone project, its core organizing logic is inherited directly from [`batchmp`](https://github.com/akpw/batch-mp-tools). You can find detailed tutorials and deep-dives on how to master its capabilities in the original blog posts:
|
|
95
|
+
- [Renamer Organize & Virtual Views](https://akpw.github.io/articles/2025/09/22/Print-and-Organize.html) – *Highly recommended reading for mastering virtual directory views*
|
|
96
|
+
- [BatchMP Tools Tutorial, Part II: renaming files with renamer](https://akpw.github.io/articles/2015/04/11/batchmp-tutorial-part-ii.html)
|
|
97
|
+
- [Practical BatchMP Series](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
98
|
+
|
|
99
|
+
## Usage
|
|
100
|
+
Run `reny --help` to see all available filesystem operations!
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
To set up the project for development:
|
|
104
|
+
|
|
105
|
+
1. Clone the repository and navigate into it:
|
|
106
|
+
```bash
|
|
107
|
+
git clone https://github.com/akpw/reny.git
|
|
108
|
+
cd reny
|
|
109
|
+
```
|
|
110
|
+
2. Create and activate a virtual environment:
|
|
111
|
+
```bash
|
|
112
|
+
python3 -m venv .venv
|
|
113
|
+
source .venv/bin/activate
|
|
114
|
+
```
|
|
115
|
+
3. Install the project in editable mode along with testing dependencies:
|
|
116
|
+
```bash
|
|
117
|
+
pip install -e ".[test]"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Running Tests
|
|
121
|
+
The project uses `pytest` for its test suite. Because `reny` performs real filesystem operations, the tests are designed to dynamically create and clean up safe temporary sandbox folders during execution.
|
|
122
|
+
|
|
123
|
+
To run the full test suite:
|
|
124
|
+
```bash
|
|
125
|
+
pytest -v --tb=short tests/
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
To run a specific test file:
|
|
129
|
+
```bash
|
|
130
|
+
pytest tests/fs/test_fs_organize.py
|
|
131
|
+
```
|
reny-1.0.0/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Reny
|
|
2
|
+
A lightweight, fast, and safe batch renaming and filesystem organization tool.
|
|
3
|
+
|
|
4
|
+
## Background
|
|
5
|
+
`reny` was originally created as the `renamer` component inside the larger [`batchmp`](https://github.com/akpw/batch-mp-tools) (Batch Media Processing) suite. It was spun off into its own standalone package to provide for an ultra-lightweight, safe, pure-filesystem organizing tool without FFmpeg / Mutagen media dependencies.
|
|
6
|
+
|
|
7
|
+
If you need advanced media operations (like denoising, cover-art extraction, or format transcoding), check out the original [`batchmp`](https://github.com/akpw/batch-mp-tools) project. If you just need to safely organize your files with surgical precision, `reny` is all you need.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
You can install `reny` globally using pipx:
|
|
11
|
+
```bash
|
|
12
|
+
pipx install .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
- **Virtual Views:** Preview how a directory structure would look when reorganised by type, size, or date without moving or changing anything
|
|
17
|
+
- **Indexing:** Multi-level indexing across nested directories, supporting multiple indexing schemes
|
|
18
|
+
- **Padding:** Automatically pad existing numbers in filenames with leading zeros to fix sorting orders
|
|
19
|
+
- **Flattening:** Safely collapse nested directory structures into a single folder
|
|
20
|
+
- **Regex Replacement:** Powerful batch renaming using standard regular expressions
|
|
21
|
+
- **Dry-Run by Default:** `reny` will always visualize targeted changes and ask for confirmation before it ever touches your files
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
### Basic Operations
|
|
26
|
+
**Print current directory structure:**
|
|
27
|
+
```bash
|
|
28
|
+
reny
|
|
29
|
+
```
|
|
30
|
+
*(Without arguments, `reny` defaults to the `print` command)*
|
|
31
|
+
|
|
32
|
+
**Add a sequential index to all `.txt` files recursively:**
|
|
33
|
+
```bash
|
|
34
|
+
reny -r -in '*.txt' index
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Pad existing numbers with leading zeros (e.g., `2 kms.png` becomes `02 kms.png`):**
|
|
38
|
+
```bash
|
|
39
|
+
reny pad -md 2
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Regex Replace:**
|
|
43
|
+
Change spaces to underscores in all filenames:
|
|
44
|
+
```bash
|
|
45
|
+
reny replace -fs ' ' -rs '_'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Advanced Operations & Virtual Views
|
|
49
|
+
**Flattening nested directories:**
|
|
50
|
+
Collapse all sub-directories and bring their files up to the current folder level:
|
|
51
|
+
```bash
|
|
52
|
+
reny flatten
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Organize by File Type:**
|
|
56
|
+
Move files into sub-directories grouped by their file extension (e.g., `png/`, `pdf/`):
|
|
57
|
+
```bash
|
|
58
|
+
reny organize -b type
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Virtual Views (Dry-Run Preview):**
|
|
62
|
+
Preview how files would look if organized by year and month, *without actually moving any files on your drive*:
|
|
63
|
+
```bash
|
|
64
|
+
reny print -b date --date-format "%Y/%m"
|
|
65
|
+
```
|
|
66
|
+
```text
|
|
67
|
+
Virtual view by date:
|
|
68
|
+
~/Downloads
|
|
69
|
+
|- 2025/
|
|
70
|
+
|- 01/
|
|
71
|
+
|- document.pdf
|
|
72
|
+
|- 02/
|
|
73
|
+
|- image.png
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Documentation & Tutorials
|
|
77
|
+
Although `reny` is a standalone project, its core organizing logic is inherited directly from [`batchmp`](https://github.com/akpw/batch-mp-tools). You can find detailed tutorials and deep-dives on how to master its capabilities in the original blog posts:
|
|
78
|
+
- [Renamer Organize & Virtual Views](https://akpw.github.io/articles/2025/09/22/Print-and-Organize.html) – *Highly recommended reading for mastering virtual directory views*
|
|
79
|
+
- [BatchMP Tools Tutorial, Part II: renaming files with renamer](https://akpw.github.io/articles/2015/04/11/batchmp-tutorial-part-ii.html)
|
|
80
|
+
- [Practical BatchMP Series](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
Run `reny --help` to see all available filesystem operations!
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
To set up the project for development:
|
|
87
|
+
|
|
88
|
+
1. Clone the repository and navigate into it:
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/akpw/reny.git
|
|
91
|
+
cd reny
|
|
92
|
+
```
|
|
93
|
+
2. Create and activate a virtual environment:
|
|
94
|
+
```bash
|
|
95
|
+
python3 -m venv .venv
|
|
96
|
+
source .venv/bin/activate
|
|
97
|
+
```
|
|
98
|
+
3. Install the project in editable mode along with testing dependencies:
|
|
99
|
+
```bash
|
|
100
|
+
pip install -e ".[test]"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Running Tests
|
|
104
|
+
The project uses `pytest` for its test suite. Because `reny` performs real filesystem operations, the tests are designed to dynamically create and clean up safe temporary sandbox folders during execution.
|
|
105
|
+
|
|
106
|
+
To run the full test suite:
|
|
107
|
+
```bash
|
|
108
|
+
pytest -v --tb=short tests/
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
To run a specific test file:
|
|
112
|
+
```bash
|
|
113
|
+
pytest tests/fs/test_fs_organize.py
|
|
114
|
+
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf8
|
|
3
|
+
## Copyright (c) 2014 Arseniy Kuznetsov
|
|
4
|
+
##
|
|
5
|
+
## This program is free software; you can redistribute it and/or
|
|
6
|
+
## modify it under the terms of the GNU General Public License
|
|
7
|
+
## as published by the Free Software Foundation; either version 2
|
|
8
|
+
## of the License, or (at your option) any later version.
|
|
9
|
+
##
|
|
10
|
+
## This program is distributed in the hope that it will be useful,
|
|
11
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
## GNU General Public License for more details.
|
|
14
|
+
|
|
15
|
+
from importlib import metadata
|
|
16
|
+
import reny.cli.base.vchk
|
|
17
|
+
from reny.cli.base.bmp_options import BatchMPArgParser, BatchMPBaseCommands
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BatchMPDispatcher:
|
|
21
|
+
''' Base BatchMP Commands Dispatcher
|
|
22
|
+
'''
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.option_parser = BatchMPArgParser()
|
|
25
|
+
|
|
26
|
+
# Dispatcher
|
|
27
|
+
def dispatch(self):
|
|
28
|
+
args = self.option_parser.parse_options()
|
|
29
|
+
|
|
30
|
+
if args['sub_cmd'] == BatchMPBaseCommands.VERSION:
|
|
31
|
+
self.print_version()
|
|
32
|
+
|
|
33
|
+
elif args['sub_cmd'] == BatchMPBaseCommands.INFO:
|
|
34
|
+
self.print_info()
|
|
35
|
+
|
|
36
|
+
else:
|
|
37
|
+
# nothing to dispatch
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
# Dispatched methods
|
|
43
|
+
def print_version(self):
|
|
44
|
+
''' Prints BatchMP version info
|
|
45
|
+
'''
|
|
46
|
+
version = metadata.version("reny")
|
|
47
|
+
print('BatchMP tools version {}'.format(version))
|
|
48
|
+
|
|
49
|
+
def print_info(self):
|
|
50
|
+
print('\nBatch Media Processing Tools: {}'.format(self.option_parser.script_name))
|
|
51
|
+
print(self.option_parser.description)
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
''' BatchMP entry point
|
|
55
|
+
'''
|
|
56
|
+
BatchMPDispatcher().dispatch()
|
|
57
|
+
|
|
58
|
+
if __name__ == '__main__':
|
|
59
|
+
main()
|
|
60
|
+
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf8
|
|
3
|
+
## Copyright (c) 2014 Arseniy Kuznetsov
|
|
4
|
+
##
|
|
5
|
+
## This program is free software; you can redistribute it and/or
|
|
6
|
+
## modify it under the terms of the GNU General Public License
|
|
7
|
+
## as published by the Free Software Foundation; either version 2
|
|
8
|
+
## of the License, or (at your option) any later version.
|
|
9
|
+
##
|
|
10
|
+
## This program is distributed in the hope that it will be useful,
|
|
11
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
## GNU General Public License for more details.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
""" Global options parsing:
|
|
17
|
+
[-r, --recursive] Recurse into nested folders
|
|
18
|
+
[-el, --end-level] End level for recursion into nested folders
|
|
19
|
+
|
|
20
|
+
[-in, --include] Include names pattern (Unix style)
|
|
21
|
+
[-ex, --exclude] Exclude names pattern (Unix style)
|
|
22
|
+
(excludes hidden files by default)
|
|
23
|
+
[-ad, --all-dirs] Prevent using Include/Exclude patterns on directories
|
|
24
|
+
[-af, --all-files] Prevent using Include/Exclude patterns on files
|
|
25
|
+
(shows hidden files excluded by default)
|
|
26
|
+
|
|
27
|
+
[-s, --sort]{na|nd|sa|sd} Sort order for files / folders (name | date, asc | desc)
|
|
28
|
+
[-ni, nested-indent] Indent for printing nested directories
|
|
29
|
+
[-q, --quiet] Do not visualise changes / show messages during processing
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import os, sys, string
|
|
33
|
+
from argparse import ArgumentParser, HelpFormatter
|
|
34
|
+
from reny.commons.utils import strtobool
|
|
35
|
+
from urllib.parse import urlparse
|
|
36
|
+
from reny.commons.utils import MiscHelpers
|
|
37
|
+
from reny.fstools.fsutils import FSH
|
|
38
|
+
from reny.fstools.builders.fsentry import FSEntry, FSEntryDefaults
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BatchMPBaseCommands:
|
|
43
|
+
VERSION = 'version'
|
|
44
|
+
INFO = 'info'
|
|
45
|
+
PRINT = 'print'
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def commands_meta(cls):
|
|
49
|
+
return ''.join(('{',
|
|
50
|
+
'{}, '.format(cls.INFO),
|
|
51
|
+
'{}'.format(cls.VERSION),
|
|
52
|
+
'}'))
|
|
53
|
+
|
|
54
|
+
class BatchMPArgParser:
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self._script_name = 'Reny'
|
|
57
|
+
self._description = '''
|
|
58
|
+
Reny provides management of files, directories, etc...
|
|
59
|
+
|
|
60
|
+
Reny tools consist of three main command-line utilities.
|
|
61
|
+
For more information, run:
|
|
62
|
+
$ renamer -h
|
|
63
|
+
$ tagger -h
|
|
64
|
+
$ bmfp -h
|
|
65
|
+
'''
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def description(self):
|
|
69
|
+
return self._description
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def script_name(self):
|
|
73
|
+
return self._script_name
|
|
74
|
+
|
|
75
|
+
# Args parsing
|
|
76
|
+
def parse_options(self):
|
|
77
|
+
''' Common workflow for parsing options
|
|
78
|
+
'''
|
|
79
|
+
parser = ArgumentParser(prog = self._script_name, description = self._description,
|
|
80
|
+
formatter_class=BatchMPHelpFormatter)
|
|
81
|
+
|
|
82
|
+
self.parse_global_options(parser)
|
|
83
|
+
|
|
84
|
+
self.parse_commands(parser)
|
|
85
|
+
|
|
86
|
+
args = vars(parser.parse_args())
|
|
87
|
+
|
|
88
|
+
self.check_args(args, parser)
|
|
89
|
+
|
|
90
|
+
return args
|
|
91
|
+
|
|
92
|
+
def parse_global_options(self, parser):
|
|
93
|
+
''' Parses global options
|
|
94
|
+
'''
|
|
95
|
+
source_mode_group = parser.add_argument_group('Input source mode')
|
|
96
|
+
source_mode_group.add_argument("-d", "--dir", dest = "dir",
|
|
97
|
+
type = lambda d: self._is_valid_dir_path(parser, d),
|
|
98
|
+
help = "Source directory (default is current directory)",
|
|
99
|
+
default = os.curdir)
|
|
100
|
+
source_mode_group.add_argument("-f", "--file", dest = "file",
|
|
101
|
+
type = lambda f: self._is_valid_file_path(parser, f),
|
|
102
|
+
help = "File to process")
|
|
103
|
+
|
|
104
|
+
recursive_mode_group = parser.add_argument_group('Recursion mode')
|
|
105
|
+
recursive_mode_group.add_argument("-r", "--recursive", dest = "recursive",
|
|
106
|
+
help = "Recurse into nested folders",
|
|
107
|
+
action = 'store_true')
|
|
108
|
+
recursive_mode_group.add_argument("-el", "--end-level", dest = "end_level",
|
|
109
|
+
help = "End level for recursion into nested folders",
|
|
110
|
+
type = int,
|
|
111
|
+
default = 0)
|
|
112
|
+
|
|
113
|
+
include_mode_group = parser.add_argument_group('Filter files or folders')
|
|
114
|
+
include_mode_group.add_argument("-in", "--include", dest = "include",
|
|
115
|
+
help = "Include: Unix-style name patterns separated by ';'",
|
|
116
|
+
type = str,
|
|
117
|
+
default = FSEntryDefaults.DEFAULT_INCLUDE)
|
|
118
|
+
include_mode_group.add_argument("-ex", "--exclude", dest = "exclude",
|
|
119
|
+
help = "Exclude: Unix-style name patterns separated by ';' (excludes hidden files by default)",
|
|
120
|
+
type = str,
|
|
121
|
+
default = FSEntryDefaults.DEFAULT_EXCLUDE)
|
|
122
|
+
include_mode_group.add_argument("-ad", "--all-dirs", dest = "all_dirs",
|
|
123
|
+
help = "Disable Include/Exclude patterns on directories",
|
|
124
|
+
action = 'store_true')
|
|
125
|
+
include_mode_group.add_argument("-af", "--all-files", dest = "all_files",
|
|
126
|
+
help = "Disable Include/Exclude patterns on files (shows hidden files excluded by default)",
|
|
127
|
+
action = 'store_true')
|
|
128
|
+
|
|
129
|
+
media_types_group = parser.add_argument_group('File media types')
|
|
130
|
+
media_types_group.add_argument("-ft", "--file-type", dest = "file_type",
|
|
131
|
+
help = "File Media Type",
|
|
132
|
+
type = str,
|
|
133
|
+
choices = ['image', 'video', 'audio', 'media', 'nonmedia', 'playable', 'nonplayable', 'any'],
|
|
134
|
+
default = FSEntryDefaults.DEFAULT_FILE_TYPE)
|
|
135
|
+
media_types_group.add_argument("-ms", "--media-scan", dest = "media_scan",
|
|
136
|
+
help = "Scan for media types, instead of using file extensions (can take a long time)",
|
|
137
|
+
action = 'store_true')
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Add Default Miscellaneous Group
|
|
141
|
+
self._add_arg_misc_group(parser)
|
|
142
|
+
|
|
143
|
+
def parse_commands(self, parser):
|
|
144
|
+
''' Specific commands parsing
|
|
145
|
+
'''
|
|
146
|
+
subparsers = parser.add_subparsers(dest = 'sub_cmd',
|
|
147
|
+
title = 'BatchMP commands',
|
|
148
|
+
metavar = BatchMPBaseCommands.commands_meta())
|
|
149
|
+
self._add_version(subparsers)
|
|
150
|
+
self._add_info(subparsers)
|
|
151
|
+
|
|
152
|
+
# Args checking
|
|
153
|
+
def check_cmd_args(self, args, parser,
|
|
154
|
+
show_help = False,
|
|
155
|
+
exit = False):
|
|
156
|
+
if not args.get('sub_cmd'):
|
|
157
|
+
if show_help:
|
|
158
|
+
parser.print_help()
|
|
159
|
+
if exit:
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
# if not exiting, need to default
|
|
163
|
+
self.default_command(args, parser)
|
|
164
|
+
|
|
165
|
+
def default_command(self, args, parser):
|
|
166
|
+
args['sub_cmd'] = BatchMPBaseCommands.INFO
|
|
167
|
+
|
|
168
|
+
def check_args(self, args, parser):
|
|
169
|
+
''' Validation of supplied CLI arguments
|
|
170
|
+
'''
|
|
171
|
+
# check if there is a cmd to execute
|
|
172
|
+
self.check_cmd_args(args, parser)
|
|
173
|
+
|
|
174
|
+
# if input source is a file, need to adjust
|
|
175
|
+
if args['file']:
|
|
176
|
+
args['dir'] = os.path.dirname(args['file'])
|
|
177
|
+
args['include'] = os.path.basename(args['file'])
|
|
178
|
+
args['exclude'] = ''
|
|
179
|
+
args['end_level'] = 0
|
|
180
|
+
args['all_files'] = False
|
|
181
|
+
args['all_dirs'] = False
|
|
182
|
+
|
|
183
|
+
# check recursion
|
|
184
|
+
if args['recursive'] and args['end_level'] == 0:
|
|
185
|
+
args['end_level'] = sys.maxsize
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if args['media_scan']:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
if args['sub_cmd'] == BatchMPBaseCommands.PRINT:
|
|
192
|
+
if args['start_level'] != 0:
|
|
193
|
+
if args['file']:
|
|
194
|
+
print ('Start Level parameter requires a source directory\n Ignoring requested Start Level...')
|
|
195
|
+
args['start_level'] = 0
|
|
196
|
+
elif args['end_level'] < args['start_level']:
|
|
197
|
+
''' print ('Start Level should be greater than or equal to the Recursion End Level Global Option\n'
|
|
198
|
+
'... Adjusting End Level to: {}'.format(args['start_level']))
|
|
199
|
+
'''
|
|
200
|
+
args['end_level'] = args['start_level']
|
|
201
|
+
|
|
202
|
+
# Internal Helpers
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _is_valid_dir_path(parser, path_arg):
|
|
205
|
+
""" Checks if path_arg is a valid dir path
|
|
206
|
+
"""
|
|
207
|
+
path_arg = FSH.full_path(path_arg)
|
|
208
|
+
if not (os.path.exists(path_arg) and os.path.isdir(path_arg)):
|
|
209
|
+
parser.error('"{}" does not seem to be an existing directory path'.format(path_arg))
|
|
210
|
+
else:
|
|
211
|
+
return path_arg
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _is_valid_file_path(parser, path_arg):
|
|
215
|
+
""" Checks if path_arg is a valid file path
|
|
216
|
+
"""
|
|
217
|
+
path_arg = FSH.full_path(path_arg)
|
|
218
|
+
if not (os.path.exists(path_arg) and os.path.isfile(path_arg)):
|
|
219
|
+
parser.error('"{}" does not seem to be an existing file path'.format(path_arg))
|
|
220
|
+
else:
|
|
221
|
+
return path_arg
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _is_boolean(parser, bool_arg):
|
|
225
|
+
""" Checks if bool_arg can be interpreted as a boolean value
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
bool_arg = True if strtobool(bool_arg) else False
|
|
229
|
+
except ValueError:
|
|
230
|
+
parser.error('"{}": Please enter a boolean value'.format(bool_arg))
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _is_valid_url(parser, url_arg):
|
|
235
|
+
url_parts = urlparse(url_arg)
|
|
236
|
+
|
|
237
|
+
def _parser_error():
|
|
238
|
+
parser.error('"{}": Please enter a valid URL'.format(url_arg))
|
|
239
|
+
|
|
240
|
+
if url_parts.scheme in (None, '') and url_parts.netloc in (None, ''):
|
|
241
|
+
_parser_error()
|
|
242
|
+
|
|
243
|
+
if url_parts.scheme == 'file':
|
|
244
|
+
if url_parts.netloc == '~':
|
|
245
|
+
fpath = '~{}'.format(url_parts.path)
|
|
246
|
+
else:
|
|
247
|
+
fpath = url_parts.path
|
|
248
|
+
return BatchMPArgParser._is_valid_file_path(parser, fpath)
|
|
249
|
+
|
|
250
|
+
if not set(url_parts.netloc).issubset(set(string.ascii_letters + string.digits + '-.')):
|
|
251
|
+
_parser_error()
|
|
252
|
+
|
|
253
|
+
if not url_parts.scheme in ['http', 'https', 'ftp', 'file']:
|
|
254
|
+
_parser_error()
|
|
255
|
+
|
|
256
|
+
return url_arg
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _is_valid_url_or_file_path(parser, url_or_file_path_arg):
|
|
260
|
+
url_parts = urlparse(url_or_file_path_arg)
|
|
261
|
+
if url_parts.scheme in (None, '') and url_parts.netloc in (None, ''):
|
|
262
|
+
return BatchMPArgParser._is_valid_file_path(parser, url_or_file_path_arg)
|
|
263
|
+
else:
|
|
264
|
+
return BatchMPArgParser._is_valid_url(parser, url_or_file_path_arg)
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def _is_timedelta(parser, td_arg):
|
|
268
|
+
try:
|
|
269
|
+
td = MiscHelpers.time_delta(td_arg)
|
|
270
|
+
except ValueError:
|
|
271
|
+
parser.error('"{}": Please enter a valid value, ' \
|
|
272
|
+
'in seconds or in the "hh:mm:ss[.xxx]" format'.format(td_arg))
|
|
273
|
+
return td
|
|
274
|
+
|
|
275
|
+
# Processing mode for relevant commands
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _add_arg_display_curent_state_mode(parser):
|
|
278
|
+
parser.add_argument('-dc', '--display-current', dest = 'display_current',
|
|
279
|
+
help ='Unless in quiet mode, display current (pre-processing) state in the confirmation propmt',
|
|
280
|
+
action = 'store_true')
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _add_arg_misc_group(parser):
|
|
284
|
+
misc_group = parser.add_argument_group('Miscellaneous')
|
|
285
|
+
misc_group.add_argument('-s', '--sort', dest = 'sort',
|
|
286
|
+
help = "Sorting for files ('na', i.e. by name ascending by default)",
|
|
287
|
+
type = str,
|
|
288
|
+
choices = ['na', 'nd', 'sa', 'sd'],
|
|
289
|
+
default = FSEntryDefaults.DEFAULT_SORT)
|
|
290
|
+
misc_group.add_argument('-ni', '--nested_indent', dest = 'nested_indent',
|
|
291
|
+
help = "Indent for printing nested directories",
|
|
292
|
+
type = str,
|
|
293
|
+
default = ' ')
|
|
294
|
+
misc_group.add_argument("-q", "--quiet", dest = 'quiet',
|
|
295
|
+
help = "Disable visualising changes & displaying info messages during processing",
|
|
296
|
+
action = 'store_true')
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def _add_version(parser):
|
|
300
|
+
''' Adds the version command
|
|
301
|
+
'''
|
|
302
|
+
parser.add_parser(BatchMPBaseCommands.VERSION,
|
|
303
|
+
description = 'Displays BatchMP version info',
|
|
304
|
+
formatter_class=BatchMPHelpFormatter)
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _add_info(parser):
|
|
308
|
+
''' Adds the info command
|
|
309
|
+
'''
|
|
310
|
+
parser.add_parser(BatchMPBaseCommands.INFO,
|
|
311
|
+
description = 'Displays BatchMP info',
|
|
312
|
+
formatter_class=BatchMPHelpFormatter)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class BatchMPHelpFormatter(HelpFormatter):
|
|
317
|
+
''' Custom ArgumentParser formatter
|
|
318
|
+
Disables double metavar display, showing it only for long-named options
|
|
319
|
+
'''
|
|
320
|
+
def _format_action_invocation(self, action):
|
|
321
|
+
if not action.option_strings:
|
|
322
|
+
metavar, = self._metavar_formatter(action, action.dest)(1)
|
|
323
|
+
return metavar
|
|
324
|
+
else:
|
|
325
|
+
parts = []
|
|
326
|
+
# if the Optional doesn't take a value, format is:
|
|
327
|
+
# -s, --long
|
|
328
|
+
if action.nargs == 0:
|
|
329
|
+
parts.extend(action.option_strings)
|
|
330
|
+
|
|
331
|
+
# if the Optional takes a value, format is:
|
|
332
|
+
# -s ARGS, --long ARGS
|
|
333
|
+
# change to
|
|
334
|
+
# -s, --long ARGS
|
|
335
|
+
else:
|
|
336
|
+
default = action.dest.upper()
|
|
337
|
+
args_string = self._format_args(action, default)
|
|
338
|
+
for option_string in action.option_strings:
|
|
339
|
+
#parts.append('%s %s' % (option_string, args_string))
|
|
340
|
+
parts.append('%s' % option_string)
|
|
341
|
+
parts[-1] += ' %s'%args_string
|
|
342
|
+
return ', '.join(parts)
|