Test Failed
Push — dev ( 3a28f6...0451e7 )
by Konstantinos
05:08
created

MetadataDealer.write_metadata()   B

Complexity

Conditions 5

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.675

Importance

Changes 0
Metric Value
cc 5
eloc 22
nop 3
dl 0
loc 31
ccs 7
cts 10
cp 0.7
crap 5.675
rs 8.8853
c 0
b 0
f 0
1 1
import glob
2 1
import logging
3 1
import os
4 1
import re
5 1
from collections import defaultdict
6
7 1
import click
8 1
from mutagen.id3 import ID3, TALB, TDRC, TIT2, TPE1, TPE2, TRCK
9 1
10
from music_album_creation.tracks_parsing import StringParser
11
12
# The main notable classes in mutagen are FileType, StreamInfo, Tags, Metadata and for error handling the MutagenError exception.
13
14 1
15
logger = logging.getLogger(__name__)
16
17 1
18
class MetadataDealerType(object):
19 1
    _filters = defaultdict(
20
        lambda: lambda y: y, track_number=lambda y: MetadataDealerType._parse_year(y)
21 1
    )
22
23 1
    @classmethod
24
    def _parse_year(cls, year):
25 1
        if year == '':
26 1
            return ''
27
        c = re.match(r'0*(\d+)', year)
28 1
        if not c:
29
            raise InvalidInputYearError("Input year tag '{}' is invalid".format(year))
30
        return c.group(1)
31 1
32
33
class MetadataDealer(MetadataDealerType):
34
    #############
35
    # simply add keys and constructor pairs to enrich the support of the API for writting tags/frames to audio files
36 1
    # you can use the cls._filters to add a new post processing filter as shown in MetadataDealerType constructor above
37
    _d = {
38
        'artist': TPE1,  # 4.2.1   TPE1    [#TPE1 Lead performer(s)/Soloist(s)]  ; taken from http://id3.org/id3v2.3.0
39
        #  in clementine temrs, it affects the 'Artist' tab but not the 'Album artist'
40
        'album_artist': TPE2,  # 4.2.1   TPE2    [#TPE2 Band/orchestra/accompaniment]
41
        # in clementine terms, it affects the 'Artist' tab but not the 'Album artist'
42
        'album': TALB,  # 4.2.1   TALB    [#TALB Album/Movie/Show title]
43
        'year': TDRC,  # TDRC (recording time) consolidates TDAT (date), TIME (time), TRDA (recording dates), and TYER (year).
44
    }
45 1
46
    # supported metadata to try and infer automatically
47
    _auto_data = [
48 1
        ('track_number', TRCK),  # 4.2.1   TRCK    [#TRCK Track number/Position in set]
49
        ('track_name', TIT2),
50 1
    ]  # 4.2.1   TIT2    [#TIT2 Title/songname/content description]
51 1
52 1
    _all = dict(_d, **dict(_auto_data))
53
54 1
    @classmethod
55
    def set_album_metadata(
56 1
        cls,
57 1
        album_directory,
58 1
        track_number=True,
59 1
        track_name=True,
60 1
        artist='',
61
        album_artist='',
62
        album='',
63 1
        year='',
64
    ):
65 1
        cls._write_metadata(
66
            album_directory,
67
            track_number=track_number,
68 1
            track_name=track_name,
69 1
            artist=artist,
70 1
            album_artist=album_artist,
71 1
            album=album,
72 1
            year=str(year),
73
        )
74 1
75 1
    @classmethod
76
    def _write_metadata(cls, album_directory, **kwargs):
77 1
        files = glob.glob('{}/*.mp3'.format(album_directory))
78
        logger.info("Album directory: {}".format(album_directory))
79
        for file in files:
80 1
            logger.info("File: {}".format(os.path.basename(file)))
81 1
            cls.write_metadata(
82
                file,
83 1
                **dict(
84
                    cls._filter_auto_inferred(
85
                        StringParser().parse_track_number_n_name(file), **kwargs
86
                    ),
87
                    **{k: kwargs.get(k, '') for k in cls._d.keys()}
88
                )
89 1
            )
90 1
91
    @classmethod
92
    def write_metadata(cls, file, **kwargs):
93 1
        if not all(map(lambda x: x[0] in cls._all.keys(), kwargs.items())):
94 1
            raise RuntimeError(
95 1
                "Some of the input keys [{}] used to request the addition of metadata, do not correspond"
96 1
                " to a tag/frame of the supported [{}]".format(
97 1
                    ', '.join(kwargs.keys()), ' '.join(cls._d)
98 1
                )
99
            )
100
        audio = ID3(file)
101
        for metadata_name, v in kwargs.items():
102
            if bool(v):
103
                audio.add(
104 1
                    cls._all[metadata_name](
105
                        encoding=3, text=u'{}'.format(cls._filters[metadata_name](v))
106
                    )
107
                )
108
                logger.info(
109
                    " {}: {}={}".format(
110
                        metadata_name,
111
                        cls._all[metadata_name].__name__,
112
                        cls._filters[metadata_name](v),
113
                    )
114
                )
115
            else:
116
                logger.warning(
117
                    "Skipping metadata '{}::'{}' because bool({}) == False".format(
118
                        metadata_name, cls._all[metadata_name].__name__, v
119
                    )
120
                )
121
        audio.save()
122
123
    @classmethod
124
    def _filter_auto_inferred(cls, d, **kwargs):
125
        """Given a dictionary (like the one outputted by _infer_track_number_n_name), deletes entries unless it finds them declared in kwargs as key_name=True"""
126
        for k in cls._auto_data:
127
            if not kwargs.get(k, False) and k in d:
128
                del d[k]
129
        return d
130
131
132
class InvalidInputYearError(Exception):
133
    pass
134
135
136
@click.command()
137
@click.option(
138
    '--album-dir',
139
    required=True,
140
    help="The directory where a music album resides. Currently only mp3 "
141
    "files are supported as contents of the directory. Namely only "
142
    "such files will be apprehended as tracks of the album.",
143
)
144
@click.option(
145
    '--track_name/--no-track_name',
146
    default=True,
147
    show_default=True,
148
    help='Whether to extract the track names from the mp3 files and write them as metadata correspondingly.',
149
)
150
@click.option(
151
    '--track_number/--no-track_number',
152
    default=True,
153
    show_default=True,
154
    help='Whether to extract the track numbers from the mp3 files and write them as metadata correspondingly.',
155
)
156
@click.option(
157
    '--artist',
158
    '-a',
159
    help="If given, then value shall be used as the TPE1 tag: 'Lead performer(s)/Soloist(s)'.  In the music player 'clementine' it corresponds to the 'Artist' column.",
160
)
161
@click.option(
162
    '--album_artist',
163
    '-aa',
164
    help="If given, then value shall be used as the TPE2 tag: 'Band/orchestra/accompaniment'.  In the music player 'clementine' it corresponds to the 'Album artist' column.",
165
)
166
@click.option(
167
    '--album',
168
    '-al',
169
    help="If given, then value shall be used as the TALB tag: 'Album/Movie/Show title'.  In the music player 'clementine' it corresponds to the 'Album' column.",
170
)
171
@click.option(
172
    '--year',
173
    'y',
174
    help="If given, then value shall be used as the TDRC tag: 'Recoring time'.  In the music player 'clementine' it corresponds to the 'Year' column.",
175
)
176
def main(album_dir, track_name, track_number, artist, album_artist, album, year):
177
    md = MetadataDealer()
178
    md.set_album_metadata(
179
        album_dir,
180
        track_number=track_number,
181
        track_name=track_name,
182
        artist=artist,
183
        album_artist=album_artist,
184
        album=album,
185
        year=year,
186
    )
187
188
189
if __name__ == '__main__':
190
    main()
191