more consistent get_size()
[riffle.git] / shuffle.py
blob5f99fad1192d792848a1c17dbc28840ee94b60ed
1 """
2 iPod Shuffle database access
4 Documentation:
5 - http://ipodlinux.org/ITunesDB#iTunesSD_file and further
7 Author: Artem Baguinski
8 """
10 from __future__ import with_statement
11 import struct, os, sys
13 BIG_ENDIAN = True
14 LITTLE_ENDIAN = False
15 READ = 'rb'
16 WRITE = 'w+b'
18 class BaseField:
19 def set_bigendian(self, ignore):
20 pass
21 def set_reclen(self, ignore):
22 pass
24 class Skip(BaseField):
25 def __init__(self, n):
26 self.size = n
27 def skip(self, file):
28 file.seek(self.size, os.SEEK_CUR)
29 def read(self, file, dict):
30 self.skip(file)
31 def write(self, file, dict):
32 self.skip(file)
33 def get_size(self):
34 return self.size
36 class Field(BaseField):
37 class Named:
38 def __init__(self, name):
39 self.name = name
40 def get(self, dict):
41 return dict[self.name]
42 def put(self, dict, value):
43 dict[self.name] = value
45 class Const:
46 def __init__(self, const, check):
47 self.const = const
48 self.check = check
49 def get(self, dict):
50 return self.const
51 def put(self, dict, value):
52 if self.check and value != self.const:
53 raise "Format error"
55 def __init__(self, packer, name=None, const=None, check = False):
56 self.packer = packer
57 self.get_size = packer.get_size
58 if const is not None:
59 self.value_handler = Field.Const(const, check)
60 elif name == '%reclen%':
61 def const_later(const):
62 self.value_handler = Field.Const(const, check)
63 self.set_reclen = const_later
64 elif name is not None:
65 self.value_handler = Field.Named(name)
66 elif callback is not None:
67 self.value_handler = Field.Callback(callback, check)
68 else:
69 raise "Bad field parameters"
71 def read(self, file, dict):
72 self.put(dict, self.unpack( file.read( self.get_size() )))
73 def write(self, file, dict):
74 file.write( self.pack( self.value_handler.get(dict) ))
75 def put(self, dict, val):
76 self.value_handler.put(dict, val)
77 def get(self, dict):
78 return self.value_handler.get(dict)
79 def unpack(self, str):
80 return self.packer.unpack(str)
81 def pack(self, val):
82 return self.packer.pack(val)
83 def set_bigendian(self, bigendian):
84 self.packer.bigendian = bigendian
86 class SimplePacker:
87 def __init__(self, fmt):
88 self.fmt = fmt
89 self.size = struct.calcsize(fmt)
90 def pack(self,val): return struct.pack(self.fmt,val)
91 def unpack(self,str): return struct.unpack(self.fmt,str)[0]
92 def get_size(self): return self.size
94 class Uint8(SimplePacker):
95 def __init__(self): SimplePacker.__init__(self,"B")
97 class Bool8(Uint8):
98 # def __init__(self): Uint8.__init__(self)
99 def pack(self, val): return Uint8.pack(self, (1 if val else 0))
100 def unpack(self, str): return Uint8.unpack(self, str) != 0
102 class Uint24:
103 def __init__(self, bigendian = LITTLE_ENDIAN):
104 self.bigendian = bigendian
105 def get_size(self): return 3
106 def pack(self, i):
107 if self.bigendian:
108 return struct.pack(">I",i)[1:4]
109 else:
110 return struct.pack("<I",i)[0:3]
111 def unpack(self, s):
112 if self.bigendian:
113 return struct.unpack('>I','\x00' + s[0:3])[0]
114 else:
115 return struct.unpack('<I',s[0:3] + '\x00')[0]
117 class Int24(Uint24):
118 def __init__(self, bigendian = LITTLE_ENDIAN):
119 self.bigendian = bigendian
120 def pack(self, i):
121 if self.bigendian:
122 return struct.pack(">i",i)[1:4]
123 else:
124 return struct.pack("<i",i)[0:3]
125 def unpack(self, s):
126 u = Uint24.unpack(self,s)
127 if (u & 0x800) != 0:
128 return - ((~u + 1) & 0xfff)
129 else:
130 return u
132 class Bool24(Int24):
133 def __init__(self): Int24.__init__(self)
134 def pack(self, val): return Int24.pack(self, (-1 if val else 0))
135 def unpack(self, str): return Int24.unpack(self, str) != 0
137 class ZeroPaddedString:
138 def __init__(self, len, enc):
139 self.size = len
140 self.enc = enc
141 def pack(self, val):
142 return val.encode(self.enc).ljust(self.size,'\x00')
143 def unpack(self, str):
144 return str.decode(self.enc).rstrip('\x00')
145 def get_size(self): return self.size
147 class Record:
148 def __init__(self, fields, bigendian):
149 self.fields = fields
150 reclen = self.get_size()
151 for f in fields:
152 f.set_bigendian(bigendian)
153 f.set_reclen(reclen)
155 def read(self, file, dict = {}):
156 for f in self.fields:
157 f.read(file, dict)
158 return dict
160 def write(self, file, dict):
161 for f in self.fields:
162 f.write(file, dict)
164 def get_size(self):
165 size = 0
166 for f in self.fields:
167 size += f.get_size()
168 return size
170 class Track:
171 supported_file_types = (".mp3", ".aa", ".m4a", ".m4b", ".m4p", ".wav")
173 starttime = 0
174 stoptime = 0
175 volume = 0x64
176 bookmarktime = -1
177 playcount = 0
178 skippedcount = 0
179 filename = None
180 file_type = 0
181 bookmarkflag = False
182 shuffleflag = True
184 def set_filename(self, filename):
185 self.filename = filename
186 if filename.endswith((".mp3",".aa")):
187 self.file_type = 1
188 elif filename.endswith((".m4a", ".m4b", ".m4p")):
189 self.file_type = 2
190 elif filename.endswith(".wav"):
191 self.file_type = 4
192 else:
193 raise "%s: unsupported file type" % (filename)
194 if filename.endswith((".aa",".m4b")):
195 self.bookmarkflag = True
196 else:
197 self.bookmarkflag = False
198 self.shuffleflag = not self.bookmarkflag
200 def __str__(self):
201 s = "%s\n vol: %d " % (self.filename, self.volume)
202 if self.starttime != 0 or self.stoptime != 0:
203 s += "%5.3fs-%5.3fs " % (self.starttime*0.256, self.stoptime*0.256)
204 if self.bookmarkflag:
205 bm = self.bookmarktime
206 if bm<0:
207 bm=0
208 s += "bookmark: %5.3fs " % (bm*0.256)
209 if self.shuffleflag:
210 s += "shuffle "
211 s += "played: %d skipped: %d" % (self.playcount, self.skippedcount)
212 return s
214 # persistency
215 old_tracks = {}
217 @classmethod
218 def new(cls, filename):
219 if Track.old_tracks.has_key(filename):
220 return Track.old_tracks[filename]
221 else:
222 t = cls()
223 t.set_filename(filename)
224 return t
226 @classmethod
227 def set_old_tracks(cls, lst):
228 cls.old_tracks = {}
229 for i in xrange(len(lst)):
230 t = lst[i]
231 cls.old_tracks[t.filename] = t
232 cls.old_tracks[i] = t
234 class PState:
235 volume = 29
236 shufflepos = 0
237 trackno = 0
238 shuffleflag = False
239 trackpos = 0
241 def __str__(self):
242 return """Player state:
243 volume: %d
244 shuffle mode: %s
245 shuffle position: %d
246 track number: %d
247 track position: %d""" % (self.volume, self.shuffleflag,
248 self.shufflepos, self.trackno, self.trackpos)
250 class ShuffleDB:
251 iTunesSD_hdr = Record([
252 Field(Uint24(), 'tracks'),
253 Field(Uint24(), const=0x010800),
254 Field(Uint24(), '%reclen%', check=True),
255 Skip(9)],
256 BIG_ENDIAN)
258 iTunesSD_track = Record([
259 Field(Uint24(), '%reclen%', check=True),
260 Skip(3),
261 Field(Uint24(), 'starttime'),
262 Skip(6),
263 Field(Uint24(), 'stoptime'),
264 Skip(6),
265 Field(Uint24(), 'volume'),
266 Field(Uint24(), 'file_type'),
267 Skip(3),
268 Field(ZeroPaddedString(522, 'UTF-16-LE'), 'filename'),
269 Field(Bool8(), 'shuffleflag'),
270 Field(Bool8(), 'bookmarkflag'),
271 Skip(1)],
272 BIG_ENDIAN)
274 iTunesStats_hdr = Record([
275 Field( Uint24(), 'tracks'),
276 Skip(3)],
277 LITTLE_ENDIAN)
279 iTunesStats_track = Record([
280 Field( Uint24(), '%reclen%', check = True),
281 Field( Int24(), 'bookmarktime'),
282 Skip(6),
283 Field( Uint24(), 'playcount'),
284 Field( Uint24(), 'skippedcount')],
285 LITTLE_ENDIAN)
287 iTunesPState = Record([
288 Field( Uint8(), 'volume' ),
289 Field( Uint24(), 'shufflepos' ),
290 Field( Uint24(), 'trackno' ),
291 Field( Bool24(), 'shuffleflag'),
292 Field( Uint24(), 'trackpos'),
293 Skip(19)],
294 LITTLE_ENDIAN)
296 def write_iTunesSD(self, tracks):
297 with open('iTunesSD', WRITE) as file:
298 self.iTunesSD_hdr.write(file, {'tracks':len(tracks)})
299 for t in tracks:
300 self.iTunesSD_track.write(file, t.__dict__)
301 file.truncate()
303 def read_iTunesSD(self):
304 with open('iTunesSD', READ) as file:
305 num_tracks = self.iTunesSD_hdr.read(file)['tracks']
306 tracks = []
307 for n in xrange(0, num_tracks):
308 t = Track()
309 self.iTunesSD_track.read(file, t.__dict__)
310 tracks.append( t )
311 return tracks
313 def write_iTunesStats(self, tracks):
314 with open('iTunesStats', WRITE) as file:
315 self.iTunesStats_hdr.write(file, {'tracks':len(tracks)})
316 for t in tracks:
317 self.iTunesStats_track.write(file, t.__dict__)
318 file.truncate()
320 def read_iTunesStats(self, tracks):
321 with open('iTunesStats', READ) as file:
322 num_tracks = self.iTunesStats_hdr.read(file)['tracks']
323 if num_tracks != len(tracks):
324 raise "Inconsistent number of songs in iTunesSD and iTunesStats"
325 for t in tracks:
326 self.iTunesStats_track.read(file, t.__dict__)
328 def write_iTunesPState(self, pstate):
329 mode = 'r+b' if os.path.exists('iTunesPState') else WRITE
330 with open('iTunesPState', mode) as file:
331 self.iTunesPState.write(file, pstate.__dict__)
332 file.truncate()
334 def read_iTunesPState(self):
335 pstate = PState()
336 with open('iTunesPState', READ) as file:
337 self.iTunesPState.read(file, pstate.__dict__)
338 return pstate
340 def read_all(self):
341 tracks = self.read_iTunesSD()
342 self.read_iTunesStats(tracks)
343 pstate = self.read_iTunesPState()
344 return (tracks, pstate)
346 def write_all(self, tracks, pstate):
347 self.write_iTunesSD(tracks)
348 self.write_iTunesStats(tracks)
349 self.write_iTunesPState(pstate)
351 #####################################################################
352 if __name__ == '__main__':
353 def print_list(xs):
354 for x in xs:
355 print x
357 if len(sys.argv) > 1:
358 # try reading
359 start_dir = os.getcwd()
360 os.chdir(sys.argv[1])
361 db = ShuffleDB()
362 tracks, pstate = db.read_all()
363 print_list( tracks )
364 print pstate
366 # try cache
367 Track.set_old_tracks( tracks )
368 t = Track.new( "foo.mp3" )
369 print t
370 t = Track.new( tracks[0].filename )
371 print t
373 if len(sys.argv) > 2:
374 # try writing
375 os.chdir(start_dir)
376 os.chdir(sys.argv[2])
377 db.write_all(tracks, pstate)