Fixes MB#8912, crashes when saving song metadata
[mussorgsky] / src / edit_panel_tm.py
1 #!/usr/bin/env python2.5
2 import hildon
3 import gtk, gobject
4 from mutagen_backend import MutagenBackend
5 from player_backend import MediaPlayer
6 from utils import escape_html
7 import album_art_spec
8 import os
9
10 # Fields in the tuple!
11 # Shared with browse_panel
12 URI_COLUMN = 0
13 ARTIST_COLUMN = 2
14 TITLE_COLUMN = 3
15 ALBUM_COLUMN = 4
16 MIME_COLUMN = 5
17 UI_COLUMN = 6
18 SEARCH_COLUMN = 7
19
20 THEME_PATH = "/usr/share/themes/default"
21
22 class MussorgskyEditPanel (hildon.StackableWindow):
23
24     def __init__ (self):
25         hildon.StackableWindow.__init__ (self)
26         self.set_border_width (12)
27         self.album_change_handler = -1
28         self.artist_change_handler = -1
29         self.writer = MutagenBackend ()
30         self.player = MediaPlayer ()
31         self.__create_view ()
32         self.data_loaded = False
33         self.artist_list = None
34         self.albums_list = None
35         self.current = None
36         self.connect ("delete-event", self.close_function)
37
38     def close_function (self, widget, event):
39         if (not self.data_loaded):
40             return
41         
42         if self.__is_view_dirty ():
43             self.save_metadata ()
44
45         
46     def update_title (self):
47         self.set_title ("Edit (%d/%d)" % (self.model.get_path (self.current)[0] + 1,
48                                           len (self.model)))
49
50     def get_current_row (self):
51         if (not self.current):
52             return 6 * (None,)
53         return self.model.get (self.current, 0, 1, 2, 3, 4, 5)
54
55     def set_model (self, model, current=None):
56         assert type(model) == gtk.TreeModelFilter
57         try:
58             if self.artists_list or self.albums_list:
59                 pass
60         except AttributeError, e:
61             print "**** Set album and artist alternatives before setting a model"
62             raise e
63         
64         self.model = model
65         if (current):
66             self.current = current
67         else:
68             self.current = self.model.get_iter_first ()
69         self.data_loaded = True
70         self.set_data_in_view (self.get_current_row ())
71         self.update_title ()
72
73     def set_current (self, current):
74         """
75         Iterator to current element
76         """
77         self.current = current
78         self.set_data_in_view (self.get_current_row ())
79         self.update_title ()
80
81
82     def set_artist_alternatives (self, alternatives):
83         self.artists_list = alternatives
84         artist_selector = hildon.TouchSelectorEntry (text=True)
85         for a in self.artists_list:
86             artist_selector.append_text (a)
87         self.artist_button.set_selector (artist_selector)
88
89     def set_album_alternatives (self, alternatives):
90         self.albums_list = alternatives
91         album_selector = hildon.TouchSelectorEntry (text=True)
92         for a in self.albums_list:
93             album_selector.append_text (a)
94         self.album_button.set_selector (album_selector)
95
96
97     def press_back_cb (self, widget):
98         if (self.player.is_playing ()):
99             self.player.stop ()
100
101         if self.__is_view_dirty ():
102             print "Modified data. Save!"
103             self.save_metadata ()
104             
105         path = self.model.get_path (self.current)
106         if (path[0] == 0):
107             self.destroy ()
108         else:
109             new_path = ( path[0] -1, )
110             self.current = self.model.get_iter (new_path)
111             self.set_data_in_view (self.get_current_row ())
112             self.update_title ()
113
114     def press_next_cb (self, widget):
115         if (self.player.is_playing ()):
116             self.player.stop ()
117
118         if self.__is_view_dirty ():
119             print "Modified data. Save!"
120             self.save_metadata ()
121
122         self.current = self.model.iter_next (self.current)
123         if (not self.current):
124             self.destroy ()
125         else:
126             self.set_data_in_view (self.get_current_row ())
127             self.update_title ()
128
129             
130     def save_metadata (self):
131         # Save the data in the online model to show the appropiate data
132         # in the UI while tracker process the update.
133
134         # 0 - filename -> doesn't change
135         # 1 - "Music"  -> doesn't change
136         # 5 - mimetype -> doesn't change
137         m = self.model.get_model ()
138         c = self.model.convert_iter_to_child_iter (self.current)
139
140         artist = self.artist_button.get_value ()
141         title = self.title_entry.get_text ()
142         album = self.album_button.get_value ()
143         text = "<b>%s</b>\n<small>%s</small>" % (escape_html (title),
144                                                  escape_html (artist) + " / " + escape_html (album))
145         search_str = artist.lower () + " " + title.lower () + " " + album.lower ()
146
147         uri, mime = m.get (c, URI_COLUMN, MIME_COLUMN)
148         m.set (c,
149                ARTIST_COLUMN, artist,
150                TITLE_COLUMN, title,
151                ALBUM_COLUMN, album,
152                UI_COLUMN, text,
153                SEARCH_COLUMN, search_str)
154         try:
155             self.writer.save_metadata_on_file (uri,#new_song[URI_COLUMN],
156                                                mime, #new_song[MIME_COLUMN],
157                                                self.artist_button.get_value (),
158                                                self.title_entry.get_text (),
159                                                self.album_button.get_value ())
160         except IOError, e:
161             # This error in case of tracker returning unexistent files.
162             # Uhm.... for instance after removing a memory card we are editing!
163             pass
164         
165
166     def __is_view_dirty (self):
167         """
168         True if the data has been modified in the widgets
169         """
170         song = self.get_current_row ()
171
172         return not (self.filename_data.get_text() == song[URI_COLUMN] and
173                     self.artist_button.get_value () == song[ARTIST_COLUMN] and
174                     self.title_entry.get_text () == song[TITLE_COLUMN] and
175                     self.album_button.get_value () == song[ALBUM_COLUMN] )
176         
177
178     def __create_view (self):
179         view_vbox = gtk.VBox (homogeneous=False, spacing = 12)
180
181         filename_row = gtk.HBox ()
182         self.filename_data = gtk.Label ("")
183         filename_row.pack_start (self.filename_data, expand=True)
184
185         #filename_row.pack_start (play_button, expand=False, fill=False)
186         view_vbox.pack_start (filename_row, expand=False);
187
188         central_panel = gtk.HBox (spacing=12)
189
190         table = gtk.Table (3, 2, False)
191         table.set_col_spacings (12)
192         table.set_row_spacings (12)
193
194         central_panel.pack_start (table, fill=True)
195         view_vbox.pack_start (central_panel, expand=True, fill=True)
196
197         # Title row
198         label_title = gtk.Label ("Title:")
199         table.attach (label_title, 0, 1, 0, 1, 0)
200         self.title_entry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
201         table.attach (self.title_entry, 1, 2, 0, 1)
202
203         # Artist row
204         self.artist_button = hildon.PickerButton (gtk.HILDON_SIZE_THUMB_HEIGHT,
205                                                   hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
206         self.artist_button.set_title ("Artist: ")
207         # Set data will set the selector
208         table.attach (self.artist_button, 0, 2, 1, 2)
209
210
211         # Album row
212         self.album_button = hildon.PickerButton (gtk.HILDON_SIZE_THUMB_HEIGHT,
213                                                  hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
214         self.album_button.set_title ("Album: ")
215         # set_data will set the selector
216         table.attach (self.album_button, 0, 2, 2, 3)
217         
218
219         # Album art space
220         self.album_art = gtk.Image ()
221         self.album_art.set_size_request (124, 124)
222         central_panel.pack_start (self.album_art, expand=False, fill=False)
223         
224         # Buttons row
225         button_box = gtk.Toolbar ()
226         play_image = os.path.join (THEME_PATH, "mediaplayer", "Play.png")
227         play_button = gtk.ToolButton (gtk.image_new_from_file (play_image))
228         play_button.connect ("clicked", self.clicked_play)                   
229         play_button.set_expand (True)
230         button_box.insert (play_button, -1)
231         
232         separator = gtk.SeparatorToolItem ()
233         separator.set_expand (True)
234         button_box.insert  (separator, -1)
235
236         back_image = os.path.join (THEME_PATH, "mediaplayer", "Back.png")
237         back_button = gtk.ToolButton (gtk.image_new_from_file (back_image))
238         back_button.connect ("clicked", self.press_back_cb)
239         back_button.set_expand (True)
240         button_box.insert (back_button, -1)
241
242         next_image = os.path.join (THEME_PATH, "mediaplayer", "Forward.png")
243         next_button = gtk.ToolButton (gtk.image_new_from_file (next_image))
244         next_button.connect ("clicked", self.press_next_cb)
245         next_button.set_expand (True)
246         button_box.insert (next_button, -1)
247
248         self.add_toolbar (button_box)
249         
250         self.add (view_vbox)
251
252
253     def set_data_in_view (self, song):
254         """
255         Place in the screen the song information.
256         Song is a tuple like (filename, 'Music', title, artist, album, mime)
257         """
258         assert len (song) == 6
259         
260         self.filename_data.set_markup ("<small>" + song[URI_COLUMN] + "</small>")
261         self.title_entry.set_text (song[TITLE_COLUMN])
262         
263
264         # Disconnect the value-change signal to avoid extra album art retrievals
265         if (self.album_button.handler_is_connected (self.album_change_handler)):
266             self.album_button.disconnect (self.album_change_handler)
267             
268         if (self.artist_button.handler_is_connected (self.artist_change_handler)):
269             self.artist_button.disconnect (self.artist_change_handler)
270
271         # Set values in the picker buttons
272         try:
273             self.artist_button.set_active (self.artists_list.index(song[ARTIST_COLUMN]))
274         except ValueError:
275             print "'%s' not in artist list!?" % (song[ARTIST_COLUMN])
276             self.artist_button.set_value ("")
277         except AttributeError:
278             print "WARNING: Use set_artist_alternatives method to set a list of artists"
279             
280         try:
281             self.album_button.set_active (self.albums_list.index (song[ALBUM_COLUMN]))
282         except ValueError:
283             print "'%s' is not in the album list!?" % (song[ALBUM_COLUMN])
284             self.album_button.set_value ("")
285         except AttributeError:
286             print "WARNING: Use set_album_alternatives method to set a list of artists"
287
288         # Reconnect the signals!
289         self.album_change_handler = self.album_button.connect ("value-changed",
290                                                                self.album_selection_cb)
291
292         self.artist_change_handler = self.artist_button.connect ("value-changed",
293                                                                  self.artist_selection_cb)
294
295         # Set the album art given the current data
296         self.set_album_art (song[ALBUM_COLUMN])
297
298         if (not song[MIME_COLUMN] in self.writer.get_supported_mimes ()):
299             self.artist_button.set_sensitive (False)
300             self.album_button.set_sensitive (False)
301             self.title_entry.set_sensitive (False)
302         else:
303             self.artist_button.set_sensitive (True)
304             self.album_button.set_sensitive (True)
305             self.title_entry.set_sensitive (True)
306
307     def set_album_art (self, album):
308         has_album = False
309         if (album):
310             thumb = album_art_spec.getCoverArtThumbFileName (album)
311             print "%s -> %s" % (album, thumb)
312             if (os.path.exists (thumb)):
313                 self.album_art.set_from_file (thumb)
314                 has_album = True
315             
316         if (not has_album):
317             self.album_art.set_from_stock (gtk.STOCK_CDROM, gtk.ICON_SIZE_DIALOG)
318
319         
320     def clicked_play (self, widget):
321         if (self.player.is_playing ()):
322             self.player.stop ()
323         else:
324             song = self.get_current_row ()
325             self.player.play ("file://" + song[URI_COLUMN])
326
327     def album_selection_cb (self, widget):
328         """
329         On album change, add the album the local list of albums and the selector
330         if it doesn't exist already. So we show the new entry in the selector next time.
331         """
332         song = self.get_current_row ()
333         if (not widget.get_value () in self.albums_list):
334             print "Inserting ", widget.get_value ()
335             widget.get_selector ().prepend_text (widget.get_value ())
336             self.albums_list.insert (0, widget.get_value ())
337         self.set_album_art (widget.get_value ())
338
339     def artist_selection_cb (self, widget):
340         """
341         On artist change, add the artist the local list of artists and the selector
342         if it doesn't exist already. So we show the new entry in the selector next time
343         """
344         song = self.get_current_row ()
345         if (not widget.get_value () in self.artists_list):
346             print "Inserting artist", widget.get_value ()
347             widget.get_selector ().prepend_text (widget.get_value ())
348             self.artists_list.insert (0, widget.get_value ())
349
350 # Testing porpuses
351 if __name__ == "__main__":
352
353     TEST_DATA = [("/a/b/c/%d.mp3" %i, "Music",
354                   "Artist %d" % i, "Title %d" % i, "Album %d" % (i*100),
355                   "audio/mpeg",
356                   "artist %d album %d" % (i, i*100),
357                   "text to be searched artist %d album %d" % (i, i*100)) for i in range (0, 4)]
358
359     model = gtk.ListStore (str, str, str, str, str, str, str, str)
360     for t in TEST_DATA:
361         print t
362         model.append (t)
363
364     window = MussorgskyEditPanel ()
365     window.set_artist_alternatives (["Artist %d" % i for i in range (0, 4)])
366     window.set_album_alternatives (["Album %d" % (i*100) for i in range (0, 4)])
367     window.set_model (model.filter_new ())
368     window.connect ("destroy", gtk.main_quit)
369     window.show_all ()
370     gtk.main ()