batchmp 1.4__py3-none-any.whl
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.
- batchmp/__init__.py +0 -0
- batchmp/cli/__init__.py +0 -0
- batchmp/cli/base/__init__.py +0 -0
- batchmp/cli/base/bmp_dispatch.py +60 -0
- batchmp/cli/base/bmp_options.py +349 -0
- batchmp/cli/base/vchk.py +47 -0
- batchmp/cli/bmfp/__init__.py +0 -0
- batchmp/cli/bmfp/bmfp_dispatch.py +120 -0
- batchmp/cli/bmfp/bmfp_options.py +442 -0
- batchmp/cli/renamer/__init__.py +0 -0
- batchmp/cli/renamer/renamer_dispatch.py +135 -0
- batchmp/cli/renamer/renamer_options.py +355 -0
- batchmp/cli/tagger/__init__.py +0 -0
- batchmp/cli/tagger/tagger_dispatch.py +143 -0
- batchmp/cli/tagger/tagger_options.py +338 -0
- batchmp/commons/__init__.py +0 -0
- batchmp/commons/chainedhandler.py +102 -0
- batchmp/commons/descriptors.py +173 -0
- batchmp/commons/progressbar.py +154 -0
- batchmp/commons/taskprocessor.py +149 -0
- batchmp/commons/utils.py +194 -0
- batchmp/ffmptools/__init__.py +0 -0
- batchmp/ffmptools/ffcommands/__init__.py +0 -0
- batchmp/ffmptools/ffcommands/cmdopt.py +115 -0
- batchmp/ffmptools/ffcommands/convert.py +130 -0
- batchmp/ffmptools/ffcommands/cuesplit.py +223 -0
- batchmp/ffmptools/ffcommands/denoise.py +173 -0
- batchmp/ffmptools/ffcommands/fragment.py +121 -0
- batchmp/ffmptools/ffcommands/normalize_peak.py +135 -0
- batchmp/ffmptools/ffcommands/segment.py +157 -0
- batchmp/ffmptools/ffcommands/silencesplit.py +159 -0
- batchmp/ffmptools/ffrunner.py +189 -0
- batchmp/ffmptools/ffutils.py +300 -0
- batchmp/ffmptools/processors/__init__.py +0 -0
- batchmp/ffmptools/processors/basefp.py +92 -0
- batchmp/ffmptools/processors/ffentry.py +81 -0
- batchmp/ffmptools/utils/__init__.py +0 -0
- batchmp/ffmptools/utils/cueparse.py +227 -0
- batchmp/ffmptools/utils/cuesheet.py +239 -0
- batchmp/fstools/__init__.py +0 -0
- batchmp/fstools/builders/__init__.py +0 -0
- batchmp/fstools/builders/fsb.py +221 -0
- batchmp/fstools/builders/fsentry.py +60 -0
- batchmp/fstools/builders/fsprms.py +372 -0
- batchmp/fstools/dirtools.py +549 -0
- batchmp/fstools/fsutils.py +272 -0
- batchmp/fstools/rename.py +390 -0
- batchmp/fstools/walker.py +79 -0
- batchmp/tags/__init__.py +0 -0
- batchmp/tags/handlers/__init__.py +0 -0
- batchmp/tags/handlers/basehandler.py +99 -0
- batchmp/tags/handlers/ffmphandler.py +75 -0
- batchmp/tags/handlers/ffmphandlers/__init__.py +0 -0
- batchmp/tags/handlers/ffmphandlers/base.py +243 -0
- batchmp/tags/handlers/mtghandler.py +56 -0
- batchmp/tags/handlers/pmhandler.py +36 -0
- batchmp/tags/handlers/tagsholder.py +264 -0
- batchmp/tags/output/__init__.py +0 -0
- batchmp/tags/output/formatters.py +218 -0
- batchmp/tags/processors/__init__.py +0 -0
- batchmp/tags/processors/basetp.py +266 -0
- batchmp-1.4.dist-info/METADATA +422 -0
- batchmp-1.4.dist-info/RECORD +68 -0
- batchmp-1.4.dist-info/WHEEL +5 -0
- batchmp-1.4.dist-info/entry_points.txt +5 -0
- batchmp-1.4.dist-info/licenses/LICENSE +11 -0
- batchmp-1.4.dist-info/top_level.txt +1 -0
- batchmp-1.4.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,338 @@
|
|
|
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
|
+
""" Batch management of media files metadata (tags & artwork)
|
|
17
|
+
. Supported formats:
|
|
18
|
+
'MP3', 'MP4', 'M4A', M4V', 'AIFF', 'ASF', 'QuickTime / MOV',
|
|
19
|
+
'FLAC', 'MonkeysAudio', 'Musepack',
|
|
20
|
+
'Ogg FLAC', 'Ogg Speex', 'Ogg Theora', 'Ogg Vorbis',
|
|
21
|
+
'True Audio', 'WavPack', 'OptimFROG'
|
|
22
|
+
|
|
23
|
+
'AVI', 'FLV', 'MKV', 'MKA' (support via FFmpeg)
|
|
24
|
+
. source directory / source file modes
|
|
25
|
+
. include / exclude patterns, etc. (see list of Global Options for details)
|
|
26
|
+
. visualises original / targeted files metadata structure
|
|
27
|
+
. display sorting:
|
|
28
|
+
.. by size/date, ascending/descending
|
|
29
|
+
. action commands:
|
|
30
|
+
.. print Prints media files
|
|
31
|
+
.. set Sets tags in media files, including artwork, e.g:
|
|
32
|
+
$ tagger set --album 'The Album' -art '~/Desktop/art.jpg'
|
|
33
|
+
Supports expandable templates. To specify a template value,
|
|
34
|
+
use the long tag field name preceded by $:
|
|
35
|
+
$ tagger set --title '$title, $track of $tracktotal'
|
|
36
|
+
In addition to tag fields templates, file system names are also supported:
|
|
37
|
+
$ tagger set --title '$filename' --album '$dirname' --artist '$pardirname'...
|
|
38
|
+
.. copy Copies tags from a specified media file
|
|
39
|
+
.. index Indexes Track / Track Total tags
|
|
40
|
+
.. remove Removes tags from media files
|
|
41
|
+
.. replace RegExp-based replace in specified tags
|
|
42
|
+
e.g., to remove the first three characters in title:
|
|
43
|
+
$ tagger replace -tf 'title' -fs '^[\s\S]{0,3}' -rs ''
|
|
44
|
+
.. capitalize Capitalizes words in specified tags
|
|
45
|
+
.. detauch Extracts artwork
|
|
46
|
+
|
|
47
|
+
Usage: tagger [-h] [-d DIR] [-f FILE] [GLobal Options] {Commands}[Commands Options]
|
|
48
|
+
Input source mode:
|
|
49
|
+
[-d, --dir] Source directory (default is the current directory)
|
|
50
|
+
[-f, --file] File to process
|
|
51
|
+
|
|
52
|
+
Recursion mode:
|
|
53
|
+
[-r, --recursive] Recurse into nested folders
|
|
54
|
+
[-el, --end-level] End level for recursion into nested folders
|
|
55
|
+
|
|
56
|
+
Filter files or folders:
|
|
57
|
+
[-in, --include] Include: Unix-style name patterns separated by ';'
|
|
58
|
+
[-sh, --show-hidden] Shows hidden files
|
|
59
|
+
[-ex, --exclude] Exclude: Unix-style name patterns separated by ';'
|
|
60
|
+
(excludes hidden files by default)
|
|
61
|
+
[-fd, --filter-dirs] Enable Include/Exclude patterns on directories
|
|
62
|
+
[-af, --all-files] Disable Include/Exclude patterns on files
|
|
63
|
+
(shows hidden files excluded by default)
|
|
64
|
+
|
|
65
|
+
Miscellaneous:
|
|
66
|
+
[-s, --sort]{na|nd|sa|sd} Sort order for files / folders (name | date, asc | desc)
|
|
67
|
+
[-ni, nested-indent] Indent for printing nested directories
|
|
68
|
+
[-q, --quiet] Do not visualise changes / show messages during processing
|
|
69
|
+
|
|
70
|
+
Commands:
|
|
71
|
+
{print, set, copy, index, remove, replace, capitalize, detauch, version, info}
|
|
72
|
+
$ tagger {command} -h #run this for detailed help on individual commands
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
from batchmp.cli.base.bmp_options import BatchMPArgParser, BatchMPHelpFormatter, BatchMPBaseCommands
|
|
76
|
+
from batchmp.tags.handlers.tagsholder import TagHolder
|
|
77
|
+
from batchmp.fstools.fsutils import FSH
|
|
78
|
+
from batchmp.fstools.builders.fsentry import FSEntryDefaults
|
|
79
|
+
|
|
80
|
+
class TaggerCommands(BatchMPBaseCommands):
|
|
81
|
+
SET = 'set'
|
|
82
|
+
COPY = 'copy'
|
|
83
|
+
INDEX = 'index'
|
|
84
|
+
REMOVE = 'remove'
|
|
85
|
+
REPLACE = 'replace'
|
|
86
|
+
CAPITALIZE = 'capitalize'
|
|
87
|
+
DETAUCH = 'detauch'
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def commands_meta(cls):
|
|
91
|
+
return ''.join(('{',
|
|
92
|
+
'{}, '.format(cls.PRINT),
|
|
93
|
+
'{}, '.format(cls.SET),
|
|
94
|
+
'{}, '.format(cls.COPY),
|
|
95
|
+
'{}, '.format(cls.INDEX),
|
|
96
|
+
'{}, '.format(cls.REMOVE),
|
|
97
|
+
'{}, '.format(cls.REPLACE),
|
|
98
|
+
'{}, '.format(cls.CAPITALIZE),
|
|
99
|
+
'{}, '.format(cls.DETAUCH),
|
|
100
|
+
'{}, '.format(cls.INFO),
|
|
101
|
+
'{}'.format(cls.VERSION),
|
|
102
|
+
'}'))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TaggerArgParser(BatchMPArgParser):
|
|
106
|
+
''' Tagger Commands parsing
|
|
107
|
+
'''
|
|
108
|
+
SUPPORTED_TEXTUAL_TAGGABLE_FIELDS = [field for field in sorted(TagHolder.textual_fields())]
|
|
109
|
+
SUPPORTED_TAGGABLE_FIELDS = [field for field in sorted(TagHolder.taggable_fields())]
|
|
110
|
+
|
|
111
|
+
def __init__(self):
|
|
112
|
+
self._script_name = 'Tagger'
|
|
113
|
+
self._description = \
|
|
114
|
+
'''
|
|
115
|
+
Tagger manages media metadata, such as tags and
|
|
116
|
+
artwork. It can read and write metadata across
|
|
117
|
+
many different formats, with support for advanced
|
|
118
|
+
metadata manipulation such as regexp-based replace
|
|
119
|
+
in tags, expandable template processing, etc.
|
|
120
|
+
|
|
121
|
+
As default behavior, Tagger first visualises targeted
|
|
122
|
+
changes and ask for confirmation before actually
|
|
123
|
+
changing anything.
|
|
124
|
+
'''
|
|
125
|
+
|
|
126
|
+
# Args parsing
|
|
127
|
+
def parse_commands(self, parser):
|
|
128
|
+
''' parses Tagger commands
|
|
129
|
+
'''
|
|
130
|
+
def _add_arg_diff_tags_only_mode(parser):
|
|
131
|
+
parser.add_argument('-do', '--diff-only', dest = 'diff_tags_only',
|
|
132
|
+
help ='Show only changed tags in the confirmation propmt',
|
|
133
|
+
action = 'store_true')
|
|
134
|
+
|
|
135
|
+
subparsers = parser.add_subparsers(dest='sub_cmd',
|
|
136
|
+
title = 'Tagger Commands',
|
|
137
|
+
metavar = TaggerCommands.commands_meta())
|
|
138
|
+
self._add_version(subparsers)
|
|
139
|
+
self._add_info(subparsers)
|
|
140
|
+
|
|
141
|
+
# Print
|
|
142
|
+
print_parser = subparsers.add_parser(TaggerCommands.PRINT,
|
|
143
|
+
description = 'Prints info about media files metadata, such as tags and artwork',
|
|
144
|
+
formatter_class = BatchMPHelpFormatter)
|
|
145
|
+
print_parser.add_argument('-sl', '--startlevel', dest='start_level',
|
|
146
|
+
help = 'Initial nested level for printing (0, i.e. root source directory by default)',
|
|
147
|
+
type = int,
|
|
148
|
+
default = 0)
|
|
149
|
+
print_parser.add_argument('-ss', '--showsize', dest='show_size',
|
|
150
|
+
help ='Shows files size',
|
|
151
|
+
action = 'store_true')
|
|
152
|
+
print_parser.add_argument('-ff', '--fullformat', dest='full_format',
|
|
153
|
+
help ='Shows all media tags',
|
|
154
|
+
action = 'store_true')
|
|
155
|
+
print_parser.add_argument('-st', '--showstats', dest='show_stats',
|
|
156
|
+
help ='Shows media file statistics',
|
|
157
|
+
action = 'store_true')
|
|
158
|
+
|
|
159
|
+
# Set Tags
|
|
160
|
+
set_tags_parser = subparsers.add_parser(TaggerCommands.SET,
|
|
161
|
+
description = 'Sets specified tags in media files. ' \
|
|
162
|
+
'Supports expandable templates, such as $filename, $dirname, $pardirname, $title, $album, ... ',
|
|
163
|
+
formatter_class = BatchMPHelpFormatter)
|
|
164
|
+
set_tags_parser.add_argument('-ti', '--title', dest='title',
|
|
165
|
+
help = "Sets the Title tag",
|
|
166
|
+
type = str)
|
|
167
|
+
set_tags_parser.add_argument('-al', '--album', dest='album',
|
|
168
|
+
help = "Sets the Album tag",
|
|
169
|
+
type = str)
|
|
170
|
+
set_tags_parser.add_argument('-ar', '--artist', dest='artist',
|
|
171
|
+
help = "Sets the Artist tag",
|
|
172
|
+
type = str)
|
|
173
|
+
set_tags_parser.add_argument('-aa', '--albumartist', dest='albumartist',
|
|
174
|
+
help = "Sets the Album Artist tag",
|
|
175
|
+
type = str)
|
|
176
|
+
set_tags_parser.add_argument('-g', '--genre', dest='genre',
|
|
177
|
+
help = "Sets the Genre tag",
|
|
178
|
+
type = str)
|
|
179
|
+
set_tags_parser.add_argument('-c', '--composer', dest='composer',
|
|
180
|
+
help = "Sets the Composer tag",
|
|
181
|
+
type = str)
|
|
182
|
+
set_tags_parser.add_argument('-tr', '--track', dest='track',
|
|
183
|
+
help = "Sets the Track tag",
|
|
184
|
+
type = int)
|
|
185
|
+
set_tags_parser.add_argument('-tt', '--tracktotal', dest='tracktotal',
|
|
186
|
+
help = 'Set the Track Total tag for selected media files',
|
|
187
|
+
type = int)
|
|
188
|
+
set_tags_parser.add_argument('-d', '--disc', dest='disc',
|
|
189
|
+
help = "Sets the Disc tag",
|
|
190
|
+
type = int)
|
|
191
|
+
set_tags_parser.add_argument('-dt', '--disctotal', dest='disctotal',
|
|
192
|
+
help = "Sets the Disctotal tag",
|
|
193
|
+
type = int)
|
|
194
|
+
set_tags_parser.add_argument('-y', '--year', dest='year',
|
|
195
|
+
help = "Sets the Year tag",
|
|
196
|
+
type = int)
|
|
197
|
+
set_tags_parser.add_argument('-en', '--encoder', dest='encoder',
|
|
198
|
+
help = "Sets the Encoder tag",
|
|
199
|
+
type = str)
|
|
200
|
+
set_tags_parser.add_argument('-art', '--artwork', dest='artwork',
|
|
201
|
+
help = "Sets Artwork Image from file path or URL",
|
|
202
|
+
type = lambda fpath: self._is_valid_url_or_file_path(parser, fpath))
|
|
203
|
+
set_tags_parser.add_argument('-bm', '--bpm', dest='bpm',
|
|
204
|
+
help = "Sets the BPM tag",
|
|
205
|
+
type = str)
|
|
206
|
+
set_tags_parser.add_argument('-cmp', '--compilaton', dest='compilaton',
|
|
207
|
+
help = "Sets the Compilaton tag",
|
|
208
|
+
type = lambda fpath: self._is_boolean(parser, fpath))
|
|
209
|
+
set_tags_parser.add_argument('-grp', '--grouping', dest='grouping',
|
|
210
|
+
help = "Sets the Grouping tag",
|
|
211
|
+
type = str)
|
|
212
|
+
set_tags_parser.add_argument('-com', '--comments', dest='comments',
|
|
213
|
+
help = "Sets the Comments tag",
|
|
214
|
+
type = str)
|
|
215
|
+
set_tags_parser.add_argument('-lr', '--lyrics', dest='lyrics',
|
|
216
|
+
help = "Sets the Lyrics tag",
|
|
217
|
+
type = str)
|
|
218
|
+
self._add_arg_display_curent_state_mode(set_tags_parser)
|
|
219
|
+
_add_arg_diff_tags_only_mode(set_tags_parser)
|
|
220
|
+
|
|
221
|
+
# Copy Tags
|
|
222
|
+
copy_tags_parser = subparsers.add_parser(TaggerCommands.COPY,
|
|
223
|
+
description = 'Copies tags from a specified media file',
|
|
224
|
+
formatter_class = BatchMPHelpFormatter)
|
|
225
|
+
copy_tags_parser.add_argument('-th', '--tagholder', dest='tagholder',
|
|
226
|
+
help = "TagHolder Media file: /Path_to_TagHolder_Media_File",
|
|
227
|
+
required = True,
|
|
228
|
+
type = lambda fpath: self._is_valid_file_path(parser, fpath))
|
|
229
|
+
self._add_arg_display_curent_state_mode(copy_tags_parser)
|
|
230
|
+
_add_arg_diff_tags_only_mode(copy_tags_parser)
|
|
231
|
+
|
|
232
|
+
# Index
|
|
233
|
+
index_parser = subparsers.add_parser(TaggerCommands.INDEX,
|
|
234
|
+
description = 'Index Tracks for selected media files',
|
|
235
|
+
formatter_class = BatchMPHelpFormatter)
|
|
236
|
+
index_parser.add_argument('-sf', '--startfrom', dest='start_from',
|
|
237
|
+
help = 'A number from which the indexing starts, 1 by default',
|
|
238
|
+
type = int,
|
|
239
|
+
default = 1)
|
|
240
|
+
self._add_arg_display_curent_state_mode(index_parser)
|
|
241
|
+
_add_arg_diff_tags_only_mode(index_parser)
|
|
242
|
+
|
|
243
|
+
# Remove Tags
|
|
244
|
+
remove_tags_parser = subparsers.add_parser(TaggerCommands.REMOVE,
|
|
245
|
+
description = 'Remove tags from media files',
|
|
246
|
+
formatter_class = BatchMPHelpFormatter)
|
|
247
|
+
remove_tags_parser.add_argument('-tf', '--tag-fields', dest='tag_fields',
|
|
248
|
+
help = "Comma-separated list of tag fields to remove. " \
|
|
249
|
+
"Supported tag fields: {}".format(', '.join(self.SUPPORTED_TAGGABLE_FIELDS)),
|
|
250
|
+
type = str)
|
|
251
|
+
self._add_arg_display_curent_state_mode(remove_tags_parser)
|
|
252
|
+
_add_arg_diff_tags_only_mode(remove_tags_parser)
|
|
253
|
+
|
|
254
|
+
# Replace Tags
|
|
255
|
+
replace_parser = subparsers.add_parser(TaggerCommands.REPLACE,
|
|
256
|
+
description = 'RegExp-based replace in specified tag fields',
|
|
257
|
+
formatter_class = BatchMPHelpFormatter)
|
|
258
|
+
replace_parser.add_argument('-tf', '--tag-fields', dest='tag_fields',
|
|
259
|
+
help = "Comma-separated list of tag fields in which to replace. " \
|
|
260
|
+
"Supported tag fields: {}".format(', '.join(self.SUPPORTED_TEXTUAL_TAGGABLE_FIELDS)),
|
|
261
|
+
type = str,
|
|
262
|
+
required=True)
|
|
263
|
+
replace_parser.add_argument('-fs', '--find-string', dest='find_str',
|
|
264
|
+
help = "Find pattern to look for",
|
|
265
|
+
type = str,
|
|
266
|
+
required=True)
|
|
267
|
+
replace_parser.add_argument('-rs', '--replace-string', dest='replace_str',
|
|
268
|
+
help = "Replace pattern to replace with."\
|
|
269
|
+
"If not specified and there is a match from the find pattern," \
|
|
270
|
+
"the entire string will be replaced with that match",
|
|
271
|
+
type = str)
|
|
272
|
+
replace_parser.add_argument('-ic', '--ignorecase', dest='ignore_case',
|
|
273
|
+
help = 'Case insensitive',
|
|
274
|
+
action = 'store_true')
|
|
275
|
+
self._add_arg_display_curent_state_mode(replace_parser)
|
|
276
|
+
_add_arg_diff_tags_only_mode(replace_parser)
|
|
277
|
+
|
|
278
|
+
# Capitalize Tags
|
|
279
|
+
capitalize_parser = subparsers.add_parser(TaggerCommands.CAPITALIZE,
|
|
280
|
+
description = 'Capitalize words in specified tag fields',
|
|
281
|
+
formatter_class = BatchMPHelpFormatter)
|
|
282
|
+
capitalize_parser.add_argument('-tf', '--tag-fields', dest='tag_fields',
|
|
283
|
+
help = "Comma-separated list of tag fields in which to capitalize words. " \
|
|
284
|
+
"Supported tag fields: {}".format(', '.join(self.SUPPORTED_TEXTUAL_TAGGABLE_FIELDS)),
|
|
285
|
+
type = str,
|
|
286
|
+
required=True)
|
|
287
|
+
self._add_arg_display_curent_state_mode(capitalize_parser)
|
|
288
|
+
_add_arg_diff_tags_only_mode(capitalize_parser)
|
|
289
|
+
|
|
290
|
+
# Detauch Art
|
|
291
|
+
detauch_parser = subparsers.add_parser(TaggerCommands.DETAUCH,
|
|
292
|
+
description = 'Detauches art into specified target directory',
|
|
293
|
+
formatter_class = BatchMPHelpFormatter)
|
|
294
|
+
detauch_parser.add_argument("-td", "--target_dir", dest = "target_dir",
|
|
295
|
+
type = lambda fpath: FSH.full_path(fpath),
|
|
296
|
+
default = None,
|
|
297
|
+
help = "Target directory for detauching art. When omitted, detauched art will be stored in "
|
|
298
|
+
"the top-level media files source directory")
|
|
299
|
+
|
|
300
|
+
# Args Checking
|
|
301
|
+
def default_command(self, args, parser):
|
|
302
|
+
args['sub_cmd'] = TaggerCommands.PRINT
|
|
303
|
+
args['start_level'] = 0
|
|
304
|
+
args['show_size'] = False
|
|
305
|
+
args['show_stats'] = False
|
|
306
|
+
args['full_format'] = False
|
|
307
|
+
|
|
308
|
+
def check_args(self, args, parser):
|
|
309
|
+
super().check_args(args, parser)
|
|
310
|
+
|
|
311
|
+
def parse_tag_fields(fields, supported_fields):
|
|
312
|
+
fields = [r.strip() for r in fields.split(',')]
|
|
313
|
+
for tag_field in fields:
|
|
314
|
+
if tag_field not in supported_fields:
|
|
315
|
+
parser.error('The tag field "{0}" is not supported\n\t' \
|
|
316
|
+
'Supported tag fields: {1}'.format(tag_field, ', '.join(supported_fields)))
|
|
317
|
+
return fields
|
|
318
|
+
|
|
319
|
+
# only consider playable media files by default
|
|
320
|
+
if args['file_type'] == FSEntryDefaults.DEFAULT_FILE_TYPE:
|
|
321
|
+
args['file_type'] = FSEntryDefaults.DEFAULT_MEDIA_TYPE
|
|
322
|
+
|
|
323
|
+
if args['sub_cmd'] == TaggerCommands.INDEX:
|
|
324
|
+
if args['start_from'] < 1:
|
|
325
|
+
parser.error('Track indexing should start from 1, or a larger int number')
|
|
326
|
+
|
|
327
|
+
elif args['sub_cmd'] == TaggerCommands.REMOVE:
|
|
328
|
+
if args['tag_fields'] is not None:
|
|
329
|
+
args['tag_fields'] = parse_tag_fields(args['tag_fields'], \
|
|
330
|
+
self.SUPPORTED_TAGGABLE_FIELDS)
|
|
331
|
+
|
|
332
|
+
elif args['sub_cmd'] in (TaggerCommands.REPLACE, TaggerCommands.CAPITALIZE):
|
|
333
|
+
args['tag_fields'] = parse_tag_fields(args['tag_fields'], \
|
|
334
|
+
self.SUPPORTED_TEXTUAL_TAGGABLE_FIELDS)
|
|
335
|
+
|
|
336
|
+
elif args['sub_cmd'] == TaggerCommands.DETAUCH:
|
|
337
|
+
if args['target_dir'] is None:
|
|
338
|
+
args['target_dir'] = args['dir']
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# coding=utf8
|
|
2
|
+
## Copyright (c) 2014 Arseniy Kuznetsov
|
|
3
|
+
##
|
|
4
|
+
## This program is free software; you can redistribute it and/or
|
|
5
|
+
## modify it under the terms of the GNU General Public License
|
|
6
|
+
## as published by the Free Software Foundation; either version 2
|
|
7
|
+
## of the License, or (at your option) any later version.
|
|
8
|
+
##
|
|
9
|
+
## This program is distributed in the hope that it will be useful,
|
|
10
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
## GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
''' A Chain of Responsibility impl
|
|
16
|
+
Usage:
|
|
17
|
+
>>> handler = ConcreteHandler1() + ConcreteHandler2() + ...
|
|
18
|
+
>>> if handler.can_handle(request):
|
|
19
|
+
.... handler.operation()
|
|
20
|
+
'''
|
|
21
|
+
from abc import ABCMeta, abstractmethod
|
|
22
|
+
from batchmp.commons.descriptors import LazyFunctionPropertyDescriptor
|
|
23
|
+
from weakref import ref, ReferenceType
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ChainedHandler(metaclass = ABCMeta):
|
|
27
|
+
class HandlersChainDispatcher:
|
|
28
|
+
''' Internal dispatcher for chained handlers
|
|
29
|
+
'''
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self._handlers_chain = []
|
|
32
|
+
self._responder_idx = -1
|
|
33
|
+
|
|
34
|
+
def add_handler(self, handler):
|
|
35
|
+
''' Adds a handler to the chain
|
|
36
|
+
'''
|
|
37
|
+
if len(self._handlers_chain) == 0:
|
|
38
|
+
# the first handler owns the chain,
|
|
39
|
+
# hence it needs to be added as a weakref
|
|
40
|
+
self._handlers_chain.append(ref(handler))
|
|
41
|
+
else:
|
|
42
|
+
self._handlers_chain.append(handler)
|
|
43
|
+
|
|
44
|
+
def has_responder(self, request):
|
|
45
|
+
''' Evaluates the handler chain and select a suitable responder
|
|
46
|
+
'''
|
|
47
|
+
for idx, handler in enumerate(self._handlers_chain):
|
|
48
|
+
if isinstance(handler, ReferenceType):
|
|
49
|
+
handler = handler()
|
|
50
|
+
if handler and handler._can_handle(request):
|
|
51
|
+
self._responder_idx = idx
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def responder(self):
|
|
57
|
+
''' Returns the curent responder
|
|
58
|
+
'''
|
|
59
|
+
if self._responder_idx >= 0:
|
|
60
|
+
handler = self._handlers_chain[self._responder_idx]
|
|
61
|
+
if isinstance(handler, ReferenceType):
|
|
62
|
+
handler = handler()
|
|
63
|
+
return handler
|
|
64
|
+
else:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
@LazyFunctionPropertyDescriptor
|
|
68
|
+
def _handler_chain(self):
|
|
69
|
+
''' lazily creates the chain dispatcher and
|
|
70
|
+
stores is as an internal property
|
|
71
|
+
via @LazyFunctionPropertyDescriptor
|
|
72
|
+
'''
|
|
73
|
+
handler_chain = ChainedHandler.HandlersChainDispatcher()
|
|
74
|
+
handler_chain.add_handler(self)
|
|
75
|
+
return handler_chain
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def responder(self):
|
|
79
|
+
''' Returns active responder
|
|
80
|
+
'''
|
|
81
|
+
return self._handler_chain.responder
|
|
82
|
+
|
|
83
|
+
def __add__(self, handler):
|
|
84
|
+
''' Adds a handler to the handlers chain
|
|
85
|
+
'''
|
|
86
|
+
if not isinstance(handler, ChainedHandler):
|
|
87
|
+
raise TypeError('ChainedHandler.__add__() expects a ChainedHandler instance')
|
|
88
|
+
handler._handler_chain = self._handler_chain
|
|
89
|
+
self._handler_chain.add_handler(handler)
|
|
90
|
+
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def can_handle(self, request):
|
|
94
|
+
return self._handler_chain.has_responder(request)
|
|
95
|
+
|
|
96
|
+
# Abstract methods
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def _can_handle(self, request):
|
|
99
|
+
''' implement in specific handlers
|
|
100
|
+
'''
|
|
101
|
+
return False
|
|
102
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# coding=utf8
|
|
2
|
+
## Copyright (c) 2014 Arseniy Kuznetsov
|
|
3
|
+
##
|
|
4
|
+
## This program is free software; you can redistribute it and/or
|
|
5
|
+
## modify it under the terms of the GNU General Public License
|
|
6
|
+
## as published by the Free Software Foundation; either version 2
|
|
7
|
+
## of the License, or (at your option) any later version.
|
|
8
|
+
##
|
|
9
|
+
## This program is distributed in the hope that it will be useful,
|
|
10
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
## GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
''' Properties Descriptors Types
|
|
16
|
+
'''
|
|
17
|
+
from importlib import import_module
|
|
18
|
+
import inspect
|
|
19
|
+
from types import MethodType, FunctionType
|
|
20
|
+
from weakref import WeakKeyDictionary, WeakMethod
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PropertyDescriptor:
|
|
24
|
+
''' Base Property Descriptor (Python 3.6+)
|
|
25
|
+
'''
|
|
26
|
+
# the python 3.6 initializer:
|
|
27
|
+
def __set_name__(self, owner, name):
|
|
28
|
+
self.name = name
|
|
29
|
+
|
|
30
|
+
def __get__(self, instance, owner):
|
|
31
|
+
if instance is None:
|
|
32
|
+
return self
|
|
33
|
+
return instance.__dict__.get(self.name, None)
|
|
34
|
+
|
|
35
|
+
def __set__(self, instance, value):
|
|
36
|
+
instance.__dict__[self.name] = value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LazyClassPropertyDescriptor(PropertyDescriptor):
|
|
40
|
+
''' Dynamically loads class property of a given custom type
|
|
41
|
+
Example:
|
|
42
|
+
fs_entry_builder = LazyClassPropertyDescriptor('batchmp.fstools.builders.fsb.FSEntryBuilderBase')
|
|
43
|
+
'''
|
|
44
|
+
def __init__(self, property_type_classpath, initialize = True):
|
|
45
|
+
super().__init__()
|
|
46
|
+
self._pt_cpath = property_type_classpath
|
|
47
|
+
|
|
48
|
+
def __get__(self, instance, owner=None):
|
|
49
|
+
value = super().__get__(instance, owner = owner)
|
|
50
|
+
if not value:
|
|
51
|
+
value = self.load_lazy_property_class()
|
|
52
|
+
self.__set__(instance, value)
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
def __set__(self, instance, value):
|
|
56
|
+
classname = value.__name__ if inspect.isclass(value) else value.__class__.__name__
|
|
57
|
+
classpath = '.'.join((value.__module__, classname))
|
|
58
|
+
if classpath == self._pt_cpath:
|
|
59
|
+
super().__set__(instance, value)
|
|
60
|
+
else:
|
|
61
|
+
raise TypeError("Type error: {0} is not {1}".format(classpath, self._pt_cpath))
|
|
62
|
+
|
|
63
|
+
# Helpers
|
|
64
|
+
def load_lazy_property_class(self):
|
|
65
|
+
split_path = self._pt_cpath.split('.')
|
|
66
|
+
module_path = '.'.join(split_path[:-1])
|
|
67
|
+
class_name = split_path[-1:][0]
|
|
68
|
+
module = import_module(module_path)
|
|
69
|
+
return getattr(module, class_name)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LazyInstancePropertyDescriptor(LazyClassPropertyDescriptor):
|
|
73
|
+
''' Dynamically loads instance property of a given custom type
|
|
74
|
+
Example:
|
|
75
|
+
tag_holder = LazyInstancePropertyDescriptor('batchmp.tags.handlers.tagsholder.TagHolder')
|
|
76
|
+
'''
|
|
77
|
+
# Helpers
|
|
78
|
+
def load_lazy_property_class(self):
|
|
79
|
+
return super().load_lazy_property_class()()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class LazyFunctionPropertyDescriptor:
|
|
83
|
+
''' Provides lazy property access on the class level
|
|
84
|
+
'''
|
|
85
|
+
def __init__(self, func):
|
|
86
|
+
self._func = func
|
|
87
|
+
def __get__(self, instance, owner=None):
|
|
88
|
+
if instance is None:
|
|
89
|
+
return self
|
|
90
|
+
# this method will only be called when
|
|
91
|
+
# the property has not yet been set on the instance level
|
|
92
|
+
# so checking the instance dictionary here is mostly for the sake of good manners...
|
|
93
|
+
value = instance.__dict__.get(self._func.__name__)
|
|
94
|
+
if not value:
|
|
95
|
+
# the property has not been set yet
|
|
96
|
+
# calculate the value and store it on the instance
|
|
97
|
+
value = self._func(instance)
|
|
98
|
+
instance.__dict__[self._func.__name__] = value
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class FunctionPropertyDescriptor(PropertyDescriptor):
|
|
103
|
+
''' A function type property descriptor
|
|
104
|
+
'''
|
|
105
|
+
def __set__(self, instance, value):
|
|
106
|
+
if (value is None) or isinstance(value, FunctionType):
|
|
107
|
+
super().__set__(instance, value)
|
|
108
|
+
else:
|
|
109
|
+
raise TypeError("Not a Function Type: {}".format(value))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ClassPropertyDescriptor(PropertyDescriptor):
|
|
113
|
+
''' A function type property descriptor
|
|
114
|
+
'''
|
|
115
|
+
def __set__(self, instance, value):
|
|
116
|
+
if (value is None) or inspect.isclass(value):
|
|
117
|
+
super().__set__(instance, value)
|
|
118
|
+
else:
|
|
119
|
+
raise TypeError("Not a Class: {}".format(instance.__class__))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class WeakMethodPropertyDescriptor(PropertyDescriptor):
|
|
123
|
+
''' A bound method type property descriptor
|
|
124
|
+
Uses WeakMethod to prevent reference cycles
|
|
125
|
+
'''
|
|
126
|
+
def __get__(self, instance, owner=None):
|
|
127
|
+
value = super().__get__(instance, owner = owner)
|
|
128
|
+
if value:
|
|
129
|
+
return value()
|
|
130
|
+
else:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def __set__(self, instance, value):
|
|
134
|
+
if isinstance(value, MethodType):
|
|
135
|
+
super().__set__(instance, WeakMethod(value))
|
|
136
|
+
else:
|
|
137
|
+
raise TypeError("Not a Method Type: {}".format(value))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class BooleanPropertyDescriptor(PropertyDescriptor):
|
|
141
|
+
''' A boolean type property descriptor
|
|
142
|
+
'''
|
|
143
|
+
def __set__(self, instance, value):
|
|
144
|
+
if isinstance(value, bool):
|
|
145
|
+
super().__set__(instance, value)
|
|
146
|
+
else:
|
|
147
|
+
raise TypeError("Not a Boolean Type: {}".format(value))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# class PropertyDescriptor:
|
|
156
|
+
# ''' Base Property Descriptor
|
|
157
|
+
# '''
|
|
158
|
+
# def __init__(self):
|
|
159
|
+
# self.data = WeakKeyDictionary()
|
|
160
|
+
#
|
|
161
|
+
# def __get__(self, instance, type=None):
|
|
162
|
+
# return self.data.get(instance)
|
|
163
|
+
#
|
|
164
|
+
# def __set__(self, instance, value):
|
|
165
|
+
# self.data[instance] = value
|
|
166
|
+
#
|
|
167
|
+
# def __delete__(self, instance):
|
|
168
|
+
# if self.data.get(instance):
|
|
169
|
+
# del self.data[instance]
|
|
170
|
+
#
|
|
171
|
+
# def __set_name__(self, owner, name):
|
|
172
|
+
# self.name = '_' + name
|
|
173
|
+
|