music_album_creation.create_album   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 274
Duplicated Lines 0 %

Test Coverage

Coverage 21.26%

Importance

Changes 0
Metric Value
eloc 188
dl 0
loc 274
ccs 27
cts 127
cp 0.2126
rs 10
c 0
b 0
f 0
wmc 24

2 Methods

Rating   Name   Duplication   Size   Complexity  
A TabCompleter.createListCompleter() 0 15 2
A TabCompleter.pathCompleter() 0 4 1

2 Functions

Rating   Name   Duplication   Size   Complexity  
A music_lib_directory() 0 16 5
F main() 0 176 16
1
#!/usr/bin/env python3
2
3 1
import glob
4 1
import logging
5 1
import os
6 1
import shutil
7 1
import sys
8
import time
9 1
from time import sleep
10 1
11
import click
12 1
13 1
from music_album_creation.ffprobe_client import FFProbeClient
14
15 1
from .audio_segmentation import (
16
    AudioSegmenter,
17 1
    SegmentationInformation,
18 1
    TracksInformation,
19
)
20 1
from .audio_segmentation.data import TrackTimestampsSequenceError
21
22 1
# 'front-end', interface, interactive dialogs are imported below
23
from .dialogs import DialogCommander as inout
24
from .downloading import (
25
    InvalidUrlError,
26 1
    TokenParameterNotInVideoInfoError,
27
    UnavailableVideoError,
28
)
29 1
from .ffmpeg import FFProbe
30
from .metadata import MetadataDealer
31
from .music_master import MusicMaster
32
33
ffprobe = FFProbe(os.environ.get('MUSIC_FFPROBE', 'ffprobe'))
34
ffprobe_client = FFProbeClient(ffprobe)
35
36
37
if os.name == 'nt':
38
    from pyreadline import Readline
39
40
    readline = Readline()
41
42
this_dir = os.path.dirname(os.path.realpath(__file__))
43
logger = logging.getLogger(__name__)
44
45 1
46 1
def music_lib_directory(verbose=True):
47
    music_dir = os.getenv('MUSIC_LIB_ROOT', None)
48
    if music_dir is None:
49
        print(
50
            "Please set the environment variable MUSIC_LIB_ROOT to point to a directory that stores music."
51
        )
52 1
        sys.exit(0)
53 1
    if not os.path.isdir(music_dir):
54 1
        try:
55 1
            os.makedirs(music_dir)
56 1
            if verbose:
57
                print("Created directory '{}'".format(music_dir))
58
        except (PermissionError, FileNotFoundError) as e:
59
            print(e)
60
            sys.exit(1)
61
    return music_dir
62
63
64
@click.command()
65
@click.option(
66
    '--tracks_info',
67
    '-t_i',
68
    type=click.File('r'),
69
    help='File in which there is tracks information necessary to segment a music ablum into tracks.'
70
    'If not provided, a prompt will allow you to type the input tracks information.',
71
)
72
@click.option(
73
    '--track_name/--no-track_name',
74
    default=True,
75
    show_default=True,
76
    help='Whether to extract the track names from the mp3 files and write them as metadata correspondingly',
77
)
78
@click.option(
79
    '--track_number/--no-track_number',
80
    default=True,
81
    show_default=True,
82
    help='Whether to extract the track numbers from the mp3 files and write them as metadata correspondingly',
83
)
84
@click.option(
85
    '--artist',
86
    '-a',
87
    help="If given, then value shall be used as the PTE1 tag: 'Lead performer(s)/Soloist(s)'.  In the music player 'clementine' it corresponds to the 'artist' column (and not the 'Album artist column) ",
88
)
89
@click.option(
90
    '--album_artist',
91
    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",
92
)
93
@click.option('--video_url', '-u', help='the youtube video url')
94
def main(tracks_info, track_name, track_number, artist, album_artist, video_url):
95
    music_dir = music_lib_directory(verbose=True)
96
    print("Music library: {}".format(music_dir))
97
    logger.error("Music library: {}".format(str(music_dir)))
98
99
    ## Render Logo
100
    inout.logo()
101
102
    print('\n')
103
    if not video_url:
104
        video_url = inout.input_youtube_url_dialog()
105
        print('\n')
106
    logger.error(f"URL {video_url}")
107
    ## Init
108
    music_master = MusicMaster(music_dir)
109
    # Segments Audio files into tracks and stores them in the system's temp dir (ie /tmp on Debian)
110
    audio_segmenter = AudioSegmenter()
111
112
    ## DOWNLOAD
113
    while 1:
114
        try:
115
            album_file = music_master.url2mp3(
116
                video_url, suppress_certificate_validation=False, force_download=False
117
            )
118
            break
119
        except TokenParameterNotInVideoInfoError as e:
120
            print(e, '\n')
121
            if inout.update_and_retry_dialog()['update-youtube-dl']:
122
                music_master.update_youtube()
123
            else:
124
                print("Exiting ..")
125
                sys.exit(1)
126
        except (InvalidUrlError, UnavailableVideoError) as e:
127
            print(e, '\n')
128
            video_url = inout.input_youtube_url_dialog()
129
            print('\n')
130
    print('\n')
131
132
    print("Album file: {}".format(album_file))
133
134
    ### RECEIVE TRACKS INFORMATION
135
    if tracks_info:
136
        tracks_info = TracksInformation.from_multiline(tracks_info.read().strip())
137
    else:  # Interactive track type input
138
        sleep(0.5)
139
        tracks_info = TracksInformation.from_multiline(
140
            inout.interactive_track_info_input_dialog().strip()
141
        )
142
        print()
143
144
    # Ask user if the input represents song timestamps (withing the whole playtime) OR
145
    # if the input represents song durations (that sum up to the total playtime)
146
    answer = inout.track_information_type_dialog()
147
148
    segmentation_info = SegmentationInformation.from_tracks_information(
149
        tracks_info, hhmmss_type=answer.lower()
150
    )
151
152
    # SEGMENTATION
153
    try:
154
        audio_file_paths = audio_segmenter.segment(
155
            album_file,
156
            segmentation_info,
157
            sleep_seconds=0,
158
        )
159
    except TrackTimestampsSequenceError as e:
160 1
        print(e)
161
        sys.exit(1)
162 1
        # TODO capture ctrl-D to signal possible change of type from timestamp to durations and vice-versa...
163
        # in order to put the above statement outside of while loop
164
165
    # TODO re-implement the below using the ffmpeg proxy
166
    durations = [
167 1
        time.strftime(
168
            '%H:%M:%S',
169
            time.gmtime(int(float(ffprobe_client.get_stream_info(f)['format']['duration']))),
170
        )
171
        for f in audio_file_paths
172
    ]
173
174
    # durations = [StringParser.hhmmss_format(getattr(mutagen.File(t).info, 'length', 0)) for t in audio_file_paths]
175
    max_row_length = max(len(_[0]) + len(_[1]) for _ in zip(audio_file_paths, durations))
176
    print("\n\nThese are the tracks created.\n")
177
    print(
178
        '\n'.join(
179
            sorted(
180
                [
181
                    ' {}{}  {}'.format(t, (max_row_length - len(t) - len(d)) * ' ', d)
182
                    for t, d in zip(audio_file_paths, durations)
183 1
                ]
184
            )
185
        ),
186
        '\n',
187
    )
188
    # TODO
189
190
    ### STORE TRACKS IN DIR in MUSIC LIBRARY ROOT
191
    while 1:
192
        print(type(music_dir), type(music_master.guessed_info))
193
        print(music_dir)
194
        print(music_master.guessed_info)
195
        album_dir = inout.album_directory_path_dialog(music_dir, **music_master.guessed_info)
196
        try:
197
            os.makedirs(album_dir)
198
        except FileExistsError:
199
            if not inout.confirm_copy_tracks_dialog(album_dir):
200
                continue
201
        except FileNotFoundError:
202
            print("The selected destination directory '{}' is not valid.".format(album_dir))
203
            continue
204
        except PermissionError:
205
            print(
206
                "You don't have permision to create a directory in path '{}'".format(album_dir)
207
            )
208
            continue
209
        try:
210
            for track in audio_file_paths:
211
                destination_file_path = os.path.join(album_dir, os.path.basename(track))
212
                if os.path.isfile(destination_file_path):
213
                    print(
214
                        " File '{}' already exists. in '{}'. Skipping".format(
215
                            os.path.basename(track), album_dir
216
                        )
217
                    )
218
                else:
219
                    shutil.copyfile(track, destination_file_path)
220
            print("Album tracks reside in '{}'".format(album_dir))
221
            break
222
        except PermissionError:
223
            print(
224
                "Can't copy tracks to '{}' folder. You don't have write permissions in this directory".format(
225
                    album_dir
226
                )
227
            )
228
229
    ### WRITE METADATA
230
    md = MetadataDealer()
231
    answers = inout.interactive_metadata_dialogs(**music_master.guessed_info)
232
    md.set_album_metadata(
233
        album_dir,
234
        track_number=track_number,
235
        track_name=track_name,
236
        artist=answers['artist'],
237
        album_artist=answers['album-artist'],
238
        album=answers['album'],
239
        year=answers['year'],
240
    )
241
242
243
class TabCompleter:
244
    """A tab completer that can either complete from the filesystem or from a list."""
245
246
    def pathCompleter(self, text, state):
247
        """This is the tab completer for systems paths. Only tested on *nix systems"""
248
        _ = readline.get_line_buffer().split()
0 ignored issues
show
introduced by
The variable readline does not seem to be defined in case os.name == 'nt' on line 37 is False. Are you sure this can never be the case?
Loading history...
249
        return [x for x in glob.glob(text + '*')][state]
250
251
    def createListCompleter(self, ll):
252
        """
253
        This is a closure that creates a method that autocompletes from the given list. Since the autocomplete
254
        function can't be given a list to complete from a closure is used to create the listCompleter function with a
255
        list to complete from.
256
        """
257
258
        def listCompleter(text, state):
259
            line = readline.get_line_buffer()
0 ignored issues
show
introduced by
The variable readline does not seem to be defined in case os.name == 'nt' on line 37 is False. Are you sure this can never be the case?
Loading history...
260
            if not line:
261
                return [c + " " for c in ll][state]
262
            else:
263
                return [c + " " for c in ll if c.startswith(line)][state]
264
265
        self.listCompleter = listCompleter
266
267
268
if __name__ == '__main__':
269
    completer = TabCompleter()
270
    readline.set_completer_delims('\t')
0 ignored issues
show
introduced by
The variable readline does not seem to be defined in case os.name == 'nt' on line 37 is False. Are you sure this can never be the case?
Loading history...
271
    readline.parse_and_bind("tab: complete")
272
    readline.set_completer(completer.pathCompleter)
273
    main()
274