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 audiotools
23import audiotools.ui
24import os.path
25import shutil
26import termios
27import audiotools.text as _
28from audiotools import PY3
29
30
31if audiotools.ui.AVAILABLE:
32    urwid = audiotools.ui.urwid
33
34    class Trackrename(urwid.Pile):
35        def __init__(self, audio_class, format_string,
36                     input_filenames, metadatas):
37            """audio_class is an AudioFile class
38            format_string is a UTF-8 encoded plain string
39            input_filenames is a list of Filename objects
40            metadata is a list of MetaData objects, or Nones"""
41
42            assert(isinstance(format_string, str))
43            assert(len(input_filenames) == len(metadatas))
44            for filename in input_filenames:
45                assert(isinstance(filename, audiotools.Filename))
46
47            self.__cancelled__ = True
48
49            self.input_filenames = input_filenames
50
51            # setup a previous/finish button set
52            metadata_buttons = urwid.Filler(
53                urwid.Columns(
54                    widget_list=[('weight', 1,
55                                  urwid.Button(_.LAB_CANCEL_BUTTON,
56                                               on_press=self.exit)),
57                                 ('weight', 2,
58                                  urwid.Button(_.LAB_TRACKRENAME_RENAME,
59                                               on_press=self.apply))],
60                    dividechars=3,
61                    focus_column=1))
62
63            # setup a widget with an output preview
64            self.track_previews = [urwid.Text(u"") for m in metadatas]
65            self.output_tracks_list = urwid.ListBox(
66                self.track_previews)
67            self.output_tracks_frame = urwid.Frame(
68                body=self.output_tracks_list)
69            self.invalid_output_format = urwid.Filler(
70                urwid.Text(_.ERR_INVALID_FILENAME_FORMAT,
71                           align="center"))
72
73            # setup a widget with the rename template
74            output_format = urwid.Edit(
75                edit_text=(format_string if PY3 else
76                           format_string.decode('utf-8')),
77                wrap='clip')
78            urwid.connect_signal(output_format,
79                                 'change',
80                                 self.format_changed,
81                                 (audio_class,
82                                  input_filenames,
83                                  metadatas))
84
85            browse_fields = audiotools.ui.BrowseFields(output_format)
86            template_row = urwid.Columns(
87                [('fixed', 10,
88                  urwid.Text(('label',
89                              u"%s : " %
90                              (_.LAB_OPTIONS_FILENAME_FORMAT)),
91                             align="right")),
92                 ('weight', 1, output_format),
93                 ('fixed', 10, browse_fields)])
94
95            # perform initial data population
96            self.format_changed(output_format,
97                                format_string,
98                                (audio_class,
99                                 input_filenames,
100                                 metadatas))
101
102            urwid.Pile.__init__(
103                self,
104                [('fixed', 3, urwid.LineBox(urwid.Filler(template_row))),
105                 ('weight', 1,
106                  urwid.LineBox(self.output_tracks_frame,
107                                title=_.LAB_OPTIONS_OUTPUT_FILES)),
108                 ('fixed', 1, metadata_buttons)])
109
110        def apply(self, button):
111            if not self.has_errors:
112                self.__cancelled__ = False
113                raise urwid.ExitMainLoop()
114
115        def exit(self, button):
116            self.__cancelled__ = True
117            raise urwid.ExitMainLoop()
118
119        def cancelled(self):
120            return self.__cancelled__
121
122        def handle_text(self, i):
123            if i == 'esc':
124                self.exit(None)
125
126        def format_changed(self, widget, new_value, user_data):
127            (output_class,
128             input_filenames,
129             metadatas) = user_data
130
131            try:
132                # generate list of Filename objects
133                # from paths, metadatas and format
134                format_string = new_value if PY3 else new_value.encode("UTF-8")
135
136                self.output_filenames = [
137                    audiotools.Filename(
138                        output_class.track_name(
139                            file_path=str(filename),
140                            track_metadata=metadata,
141                            format=format_string))
142                    for (filename,
143                         metadata) in zip(input_filenames, metadatas)]
144
145                # and populate output files list
146                for (path, preview) in zip(self.output_filenames,
147                                           self.track_previews):
148                    preview.set_text(path.__unicode__())
149
150                if ((self.output_tracks_frame.get_body() is not
151                     self.output_tracks_list)):
152                    self.output_tracks_frame.set_body(
153                        self.output_tracks_list)
154                self.has_errors = False
155            except (audiotools.UnsupportedTracknameField,
156                    audiotools.InvalidFilenameFormat):
157                # invalid filename string
158                if ((self.output_tracks_frame.get_body() is not
159                     self.invalid_output_format)):
160                    self.output_tracks_frame.set_body(
161                        self.invalid_output_format)
162                self.has_errors = True
163
164        def to_rename(self):
165            """yields (old_name, new_name) tuples
166            where old_name and new_name differ
167            where the names are Filename objects"""
168
169            if not self.has_errors:
170                for (old_name, new_name) in zip(self.input_filenames,
171                                                self.output_filenames):
172                    if old_name != new_name:
173                        yield (old_name, new_name)
174
175
176if (__name__ == '__main__'):
177    import argparse
178
179    parser = argparse.ArgumentParser(description=_.DESCRIPTION_TRACKRENAME)
180
181    parser.add_argument("--version",
182                        action="version",
183                        version="Python Audio Tools %s" % (audiotools.VERSION))
184
185    parser.add_argument("-I", "--interactive",
186                        action="store_true",
187                        default=False,
188                        dest="interactive",
189                        help=_.OPT_INTERACTIVE_OPTIONS)
190
191    parser.add_argument("-V", "--verbose",
192                        dest="verbosity",
193                        choices=audiotools.VERBOSITY_LEVELS,
194                        default=audiotools.DEFAULT_VERBOSITY,
195                        help=_.OPT_VERBOSE)
196
197    parser.add_argument('--format',
198                        default=audiotools.FILENAME_FORMAT,
199                        dest='format',
200                        help=_.OPT_FORMAT)
201
202    parser.add_argument("filenames",
203                        metavar="FILENAME",
204                        nargs="+",
205                        help=_.OPT_INPUT_FILENAME)
206
207    options = parser.parse_args()
208    msg = audiotools.Messenger(options.verbosity == "quiet")
209
210    # ensure interactive mode is available, if selected
211    if options.interactive and (not audiotools.ui.AVAILABLE):
212        audiotools.ui.not_available_message(msg)
213        sys.exit(1)
214
215    try:
216        audiofiles = audiotools.open_files(options.filenames,
217                                           messenger=msg,
218                                           no_duplicates=True)
219    except audiotools.DuplicateFile as err:
220        msg.error(_.ERR_DUPLICATE_FILE % (err.filename,))
221        sys.exit(1)
222
223    if len(audiofiles) < 1:
224        msg.error(_.ERR_FILES_REQUIRED)
225        sys.exit(1)
226
227    # get a set of files to be renamed
228    # and generate an error if a duplicate occurs
229    renamed_filenames = {audiotools.Filename(t.filename) for t in audiofiles}
230
231    if options.interactive:
232        widget = Trackrename(
233            audio_class=audiofiles[0].__class__,
234            format_string=options.format,
235            input_filenames=[audiotools.Filename(t.filename) for t in
236                             audiofiles],
237            metadatas=[t.get_metadata() for t in audiofiles])
238        loop = audiotools.ui.urwid.MainLoop(
239            widget,
240            audiotools.ui.style(),
241            unhandled_input=widget.handle_text,
242            pop_ups=True)
243
244        try:
245            loop.run()
246            msg.ansi_clearscreen()
247        except (termios.error, IOError):
248            msg.error(_.ERR_TERMIOS_ERROR)
249            msg.info(_.ERR_TERMIOS_SUGGESTION)
250            msg.info(audiotools.ui.xargs_suggestion(sys.argv))
251            sys.exit(1)
252
253        if not widget.cancelled():
254            to_rename = list(widget.to_rename())
255        else:
256            sys.exit(0)
257    else:
258        to_rename = []  # a (old_name, new_name) tuple
259        try:
260            for track in audiofiles:
261                original_filename = audiotools.Filename(track.filename)
262                new_filename = audiotools.Filename(
263                    os.path.join(
264                        os.path.dirname(track.filename),
265                        track.track_name(file_path=track.filename,
266                                         track_metadata=track.get_metadata(),
267                                         format=options.format)))
268                if new_filename != original_filename:
269                    if new_filename not in renamed_filenames:
270                        renamed_filenames.add(new_filename)
271                        to_rename.append((original_filename, new_filename))
272                    else:
273                        msg.error(_.ERR_DUPLICATE_OUTPUT_FILE %
274                                  (new_filename,))
275                        sys.exit(1)
276        except audiotools.UnsupportedTracknameField as err:
277            err.error_msg(msg)
278            sys.exit(1)
279        except audiotools.InvalidFilenameFormat as err:
280            msg.error(err)
281            sys.exit(1)
282
283    # create subdirectories for renamed files if necessary
284    for (original_filename, new_filename) in to_rename:
285        try:
286            audiotools.make_dirs(str(new_filename))
287        except OSError as err:
288            msg.os_error(err)
289            sys.exit(1)
290
291    # perform the actual renaming itself
292    for (i, (original_filename, new_filename)) in enumerate(to_rename):
293        try:
294            shutil.move(str(original_filename), str(new_filename))
295            msg.info(
296                audiotools.output_progress(
297                    _.LAB_ENCODE % {"source": original_filename,
298                                    "destination": new_filename},
299                    i + 1, len(to_rename)))
300        except IOError as err:
301            msg.error(_.ERR_RENAME %
302                      {"source": original_filename,
303                       "target": new_filename})
304            sys.exit(1)
305