]> code.delx.au - notipod/blobdiff - notipod_gui.py
Ignore tracks not in the library
[notipod] / notipod_gui.py
index 5a0d3290de5eab19fea3038f1298d9fa5038aac2..11c6880a86604a493a22d27dca616e220d228ce6 100644 (file)
@@ -4,6 +4,10 @@
 
 import logging
 import os
+import sys
+import time
+import traceback
+import uuid
 
 import objc
 from Foundation import *
@@ -20,6 +24,7 @@ class PlaylistModel(NSObject):
                self.root = []
                self.playlists = {}
                self.outlineView.setDataSource_(self)
+               self.outlineView.setEnabled_(False)
 
        def setPlaylists(self, playlists):
                self.root = []
@@ -76,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)
@@ -90,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)
@@ -104,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:
@@ -118,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()
 
@@ -127,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()
@@ -147,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):
@@ -159,14 +174,18 @@ 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()
                        finish = fail
                self.performSelectorOnMainThread_withObject_waitUntilDone_(
                        self.stopGenerator, finish, True)
@@ -180,25 +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 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..."
 
-               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 = 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_tracks = []
-               for playlist in playlists:
-                       all_tracks.extend(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)
 
@@ -216,18 +293,50 @@ class NotiPodController(NSObject):
 
        @objc.IBAction
        def doSync_(self, sender):
-               folder = self.folders()[0]
-               playlists = [self.library.get_playlist_pid(pid) for pid in self.playlists()]
-
-               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 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)
@@ -236,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,
@@ -245,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():