Make things platform-independent
[ordnung.git] / ordnung.py
blob8e2561e2a6ba7fd0a377195903c693de763a3571
1 # Copyright 2009, Erik Hahn
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 import warnings
17 warnings.filterwarnings("ignore", message="the md5 module is deprecated; " + \
18 "use hashlib instead")
20 from path import path
21 import fnmatch
22 import os
23 import shutil
24 import sys
26 from optparse import OptionParser, OptionGroup
27 from tag_wrapper import tag, TagException
29 # Some extensions might be missing
30 ACCEPTEXTS = [
31 ".asf", ".wma", ".wmv", # ASF, Windows Media
32 ".flac", # FLAC
33 ".mp3", # MP3
34 # Which extensions for Monkeysaudio?
35 ".mp4", ".m4a",
36 ".mpc", ".mpp", ".mp+", # Musepack (APEv2)
37 ".oga", ".ogg", # Ogg Vorbis
38 ".tta", # True Audio (hopefully)
39 ".wv", ".wvc" # WavPack
42 def issupportedfile(name):
43 for ext in ACCEPTEXTS:
44 if os.path.splitext(name)[1] == ext:
45 return True
46 return False
49 def mkdir(newdir):
50 if not os.path.isdir(newdir):
51 os.makedirs(newdir)
54 def newpath(file, pattern):
55 # Create the tokens dictionary
56 tokens = {}
57 tags = tag(file)
58 # TODO: add tracknumber, disknumber and possibily compilation
59 for t in ["title", "artist", "album artist", "album", "composer", "genre", "date"]:
60 try:
61 tokens[t] = tags[t]
62 except KeyError:
63 tokens[t] = None
64 # %album artist% is %artist% if nothing can be found in the tags
65 if tokens["album artist"] is None:
66 tokens["album artist"] = tokens["artist"]
68 # Now replace all tokens by their values
69 for i in tokens:
70 repl = "%" + i + "%"
71 if tokens[i] is not None:
72 if Options["windows"]:
73 val = sanitize_path(tokens[i][0])
74 else:
75 val = tokens[i][0]
76 pattern = pattern.replace(repl, val)
78 # Add the extension and return the new path
79 return pattern + os.path.splitext(file)[1]
82 def safeprint(string):
83 """
84 Print string first trying to normally encode it, sending it to the
85 console in "raw" UTF-8 if it fails.
87 This is a workaround for Windows's broken console
88 """
89 try:
90 print string
91 except UnicodeEncodeError:
92 print string.encode("utf-8")
95 def sanitize_path(string):
96 # replaces all characters invalid in windows path and file names by '+'
97 for i in ['/', '\\', ':', '*', '?', '"', '<', '>', '|']:
98 string = string.replace(i, '+')
99 return string
102 def files_to_move(base_dir, pattern):
103 # Figure out which files to move where
104 basepath = path(base_dir)
105 if Options["recursive"]:
106 files = basepath.walkfiles()
107 else:
108 files = basepath.files()
110 files_return = []
112 for file in files:
113 if issupportedfile(file):
114 try:
115 t = [ file, newpath(file, pattern) ]
116 except TagException:
117 print "Error reading tags from " + file
118 else:
119 files_return.append(t)
121 return files_return
123 Options = {}
125 def main():
126 # Pseudo-enum for actions
127 COPY = 0
128 MOVE = 1
129 PREVIEW = 2
130 NOTHING = 3
132 # Handle arguments
133 usage = "Usage: %prog [options] directory pattern"
134 version = "Ordnung 0.1 alpha 2"
135 parser = OptionParser(usage=usage, version=version)
136 parser.add_option("-r", "--recursive", action="store_true",
137 dest="recursive", help="also move/copy files from sub-directories",
138 default=False)
139 parser.add_option("-w", "--windows", action="store_true", dest="windows",
140 help="generate Windows-compatible file and path names",
141 default=False)
143 actions = OptionGroup(parser, "Possible actions")
144 actions.add_option("--preview", "-p", action="store_const",
145 const=PREVIEW, dest="action", help="preview, don't make any changes (default)")
146 actions.add_option("--copy", "-c", action="store_const",
147 const=COPY, dest="action", help="copy files")
148 actions.add_option("--move", "-m", action="store_const",
149 const=MOVE, dest="action", help="move files")
150 actions.add_option("--nothing", "-n", action="store_const",
151 const=MOVE, dest="action", help="do nothing (for debugging)")
152 parser.add_option_group(actions)
153 (options, args) = parser.parse_args()
155 # Passing these values in chains of arguments would be error-prone
156 Options["recursive"] = options.recursive
157 # What about Win 9x and possibily other platforms?
158 if os.name == "nt":
159 Options["windows"] = True
160 else:
161 Options["windows"] = options.windows
163 try:
164 base = args[0]
165 except IndexError:
166 print "You must specify a directory"
167 sys.exit()
169 try:
170 pattern = args[1]
171 except IndexError:
172 print "You must specify a pattern"
173 sys.exit()
175 # Do stuff
176 for i in files_to_move(base_dir=base, pattern=pattern):
177 if Options["windows"]:
178 # Windows doesn't accept trailing dots in filenames
179 t = i[1].split(os.sep)
180 if os.name == "nt":
181 # Am I working around a bug or a feature?
182 tpath = t[0] + '\\'
183 else:
184 tpath = t[0]
185 for j in t[1:-1]:
186 tpath = os.path.join(tpath, (j.rstrip('.')))
187 tfile = os.path.join(tpath, t[-1])
188 else:
189 tpath = os.path.split(i[1])[0]
190 tfile = i[1]
192 if options.action == MOVE:
193 mkdir(tpath)
194 shutil.move(i[0], tfile)
195 elif options.action == COPY:
196 mkdir(tpath)
197 shutil.copy2(i[0], tfile)
198 elif options.action == PREVIEW:
199 safeprint (i[0] + " --> " + tfile)
200 else: # options.action == NOTHING
201 pass
204 if __name__ == "__main__":
205 main()