tracks are "records" with defaults
[riffle.git] / shuffle.py
blob5ce694bf64b45e162ca89a765950945d09d5659a
1 """
2 iPod Shuffle database access
4 Documentation:
5 - http://ipodlinux.org/ITunesDB#iTunesSD_file and further
7 Author: Artem Baguinski
8 """
10 import struct, os, sys
12 BIG_ENDIAN = True
13 LITTLE_ENDIAN = False
14 READ = 'rb'
15 WRITE = 'w+b'
17 def pack_uint24(i,bigendian):
18 "Pack an unsigned integer to a string of three bytes"
19 if bigendian:
20 return struct.pack(">I",i)[1:4]
21 else:
22 return struct.pack("<I",i)[0:3]
24 def unpack_uint24(s,bigendian):
25 "Unpack an unsigned integer from first three bytes of a string"
26 if bigendian:
27 return struct.unpack('>I','\x00' + s[0:3])[0]
28 else:
29 return struct.unpack('<I',s[0:3] + '\x00')[0]
31 def pack_int24(i,bigendian):
32 "Pack a signed integer to a string of three bytes"
33 if bigendian:
34 return struct.pack(">i",i)[1:4]
35 else:
36 return struct.pack("<i",i)[0:3]
38 def unpack_int24(s,bigendian):
39 "Unpack a signed integer from first three bytes of a string"
40 u = unpack_uint24(s,bigendian)
41 if (u & 0x800) != 0:
42 return - ((~u + 1) & 0xfff)
43 else:
44 return u
46 class Track:
47 starttime = 0
48 stoptime = 0
49 volume = 0x64
50 bookmarktime = -1
51 playcount = 0
52 skippedcount = 0
53 filename = None
54 file_type = 0
55 bookmarkflag = False
56 shuffleflag = True
58 def __init__(self, filename=None):
59 if filename is not None:
60 self.set_filename(filename)
62 def set_filename(self, filename):
63 self.filename = filename
64 if filename.endswith((".mp3",".aa")):
65 self.file_type = 1
66 elif filename.endswith((".m4a", ".m4b", ".m4p")):
67 self.file_type = 2
68 elif filename.endswith(".wav"):
69 self.file_type = 4
70 else:
71 raise "%s: unsupported file type" % (filename)
72 if filename.endswith((".aa",".m4b")):
73 self.bookmarkflag = True
74 else:
75 self.bookmarkflag = False
76 self.shuffleflag = not self.bookmarkflag
78 def __str__(self):
79 s = "%s\n vol: %d " % (self.filename, self.volume)
80 if self.starttime != 0 or self.stoptime != 0:
81 s += "%5.3fs-%5.3fs " % (self.starttime*0.256, self.stoptime*0.256)
82 if self.bookmarkflag:
83 bm = self.bookmarktime
84 if bm<0:
85 bm=0
86 s += "bookmark: %5.3fs " % (bm*0.256)
87 if self.shuffleflag:
88 s += "shuffle "
89 s += "played: %d skipped: %d" % (self.playcount, self.skippedcount)
90 return s
93 class ShuffleDB:
94 endian = BIG_ENDIAN
95 file = None
96 mode = READ
98 def skip(self, n):
99 if self.file is not None:
100 self.file.seek(n, os.SEEK_CUR)
102 def read(self, n):
103 if self.file is None:
104 raise "No current file"
105 return self.file.read(n)
107 def write(self, buf):
108 if self.file is None:
109 raise "No current file"
110 self.file.write(buf)
112 def read_u24(self, check = None):
113 u24 = unpack_uint24( self.read(3), self.endian )
114 if check is not None and u24 != check:
115 raise "Format error"
116 return u24
118 def read_i24(self, check = None):
119 i24 = unpack_int24( self.read(3), self.endian )
120 if check is not None and i24 != check:
121 raise "Format error"
122 return i24
124 def write_u24(self, u):
125 self.write( pack_uint24(u, self.endian) )
127 def write_i24(self, u):
128 self.write( pack_int24(u, self.endian) )
130 def read_bool(self):
131 return self.read(1) != '\x00'
133 def write_bool(self, v):
134 if v:
135 self.write('\x01')
136 else:
137 self.write('\x00')
139 def read_string(self, raw_len):
140 return self.read(raw_len).decode("UTF-16-le").rstrip("\x00")
142 def write_string(self, string, pad_to):
143 self.write( string.encode("UTF-16-le").ljust(pad_to, '\x00') )
146 def open_file(self, fname, endian, mode=READ):
147 if self.file is not None:
148 if self.mode == WRITE:
149 self.file.truncate()
150 self.file.close()
152 if fname is not None:
153 self.file = open(fname,mode)
155 self.endian = endian
156 self.mode = mode
158 def close_file(self):
159 self.open_file(None, BIG_ENDIAN)
161 def write_iTunesSD(self, tracks):
162 self.open_file('iTunesSD', BIG_ENDIAN, WRITE)
163 self.write_u24(len(tracks))
164 self.write_u24(0x010800) # like iTunes 7.2 does
165 self.write_u24(18) # header size
166 self.skip(9)
167 for t in tracks:
168 self.write_iTunesSD_track(t)
169 self.close_file()
171 def write_iTunesSD_track(self, t):
172 self.write_u24( 558 ) # record length
173 self.skip(3)
174 self.write_u24( t.starttime )
175 self.skip(6)
176 self.write_u24( t.stoptime )
177 self.skip(6)
178 self.write_u24( t.volume )
179 self.write_u24( t.file_type )
180 self.skip(3)
181 self.write_string( t.filename, 522)
182 self.write_bool( t.shuffleflag )
183 self.write_bool( t.bookmarkflag )
184 self.skip(1)
186 def read_iTunesSD(self):
187 self.open_file('iTunesSD', BIG_ENDIAN)
188 num_tracks = self.read_u24()
189 self.skip(15) # skip the rest of the header
190 tracks = []
191 for n in xrange(0, num_tracks):
192 tracks.append( self.read_iTunesSD_track() )
193 self.close_file()
194 return tracks
196 def read_iTunesSD_track(self):
197 t = Track()
198 self.read_u24( 558 ) # sanity check (record length)
199 self.skip(3)
200 t.starttime = self.read_u24()
201 self.skip(6)
202 t.stoptime = self.read_u24()
203 self.skip(6)
204 t.volume = self.read_u24()
205 t.file_type = self.read_u24()
206 self.skip(3)
207 t.filename = self.read_string(522)
208 t.shuffleflag = self.read_bool()
209 t.bookmarkflag = self.read_bool()
210 self.skip(1)
211 return t
213 def write_iTunesStats(self, tracks):
214 self.open_file('iTunesStats', LITTLE_ENDIAN, WRITE)
215 self.write_u24( len(tracks) )
216 self.skip(3)
217 for t in tracks:
218 self.write_iTunesStats_track(t)
219 self.close_file()
221 def write_iTunesStats_track(self, t):
222 self.write_u24( 18 ) # record length
223 self.write_i24( t.bookmarktime )
224 self.skip(6)
225 self.write_u24( t.playcount )
226 self.write_u24( t.skippedcount )
228 def read_iTunesStats(self, tracks):
229 self.open_file('iTunesStats', LITTLE_ENDIAN)
230 num_tracks = self.read_u24()
231 if num_tracks != len(tracks):
232 raise "Inconsistent iTunesSD and iTunesStats"
233 self.skip(3)
234 for t in tracks:
235 self.read_iTunesStats_track(t)
236 self.close_file()
238 def read_iTunesStats_track(self, t):
239 self.read_u24( 18 ) # sanity check (record length)
240 t.bookmarktime = self.read_i24()
241 self.skip(6)
242 t.playcount = self.read_u24()
243 t.skippedcount = self.read_u24()
245 def write_iTunesPState(self, pstate):
246 self.open_file('iTunesPState', LITTLE_ENDIAN, WRITE)
247 self.write_u24( pstate['volume'] )
248 self.write_u24( pstate['shufflepos'] )
249 self.write_u24( pstate['trackno'] )
250 self.write_u24( pstate['shuffleflag'] )
251 self.write_u24( pstate['trackpos'] )
252 self.skip(6)
253 self.close_file()
256 def read_iTunesPState(self):
257 pstate = {}
258 self.open_file('iTunesPState', LITTLE_ENDIAN)
259 pstate['volume'] = self.read_u24()
260 pstate['shufflepos'] = self.read_u24()
261 pstate['trackno'] = self.read_u24()
262 pstate['shuffleflag'] = self.read_u24()
263 pstate['trackpos'] = self.read_u24()
264 self.skip(6)
265 self.close_file()
266 return pstate
268 def read_all(self):
269 tracks = self.read_iTunesSD()
270 self.read_iTunesStats(tracks)
271 pstate = self.read_iTunesPState()
272 return (tracks, pstate)
274 def write_all(self, tracks, pstate):
275 self.write_iTunesSD(tracks)
276 self.write_iTunesStats(tracks)
277 self.write_iTunesPState(pstate)
279 #####################################################################
280 if __name__ == '__main__':
281 # try conversions
282 b100 = pack_uint24(100,True)
283 l100 = pack_uint24(100,False)
284 print b100 == '\x00\x00d'
285 print l100 == 'd\x00\x00'
286 print unpack_uint24(b100,True) == 100
287 print unpack_uint24(l100,False) == 100
289 b100 = pack_int24(-100,True)
290 l100 = pack_int24(-100,False)
291 print b100 == '\xff\xff\x9c'
292 print l100 == '\x9c\xff\xff'
293 print unpack_int24(b100,True) == -100
294 print unpack_int24(l100,False) == -100
296 def print_list(xs):
297 for x in xs:
298 print x
300 # try reading
301 if len(sys.argv) > 1:
302 start_dir = os.getcwd()
303 os.chdir(sys.argv[1])
304 db = ShuffleDB()
305 tracks, pstate = db.read_all()
306 print_list( tracks )
307 print pstate
309 # try writing
310 if len(sys.argv) > 2:
311 os.chdir(start_dir)
312 os.chdir(sys.argv[2])
313 db.write_all(tracks, pstate)