Completed
Push — dev ( 232b11...6092be )
by Konstantinos
04:15 queued 01:32
created

music_album_creation.metadata   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 109
Duplicated Lines 0 %

Test Coverage

Coverage 88.14%

Importance

Changes 0
Metric Value
eloc 73
dl 0
loc 109
ccs 52
cts 59
cp 0.8814
rs 10
c 0
b 0
f 0
wmc 20

6 Methods

Rating   Name   Duplication   Size   Complexity  
A MetadataDealerType.__parse_year() 0 8 3
A MetadataDealer.write_metadata() 0 11 5
A MetadataDealer._write_metadata() 0 7 2
A MetadataDealer.set_album_metadata() 0 3 1
A MetadataDealer._filter_auto_inferred() 0 7 4
A MetadataDealerType.__new__() 0 4 4

1 Function

Rating   Name   Duplication   Size   Complexity  
A main() 0 13 1
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
10 1
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
15 1
logger = logging.getLogger(__name__)
16
17
18 1
class MetadataDealerType(type):
19
20 1
    @staticmethod
21
    def __parse_year(year):
22 1
        if year == '':
23
            return ''
24 1
        c = re.match(r'0*(\d+)', year)
25 1
        if not c:
26
            raise InvalidInputYearError("Input year tag '{}' is invalid".format(year))
27 1
        return c.group(1)
28
29 1
    def __new__(mcs, name, bases, attributes):
30 1
        x = super().__new__(mcs, name, bases, attributes)
31 1
        x._filters = defaultdict(lambda: lambda y: y, track_number=lambda y: mcs.__parse_year(y))
32 1
        return x
33
34
35 1
class MetadataDealer(metaclass=MetadataDealerType):
36
37
    #############
38
    # simply add keys and constructor pairs to enrich the support of the API for writting tags/frames to audio files
39
    # you can use the cls._filters to add a new post processing filter as shown in MetadataDealerType constructor above
40 1
    _d = {'artist': TPE1,  # 4.2.1   TPE1    [#TPE1 Lead performer(s)/Soloist(s)]  ; taken from http://id3.org/id3v2.3.0
41
          #  in clementine temrs, it affects the 'Artist' tab but not the 'Album artist'
42
          'album_artist': TPE2,  # 4.2.1   TPE2    [#TPE2 Band/orchestra/accompaniment]
43
          # in clementine terms, it affects the 'Artist' tab but not the 'Album artist'
44
          'album': TALB,  # 4.2.1   TALB    [#TALB Album/Movie/Show title]
45
          'year': TDRC  # TDRC (recording time) consolidates TDAT (date), TIME (time), TRDA (recording dates), and TYER (year).
46
          }
47
48
    # supported metadata to try and infer automatically
49 1
    _auto_data = [('track_number', TRCK),  # 4.2.1   TRCK    [#TRCK Track number/Position in set]
50
                  ('track_name', TIT2)]   # 4.2.1   TIT2    [#TIT2 Title/songname/content description]
51
52 1
    _all = dict(_d, **dict(_auto_data))
53
54
    # reg = re.compile(r'(?:(\d{1,2})(?:[ \t]*[\-\.][ \t]*|[ \t]+)|^)?([\w\'\(\) ’]*[\w)])\.mp3$')  # use to parse track file names like "1. Loyal to the Pack.mp3"
55
56 1
    @classmethod
57 1
    def set_album_metadata(cls, album_directory, track_number=True, track_name=True, artist='', album_artist='', album='', year='', verbose=False):
58 1
        cls._write_metadata(album_directory, track_number=track_number, track_name=track_name, artist=artist, album_artist=album_artist, album=album, year=str(year))
59
60 1
    @classmethod
61
    def _write_metadata(cls, album_directory, **kwargs):
62 1
        files = glob.glob('{}/*.mp3'.format(album_directory))
63 1
        logger.info("Files selected: [{}]".format(', '.join(map(os.path.basename, files))))
64 1
        for file in files:
65 1
            cls.write_metadata(file, **dict(cls._filter_auto_inferred(StringParser.parse_track_number_n_name(file), **kwargs),
66
                                            **{k: kwargs.get(k, '') for k in cls._d.keys()}))
67
68 1
    @classmethod
69
    def write_metadata(cls, file, **kwargs):
70 1
        if not all(map(lambda x: x[0] in cls._all.keys(), kwargs.items())):
71
            raise RuntimeError("Some of the input keys [{}] used to request the addition of metadata, do not correspond"
72
                               " to a tag/frame of the supported [{}]".format(', '.join(kwargs.keys()), ' '.join(cls._d)))
73 1
        audio = ID3(file)
74 1
        for k, v in kwargs.items():
75 1
            if bool(v):
76 1
                audio.add(cls._all[k](encoding=3, text=u'{}'.format(cls._filters[k](v))))
77 1
                logger.info("Track '{}'; set {}: {}={}".format(file, k, cls._all[k].__name__, cls._filters[k](v)))
78 1
        audio.save()
79
80 1
    @classmethod
81
    def _filter_auto_inferred(cls, d, **kwargs):
82
        """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"""
83 1
        for k in cls._auto_data:
84 1
            if not kwargs.get(k, False) and k in d:
85
                del d[k]
86 1
        return d
87
88
89
class InvalidInputYearError(Exception): pass
90
91
92 1
@click.command()
93 1
@click.option('--album-dir', required=True, help="The directory where a music album resides. Currently only mp3 "
94
                                                 "files are supported as contents of the directory. Namely only "
95
                                                 "such files will be apprehended as tracks of the album.")
96 1
@click.option('--track_name/--no-track_name', default=True, show_default=True, help='Whether to extract the track names from the mp3 files and write them as metadata correspondingly.')
97 1
@click.option('--track_number/--no-track_number', default=True, show_default=True, help='Whether to extract the track numbers from the mp3 files and write them as metadata correspondingly.')
98 1
@click.option('--artist', '-a', 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.")
99 1
@click.option('--album_artist', '-aa', 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.")
100 1
@click.option('--album', '-al', 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.")
101 1
@click.option('--year', 'y', 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.")
102
def main(album_dir, track_name, track_number, artist, album_artist, album, year):
103
    md = MetadataDealer()
104
    md.set_album_metadata(album_dir, track_number=track_number, track_name=track_name, artist=artist, album_artist=album_artist, album=album, year=year, verbose=True)
105
106
107 1
if __name__ == '__main__':
108
    main()
109