Passed
Push — dev ( ed36f9...ede60a )
by Konstantinos
05:48 queued 58s
created

music_album_creation.metadata   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 106
Duplicated Lines 0 %

Test Coverage

Coverage 87.93%

Importance

Changes 0
Metric Value
wmc 16
eloc 72
dl 0
loc 106
rs 10
c 0
b 0
f 0
ccs 51
cts 58
cp 0.8793

1 Function

Rating   Name   Duplication   Size   Complexity  
A main() 0 13 1

5 Methods

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