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