1# Copyright (C) 2012 Rocco Aliberti
2# This program is free software; you can redistribute it and/or modify
3# it under the terms of the GNU General Public License as published by
4# the Free Software Foundation; either version 2, or (at your option)
5# any later version.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program; if not, write to the Free Software
14# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
15
16import logging
17import operator
18
19logger = logging.getLogger(__name__)
20import os
21from urllib.parse import urlparse
22import http.client
23import socket
24
25import xml.etree.ElementTree as ETree
26from xl import event, main, playlist, xdg
27from xl.radio import RadioStation, RadioList, RadioItem
28from xl.nls import gettext as _
29from xlgui.panel import radio
30
31STATION = None
32
33
34def enable(exaile):
35    if exaile.loading:
36        event.add_callback(_enable, 'exaile_loaded')
37    else:
38        _enable(None, exaile, None)
39
40
41def _enable(o1, exaile, o2):
42    global STATION
43
44    STATION = SomaFMRadioStation()
45    exaile.radio.add_station(STATION)
46
47
48def disable(exaile):
49    global STATION
50    exaile.radio.remove_station(STATION)
51    STATION = None
52
53
54def set_status(message, timeout=0):
55    radio.set_status(message, timeout)
56
57
58class SomaFMRadioStation(RadioStation):
59
60    name = "somafm"
61
62    def __init__(self):
63        """
64        Initializes the somafm radio station
65        """
66        self.user_agent = main.exaile().get_user_agent_string('somafm')
67        self.somafm_url = 'https://somafm.com/'
68        self.channels_xml_url = self.somafm_url + 'channels.xml'
69        self.cache_file = os.path.join(xdg.get_cache_dir(), 'somafm.cache')
70        self.channelist = ''
71        self.data = {}
72        self._load_cache()
73        self.subs = {}
74        self.playlists = {}
75        self.playlist_id = 0
76        logger.debug(self.user_agent)
77
78    def get_document(self, url):
79        """
80        Connects to the server and retrieves the document
81        """
82        set_status(_('Contacting SomaFM server...'))
83        hostinfo = urlparse(url)
84
85        try:
86            c = http.client.HTTPConnection(hostinfo.netloc, timeout=20)
87        except TypeError:
88            c = http.client.HTTPConnection(hostinfo.netloc)
89
90        try:
91            c.request('GET', hostinfo.path, headers={'User-Agent': self.user_agent})
92            response = c.getresponse()
93        except (socket.timeout, socket.error):
94            raise radio.RadioException(_('Error connecting to SomaFM server.'))
95
96        if response.status != 200:
97            raise radio.RadioException(_('Error connecting to SomaFM server.'))
98
99        document = response.read()
100        c.close()
101
102        set_status('')
103        return document
104
105    def _load_cache(self):
106        """
107        Loads somafm data from cache
108        """
109        self.data = {}
110        if os.path.isfile(self.cache_file):
111            tree = ETree.parse(self.cache_file)
112            for channel in tree.findall('channel'):
113                self.data[channel.get("id")] = channel.get("name")
114
115    def _save_cache(self):
116        """
117        Saves cache data
118        """
119        channellist = ETree.Element('channellist')
120        for channel_id, channel_name in self.data.items():
121            ETree.SubElement(channellist, 'channel', id=channel_id, name=channel_name)
122
123        with open(self.cache_file, 'w') as h:
124            h.write('<?xml version="1.0" encoding="UTF-8"?>')
125            h.write(ETree.tostring(channellist, 'unicode'))
126
127    def get_lists(self, no_cache=False):
128        """
129        Returns the rlists for somafm
130        """
131        if no_cache or not self.data:
132            self.channellist = self.get_document(self.channels_xml_url)
133            data = {}
134            tree = ETree.fromstring(self.channellist)
135
136            for channel in tree.findall('channel'):
137                name = channel.find('title').text
138                data[channel.get("id")] = name
139
140            self.data = data
141            self._save_cache()
142
143        else:
144            data = self.data
145
146        rlists = []
147
148        for id, name in data.items():
149            rlist = RadioList(name, station=self)
150            rlist.get_items = lambda no_cache, id=id: self._get_subrlists(
151                id=id, no_cache=no_cache
152            )
153            rlists.append(rlist)
154
155        rlists.sort(key=operator.attrgetter('name'))
156        self.rlists = rlists
157
158        return rlists
159
160    def _get_subrlists(self, id, no_cache=False):
161        """
162        Gets the subrlists for a rlist
163        """
164        if no_cache or id not in self.subs:
165
166            rlists = self._get_stations(id)
167            rlists.sort(key=operator.attrgetter('name'))
168
169            self.subs[id] = rlists
170
171        return self.subs[id]
172
173    def _get_playlist(self, url, playlist_id):
174        """
175        Gets the playlist for the given url and id
176        """
177        if playlist_id not in self.playlists:
178            set_status(_('Contacting SomaFM server...'))
179            try:
180                self.playlists[playlist_id] = playlist.import_playlist(url)
181            except Exception:
182                set_status(_("Error importing playlist"))
183                logger.exception("Error importing playlist")
184            set_status('')
185
186        return self.playlists[playlist_id]
187
188    def _get_stations(self, id):
189        if not self.channelist:
190            self.channelist = self.get_document(self.channels_xml_url)
191
192        tree = ETree.fromstring(self.channelist)
193        channel = tree.find('.//channel[@id="%s"]' % id)
194        plss = channel.findall('.//*[@format]')
195
196        rlists = []
197        for pls in plss:
198            type = pls.tag.replace('pls', '')
199            format = pls.attrib['format'].upper()
200            url = pls.text
201            display_name = format + " - " + type
202
203            rlist = RadioItem(display_name, station=self)
204            rlist.format = format
205            rlist.get_playlist = (
206                lambda url=url, playlist_id=self.playlist_id: self._get_playlist(
207                    url, playlist_id
208                )
209            )
210
211            self.playlist_id += 1
212            rlists.append(rlist)
213        return rlists
214
215    def get_menu(self, parent):
216        return parent.get_menu()
217