import logging
import os
+import sys
+import time
+import traceback
+import uuid
import objc
from Foundation import *
self.root = []
self.playlists = {}
self.outlineView.setDataSource_(self)
+ self.outlineView.setEnabled_(False)
def setPlaylists(self, playlists):
self.root = []
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)
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)
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:
loadingLabel = objc.IBOutlet()
loadingIndicator = objc.IBOutlet()
+ advancedSheet = objc.IBOutlet()
+ advancedSyncFolder = objc.IBOutlet()
+ advancedPathPrefix = objc.IBOutlet()
+
previewWindow = objc.IBOutlet()
previewText = objc.IBOutlet()
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()
def applicationShouldTerminateAfterLastWindowClosed_(self, _):
return True
+ def windowDidBecomeKey_(self, _):
+ if self.library.needs_reload():
+ self.loadLibrary_(self)
+
# Utility methods
def runGenerator(self, func, finish, fail):
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)
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)
@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)
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,
)
- # 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():