X-Git-Url: https://code.delx.au/notipod/blobdiff_plain/a9de8db967ce3c8d7063301bf3a806a1b1597503..940330ce5a487a919446ee62f920646c2e38fee5:/notipod_gui.py diff --git a/notipod_gui.py b/notipod_gui.py index 85d2f15..11c6880 100644 --- a/notipod_gui.py +++ b/notipod_gui.py @@ -5,7 +5,9 @@ import logging import os import sys +import time import traceback +import uuid import objc from Foundation import * @@ -22,6 +24,7 @@ class PlaylistModel(NSObject): self.root = [] self.playlists = {} self.outlineView.setDataSource_(self) + self.outlineView.setEnabled_(False) def setPlaylists(self, playlists): self.root = [] @@ -78,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) @@ -92,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) @@ -106,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: @@ -120,6 +123,10 @@ class NotiPodController(NSObject): loadingLabel = objc.IBOutlet() loadingIndicator = objc.IBOutlet() + advancedSheet = objc.IBOutlet() + advancedSyncFolder = objc.IBOutlet() + advancedPathPrefix = objc.IBOutlet() + previewWindow = objc.IBOutlet() previewText = objc.IBOutlet() @@ -129,19 +136,21 @@ class NotiPodController(NSObject): def awakeFromNib(self): self.runningGenerator = False - self.previewWindow.setReleasedWhenClosed_(False) # Delegate methods def applicationWillFinishLaunching_(self, _): 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()) - def fail(): - sys.exit(0) - self.runGenerator(lambda: self.library.load_(None), finish, fail) + self.loadLibrary_(self) def applicationWillTerminate_(self, _): self.prefs().synchronize() @@ -149,6 +158,10 @@ 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, fail): @@ -161,12 +174,15 @@ class NotiPodController(NSObject): def runGeneratorThread(self, (gen, finish, fail)): pool = NSAutoreleasePool.alloc().init() + last_time = 0 try: for msg in gen: if not self.runningGenerator: break - self.loadingLabel.performSelectorOnMainThread_withObject_waitUntilDone_( - self.loadingLabel.setStringValue_, msg, True) + 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() @@ -183,29 +199,83 @@ class NotiPodController(NSObject): 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 doPreviewThread(self): - yield "Calculating changes..." + def getCheckTarget(self): + target = self.getCurrentTarget() + if not target: + NSRunAlertPanel("Error!", "You must choose a folder first!", "Ok", None, None) + return + folder = target["folder"] - folder = self.folders()[0] if not os.path.isdir(folder.encode("utf-8")): NSRunAlertPanel("Error!", "Destination " + folder + " does not exist, try mounting it first?", "Ok", None, None) return - all_tracks = [] + 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.extend(playlist.tracks) + 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=folder, - files_to_copy=all_tracks + dest=target["folder"], + files_to_copy=all_filenames, ) self.previewResult = "\n".join(gen) @@ -223,21 +293,50 @@ class NotiPodController(NSObject): @objc.IBAction def doSync_(self, sender): - folder = self.folders()[0] - if not os.path.isdir(folder.encode("utf-8")): - NSRunAlertPanel("Error!", "Destination " + folder + " does not exist, try mounting it first?", "Ok", None, None) + target = self.getCheckTarget() + if not target: return - all_tracks = [] - for playlist_id in self.playlists(): + 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.extend(playlist.tracks) - libnotipod.export_m3u(dry_run=False, dest=folder, path_prefix="", - playlist_name=playlist.name, files=playlist.tracks) + 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) @@ -246,8 +345,8 @@ 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, @@ -255,40 +354,111 @@ class NotiPodController(NSObject): ) - # 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() - def _saveArray(self, key, array): - self.prefs().setObject_forKey_(array, key) + playlists = p.stringArrayForKey_("playlists") + if playlists is not None: + p.removeObjectForKey_("playlists") + else: + playlists = [] + + 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():