]> code.delx.au - notipod/blob - notipod_gui.py
Allow user to create directories in "Choose folder" dialog
[notipod] / notipod_gui.py
1 #!/usr/bin/env python
2 # Copyright 2009 James Bunton <jamesbunton@fastmail.fm>
3 # Licensed for distribution under the GPL version 2, check COPYING for details
4
5 import logging
6 import os
7 import sys
8 import traceback
9
10 import objc
11 from Foundation import *
12 from AppKit import *
13 from PyObjCTools import AppHelper
14
15 import libnotipod
16
17
18 class PlaylistModel(NSObject):
19 outlineView = objc.IBOutlet()
20
21 def awakeFromNib(self):
22 self.root = []
23 self.playlists = {}
24 self.outlineView.setDataSource_(self)
25
26 def setPlaylists(self, playlists):
27 self.root = []
28 self.playlists = playlists
29 for playlist in self.playlists:
30 if playlist.parent is None:
31 self.root.append(playlist)
32 self.outlineView.reloadData()
33 self.outlineView.expandItem_expandChildren_(None, True)
34
35 def outlineView_child_ofItem_(self, _, childIndex, playlist):
36 if playlist == None:
37 return self.root[childIndex]
38 else:
39 return playlist.children[childIndex]
40
41 def outlineView_isItemExpandable_(self, _, playlist):
42 if playlist == None:
43 return True
44 else:
45 return len(playlist.children) > 0
46
47 def outlineView_numberOfChildrenOfItem_(self, _, playlist):
48 if playlist == None:
49 return len(self.root)
50 else:
51 return len(playlist.children)
52
53 def outlineView_objectValueForTableColumn_byItem_(self, _, col, playlist):
54 if not col:
55 return
56 col = col.identifier()
57
58 if col == "selected":
59 selected = NSApp.delegate().playlists()
60 return playlist.pid in selected
61 if col == "icon":
62 return NSImage.imageNamed_("playlist-" + playlist.ptype)
63 if col == "playlist":
64 return playlist.name
65
66 def outlineView_setObjectValue_forTableColumn_byItem_(self, _, v, col, playlist):
67 if not col:
68 return
69 col = col.identifier()
70
71
72 if col != "selected":
73 return
74 NSApp.delegate().setPlaylist_selected_(playlist.pid, v)
75
76
77 class FolderModel(NSObject):
78 window = objc.IBOutlet()
79 folderPopup = objc.IBOutlet()
80
81 def awakeFromNib(self):
82 folders = NSApp.delegate().folders()
83 self.folderPopup.addItemsWithTitles_(folders)
84 if len(folders) > 0:
85 self.folderPopup.selectItemAtIndex_(2)
86 self.lastIndex = 2
87 else:
88 self.lastIndex = 0
89
90 @objc.IBAction
91 def doSelectFolder_(self, sender):
92 currentIndex = self.folderPopup.indexOfSelectedItem()
93 if currentIndex >= 2:
94 self.lastIndex = currentIndex
95 NSApp.delegate().addFolder_(self.folderPopup.titleOfSelectedItem())
96 return
97 panel = NSOpenPanel.openPanel()
98 panel.setCanChooseFiles_(False)
99 panel.setCanChooseDirectories_(True)
100 panel.setCanCreateDirectories_(True)
101 panel.setAllowsMultipleSelection_(False)
102 panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
103 None, None, [], self.window, self, self.selectFolderEnd_returnCode_contextInfo_, None)
104
105 @objc.signature("v@:@ii")
106 def selectFolderEnd_returnCode_contextInfo_(self, panel, ret, _):
107 if ret == NSOKButton:
108 assert len(panel.filenames()) == 1
109 folder = panel.filenames()[0]
110 NSApp.delegate().addFolder_(folder)
111 self.folderPopup.insertItemWithTitle_atIndex_(folder, 2)
112 self.folderPopup.selectItemAtIndex_(2)
113 else:
114 self.folderPopup.selectItemAtIndex_(self.lastIndex)
115
116
117 class NotiPodController(NSObject):
118 window = objc.IBOutlet()
119
120 loadingSheet = objc.IBOutlet()
121 loadingLabel = objc.IBOutlet()
122 loadingIndicator = objc.IBOutlet()
123
124 previewWindow = objc.IBOutlet()
125 previewText = objc.IBOutlet()
126
127 playlistModel = objc.IBOutlet()
128 folderModel = objc.IBOutlet()
129
130
131 def awakeFromNib(self):
132 self.runningGenerator = False
133
134 # Delegate methods
135 def applicationWillFinishLaunching_(self, _):
136 pass
137
138 def applicationDidFinishLaunching_(self, _):
139 self.library = libnotipod.ITunesLibrary.alloc().init()
140 def finish():
141 self.playlistModel.setPlaylists(self.library.get_playlists())
142 def fail():
143 sys.exit(0)
144 self.runGenerator(lambda: self.library.load_(None), finish, fail)
145
146 def applicationWillTerminate_(self, _):
147 self.prefs().synchronize()
148
149 def applicationShouldTerminateAfterLastWindowClosed_(self, _):
150 return True
151
152
153 # Utility methods
154 def runGenerator(self, func, finish, fail):
155 assert not self.runningGenerator
156 self.runningGenerator = True
157 self.loadingIndicator.startAnimation_(self)
158 NSApp.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self.loadingSheet, self.window, None, None, None)
159 arg = (func(), finish, fail)
160 self.performSelectorInBackground_withObject_(self.runGeneratorThread, arg)
161
162 def runGeneratorThread(self, (gen, finish, fail)):
163 pool = NSAutoreleasePool.alloc().init()
164 try:
165 for msg in gen:
166 if not self.runningGenerator:
167 break
168 self.loadingLabel.performSelectorOnMainThread_withObject_waitUntilDone_(
169 self.loadingLabel.setStringValue_, msg, True)
170 except Exception, e:
171 NSRunAlertPanel("Error!", str(e), "Ok", None, None)
172 traceback.print_exc()
173 finish = fail
174 self.performSelectorOnMainThread_withObject_waitUntilDone_(
175 self.stopGenerator, finish, True)
176 self.runningGenerator = False
177
178 def stopGenerator(self, finish):
179 self.runningGenerator = False
180 NSApp.endSheet_(self.loadingSheet)
181 self.loadingSheet.orderOut_(self)
182 self.loadingIndicator.stopAnimation_(self)
183 if finish:
184 finish()
185
186 @objc.IBAction
187 def doCancel_(self, sender):
188 self.runningGenerator = False
189
190 def getDestFolder(self):
191 folders = self.folders()
192 if not folders:
193 NSRunAlertPanel("Error!", "You must choose a folder first!", "Ok", None, None)
194 return
195 folder = folders[0]
196 if not os.path.isdir(folder.encode("utf-8")):
197 NSRunAlertPanel("Error!", "Destination " + folder + " does not exist, try mounting it first?", "Ok", None, None)
198 return
199 return folder
200
201 def doPreviewThread(self):
202 yield "Calculating changes..."
203
204 folder = self.getDestFolder()
205 if not folder:
206 return
207
208 all_tracks = set()
209 for playlist_id in self.playlists():
210 playlist = self.library.get_playlist_pid(playlist_id)
211 if playlist is not None:
212 all_tracks.update(set(playlist.tracks))
213
214 all_filenames = []
215 for trackID in all_tracks:
216 all_filenames.append(self.library.get_track_filename(trackID))
217
218 gen = libnotipod.sync(
219 dry_run=True,
220 source=self.library.folder,
221 dest=folder,
222 files_to_copy=all_filenames,
223 )
224 self.previewResult = "\n".join(gen)
225
226 @objc.IBAction
227 def doPreview_(self, sender):
228 self.previewResult = ""
229 self.previewWindow.orderOut_(self)
230
231 def finish():
232 self.previewText.textStorage().mutableString().setString_(self.previewResult)
233 self.previewWindow.center()
234 self.previewWindow.makeKeyAndOrderFront_(self)
235
236 self.runGenerator(self.doPreviewThread, finish, None)
237
238 @objc.IBAction
239 def doSync_(self, sender):
240 folder = self.getDestFolder()
241 if not folder:
242 return
243
244 all_tracks = set()
245 orig_playlists = set(self.playlists())
246 all_playlists = orig_playlists.copy()
247 for playlist_id in all_playlists:
248 playlist = self.library.get_playlist_pid(playlist_id)
249 if playlist is None:
250 print "Forgetting unknown playlist:", playlist_id
251 self.setPlaylist_selected_(playlist_id, False)
252 continue
253 all_tracks.update(set(playlist.tracks))
254
255 all_filenames = []
256 for trackID in all_tracks:
257 all_filenames.append(self.library.get_track_filename(trackID))
258 all_playlists.update(self.library.get_track_playlists(trackID))
259
260 for playlist_id in all_playlists:
261 playlist = self.library.get_playlist_pid(playlist_id)
262 if playlist is None:
263 continue
264 tracks = []
265 for trackID in playlist.tracks:
266 if trackID in all_tracks:
267 tracks.append(self.library.get_track_filename(trackID))
268 if playlist_id not in orig_playlists and len(tracks) < 10:
269 continue
270 libnotipod.export_m3u(dry_run=False, dest=folder, path_prefix="",
271 playlist_name=playlist.name, files=tracks)
272
273 def finish():
274 NSRunAlertPanel("Complete!", "Synchronisation is complete", "Ok", None, None)
275 self.runGenerator(
276 lambda:
277 libnotipod.sync(
278 dry_run=False,
279 source=self.library.folder,
280 dest=folder,
281 files_to_copy=all_filenames,
282 )
283 ,
284 finish,
285 None
286 )
287
288
289 # Public accessors
290
291 def prefs(self):
292 return NSUserDefaults.standardUserDefaults()
293
294 def _getArray(self, key):
295 res = self.prefs().stringArrayForKey_(key)
296 return list(res) if res else []
297
298 def _saveArray(self, key, array):
299 self.prefs().setObject_forKey_(array, key)
300
301 def playlists(self):
302 return self._getArray("playlists")
303
304 def folders(self):
305 return self._getArray("folders")
306
307 def addFolder_(self, folder):
308 folders = self.folders()
309 while folder in folders:
310 folders.remove(folder)
311 folders.insert(0, folder)
312 folders = folders[:10]
313 self._saveArray("folders", folders)
314
315 def setPlaylist_selected_(self, playlist, selected):
316 playlists = self.playlists()
317 if selected:
318 playlists.append(playlist)
319 else:
320 playlists.remove(playlist)
321 playlists = list(set(playlists))
322 self._saveArray("playlists", list(set(playlists)))
323
324
325 def main():
326 ### logging.basicConfig(format="%(levelname)s: %(message)s")
327 ### logging.getLogger().setLevel(logging.DEBUG)
328 AppHelper.runEventLoop()
329
330 if __name__ == "__main__":
331 main()
332