2 # Copyright 2009 James Bunton <jamesbunton@fastmail.fm>
3 # Licensed for distribution under the GPL version 2, check COPYING for details
12 from Foundation
import *
14 from PyObjCTools
import AppHelper
19 class PlaylistModel(NSObject
):
20 outlineView
= objc
.IBOutlet()
22 def awakeFromNib(self
):
25 self
.outlineView
.setDataSource_(self
)
26 self
.outlineView
.setEnabled_(False)
28 def setPlaylists(self
, playlists
):
30 self
.playlists
= playlists
31 for playlist
in self
.playlists
:
32 if playlist
.parent
is None:
33 self
.root
.append(playlist
)
34 self
.outlineView
.reloadData()
35 self
.outlineView
.expandItem_expandChildren_(None, True)
37 def outlineView_child_ofItem_(self
, _
, childIndex
, playlist
):
39 return self
.root
[childIndex
]
41 return playlist
.children
[childIndex
]
43 def outlineView_isItemExpandable_(self
, _
, playlist
):
47 return len(playlist
.children
) > 0
49 def outlineView_numberOfChildrenOfItem_(self
, _
, playlist
):
53 return len(playlist
.children
)
55 def outlineView_objectValueForTableColumn_byItem_(self
, _
, col
, playlist
):
58 col
= col
.identifier()
61 selected
= NSApp
.delegate().playlists()
62 return playlist
.pid
in selected
64 return NSImage
.imageNamed_("playlist-" + playlist
.ptype
)
68 def outlineView_setObjectValue_forTableColumn_byItem_(self
, _
, v
, col
, playlist
):
71 col
= col
.identifier()
76 NSApp
.delegate().setPlaylist_selected_(playlist
.pid
, v
)
79 class FolderModel(NSObject
):
80 window
= objc
.IBOutlet()
81 folderPopup
= objc
.IBOutlet()
83 def loadFolders_(self
, folders
):
84 self
.folderPopup
.addItemsWithTitles_(folders
)
86 self
.folderPopup
.selectItemAtIndex_(2)
92 def doSelectFolder_(self
, sender
):
93 currentIndex
= self
.folderPopup
.indexOfSelectedItem()
95 self
.lastIndex
= currentIndex
96 NSApp
.delegate().setFolder_(self
.folderPopup
.titleOfSelectedItem())
98 panel
= NSOpenPanel
.openPanel()
99 panel
.setCanChooseFiles_(False)
100 panel
.setCanChooseDirectories_(True)
101 panel
.setCanCreateDirectories_(True)
102 panel
.setAllowsMultipleSelection_(False)
103 panel
.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
104 None, None, [], self
.window
, self
, self
.selectFolderEnd_returnCode_contextInfo_
, None)
106 @objc.signature("v@:@ii")
107 def selectFolderEnd_returnCode_contextInfo_(self
, panel
, ret
, _
):
108 if ret
== NSOKButton
:
109 assert len(panel
.filenames()) == 1
110 folder
= panel
.filenames()[0]
111 NSApp
.delegate().setFolder_(folder
)
112 self
.folderPopup
.insertItemWithTitle_atIndex_(folder
, 2)
113 self
.folderPopup
.selectItemAtIndex_(2)
115 self
.folderPopup
.selectItemAtIndex_(self
.lastIndex
)
118 class NotiPodController(NSObject
):
119 window
= objc
.IBOutlet()
121 loadingSheet
= objc
.IBOutlet()
122 loadingLabel
= objc
.IBOutlet()
123 loadingIndicator
= objc
.IBOutlet()
125 advancedSheet
= objc
.IBOutlet()
126 advancedSyncFolder
= objc
.IBOutlet()
127 advancedPathPrefix
= objc
.IBOutlet()
129 previewWindow
= objc
.IBOutlet()
130 previewText
= objc
.IBOutlet()
132 playlistModel
= objc
.IBOutlet()
133 folderModel
= objc
.IBOutlet()
136 def awakeFromNib(self
):
137 self
.runningGenerator
= False
140 def applicationWillFinishLaunching_(self
, _
):
143 def applicationDidFinishLaunching_(self
, _
):
147 for target
in self
.targets
:
148 folders
.append(target
["folder"])
149 self
.folderModel
.loadFolders_(folders
)
151 self
.library
= libnotipod
.ITunesLibrary
.alloc().init()
152 self
.loadLibrary_(self
)
154 def applicationWillTerminate_(self
, _
):
155 self
.prefs().synchronize()
157 def applicationShouldTerminateAfterLastWindowClosed_(self
, _
):
160 def windowDidBecomeKey_(self
, _
):
161 if self
.library
.needs_reload():
162 self
.loadLibrary_(self
)
166 def runGenerator(self
, func
, finish
, fail
):
167 assert not self
.runningGenerator
168 self
.runningGenerator
= True
169 self
.loadingIndicator
.startAnimation_(self
)
170 NSApp
.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self
.loadingSheet
, self
.window
, None, None, None)
171 arg
= (func(), finish
, fail
)
172 self
.performSelectorInBackground_withObject_(self
.runGeneratorThread
, arg
)
174 def runGeneratorThread(self
, (gen
, finish
, fail
)):
175 pool
= NSAutoreleasePool
.alloc().init()
178 if not self
.runningGenerator
:
180 self
.loadingLabel
.performSelectorOnMainThread_withObject_waitUntilDone_(
181 self
.loadingLabel
.setStringValue_
, msg
, True)
183 NSRunAlertPanel("Error!", str(e
), "Ok", None, None)
184 traceback
.print_exc()
186 self
.performSelectorOnMainThread_withObject_waitUntilDone_(
187 self
.stopGenerator
, finish
, True)
188 self
.runningGenerator
= False
190 def stopGenerator(self
, finish
):
191 self
.runningGenerator
= False
192 NSApp
.endSheet_(self
.loadingSheet
)
193 self
.loadingSheet
.orderOut_(self
)
194 self
.loadingIndicator
.stopAnimation_(self
)
200 def loadLibrary_(self
, sender
):
201 if self
.runningGenerator
:
205 self
.playlistModel
.setPlaylists(self
.library
.get_playlists())
207 NSRunAlertPanel("Error!", "Unable to load iTunes library! Exiting...", "Ok", None, None)
209 self
.runGenerator(lambda: self
.library
.load_(None), finish
, fail
)
212 def showAdvancedOptions_(self
, sender
):
213 if self
.runningGenerator
:
215 target
= self
.getCurrentTarget()
216 self
.advancedSyncFolder
.setStringValue_(target
["folder"])
217 self
.advancedPathPrefix
.setStringValue_(target
["path_prefix"])
218 NSApp
.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self
.advancedSheet
, self
.window
, None, None, None)
221 def finishAdvancedOptions_(self
, sender
):
222 target
= self
.getCurrentTarget()
223 target
["folder"] = self
.advancedSyncFolder
.stringValue()
224 target
["path_prefix"] = self
.advancedPathPrefix
.stringValue()
226 NSApp
.endSheet_(self
.advancedSheet
)
227 self
.advancedSheet
.orderOut_(self
)
230 def doCancel_(self
, sender
):
231 self
.runningGenerator
= False
233 def getCheckTarget(self
):
234 target
= self
.getCurrentTarget()
236 NSRunAlertPanel("Error!", "You must choose a folder first!", "Ok", None, None)
238 folder
= target
["folder"]
239 if not os
.path
.isdir(folder
.encode("utf-8")):
240 NSRunAlertPanel("Error!", "Destination " + folder
+ " does not exist, try mounting it first?", "Ok", None, None)
242 target
["folder"] = folder
243 target
["path_prefix"] = target
["path_prefix"].encode("utf-8")
246 def doPreviewThread(self
):
247 yield "Calculating changes..."
249 target
= self
.getCheckTarget()
254 for playlist_id
in self
.playlists():
255 playlist
= self
.library
.get_playlist_pid(playlist_id
)
256 if playlist
is not None:
257 all_tracks
.update(set(playlist
.tracks
))
260 for trackID
in all_tracks
:
261 all_filenames
.append(self
.library
.get_track_filename(trackID
))
263 gen
= libnotipod
.sync(
265 source
=self
.library
.folder
,
266 dest
=target
["folder"],
267 files_to_copy
=all_filenames
,
269 self
.previewResult
= "\n".join(gen
)
272 def doPreview_(self
, sender
):
273 self
.previewResult
= ""
274 self
.previewWindow
.orderOut_(self
)
277 self
.previewText
.textStorage().mutableString().setString_(self
.previewResult
)
278 self
.previewWindow
.center()
279 self
.previewWindow
.makeKeyAndOrderFront_(self
)
281 self
.runGenerator(self
.doPreviewThread
, finish
, None)
284 def doSync_(self
, sender
):
285 target
= self
.getCheckTarget()
290 orig_playlists
= set(self
.playlists())
291 all_playlists
= orig_playlists
.copy()
292 for playlist_id
in all_playlists
:
293 playlist
= self
.library
.get_playlist_pid(playlist_id
)
295 print "Forgetting unknown playlist:", playlist_id
296 self
.setPlaylist_selected_(playlist_id
, False)
298 all_tracks
.update(set(playlist
.tracks
))
301 for trackID
in all_tracks
:
302 all_filenames
.append(self
.library
.get_track_filename(trackID
))
303 all_playlists
.update(self
.library
.get_track_playlists(trackID
))
305 for playlist_id
in all_playlists
:
306 playlist
= self
.library
.get_playlist_pid(playlist_id
)
310 for trackID
in playlist
.tracks
:
311 if trackID
in all_tracks
:
312 tracks
.append(self
.library
.get_track_filename(trackID
))
313 if playlist_id
not in orig_playlists
and len(tracks
) < 10:
315 libnotipod
.export_m3u(
317 dest
=target
["folder"],
318 path_prefix
=target
["path_prefix"],
319 playlist_name
=playlist
.name
,
324 NSRunAlertPanel("Complete!", "Synchronisation is complete", "Ok", None, None)
329 source
=self
.library
.folder
,
330 dest
=target
["folder"],
331 files_to_copy
=all_filenames
,
342 return NSUserDefaults
.standardUserDefaults()
344 def _migratePrefs(self
):
347 playlists
= p
.stringArrayForKey_("playlists")
348 if playlists
is not None:
349 p
.removeObjectForKey_("playlists")
353 folders
= p
.stringArrayForKey_("folders")
356 p
.removeObjectForKey_("folders")
362 target
["playlists"] = list(playlists
)
363 target
["uuid"] = uuid
.uuid1().get_hex()
364 target
["path_prefix"] = "../"
367 self
.setCurrentTarget_(target
["uuid"])
368 self
.targets
.addObject_(target
)
372 def _loadPrefs(self
):
375 self
.currentTarget
= None
376 self
.setCurrentTarget_(p
.stringForKey_("currentTarget"))
378 self
.targets
= self
.prefs().arrayForKey_("targets")
379 if self
.targets
is None:
380 self
.targets
= NSMutableArray
.array()
382 self
.targets
= NSMutableArray
.arrayWithArray_(self
.targets
)
384 if self
.getCurrentTarget() is None:
387 def _savePrefs(self
):
389 p
.setObject_forKey_(self
.currentTarget
, "currentTarget")
390 p
.setObject_forKey_(self
.targets
, "targets")
393 def getCurrentTarget(self
):
394 for target
in self
.targets
:
395 if target
["uuid"] == self
.currentTarget
:
399 def setCurrentTarget_(self
, targetUuid
):
400 oldUuid
= self
.currentTarget
401 self
.currentTarget
= targetUuid
402 if oldUuid
is None and targetUuid
is not None:
403 self
.playlistModel
.outlineView
.setEnabled_(True)
404 if oldUuid
!= targetUuid
:
405 self
.playlistModel
.outlineView
.reloadItem_reloadChildren_(None, True)
408 target
= self
.getCurrentTarget()
411 return list(target
["playlists"])
413 def setFolder_(self
, folder
):
414 for i
, target
in enumerate(self
.targets
):
415 if target
["folder"] == folder
:
416 self
.targets
.removeObjectAtIndex_(i
)
417 self
.targets
.insertObject_atIndex_(target
, 0)
421 target
["folder"] = folder
422 target
["playlists"] = self
.playlists()
423 target
["uuid"] = uuid
.uuid1().get_hex()
424 target
["path_prefix"] = "../"
425 self
.targets
.insertObject_atIndex_(target
, 0)
427 self
.setCurrentTarget_(target
["uuid"])
431 def setPlaylist_selected_(self
, playlist
, selected
):
432 target
= self
.getCurrentTarget()
434 raise AssertionError("No target selected when editing playlists")
436 playlists
= target
["playlists"]
438 playlists
.append(playlist
)
440 playlists
.remove(playlist
)
441 target
["playlists"] = list(set(playlists
))
447 ### logging.basicConfig(format="%(levelname)s: %(message)s")
448 ### logging.getLogger().setLevel(logging.DEBUG)
449 AppHelper
.runEventLoop()
451 if __name__
== "__main__":