Mercurial > winamplibrarypy
changeset 0:af65171c2294
Read support, experimental write support (.dat only)
author | brad |
---|---|
date | Fri, 09 Nov 2012 19:10:59 -0600 |
parents | |
children | a7bd26076340 |
files | Media.py WinampLibrary.py test.py |
diffstat | 3 files changed, 274 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Media.py Fri Nov 09 19:10:59 2012 -0600 @@ -0,0 +1,10 @@ +class Media: + + file_name = title = artist = album = genre = comment = album_artist = \ + composer = publisher = category = year = track = length = type = \ + last_update = last_play = rating = play_count = file_time = \ + file_size = bit_rate = disc = replaygain_album_gain = \ + replaygain_track_gain = bpm = discs = tracks = is_podcast = \ + podcast_channel = podcast_pub_date = gracenote_file_id = \ + gracenote_ext_data = lossless = codec = director = producer = width = \ + height = tuid2 = None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WinampLibrary.py Fri Nov 09 19:10:59 2012 -0600 @@ -0,0 +1,260 @@ +import os +import struct +import Media +import time +from datetime import datetime + +class WinampLibrary: + + base_dir = '' + # Fields mappings in the format: + # <MLMedia attribute>: {'name': <Winamp internal name>, 'type': <Winamp data type>, 'winamp_id': <Winamp field ID>} + field_encode = { + 'file_name': {'name': 'filename', 'type': 12, 'winamp_id': 0}, + 'title': {'name': 'title', 'type': 3, 'winamp_id': 1}, + 'artist': {'name': 'artist', 'type': 3, 'winamp_id': 2}, + 'album': {'name': 'album', 'type': 3, 'winamp_id': 3}, + 'year': {'name': 'year', 'type': 4, 'winamp_id': 4}, + 'genre': {'name': 'genre', 'type': 3, 'winamp_id': 5}, + 'comment': {'name': 'comment', 'type': 3, 'winamp_id': 6}, + 'album_artist': {'name': 'albumartist', 'type': 3, 'winamp_id': 20}, + 'composer': {'name': 'composer', 'type': 3, 'winamp_id': 24}, + 'publisher': {'name': 'publisher', 'type': 3, 'winamp_id': 23}, + 'category': {'name': 'category', 'type': 3, 'winamp_id': 34}, + 'track': {'name': 'trackno', 'type': 4, 'winamp_id': 7}, + 'length': {'name': 'length', 'type': 11, 'winamp_id': 8}, + 'type': {'name': 'type', 'type': 4, 'winamp_id': 9}, + 'last_update': {'name': 'lastupd', 'type': 10, 'winamp_id': 10}, + 'last_play': {'name': 'lastplay', 'type': 10, 'winamp_id': 11}, + 'rating': {'name': 'rating', 'type': 4, 'winamp_id': 12}, + 'play_count': {'name': 'playcount', 'type': 4, 'winamp_id': 15}, + 'file_time': {'name': 'filetime', 'type': 10, 'winamp_id': 16}, + 'file_size': {'name': 'filesize', 'type': 4, 'winamp_id': 17}, + 'bit_rate': {'name': 'bitrate', 'type': 4, 'winamp_id': 18}, + 'disc': {'name': 'disc', 'type': 4, 'winamp_id': 19}, + 'replaygain_album_gain': {'name': 'replaygain_album_gain', 'type': 3, 'winamp_id': 21}, + 'replaygain_track_gain': {'name': 'replaygain_track_gain', 'type': 3, 'winamp_id': 22}, + 'bpm': {'name': 'bpm', 'type': 4, 'winamp_id': 25}, + 'discs': {'name': 'discs', 'type': 4, 'winamp_id': 26}, + 'tracks': {'name': 'tracks', 'type': 4, 'winamp_id': 27}, + 'is_podcast': {'name': 'ispodcast', 'type': 4, 'winamp_id': 28}, + 'podcast_channel': {'name': 'podcastchannel', 'type': 3, 'winamp_id': 29}, + 'podcast_pub_date': {'name': 'podcastpubdate', 'type': 10, 'winamp_id': 30}, + 'gracenote_file_id': {'name': 'GracenoteFileID', 'type': 3, 'winamp_id': 31}, + 'gracenote_ext_data': {'name': 'GracenoteExtData', 'type': 3, 'winamp_id': 32}, + 'lossless': {'name': 'lossless', 'type': 4, 'winamp_id': 33}, + 'codec': {'name': 'codec', 'type': 3, 'winamp_id': 35}, + 'director': {'name': 'director', 'type': 3, 'winamp_id': 36}, + 'producer': {'name': 'producer', 'type': 3, 'winamp_id': 37}, + 'width': {'name': 'width', 'type': 4, 'winamp_id': 38}, + 'height': {'name': 'height', 'type': 4, 'winamp_id': 39}, + 'tuid2': {'name': 'tuid2', 'type': 3, 'winamp_id': 14}, + } + field_decode = dict((v['name'], k) for k, v in field_encode.iteritems()) + medias = [] + + #def __init__(self): + # self.base_dir = base_dir + + def files_from_path(self, path): + path = os.path.normpath(path) + if not os.path.isdir(path): + path = os.path.dirname(path) + idxfile = os.path.join(path, 'main.idx') + datfile = os.path.join(path, 'main.dat') + return idxfile, datfile + + def read(self, path): + self.fields = {} + idxfile, datfile = self.files_from_path(path) + if not os.path.isfile(idxfile): + raise IOError('Could not find Winamp library index file ' + idxfile) + if not os.path.isfile(idxfile): + raise IOError('Could not find Winamp library data file ' + datfile) + + idx = open(idxfile, 'rb') + if idx.read(8) != 'NDEINDEX': + idx.close() + raise Exception(idxfile + ' does not appear to be a valid Winamp library index file') + num_records, = struct.unpack('<i', idx.read(4)) + idx.read(4) # no one seems to know what these four bytes are for + dat = open(datfile, 'rb') + for _ in range(0, num_records): + data = idx.read(8) + if len(data) < 8: + break + offset, media_id = struct.unpack('ii', data) + print offset, media_id + media = self.read_media(dat, offset) + if media is not None: + self.medias.append(media) + idx.close() + dat.close() + + def write(self, path): + fields_sorted = sorted(self.field_encode.itervalues(), key = lambda field: field['winamp_id']) + self.fields = list(self.field_decode[i['name']] for i in fields_sorted) + + idxfile, datfile = self.files_from_path(path) + idx = open(idxfile, 'wb') + dat = open(datfile, 'wb') + idx.write('NDEINDEX') + dat.write('NDETABLE') + self.last_write_offset = 0 + self.current_write_offset = 8 + # Number of actual media files, plus the column and index records + num_records = len(self.medias) + 2 + # Write the number of records plus four mystery bytes to the index + idx.write(struct.pack('<i', num_records) + '\xFF\x00\x00\x00') + + self.write_column_record(dat) + self.write_mystery_record(dat) + + for media in self.medias: + self.write_media_record(dat, media) + + def write_column_record(self, dat): + position = 'first' + n = 0 + for field in self.fields: + if n == len(self.fields) - 1: + position = 'last' + self.write_field(dat, field, self.field_encode[field]['name'], position, header_record = True) + n += 1 + if n == 1: + position = 'middle' + + # Who knows what this is for? + def write_mystery_record(self, dat): + data_packed = '\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x04\x4E\x6F\x6E\x65\xAE' + offset_next = self.current_write_offset + len(data_packed) + 14 + header = struct.pack('<BBiii', 255, 1, len(data_packed), offset_next, 0) + + write_data = header + data_packed + dat.write(write_data) + + self.last_write_offset = self.current_write_offset + self.current_write_offset += len(write_data) + + data_packed = '\x00\x00\x00\x00\x0C\x00\x00\x00\x08\x66\x69\x6C\x65\x6E\x61\x6D\x65\x00' + offset_prev = self.last_write_offset + header = struct.pack('<BBiii', 0, 1, len(data_packed), 0, offset_prev) + + write_data = header + data_packed + dat.write(write_data) + + self.last_write_offset = self.current_write_offset + self.current_write_offset += len(write_data) + + def write_media_record(self, dat, media): + position = 'first' + n = 0 + for field in self.fields: + if n == len(self.fields) - 1: + position = 'last' + self.write_field(dat, field, getattr(media, field), position) + n += 1 + if n == 1: + position = 'middle' + + def write_field(self, dat, field, data, position, header_record = False): + if data is None: + return + + field_id = self.field_encode[field]['winamp_id'] + data_type = self.field_encode[field]['type'] + + if header_record: + data_packed = struct.pack('<BBB', data_type, 0, len(data)) + data_packed += data + data_type = 0 + else: + # String field + if data_type == 3 or data_type == 12: + data_encoded = data.encode('utf-16') + data_packed = struct.pack('<H', len(data_encoded)) + data_encoded + + # Integer field + elif data_type == 4 or data_type == 11: + data_packed = struct.pack('<i', data) + + # Date field + elif data_type == 10: + timestamp = int(time.mktime(data.timetuple())) + data_packed = struct.pack('<i', timestamp) + + else: + print 'Warning: unsupported data type ' + str(data_type) + ' for field ' + field + + offset_prev = self.last_write_offset + offset_next = self.current_write_offset + len(data_packed) + 14 + if position == 'first': + offset_prev = 0 + elif position == 'last': + offset_next = 0 + + header = struct.pack('<BBiii', field_id, data_type, len(data_packed), offset_next, offset_prev) + write_data = header + data_packed + dat.write(write_data) + + self.last_write_offset = self.current_write_offset + self.current_write_offset += len(write_data) + + def read_media(self, dat, offset): + dat.seek(offset) + media = Media.Media() + while True: + field_id, data_type, size, offset_next, offset_prev = struct.unpack('<BBiii', dat.read(14)) + print 'data', field_id, data_type, size, offset_next, offset_prev + # Column field + if data_type == 0: + # The column name is always a string, and the middle byte appears to be unused + _, _, data_size = struct.unpack('<BBB', dat.read(3)) + field_name = dat.read(data_size) + if field_name in self.field_decode: + self.fields[field_id] = field_name + else: + print 'Warning: unknown field name ' + field_name + + elif field_id in self.fields: + prop = self.field_decode[self.fields[field_id]] + value = None + + # String field + if data_type == 3 or data_type == 12: + data_size, = struct.unpack('<H', dat.read(2)) + data = dat.read(data_size) + enc = 'utf-16' if data.find('\xff\xfe') == 0 else 'ascii' + value = unicode(data, encoding=enc, errors='ignore') + + # Integer field + elif data_type == 4 or data_type == 11: + value, = struct.unpack('<i', dat.read(4)) + + # Date field + elif data_type == 10: + timestamp, = struct.unpack('<i', dat.read(4)) + value = datetime.fromtimestamp(timestamp) + + # Index field? Not actually a media + elif data_type == 1: + return + + else: + print 'Warning: unsupported data type ' + str(data_type) + ' for field ' + self.fields[field_id] + + if value is not None: + setattr(media, prop, value) + + else: + print '?' + str(field_id) + if offset_next == 0: + break + dat.seek(offset_next) + + if data_type > 1: # not column or index field + return media + +lib = WinampLibrary() +lib.read('C:\\Users\\Brad\\AppData\\Roaming\\Winamp\\Plugins\\ml') +lib.write('C:\\Users\\Brad\\AppData\\Roaming\\Winamp\\Plugins\\ml\\out')