X-Git-Url: https://code.delx.au/notipod/blobdiff_plain/191063847cdbe03cb41e82c2b194fe48395a4b66..940330ce5a487a919446ee62f920646c2e38fee5:/notipod_gui.py diff --git a/notipod_gui.py b/notipod_gui.py index 084b1d7..11c6880 100644 --- a/notipod_gui.py +++ b/notipod_gui.py @@ -3,6 +3,11 @@ # Licensed for distribution under the GPL version 2, check COPYING for details import logging +import os +import sys +import time +import traceback +import uuid import objc from Foundation import * @@ -19,6 +24,7 @@ class PlaylistModel(NSObject): self.root = [] self.playlists = {} self.outlineView.setDataSource_(self) + self.outlineView.setEnabled_(False) def setPlaylists(self, playlists): self.root = [] @@ -48,16 +54,23 @@ class PlaylistModel(NSObject): return len(playlist.children) def outlineView_objectValueForTableColumn_byItem_(self, _, col, playlist): - col = col.identifier() if col else "playlist" + if not col: + return + col = col.identifier() if col == "selected": selected = NSApp.delegate().playlists() return playlist.pid in selected - if col == None or col == "playlist": + if col == "icon": + return NSImage.imageNamed_("playlist-" + playlist.ptype) + if col == "playlist": return playlist.name def outlineView_setObjectValue_forTableColumn_byItem_(self, _, v, col, playlist): - col = col.identifier() if col else "playlist" + if not col: + return + col = col.identifier() + if col != "selected": return @@ -68,8 +81,7 @@ class FolderModel(NSObject): window = objc.IBOutlet() folderPopup = objc.IBOutlet() - def awakeFromNib(self): - folders = NSApp.delegate().folders() + def loadFolders_(self, folders): self.folderPopup.addItemsWithTitles_(folders) if len(folders) > 0: self.folderPopup.selectItemAtIndex_(2) @@ -82,11 +94,12 @@ class FolderModel(NSObject): currentIndex = self.folderPopup.indexOfSelectedItem() if currentIndex >= 2: self.lastIndex = currentIndex - NSApp.delegate().addFolder_(self.folderPopup.titleOfSelectedItem()) + NSApp.delegate().setFolder_(self.folderPopup.titleOfSelectedItem()) return panel = NSOpenPanel.openPanel() panel.setCanChooseFiles_(False) panel.setCanChooseDirectories_(True) + panel.setCanCreateDirectories_(True) panel.setAllowsMultipleSelection_(False) panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_( None, None, [], self.window, self, self.selectFolderEnd_returnCode_contextInfo_, None) @@ -96,7 +109,7 @@ class FolderModel(NSObject): if ret == NSOKButton: assert len(panel.filenames()) == 1 folder = panel.filenames()[0] - NSApp.delegate().addFolder_(folder) + NSApp.delegate().setFolder_(folder) self.folderPopup.insertItemWithTitle_atIndex_(folder, 2) self.folderPopup.selectItemAtIndex_(2) else: @@ -105,10 +118,21 @@ class FolderModel(NSObject): class NotiPodController(NSObject): window = objc.IBOutlet() - playlistModel = objc.IBOutlet() - folderModel = objc.IBOutlet() + loadingSheet = objc.IBOutlet() loadingLabel = objc.IBOutlet() + loadingIndicator = objc.IBOutlet() + + advancedSheet = objc.IBOutlet() + advancedSyncFolder = objc.IBOutlet() + advancedPathPrefix = objc.IBOutlet() + + previewWindow = objc.IBOutlet() + previewText = objc.IBOutlet() + + playlistModel = objc.IBOutlet() + folderModel = objc.IBOutlet() + def awakeFromNib(self): self.runningGenerator = False @@ -118,10 +142,15 @@ class NotiPodController(NSObject): pass def applicationDidFinishLaunching_(self, _): + self._loadPrefs() + + folders = [] + for target in self.targets: + folders.append(target["folder"]) + self.folderModel.loadFolders_(folders) + self.library = libnotipod.ITunesLibrary.alloc().init() - def finish(): - self.playlistModel.setPlaylists(self.library.get_playlists()) - self.runGenerator(lambda: self.library.load_(None), finish) + self.loadLibrary_(self) def applicationWillTerminate_(self, _): self.prefs().synchronize() @@ -129,47 +158,185 @@ class NotiPodController(NSObject): def applicationShouldTerminateAfterLastWindowClosed_(self, _): return True + def windowDidBecomeKey_(self, _): + if self.library.needs_reload(): + self.loadLibrary_(self) + # Utility methods - def runGenerator(self, func, finish): + def runGenerator(self, func, finish, fail): assert not self.runningGenerator self.runningGenerator = True + self.loadingIndicator.startAnimation_(self) NSApp.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self.loadingSheet, self.window, None, None, None) - arg = (func(), finish) + arg = (func(), finish, fail) self.performSelectorInBackground_withObject_(self.runGeneratorThread, arg) - def runGeneratorThread(self, (gen, finish)): + def runGeneratorThread(self, (gen, finish, fail)): pool = NSAutoreleasePool.alloc().init() - for msg in gen: - if not self.runningGenerator: - break - self.loadingLabel.performSelectorOnMainThread_withObject_waitUntilDone_( - self.loadingLabel.setStringValue_, msg, True) + last_time = 0 + try: + for msg in gen: + if not self.runningGenerator: + break + now = time.time() + if now - last_time > 0.1: + self.loadingLabel.performSelectorOnMainThread_withObject_waitUntilDone_( + self.loadingLabel.setStringValue_, msg, True) + except Exception, e: + NSRunAlertPanel("Error!", str(e), "Ok", None, None) + traceback.print_exc() + finish = fail self.performSelectorOnMainThread_withObject_waitUntilDone_( self.stopGenerator, finish, True) self.runningGenerator = False - del pool - + def stopGenerator(self, finish): self.runningGenerator = False NSApp.endSheet_(self.loadingSheet) self.loadingSheet.orderOut_(self) - finish() + self.loadingIndicator.stopAnimation_(self) + if finish: + finish() + + + @objc.IBAction + def loadLibrary_(self, sender): + if self.runningGenerator: + return + + def finish(): + self.playlistModel.setPlaylists(self.library.get_playlists()) + def fail(): + NSRunAlertPanel("Error!", "Unable to load iTunes library! Exiting...", "Ok", None, None) + os._exit(0) + self.runGenerator(lambda: self.library.load_(None), finish, fail) + + @objc.IBAction + def showAdvancedOptions_(self, sender): + if self.runningGenerator: + return + target = self.getCurrentTarget() + self.advancedSyncFolder.setStringValue_(target["folder"]) + self.advancedPathPrefix.setStringValue_(target["path_prefix"]) + NSApp.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self.advancedSheet, self.window, None, None, None) + + @objc.IBAction + def finishAdvancedOptions_(self, sender): + target = self.getCurrentTarget() + target["folder"] = self.advancedSyncFolder.stringValue() + target["path_prefix"] = self.advancedPathPrefix.stringValue() + self._savePrefs() + NSApp.endSheet_(self.advancedSheet) + self.advancedSheet.orderOut_(self) @objc.IBAction def doCancel_(self, sender): self.runningGenerator = False + def getCheckTarget(self): + target = self.getCurrentTarget() + if not target: + NSRunAlertPanel("Error!", "You must choose a folder first!", "Ok", None, None) + return + folder = target["folder"] + + if not os.path.isdir(folder.encode("utf-8")): + NSRunAlertPanel("Error!", "Destination " + folder + " does not exist, try mounting it first?", "Ok", None, None) + return + + folder_contents = [f for f in os.listdir(folder) if not f.startswith(".")] + if len(folder_contents) > 0 and "-Playlists-" not in folder_contents: + NSRunAlertPanel("Error!", "Refusing to clobber files in non-empty folder: " + folder, "Ok", None, None) + return + + return target + + def doPreviewThread(self): + yield "Calculating changes..." + + target = self.getCheckTarget() + if not target: + return + + all_tracks = set() + for playlist_id in self.playlists(): + playlist = self.library.get_playlist_pid(playlist_id) + if playlist is not None: + all_tracks.update(set(playlist.tracks)) + + all_filenames = [] + for trackID in all_tracks: + f = self.library.get_track_filename(trackID) + if f: + all_filenames.append(f) + + gen = libnotipod.sync( + dry_run=True, + source=self.library.folder, + dest=target["folder"], + files_to_copy=all_filenames, + ) + self.previewResult = "\n".join(gen) + + @objc.IBAction + def doPreview_(self, sender): + self.previewResult = "" + self.previewWindow.orderOut_(self) + + def finish(): + self.previewText.textStorage().mutableString().setString_(self.previewResult) + self.previewWindow.center() + self.previewWindow.makeKeyAndOrderFront_(self) + + self.runGenerator(self.doPreviewThread, finish, None) + @objc.IBAction def doSync_(self, sender): - folder = self.folders()[0] - playlists = [self.library.get_playlist_pid(pid) for pid in self.playlists()] + target = self.getCheckTarget() + if not target: + return - all_tracks = [] - for playlist in playlists: - all_tracks.extend(playlist.tracks) - libnotipod.export_m3u(dry_run=False, dest=folder, path_prefix="", - playlist_name=playlist.name, files=playlist.tracks) + all_tracks = set() + orig_playlists = set(self.playlists()) + all_playlists = orig_playlists.copy() + for playlist_id in all_playlists: + playlist = self.library.get_playlist_pid(playlist_id) + if playlist is None: + print "Forgetting unknown playlist:", playlist_id + self.setPlaylist_selected_(playlist_id, False) + continue + all_tracks.update(set(playlist.tracks)) + + all_filenames = [] + for trackID in all_tracks: + f = self.library.get_track_filename(trackID) + if f: + all_filenames.append(f) + all_playlists.update(self.library.get_track_playlists(trackID)) + + libnotipod.delete_playlists(dry_run=False, dest=target["folder"]) + + for playlist_id in all_playlists: + playlist = self.library.get_playlist_pid(playlist_id) + if playlist is None: + continue + tracks = [] + for trackID in playlist.tracks: + if trackID not in all_tracks: + continue + f = self.library.get_track_filename(trackID) + if f: + tracks.append(f) + if playlist_id not in orig_playlists and len(tracks) < 10: + continue + libnotipod.export_m3u( + dry_run=False, + dest=target["folder"], + path_prefix=target["path_prefix"], + playlist_name=playlist.name, + files=tracks + ) def finish(): NSRunAlertPanel("Complete!", "Synchronisation is complete", "Ok", None, None) @@ -178,48 +345,120 @@ class NotiPodController(NSObject): libnotipod.sync( dry_run=False, source=self.library.folder, - dest=folder, - files_to_copy=all_tracks + dest=target["folder"], + files_to_copy=all_filenames, ) , - finish + finish, + None ) - # Public accessors + # Preferences def prefs(self): return NSUserDefaults.standardUserDefaults() - def _getArray(self, key): - res = self.prefs().stringArrayForKey_(key) - return list(res) if res else [] + def _migratePrefs(self): + p = self.prefs() + + playlists = p.stringArrayForKey_("playlists") + if playlists is not None: + p.removeObjectForKey_("playlists") + else: + playlists = [] - def _saveArray(self, key, array): - self.prefs().setObject_forKey_(array, key) + folders = p.stringArrayForKey_("folders") + if not folders: + return + p.removeObjectForKey_("folders") + + first = True + for f in folders: + target = {} + target["folder"] = f + target["playlists"] = list(playlists) + target["uuid"] = uuid.uuid4().get_hex() + target["path_prefix"] = "../" + if first: + first = False + self.setCurrentTarget_(target["uuid"]) + self.targets.addObject_(target) + + self._savePrefs() + + def _loadPrefs(self): + p = self.prefs() + + self.currentTarget = None + self.setCurrentTarget_(p.stringForKey_("currentTarget")) + + self.targets = self.prefs().arrayForKey_("targets") + if self.targets is None: + self.targets = NSMutableArray.array() + else: + self.targets = NSMutableArray.arrayWithArray_(self.targets) + + if self.getCurrentTarget() is None: + self._migratePrefs() + + def _savePrefs(self): + p = self.prefs() + p.setObject_forKey_(self.currentTarget, "currentTarget") + p.setObject_forKey_(self.targets, "targets") + p.synchronize() + + def getCurrentTarget(self): + for target in self.targets: + if target["uuid"] == self.currentTarget: + return target + return None + + def setCurrentTarget_(self, targetUuid): + oldUuid = self.currentTarget + self.currentTarget = targetUuid + if oldUuid is None and targetUuid is not None: + self.playlistModel.outlineView.setEnabled_(True) + if oldUuid != targetUuid: + self.playlistModel.outlineView.reloadItem_reloadChildren_(None, True) def playlists(self): - return self._getArray("playlists") + target = self.getCurrentTarget() + if not target: + return [] + return list(target["playlists"]) + + def setFolder_(self, folder): + for i, target in enumerate(self.targets): + if target["folder"] == folder: + self.targets.removeObjectAtIndex_(i) + self.targets.insertObject_atIndex_(target, 0) + break + else: + target = {} + target["folder"] = folder + target["playlists"] = self.playlists() + target["uuid"] = uuid.uuid4().get_hex() + target["path_prefix"] = "../" + self.targets.insertObject_atIndex_(target, 0) - def folders(self): - return self._getArray("folders") + self.setCurrentTarget_(target["uuid"]) - def addFolder_(self, folder): - folders = self.folders() - while folder in folders: - folders.remove(folder) - folders.insert(0, folder) - folders = folders[:10] - self._saveArray("folders", folders) + self._savePrefs() def setPlaylist_selected_(self, playlist, selected): - playlists = self.playlists() + target = self.getCurrentTarget() + if not target: + raise AssertionError("No target selected when editing playlists") + + playlists = target["playlists"] if selected: playlists.append(playlist) else: playlists.remove(playlist) - playlists = list(set(playlists)) - self._saveArray("playlists", list(set(playlists))) + target["playlists"] = list(set(playlists)) + + self._savePrefs() def main():