]> code.delx.au - notipod/blob - itunes.py
Fixes
[notipod] / itunes.py
1 #!/usr/bin/env python
2 # Copyright 2009 James Bunton <jamesbunton@fastmail.fm>
3 # Licensed for distribution under the GPL version 2, check COPYING for details
4
5 import logging
6 import os
7 import shutil
8 import urllib
9
10 from Foundation import *
11
12
13 def read_plist(filename):
14 try:
15 data = buffer(open(filename).read())
16 except IOError:
17 return None
18 plist, fmt, err = NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(data, NSPropertyListMutableContainers, None, None)
19 if err is not None:
20 errStr = err.encode("utf-8")
21 err.release() # Doesn't follow Cocoa conventions for some reason
22 raise TypeError(errStr)
23 return plist
24
25 class Playlist(object):
26 def __init__(self, name, tracks, parent=None):
27 self.name = name
28 self.children = []
29 self.tracks = tracks
30 if parent is not None:
31 parent.children.append(self)
32
33 class Library(NSObject):
34 def init(self):
35 return self.initWithFilename_("~/Music/iTunes/iTunes Music Library.xml")
36
37 def initWithFilename_(self, filename):
38 filename = os.path.expanduser(filename)
39 plist = read_plist(os.path.expanduser(filename))
40 self.folder = self.loc2name(plist["Music Folder"])
41 pl_tracks = plist["Tracks"]
42 self.playlists = []
43 for pl_playlist in plist["Playlists"]:
44 self.playlists.append(self.make_playlist(pl_playlist, pl_tracks))
45 return self
46
47 def loc2name(self, location):
48 return urllib.splithost(urllib.splittype(urllib.unquote(location))[1])[1]
49
50 def make_playlist(self, pl_playlist, pl_tracks):
51 name = pl_playlist["Name"]
52 tracks = []
53 for item in pl_playlist.get("Playlist Items", []):
54 trackID = item["Track ID"]
55 filename = str(pl_tracks[str(trackID)]["Location"])
56 filename = self.loc2name(filename)
57 filename = filename[len(self.folder):]
58 filename = eval(repr(filename).lstrip("u")).decode("utf-8")
59 tracks.append(filename)
60 return Playlist(name, tracks)
61
62 def has_playlist(self, playlist):
63 for p in self.playlists:
64 if p.name == playlist:
65 return True
66 return False
67
68 def get_playlist(self, name):
69 playlist = [p for p in self.playlists if p.name == name][0]
70 return playlist.tracks
71
72 def list_playlists(self):
73 return [p.name for p in self.playlists]
74
75 def outlineView_numberOfChildrenOfItem_(self, view, item):
76 if item == None:
77 return len(self.playlists)
78 else:
79 return 0
80
81 def outlineView_isItemExpandable_(self, view, item):
82 return False
83
84 def outlineView_child_ofItem_(self, view, index, item):
85 if item == None:
86 return self.playlists[index]
87 else:
88 return None
89
90 def outlineView_objectValueForTableColumn_byItem_(self, view, column, item):
91 return item.name
92
93
94 def export_m3u(dry_run, dest, drive_letter, music_dir, playlist_name, files):
95 if dry_run:
96 return
97 f = open(os.path.join(dest, playlist_name) + ".m3u", "w")
98 for filename in files:
99 filename = filename.replace("/", "\\").encode("utf-8")
100 f.write("%s:\\%s\\%s\n" % (drive_letter, music_dir, filename))
101 f.close()
102
103 def strip_prefix(s, prefix):
104 assert s.startswith(prefix)
105 s = s[len(prefix):]
106 if s.startswith("/"):
107 s = s[1:]
108 return s
109
110 def mkdirhier(path):
111 if os.path.isdir(path):
112 return
113 paths = [path]
114 while path != "/":
115 path = os.path.split(path)[0]
116 paths.append(path)
117 for path in reversed(paths):
118 try:
119 os.mkdir(path)
120 except OSError:
121 pass
122
123 def sync(dry_run, source, dest, files):
124 join = os.path.join
125
126 logging.info("Calculating files to sync and deleting old files")
127 files = set(files)
128 for dirpath, dirnames, filenames in os.walk(dest):
129 full_dirpath = dirpath
130 dirpath = strip_prefix(dirpath, dest)
131
132 for filename in filenames:
133 filename = join(dirpath, filename).decode("utf-8")
134
135 # Whenever 'file' is deleted OSX will helpfully remove '._file'
136 if not os.path.exists(join(dest, filename)):
137 continue
138
139 if filename in files:
140 sourcestat = os.stat(join(source, filename))
141 deststat = os.stat(join(dest, filename))
142 same_time = abs(sourcestat.st_mtime - deststat.st_mtime) < 5
143 same_size = sourcestat.st_size == deststat.st_size
144 if same_time and same_size:
145 files.remove(filename)
146 logging.debug("keep: " + filename)
147 else:
148 logging.debug("update: " + filename)
149
150 elif not filename.startswith("Playlists/"):
151 logging.debug("delete: " + filename)
152 if not dry_run:
153 os.unlink(join(dest, filename))
154
155 if len(os.listdir(full_dirpath)) == 0:
156 logging.debug("rmdir: " + dirpath)
157 if not dry_run:
158 os.rmdir(full_dirpath)
159
160
161 logging.info("Copying new files")
162 files = list(files)
163 files.sort()
164 for filename in files:
165 logging.debug("copy: " + filename)
166 if not dry_run:
167 mkdirhier(os.path.dirname(join(dest, filename)))
168 shutil.copy2(join(source, filename), join(dest, filename))
169
170