1
2 """
3 This script converts Vorbis OGG and mp3 files to low-bitrate mp3 files.
4 Requires at least the following Debian packages:
5 python, python-pyvorbis, python-eyeD3, vorbistools, lame
6 """
7
8 import sys, os, ogg, ogg.vorbis, eyeD3
9 import getopt, re
10
11 usage = """Usage:
12 %s [options] <source> [<source>...]"""%(os.path.basename(sys.argv[0]))
13 help = """
14 -h --help this help
15 -n --simple-names Remove non-alnum chars from filename
16 -d --single-directory Do not create "artist - album" directory
17 -l --list-files List source and target files only
18 -F --force-convert Force conversion even if target file exists
19
20 source File or directory, directories are searched for ogg
21 and mp3 -files.
22
23 Found files are converted to 96kbps mp3-files, "-lbr" is appended to the
24 filename. By default the files are placed in a "artist - album" directory.
25 """
26
27
28
29
30
31 class autosuper(type):
32 """Automatically adds __super to the class"""
33 def __init__(cls, name, bases, dict):
34 super(autosuper, cls).__init__(name, bases, dict)
35 setattr(cls, "_%s__super"%(name), super(cls))
36
37
38
39
40
41 class AudioStream:
42 """Generic AudioStream base-class."""
43 __metaclass__ = autosuper
44 def __init__(self, filename):
45 self.filename=filename
46 self.artist=None
47 self.track=None
48 self.title=None
49 self.album=None
50 self.outputname=None
51 self.outputflags=None
52 self.lame_opts="--lowpass 14 --preset 96"
53
54 def convertToMP3(self, outputname=None):
55 """Method to convert the file into LBR MP3, the actual conversion is
56 left to sub-classes. This class only make sure the directory exists."""
57 path = os.path.dirname(target)
58 if path:
59 if not os.path.exists(path):
60 os.makedirs(path)
61 elif not os.path.isdir(path):
62 print >>sys.stderr, "ERROR:"\
"'%s' exists and is not a directory"%(path)
63
64
65 def getNewName(self, keep_names=1, single_dir=1):
66 """Returns the name of the resulting file"""
67 if self.outputname and self.outputflags == (keep_names, single_dir):
68 return self.outputname
69 if not keep_names:
70 result = re.sub("\W+", "",
71 os.path.basename(self.filename[:-4]))+"-lbr.mp3"
72 else:
73 result = os.path.basename(self.filename)[:-4]+"-lbr.mp3"
74 if not single_dir:
75 result = "%s - %s%s%s"%(self.artist.replace(os.sep, ""),
76 self.album.replace(os.sep, ""), os.sep, result)
77 self.outputname=result
78 self.outputflags = (keep_names, single_dir)
79 return result
80
81 def _escape(self, s):
82 """Escapes " -chars for os.system() calls"""
83 return s.replace('"', '\\"')
84
85
86
87
88
89 class MP3Stream(AudioStream):
90 def __init__(self, filename):
91 self.__super.__init__(filename)
92
93 tags = eyeD3.Mp3AudioFile(self.filename).getTag()
94 self.artist = tags.getArtist() or "unknown"
95 self.album = tags.getAlbum() or "unknown"
96 self.track = str(tags.getTrackNum()[0] or "")
97 self.title = tags.getTitle() or "unknown"
98
99 def convertToMP3(self, filename=None):
100 if not self.outputname and not filename:
101 self.getNewName()
102 self.__super.convertToMP3(filename)
103 os.system('lame "%s" %s "%s" --tt "%s" --ta "%s"'%(
104 self._escape(self.filename), self.lame_opts,
105 self._escape(filename or self.outputname),
106 " - ".join(filter(None, (self.track, self._escape(self.title)))),
107 " - ".join(filter(None, (self._escape(self.artist),
108 self._escape(self.album))))))
109
110
111
112
113
114 class OGGStream(AudioStream):
115 def __init__(self, filename):
116 self.__super.__init__(filename)
117 stream = ogg.vorbis.VorbisFile(self.outputname)
118 comments = stream.comment().as_dict()
119 self.artist = comments.get("ARTIST", [""])[0].encode("latin_1", "ignore")
120 self.album = comments.get("ALBUM", [""])[0].encode("latin_1", "ignore")
121 self.track = comments.get("TRACKNUMBER", [""])[0].encode("latin_1", "ignore")
122 self.title = comments.get("TITLE", [""])[0].encode("latin_1", "ignore")
123
124 def convertToMP3(self, filename=None):
125 if not self.outputname and not filename:
126 self.getNewName()
127 self.__super.convertToMP3(filename)
128 tempname = "/tmp/oggconvert-%d.wav"%(os.getpid())
129 print "Converting %s to wav"%(os.path.basename(self.filename))
130 os.system('ogg123 -q -d wav -f "%s" "%s"'%(
131 self._escape(tempname), self._escape(self.filename)))
132 os.system('lame "%s" %s "%s" --tt "%s" --ta "%s"'%(
133 self._escape(tempname), self.lame_opts,
134 self._escape(filename or self.outputname),
135 " - ".join(filter(None, (self.track, self._escape(self.title)))),
136 " - ".join(filter(None, (self._escape(self.artist),
137 self._escape(self.album))))))
138 os.remove(tempname)
139
140
141
142
143 class Converter(object):
144 def __init__(self, args=[]):
145 self.keep_filenames = 1
146 self.single_directory = 0
147 self.list_only=0
148 self.force_convert=0
149 self.registry={}
150 self.source=[]
151 quit=self.parseArgs(sys.argv[1:])
152 if quit:
153 sys.exit(quit-1)
154
155 def parseArgs(self, args):
156 opts, args=getopt.getopt(args, "hndlF",
157 ("help", "simple-names", "single-directory", "list-files",
158 "force-convert"))
159 self.sources=args
160 for name, value in opts:
161 if name in ("-h", "--help"):
162 print usage
163 print help
164 return 1
165 elif name in ("-n", "--simple-names"):
166 self.keep_filenames=0
167 elif name in ("-d", "--single-directory"):
168 self.single_directory=1
169 elif name in ("-l", "--list-files"):
170 self.list_only=1
171 elif name in ("-F", "--force-convert"):
172 self.force_convert=1
173 if not args:
174 print usage
175 return 1
176 return 0
177
178 def registerExtension(self, extension, classref):
179 """Registers the extension to a class. The class init is called with a
180 filename as the only argument."""
181 self.registry[extension]=classref
182
183 def getAudioStream(self, filename):
184 try:
185 ext=filename.split(".")[-1]
186 except:
187 raise Warning, "No extension found."
188 return self.registry[ext](filename)
189
190 def getStreams(self):
191 for source in self.sources:
192 if os.path.isdir(source):
193 for file in self.__subfiles(source):
194 yield self.getAudioStream(file)
195 elif os.path.isfile(source):
196 yield self.getAudioStream(source)
197 else:
198 print>>sys.stderr, "ERROR:"\
"'%s' is not a file or a directory."%source
199
200 def __subfiles(self, path):
201 for dirpath, dirnames, filenames in os.walk(path):
202 for filename in filenames:
203 outputname=os.path.join(dirpath, filename)
204 if os.path.isfile(outputname) and\
filename[-4:].lower() in (".ogg", ".mp3"):
205 yield outputname
206 else:
207 print >>sys.stderr, "ERROR: ignoring '%s'"%(filename)
208
209
210
211
212
213
214 if __name__ == "__main__":
215 converter = Converter(sys.argv[1:])
216
217
218 converter.registerExtension("mp3", MP3Stream)
219 converter.registerExtension("MP3", MP3Stream)
220 converter.registerExtension("OGG", OGGStream)
221 converter.registerExtension("ogg", OGGStream)
222
223 for stream in converter.getStreams():
224 print "Source:", stream.filename
225 target=stream.getNewName(converter.keep_filenames, converter.single_directory)
226 if converter.list_only:
227 print "Target: %s"%(target)
228 elif converter.force_convert or not os.path.exists(target):
229 stream.convertToMP3()
230 else:
231 print "Target exists, not converting. Use -F to force."