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