1#!/usr/bin/python
2
3# Audio Tools, a module and set of tools for manipulating audio data
4# Copyright (C) 2007-2014  Brian Langenberger
5
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20
21import sys
22import os
23import os.path
24import select
25from operator import concat
26import audiotools
27import audiotools.ui
28import audiotools.text as _
29import termios
30
31
32MAX_CPUS = audiotools.MAX_JOBS
33
34
35def convert(progress,
36            source_audiofile,
37            destination_filename,
38            destination_class,
39            compression,
40            metadata,
41            replay_gain,
42            sample_rate,
43            channels,
44            bits_per_sample):
45    try:
46        if (((sample_rate is None) and
47             (channels is None) and
48             (bits_per_sample is None))):
49            destination_audiofile = source_audiofile.convert(
50                destination_filename,
51                destination_class,
52                compression,
53                progress)
54        else:
55            pcmreader = source_audiofile.to_pcm()
56            destination_audiofile = destination_class.from_pcm(
57                destination_filename,
58                audiotools.PCMConverter(
59                    audiotools.PCMReaderProgress(
60                        pcmreader,
61                        source_audiofile.total_frames(),
62                        progress),
63                    sample_rate if
64                    (sample_rate is not None) else
65                    pcmreader.sample_rate,
66                    channels if
67                    (channels is not None) else
68                    pcmreader.channels,
69                    0 if
70                    (channels is not None) else
71                    pcmreader.channel_mask,
72                    bits_per_sample if (bits_per_sample is not None) else
73                    pcmreader.bits_per_sample),
74                compression,
75                source_audiofile.total_frames() if
76                (source_audiofile.lossless() and (sample_rate is None))
77                else None)
78
79        if metadata is not None:
80            destination_audiofile.set_metadata(metadata)
81
82        if replay_gain is not None:
83            destination_audiofile.set_replay_gain(replay_gain)
84
85        existing_cuesheet = source_audiofile.get_cuesheet()
86        if existing_cuesheet is not None:
87            destination_audiofile.set_cuesheet(existing_cuesheet)
88    except KeyboardInterrupt:
89        # delete partially-encoded file
90        try:
91            os.unlink(destination_filename)
92        except OSError:
93            pass
94
95    return destination_filename
96
97
98def __add_replay_gain__(tracks, progress=None):
99    """a wrapper around add_replay_gain that catches KeyboardInterrupt"""
100
101    try:
102        audiotools.add_replay_gain(tracks=tracks, progress=progress)
103    except KeyboardInterrupt:
104        pass
105
106
107if audiotools.ui.AVAILABLE:
108    urwid = audiotools.ui.urwid
109
110    class MultiOutputFiller(audiotools.ui.OutputFiller):
111        def __init__(self,
112                     track_labels,
113                     metadata_choices,
114                     input_filenames,
115                     output_directory,
116                     format_string,
117                     output_class,
118                     quality,
119                     completion_label=u"Apply"):
120            """track_labels[a][t]
121            is a unicode string per album "a", per track "t"
122
123            metadata_choices[a][c][t]
124            is a MetaData object per album "a", per choice "c", per track "t"
125            must have same album count as track_labels
126            and each album's choice must have the same number of tracks
127            (but may have different number of choices)
128
129            input_filenames[a][t]
130            is a Filename object per album "a", per track "t"
131
132            output_directory is a string of the default output dir
133
134            format_string is a UTF-8 encoded format string
135
136            output_class is the default AudioFile-compatible class
137
138            quality is a string of the default output quality to use
139            """
140
141            from functools import reduce
142
143            self.__cancelled__ = True
144
145            # ensure there's at least one album
146            assert(len(track_labels) > 0)
147
148            # ensure album count is consistent
149            assert(len(track_labels) ==
150                   len(metadata_choices) ==
151                   len(input_filenames))
152
153            # ensure track count is consistent
154            for (labels, filenames) in zip(track_labels, input_filenames):
155                assert(len(labels) == len(filenames))
156
157            # ensure there's at least one track
158            assert(len(track_labels[0]) > 0)
159
160            # ensure there's at least one set of choices per album
161            # and all have the same number of tracks
162            for (a, choice_labels) in zip(metadata_choices, track_labels):
163                assert(len(a) > 0)
164                for c in a:
165                    assert(len(c) == len(choice_labels))
166
167            # ensure all input filenames are Filename objects
168            for a in input_filenames:
169                for f in a:
170                    assert(isinstance(f, audiotools.Filename))
171
172            # setup status bars for output messages
173            self.metadatas_status = [urwid.Text(u"") for m in track_labels]
174            self.options_status = urwid.Text(u"")
175
176            # setup widgets for populated metadata fields
177            self.metadatas = [
178                audiotools.ui.MetaDataFiller(labels, choices, status)
179                for (labels, choices, status) in zip(track_labels,
180                                                     metadata_choices,
181                                                     self.metadatas_status)]
182
183            # setup a widget for populating output parameters
184            self.options = audiotools.ui.OutputOptions(
185                output_dir=output_directory,
186                format_string=format_string,
187                audio_class=output_class,
188                quality=quality,
189                input_filenames=reduce(concat, input_filenames),
190                metadatas=[None for f in reduce(concat, input_filenames)])
191
192            # finish initialization
193            from audiotools.text import LAB_CANCEL_BUTTON
194
195            self.wizard = audiotools.ui.Wizard(
196                self.metadatas + [self.options],
197                urwid.Button(LAB_CANCEL_BUTTON, on_press=self.exit),
198                urwid.Button(completion_label, on_press=self.complete),
199                self.page_changed)
200
201            self.__current_page__ = self.metadatas[0]
202
203            urwid.Frame.__init__(self,
204                                 body=self.wizard,
205                                 footer=self.metadatas_status[0])
206
207        def page_changed(self, new_page):
208            self.__current_page__ = new_page
209            if hasattr(new_page, "status"):
210                # one of the metadata pages is selected
211                self.set_footer(new_page.status)
212            else:
213                from functools import reduce
214
215                # the final options page is selected
216                self.options.set_metadatas(
217                    reduce(concat, [list(m.populated_metadata())
218                                    for m in self.metadatas]))
219                self.set_footer(self.options_status)
220
221        def handle_text(self, i):
222            if ((i == "f1") and hasattr(self.__current_page__,
223                                        "select_previous_item")):
224                self.__current_page__.select_previous_item()
225            elif ((i == "f2") and (hasattr(self.__current_page__,
226                                           "select_next_item"))):
227                self.__current_page__.select_next_item()
228
229        def output_albums(self):
230            """for each album
231               yields (output_class,
232                       output_filename,
233                       output_quality,
234                       output_metadata) tuple for each input audio file
235
236            as a nested iterator
237
238            output_metadata is a newly created MetaData object"""
239
240            (audiofile_class,
241             quality,
242             output_filenames) = self.options.selected_options()
243
244            for widget in self.metadatas:
245                album = []
246                for metadata in widget.populated_metadata():
247                    album.append((audiofile_class,
248                                  output_filenames.pop(0),
249                                  quality,
250                                  metadata))
251                yield album
252
253
254if (__name__ == '__main__'):
255    import argparse
256
257    parser = argparse.ArgumentParser(description=_.DESCRIPTION_TRACK2TRACK)
258
259    parser.add_argument("--version",
260                        action="version",
261                        version="Python Audio Tools %s" % (audiotools.VERSION))
262
263    parser.add_argument("-I", "--interactive",
264                        action="store_true",
265                        default=False,
266                        dest="interactive",
267                        help=_.OPT_INTERACTIVE_OPTIONS)
268
269    parser.add_argument("-V", "--verbose",
270                        dest="verbosity",
271                        choices=audiotools.VERBOSITY_LEVELS,
272                        default=audiotools.DEFAULT_VERBOSITY,
273                        help=_.OPT_VERBOSE)
274
275    conversion = parser.add_argument_group(_.OPT_CAT_CONVERSION)
276
277    conversion.add_argument("-t", "--type",
278                            dest="type",
279                            choices=sorted(list(audiotools.TYPE_MAP.keys()) +
280                                           ["help"]),
281                            help=_.OPT_TYPE)
282
283    conversion.add_argument("-q", "--quality",
284                            dest="quality",
285                            help=_.OPT_QUALITY)
286
287    conversion.add_argument("-d", "--dir",
288                            dest="dir",
289                            default=".",
290                            help=_.OPT_DIR)
291
292    conversion.add_argument("--format",
293                            default=None,
294                            dest="format",
295                            help=_.OPT_FORMAT)
296
297    conversion.add_argument("-o", "--output",
298                            dest="output",
299                            help=_.OPT_OUTPUT_TRACK2TRACK)
300
301    conversion.add_argument("-j", "--joint",
302                            type=int,
303                            default=MAX_CPUS,
304                            dest="max_processes",
305                            help=_.OPT_JOINT)
306
307    format = parser.add_argument_group(_.OPT_CAT_OUTPUT_FORMAT)
308
309    format.add_argument("--sample-rate",
310                        type=int,
311                        dest="sample_rate",
312                        metavar="RATE",
313                        help=_.OPT_SAMPLE_RATE)
314
315    format.add_argument("--channels",
316                        type=int,
317                        dest="channels",
318                        help=_.OPT_CHANNELS)
319
320    format.add_argument("--bits-per-sample",
321                        type=int,
322                        dest="bits_per_sample",
323                        metavar="BITS",
324                        help=_.OPT_BPS)
325
326    lookup = parser.add_argument_group(_.OPT_CAT_CD_LOOKUP)
327
328    lookup.add_argument("-M", "--metadata-lookup",
329                        action="store_true",
330                        default=False,
331                        dest="metadata_lookup",
332                        help=_.OPT_METADATA_LOOKUP)
333
334    lookup.add_argument("--musicbrainz-server",
335                        dest="musicbrainz_server",
336                        default=audiotools.MUSICBRAINZ_SERVER,
337                        metavar="HOSTNAME")
338
339    lookup.add_argument("--musicbrainz-port",
340                        type=int,
341                        dest="musicbrainz_port",
342                        default=audiotools.MUSICBRAINZ_PORT,
343                        metavar="PORT")
344
345    lookup.add_argument("--no-musicbrainz",
346                        action="store_false",
347                        dest="use_musicbrainz",
348                        default=audiotools.MUSICBRAINZ_SERVICE,
349                        help=_.OPT_NO_MUSICBRAINZ)
350
351    lookup.add_argument("--freedb-server",
352                        dest="freedb_server",
353                        default=audiotools.FREEDB_SERVER,
354                        metavar="HOSTNAME")
355
356    lookup.add_argument("--freedb-port",
357                        type=int,
358                        dest="freedb_port",
359                        default=audiotools.FREEDB_PORT,
360                        metavar="PORT")
361
362    lookup.add_argument("--no-freedb",
363                        action="store_false",
364                        dest="use_freedb",
365                        default=audiotools.FREEDB_SERVICE,
366                        help=_.OPT_NO_FREEDB)
367
368    lookup.add_argument("-D", "--default",
369                        dest="use_default",
370                        action="store_true",
371                        default=False,
372                        help=_.OPT_DEFAULT)
373
374    metadata = parser.add_argument_group(_.OPT_CAT_METADATA)
375
376    metadata.add_argument("--replay-gain",
377                          action="store_true",
378                          default=None,
379                          dest="add_replay_gain",
380                          help=_.OPT_REPLAY_GAIN)
381
382    metadata.add_argument("--no-replay-gain",
383                          action="store_false",
384                          default=None,
385                          dest="add_replay_gain",
386                          help=_.OPT_NO_REPLAY_GAIN)
387
388    parser.add_argument("filenames",
389                        metavar="FILENAME",
390                        nargs="+",
391                        help=_.OPT_INPUT_FILENAME)
392
393    options = parser.parse_args()
394
395    msg = audiotools.Messenger(options.verbosity == "quiet")
396
397    # ensure interactive mode is available, if selected
398    if options.interactive and (not audiotools.ui.AVAILABLE):
399        audiotools.ui.not_available_message(msg)
400        sys.exit(1)
401
402    # if one specifies incompatible output options,
403    # complain about it right away
404    if options.output is not None:
405        if options.dir != ".":
406            msg.error(_.ERR_TRACK2TRACK_O_AND_D)
407            msg.info(_.ERR_TRACK2TRACK_O_AND_D_SUGGESTION)
408            sys.exit(1)
409
410        if options.format is not None:
411            msg.warning(_.ERR_TRACK2TRACK_O_AND_FORMAT)
412
413    # get the AudioFile class we are converted to
414    if options.type == 'help':
415        audiotools.ui.show_available_formats(msg)
416        sys.exit(0)
417    elif options.output is None:
418        if options.type is not None:
419            AudioType = audiotools.TYPE_MAP[options.type]
420        else:
421            AudioType = audiotools.TYPE_MAP[audiotools.DEFAULT_TYPE]
422    else:
423        if options.type is not None:
424            AudioType = audiotools.TYPE_MAP[options.type]
425        else:
426            try:
427                AudioType = audiotools.filename_to_type(options.output)
428            except audiotools.UnknownAudioType as exp:
429                exp.error_msg(msg)
430                sys.exit(1)
431
432    # whether to add ReplayGain to newly converted files
433    if AudioType.supports_replay_gain():
434        add_replay_gain = ((options.add_replay_gain if
435                            (options.add_replay_gain is not None) else
436                           audiotools.ADD_REPLAYGAIN))
437    else:
438        add_replay_gain = False
439
440    # ensure the selected compression is compatible with that class
441    if options.quality == 'help':
442        audiotools.ui.show_available_qualities(msg, AudioType)
443        sys.exit(0)
444    elif options.quality is None:
445        options.quality = audiotools.__default_quality__(AudioType.NAME)
446    elif options.quality not in AudioType.COMPRESSION_MODES:
447        msg.error(_.ERR_UNSUPPORTED_COMPRESSION_MODE %
448                  {"quality": options.quality,
449                   "type": AudioType.NAME})
450        sys.exit(1)
451
452    # grab the list of AudioFile objects we are converting from
453    input_filenames = set()
454    try:
455        audiofiles = audiotools.open_files(options.filenames,
456                                           messenger=msg,
457                                           no_duplicates=True,
458                                           opened_files=input_filenames)
459    except audiotools.DuplicateFile as err:
460        msg.error(_.ERR_DUPLICATE_FILE % (err.filename,))
461        sys.exit(1)
462
463    if len(audiofiles) < 1:
464        msg.error(_.ERR_FILES_REQUIRED)
465        sys.exit(1)
466
467    if (options.sample_rate is not None) and (options.sample_rate < 1):
468        msg.error(_.ERR_INVALID_SAMPLE_RATE)
469        sys.exit(1)
470
471    if (options.channels is not None) and (options.channels < 1):
472        msg.error(_.ERR_INVALID_CHANNEL_COUNT)
473        sys.exit(1)
474
475    if (((options.bits_per_sample is not None) and
476         (options.bits_per_sample < 1))):
477        msg.error(_.ERR_INVALID_BITS_PER_SAMPLE)
478        sys.exit(1)
479
480    if options.max_processes < 1:
481        msg.error(_.ERR_INVALID_JOINT)
482        sys.exit(1)
483
484    if (options.output is not None) and (len(audiofiles) != 1):
485        msg.error(_.ERR_TRACK2TRACK_O_AND_MULTIPLE)
486        sys.exit(1)
487
488    if options.output is None:
489        # the default encoding method, without an output file
490
491        queue = audiotools.ExecProgressQueue(msg)
492
493        # split tracks by album
494        albums_iter = audiotools.group_tracks(audiofiles)
495
496        # input_tracks[a][t] is an AudioFile object
497        # per album "a", per track "t"
498        input_tracks = []
499
500        # input_metadatas[a][t] is a MetaData object (or None)
501        # per album "a", per track "t"
502        input_metadatas = []
503
504        # track_labels[a][t] is a unicode string per album "a", per track "t"
505        track_labels = []
506
507        # metadata_choices[a][c][t] is a MetaData object
508        # per album "a", per choice "c", per track "t"
509        metadata_choices = []
510
511        # input_filenames[a][t] is a Filename object
512        # per album "a", per track "t"
513        input_filenames = []
514
515        for album_tracks in albums_iter:
516            input_tracks.append(album_tracks)
517
518            track_metadatas = [f.get_metadata() for f in album_tracks]
519            input_metadatas.append(track_metadatas)
520
521            filenames = [audiotools.Filename(f.filename)
522                         for f in album_tracks]
523            track_labels.append([f.basename().__unicode__()
524                                 for f in filenames])
525            input_filenames.append(filenames)
526
527            if not options.metadata_lookup:
528                # pull metadata from existing files, if any
529                metadata_choices.append([[f.get_metadata() for f in
530                                          album_tracks]])
531            else:
532                # perform CD lookup for existing files
533                try:
534                    metadata_choices.append(audiotools.track_metadata_lookup(
535                        audiofiles=album_tracks,
536                        musicbrainz_server=options.musicbrainz_server,
537                        musicbrainz_port=options.musicbrainz_port,
538                        freedb_server=options.freedb_server,
539                        freedb_port=options.freedb_port,
540                        use_musicbrainz=options.use_musicbrainz,
541                        use_freedb=options.use_freedb))
542                except KeyboardInterrupt:
543                    msg.ansi_clearline()
544                    msg.error(_.ERR_CANCELLED)
545                    sys.exit(1)
546
547                # and prepend metadata from existing files as an option, if any
548                if track_metadatas != [None] * len(track_metadatas):
549                    metadata_choices[-1].insert(
550                        0,
551                        [(m if m is not None else audiotools.MetaData())
552                         for m in track_metadatas])
553
554                # avoid performing too many lookups in a row too quickly
555                from time import sleep
556                sleep(1)
557
558        # a list of (audiofile,
559        #           output_class,
560        #           output_filename,
561        #           output_quality,
562        #           output_metadata,
563        #           output_replay_gain) tuples to be executed
564        conversion_jobs = []
565
566        # a list of [audiofile, audiofile, ...] lists
567        # each containing an album of tracks to add ReplayGain to
568        # once conversion is finished
569        replaygain_jobs = []
570
571        if options.interactive:
572            # pick options using interactive widget
573
574            output_widget = MultiOutputFiller(
575                track_labels=track_labels,
576                metadata_choices=metadata_choices,
577                input_filenames=input_filenames,
578                output_directory=options.dir,
579                format_string=(options.format if
580                               (options.format is not None) else
581                               audiotools.FILENAME_FORMAT),
582                output_class=AudioType,
583                quality=options.quality,
584                completion_label=(_.LAB_TRACK2TRACK_APPLY if
585                                  (sum(map(len, input_tracks)) != 1)
586                                  else _.LAB_TRACK2TRACK_APPLY_1))
587
588            loop = audiotools.ui.urwid.MainLoop(
589                output_widget,
590                audiotools.ui.style(),
591                unhandled_input=output_widget.handle_text,
592                pop_ups=True)
593            try:
594                loop.run()
595                msg.ansi_clearscreen()
596            except (termios.error, IOError):
597                msg.error(_.ERR_TERMIOS_ERROR)
598                msg.info(_.ERR_TERMIOS_SUGGESTION)
599                msg.info(audiotools.ui.xargs_suggestion(sys.argv))
600                sys.exit(1)
601
602            if output_widget.cancelled():
603                sys.exit(0)
604
605            for (album_tracks,
606                 album_metadatas,
607                 album_output) in zip(input_tracks,
608                                      input_metadatas,
609                                      output_widget.output_albums()):
610
611                # use existing ReplayGain values if we're to add it
612                # and *all* input tracks have it
613                if add_replay_gain:
614                    album_track_gains = [t.get_replay_gain() for t in
615                                         album_tracks]
616                    if None in album_track_gains:
617                        album_track_gains = [None for t in album_tracks]
618                        replaygain_jobs.append(
619                            [str(o[1]) for o in album_output])
620                else:
621                    album_track_gains = [None for t in album_tracks]
622
623                for (input_track,
624                     current_metadata,
625                     (output_class,
626                      output_filename,
627                      output_quality,
628                      output_metadata),
629                     output_replay_gain) in zip(album_tracks,
630                                                album_metadatas,
631                                                album_output,
632                                                album_track_gains):
633                    # merge current track metadata (if any)
634                    # with metadata returned from widget
635                    if current_metadata is not None:
636                        for attr in audiotools.MetaData.FIELDS:
637                            original_value = getattr(current_metadata, attr)
638                            updated_value = getattr(output_metadata, attr)
639                            if original_value != updated_value:
640                                setattr(current_metadata, attr, updated_value)
641                        # and queue up conversion job to be executed
642                        conversion_jobs.append((input_track,
643                                                output_class,
644                                                output_filename,
645                                                output_quality,
646                                                current_metadata,
647                                                output_replay_gain))
648                    else:
649                        # or simply queue up conversion job to be executed
650                        # using only new metadata
651                        conversion_jobs.append((input_track,
652                                                output_class,
653                                                output_filename,
654                                                output_quality,
655                                                output_metadata,
656                                                output_replay_gain))
657
658        else:
659            # pick options without using GUI
660            for (album_tracks,
661                 album_metadata_choices,
662                 album_filenames) in zip(input_tracks,
663                                         metadata_choices,
664                                         input_filenames):
665                try:
666                    output_tracks = list(audiotools.ui.process_output_options(
667                        metadata_choices=album_metadata_choices,
668                        input_filenames=album_filenames,
669                        output_directory=options.dir,
670                        format_string=options.format,
671                        output_class=AudioType,
672                        quality=options.quality,
673                        msg=msg,
674                        use_default=options.use_default))
675
676                    # use existing ReplayGain values if we're to add it
677                    # and *all* input tracks have it
678                    if add_replay_gain:
679                        album_track_gains = [t.get_replay_gain() for t in
680                                             album_tracks]
681                        if None in album_track_gains:
682                            album_track_gains = [None for t in album_tracks]
683                            replaygain_jobs.append(
684                                [str(o[1]) for o in output_tracks])
685                    else:
686                        album_track_gains = [None for t in album_tracks]
687
688                    # queue jobs to be executed
689                    for (album_track,
690                         (output_class,
691                          output_filename,
692                          output_quality,
693                          output_metadata),
694                         output_replay_gain) in zip(album_tracks,
695                                                    output_tracks,
696                                                    album_track_gains):
697                        conversion_jobs.append((album_track,
698                                                output_class,
699                                                output_filename,
700                                                output_quality,
701                                                output_metadata,
702                                                output_replay_gain))
703                except audiotools.UnsupportedTracknameField as err:
704                    err.error_msg(msg)
705                    sys.exit(1)
706                except (audiotools.InvalidFilenameFormat,
707                        audiotools.OutputFileIsInput,
708                        audiotools.DuplicateOutputFile) as err:
709                    msg.error(err)
710                    sys.exit(1)
711
712        # re-check that the same output file doesn't occur more than once
713        # (although process_output_options also performs that check,
714        # it's possible processing multiple albums at once
715        # may result in the same output file occurring across the whole
716        # job, but not in any individual album)
717        output_filenames = set()
718        for job in conversion_jobs:
719            output_filename = job[2]
720            if output_filename not in output_filenames:
721                output_filenames.add(output_filename)
722            else:
723                msg.error(_.ERR_DUPLICATE_OUTPUT_FILE % (output_filename,))
724                sys.exit(1)
725
726        # queue conversion jobs to ProgressQueue
727        for (audiofile,
728             output_class,
729             output_filename,
730             output_quality,
731             output_metadata,
732             output_replay_gain) in conversion_jobs:
733            # try to create subdirectories in advance
734            # so to bail out early if there's an error creating one
735            try:
736                audiotools.make_dirs(str(output_filename))
737            except OSError:
738                msg.error(_.ERR_ENCODING_ERROR % (output_filename,))
739                sys.exit(1)
740
741            queue.execute(
742                function=convert,
743                progress_text=output_filename.__unicode__(),
744                completion_output=(_.LAB_ENCODE % {
745                    "source": audiotools.Filename(audiofile.filename),
746                    "destination": output_filename}),
747                source_audiofile=audiofile,
748                destination_filename=str(output_filename),
749                destination_class=output_class,
750                compression=output_quality,
751                metadata=output_metadata,
752                replay_gain=output_replay_gain,
753                sample_rate=options.sample_rate,
754                channels=options.channels,
755                bits_per_sample=options.bits_per_sample)
756
757        # perform actual track conversion
758        try:
759            output_files = audiotools.open_files(
760                queue.run(options.max_processes))
761        except audiotools.EncodingError as err:
762            msg.error(err)
763            sys.exit(1)
764        except KeyboardInterrupt:
765            msg.error(_.ERR_CANCELLED)
766            sys.exit(1)
767
768        # add ReplayGain to converted files, if necessary
769
770        # separate encoded files by album_name and album_number
771        for album in [[audiotools.open(f) for f in fs]
772                      for fs in replaygain_jobs]:
773            # add ReplayGain to groups of files
774            # belonging to the same album
775
776            album_number = {(m.album_number if m is not None else None)
777                            for m in
778                            [f.get_metadata() for f in album]}.pop()
779
780            if album_number is None:
781                progress_text = _.RG_ADDING_REPLAYGAIN
782                completion_output = _.RG_REPLAYGAIN_ADDED
783            else:
784                progress_text = _.RG_ADDING_REPLAYGAIN_TO_ALBUM % \
785                    (album_number)
786                completion_output = _.RG_REPLAYGAIN_ADDED_TO_ALBUM % \
787                    (album_number)
788
789            queue.execute(function=__add_replay_gain__,
790                          progress_text=progress_text,
791                          completion_output=completion_output,
792                          tracks=album)
793
794        try:
795            queue.run(options.max_processes)
796        except ValueError as err:
797            msg.error(err)
798            sys.exit(1)
799        except KeyboardInterrupt:
800            msg.error(_.ERR_CANCELLED)
801            sys.exit(1)
802    else:
803        # encoding only a single file
804        audiofile = audiofiles[0]
805        input_filename = audiotools.Filename(audiofile.filename)
806
807        if options.interactive:
808            track_metadata = audiofile.get_metadata()
809
810            output_widget = audiotools.ui.SingleOutputFiller(
811                track_label=input_filename.__unicode__(),
812                metadata_choices=[track_metadata if track_metadata is not None
813                                  else audiotools.MetaData()],
814                input_filenames=[input_filename],
815                output_file=options.output,
816                output_class=AudioType,
817                quality=options.quality,
818                completion_label=_.LAB_TRACK2TRACK_APPLY_1)
819            loop = audiotools.ui.urwid.MainLoop(
820                output_widget,
821                audiotools.ui.style(),
822                unhandled_input=output_widget.handle_text,
823                pop_ups=True)
824            loop.run()
825            msg.ansi_clearscreen()
826
827            if not output_widget.cancelled():
828                (destination_class,
829                 output_filename,
830                 compression,
831                 track_metadata) = output_widget.output_track()
832            else:
833                sys.exit(0)
834        else:
835            output_filename = audiotools.Filename(options.output)
836            destination_class = AudioType
837            compression = options.quality
838            track_metadata = audiofile.get_metadata()
839
840            if input_filename == output_filename:
841                msg.error(_.ERR_OUTPUT_IS_INPUT %
842                          (output_filename,))
843                sys.exit(1)
844
845        progress = audiotools.SingleProgressDisplay(
846            messenger=msg,
847            progress_text=output_filename.__unicode__())
848        try:
849            convert(progress=progress.update,
850                    source_audiofile=audiofile,
851                    destination_filename=str(output_filename),
852                    destination_class=destination_class,
853                    compression=compression,
854                    metadata=track_metadata,
855                    replay_gain=(audiofile.get_replay_gain() if
856                                 add_replay_gain else None),
857                    sample_rate=options.sample_rate,
858                    channels=options.channels,
859                    bits_per_sample=options.bits_per_sample)
860            progress.clear_rows()
861
862            msg.output(_.LAB_ENCODE % {"source": input_filename,
863                                       "destination": output_filename})
864        except audiotools.EncodingError as err:
865            progress.clear_rows()
866            msg.error(err)
867            sys.exit(1)
868        except KeyboardInterrupt:
869            progress.clear_rows()
870            try:
871                os.unlink(str(output_filename))
872            except OSError:
873                pass
874            msg.error(_.ERR_CANCELLED)
875            sys.exit(1)
876