Added advanced options sheet
[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 advancedSheet = objc.IBOutlet()
126 advancedSyncFolder = objc.IBOutlet()
127 advancedPathPrefix = objc.IBOutlet()
128
129 previewWindow = objc.IBOutlet()
130 previewText = objc.IBOutlet()
131
132 playlistModel = objc.IBOutlet()
133 folderModel = objc.IBOutlet()
134
135
136 def awakeFromNib(self):
137 self.runningGenerator = False
138
139 # Delegate methods
140 def applicationWillFinishLaunching_(self, _):
141 pass
142
143 def applicationDidFinishLaunching_(self, _):
144 self._loadPrefs()
145
146 folders = []
147 for target in self.targets:
148 folders.append(target["folder"])
149 self.folderModel.loadFolders_(folders)
150
151 self.library = libnotipod.ITunesLibrary.alloc().init()
152 self.loadLibrary_(self)
153
154 def applicationWillTerminate_(self, _):
155 self.prefs().synchronize()
156
157 def applicationShouldTerminateAfterLastWindowClosed_(self, _):
158 return True
159
160 def windowDidBecomeKey_(self, _):
161 if self.library.needs_reload():
162 self.loadLibrary_(self)
163
164
165 # Utility methods
166 def runGenerator(self, func, finish, fail):
167 assert not self.runningGenerator
168 self.runningGenerator = True
169 self.loadingIndicator.startAnimation_(self)
170 NSApp.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self.loadingSheet, self.window, None, None, None)
171 arg = (func(), finish, fail)
172 self.performSelectorInBackground_withObject_(self.runGeneratorThread, arg)
173
174 def runGeneratorThread(self, (gen, finish, fail)):
175 pool = NSAutoreleasePool.alloc().init()
176 try:
177 for msg in gen:
178 if not self.runningGenerator:
179 break
180 self.loadingLabel.performSelectorOnMainThread_withObject_waitUntilDone_(
181 self.loadingLabel.setStringValue_, msg, True)
182 except Exception, e:
183 NSRunAlertPanel("Error!", str(e), "Ok", None, None)
184 traceback.print_exc()
185 finish = fail
186 self.performSelectorOnMainThread_withObject_waitUntilDone_(
187 self.stopGenerator, finish, True)
188 self.runningGenerator = False
189
190 def stopGenerator(self, finish):
191 self.runningGenerator = False
192 NSApp.endSheet_(self.loadingSheet)
193 self.loadingSheet.orderOut_(self)
194 self.loadingIndicator.stopAnimation_(self)
195 if finish:
196 finish()
197
198
199 @objc.IBAction
200 def loadLibrary_(self, sender):
201 if self.runningGenerator:
202 return
203
204 def finish():
205 self.playlistModel.setPlaylists(self.library.get_playlists())
206 def fail():
207 NSRunAlertPanel("Error!", "Unable to load iTunes library! Exiting...", "Ok", None, None)
208 sys.exit(0)
209 self.runGenerator(lambda: self.library.load_(None), finish, fail)
210
211 @objc.IBAction
212 def showAdvancedOptions_(self, sender):
213 if self.runningGenerator:
214 return
215 target = self.getCurrentTarget()
216 self.advancedSyncFolder.setStringValue_(target["folder"])
217 self.advancedPathPrefix.setStringValue_(target["path_prefix"])
218 NSApp.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self.advancedSheet, self.window, None, None, None)
219
220 @objc.IBAction
221 def finishAdvancedOptions_(self, sender):
222 target = self.getCurrentTarget()
223 target["folder"] = self.advancedSyncFolder.stringValue()
224 target["path_prefix"] = self.advancedPathPrefix.stringValue()
225 self._savePrefs()
226 NSApp.endSheet_(self.advancedSheet)
227 self.advancedSheet.orderOut_(self)
228
229 @objc.IBAction
230 def doCancel_(self, sender):
231 self.runningGenerator = False
232
233 def getCheckTarget(self):
234 target = self.getCurrentTarget()
235 if not target:
236 NSRunAlertPanel("Error!", "You must choose a folder first!", "Ok", None, None)
237 return
238 folder = target["folder"]
239 if not os.path.isdir(folder.encode("utf-8")):
240 NSRunAlertPanel("Error!", "Destination " + folder + " does not exist, try mounting it first?", "Ok", None, None)
241 return
242 target["folder"] = folder
243 target["path_prefix"] = target["path_prefix"].encode("utf-8")
244 return target
245
246 def doPreviewThread(self):
247 yield "Calculating changes..."
248
249 target = self.getCheckTarget()
250 if not target:
251 return
252
253 all_tracks = set()
254 for playlist_id in self.playlists():
255 playlist = self.library.get_playlist_pid(playlist_id)
256 if playlist is not None:
257 all_tracks.update(set(playlist.tracks))
258
259 all_filenames = []
260 for trackID in all_tracks:
261 all_filenames.append(self.library.get_track_filename(trackID))
262
263 gen = libnotipod.sync(
264 dry_run=True,
265 source=self.library.folder,
266 dest=target["folder"],
267 files_to_copy=all_filenames,
268 )
269 self.previewResult = "\n".join(gen)
270
271 @objc.IBAction
272 def doPreview_(self, sender):
273 self.previewResult = ""
274 self.previewWindow.orderOut_(self)
275
276 def finish():
277 self.previewText.textStorage().mutableString().setString_(self.previewResult)
278 self.previewWindow.center()
279 self.previewWindow.makeKeyAndOrderFront_(self)
280
281 self.runGenerator(self.doPreviewThread, finish, None)
282
283 @objc.IBAction
284 def doSync_(self, sender):
285 target = self.getCheckTarget()
286 if not target:
287 return
288
289 all_tracks = set()
290 orig_playlists = set(self.playlists())
291 all_playlists = orig_playlists.copy()
292 for playlist_id in all_playlists:
293 playlist = self.library.get_playlist_pid(playlist_id)
294 if playlist is None:
295 print "Forgetting unknown playlist:", playlist_id
296 self.setPlaylist_selected_(playlist_id, False)
297 continue
298 all_tracks.update(set(playlist.tracks))
299
300 all_filenames = []
301 for trackID in all_tracks:
302 all_filenames.append(self.library.get_track_filename(trackID))
303 all_playlists.update(self.library.get_track_playlists(trackID))
304
305 for playlist_id in all_playlists:
306 playlist = self.library.get_playlist_pid(playlist_id)
307 if playlist is None:
308 continue
309 tracks = []
310 for trackID in playlist.tracks:
311 if trackID in all_tracks:
312 tracks.append(self.library.get_track_filename(trackID))
313 if playlist_id not in orig_playlists and len(tracks) < 10:
314 continue
315 libnotipod.export_m3u(
316 dry_run=False,
317 dest=target["folder"],
318 path_prefix=target["path_prefix"],
319 playlist_name=playlist.name,
320 files=tracks
321 )
322
323 def finish():
324 NSRunAlertPanel("Complete!", "Synchronisation is complete", "Ok", None, None)
325 self.runGenerator(
326 lambda:
327 libnotipod.sync(
328 dry_run=False,
329 source=self.library.folder,
330 dest=target["folder"],
331 files_to_copy=all_filenames,
332 )
333 ,
334 finish,
335 None
336 )
337
338
339 # Preferences
340
341 def prefs(self):
342 return NSUserDefaults.standardUserDefaults()
343
344 def _migratePrefs(self):
345 p = self.prefs()
346
347 playlists = p.stringArrayForKey_("playlists")
348 if playlists is not None:
349 p.removeObjectForKey_("playlists")
350 else:
351 playlists = []
352
353 folders = p.stringArrayForKey_("folders")
354 if not folders:
355 return
356 p.removeObjectForKey_("folders")
357
358 first = True
359 for f in folders:
360 target = {}
361 target["folder"] = f
362 target["playlists"] = list(playlists)
363 target["uuid"] = uuid.uuid1().get_hex()
364 target["path_prefix"] = "../"
365 if first:
366 first = False
367 self.setCurrentTarget_(target["uuid"])
368 self.targets.addObject_(target)
369
370 self._savePrefs()
371
372 def _loadPrefs(self):
373 p = self.prefs()
374
375 self.currentTarget = None
376 self.setCurrentTarget_(p.stringForKey_("currentTarget"))
377
378 self.targets = self.prefs().arrayForKey_("targets")
379 if self.targets is None:
380 self.targets = NSMutableArray.array()
381 else:
382 self.targets = NSMutableArray.arrayWithArray_(self.targets)
383
384 if self.getCurrentTarget() is None:
385 self._migratePrefs()
386
387 def _savePrefs(self):
388 p = self.prefs()
389 p.setObject_forKey_(self.currentTarget, "currentTarget")
390 p.setObject_forKey_(self.targets, "targets")
391 p.synchronize()
392
393 def getCurrentTarget(self):
394 for target in self.targets:
395 if target["uuid"] == self.currentTarget:
396 return target
397 return None
398
399 def setCurrentTarget_(self, targetUuid):
400 oldUuid = self.currentTarget
401 self.currentTarget = targetUuid
402 if oldUuid is None and targetUuid is not None:
403 self.playlistModel.outlineView.setEnabled_(True)
404 if oldUuid != targetUuid:
405 self.playlistModel.outlineView.reloadItem_reloadChildren_(None, True)
406
407 def playlists(self):
408 target = self.getCurrentTarget()
409 if not target:
410 return []
411 return list(target["playlists"])
412
413 def setFolder_(self, folder):
414 for i, target in enumerate(self.targets):
415 if target["folder"] == folder:
416 self.targets.removeObjectAtIndex_(i)
417 self.targets.insertObject_atIndex_(target, 0)
418 break
419 else:
420 target = {}
421 target["folder"] = folder
422 target["playlists"] = self.playlists()
423 target["uuid"] = uuid.uuid1().get_hex()
424 target["path_prefix"] = "../"
425 self.targets.insertObject_atIndex_(target, 0)
426
427 self.setCurrentTarget_(target["uuid"])
428
429 self._savePrefs()
430
431 def setPlaylist_selected_(self, playlist, selected):
432 target = self.getCurrentTarget()
433 if not target:
434 raise AssertionError("No target selected when editing playlists")
435
436 playlists = target["playlists"]
437 if selected:
438 playlists.append(playlist)
439 else:
440 playlists.remove(playlist)
441 target["playlists"] = list(set(playlists))
442
443 self._savePrefs()
444
445
446 def main():
447 ### logging.basicConfig(format="%(levelname)s: %(message)s")
448 ### logging.getLogger().setLevel(logging.DEBUG)
449 AppHelper.runEventLoop()
450
451 if __name__ == "__main__":
452 main()
453