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