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