2 # Copyright 2009 James Bunton <jamesbunton@fastmail.fm>
3 # Licensed for distribution under the GPL version 2, check COPYING for details
13 from Foundation
import *
15 from PyObjCTools
import AppHelper
20 class PlaylistModel(NSObject
):
21 outlineView
= objc
.IBOutlet()
23 def awakeFromNib(self
):
26 self
.outlineView
.setDataSource_(self
)
27 self
.outlineView
.setEnabled_(False)
29 def setPlaylists(self
, playlists
):
31 self
.playlists
= playlists
32 for playlist
in self
.playlists
:
33 if playlist
.parent
is None:
34 self
.root
.append(playlist
)
35 self
.outlineView
.reloadData()
36 self
.outlineView
.expandItem_expandChildren_(None, True)
38 def outlineView_child_ofItem_(self
, _
, childIndex
, playlist
):
40 return self
.root
[childIndex
]
42 return playlist
.children
[childIndex
]
44 def outlineView_isItemExpandable_(self
, _
, playlist
):
48 return len(playlist
.children
) > 0
50 def outlineView_numberOfChildrenOfItem_(self
, _
, playlist
):
54 return len(playlist
.children
)
56 def outlineView_objectValueForTableColumn_byItem_(self
, _
, col
, playlist
):
59 col
= col
.identifier()
62 selected
= NSApp
.delegate().playlists()
63 return playlist
.pid
in selected
65 return NSImage
.imageNamed_("playlist-" + playlist
.ptype
)
69 def outlineView_setObjectValue_forTableColumn_byItem_(self
, _
, v
, col
, playlist
):
72 col
= col
.identifier()
77 NSApp
.delegate().setPlaylist_selected_(playlist
.pid
, v
)
80 class FolderModel(NSObject
):
81 window
= objc
.IBOutlet()
82 folderPopup
= objc
.IBOutlet()
84 def loadFolders_(self
, folders
):
85 self
.folderPopup
.addItemsWithTitles_(folders
)
87 self
.folderPopup
.selectItemAtIndex_(2)
93 def doSelectFolder_(self
, sender
):
94 currentIndex
= self
.folderPopup
.indexOfSelectedItem()
96 self
.lastIndex
= currentIndex
97 NSApp
.delegate().setFolder_(self
.folderPopup
.titleOfSelectedItem())
99 panel
= NSOpenPanel
.openPanel()
100 panel
.setCanChooseFiles_(False)
101 panel
.setCanChooseDirectories_(True)
102 panel
.setCanCreateDirectories_(True)
103 panel
.setAllowsMultipleSelection_(False)
104 panel
.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
105 None, None, [], self
.window
, self
, self
.selectFolderEnd_returnCode_contextInfo_
, None)
107 @objc.signature("v@:@ii")
108 def selectFolderEnd_returnCode_contextInfo_(self
, panel
, ret
, _
):
109 if ret
== NSOKButton
:
110 assert len(panel
.filenames()) == 1
111 folder
= panel
.filenames()[0]
112 NSApp
.delegate().setFolder_(folder
)
113 self
.folderPopup
.insertItemWithTitle_atIndex_(folder
, 2)
114 self
.folderPopup
.selectItemAtIndex_(2)
116 self
.folderPopup
.selectItemAtIndex_(self
.lastIndex
)
119 class NotiPodController(NSObject
):
120 window
= objc
.IBOutlet()
122 loadingSheet
= objc
.IBOutlet()
123 loadingLabel
= objc
.IBOutlet()
124 loadingIndicator
= objc
.IBOutlet()
126 advancedSheet
= objc
.IBOutlet()
127 advancedSyncFolder
= objc
.IBOutlet()
128 advancedPathPrefix
= objc
.IBOutlet()
130 previewWindow
= objc
.IBOutlet()
131 previewText
= objc
.IBOutlet()
133 playlistModel
= objc
.IBOutlet()
134 folderModel
= objc
.IBOutlet()
137 def awakeFromNib(self
):
138 self
.runningGenerator
= False
141 def applicationWillFinishLaunching_(self
, _
):
144 def applicationDidFinishLaunching_(self
, _
):
148 for target
in self
.targets
:
149 folders
.append(target
["folder"])
150 self
.folderModel
.loadFolders_(folders
)
152 self
.library
= libnotipod
.ITunesLibrary
.alloc().init()
153 self
.loadLibrary_(self
)
155 def applicationWillTerminate_(self
, _
):
156 self
.prefs().synchronize()
158 def applicationShouldTerminateAfterLastWindowClosed_(self
, _
):
161 def windowDidBecomeKey_(self
, _
):
162 if self
.library
.needs_reload():
163 self
.loadLibrary_(self
)
167 def runGenerator(self
, func
, finish
, fail
):
168 assert not self
.runningGenerator
169 self
.runningGenerator
= True
170 self
.loadingIndicator
.startAnimation_(self
)
171 NSApp
.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self
.loadingSheet
, self
.window
, None, None, None)
172 arg
= (func(), finish
, fail
)
173 self
.performSelectorInBackground_withObject_(self
.runGeneratorThread
, arg
)
175 def runGeneratorThread(self
, (gen
, finish
, fail
)):
176 pool
= NSAutoreleasePool
.alloc().init()
180 if not self
.runningGenerator
:
183 if now
- last_time
> 0.1:
184 self
.loadingLabel
.performSelectorOnMainThread_withObject_waitUntilDone_(
185 self
.loadingLabel
.setStringValue_
, msg
, True)
187 NSRunAlertPanel("Error!", str(e
), "Ok", None, None)
188 traceback
.print_exc()
190 self
.performSelectorOnMainThread_withObject_waitUntilDone_(
191 self
.stopGenerator
, finish
, True)
192 self
.runningGenerator
= False
194 def stopGenerator(self
, finish
):
195 self
.runningGenerator
= False
196 NSApp
.endSheet_(self
.loadingSheet
)
197 self
.loadingSheet
.orderOut_(self
)
198 self
.loadingIndicator
.stopAnimation_(self
)
204 def loadLibrary_(self
, sender
):
205 if self
.runningGenerator
:
209 self
.playlistModel
.setPlaylists(self
.library
.get_playlists())
211 NSRunAlertPanel("Error!", "Unable to load iTunes library! Exiting...", "Ok", None, None)
213 self
.runGenerator(lambda: self
.library
.load_(None), finish
, fail
)
216 def showAdvancedOptions_(self
, sender
):
217 if self
.runningGenerator
:
219 target
= self
.getCurrentTarget()
220 self
.advancedSyncFolder
.setStringValue_(target
["folder"])
221 self
.advancedPathPrefix
.setStringValue_(target
["path_prefix"])
222 NSApp
.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self
.advancedSheet
, self
.window
, None, None, None)
225 def finishAdvancedOptions_(self
, sender
):
226 target
= self
.getCurrentTarget()
227 target
["folder"] = self
.advancedSyncFolder
.stringValue()
228 target
["path_prefix"] = self
.advancedPathPrefix
.stringValue()
230 NSApp
.endSheet_(self
.advancedSheet
)
231 self
.advancedSheet
.orderOut_(self
)
234 def doCancel_(self
, sender
):
235 self
.runningGenerator
= False
237 def getCheckTarget(self
):
238 target
= self
.getCurrentTarget()
240 NSRunAlertPanel("Error!", "You must choose a folder first!", "Ok", None, None)
242 folder
= target
["folder"]
244 if not os
.path
.isdir(folder
.encode("utf-8")):
245 NSRunAlertPanel("Error!", "Destination " + folder
+ " does not exist, try mounting it first?", "Ok", None, None)
248 folder_contents
= [f
for f
in os
.listdir(folder
) if not f
.startswith(".")]
249 if len(folder_contents
) > 0 and "-Playlists-" not in folder_contents
:
250 NSRunAlertPanel("Error!", "Refusing to clobber files in non-empty folder: " + folder
, "Ok", None, None)
255 def doPreviewThread(self
):
256 yield "Calculating changes..."
258 target
= self
.getCheckTarget()
263 for playlist_id
in self
.playlists():
264 playlist
= self
.library
.get_playlist_pid(playlist_id
)
265 if playlist
is not None:
266 all_tracks
.update(set(playlist
.tracks
))
269 for trackID
in all_tracks
:
270 f
= self
.library
.get_track_filename(trackID
)
272 all_filenames
.append(f
)
274 gen
= libnotipod
.sync(
276 source
=self
.library
.folder
,
277 dest
=target
["folder"],
278 files_to_copy
=all_filenames
,
280 self
.previewResult
= "\n".join(gen
)
283 def doPreview_(self
, sender
):
284 self
.previewResult
= ""
285 self
.previewWindow
.orderOut_(self
)
288 self
.previewText
.textStorage().mutableString().setString_(self
.previewResult
)
289 self
.previewWindow
.center()
290 self
.previewWindow
.makeKeyAndOrderFront_(self
)
292 self
.runGenerator(self
.doPreviewThread
, finish
, None)
295 def doSync_(self
, sender
):
296 target
= self
.getCheckTarget()
301 orig_playlists
= set(self
.playlists())
302 all_playlists
= orig_playlists
.copy()
303 for playlist_id
in all_playlists
:
304 playlist
= self
.library
.get_playlist_pid(playlist_id
)
306 print "Forgetting unknown playlist:", playlist_id
307 self
.setPlaylist_selected_(playlist_id
, False)
309 all_tracks
.update(set(playlist
.tracks
))
312 for trackID
in all_tracks
:
313 f
= self
.library
.get_track_filename(trackID
)
315 all_filenames
.append(f
)
316 all_playlists
.update(self
.library
.get_track_playlists(trackID
))
318 libnotipod
.delete_playlists(dry_run
=False, dest
=target
["folder"])
320 for playlist_id
in all_playlists
:
321 playlist
= self
.library
.get_playlist_pid(playlist_id
)
325 for trackID
in playlist
.tracks
:
326 if trackID
not in all_tracks
:
328 f
= self
.library
.get_track_filename(trackID
)
331 if playlist_id
not in orig_playlists
and len(tracks
) < 10:
333 libnotipod
.export_m3u(
335 dest
=target
["folder"],
336 path_prefix
=target
["path_prefix"],
337 playlist_name
=playlist
.name
,
342 NSRunAlertPanel("Complete!", "Synchronisation is complete", "Ok", None, None)
347 source
=self
.library
.folder
,
348 dest
=target
["folder"],
349 files_to_copy
=all_filenames
,
360 return NSUserDefaults
.standardUserDefaults()
362 def _migratePrefs(self
):
365 playlists
= p
.stringArrayForKey_("playlists")
366 if playlists
is not None:
367 p
.removeObjectForKey_("playlists")
371 folders
= p
.stringArrayForKey_("folders")
374 p
.removeObjectForKey_("folders")
380 target
["playlists"] = list(playlists
)
381 target
["uuid"] = uuid
.uuid4().get_hex()
382 target
["path_prefix"] = "../"
385 self
.setCurrentTarget_(target
["uuid"])
386 self
.targets
.addObject_(target
)
390 def _loadPrefs(self
):
393 self
.currentTarget
= None
394 self
.setCurrentTarget_(p
.stringForKey_("currentTarget"))
396 self
.targets
= self
.prefs().arrayForKey_("targets")
397 if self
.targets
is None:
398 self
.targets
= NSMutableArray
.array()
400 self
.targets
= NSMutableArray
.arrayWithArray_(self
.targets
)
402 if self
.getCurrentTarget() is None:
405 def _savePrefs(self
):
407 p
.setObject_forKey_(self
.currentTarget
, "currentTarget")
408 p
.setObject_forKey_(self
.targets
, "targets")
411 def getCurrentTarget(self
):
412 for target
in self
.targets
:
413 if target
["uuid"] == self
.currentTarget
:
417 def setCurrentTarget_(self
, targetUuid
):
418 oldUuid
= self
.currentTarget
419 self
.currentTarget
= targetUuid
420 if oldUuid
is None and targetUuid
is not None:
421 self
.playlistModel
.outlineView
.setEnabled_(True)
422 if oldUuid
!= targetUuid
:
423 self
.playlistModel
.outlineView
.reloadItem_reloadChildren_(None, True)
426 target
= self
.getCurrentTarget()
429 return list(target
["playlists"])
431 def setFolder_(self
, folder
):
432 for i
, target
in enumerate(self
.targets
):
433 if target
["folder"] == folder
:
434 self
.targets
.removeObjectAtIndex_(i
)
435 self
.targets
.insertObject_atIndex_(target
, 0)
439 target
["folder"] = folder
440 target
["playlists"] = self
.playlists()
441 target
["uuid"] = uuid
.uuid4().get_hex()
442 target
["path_prefix"] = "../"
443 self
.targets
.insertObject_atIndex_(target
, 0)
445 self
.setCurrentTarget_(target
["uuid"])
449 def setPlaylist_selected_(self
, playlist
, selected
):
450 target
= self
.getCurrentTarget()
452 raise AssertionError("No target selected when editing playlists")
454 playlists
= target
["playlists"]
456 playlists
.append(playlist
)
458 playlists
.remove(playlist
)
459 target
["playlists"] = list(set(playlists
))
465 ### logging.basicConfig(format="%(levelname)s: %(message)s")
466 ### logging.getLogger().setLevel(logging.DEBUG)
467 AppHelper
.runEventLoop()
469 if __name__
== "__main__":