Mercurial > winamplibrarypy
comparison WinampLibrary.py @ 0:af65171c2294
Read support, experimental write support (.dat only)
author | brad |
---|---|
date | Fri, 09 Nov 2012 19:10:59 -0600 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:af65171c2294 |
---|---|
1 import os | |
2 import struct | |
3 import Media | |
4 import time | |
5 from datetime import datetime | |
6 | |
7 class WinampLibrary: | |
8 | |
9 base_dir = '' | |
10 # Fields mappings in the format: | |
11 # <MLMedia attribute>: {'name': <Winamp internal name>, 'type': <Winamp data type>, 'winamp_id': <Winamp field ID>} | |
12 field_encode = { | |
13 'file_name': {'name': 'filename', 'type': 12, 'winamp_id': 0}, | |
14 'title': {'name': 'title', 'type': 3, 'winamp_id': 1}, | |
15 'artist': {'name': 'artist', 'type': 3, 'winamp_id': 2}, | |
16 'album': {'name': 'album', 'type': 3, 'winamp_id': 3}, | |
17 'year': {'name': 'year', 'type': 4, 'winamp_id': 4}, | |
18 'genre': {'name': 'genre', 'type': 3, 'winamp_id': 5}, | |
19 'comment': {'name': 'comment', 'type': 3, 'winamp_id': 6}, | |
20 'album_artist': {'name': 'albumartist', 'type': 3, 'winamp_id': 20}, | |
21 'composer': {'name': 'composer', 'type': 3, 'winamp_id': 24}, | |
22 'publisher': {'name': 'publisher', 'type': 3, 'winamp_id': 23}, | |
23 'category': {'name': 'category', 'type': 3, 'winamp_id': 34}, | |
24 'track': {'name': 'trackno', 'type': 4, 'winamp_id': 7}, | |
25 'length': {'name': 'length', 'type': 11, 'winamp_id': 8}, | |
26 'type': {'name': 'type', 'type': 4, 'winamp_id': 9}, | |
27 'last_update': {'name': 'lastupd', 'type': 10, 'winamp_id': 10}, | |
28 'last_play': {'name': 'lastplay', 'type': 10, 'winamp_id': 11}, | |
29 'rating': {'name': 'rating', 'type': 4, 'winamp_id': 12}, | |
30 'play_count': {'name': 'playcount', 'type': 4, 'winamp_id': 15}, | |
31 'file_time': {'name': 'filetime', 'type': 10, 'winamp_id': 16}, | |
32 'file_size': {'name': 'filesize', 'type': 4, 'winamp_id': 17}, | |
33 'bit_rate': {'name': 'bitrate', 'type': 4, 'winamp_id': 18}, | |
34 'disc': {'name': 'disc', 'type': 4, 'winamp_id': 19}, | |
35 'replaygain_album_gain': {'name': 'replaygain_album_gain', 'type': 3, 'winamp_id': 21}, | |
36 'replaygain_track_gain': {'name': 'replaygain_track_gain', 'type': 3, 'winamp_id': 22}, | |
37 'bpm': {'name': 'bpm', 'type': 4, 'winamp_id': 25}, | |
38 'discs': {'name': 'discs', 'type': 4, 'winamp_id': 26}, | |
39 'tracks': {'name': 'tracks', 'type': 4, 'winamp_id': 27}, | |
40 'is_podcast': {'name': 'ispodcast', 'type': 4, 'winamp_id': 28}, | |
41 'podcast_channel': {'name': 'podcastchannel', 'type': 3, 'winamp_id': 29}, | |
42 'podcast_pub_date': {'name': 'podcastpubdate', 'type': 10, 'winamp_id': 30}, | |
43 'gracenote_file_id': {'name': 'GracenoteFileID', 'type': 3, 'winamp_id': 31}, | |
44 'gracenote_ext_data': {'name': 'GracenoteExtData', 'type': 3, 'winamp_id': 32}, | |
45 'lossless': {'name': 'lossless', 'type': 4, 'winamp_id': 33}, | |
46 'codec': {'name': 'codec', 'type': 3, 'winamp_id': 35}, | |
47 'director': {'name': 'director', 'type': 3, 'winamp_id': 36}, | |
48 'producer': {'name': 'producer', 'type': 3, 'winamp_id': 37}, | |
49 'width': {'name': 'width', 'type': 4, 'winamp_id': 38}, | |
50 'height': {'name': 'height', 'type': 4, 'winamp_id': 39}, | |
51 'tuid2': {'name': 'tuid2', 'type': 3, 'winamp_id': 14}, | |
52 } | |
53 field_decode = dict((v['name'], k) for k, v in field_encode.iteritems()) | |
54 medias = [] | |
55 | |
56 #def __init__(self): | |
57 # self.base_dir = base_dir | |
58 | |
59 def files_from_path(self, path): | |
60 path = os.path.normpath(path) | |
61 if not os.path.isdir(path): | |
62 path = os.path.dirname(path) | |
63 idxfile = os.path.join(path, 'main.idx') | |
64 datfile = os.path.join(path, 'main.dat') | |
65 return idxfile, datfile | |
66 | |
67 def read(self, path): | |
68 self.fields = {} | |
69 idxfile, datfile = self.files_from_path(path) | |
70 if not os.path.isfile(idxfile): | |
71 raise IOError('Could not find Winamp library index file ' + idxfile) | |
72 if not os.path.isfile(idxfile): | |
73 raise IOError('Could not find Winamp library data file ' + datfile) | |
74 | |
75 idx = open(idxfile, 'rb') | |
76 if idx.read(8) != 'NDEINDEX': | |
77 idx.close() | |
78 raise Exception(idxfile + ' does not appear to be a valid Winamp library index file') | |
79 num_records, = struct.unpack('<i', idx.read(4)) | |
80 idx.read(4) # no one seems to know what these four bytes are for | |
81 dat = open(datfile, 'rb') | |
82 for _ in range(0, num_records): | |
83 data = idx.read(8) | |
84 if len(data) < 8: | |
85 break | |
86 offset, media_id = struct.unpack('ii', data) | |
87 print offset, media_id | |
88 media = self.read_media(dat, offset) | |
89 if media is not None: | |
90 self.medias.append(media) | |
91 idx.close() | |
92 dat.close() | |
93 | |
94 def write(self, path): | |
95 fields_sorted = sorted(self.field_encode.itervalues(), key = lambda field: field['winamp_id']) | |
96 self.fields = list(self.field_decode[i['name']] for i in fields_sorted) | |
97 | |
98 idxfile, datfile = self.files_from_path(path) | |
99 idx = open(idxfile, 'wb') | |
100 dat = open(datfile, 'wb') | |
101 idx.write('NDEINDEX') | |
102 dat.write('NDETABLE') | |
103 self.last_write_offset = 0 | |
104 self.current_write_offset = 8 | |
105 # Number of actual media files, plus the column and index records | |
106 num_records = len(self.medias) + 2 | |
107 # Write the number of records plus four mystery bytes to the index | |
108 idx.write(struct.pack('<i', num_records) + '\xFF\x00\x00\x00') | |
109 | |
110 self.write_column_record(dat) | |
111 self.write_mystery_record(dat) | |
112 | |
113 for media in self.medias: | |
114 self.write_media_record(dat, media) | |
115 | |
116 def write_column_record(self, dat): | |
117 position = 'first' | |
118 n = 0 | |
119 for field in self.fields: | |
120 if n == len(self.fields) - 1: | |
121 position = 'last' | |
122 self.write_field(dat, field, self.field_encode[field]['name'], position, header_record = True) | |
123 n += 1 | |
124 if n == 1: | |
125 position = 'middle' | |
126 | |
127 # Who knows what this is for? | |
128 def write_mystery_record(self, dat): | |
129 data_packed = '\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x04\x4E\x6F\x6E\x65\xAE' | |
130 offset_next = self.current_write_offset + len(data_packed) + 14 | |
131 header = struct.pack('<BBiii', 255, 1, len(data_packed), offset_next, 0) | |
132 | |
133 write_data = header + data_packed | |
134 dat.write(write_data) | |
135 | |
136 self.last_write_offset = self.current_write_offset | |
137 self.current_write_offset += len(write_data) | |
138 | |
139 data_packed = '\x00\x00\x00\x00\x0C\x00\x00\x00\x08\x66\x69\x6C\x65\x6E\x61\x6D\x65\x00' | |
140 offset_prev = self.last_write_offset | |
141 header = struct.pack('<BBiii', 0, 1, len(data_packed), 0, offset_prev) | |
142 | |
143 write_data = header + data_packed | |
144 dat.write(write_data) | |
145 | |
146 self.last_write_offset = self.current_write_offset | |
147 self.current_write_offset += len(write_data) | |
148 | |
149 def write_media_record(self, dat, media): | |
150 position = 'first' | |
151 n = 0 | |
152 for field in self.fields: | |
153 if n == len(self.fields) - 1: | |
154 position = 'last' | |
155 self.write_field(dat, field, getattr(media, field), position) | |
156 n += 1 | |
157 if n == 1: | |
158 position = 'middle' | |
159 | |
160 def write_field(self, dat, field, data, position, header_record = False): | |
161 if data is None: | |
162 return | |
163 | |
164 field_id = self.field_encode[field]['winamp_id'] | |
165 data_type = self.field_encode[field]['type'] | |
166 | |
167 if header_record: | |
168 data_packed = struct.pack('<BBB', data_type, 0, len(data)) | |
169 data_packed += data | |
170 data_type = 0 | |
171 else: | |
172 # String field | |
173 if data_type == 3 or data_type == 12: | |
174 data_encoded = data.encode('utf-16') | |
175 data_packed = struct.pack('<H', len(data_encoded)) + data_encoded | |
176 | |
177 # Integer field | |
178 elif data_type == 4 or data_type == 11: | |
179 data_packed = struct.pack('<i', data) | |
180 | |
181 # Date field | |
182 elif data_type == 10: | |
183 timestamp = int(time.mktime(data.timetuple())) | |
184 data_packed = struct.pack('<i', timestamp) | |
185 | |
186 else: | |
187 print 'Warning: unsupported data type ' + str(data_type) + ' for field ' + field | |
188 | |
189 offset_prev = self.last_write_offset | |
190 offset_next = self.current_write_offset + len(data_packed) + 14 | |
191 if position == 'first': | |
192 offset_prev = 0 | |
193 elif position == 'last': | |
194 offset_next = 0 | |
195 | |
196 header = struct.pack('<BBiii', field_id, data_type, len(data_packed), offset_next, offset_prev) | |
197 write_data = header + data_packed | |
198 dat.write(write_data) | |
199 | |
200 self.last_write_offset = self.current_write_offset | |
201 self.current_write_offset += len(write_data) | |
202 | |
203 def read_media(self, dat, offset): | |
204 dat.seek(offset) | |
205 media = Media.Media() | |
206 while True: | |
207 field_id, data_type, size, offset_next, offset_prev = struct.unpack('<BBiii', dat.read(14)) | |
208 print 'data', field_id, data_type, size, offset_next, offset_prev | |
209 # Column field | |
210 if data_type == 0: | |
211 # The column name is always a string, and the middle byte appears to be unused | |
212 _, _, data_size = struct.unpack('<BBB', dat.read(3)) | |
213 field_name = dat.read(data_size) | |
214 if field_name in self.field_decode: | |
215 self.fields[field_id] = field_name | |
216 else: | |
217 print 'Warning: unknown field name ' + field_name | |
218 | |
219 elif field_id in self.fields: | |
220 prop = self.field_decode[self.fields[field_id]] | |
221 value = None | |
222 | |
223 # String field | |
224 if data_type == 3 or data_type == 12: | |
225 data_size, = struct.unpack('<H', dat.read(2)) | |
226 data = dat.read(data_size) | |
227 enc = 'utf-16' if data.find('\xff\xfe') == 0 else 'ascii' | |
228 value = unicode(data, encoding=enc, errors='ignore') | |
229 | |
230 # Integer field | |
231 elif data_type == 4 or data_type == 11: | |
232 value, = struct.unpack('<i', dat.read(4)) | |
233 | |
234 # Date field | |
235 elif data_type == 10: | |
236 timestamp, = struct.unpack('<i', dat.read(4)) | |
237 value = datetime.fromtimestamp(timestamp) | |
238 | |
239 # Index field? Not actually a media | |
240 elif data_type == 1: | |
241 return | |
242 | |
243 else: | |
244 print 'Warning: unsupported data type ' + str(data_type) + ' for field ' + self.fields[field_id] | |
245 | |
246 if value is not None: | |
247 setattr(media, prop, value) | |
248 | |
249 else: | |
250 print '?' + str(field_id) | |
251 if offset_next == 0: | |
252 break | |
253 dat.seek(offset_next) | |
254 | |
255 if data_type > 1: # not column or index field | |
256 return media | |
257 | |
258 lib = WinampLibrary() | |
259 lib.read('C:\\Users\\Brad\\AppData\\Roaming\\Winamp\\Plugins\\ml') | |
260 lib.write('C:\\Users\\Brad\\AppData\\Roaming\\Winamp\\Plugins\\ml\\out') |