1# Copyright (C) 2008-2010 Adam Olsen 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2, or (at your option) 6# any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# 18# The developers of the Exaile media player hereby grant permission 19# for non-GPL compatible GStreamer and Exaile plugins to be used and 20# distributed together with GStreamer and Exaile. This permission is 21# above and beyond the permissions granted by the GPL license by which 22# Exaile is covered. If you modify this code, you may extend this 23# exception to your version of the code, but you are not obligated to 24# do so. If you do not wish to do so, delete this exception statement 25# from your version. 26 27from gi.repository import Gdk 28from gi.repository import GdkPixbuf 29from gi.repository import Gio 30from gi.repository import GLib 31from gi.repository import GObject 32from gi.repository import Gtk 33 34import xl.radio 35import xl.playlist 36from xl import event, common, settings, trax 37from xl.nls import gettext as _ 38import xlgui.panel.playlists as playlistpanel 39from xlgui.panel import menus 40from xlgui import icons, panel 41from xlgui.widgets.common import DragTreeView 42from xlgui.widgets import dialogs, menu 43 44 45class RadioException(Exception): 46 pass 47 48 49class ConnectionException(RadioException): 50 pass 51 52 53class RadioPanel(panel.Panel, playlistpanel.BasePlaylistPanelMixin): 54 """ 55 The Radio Panel 56 """ 57 58 __gsignals__ = { 59 'playlist-selected': (GObject.SignalFlags.RUN_LAST, None, (object,)), 60 'append-items': (GObject.SignalFlags.RUN_LAST, None, (object, bool)), 61 'replace-items': (GObject.SignalFlags.RUN_LAST, None, (object,)), 62 'queue-items': (GObject.SignalFlags.RUN_LAST, None, (object,)), 63 } 64 __gsignals__.update(playlistpanel.BasePlaylistPanelMixin._gsignals_) 65 66 ui_info = ('radio.ui', 'RadioPanel') 67 _radiopanel = None 68 69 def __init__(self, parent, collection, radio_manager, station_manager, name): 70 """ 71 Initializes the radio panel 72 """ 73 panel.Panel.__init__(self, parent, name, _('Radio')) 74 playlistpanel.BasePlaylistPanelMixin.__init__(self) 75 76 self.collection = collection 77 self.manager = radio_manager 78 self.playlist_manager = station_manager 79 self.nodes = {} 80 self.load_nodes = {} 81 self.complete_reload = {} 82 self.loaded_nodes = [] 83 84 self._setup_tree() 85 self._setup_widgets() 86 self.playlist_image = icons.MANAGER.pixbuf_from_icon_name( 87 'music-library', Gtk.IconSize.SMALL_TOOLBAR 88 ) 89 90 # menus 91 self.playlist_menu = menus.RadioPanelPlaylistMenu(self) 92 self.track_menu = menus.TrackPanelMenu(self) 93 self._connect_events() 94 95 self.load_streams() 96 RadioPanel._radiopanel = self 97 98 @property 99 def menu(self): 100 """ 101 Gets a menu for the selected item 102 :return: xlgui.widgets.menu.Menu or None if do not have it 103 """ 104 model, it = self.tree.get_selection().get_selected() 105 item = model[it][2] 106 if isinstance(item, xl.playlist.Playlist): 107 return self.playlist_menu 108 elif isinstance(item, playlistpanel.TrackWrapper): 109 return self.track_menu 110 else: 111 station = ( 112 item 113 if isinstance(item, xl.radio.RadioStation) 114 else item.station 115 if isinstance(item, (xl.radio.RadioList, xl.radio.RadioItem)) 116 else None 117 ) 118 if station and hasattr(station, 'get_menu'): 119 return station.get_menu(self) 120 121 def load_streams(self): 122 """ 123 Loads radio streams from plugins 124 """ 125 for name in self.playlist_manager.playlists: 126 pl = self.playlist_manager.get_playlist(name) 127 if pl is not None: 128 self.playlist_nodes[pl] = self.model.append( 129 self.custom, [self.playlist_image, pl.name, pl] 130 ) 131 self._load_playlist_nodes(pl) 132 self.tree.expand_row(self.model.get_path(self.custom), False) 133 134 for name, value in self.manager.stations.items(): 135 self.add_driver(value) 136 137 def _add_driver_cb(self, type, object, driver): 138 self.add_driver(driver) 139 140 def add_driver(self, driver): 141 """ 142 Adds a driver to the radio panel 143 """ 144 node = self.model.append(self.radio_root, [self.folder, str(driver), driver]) 145 self.nodes[driver] = node 146 self.load_nodes[driver] = self.model.append( 147 node, [self.refresh_image, _('Loading streams...'), None] 148 ) 149 self.tree.expand_row(self.model.get_path(self.radio_root), False) 150 151 if settings.get_option('gui/radio/%s_station_expanded' % driver.name, False): 152 self.tree.expand_row(self.model.get_path(node), False) 153 154 def _remove_driver_cb(self, type, object, driver): 155 self.remove_driver(driver) 156 157 def remove_driver(self, driver): 158 """ 159 Removes a driver from the radio panel 160 """ 161 if driver in self.nodes: 162 self.model.remove(self.nodes[driver]) 163 del self.nodes[driver] 164 165 def _setup_widgets(self): 166 """ 167 Sets up the various widgets required for this panel 168 """ 169 self.status = self.builder.get_object('status_label') 170 171 @common.idle_add() 172 def _set_status(self, message, timeout=0): 173 self.status.set_text(message) 174 175 if timeout: 176 GLib.timeout_add_seconds(timeout, self._set_status, '', 0) 177 178 def _connect_events(self): 179 """ 180 Connects events used in this panel 181 """ 182 183 self.builder.connect_signals( 184 {'on_add_button_clicked': self._on_add_button_clicked} 185 ) 186 self.tree.connect('row-expanded', self.on_row_expand) 187 self.tree.connect('row-collapsed', self.on_collapsed) 188 self.tree.connect('row-activated', self.on_row_activated) 189 self.tree.connect('key-release-event', self.on_key_released) 190 191 event.add_ui_callback(self._add_driver_cb, 'station_added', self.manager) 192 event.add_ui_callback(self._remove_driver_cb, 'station_removed', self.manager) 193 194 def _on_add_button_clicked(self, *e): 195 dialog = dialogs.MultiTextEntryDialog(self.parent, _("Add Radio Station")) 196 197 dialog.add_field(_("Name:")) 198 url_field = dialog.add_field(_("URL:")) 199 200 clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) 201 text = clipboard.wait_for_text() 202 203 if text is not None: 204 location = Gio.File.new_for_uri(text) 205 206 if location.get_uri_scheme() is not None: 207 url_field.set_text(text) 208 209 result = dialog.run() 210 dialog.hide() 211 212 if result == Gtk.ResponseType.OK: 213 (name, uri) = dialog.get_values() 214 self._do_add_playlist(name, uri) 215 216 @common.threaded 217 def _do_add_playlist(self, name, uri): 218 from xl import playlist, trax 219 220 if playlist.is_valid_playlist(uri): 221 pl = playlist.import_playlist(uri) 222 pl.name = name 223 else: 224 pl = playlist.Playlist(name) 225 tracks = trax.get_tracks_from_uri(uri) 226 pl.extend(tracks) 227 228 self.playlist_manager.save_playlist(pl) 229 self._add_to_tree(pl) 230 231 @common.idle_add() 232 def _add_to_tree(self, pl): 233 self.playlist_nodes[pl] = self.model.append( 234 self.custom, [self.playlist_image, pl.name, pl] 235 ) 236 self._load_playlist_nodes(pl) 237 238 def _setup_tree(self): 239 """ 240 Sets up the tree that displays the radio panel 241 """ 242 box = self.builder.get_object('RadioPanel') 243 self.tree = playlistpanel.PlaylistDragTreeView(self, True, True) 244 self.tree.set_headers_visible(False) 245 246 self.targets = [Gtk.TargetEntry.new('text/uri-list', 0, 0)] 247 248 # columns 249 text = Gtk.CellRendererText() 250 if settings.get_option('gui/ellipsize_text_in_panels', False): 251 from gi.repository import Pango 252 253 text.set_property('ellipsize-set', True) 254 text.set_property('ellipsize', Pango.EllipsizeMode.END) 255 icon = Gtk.CellRendererPixbuf() 256 col = Gtk.TreeViewColumn('radio') 257 col.pack_start(icon, False) 258 col.pack_start(text, True) 259 col.set_attributes(icon, pixbuf=0) 260 col.set_cell_data_func(text, self.cell_data_func) 261 self.tree.append_column(col) 262 263 self.model = Gtk.TreeStore(GdkPixbuf.Pixbuf, str, object) 264 self.tree.set_model(self.model) 265 266 self.track = icons.MANAGER.pixbuf_from_icon_name( 267 'audio-x-generic', Gtk.IconSize.SMALL_TOOLBAR 268 ) 269 self.folder = icons.MANAGER.pixbuf_from_icon_name( 270 'folder', Gtk.IconSize.SMALL_TOOLBAR 271 ) 272 self.refresh_image = icons.MANAGER.pixbuf_from_icon_name('view-refresh') 273 274 self.custom = self.model.append(None, [self.folder, _("Saved Stations"), None]) 275 self.radio_root = self.model.append( 276 None, [self.folder, _("Radio " "Streams"), None] 277 ) 278 279 scroll = Gtk.ScrolledWindow() 280 scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 281 scroll.add(self.tree) 282 scroll.set_shadow_type(Gtk.ShadowType.IN) 283 284 box.pack_start(scroll, True, True, 0) 285 286 def on_row_activated(self, tree, path, column): 287 item = self.model[path][2] 288 if isinstance(item, xl.radio.RadioItem): 289 self.emit('playlist-selected', item.get_playlist()) 290 elif isinstance(item, playlistpanel.TrackWrapper): 291 self.emit('playlist-selected', item.playlist) 292 elif isinstance(item, xl.playlist.Playlist): 293 self.open_station(item) 294 295 def open_station(self, playlist): 296 """ 297 Opens a saved station 298 """ 299 self.emit('playlist-selected', playlist) 300 301 def get_menu(self): 302 """ 303 Returns the menu that all radio stations use 304 """ 305 m = menu.Menu(None) 306 m.add_simple(_("Refresh"), self.on_reload, Gtk.STOCK_REFRESH) 307 return m 308 309 def on_key_released(self, widget, event): 310 """ 311 Called when a key is released in the tree 312 """ 313 if event.keyval == Gdk.KEY_Menu: 314 (mods, paths) = self.tree.get_selection().get_selected_rows() 315 if paths and paths[0]: 316 iter = self.model.get_iter(paths[0]) 317 item = self.model.get_value(iter, 2) 318 if isinstance( 319 item, 320 (xl.radio.RadioStation, xl.radio.RadioList, xl.radio.RadioItem), 321 ): 322 if isinstance(item, xl.radio.RadioStation): 323 station = item 324 else: 325 station = item.station 326 327 if station and hasattr(station, 'get_menu'): 328 menu = station.get_menu(self) 329 menu.popup(event) 330 elif isinstance(item, xl.playlist.Playlist): 331 Gtk.Menu.popup( 332 self.playlist_menu, None, None, None, None, 0, event.time 333 ) 334 elif isinstance(item, playlistpanel.TrackWrapper): 335 Gtk.Menu.popup( 336 self.track_menu, None, None, None, None, 0, event.time 337 ) 338 return True 339 340 if event.keyval == Gdk.KEY_Left: 341 (mods, paths) = self.tree.get_selection().get_selected_rows() 342 if paths and paths[0]: 343 self.tree.collapse_row(paths[0]) 344 return True 345 346 if event.keyval == Gdk.KEY_Right: 347 (mods, paths) = self.tree.get_selection().get_selected_rows() 348 if paths and paths[0]: 349 self.tree.expand_row(paths[0], False) 350 return True 351 352 return False 353 354 def cell_data_func(self, column, cell, model, iter, user_data): 355 """ 356 Called when the tree needs a value for column 1 357 """ 358 object = model.get_value(iter, 1) 359 cell.set_property('text', str(object)) 360 361 def drag_data_received(self, tv, context, x, y, selection, info, etime): 362 """ 363 Called when someone drags some thing onto the playlist panel 364 """ 365 # if the drag originated from radio view deny it 366 # TODO this might change if we are allowed to change the order of radio 367 if Gtk.drag_get_source_widget(context) == tv: 368 context.drop_finish(False, etime) 369 return 370 371 locs = list(selection.get_uris()) 372 373 path = self.tree.get_path_at_pos(x, y) 374 if path: 375 # Add whatever we received to the playlist at path 376 iter = self.model.get_iter(path[0]) 377 current_playlist = self.model.get_value(iter, 2) 378 379 # if it's a track that we've dragged to, get the parent 380 if isinstance(current_playlist, playlistpanel.TrackWrapper): 381 current_playlist = current_playlist.playlist 382 383 elif not isinstance(current_playlist, xl.playlist.Playlist): 384 self._add_new_station(locs) 385 return 386 (tracks, playlists) = self.tree.get_drag_data(locs) 387 current_playlist.extend(tracks) 388 # Do we save in the case when a user drags a file onto a playlist in the playlist panel? 389 # note that the playlist does not have to be open for this to happen 390 self.playlist_manager.save_playlist(current_playlist, overwrite=True) 391 self._load_playlist_nodes(current_playlist) 392 else: 393 self._add_new_station(locs) 394 395 def _add_new_station(self, locs): 396 """ 397 Add a new station 398 """ 399 # If the user dragged files prompt for a new playlist name 400 # else if they dragged a playlist add the playlist 401 402 # We don't want the tracks in the playlists to be added to the 403 # master tracks list so we pass in False 404 (tracks, playlists) = self.tree.get_drag_data(locs, False) 405 # First see if they dragged any playlist files 406 for new_playlist in playlists: 407 self.model.append( 408 self.custom, [self.playlist_image, new_playlist.name, new_playlist] 409 ) 410 # We are adding a completely new playlist with tracks so we save it 411 self.playlist_manager.save_playlist(new_playlist, overwrite=True) 412 413 # After processing playlist proceed to ask the user for the 414 # name of the new playlist to add and add the tracks to it 415 if len(tracks) > 0: 416 dialog = dialogs.TextEntryDialog( 417 _("Enter the name you want for your new playlist"), _("New Playlist") 418 ) 419 result = dialog.run() 420 if result == Gtk.ResponseType.OK: 421 name = dialog.get_value() 422 if not name == "": 423 # Create the playlist from all of the tracks 424 new_playlist = xl.playlist.Playlist(name) 425 new_playlist.extend(tracks) 426 self.playlist_nodes[new_playlist] = self.model.append( 427 self.custom, 428 [self.playlist_image, new_playlist.name, new_playlist], 429 ) 430 self.tree.expand_row(self.model.get_path(self.custom), False) 431 # We are adding a completely new playlist with tracks so we save it 432 self.playlist_manager.save_playlist(new_playlist) 433 self._load_playlist_nodes(new_playlist) 434 435 def drag_get_data(self, tv, context, selection_data, info, time): 436 """ 437 Called when the user drags a playlist from the radio panel 438 """ 439 tracks = self.tree.get_selected_tracks() 440 441 if not tracks: 442 return 443 444 for track in tracks: 445 DragTreeView.dragged_data[track.get_loc_for_io()] = track 446 447 uris = trax.util.get_uris_from_tracks(tracks) 448 selection_data.set_uris(uris) 449 450 def drag_data_delete(self, *e): 451 """ 452 stub 453 """ 454 pass 455 456 def on_reload(self, *e): 457 """ 458 Called when the refresh button is clicked 459 """ 460 selection = self.tree.get_selection() 461 info = selection.get_selected_rows() 462 if not info: 463 return 464 (model, paths) = info 465 iter = self.model.get_iter(paths[0]) 466 object = self.model.get_value(iter, 2) 467 468 try: 469 self.loaded_nodes.remove(self.nodes[object]) 470 except ValueError: 471 pass 472 473 if isinstance(object, (xl.radio.RadioList, xl.radio.RadioStation)): 474 self._clear_node(iter) 475 self.load_nodes[object] = self.model.append( 476 iter, [self.refresh_image, _("Loading streams..."), None] 477 ) 478 479 self.complete_reload[object] = True 480 self.tree.expand_row(self.model.get_path(iter), False) 481 482 @staticmethod 483 def set_station_expanded_value(station, value): 484 settings.set_option('gui/radio/%s_station_expanded' % station, True) 485 486 def on_row_expand(self, tree, iter, path): 487 """ 488 Called when a user expands a row in the tree 489 """ 490 driver = self.model.get_value(iter, 2) 491 492 if not isinstance(driver, xl.playlist.Playlist): 493 self.model.set_value(iter, 0, self.folder) 494 495 if isinstance(driver, xl.radio.RadioStation) or isinstance( 496 driver, xl.radio.RadioList 497 ): 498 if not self.nodes[driver] in self.loaded_nodes: 499 self._load_station(iter, driver) 500 501 if isinstance(driver, xl.radio.RadioStation): 502 self.set_station_expanded_value(driver.name, True) 503 504 def on_collapsed(self, tree, iter, path): 505 """ 506 Called when someone collapses a tree item 507 """ 508 driver = self.model.get_value(iter, 2) 509 510 if not isinstance(driver, xl.playlist.Playlist): 511 self.model.set_value(iter, 0, self.folder) 512 513 if isinstance(driver, xl.radio.RadioStation): 514 self.set_station_expanded_value(driver.name, False) 515 516 @common.threaded 517 def _load_station(self, iter, driver): 518 """ 519 Loads a radio station 520 """ 521 lists = None 522 no_cache = False 523 if driver in self.complete_reload: 524 no_cache = True 525 del self.complete_reload[driver] 526 527 if isinstance(driver, xl.radio.RadioStation): 528 try: 529 lists = driver.get_lists(no_cache=no_cache) 530 except RadioException as e: 531 self._set_status(str(e), 2) 532 else: 533 try: 534 lists = driver.get_items(no_cache=no_cache) 535 except RadioException as e: 536 self._set_status(str(e), 2) 537 538 if not lists: 539 return 540 GLib.idle_add(self._done_loading, iter, driver, lists) 541 542 def _done_loading(self, iter, object, items): 543 """ 544 Called when an item is done loading. Adds items to the tree 545 """ 546 self.loaded_nodes.append(self.nodes[object]) 547 for item in items: 548 if isinstance(item, xl.radio.RadioList): 549 node = self.model.append( 550 self.nodes[object], [self.folder, item.name, item] 551 ) 552 self.nodes[item] = node 553 self.load_nodes[item] = self.model.append( 554 node, [self.refresh_image, _("Loading streams..."), None] 555 ) 556 else: 557 self.model.append(self.nodes[object], [self.track, item.name, item]) 558 559 try: 560 self.model.remove(self.load_nodes[object]) 561 del self.load_nodes[object] 562 except KeyError: 563 pass 564 565 def _clear_node(self, node): 566 """ 567 Clears a node of all children 568 """ 569 remove = [] 570 iter = self.model.iter_children(node) 571 while iter: 572 remove.append(iter) 573 iter = self.model.iter_next(iter) 574 for row in remove: 575 self.model.remove(row) 576 577 578def set_status(message, timeout=0): 579 RadioPanel._radiopanel._set_status(message, timeout) 580