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