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,79 @@
|
|
|
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
|
+
import os, sys
|
|
16
|
+
from batchmp.fstools.builders.fsentry import FSEntry, FSEntryType
|
|
17
|
+
from batchmp.fstools.builders.fsprms import FSEntryParamsBase
|
|
18
|
+
from batchmp.fstools.builders.fsb import FSEntryBuilderBase
|
|
19
|
+
|
|
20
|
+
class DWalker:
|
|
21
|
+
''' Walks content of a directory, generating
|
|
22
|
+
a sequence of structured FS elements (FSEntry)
|
|
23
|
+
'''
|
|
24
|
+
@staticmethod
|
|
25
|
+
def entries(fs_entry_params, walker=os.walk):
|
|
26
|
+
''' generates a sequence of FSEntries elements
|
|
27
|
+
'''
|
|
28
|
+
# let's walk
|
|
29
|
+
for rpath, dnames, fnames in walker(fs_entry_params.src_dir):
|
|
30
|
+
# set the current dir
|
|
31
|
+
fs_entry_params.rpath = rpath
|
|
32
|
+
|
|
33
|
+
# check levels
|
|
34
|
+
if fs_entry_params.skip_iteration:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
# set siblings
|
|
38
|
+
fs_entry_params.fnames = fnames
|
|
39
|
+
fs_entry_params.dnames = dnames
|
|
40
|
+
|
|
41
|
+
# sync dnames for sorting / filtering
|
|
42
|
+
dnames[:] = fs_entry_params.merged_dnames
|
|
43
|
+
|
|
44
|
+
# yield the current folder
|
|
45
|
+
yield from fs_entry_params.fs_entry_builder.build_root_entry(fs_entry_params)
|
|
46
|
+
|
|
47
|
+
## Files processing ##
|
|
48
|
+
yield from fs_entry_params.fs_entry_builder.build_entry(fs_entry_params)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def file_entries(fs_entry_params, pass_filter = None):
|
|
52
|
+
if not pass_filter:
|
|
53
|
+
pass_filter = lambda f: True
|
|
54
|
+
|
|
55
|
+
for entry in DWalker.entries(fs_entry_params):
|
|
56
|
+
|
|
57
|
+
if entry.type in (FSEntryType.ROOT, FSEntryType.DIR):
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
if not pass_filter(entry.realpath):
|
|
61
|
+
continue
|
|
62
|
+
else:
|
|
63
|
+
yield entry
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def dir_entries(fs_entry_params, pass_filter = None):
|
|
67
|
+
if not pass_filter:
|
|
68
|
+
pass_filter = lambda f: True
|
|
69
|
+
|
|
70
|
+
for entry in DWalker.entries(fs_entry_params):
|
|
71
|
+
|
|
72
|
+
if entry.type in (FSEntryType.ROOT, FSEntryType.FILE):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if not pass_filter(entry.realpath):
|
|
76
|
+
continue
|
|
77
|
+
else:
|
|
78
|
+
yield entry
|
|
79
|
+
|
batchmp/tags/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
""" Tag Handlers responsibility chain
|
|
16
|
+
"""
|
|
17
|
+
import os
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from batchmp.commons.chainedhandler import ChainedHandler
|
|
20
|
+
from abc import abstractmethod
|
|
21
|
+
from batchmp.fstools.fsutils import UniqueDirNamesChecker
|
|
22
|
+
from batchmp.commons.descriptors import LazyInstancePropertyDescriptor
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DetauchedArtType(Enum):
|
|
26
|
+
''' Detached art type specifier
|
|
27
|
+
'''
|
|
28
|
+
PNG, JPEG = 0, 1
|
|
29
|
+
@staticmethod
|
|
30
|
+
def art_ext(type):
|
|
31
|
+
if type == DetauchedArtType.JPEG:
|
|
32
|
+
return '.jpg'
|
|
33
|
+
else:
|
|
34
|
+
return '.png'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TagHandler(ChainedHandler):
|
|
38
|
+
tag_holder = LazyInstancePropertyDescriptor('batchmp.tags.handlers.tagsholder.TagHolder')
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self._media_handler = None
|
|
42
|
+
|
|
43
|
+
def __add__(self, tag_handler):
|
|
44
|
+
tag_handler.tag_holder = self.tag_holder
|
|
45
|
+
return super().__add__(tag_handler)
|
|
46
|
+
|
|
47
|
+
def _can_handle(self, path):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
# Tag Handler operations
|
|
51
|
+
def save(self):
|
|
52
|
+
self.responder._save()
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def _save(self):
|
|
56
|
+
''' implement in specific tag handlers
|
|
57
|
+
'''
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
# Helpers
|
|
61
|
+
def _reset_handler(self):
|
|
62
|
+
''' resets the handler
|
|
63
|
+
'''
|
|
64
|
+
self._media_handler = None
|
|
65
|
+
self.tag_holder.reset_tags()
|
|
66
|
+
|
|
67
|
+
def copy_tags(self, tag_holder = None):
|
|
68
|
+
''' copies tags from a tag_holder
|
|
69
|
+
'''
|
|
70
|
+
self.tag_holder.copy_tags(tag_holder = tag_holder)
|
|
71
|
+
|
|
72
|
+
def clear_tags(self):
|
|
73
|
+
''' clear tags values
|
|
74
|
+
'''
|
|
75
|
+
self.tag_holder.clear_tags()
|
|
76
|
+
|
|
77
|
+
def detauch_art(self, dir_path = None, type = None):
|
|
78
|
+
''' detauches art, returning art file path
|
|
79
|
+
'''
|
|
80
|
+
if not type or (type not in DetauchedArtType):
|
|
81
|
+
type = DetauchedArtType.PNG
|
|
82
|
+
art_path = None
|
|
83
|
+
if self.tag_holder.art:
|
|
84
|
+
if not dir_path:
|
|
85
|
+
dir_path = os.path.basename(self._media_handler.path)
|
|
86
|
+
fname = os.path.splitext(os.path.basename(self._media_handler.path))[0] + DetauchedArtType.art_ext(type)
|
|
87
|
+
fname = UniqueDirNamesChecker(dir_path).unique_name(fname)
|
|
88
|
+
|
|
89
|
+
art_path = os.path.join(dir_path, fname)
|
|
90
|
+
with open(art_path, 'wb') as f:
|
|
91
|
+
f.write(self.tag_holder.art)
|
|
92
|
+
return art_path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
import os, shutil
|
|
16
|
+
from batchmp.commons.utils import temp_dir
|
|
17
|
+
from batchmp.tags.handlers.basehandler import TagHandler
|
|
18
|
+
from batchmp.tags.handlers.ffmphandlers.base import FFBaseFormatHandler
|
|
19
|
+
from batchmp.ffmptools.ffutils import FFH
|
|
20
|
+
from batchmp.commons.utils import run_cmd, CmdProcessingError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FFmpegTagHandler(TagHandler):
|
|
24
|
+
''' FFmpeg-Based Tag Handler
|
|
25
|
+
'''
|
|
26
|
+
def _can_handle(self, path):
|
|
27
|
+
self._reset_handler()
|
|
28
|
+
media_entry = FFH.media_file_info(path)
|
|
29
|
+
if media_entry:
|
|
30
|
+
self._media_handler = FFBaseFormatHandler(self.tag_holder) #... + FFSpecificFormatHandler() + ...
|
|
31
|
+
if self._media_handler.can_handle(media_entry):
|
|
32
|
+
self.tag_holder.filepath = path
|
|
33
|
+
self._media_handler.parse()
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def _save(self, write_artwork = True):
|
|
38
|
+
''' saves tags
|
|
39
|
+
'''
|
|
40
|
+
if not self._media_handler:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
with temp_dir() as tmp:
|
|
44
|
+
tmp_fpath = os.path.join(tmp, os.path.basename(self._media_handler.path))
|
|
45
|
+
|
|
46
|
+
artwork_writer = write_artwork and \
|
|
47
|
+
self._media_handler.artwork_writer_supported_format and \
|
|
48
|
+
self.tag_holder.art
|
|
49
|
+
art_path = self.detauch_art(dir_path = tmp) if artwork_writer else None
|
|
50
|
+
|
|
51
|
+
save_cmd = self._media_handler.build_save_cmd(art_path = art_path)
|
|
52
|
+
save_cmd = ''.join((save_cmd, ' "{}"'.format(tmp_fpath)))
|
|
53
|
+
try:
|
|
54
|
+
failed = False
|
|
55
|
+
output, _ = run_cmd(save_cmd)
|
|
56
|
+
except CmdProcessingError as e:
|
|
57
|
+
if artwork_writer:
|
|
58
|
+
self._save(write_artwork = False)
|
|
59
|
+
return
|
|
60
|
+
else:
|
|
61
|
+
failed = True
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
shutil.move(tmp_fpath, self._media_handler.path)
|
|
65
|
+
except OSError as e:
|
|
66
|
+
raise e
|
|
67
|
+
|
|
68
|
+
if failed:
|
|
69
|
+
print ('FFMP: could not process {}'.format(self._media_handler.path))
|
|
70
|
+
else:
|
|
71
|
+
if not write_artwork:
|
|
72
|
+
print ('FFMP: skipped artwork for {}'.format(self._media_handler.path))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
File without changes
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
""" FFmpeg generic handler (no format-related specifics)
|
|
16
|
+
"""
|
|
17
|
+
import os, shlex
|
|
18
|
+
from batchmp.commons.chainedhandler import ChainedHandler
|
|
19
|
+
from batchmp.ffmptools.ffutils import FFH
|
|
20
|
+
from batchmp.tags.handlers.tagsholder import TagHolder
|
|
21
|
+
from batchmp.commons.utils import temp_dir
|
|
22
|
+
from batchmp.commons.utils import (
|
|
23
|
+
run_cmd,
|
|
24
|
+
CmdProcessingError
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
class FFBaseFormatHandler(ChainedHandler):
|
|
28
|
+
ARTWORK_WRITER_SUPPORTED_FORMATS = ['MP3']
|
|
29
|
+
|
|
30
|
+
''' Base FFmpeg tags parse
|
|
31
|
+
'''
|
|
32
|
+
def __init__(self, tag_holder):
|
|
33
|
+
self.media_entry = None
|
|
34
|
+
self.tag_holder = tag_holder
|
|
35
|
+
|
|
36
|
+
def __add__(self, tag_handler):
|
|
37
|
+
tag_handler.tag_holder = self.tag_holder
|
|
38
|
+
return super().__add__(tag_handler)
|
|
39
|
+
|
|
40
|
+
def _can_handle(self, media_entry):
|
|
41
|
+
if media_entry and FFH.ffmpeg_supported_media(ffentry = media_entry):
|
|
42
|
+
self.media_entry = media_entry
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def path(self):
|
|
48
|
+
if self.media_entry:
|
|
49
|
+
return self.media_entry.path
|
|
50
|
+
else:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def type(self):
|
|
55
|
+
if self.media_entry and self.media_entry.format:
|
|
56
|
+
format = self.media_entry.format.get('format_name')
|
|
57
|
+
if format:
|
|
58
|
+
format = format.split(',')[0]
|
|
59
|
+
return format.upper()
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def artwork_writer_supported_format(self):
|
|
64
|
+
return self.type in self.ARTWORK_WRITER_SUPPORTED_FORMATS
|
|
65
|
+
|
|
66
|
+
# tag handler operations
|
|
67
|
+
def parse(self):
|
|
68
|
+
return self.responder._parse()
|
|
69
|
+
|
|
70
|
+
def build_save_cmd(self, art_path = None):
|
|
71
|
+
return self.responder._build_save_cmd(art_path = art_path)
|
|
72
|
+
|
|
73
|
+
# tag handler ops impl
|
|
74
|
+
def _parse(self):
|
|
75
|
+
''' parses tags from FFmpeg output
|
|
76
|
+
'''
|
|
77
|
+
self._parse_tags()
|
|
78
|
+
self._parse_stats()
|
|
79
|
+
self._parse_art()
|
|
80
|
+
|
|
81
|
+
def _parse_tags(self):
|
|
82
|
+
# Tags
|
|
83
|
+
if self.media_entry.format:
|
|
84
|
+
tag_info = self.media_entry.format.get('tags')
|
|
85
|
+
if not tag_info:
|
|
86
|
+
tag_info = self.media_entry.audio.get('tags')
|
|
87
|
+
if tag_info:
|
|
88
|
+
tag_info = {k.lower():v for k,v in tag_info.items()}
|
|
89
|
+
|
|
90
|
+
self.tag_holder.title = tag_info.get('title')
|
|
91
|
+
self.tag_holder.album = tag_info.get('album')
|
|
92
|
+
self.tag_holder.artist = tag_info.get('artist')
|
|
93
|
+
self.tag_holder.albumartist = tag_info.get('album_artist')
|
|
94
|
+
self.tag_holder.genre = tag_info.get('genre')
|
|
95
|
+
self.tag_holder.year = tag_info.get('date')
|
|
96
|
+
self.tag_holder.composer = tag_info.get('composer')
|
|
97
|
+
self.tag_holder.encoder = tag_info.get('encoded_by')
|
|
98
|
+
|
|
99
|
+
self.tag_holder.bpm = tag_info.get('tbpm') or tag_info.get('bpm')
|
|
100
|
+
self.tag_holder.comp = tag_info.get('compilation')
|
|
101
|
+
self.tag_holder.grouping = tag_info.get('tit1')
|
|
102
|
+
self.tag_holder.comments = tag_info.get('comment')
|
|
103
|
+
self.tag_holder.lyrics = tag_info.get('lyrics')
|
|
104
|
+
|
|
105
|
+
if 'track' in tag_info:
|
|
106
|
+
track_info = tag_info['track'].split('/')
|
|
107
|
+
if len(track_info) > 0:
|
|
108
|
+
self.tag_holder.track = track_info[0]
|
|
109
|
+
self.tag_holder.tracktotal = track_info[len(track_info) - 1]
|
|
110
|
+
if 'disc' in tag_info:
|
|
111
|
+
disc_info = tag_info['disc'].split('/')
|
|
112
|
+
if len(disc_info) > 0:
|
|
113
|
+
self.tag_holder.disc = disc_info[0]
|
|
114
|
+
self.tag_holder.disctotal = disc_info[len(disc_info) - 1]
|
|
115
|
+
|
|
116
|
+
def _parse_stats(self):
|
|
117
|
+
# Non-taggable fields
|
|
118
|
+
# non-tagable fields defaults
|
|
119
|
+
self.tag_holder.length = 0.0
|
|
120
|
+
self.tag_holder.bitrate = 0
|
|
121
|
+
self.tag_holder.samplerate = 0
|
|
122
|
+
self.tag_holder.channels = 0
|
|
123
|
+
self.tag_holder.bitdepth = 0
|
|
124
|
+
|
|
125
|
+
if self.media_entry.audio:
|
|
126
|
+
self.tag_holder.length = float(self.media_entry.audio.get('duration', 0.0))
|
|
127
|
+
self.tag_holder.bitrate = int(self.media_entry.audio.get('bit_rate', 0))
|
|
128
|
+
self.tag_holder.samplerate = int(self.media_entry.audio.get('sample_rate', 0))
|
|
129
|
+
self.tag_holder.bitdepth = int(self.media_entry.audio.get('bits_per_sample', 0))
|
|
130
|
+
self.tag_holder.channels = int(self.media_entry.audio.get('channels', 0))
|
|
131
|
+
|
|
132
|
+
if self.media_entry.format:
|
|
133
|
+
if self.tag_holder.bitrate == 0:
|
|
134
|
+
self.tag_holder.bitrate = int(self.media_entry.format.get('bit_rate', 0))
|
|
135
|
+
|
|
136
|
+
format = self.media_entry.format.get('format_name')
|
|
137
|
+
if format:
|
|
138
|
+
format = format.upper()
|
|
139
|
+
format_long = self.media_entry.format.get('format_long_name')
|
|
140
|
+
if format_long:
|
|
141
|
+
format = '{0}: {1}'.format(format, format_long)
|
|
142
|
+
|
|
143
|
+
if format:
|
|
144
|
+
self.tag_holder.format = format.upper()
|
|
145
|
+
|
|
146
|
+
if not self.tag_holder.length:
|
|
147
|
+
self.tag_holder.length = float(self.media_entry.format.get('duration', 0.0))
|
|
148
|
+
|
|
149
|
+
def _parse_art(self):
|
|
150
|
+
# Art
|
|
151
|
+
if self.media_entry.artwork:
|
|
152
|
+
self.tag_holder.deferred_art_method = self.artwork_reader
|
|
153
|
+
|
|
154
|
+
def _build_save_cmd(self, art_path = None):
|
|
155
|
+
''' build save cmd string
|
|
156
|
+
'''
|
|
157
|
+
track_tagger = disc_tagger = ''
|
|
158
|
+
if self.tag_holder.track:
|
|
159
|
+
if self.tag_holder.tracktotal:
|
|
160
|
+
track_tagger = ' -metadata track="{0}/{1}"'.format(self.tag_holder.track,
|
|
161
|
+
self.tag_holder.tracktotal)
|
|
162
|
+
else:
|
|
163
|
+
track_tagger = ' -metadata track="{}"'.format(self.tag_holder.track)
|
|
164
|
+
elif self.tag_holder.tracktotal:
|
|
165
|
+
track_tagger = ' -metadata track="0/{}"'.format(self.tag_holder.tracktotal)
|
|
166
|
+
else:
|
|
167
|
+
track_tagger = ' -metadata track=""'
|
|
168
|
+
if self.tag_holder.disc:
|
|
169
|
+
if self.tag_holder.disctotal:
|
|
170
|
+
disc_tagger = ' -metadata disc="{0}/{1}"'.format(self.tag_holder.disc,
|
|
171
|
+
self.tag_holder.disctotal)
|
|
172
|
+
else:
|
|
173
|
+
disc_tagger = ' -metadata disc="{}"'.format(self.tag_holder.disc)
|
|
174
|
+
elif self.tag_holder.disctotal:
|
|
175
|
+
disc_tagger = ' -metadata disc="0/{}"'.format(self.tag_holder.disctotal)
|
|
176
|
+
else:
|
|
177
|
+
disc_tagger = ' -metadata disc=""'
|
|
178
|
+
|
|
179
|
+
cmd = ''.join(('ffmpeg ',
|
|
180
|
+
' -v quiet',
|
|
181
|
+
' -i {}'.format(shlex.quote(self.media_entry.path)),
|
|
182
|
+
' -i {}'.format(shlex.quote(art_path)) if art_path else '',
|
|
183
|
+
' -c copy',
|
|
184
|
+
' -map_metadata 0',
|
|
185
|
+
' -map 0',
|
|
186
|
+
' -map 1' if art_path else '',
|
|
187
|
+
' -metadata title="{}"'.format(self.tag_holder.title
|
|
188
|
+
if self.tag_holder.title else ''),
|
|
189
|
+
' -metadata album="{}"'.format(self.tag_holder.album
|
|
190
|
+
if self.tag_holder.album else ''),
|
|
191
|
+
' -metadata artist="{}"'.format(self.tag_holder.artist
|
|
192
|
+
if self.tag_holder.artist else ''),
|
|
193
|
+
' -metadata album_artist="{}"'.format(self.tag_holder.albumartist
|
|
194
|
+
if self.tag_holder.albumartist else ''),
|
|
195
|
+
' -metadata genre="{}"'.format(self.tag_holder.genre
|
|
196
|
+
if self.tag_holder.genre else ''),
|
|
197
|
+
' -metadata year="{}"'.format(self.tag_holder.year
|
|
198
|
+
if self.tag_holder.year else ''),
|
|
199
|
+
' -metadata composer="{}"'.format(self.tag_holder.composer
|
|
200
|
+
if self.tag_holder.composer else ''),
|
|
201
|
+
' -metadata encoded_by="{}"'.format(self.tag_holder.encoder
|
|
202
|
+
if self.tag_holder.encoder else ''),
|
|
203
|
+
|
|
204
|
+
' -metadata BPM="{}"'.format(self.tag_holder.bpm
|
|
205
|
+
if self.tag_holder.bpm else ''),
|
|
206
|
+
' -metadata TBPM="{}"'.format(self.tag_holder.bpm
|
|
207
|
+
if self.tag_holder.bpm else ''),
|
|
208
|
+
' -metadata compilation="{}"'.format(self.tag_holder.comp
|
|
209
|
+
if self.tag_holder.comp else ''),
|
|
210
|
+
' -metadata grouping="{}"'.format(self.tag_holder.grouping
|
|
211
|
+
if self.tag_holder.grouping else ''),
|
|
212
|
+
' -metadata comment="{}"'.format(self.tag_holder.comments
|
|
213
|
+
if self.tag_holder.comments else ''),
|
|
214
|
+
' -metadata lyrics="{}"'.format(self.tag_holder.lyrics
|
|
215
|
+
if self.tag_holder.lyrics else ''),
|
|
216
|
+
track_tagger,
|
|
217
|
+
disc_tagger))
|
|
218
|
+
return cmd
|
|
219
|
+
|
|
220
|
+
def artwork_reader(self):
|
|
221
|
+
''' reads cover art from a media file
|
|
222
|
+
'''
|
|
223
|
+
artwork = None
|
|
224
|
+
if self.media_entry.artwork:
|
|
225
|
+
artwork_stream_idx = self.media_entry.artwork.get('index')
|
|
226
|
+
with temp_dir() as tmp:
|
|
227
|
+
detached_img_path = os.path.join(tmp, 'detached.png')
|
|
228
|
+
cmd = ' '.join(('ffmpeg',
|
|
229
|
+
' -v quiet',
|
|
230
|
+
' -i {}'.format(shlex.quote(self.media_entry.path)),
|
|
231
|
+
' -map 0:{}'.format(artwork_stream_idx),
|
|
232
|
+
' -an',
|
|
233
|
+
' -vcodec copy',
|
|
234
|
+
' {}'.format(detached_img_path)))
|
|
235
|
+
try:
|
|
236
|
+
output, _ = run_cmd(cmd)
|
|
237
|
+
except CmdProcessingError as e:
|
|
238
|
+
pass
|
|
239
|
+
else:
|
|
240
|
+
with open(detached_img_path, 'rb') as img:
|
|
241
|
+
artwork = img.read()
|
|
242
|
+
return artwork
|
|
243
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
from mediafile import MediaFile, UnreadableFileError, MutagenError
|
|
16
|
+
from batchmp.tags.handlers.basehandler import TagHandler
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MutagenTagHandler(TagHandler):
|
|
20
|
+
''' Mutagen-Based Tag Handler
|
|
21
|
+
'''
|
|
22
|
+
def _can_handle(self, path):
|
|
23
|
+
''' Handles the formats supported by Mutagen
|
|
24
|
+
'''
|
|
25
|
+
self._reset_handler()
|
|
26
|
+
try:
|
|
27
|
+
self._media_handler = MediaFile(path)
|
|
28
|
+
except UnreadableFileError as error:
|
|
29
|
+
return False
|
|
30
|
+
else:
|
|
31
|
+
self.tag_holder.filepath = path
|
|
32
|
+
self._parse_tags()
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
def _parse_tags(self):
|
|
36
|
+
''' copies relevant properties from Mutagen MediaFile
|
|
37
|
+
'''
|
|
38
|
+
for field in self._media_handler.readable_fields():
|
|
39
|
+
if field in dir(self.tag_holder):
|
|
40
|
+
attr = getattr(self._media_handler, field)
|
|
41
|
+
if attr:
|
|
42
|
+
setattr(self.tag_holder, field, attr)
|
|
43
|
+
#else:
|
|
44
|
+
# dev test
|
|
45
|
+
# attr = getattr(self._media_handler, field)
|
|
46
|
+
# if attr:
|
|
47
|
+
# print('Ignoring: {0} with value: {1}'.format(field, attr))
|
|
48
|
+
|
|
49
|
+
def _save(self):
|
|
50
|
+
if self._media_handler:
|
|
51
|
+
for field in self.tag_holder.taggable_fields():
|
|
52
|
+
value = getattr(self.tag_holder, field)
|
|
53
|
+
setattr(self._media_handler, field, value)
|
|
54
|
+
|
|
55
|
+
self._media_handler.save()
|
|
56
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
from batchmp.ffmptools.ffutils import FFH
|
|
16
|
+
from batchmp.tags.handlers.basehandler import TagHandler
|
|
17
|
+
from batchmp.fstools.builders.fsentry import FSMediaEntryType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PlayableMediaHandler(TagHandler):
|
|
21
|
+
''' Playable Media Cheker
|
|
22
|
+
'''
|
|
23
|
+
def _can_handle(self, path):
|
|
24
|
+
''' Quick check for right media types to handle
|
|
25
|
+
'''
|
|
26
|
+
media_type = FFH.media_type(fpath = path, fast_scan = True)
|
|
27
|
+
|
|
28
|
+
supported_media = media_type in (FSMediaEntryType.VIDEO, FSMediaEntryType.AUDIO)
|
|
29
|
+
if media_type in (FSMediaEntryType.VIDEO, FSMediaEntryType.AUDIO):
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|