Skip to content
module.py 12 KiB
Newer Older
Pierre Mazière's avatar
Pierre Mazière committed
# -*- coding: utf-8 -*-

# Copyright(C) 2013 Pierre Mazière
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.


from weboob.capabilities.radio import CapRadio, Radio
from weboob.capabilities.audiostream import BaseAudioStream
from weboob.tools.capabilities.streaminfo import StreamInfo
from weboob.capabilities.collection import CapCollection, Collection
from weboob.tools.backend import Module, BackendConfig
Pierre Mazière's avatar
Pierre Mazière committed
from weboob.tools.value import Value
from weboob.deprecated.browser import StandardBrowser
Pierre Mazière's avatar
Pierre Mazière committed
import time

Florent's avatar
Florent committed
__all__ = ['AudioAddictModule']
Florent's avatar
Florent committed

# AudioAddict playlists do not seem to be appreciated by mplayer
# VLC plays them successfully, therefore I advice to set the media_player
# option to another player in the ~/.config/weboob/radioob config file:
# [ROOT]
# media_player = your_non_mplayer_player
class AudioAddictModule(Module, CapRadio, CapCollection):
Pierre Mazière's avatar
Pierre Mazière committed
    NAME = 'audioaddict'
    MAINTAINER = u'Pierre Mazière'
    EMAIL = 'pierre.maziere@gmx.com'
Florent's avatar
Florent committed
    VERSION = '1.2'
Pierre Mazière's avatar
Pierre Mazière committed
    DESCRIPTION = u'Internet radios powered by audioaddict.com services'
    LICENSE = 'AGPLv3+'
    BROWSER = StandardBrowser

    # Data extracted from http://tobiass.eu/api-doc.html
Pierre Mazière's avatar
Pierre Mazière committed
    NETWORKS = {
        'DI': {
            'desc': 'Digitally Imported addictive electronic music',
            'domain': 'di.fm',
Florent's avatar
Florent committed
            'streams': {'android_low': {'rate': 40, 'fmt': 'aac'},
Pierre Mazière's avatar
Pierre Mazière committed
                        'android': {'rate': 64, 'fmt': 'aac'},
                        'android_high': {'rate': 96, 'fmt': 'aac'},
                        'android_premium_low': {'rate': 40, 'fmt': 'aac'},
                        'android_premium_medium': {'rate': 64, 'fmt': 'aac'},
                        'android_premium': {'rate': 128, 'fmt': 'aac'},
                        'android_premium_high': {'rate': 256, 'fmt': 'aac'},
                        'public1': {'rate': 64, 'fmt': 'aac'},
                        'public2': {'rate': 40, 'fmt': 'aac'},
                        'public3': {'rate': 96, 'fmt': 'mp3'},
                        'premium_low': {'rate': 40, 'fmt': 'aac'},
                        'premium_medium': {'rate': 64, 'fmt': 'aac'},
                        'premium': {'rate': 128, 'fmt': 'aac'},
                        'premium_high': {'rate': 256, 'fmt': 'mp3'}
                        }
        },
        'RadioTunes': {
            'desc': 'Radio Tunes',
            'domain': 'radiotunes.com',
Pierre Mazière's avatar
Pierre Mazière committed
            'streams': {'appleapp_low': {'rate': 40, 'fmt': 'aac'},
                        'appleapp': {'rate': 64, 'fmt': 'aac'},
                        'appleapp_high': {'rate': 96, 'fmt': 'mp3'},
                        'appleapp_premium_medium': {'rate': 64, 'fmt': 'aac'},
                        'appleapp_premium': {'rate': 128, 'fmt': 'aac'},
                        'appleapp_premium_high': {'rate': 256, 'fmt': 'mp3'},
                        'public1': {'rate': 40, 'fmt': 'aac'},
                        'public5': {'rate': 40, 'fmt': 'wma'},
                        'public3': {'rate': 96, 'fmt': 'mp3'},
                        'premium_low': {'rate': 40, 'fmt': 'aac'},
                        'premium_medium': {'rate': 64, 'fmt': 'aac'},
                        'premium': {'rate': 128, 'fmt': 'aac'},
                        'premium_high': {'rate': 256, 'fmt': 'mp3'}
                        }
        },
        'JazzRadio': {
            'desc': 'Jazz Radio',
            'domain': 'jazzradio.com',
            'streams': {'appleapp_low': {'rate': 40, 'fmt': 'aac'},
                        'appleapp': {'rate': 64, 'fmt': 'aac'},
                        'appleapp_premium_medium': {'rate': 64, 'fmt': 'aac'},
                        'appleapp_premium': {'rate': 128, 'fmt': 'aac'},
                        'appleapp_premium_high': {'rate': 256, 'fmt': 'mp3'},
                        'public1': {'rate': 40, 'fmt': 'aac'},
                        'public3': {'rate': 64, 'fmt': 'mp3'},
                        'premium_low': {'rate': 40, 'fmt': 'aac'},
                        'premium_medium': {'rate': 64, 'fmt': 'aac'},
                        'premium': {'rate': 128, 'fmt': 'aac'},
                        'premium_high': {'rate': 256, 'fmt': 'mp3'}
                        }
        },
        'RockRadio': {
            'desc': 'Rock Radio',
            'domain': 'rockradio.com',
            'streams': {'android_low': {'rate': 40, 'fmt': 'aac'},
                        'android': {'rate': 64, 'fmt': 'aac'},
                        'android_premium_medium': {'rate': 64, 'fmt': 'aac'},
                        'android_premium': {'rate': 128, 'fmt': 'aac'},
                        'android_premium_high': {'rate': 256, 'fmt': 'mp3'},
                        'public3': {'rate': 96, 'fmt': 'mp3'}
Florent's avatar
Florent committed
                        }
Pierre Mazière's avatar
Pierre Mazière committed
        },
        'FrescaRadio': {
            'desc': 'Fresca Radio',
            'domain': 'frescaradio.com',
            'streams': {
                        'public3': {'rate': 96, 'fmt': 'mp3'}
            }
Pierre Mazière's avatar
Pierre Mazière committed
    }

    CONFIG = BackendConfig(Value('networks',
Florent's avatar
Florent committed
                                 label='Selected Networks [%s](space separated)' %
Pierre Mazière's avatar
Pierre Mazière committed
                                 ' '.join(NETWORKS.keys()), default=''),
                           Value('quality', label='Radio streaming quality',
Florent's avatar
Florent committed
                                 choices={'h': 'high', 'l': 'low'},
Pierre Mazière's avatar
Pierre Mazière committed
                                 default='h')
                           )

    def __init__(self, *a, **kw):
Florent's avatar
Florent committed
        super(AudioAddictModule, self).__init__(*a, **kw)
Pierre Mazière's avatar
Pierre Mazière committed
        self.RADIOS = {}
Pierre Mazière's avatar
Pierre Mazière committed

    def _get_tracks_history(self, network):
        self._fetch_radio_list(network)
Florent's avatar
Florent committed
        domain = self.NETWORKS[network]['domain']
        url = 'http://api.audioaddict.com/v1/%s/track_history' %\
              (domain[:domain.rfind('.')])
Pierre Mazière's avatar
Pierre Mazière committed
        self.HISTORY[network] = self.browser.location(url)
        return self.HISTORY

    def create_default_browser(self):
        return self.create_browser(parser='json')

Florent's avatar
Florent committed
    def _get_stream_name(self, network, quality):
        streamName = 'public3'
Pierre Mazière's avatar
Pierre Mazière committed
        for name in self.NETWORKS[network]['streams'].keys():
            if name.startswith('public') and \
Florent's avatar
Florent committed
               self.NETWORKS[network]['streams'][name]['rate'] >= 64:
                if quality == 'h':
                    streamName = name
Pierre Mazière's avatar
Pierre Mazière committed
                    break
            else:
Florent's avatar
Florent committed
                if quality == 'l':
                    streamName = name
Pierre Mazière's avatar
Pierre Mazière committed
                    break
        return streamName

Florent's avatar
Florent committed
    def _fetch_radio_list(self, network=None):
        quality = self.config['quality'].get()
        for selectedNetwork in self.config['networks'].get().split():
            if network is None or network == selectedNetwork:
Florent's avatar
Florent committed
                streamName = self._get_stream_name(selectedNetwork, quality)
                if not self.RADIOS:
Florent's avatar
Florent committed
                    self.RADIOS = {}
                if selectedNetwork not in self.RADIOS:
Florent's avatar
Florent committed
                    document = self.browser.location('http://listen.%s/%s' %
                                                     (self.NETWORKS[selectedNetwork]['domain'],
                                                      streamName))
Florent's avatar
Florent committed
                    self.RADIOS[selectedNetwork] = {}
                    for info in document:
                        radio = info['key']
                        self.RADIOS[selectedNetwork][radio] = {}
                        self.RADIOS[selectedNetwork][radio]['id'] = info['id']
                        self.RADIOS[selectedNetwork][radio]['name'] = info['name']
                        self.RADIOS[selectedNetwork][radio]['playlist'] = info['playlist']
Pierre Mazière's avatar
Pierre Mazière committed

        return self.RADIOS

    def iter_radios_search(self, pattern):
        self._fetch_radio_list()

        pattern = pattern.lower()
        for network in self.config['networks'].get().split():
            for radio in self.RADIOS[network]:
                radio_dict = self.RADIOS[network][radio]
                if pattern in radio_dict['name'].lower() :
                    yield self.get_radio(radio+"."+network)
Pierre Mazière's avatar
Pierre Mazière committed

    def iter_resources(self, objs, split_path):
        self._fetch_radio_list()

        if Radio in objs:
            for network in self.config['networks'].get().split():
                if split_path == [network]:
                    for radio in self.RADIOS[network]:
                        yield self.get_radio(radio+"."+network)
Pierre Mazière's avatar
Pierre Mazière committed
                    return
            for network in self.config['networks'].get().split():
Florent's avatar
Florent committed
                yield Collection([network], self.NETWORKS[network]['desc'])
Pierre Mazière's avatar
Pierre Mazière committed

    def get_current(self, network, radio):
Florent's avatar
Florent committed
        channel = {}
        if network not in self.HISTORY:
Pierre Mazière's avatar
Pierre Mazière committed
            self._get_tracks_history(network)
Florent's avatar
Florent committed
            channel = self.HISTORY[network].get(str(self.RADIOS[network][radio]['id']))
Pierre Mazière's avatar
Pierre Mazière committed
        else:
            now=time.time()
Florent's avatar
Florent committed
            channel = self.HISTORY[network].get(str(self.RADIOS[network][radio]['id']))
Pierre Mazière's avatar
Pierre Mazière committed
            if channel is None:
                return 'Unknown', 'Unknown'
            if (channel.get('started')+channel.get('duration')) < now:
                self._get_tracks_history(network)
                channel=self.HISTORY[network].get(str(self.RADIOS[network][radio]['id']))

        artist = u'' + (channel.get('artist', '') or 'Unknown')
        title = u''+(channel.get('title', '') or 'Unknown')

Florent's avatar
Florent committed
        if artist == 'Unknown':
            track = u'' + (channel.get('track', '') or 'Unknown')
            if track != 'Unknown':
                artist = track[:track.find(' - ')]

Pierre Mazière's avatar
Pierre Mazière committed
        return artist, title

    def get_radio(self, radio):
Pierre Mazière's avatar
Pierre Mazière committed
        if not isinstance(radio, Radio):
            radio = Radio(radio)

Florent's avatar
Florent committed
        radioName, network = radio.id.split('.', 1)
        self._fetch_radio_list(network)

        if radioName not in self.RADIOS[network]:
Pierre Mazière's avatar
Pierre Mazière committed
            return None

        radio_dict = self.RADIOS[network][radioName]
Pierre Mazière's avatar
Pierre Mazière committed
        radio.title = radio_dict['name']
        radio.description = radio_dict['name']
Florent's avatar
Florent committed
        artist, title = self.get_current(network, radioName)
        current = StreamInfo(0)
        current.who = artist
        current.what = title
Pierre Mazière's avatar
Pierre Mazière committed
        radio.current = current

        radio.streams = []
Florent's avatar
Florent committed
        defaultname = self._get_stream_name(network, self.config['quality'].get())
        stream = BaseAudioStream(0)
Florent's avatar
Florent committed
        stream.bitrate = self.NETWORKS[network]['streams'][defaultname]['rate']
        stream.format = self.NETWORKS[network]['streams'][defaultname]['fmt']
        stream.title = u'%s %skbps' % (stream.format, stream.bitrate)
        stream.url = 'http://listen.%s/%s/%s.pls' %\
                     (self.NETWORKS[network]['domain'], defaultname, radioName)
Pierre Mazière's avatar
Pierre Mazière committed
        radio.streams.append(stream)
Florent's avatar
Florent committed
        i = 1
        for name in self.NETWORKS[network]['streams'].keys():
            if name == defaultname:
                continue
            stream = BaseAudioStream(i)
Florent's avatar
Florent committed
            stream.bitrate = self.NETWORKS[network]['streams'][name]['rate']
            stream.format = self.NETWORKS[network]['streams'][name]['fmt']
            stream.title = u'%s %skbps' % (stream.format, stream.bitrate)
            stream.url = 'http://listen.%s/%s/%s.pls'%\
Florent's avatar
Florent committed
                         (self.NETWORKS[network]['domain'], name, radioName)
Florent's avatar
Florent committed
            i = i + 1
Pierre Mazière's avatar
Pierre Mazière committed
        return radio

    def fill_radio(self, radio, fields):
        if 'current' in fields:
Florent's avatar
Florent committed
            radioName, network = radio.id.split('.', 1)
            radio.current = StreamInfo(0)
Florent's avatar
Florent committed
            radio.current.who, radio.current.what = self.get_current(network, radioName)
Pierre Mazière's avatar
Pierre Mazière committed
            return radio

    OBJECTS = {Radio: fill_radio}