batchmp 1.0__tar.gz → 1.4__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 (76) hide show
  1. {batchmp-1.0/batchmp.egg-info → batchmp-1.4}/PKG-INFO +78 -14
  2. batchmp-1.0/PKG-INFO → batchmp-1.4/README.md +60 -39
  3. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/base/bmp_dispatch.py +2 -2
  4. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/bmfp/bmfp_dispatch.py +3 -1
  5. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/bmfp/bmfp_options.py +8 -3
  6. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/renamer/renamer_dispatch.py +15 -4
  7. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/renamer/renamer_options.py +32 -1
  8. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffcommands/cuesplit.py +3 -3
  9. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffcommands/fragment.py +13 -3
  10. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffcommands/segment.py +7 -7
  11. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffutils.py +5 -5
  12. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/utils/cueparse.py +2 -2
  13. {batchmp-1.0 → batchmp-1.4}/batchmp/fstools/builders/fsb.py +60 -3
  14. {batchmp-1.0 → batchmp-1.4}/batchmp/fstools/builders/fsprms.py +10 -3
  15. {batchmp-1.0 → batchmp-1.4}/batchmp/fstools/dirtools.py +235 -6
  16. {batchmp-1.0 → batchmp-1.4}/batchmp/fstools/rename.py +11 -0
  17. {batchmp-1.0 → batchmp-1.4}/batchmp/fstools/walker.py +2 -2
  18. {batchmp-1.0 → batchmp-1.4}/batchmp/tags/handlers/mtghandler.py +1 -2
  19. {batchmp-1.0 → batchmp-1.4}/batchmp/tags/processors/basetp.py +1 -1
  20. batchmp-1.0/README.md → batchmp-1.4/batchmp.egg-info/PKG-INFO +103 -10
  21. {batchmp-1.0 → batchmp-1.4}/batchmp.egg-info/SOURCES.txt +0 -2
  22. batchmp-1.4/batchmp.egg-info/requires.txt +4 -0
  23. {batchmp-1.0 → batchmp-1.4}/setup.py +3 -6
  24. batchmp-1.0/batchmp/tags/extern/mediafile.py +0 -1889
  25. batchmp-1.0/batchmp/tags/processors/__init__.py +0 -0
  26. batchmp-1.0/batchmp.egg-info/requires.txt +0 -2
  27. {batchmp-1.0 → batchmp-1.4}/LICENSE +0 -0
  28. {batchmp-1.0 → batchmp-1.4}/batchmp/__init__.py +0 -0
  29. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/__init__.py +0 -0
  30. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/base/__init__.py +0 -0
  31. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/base/bmp_options.py +0 -0
  32. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/base/vchk.py +0 -0
  33. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/bmfp/__init__.py +0 -0
  34. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/renamer/__init__.py +0 -0
  35. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/tagger/__init__.py +0 -0
  36. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/tagger/tagger_dispatch.py +0 -0
  37. {batchmp-1.0 → batchmp-1.4}/batchmp/cli/tagger/tagger_options.py +0 -0
  38. {batchmp-1.0 → batchmp-1.4}/batchmp/commons/__init__.py +0 -0
  39. {batchmp-1.0 → batchmp-1.4}/batchmp/commons/chainedhandler.py +0 -0
  40. {batchmp-1.0 → batchmp-1.4}/batchmp/commons/descriptors.py +0 -0
  41. {batchmp-1.0 → batchmp-1.4}/batchmp/commons/progressbar.py +0 -0
  42. {batchmp-1.0 → batchmp-1.4}/batchmp/commons/taskprocessor.py +0 -0
  43. {batchmp-1.0 → batchmp-1.4}/batchmp/commons/utils.py +0 -0
  44. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/__init__.py +0 -0
  45. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffcommands/__init__.py +0 -0
  46. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffcommands/cmdopt.py +0 -0
  47. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffcommands/convert.py +0 -0
  48. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffcommands/denoise.py +0 -0
  49. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffcommands/normalize_peak.py +0 -0
  50. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffcommands/silencesplit.py +0 -0
  51. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/ffrunner.py +0 -0
  52. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/processors/__init__.py +0 -0
  53. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/processors/basefp.py +0 -0
  54. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/processors/ffentry.py +0 -0
  55. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/utils/__init__.py +0 -0
  56. {batchmp-1.0 → batchmp-1.4}/batchmp/ffmptools/utils/cuesheet.py +0 -0
  57. {batchmp-1.0 → batchmp-1.4}/batchmp/fstools/__init__.py +0 -0
  58. {batchmp-1.0 → batchmp-1.4}/batchmp/fstools/builders/__init__.py +0 -0
  59. {batchmp-1.0 → batchmp-1.4}/batchmp/fstools/builders/fsentry.py +0 -0
  60. {batchmp-1.0 → batchmp-1.4}/batchmp/fstools/fsutils.py +0 -0
  61. {batchmp-1.0 → batchmp-1.4}/batchmp/tags/__init__.py +0 -0
  62. {batchmp-1.0/batchmp/tags/extern → batchmp-1.4/batchmp/tags/handlers}/__init__.py +0 -0
  63. {batchmp-1.0 → batchmp-1.4}/batchmp/tags/handlers/basehandler.py +0 -0
  64. {batchmp-1.0 → batchmp-1.4}/batchmp/tags/handlers/ffmphandler.py +0 -0
  65. {batchmp-1.0/batchmp/tags/handlers → batchmp-1.4/batchmp/tags/handlers/ffmphandlers}/__init__.py +0 -0
  66. {batchmp-1.0 → batchmp-1.4}/batchmp/tags/handlers/ffmphandlers/base.py +0 -0
  67. {batchmp-1.0 → batchmp-1.4}/batchmp/tags/handlers/pmhandler.py +0 -0
  68. {batchmp-1.0 → batchmp-1.4}/batchmp/tags/handlers/tagsholder.py +0 -0
  69. {batchmp-1.0/batchmp/tags/handlers/ffmphandlers → batchmp-1.4/batchmp/tags/output}/__init__.py +0 -0
  70. {batchmp-1.0 → batchmp-1.4}/batchmp/tags/output/formatters.py +0 -0
  71. {batchmp-1.0/batchmp/tags/output → batchmp-1.4/batchmp/tags/processors}/__init__.py +0 -0
  72. {batchmp-1.0 → batchmp-1.4}/batchmp.egg-info/dependency_links.txt +0 -0
  73. {batchmp-1.0 → batchmp-1.4}/batchmp.egg-info/entry_points.txt +0 -0
  74. {batchmp-1.0 → batchmp-1.4}/batchmp.egg-info/top_level.txt +0 -0
  75. {batchmp-1.0 → batchmp-1.4}/batchmp.egg-info/zip-safe +0 -0
  76. {batchmp-1.0 → batchmp-1.4}/setup.cfg +0 -0
@@ -1,14 +1,13 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: batchmp
3
- Version: 1.0
3
+ Version: 1.4
4
4
  Summary: Command-line tools for batch media processing
5
5
  Home-page: https://github.com/akpw/batch-mp-tools
6
6
  Author: Arseniy Kuznetsov
7
7
  Author-email: k.arseniy@gmail.com
8
- License: GNU General Public License v2 (GPLv2)
8
+ License: GPL-2.0-or-later
9
9
  Keywords: batch processing media video audio CLI rename tags ID3
10
10
  Classifier: Development Status :: 4 - Beta
11
- Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
12
11
  Classifier: Programming Language :: Python
13
12
  Classifier: Programming Language :: Python :: 3.6
14
13
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -26,6 +25,21 @@ Classifier: Topic :: Software Development :: Libraries
26
25
  Classifier: Topic :: Utilities
27
26
  Description-Content-Type: text/markdown
28
27
  License-File: LICENSE
28
+ Requires-Dist: mutagen>=1.27
29
+ Requires-Dist: pygtrie>=2.3.2
30
+ Requires-Dist: filetype>=1.0.7
31
+ Requires-Dist: mediafile>=0.13.0
32
+ Dynamic: author
33
+ Dynamic: author-email
34
+ Dynamic: classifier
35
+ Dynamic: description
36
+ Dynamic: description-content-type
37
+ Dynamic: home-page
38
+ Dynamic: keywords
39
+ Dynamic: license
40
+ Dynamic: license-file
41
+ Dynamic: requires-dist
42
+ Dynamic: summary
29
43
 
30
44
 
31
45
  **Status:**
@@ -39,10 +53,11 @@ A rainy weekends project under occasional development :)
39
53
  - latest from source repository: `$ pip install git+https://github.com/akpw/batch-mp-tools.git`
40
54
 
41
55
  #### Blogs:
42
- - [Practical BatchMP](http://www.akpdev.com/tags.html#BatchMP+Tools)
43
- - [BatchMP Tools Tutorial](http://www.akpdev.com/articles/2015/04/10/batchmp-tutorial-part-i.html)
44
- - [The BatchMP Tools Project](http://www.akpdev.com/articles/2015/03/21/the-batchmp-project.html)
45
- - [Parallel batch media processing with FFmpeg and Python](http://www.akpdev.com/articles/2014/11/24/batch-media-processing-ffmpeg-python.html)
56
+ - [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
57
+ - [Renamer Organize & Virtual Views](https://akpw.github.io/articles/2025/09/22/Print-and-Organize.html)
58
+ - [BatchMP Tools Tutorial](https://akpw.github.io//articles/2015/04/10/batchmp-tutorial-part-i.html)
59
+ - [The BatchMP Tools Project](https://akpw.github.io//articles/2015/03/21/the-batchmp-project.html)
60
+ - [Parallel batch media processing with FFmpeg and Python](https://akpw.github.io//articles/2014/11/24/batch-media-processing-ffmpeg-python.html)
46
61
 
47
62
  ## Description
48
63
 
@@ -62,7 +77,7 @@ By default the tools always visualize targeted changes (whenever possible) befor
62
77
 
63
78
  A little bit more details on each utility:
64
79
 
65
- [**Renamer**](https://github.com/akpw/batch-mp-tools#renamer) is a multi-platform batch rename tool. In addition to common operations such as regexp-based replace, adding text / dates, etc. it also supports advanced operations such as expandable template processing during replace, multi-level indexing across nested directories, flattening folders, and cleaning up non-media files.
80
+ [**Renamer**](https://github.com/akpw/batch-mp-tools#renamer) is a multi-platform batch rename tool. In addition to common operations such as regexp-based replace, adding text / dates, etc. it also supports advanced operations such as expandable template processing during replace, multi-level indexing across nested directories, flattening folders, organizing files by type or date, etc. The enhanced print command now supports virtual views to preview organization without moving files.
66
81
  At its simplest, Renamer can be used to print out the content of current directory:
67
82
  ```
68
83
  $ renamer
@@ -91,6 +106,34 @@ For multi-level indexing of all M4A files in all sub-directories of the current
91
106
  ```
92
107
  Sequential indexing is supported as well using the `-sq` switch. An important detail here, by default Renamer is visualizing the targeted changes and asking for permission to proceed before actually doing anything.
93
108
 
109
+ For organizing files by media type:
110
+ ```
111
+ $ renamer organize -b type
112
+ ~/Downloads
113
+ |- image/
114
+ |- photo1.jpg
115
+ |- screenshot.png
116
+ |- video/
117
+ |- movie.mp4
118
+ |- audio/
119
+ |- song.mp3
120
+
121
+ Proceed? [y/n]:
122
+ ```
123
+
124
+ Or preview how files would look organized by date without moving them:
125
+ ```
126
+ $ renamer print -b date --date-format "%Y/%m"
127
+ Virtual view by date:
128
+ ~/Downloads
129
+ |- 2025/
130
+ |- 01/
131
+ |- document.pdf
132
+ |- photo.jpg
133
+ |- 02/
134
+ |- video.mp4
135
+ ```
136
+
94
137
 
95
138
 
96
139
  [**Tagger**](https://github.com/akpw/batch-mp-tools#tagger) manages media metadata, such as tags and artwork. Setting those in selected media file over multiple nested directories now becomes a breeze, with just a few simple commands working uniformly over almost any practically imaginable audio / video media formats. While easy to use, Tagger supports advanced metadata manipulation such as regexp-based replace, expandable template processing, etc. For example, to set the title tag to respective file names followed by the values of track and tracktotal tags:
@@ -178,7 +221,9 @@ I will follow up with more examples and common use-cases in future blogs.
178
221
  . display sorting:
179
222
  .. by size/date, ascending/descending
180
223
  . action commands:
181
- .. print Prints source directory
224
+ .. print Prints source directory. Enhanced with virtual organization views:
225
+ $ renamer print -b type # Preview organize by media type
226
+ $ renamer print -b date -df "%Y/%m" # Preview organize by date
182
227
  .. flatten Flatten all folders below target level, moving the
183
228
  files up at the target level. By default, deletes all empty flattened folders
184
229
  .. index Adds index to files and directories names
@@ -191,6 +236,10 @@ I will follow up with more examples and common use-cases in future blogs.
191
236
  .. remove Removes n characters from files and directories names
192
237
  .. capitalize Capitalizes words in files / directories names
193
238
  .. delete Delete selected files and directories
239
+ .. organize Organizes files into subdirectories based on their attributes:
240
+ $ renamer organize -b type # By media type (image/, video/, audio/)
241
+ $ renamer organize -b date -df "%Y-%m" # By date (2025-01/, 2025-02/)
242
+ $ renamer organize -b date -df "%Y/%m" -td ~/Sorted # To target directory
194
243
 
195
244
  Usage: renamer [-h] [-d DIR] [-f FILE] [Global Options] {Commands} [Commands Options]
196
245
  Global Options:
@@ -215,7 +264,7 @@ I will follow up with more examples and common use-cases in future blogs.
215
264
  [-q, --quiet] Do not visualise changes / show messages during processing
216
265
 
217
266
  Commands:
218
- {print, index, add_date, add_text, remove, replace, capitalize, flatten, delete, version, info}
267
+ {print, index, add_date, add_text, remove, replace, capitalize, flatten, delete, organize, version, info}
219
268
  $ renamer {command} -h #run this for detailed help on individual commands
220
269
 
221
270
  ### tagger
@@ -324,7 +373,7 @@ Support via FFmpeg: 'AVI', 'FLV', 'MKV', 'MKA'
324
373
  (shows hidden files excluded by default)
325
374
 
326
375
  Target output Directory Target output directory. When omitted, will be
327
- [-td, --target-dir] automatically created at the parent level of
376
+ [-td, --target-dir] automatically created inside the parent level of
328
377
  the input source. For recursive processing,
329
378
  the processed files directory structure there
330
379
  will be the same as for the original files.
@@ -348,8 +397,23 @@ Support via FFmpeg: 'AVI', 'FLV', 'MKV', 'MKA'
348
397
  ## Installing Development version
349
398
  - Clone the repo, then run: `$ python -m pip install .`
350
399
 
351
- **Running Tests**
352
- - Run via: `$ python setup.py test`
400
+ ## Running Tests
401
+
402
+ **Using pytest (recommended):**
403
+ ```bash
404
+ $ pytest -v --tb=short # Run all tests with verbose output
405
+ $ pytest tests/ # Run all tests
406
+ $ pytest tests/fs/test_fs_organize.py # Test organize functionality
407
+ $ pytest tests/fs/test_fsutils.py # Test core filesystem utilities
408
+ $ pytest tests/cli/test_renamer_cli.py # Test renamer CLI
409
+ $ pytest -k "test_organize" # Run tests matching pattern
410
+ ```
411
+
412
+ **Using unittest (fallback):**
413
+ ```bash
414
+ $ python -m unittest discover tests -v # Run all tests with verbose output
415
+ $ python -m unittest tests.fs.test_fs_organize # Run specific test module
416
+ ```
353
417
 
354
418
 
355
419
 
@@ -1,32 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: batchmp
3
- Version: 1.0
4
- Summary: Command-line tools for batch media processing
5
- Home-page: https://github.com/akpw/batch-mp-tools
6
- Author: Arseniy Kuznetsov
7
- Author-email: k.arseniy@gmail.com
8
- License: GNU General Public License v2 (GPLv2)
9
- Keywords: batch processing media video audio CLI rename tags ID3
10
- Classifier: Development Status :: 4 - Beta
11
- Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
12
- Classifier: Programming Language :: Python
13
- Classifier: Programming Language :: Python :: 3.6
14
- Classifier: Programming Language :: Python :: 3 :: Only
15
- Classifier: Intended Audience :: End Users/Desktop
16
- Classifier: Intended Audience :: Developers
17
- Classifier: Intended Audience :: System Administrators
18
- Classifier: Intended Audience :: Information Technology
19
- Classifier: Operating System :: OS Independent
20
- Classifier: Topic :: Multimedia :: Sound/Audio
21
- Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
22
- Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
23
- Classifier: Topic :: Multimedia :: Video
24
- Classifier: Topic :: Multimedia :: Video :: Conversion
25
- Classifier: Topic :: Software Development :: Libraries
26
- Classifier: Topic :: Utilities
27
- Description-Content-Type: text/markdown
28
- License-File: LICENSE
29
-
30
1
 
31
2
  **Status:**
32
3
  A rainy weekends project under occasional development :)
@@ -39,10 +10,11 @@ A rainy weekends project under occasional development :)
39
10
  - latest from source repository: `$ pip install git+https://github.com/akpw/batch-mp-tools.git`
40
11
 
41
12
  #### Blogs:
42
- - [Practical BatchMP](http://www.akpdev.com/tags.html#BatchMP+Tools)
43
- - [BatchMP Tools Tutorial](http://www.akpdev.com/articles/2015/04/10/batchmp-tutorial-part-i.html)
44
- - [The BatchMP Tools Project](http://www.akpdev.com/articles/2015/03/21/the-batchmp-project.html)
45
- - [Parallel batch media processing with FFmpeg and Python](http://www.akpdev.com/articles/2014/11/24/batch-media-processing-ffmpeg-python.html)
13
+ - [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
14
+ - [Renamer Organize & Virtual Views](https://akpw.github.io/articles/2025/09/22/Print-and-Organize.html)
15
+ - [BatchMP Tools Tutorial](https://akpw.github.io//articles/2015/04/10/batchmp-tutorial-part-i.html)
16
+ - [The BatchMP Tools Project](https://akpw.github.io//articles/2015/03/21/the-batchmp-project.html)
17
+ - [Parallel batch media processing with FFmpeg and Python](https://akpw.github.io//articles/2014/11/24/batch-media-processing-ffmpeg-python.html)
46
18
 
47
19
  ## Description
48
20
 
@@ -62,7 +34,7 @@ By default the tools always visualize targeted changes (whenever possible) befor
62
34
 
63
35
  A little bit more details on each utility:
64
36
 
65
- [**Renamer**](https://github.com/akpw/batch-mp-tools#renamer) is a multi-platform batch rename tool. In addition to common operations such as regexp-based replace, adding text / dates, etc. it also supports advanced operations such as expandable template processing during replace, multi-level indexing across nested directories, flattening folders, and cleaning up non-media files.
37
+ [**Renamer**](https://github.com/akpw/batch-mp-tools#renamer) is a multi-platform batch rename tool. In addition to common operations such as regexp-based replace, adding text / dates, etc. it also supports advanced operations such as expandable template processing during replace, multi-level indexing across nested directories, flattening folders, organizing files by type or date, etc. The enhanced print command now supports virtual views to preview organization without moving files.
66
38
  At its simplest, Renamer can be used to print out the content of current directory:
67
39
  ```
68
40
  $ renamer
@@ -91,6 +63,34 @@ For multi-level indexing of all M4A files in all sub-directories of the current
91
63
  ```
92
64
  Sequential indexing is supported as well using the `-sq` switch. An important detail here, by default Renamer is visualizing the targeted changes and asking for permission to proceed before actually doing anything.
93
65
 
66
+ For organizing files by media type:
67
+ ```
68
+ $ renamer organize -b type
69
+ ~/Downloads
70
+ |- image/
71
+ |- photo1.jpg
72
+ |- screenshot.png
73
+ |- video/
74
+ |- movie.mp4
75
+ |- audio/
76
+ |- song.mp3
77
+
78
+ Proceed? [y/n]:
79
+ ```
80
+
81
+ Or preview how files would look organized by date without moving them:
82
+ ```
83
+ $ renamer print -b date --date-format "%Y/%m"
84
+ Virtual view by date:
85
+ ~/Downloads
86
+ |- 2025/
87
+ |- 01/
88
+ |- document.pdf
89
+ |- photo.jpg
90
+ |- 02/
91
+ |- video.mp4
92
+ ```
93
+
94
94
 
95
95
 
96
96
  [**Tagger**](https://github.com/akpw/batch-mp-tools#tagger) manages media metadata, such as tags and artwork. Setting those in selected media file over multiple nested directories now becomes a breeze, with just a few simple commands working uniformly over almost any practically imaginable audio / video media formats. While easy to use, Tagger supports advanced metadata manipulation such as regexp-based replace, expandable template processing, etc. For example, to set the title tag to respective file names followed by the values of track and tracktotal tags:
@@ -178,7 +178,9 @@ I will follow up with more examples and common use-cases in future blogs.
178
178
  . display sorting:
179
179
  .. by size/date, ascending/descending
180
180
  . action commands:
181
- .. print Prints source directory
181
+ .. print Prints source directory. Enhanced with virtual organization views:
182
+ $ renamer print -b type # Preview organize by media type
183
+ $ renamer print -b date -df "%Y/%m" # Preview organize by date
182
184
  .. flatten Flatten all folders below target level, moving the
183
185
  files up at the target level. By default, deletes all empty flattened folders
184
186
  .. index Adds index to files and directories names
@@ -191,6 +193,10 @@ I will follow up with more examples and common use-cases in future blogs.
191
193
  .. remove Removes n characters from files and directories names
192
194
  .. capitalize Capitalizes words in files / directories names
193
195
  .. delete Delete selected files and directories
196
+ .. organize Organizes files into subdirectories based on their attributes:
197
+ $ renamer organize -b type # By media type (image/, video/, audio/)
198
+ $ renamer organize -b date -df "%Y-%m" # By date (2025-01/, 2025-02/)
199
+ $ renamer organize -b date -df "%Y/%m" -td ~/Sorted # To target directory
194
200
 
195
201
  Usage: renamer [-h] [-d DIR] [-f FILE] [Global Options] {Commands} [Commands Options]
196
202
  Global Options:
@@ -215,7 +221,7 @@ I will follow up with more examples and common use-cases in future blogs.
215
221
  [-q, --quiet] Do not visualise changes / show messages during processing
216
222
 
217
223
  Commands:
218
- {print, index, add_date, add_text, remove, replace, capitalize, flatten, delete, version, info}
224
+ {print, index, add_date, add_text, remove, replace, capitalize, flatten, delete, organize, version, info}
219
225
  $ renamer {command} -h #run this for detailed help on individual commands
220
226
 
221
227
  ### tagger
@@ -324,7 +330,7 @@ Support via FFmpeg: 'AVI', 'FLV', 'MKV', 'MKA'
324
330
  (shows hidden files excluded by default)
325
331
 
326
332
  Target output Directory Target output directory. When omitted, will be
327
- [-td, --target-dir] automatically created at the parent level of
333
+ [-td, --target-dir] automatically created inside the parent level of
328
334
  the input source. For recursive processing,
329
335
  the processed files directory structure there
330
336
  will be the same as for the original files.
@@ -348,8 +354,23 @@ Support via FFmpeg: 'AVI', 'FLV', 'MKV', 'MKA'
348
354
  ## Installing Development version
349
355
  - Clone the repo, then run: `$ python -m pip install .`
350
356
 
351
- **Running Tests**
352
- - Run via: `$ python setup.py test`
357
+ ## Running Tests
358
+
359
+ **Using pytest (recommended):**
360
+ ```bash
361
+ $ pytest -v --tb=short # Run all tests with verbose output
362
+ $ pytest tests/ # Run all tests
363
+ $ pytest tests/fs/test_fs_organize.py # Test organize functionality
364
+ $ pytest tests/fs/test_fsutils.py # Test core filesystem utilities
365
+ $ pytest tests/cli/test_renamer_cli.py # Test renamer CLI
366
+ $ pytest -k "test_organize" # Run tests matching pattern
367
+ ```
368
+
369
+ **Using unittest (fallback):**
370
+ ```bash
371
+ $ python -m unittest discover tests -v # Run all tests with verbose output
372
+ $ python -m unittest tests.fs.test_fs_organize # Run specific test module
373
+ ```
353
374
 
354
375
 
355
376
 
@@ -12,7 +12,7 @@
12
12
  ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
13
  ## GNU General Public License for more details.
14
14
 
15
- import pkg_resources
15
+ from importlib import metadata
16
16
  import batchmp.cli.base.vchk
17
17
  from batchmp.cli.base.bmp_options import BatchMPArgParser, BatchMPBaseCommands
18
18
 
@@ -43,7 +43,7 @@ class BatchMPDispatcher:
43
43
  def print_version(self):
44
44
  ''' Prints BatchMP version info
45
45
  '''
46
- version = pkg_resources.require("batchmp")[0].version
46
+ version = metadata.version("batchmp")
47
47
  print('BatchMP tools version {}'.format(version))
48
48
 
49
49
  def print_info(self):
@@ -94,7 +94,9 @@ class BMFPDispatcher(BatchMPDispatcher):
94
94
  ff_entry_params = FFEntryParamsExt(args)
95
95
  Fragmenter().fragment(ff_entry_params,
96
96
  fragment_starttime = args['fragment_starttime'].total_seconds(),
97
- fragment_duration = args['fragment_duration'].total_seconds())
97
+ fragment_duration = args['fragment_duration'].total_seconds(),
98
+ fragment_trim = args['fragment_trim'].total_seconds(),
99
+ )
98
100
 
99
101
  def segment(self, args):
100
102
  ff_entry_params = FFEntryParamsExt(args)
@@ -146,8 +146,9 @@ class BMFPArgParser(BatchMPArgParser):
146
146
  target_output_group = parser.add_argument_group('Target Output Directory')
147
147
  target_output_group.add_argument("-td", "--target-dir", dest = "target_dir",
148
148
  type = lambda d: self._is_valid_dir_path(parser, d),
149
+ default = '.',
149
150
  help = "Target output directory. When omitted, will be automatically "
150
- "created at the parent level of the input source. "
151
+ "created inside the parent level of the input source. "
151
152
  "For recursive processing, the processed files directory structure there "
152
153
  "will be the same as for the original files.")
153
154
 
@@ -246,14 +247,18 @@ class BMFPArgParser(BatchMPArgParser):
246
247
  description = 'Extracts a fragment via specified start time & duration',
247
248
  formatter_class = BatchMPHelpFormatter)
248
249
  group = fragment_parser.add_argument_group('Fragment parameters')
249
- group.add_argument('-fs', '--starttime', dest='fragment_starttime',
250
+ group.add_argument('-fs', '--start', dest='fragment_starttime',
250
251
  help = 'Fragment start time, in seconds or in the "hh:mm:ss[.xxx]" format',
251
252
  type = lambda f: self._is_timedelta(parser, f),
252
253
  required = True)
253
254
  group.add_argument('-fd', '--duration', dest='fragment_duration',
254
- help = 'Fragment duration, in seconds or in the "hh:mm:ss[.xxx]" format',
255
+ help = 'Fragment duration (default is full media length), in seconds or in the "hh:mm:ss[.xxx]" format',
255
256
  type = lambda f: self._is_timedelta(parser, f),
256
257
  default = timedelta(days = 380))
258
+ group.add_argument('-ft', '--trim', dest='fragment_trim',
259
+ help = 'Fragment trimming at the end (optional), in seconds or in the "hh:mm:ss[.xxx]" format',
260
+ type = lambda f: self._is_timedelta(parser, f),
261
+ default = timedelta(0))
257
262
 
258
263
  # Segment
259
264
  segment_parser = subparsers.add_parser(BMFPCommands.SEGMENT,
@@ -16,7 +16,7 @@ from batchmp.cli.base.bmp_dispatch import BatchMPDispatcher
16
16
  from batchmp.cli.renamer.renamer_options import RenameArgParser, RenamerCommands
17
17
  from batchmp.fstools.dirtools import DHandler
18
18
  from batchmp.fstools.rename import Renamer
19
- from batchmp.fstools.builders.fsprms import FSEntryParamsBase, FSEntryParamsExt, FSEntryParamsFlatten
19
+ from batchmp.fstools.builders.fsprms import FSEntryParamsBase, FSEntryParamsExt, FSEntryParamsFlatten, FSEntryParamsOrganize
20
20
  from batchmp.fstools.builders.fsb import FSEntryBuilderBase
21
21
 
22
22
  class RenameDispatcher(BatchMPDispatcher):
@@ -61,6 +61,9 @@ class RenameDispatcher(BatchMPDispatcher):
61
61
  elif args['sub_cmd'] == RenamerCommands.STATS:
62
62
  self.stats(args)
63
63
 
64
+ elif args['sub_cmd'] == RenamerCommands.ORGANIZE:
65
+ self.organize(args)
66
+
64
67
  else:
65
68
  print('Nothing to dispatch')
66
69
  return False
@@ -69,8 +72,13 @@ class RenameDispatcher(BatchMPDispatcher):
69
72
 
70
73
  # Dispatched Methods
71
74
  def print_dir(self, args):
72
- fs_entry_params = FSEntryParamsBase(args)
73
- DHandler.print_dir(fs_entry_params)
75
+ # Check if organize view is requested
76
+ if args.get('by'):
77
+ fs_entry_params = FSEntryParamsOrganize(args)
78
+ DHandler.print_organized_view(fs_entry_params)
79
+ else:
80
+ fs_entry_params = FSEntryParamsBase(args)
81
+ DHandler.print_dir(fs_entry_params)
74
82
 
75
83
  def stats(self, args):
76
84
  fs_entry_params = FSEntryParamsBase(args)
@@ -117,7 +125,10 @@ class RenameDispatcher(BatchMPDispatcher):
117
125
  fs_entry_params = FSEntryParamsExt(args)
118
126
  Renamer.delete(fs_entry_params)
119
127
 
120
-
128
+ def organize(self, args):
129
+ fs_entry_params = FSEntryParamsOrganize(args)
130
+ DHandler.organize(fs_entry_params)
131
+
121
132
  def main():
122
133
  ''' Renamer entry point
123
134
  '''
@@ -74,6 +74,7 @@ class RenamerCommands(BatchMPBaseCommands):
74
74
  FLATTEN = 'flatten'
75
75
  DELETE = 'delete'
76
76
  STATS = 'stats'
77
+ ORGANIZE = 'organize'
77
78
 
78
79
  @classmethod
79
80
  def commands_meta(cls):
@@ -89,7 +90,8 @@ class RenamerCommands(BatchMPBaseCommands):
89
90
  '{}, '.format(cls.DELETE),
90
91
  '{}, '.format(cls.STATS),
91
92
  '{}, '.format(cls.INFO),
92
- '{}'.format(cls.VERSION),
93
+ '{}, '.format(cls.VERSION),
94
+ '{}' .format(cls.ORGANIZE),
93
95
  '}'))
94
96
 
95
97
 
@@ -142,6 +144,14 @@ class RenameArgParser(BatchMPArgParser):
142
144
  print_parser.add_argument('-ss', '--show-size', dest = 'show_size',
143
145
  help ='Show files size',
144
146
  action = 'store_true')
147
+ print_parser.add_argument('-b', '--by', dest = 'by',
148
+ help = 'Show organized virtual view by type or date',
149
+ type = str,
150
+ choices = ['type', 'date'])
151
+ print_parser.add_argument('-df', '--date-format', dest = 'date_format',
152
+ help = 'Date format for subdirectories when using -b date (e.g., %%Y/%%m)',
153
+ type = str,
154
+ default = '%Y-%m-%d')
145
155
 
146
156
  # Stats
147
157
  stats_parser = subparsers.add_parser(RenamerCommands.STATS,
@@ -299,6 +309,27 @@ class RenameArgParser(BatchMPArgParser):
299
309
  self._add_arg_display_curent_state_mode(delete_parser)
300
310
 
301
311
 
312
+ # Organize
313
+ organize_parser = subparsers.add_parser(RenamerCommands.ORGANIZE,
314
+ description='Organize selected files into directories by specified attributes',
315
+ formatter_class=BatchMPHelpFormatter)
316
+ organize_parser.add_argument('-b', '--by', dest='by',
317
+ help='Organization strategy: by type or by date',
318
+ type=str,
319
+ choices=['type', 'date'],
320
+ required=True)
321
+ organize_parser.add_argument('-df', '--date-format', dest='date_format',
322
+ help='Date format for subdirectories (e.g., %%Y/%%m)',
323
+ type=str,
324
+ default='%Y-%m-%d')
325
+ organize_parser.add_argument('-td', '--target-dir', dest='target_dir',
326
+ help='Target directory to organize files into',
327
+ type=str)
328
+ _add_include_mode_group(organize_parser)
329
+ self._add_arg_display_curent_state_mode(organize_parser)
330
+
331
+
332
+
302
333
  # Args Checking
303
334
  def default_command(self, args, parser):
304
335
  args['sub_cmd'] = RenamerCommands.PRINT
@@ -99,7 +99,7 @@ class CueSplitterTask(ConvertorTask):
99
99
  with temp_dir() as tmp_dir:
100
100
  # prepare the tmp output path
101
101
  conv_fname = '{0:02d} {1}'.format(self.track_number, self.track_title)
102
- conv_fname = re.sub('[^\w\-_\. ]', '_', conv_fname)
102
+ conv_fname = re.sub(r'[^\w\-_\. ]', '_', conv_fname)
103
103
  conv_fname = ''.join((conv_fname, self.target_format))
104
104
  conv_fpath = os.path.join(tmp_dir, conv_fname)
105
105
 
@@ -198,12 +198,12 @@ class CueSplitter(FFMPRunner):
198
198
  if cue_sheet.rem:
199
199
  for rem_item in cue_sheet.rem:
200
200
  if not tag_holder.year:
201
- match = re.match('DATE.+(\d{4})', rem_item)
201
+ match = re.match(r'DATE.+(\d{4})', rem_item)
202
202
  if match:
203
203
  tag_holder.year = match.group(1)
204
204
  continue
205
205
  if not tag_holder.genre:
206
- match = re.match('GENRE\s+(.+)$', rem_item)
206
+ match = re.match(r'GENRE\s+(.+)$', rem_item)
207
207
  if match:
208
208
  tag_holder.genre = match.group(1)
209
209
  tag_holder.comments = ', '.join(cue_sheet.rem)
@@ -19,6 +19,7 @@ from batchmp.commons.utils import temp_dir
19
19
  from batchmp.ffmptools.ffrunner import FFMPRunner, FFMPRunnerTask, LogLevel
20
20
  from batchmp.commons.taskprocessor import TaskResult
21
21
  from batchmp.ffmptools.ffcommands.cmdopt import FFmpegCommands, FFmpegBitMaskOptions
22
+ from batchmp.ffmptools.ffcommands.segment import Segmenter
22
23
  from batchmp.commons.utils import (
23
24
  timed,
24
25
  run_cmd,
@@ -30,10 +31,11 @@ class FragmenterTask(FFMPRunnerTask):
30
31
  '''
31
32
  def __init__(self, fpath, target_dir, log_level,
32
33
  ff_general_options, ff_other_options, preserve_metadata,
33
- fragment_starttime, fragment_duration):
34
+ fragment_starttime, fragment_duration, fragment_trim):
34
35
 
35
36
  self.fragment_starttime = fragment_starttime
36
37
  self.fragment_duration = fragment_duration
38
+ self.fragment_trim = fragment_trim
37
39
 
38
40
  super().__init__(fpath, target_dir, log_level,
39
41
  ff_general_options, ff_other_options, preserve_metadata)
@@ -42,6 +44,9 @@ class FragmenterTask(FFMPRunnerTask):
42
44
  def ff_cmd(self):
43
45
  ''' Fragment command builder
44
46
  '''
47
+ if self.fragment_trim:
48
+ media_duration = Segmenter._media_duration(self.fpath)
49
+ self.fragment_duration = media_duration - self.fragment_trim - self.fragment_starttime
45
50
  return ''.join((super().ff_cmd,
46
51
  ' -ss {}'.format(self.fragment_starttime),
47
52
  ' -t {}'.format(self.fragment_duration)
@@ -65,6 +70,11 @@ class FragmenterTask(FFMPRunnerTask):
65
70
 
66
71
  # run ffmpeg command as a subprocess
67
72
  try:
73
+ if self.fragment_duration < 0:
74
+ raise CmdProcessingError('A problem while processing media file {0}: \
75
+ Negative media duration {1}s, check your input parameters to add up correctly'\
76
+ .format(self.fpath, int(self.fragment_duration)))
77
+
68
78
  _, task_elapsed = run_cmd(p_in)
69
79
  task_result.add_task_step_duration(task_elapsed)
70
80
  except CmdProcessingError as e:
@@ -87,7 +97,7 @@ class FragmenterTask(FFMPRunnerTask):
87
97
 
88
98
  class Fragmenter(FFMPRunner):
89
99
  def fragment(self, ff_entry_params,
90
- fragment_starttime = None, fragment_duration = None):
100
+ fragment_starttime = None, fragment_duration = None, fragment_trim = None):
91
101
 
92
102
  ''' Fragment media file by specified starttime & duration
93
103
  '''
@@ -99,7 +109,7 @@ class Fragmenter(FFMPRunner):
99
109
  # build tasks
100
110
  tasks_params = [(media_file, target_dir_path, ff_entry_params.log_level,
101
111
  ff_entry_params.ff_general_options, ff_entry_params.ff_other_options, ff_entry_params.preserve_metadata,
102
- fragment_starttime, fragment_duration)
112
+ fragment_starttime, fragment_duration, fragment_trim)
103
113
  for media_file, target_dir_path in zip(media_files, target_dirs)]
104
114
  for task_param in tasks_params:
105
115
  task = FragmenterTask(*task_param)
@@ -120,15 +120,15 @@ class Segmenter(FFMPRunner):
120
120
  '''
121
121
  tasks = []
122
122
  if segment_size_MB or segment_length_secs:
123
- if segment_length_secs:
124
- # here need to determine media length
125
- pass_filter = lambda fpath: self._media_duration(fpath) > segment_length_secs
126
- elif segment_size_MB:
127
- # simple media selection by size
128
- pass_filter = lambda fpath: FFH.ffmpeg_supported_media(fpath) and (self._media_size_MB(fpath) > segment_size_MB)
123
+ # if segment_length_secs:
124
+ # # here need to determine media length
125
+ # pass_filter = lambda fpath: self._media_duration(fpath) > segment_length_secs
126
+ # elif segment_size_MB:
127
+ # # simple media selection by size
128
+ # pass_filter = lambda fpath: FFH.ffmpeg_supported_media(fpath) and (self._media_size_MB(fpath) > segment_size_MB)
129
129
 
130
130
  ff_entry_params.target_dir_prefix = 'segmented'
131
- media_files, target_dirs = self._prepare_files(ff_entry_params, pass_filter = pass_filter)
131
+ media_files, target_dirs = self._prepare_files(ff_entry_params)
132
132
 
133
133
  # build tasks
134
134
  tasks_params = [(media_file, target_dir_path, ff_entry_params.log_level,
@@ -239,8 +239,8 @@ class FFH:
239
239
  except CmdProcessingError as e:
240
240
  return None
241
241
  else:
242
- silence_starts = re.findall('(?<=silence_start:)(?:\D*)(\d*\.?\d+)', output)
243
- silence_ends = re.findall('(?<=silence_end:)(?:\D*)(\d*\.?\d+)', output)
242
+ silence_starts = re.findall(r'(?<=silence_start:)(?:\D*)(\d*\.?\d+)', output)
243
+ silence_ends = re.findall(r'(?<=silence_end:)(?:\D*)(\d*\.?\d+)', output)
244
244
 
245
245
  SilenceEntry = namedtuple('SilenceEntry', ['silence_start', 'silence_end'])
246
246
  silence_entries = []
@@ -250,7 +250,7 @@ class FFH:
250
250
  if len(silence_entries) < len(silence_starts):
251
251
  # matched non-balanced silence at the end
252
252
  # try to parse output audio duration and use it as the silence_end value
253
- found = re.findall('(?<=Duration:)(?:\D*)([\d:\.]*)', output)
253
+ found = re.findall(r'(?<=Duration:)(?:\D*)([\d:\.]*)', output)
254
254
  if found:
255
255
  duration = MiscHelpers.time_delta(found[0]).total_seconds()
256
256
  else:
@@ -282,12 +282,12 @@ class FFH:
282
282
  mean_volume = max_volume = 0
283
283
 
284
284
  # mean volume
285
- found = re.findall('(?<=mean_volume:)(?:\D*)(\d*\.?\d+)', output)
285
+ found = re.findall(r'(?<=mean_volume:)(?:\D*)(\d*\.?\d+)', output)
286
286
  if found:
287
287
  mean_volume = float(found[0])
288
288
 
289
289
  # max volume
290
- found = re.findall('(?<=max_volume:)(?:\D*)(\d*\.?\d+)', output)
290
+ found = re.findall(r'(?<=max_volume:)(?:\D*)(\d*\.?\d+)', output)
291
291
  if found:
292
292
  max_volume = float(found[0])
293
293