283a547782f86e5a38294e95515dc38d7ad6e523
[notipod] / notipod_gui.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 sys
8 import traceback
9 import uuid
10
11 import objc
12 from Foundation import *
13 from AppKit import *
14 from PyObjCTools import AppHelper
15
16 import libnotipod
17
18
19 class PlaylistModel(NSObject):
20 outlineView = objc.IBOutlet()
21
22 def awakeFromNib(self):
23 self.root = []
24 self.playlists = {}
25 self.outlineView.setDataSource_(self)
26 self.outlineView.setEnabled_(False)
27
28 def setPlaylists(self, playlists):
29 self.root = []
30 self.playlists = playlists
31 for playlist in self.playlists:
32 if playlist.parent is None:
33 self.root.append(playlist)
34 self.outlineView.reloadData()
35 self.outlineView.expandItem_expandChildren_(None, True)
36
37 def outlineView_child_ofItem_(self, _, childIndex, playlist):
38 if playlist == None:
39 return self.root[childIndex]
40 else:
41 return playlist.children[childIndex]
42
43 def outlineView_isItemExpandable_(self, _, playlist):
44 if playlist == None:
45 return True
46 else:
47 return len(playlist.children) > 0
48
49 def outlineView_numberOfChildrenOfItem_(self, _, playlist):
50 if playlist == None:
51 return len(self.root)
52 else:
53 return len(playlist.children)
54
55 def outlineView_objectValueForTableColumn_byItem_(self, _, col, playlist):
56 if not col:
57 return
58 col = col.identifier()
59
60 if col == "selected":
61 selected = NSApp.delegate().playlists()
62 return playlist.pid in selected
63 if col == "icon":
64 return NSImage.imageNamed_("playlist-" + playlist.ptype)
65 if col == "playlist":
66 return playlist.name
67
68 def outlineView_setObjectValue_forTableColumn_byItem_(self, _, v, col, playlist):
69 if not col:
70 return
71 col = col.identifier()
72
73
74 if col != "selected":
75 return
76 NSApp.delegate().setPlaylist_selected_(playlist.pid, v)
77
78
79 class FolderModel(NSObject):
80 window = objc.IBOutlet()
81 folderPopup = objc.IBOutlet()
82
83 def loadFolders_(self, folders):
84 self.folderPopup.addItemsWithTitles_(folders)
85 if len(folders) > 0:
86 self.folderPopup.selectItemAtIndex_(2)
87 self.lastIndex = 2
88 else:
89 self.lastIndex = 0
90
91 @objc.IBAction
92 def doSelectFolder_(self, sender):
93 currentIndex = self.folderPopup.indexOfSelectedItem()
94 if currentIndex >= 2:
95 self.lastIndex = currentIndex
96 NSApp.delegate().setFolder_(self.folderPopup.titleOfSelectedItem())
97 return
98 panel = NSOpenPanel.openPanel()
99 panel.setCanChooseFiles_(False)
100 panel.setCanChooseDirectories_(True)
101 panel.setCanCreateDirectories_(True)
102 panel.setAllowsMultipleSelection_(False)
103 panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
104 None, None, [], self.window, self, self.selectFolderEnd_returnCode_contextInfo_, None)
105
106 @objc.signature("v@:@ii")
107 def selectFolderEnd_returnCode_contextInfo_(self, panel, ret, _):
108 if ret == NSOKButton:
109 assert len(panel.filenames()) == 1
110 folder = panel.filenames()[0]
111 NSApp.delegate().setFolder_(folder)
112 self.folderPopup.insertItemWithTitle_atIndex_(folder, 2)
113 self.folderPopup.selectItemAtIndex_(2)
114 else:
115 self.folderPopup.selectItemAtIndex_(self.lastIndex)
116
117
118 class NotiPodController(NSObject):
119 window = objc.IBOutlet()
120
121 loadingSheet = objc.IBOutlet()
122 loadingLabel = objc.IBOutlet()
123 loadingIndicator = objc.IBOutlet()
124
125 previewWindow = objc.IBOutlet()
126 previewText = objc.IBOutlet()
127
128 playlistModel = objc.IBOutlet()
129 folderModel = objc.IBOutlet()
130
131
132 def awakeFromNib(self):
133 self.runningGenerator = False
134
135 # Delegate methods
136 def applicationWillFinishLaunching_(self, _):
137 pass
138
139 def applicationDidFinishLaunching_(self, _):
140 self._loadPrefs()
141
142 folders = []
143 for target in self.targets:
144 folders.append(target["folder"])
145 self.folderModel.loadFolders_(folders)
146
147 self.library = libnotipod.ITunesLibrary.alloc().init()
148 self.loadLibrary()
149
150 def applicationWillTerminate_(self, _):
151 self.prefs().synchronize()
152
153 def applicationShouldTerminateAfterLastWindowClosed_(self, _):
154 return True
155
156 def windowDidBecomeKey_(self, _):
157 if self.library.needs_reload():
158 print "needs reload!"
159 self.loadLibrary()
160 else:
161 print "no reloading this time"
162
163
164 # Utility methods
165 def loadLibrary(self):
166 if self.runningGenerator:
167 return
168
169 def finish():
170 self.playlistModel.setPlaylists(self.library.get_playlists())
171 def fail():
172 NSRunAlertPanel("Error!", "Unable to load iTunes library! Exiting...", "Ok", None, None)
173 sys.exit(0)
174 self.runGenerator(lambda: self.library.load_(None), finish, fail)
175
176 def runGenerator(self, func, finish, fail):
177 assert not self.runningGenerator
178 self.runningGenerator = True
179 self.loadingIndicator.startAnimation_(self)
180 NSApp.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self.loadingSheet, self.window, None, None, None)
181 arg = (func(), finish, fail)
182 self.performSelectorInBackground_withObject_(self.runGeneratorThread, arg)
183
184 def runGeneratorThread(self, (gen, finish, fail)):
185 pool = NSAutoreleasePool.alloc().init()
186 try:
187 for msg in gen:
188 if not self.runningGenerator:
189 break
190 self.loadingLabel.performSelectorOnMainThread_withObject_waitUntilDone_(
191 self.loadingLabel.setStringValue_, msg, True)
192 except Exception, e:
193 NSRunAlertPanel("Error!", str(e), "Ok", None, None)
194 traceback.print_exc()
195 finish = fail
196 self.performSelectorOnMainThread_withObject_waitUntilDone_(
197 self.stopGenerator, finish, True)
198 self.runningGenerator = False
199
200 def stopGenerator(self, finish):
201 self.runningGenerator = False
202 NSApp.endSheet_(self.loadingSheet)
203 self.loadingSheet.orderOut_(self)
204 self.loadingIndicator.stopAnimation_(self)
205 if finish:
206 finish()
207
208 @objc.IBAction
209 def doCancel_(self, sender):
210 self.runningGenerator = False
211
212 def getDestFolder(self):
213 target = self.getCurrentTarget()
214 if not target:
215 NSRunAlertPanel("Error!", "You must choose a folder first!", "Ok", None, None)
216 return
217 folder = target["folder"]
218 if not os.path.isdir(folder.encode("utf-8")):
219 NSRunAlertPanel("Error!", "Destination " + folder + " does not exist, try mounting it first?", "Ok", None, None)
220 return
221 return folder
222
223 def doPreviewThread(self):
224 yield "Calculating changes..."
225
226 folder = self.getDestFolder()
227 if not folder:
228 return
229
230 all_tracks = set()
231 for playlist_id in self.playlists():
232 playlist = self.library.get_playlist_pid(playlist_id)
233 if playlist is not None:
234 all_tracks.update(set(playlist.tracks))
235
236 all_filenames = []
237 for trackID in all_tracks:
238 all_filenames.append(self.library.get_track_filename(trackID))
239
240 gen = libnotipod.sync(
241 dry_run=True,
242 source=self.library.folder,
243 dest=folder,
244 files_to_copy=all_filenames,
245 )
246 self.previewResult = "\n".join(gen)
247
248 @objc.IBAction
249 def doPreview_(self, sender):
250 self.previewResult = ""
251 self.previewWindow.orderOut_(self)
252
253 def finish():
254 self.previewText.textStorage().mutableString().setString_(self.previewResult)
255 self.previewWindow.center()
256 self.previewWindow.makeKeyAndOrderFront_(self)
257
258 self.runGenerator(self.doPreviewThread, finish, None)
259
260 @objc.IBAction
261 def doSync_(self, sender):
262 folder = self.getDestFolder()
263 if not folder:
264 return
265
266 all_tracks = set()
267 orig_playlists = set(self.playlists())
268 all_playlists = orig_playlists.copy()
269 for playlist_id in all_playlists:
270 playlist = self.library.get_playlist_pid(playlist_id)
271 if playlist is None:
272 print "Forgetting unknown playlist:", playlist_id
273 self.setPlaylist_selected_(playlist_id, False)
274 continue
275 all_tracks.update(set(playlist.tracks))
276
277 all_filenames = []
278 for trackID in all_tracks:
279 all_filenames.append(self.library.get_track_filename(trackID))
280 all_playlists.update(self.library.get_track_playlists(trackID))
281
282 for playlist_id in all_playlists:
283 playlist = self.library.get_playlist_pid(playlist_id)
284 if playlist is None:
285 continue
286 tracks = []
287 for trackID in playlist.tracks:
288 if trackID in all_tracks:
289 tracks.append(self.library.get_track_filename(trackID))
290 if playlist_id not in orig_playlists and len(tracks) < 10:
291 continue
292 libnotipod.export_m3u(dry_run=False, dest=folder, path_prefix="",
293 playlist_name=playlist.name, files=tracks)
294
295 def finish():
296 NSRunAlertPanel("Complete!", "Synchronisation is complete", "Ok", None, None)
297 self.runGenerator(
298 lambda:
299 libnotipod.sync(
300 dry_run=False,
301 source=self.library.folder,
302 dest=folder,
303 files_to_copy=all_filenames,
304 )
305 ,
306 finish,
307 None
308 )
309
310
311 # Preferences
312
313 def prefs(self):
314 return NSUserDefaults.standardUserDefaults()
315
316 def _migratePrefs(self):
317 p = self.prefs()
318
319 playlists = p.stringArrayForKey_("playlists")
320 if playlists is not None:
321 p.removeObjectForKey_("playlists")
322 else:
323 playlists = []
324
325 folders = p.stringArrayForKey_("folders")
326 if not folders:
327 return
328 p.removeObjectForKey_("folders")
329
330 first = True
331 for f in folders:
332 target = {}
333 target["folder"] = f
334 target["playlists"] = list(playlists)
335 target["uuid"] = uuid.uuid1().get_hex()
336 if first:
337 first = False
338 self.setCurrentTarget_(target["uuid"])
339 self.targets.addObject_(target)
340
341 self._savePrefs()
342
343 def _loadPrefs(self):
344 p = self.prefs()
345
346 self.currentTarget = None
347 self.setCurrentTarget_(p.stringForKey_("currentTarget"))
348
349 self.targets = self.prefs().arrayForKey_("targets")
350 if self.targets is None:
351 self.targets = NSMutableArray.array()
352 else:
353 self.targets = NSMutableArray.arrayWithArray_(self.targets)
354
355 if self.getCurrentTarget() is None:
356 self._migratePrefs()
357
358 def _savePrefs(self):
359 p = self.prefs()
360 p.setObject_forKey_(self.currentTarget, "currentTarget")
361 p.setObject_forKey_(self.targets, "targets")
362 p.synchronize()
363
364 def getCurrentTarget(self):
365 for target in self.targets:
366 if target["uuid"] == self.currentTarget:
367 return target
368 return None
369
370 def setCurrentTarget_(self, targetUuid):
371 oldUuid = self.currentTarget
372 self.currentTarget = targetUuid
373 if oldUuid is None and targetUuid is not None:
374 self.playlistModel.outlineView.setEnabled_(True)
375 if oldUuid != targetUuid:
376 self.playlistModel.outlineView.reloadItem_reloadChildren_(None, True)
377
378 def playlists(self):
379 target = self.getCurrentTarget()
380 if not target:
381 return []
382 return list(target["playlists"])
383
384 def setFolder_(self, folder):
385 for i, target in enumerate(self.targets):
386 if target["folder"] == folder:
387 self.targets.removeObjectAtIndex_(i)
388 self.targets.insertObject_atIndex_(target, 0)
389 break
390 else:
391 target = {}
392 target["folder"] = folder
393 target["playlists"] = self.playlists()
394 target["uuid"] = uuid.uuid1().get_hex()
395 self.targets.insertObject_atIndex_(target, 0)
396
397 self.setCurrentTarget_(target["uuid"])
398
399 self._savePrefs()
400
401 def setPlaylist_selected_(self, playlist, selected):
402 target = self.getCurrentTarget()
403 if not target:
404 raise AssertionError("No target selected when editing playlists")
405
406 playlists = target["playlists"]
407 if selected:
408 playlists.append(playlist)
409 else:
410 playlists.remove(playlist)
411 target["playlists"] = list(set(playlists))
412
413 self._savePrefs()
414
415
416 def main():
417 ### logging.basicConfig(format="%(levelname)s: %(message)s")
418 ### logging.getLogger().setLevel(logging.DEBUG)
419 AppHelper.runEventLoop()
420
421 if __name__ == "__main__":
422 main()
423