././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1612990371.1848197
Mopidy-SomaFM-2.0.2/ 0000755 0001750 0001750 00000000000 00000000000 014565 5 ustar 00alexandre alexandre ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1612990371.1814864
Mopidy-SomaFM-2.0.2/.circleci/ 0000755 0001750 0001750 00000000000 00000000000 016420 5 ustar 00alexandre alexandre ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/.circleci/config.yml 0000644 0001750 0001750 00000002065 00000000000 020413 0 ustar 00alexandre alexandre version: 2.1
orbs:
codecov: codecov/codecov@1.0.5
workflows:
version: 2
test:
jobs:
- py38
- py37
- black
- check-manifest
- flake8
jobs:
py38: &test-template
docker:
- image: mopidy/ci-python:3.8
steps:
- checkout
- restore_cache:
name: Restoring tox cache
key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }}
- run:
name: Run tests
command: |
tox -e $CIRCLE_JOB -- \
--junit-xml=test-results/pytest/results.xml \
--cov-report=xml
- save_cache:
name: Saving tox cache
key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }}
paths:
- ./.tox
- ~/.cache/pip
- codecov/upload:
file: coverage.xml
- store_test_results:
path: test-results
py37:
<<: *test-template
docker:
- image: mopidy/ci-python:3.7
black: *test-template
check-manifest: *test-template
flake8: *test-template
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612990213.0
Mopidy-SomaFM-2.0.2/CHANGES.rst 0000644 0001750 0001750 00000003446 00000000000 016376 0 ustar 00alexandre alexandre Changelog
=========
v2.0.2 (2021-02-10)
-------------------
- #40 Fix exception on URI parsing
v2.0.1 (2021-01-07)
-------------------
- #37 Fix image display (Thanks to @dreamlayers and @morithil)
v2.0.0 (2020-03-11)
-------------------
- #36 Ready for Mopidy 3.0
v2.0.0rc1 (2019-12-04)
----------------------
- #32 Migrate to Python 3.7
v1.1.0 (2017-10-14)
-------------------
- #24: Graceful fallback
- #28: Various fix (DJ as artist, station ordering)
v1.0.1 (2016-01-19)
-------------------
- Use httpclient helper from Mopidy >= 1.1
v0.8.0 (2015-11-09)
-------------------
- #20: Replace HTTP with HTTPS for channels.xml
v0.7.1 (2015-01-04)
-------------------
- #11: Add Low Bitrate encoding (aacp)
v0.7.0 (2014-07-29)
-------------------
- #10: Remove playlists provider
v0.6.0 (2014-03-15)
-------------------
- Directly show PLS in browser
- Add precision about 'quality' and 'encoding' couple
v0.5.1 (2014-03-09)
-------------------
- Fix doc typo
v0.5.0 (2014-03-03)
-------------------
- #5: Select prefered quality and format from config
- Add tests and Travis-CI support
v0.4.0 (2014-02-16)
-------------------
- Add browse support for LibraryController
v0.3.1 (2014-01-30)
-------------------
- #3: Correct wrong subclassing
v0.3.0 (2014-01-29)
-------------------
- Require Mopidy >= 0.18
- Add proxy support for downloading SomaFM content
- #1: handle 'requests' exceptions
- Use builtin Mopidy's .pls support
- Internal code cleanup
v0.2.0 (2013-09-22)
-------------------
- PLS files are downloaded to local temp directory
- Implement library::lookup to allow adding tracks from playlist uri
v0.1.1 (2013-09-14)
-------------------
- Update Licence information
v0.1.0 (2013-09-13)
-------------------
- Initial release
- Create SomaFM extension for Mopidy
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/LICENSE 0000644 0001750 0001750 00000002076 00000000000 015577 0 ustar 00alexandre alexandre The MIT License (MIT)
Copyright (c) 2019 Alexandre Petitjean
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/MANIFEST.in 0000644 0001750 0001750 00000000412 00000000000 016320 0 ustar 00alexandre alexandre include *.py
include *.rst
include .mailmap
include LICENSE
include MANIFEST.in
include pyproject.toml
include tox.ini
recursive-include .circleci *
recursive-include .github *
include mopidy_*/ext.conf
recursive-include tests *.py
recursive-include tests/data *
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1612990371.1848197
Mopidy-SomaFM-2.0.2/Mopidy_SomaFM.egg-info/ 0000755 0001750 0001750 00000000000 00000000000 020722 5 ustar 00alexandre alexandre ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612990371.0
Mopidy-SomaFM-2.0.2/Mopidy_SomaFM.egg-info/PKG-INFO 0000644 0001750 0001750 00000006542 00000000000 022026 0 ustar 00alexandre alexandre Metadata-Version: 2.1
Name: Mopidy-SomaFM
Version: 2.0.2
Summary: SomaFM extension for Mopidy
Home-page: https://github.com/AlexandrePTJ/mopidy-somafm
Author: Alexandre Petitjean
Author-email: alpetitjean@gmail.com
License: Apache License, Version 2.0
Description: *************
Mopidy-SomaFM
*************
.. image:: https://img.shields.io/pypi/v/Mopidy-SomaFM
:target: https://pypi.org/project/Mopidy-SomaFM/
:alt: Latest PyPI version
.. image:: https://img.shields.io/circleci/build/gh/AlexandrePTJ/mopidy-somafm
:target: https://circleci.com/gh/AlexandrePTJ/mopidy-somafm
:alt: CircleCI build status
.. image:: https://img.shields.io/codecov/c/gh/AlexandrePTJ/mopidy-somafm
:target: https://codecov.io/gh/AlexandrePTJ/mopidy-somafm
:alt: Test coverage
SomaFM extension for Mopidy
Installation
============
Debian/Ubuntu
-------------
Install by running::
python3 -m pip install Mopidy-SomaFM
Or, if available, install the Debian/Ubuntu package from
`apt.mopidy.com `_.
Configuration
=============
Before starting Mopidy, you must add configuration for
Mopidy-SomaFM to your Mopidy configuration file::
[somafm]
encoding = aac
quality = highest
- ``encoding`` must be either ``aac``, ``mp3`` or ``aacp``
- ``quality`` must be one of ``highest``, ``fast``, ``slow``, ``firewall``
If the preferred quality is not available for a channel, the extension will fallback
to ``fast``. And afterwards if the preferred encoding is not available for that
quality, it will fallback to using ``mp3``.
It seems that all channels support the combination ``fast`` + ``mp3``
You can also choose to use the channel DJ as the reported track artist (default behavior)::
[somafm]
dj_as_artist = true
Project resources
=================
- `Source code `_
- `Issue tracker `_
- `Changelog `_
Credits
=======
- Original author: `Alexandre Petitjean `__
- Current maintainer: `Alexandre Petitjean `__
- `Contributors `_
Platform: UNKNOWN
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
Requires-Python: >=3.7
Provides-Extra: lint
Provides-Extra: release
Provides-Extra: test
Provides-Extra: dev
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612990371.0
Mopidy-SomaFM-2.0.2/Mopidy_SomaFM.egg-info/SOURCES.txt 0000644 0001750 0001750 00000001016 00000000000 022604 0 ustar 00alexandre alexandre CHANGES.rst
LICENSE
MANIFEST.in
README.rst
pyproject.toml
setup.cfg
setup.py
tox.ini
.circleci/config.yml
Mopidy_SomaFM.egg-info/PKG-INFO
Mopidy_SomaFM.egg-info/SOURCES.txt
Mopidy_SomaFM.egg-info/dependency_links.txt
Mopidy_SomaFM.egg-info/entry_points.txt
Mopidy_SomaFM.egg-info/not-zip-safe
Mopidy_SomaFM.egg-info/requires.txt
Mopidy_SomaFM.egg-info/top_level.txt
mopidy_somafm/__init__.py
mopidy_somafm/backend.py
mopidy_somafm/ext.conf
mopidy_somafm/somafm.py
tests/__init__.py
tests/test_extension.py
tests/test_somafm.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612990371.0
Mopidy-SomaFM-2.0.2/Mopidy_SomaFM.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 00000000000 024770 0 ustar 00alexandre alexandre
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612990371.0
Mopidy-SomaFM-2.0.2/Mopidy_SomaFM.egg-info/entry_points.txt 0000644 0001750 0001750 00000000057 00000000000 024222 0 ustar 00alexandre alexandre [mopidy.ext]
somafm = mopidy_somafm:Extension
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987350.0
Mopidy-SomaFM-2.0.2/Mopidy_SomaFM.egg-info/not-zip-safe 0000644 0001750 0001750 00000000001 00000000000 023150 0 ustar 00alexandre alexandre
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612990371.0
Mopidy-SomaFM-2.0.2/Mopidy_SomaFM.egg-info/requires.txt 0000644 0001750 0001750 00000000462 00000000000 023324 0 ustar 00alexandre alexandre Mopidy>=3.0
Pykka>=2.0.1
requests>=2.0.0
setuptools
[dev]
black
check-manifest
flake8
flake8-bugbear
flake8-import-order
isort[pyproject]
twine
wheel
pytest
pytest-cov
[lint]
black
check-manifest
flake8
flake8-bugbear
flake8-import-order
isort[pyproject]
[release]
twine
wheel
[test]
pytest
pytest-cov
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612990371.0
Mopidy-SomaFM-2.0.2/Mopidy_SomaFM.egg-info/top_level.txt 0000644 0001750 0001750 00000000016 00000000000 023451 0 ustar 00alexandre alexandre mopidy_somafm
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1612990371.1848197
Mopidy-SomaFM-2.0.2/PKG-INFO 0000644 0001750 0001750 00000006542 00000000000 015671 0 ustar 00alexandre alexandre Metadata-Version: 2.1
Name: Mopidy-SomaFM
Version: 2.0.2
Summary: SomaFM extension for Mopidy
Home-page: https://github.com/AlexandrePTJ/mopidy-somafm
Author: Alexandre Petitjean
Author-email: alpetitjean@gmail.com
License: Apache License, Version 2.0
Description: *************
Mopidy-SomaFM
*************
.. image:: https://img.shields.io/pypi/v/Mopidy-SomaFM
:target: https://pypi.org/project/Mopidy-SomaFM/
:alt: Latest PyPI version
.. image:: https://img.shields.io/circleci/build/gh/AlexandrePTJ/mopidy-somafm
:target: https://circleci.com/gh/AlexandrePTJ/mopidy-somafm
:alt: CircleCI build status
.. image:: https://img.shields.io/codecov/c/gh/AlexandrePTJ/mopidy-somafm
:target: https://codecov.io/gh/AlexandrePTJ/mopidy-somafm
:alt: Test coverage
SomaFM extension for Mopidy
Installation
============
Debian/Ubuntu
-------------
Install by running::
python3 -m pip install Mopidy-SomaFM
Or, if available, install the Debian/Ubuntu package from
`apt.mopidy.com `_.
Configuration
=============
Before starting Mopidy, you must add configuration for
Mopidy-SomaFM to your Mopidy configuration file::
[somafm]
encoding = aac
quality = highest
- ``encoding`` must be either ``aac``, ``mp3`` or ``aacp``
- ``quality`` must be one of ``highest``, ``fast``, ``slow``, ``firewall``
If the preferred quality is not available for a channel, the extension will fallback
to ``fast``. And afterwards if the preferred encoding is not available for that
quality, it will fallback to using ``mp3``.
It seems that all channels support the combination ``fast`` + ``mp3``
You can also choose to use the channel DJ as the reported track artist (default behavior)::
[somafm]
dj_as_artist = true
Project resources
=================
- `Source code `_
- `Issue tracker `_
- `Changelog `_
Credits
=======
- Original author: `Alexandre Petitjean `__
- Current maintainer: `Alexandre Petitjean `__
- `Contributors `_
Platform: UNKNOWN
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
Requires-Python: >=3.7
Provides-Extra: lint
Provides-Extra: release
Provides-Extra: test
Provides-Extra: dev
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/README.rst 0000644 0001750 0001750 00000003766 00000000000 016270 0 ustar 00alexandre alexandre *************
Mopidy-SomaFM
*************
.. image:: https://img.shields.io/pypi/v/Mopidy-SomaFM
:target: https://pypi.org/project/Mopidy-SomaFM/
:alt: Latest PyPI version
.. image:: https://img.shields.io/circleci/build/gh/AlexandrePTJ/mopidy-somafm
:target: https://circleci.com/gh/AlexandrePTJ/mopidy-somafm
:alt: CircleCI build status
.. image:: https://img.shields.io/codecov/c/gh/AlexandrePTJ/mopidy-somafm
:target: https://codecov.io/gh/AlexandrePTJ/mopidy-somafm
:alt: Test coverage
SomaFM extension for Mopidy
Installation
============
Debian/Ubuntu
-------------
Install by running::
python3 -m pip install Mopidy-SomaFM
Or, if available, install the Debian/Ubuntu package from
`apt.mopidy.com `_.
Configuration
=============
Before starting Mopidy, you must add configuration for
Mopidy-SomaFM to your Mopidy configuration file::
[somafm]
encoding = aac
quality = highest
- ``encoding`` must be either ``aac``, ``mp3`` or ``aacp``
- ``quality`` must be one of ``highest``, ``fast``, ``slow``, ``firewall``
If the preferred quality is not available for a channel, the extension will fallback
to ``fast``. And afterwards if the preferred encoding is not available for that
quality, it will fallback to using ``mp3``.
It seems that all channels support the combination ``fast`` + ``mp3``
You can also choose to use the channel DJ as the reported track artist (default behavior)::
[somafm]
dj_as_artist = true
Project resources
=================
- `Source code `_
- `Issue tracker `_
- `Changelog `_
Credits
=======
- Original author: `Alexandre Petitjean `__
- Current maintainer: `Alexandre Petitjean `__
- `Contributors `_
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1612990371.1848197
Mopidy-SomaFM-2.0.2/mopidy_somafm/ 0000755 0001750 0001750 00000000000 00000000000 017430 5 ustar 00alexandre alexandre ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/mopidy_somafm/__init__.py 0000644 0001750 0001750 00000001556 00000000000 021550 0 ustar 00alexandre alexandre import logging
import pathlib
import pkg_resources
from mopidy import config, ext
__version__ = pkg_resources.get_distribution("Mopidy-SomaFM").version
logger = logging.getLogger(__name__)
class Extension(ext.Extension):
dist_name = "Mopidy-SomaFM"
ext_name = "somafm"
version = __version__
def get_default_config(self):
return config.read(pathlib.Path(__file__).parent / "ext.conf")
def get_config_schema(self):
schema = super().get_config_schema()
schema["encoding"] = config.String(choices=("aac", "mp3", "aacp"))
schema["quality"] = config.String(
choices=("highest", "fast", "slow", "firewall")
)
schema["dj_as_artist"] = config.Boolean()
return schema
def setup(self, registry):
from .backend import SomaFMBackend
registry.add("backend", SomaFMBackend)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612990213.0
Mopidy-SomaFM-2.0.2/mopidy_somafm/backend.py 0000644 0001750 0001750 00000010246 00000000000 021374 0 ustar 00alexandre alexandre import logging
import mopidy_somafm
import pykka
import requests
import configparser
import random
from mopidy import backend, httpclient
from mopidy.models import Album, Artist, Image, Ref, Track
from .somafm import SomaFMClient, extract_somafm_channel_name_from_uri
logger = logging.getLogger(__name__)
class SomaFMBackend(pykka.ThreadingActor, backend.Backend):
def __init__(self, config, audio):
super().__init__()
user_agent = "{}/{}".format(
mopidy_somafm.Extension.dist_name, mopidy_somafm.__version__
)
self.somafm = SomaFMClient(config["proxy"], user_agent)
self.library = SomaFMLibraryProvider(backend=self)
self.playback = SomaFMPlayback(
audio=audio,
backend=self,
proxy_config=config["proxy"],
user_agent=user_agent,
)
self.uri_schemes = ["somafm"]
self.quality = config["somafm"]["quality"]
self.encoding = config["somafm"]["encoding"]
self.dj_as_artist = config["somafm"]["dj_as_artist"]
def on_start(self):
self.somafm.refresh(self.encoding, self.quality)
class SomaFMLibraryProvider(backend.LibraryProvider):
root_directory = Ref.directory(uri="somafm:root", name="SomaFM")
def lookup(self, uri):
# Whatever the uri, it will always contains one track
# which is a url to a pls
channel_name = extract_somafm_channel_name_from_uri(uri)
if channel_name is None:
return None
channel_data = self.backend.somafm.channels[channel_name]
# Artists
if self.backend.dj_as_artist:
artist = Artist(name=channel_data["dj"])
else:
artist = Artist()
# Build album (idem as playlist, but with more metada)
album = Album(
artists=[artist],
name=channel_data["title"],
)
track = Track(
artists=[artist],
album=album,
last_modified=channel_data["updated"],
comment=channel_data["description"],
genre=channel_data["genre"],
name=channel_data["title"],
uri="somafm:channel:/%s" % (channel_name),
)
return [track]
def browse(self, uri):
if uri != "somafm:root":
return []
result = []
for channel in self.backend.somafm.channels:
result.append(
Ref.track(
uri="somafm:channel:/%s" % (channel),
name=self.backend.somafm.channels[channel]["title"],
)
)
result.sort(key=lambda ref: ref.name.lower())
return result
def get_images(self, uris):
images = {}
for uri in uris:
channel_name = extract_somafm_channel_name_from_uri(uri)
if channel_name is not None:
image = Image(uri=self.backend.somafm.images[channel_name])
images[uri] = [image]
return images
class SomaFMPlayback(backend.PlaybackProvider):
def __init__(self, audio, backend, proxy_config=None, user_agent=None):
super().__init__(audio=audio, backend=backend)
# Build requests session
self.session = requests.Session()
if proxy_config is not None:
proxy = httpclient.format_proxy(proxy_config)
self.session.proxies.update({"http": proxy, "https": proxy})
full_user_agent = httpclient.format_user_agent(user_agent)
self.session.headers.update({"user-agent": full_user_agent})
def translate_uri(self, uri):
try:
channel_name = extract_somafm_channel_name_from_uri(uri)
if channel_name is None:
return None
channel_data = self.backend.somafm.channels.get(channel_name)
r = self.session.get(channel_data["pls"])
if r.status_code != 200:
return None
pls = configparser.ConfigParser()
pls.read_string(r.text)
playlist = pls["playlist"]
num = int(playlist["numberofentries"])
return playlist["File" + str(random.randint(1, num))]
except Exception:
return None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/mopidy_somafm/ext.conf 0000644 0001750 0001750 00000000112 00000000000 021071 0 ustar 00alexandre alexandre [somafm]
enabled = true
encoding = mp3
quality = fast
dj_as_artist = true
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612990213.0
Mopidy-SomaFM-2.0.2/mopidy_somafm/somafm.py 0000644 0001750 0001750 00000012432 00000000000 021266 0 ustar 00alexandre alexandre import collections
import logging
import re
from urllib.parse import urlsplit
import requests
from mopidy import httpclient
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
logger = logging.getLogger(__name__)
#
# Channels are playlist and Album
# PLS are tracks
# PLS contents for internal use
#
def extract_somafm_channel_name_from_uri(uri):
if not uri.startswith("somafm:"):
return None
channel_separator = uri.find("/")
if channel_separator == -1:
return None
return uri[channel_separator + 1 :]
class SomaFMClient:
CHANNELS_URI = "https://api.somafm.com/channels.xml"
# All channels seem to have this combination of quality/encoding available
FALLBACK_QUALITY = "fast"
FALLBACK_ENCODING = "mp3"
channels = {}
images = {}
def __init__(self, proxy_config=None, user_agent=None):
super().__init__()
# Build requests session
self.session = requests.Session()
if proxy_config is not None:
proxy = httpclient.format_proxy(proxy_config)
self.session.proxies.update({"http": proxy, "https": proxy})
full_user_agent = httpclient.format_user_agent(user_agent)
self.session.headers.update({"user-agent": full_user_agent})
def refresh(self, encoding, quality):
# clean previous data
self.channels = {}
# download channels xml file
channels_content = self._downloadContent(self.CHANNELS_URI)
if channels_content is None:
logger.error("Cannot fetch %s" % (self.CHANNELS_URI))
return
# parse XML
root = ET.fromstring(channels_content)
for child_channel in root:
pls_id = child_channel.attrib["id"]
channel_data = {}
channel_all_pls = collections.defaultdict(dict)
for child_detail in child_channel:
key = child_detail.tag
val = child_detail.text
if key in ["title", "image", "dj", "genre", "description"]:
channel_data[key] = val
elif key == "updated":
channel_data["updated"] = int(val)
elif "pls" in key:
pls_quality = key[:-3]
pls_format = child_detail.attrib["format"]
channel_all_pls[pls_quality][pls_format] = val
# firewall playlist are fastpls+mp3 but with fw path
if pls_quality == "fast" and pls_format == "mp3":
r1 = urlsplit(val)
channel_all_pls["firewall"][
"mp3"
] = "{}://{}/{}".format(
r1.scheme, r1.netloc, "fw" + r1.path
)
channel_pls = self._choose_pls(channel_all_pls, encoding, quality)
if channel_pls is not None:
channel_data["pls"] = channel_pls
self.channels[pls_id] = channel_data
self.images[pls_id] = channel_data["image"]
logger.info("Loaded %i SomaFM channels" % (len(self.channels)))
def extractStreamUrlFromPls(self, pls_uri):
pls_content = self._downloadContent(pls_uri)
if pls_content is None:
logger.error("Cannot fetch %s" % (pls_uri))
return pls_uri
# try to find FileX=
try:
m = re.search(r"^(File\d)=(?P\S+)", pls_content, re.M)
if m:
return m.group("stream_url")
else:
return pls_uri
except BaseException:
return pls_uri
def _choose_pls(self, all_pls, encoding, quality):
if not all_pls:
return None
if quality in all_pls:
quality_pls = all_pls[quality]
elif self.FALLBACK_QUALITY in all_pls:
quality_pls = all_pls[self.FALLBACK_QUALITY]
else:
quality_pls = all_pls[next(iter(all_pls))]
if not quality_pls:
return None
if encoding in quality_pls:
pls = quality_pls[encoding]
elif self.FALLBACK_ENCODING in all_pls:
pls = quality_pls[self.FALLBACK_ENCODING]
else:
pls = quality_pls[next(iter(quality_pls))]
return pls
def _downloadContent(self, url):
try:
r = self.session.get(url)
logger.debug("Get %s : %i", url, r.status_code)
if r.status_code != 200:
logger.error(
"SomaFM: %s is not reachable [http code:%i]",
url,
r.status_code,
)
return None
except requests.exceptions.RequestException as e:
logger.error("SomaFM RequestException: %s", e)
except requests.exceptions.ConnectionError as e:
logger.error("SomaFM ConnectionError: %s", e)
except requests.exceptions.URLRequired as e:
logger.error("SomaFM URLRequired: %s", e)
except requests.exceptions.TooManyRedirects as e:
logger.error("SomaFM TooManyRedirects: %s", e)
except Exception as e:
logger.error("SomaFM exception: %s", e)
else:
return r.text
return None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/pyproject.toml 0000644 0001750 0001750 00000000526 00000000000 017504 0 ustar 00alexandre alexandre [build-system]
requires = ["setuptools >= 30.3.0", "wheel"]
[tool.black]
target-version = ["py37", "py38"]
line-length = 80
[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
line_length = 88
known_tests = "tests"
sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,TESTS,LOCALFOLDER"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1612990371.1848197
Mopidy-SomaFM-2.0.2/setup.cfg 0000644 0001750 0001750 00000002575 00000000000 016417 0 ustar 00alexandre alexandre [metadata]
name = Mopidy-SomaFM
version = 2.0.2
url = https://github.com/AlexandrePTJ/mopidy-somafm
author = Alexandre Petitjean
author_email = alpetitjean@gmail.com
license = Apache License, Version 2.0
license_file = LICENSE
description = SomaFM extension for Mopidy
long_description = file: README.rst
classifiers =
Environment :: No Input/Output (Daemon)
Intended Audience :: End Users/Desktop
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Topic :: Multimedia :: Sound/Audio :: Players
[options]
zip_safe = False
include_package_data = True
packages = find:
python_requires = >= 3.7
install_requires =
Mopidy >= 3.0
Pykka >= 2.0.1
requests >= 2.0.0
setuptools
[options.extras_require]
lint =
black
check-manifest
flake8
flake8-bugbear
flake8-import-order
isort[pyproject]
release =
twine
wheel
test =
pytest
pytest-cov
dev =
%(lint)s
%(release)s
%(test)s
[options.packages.find]
exclude =
tests
tests.*
[options.entry_points]
mopidy.ext =
somafm = mopidy_somafm:Extension
[flake8]
application-import-names = mopidy_somafm, tests
max-line-length = 80
exclude = .git, .tox, build
select =
C, E, F, W
B
B950
N
ignore =
E203
E501
W503
B305
[wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/setup.py 0000644 0001750 0001750 00000000046 00000000000 016277 0 ustar 00alexandre alexandre from setuptools import setup
setup()
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1612990371.1848197
Mopidy-SomaFM-2.0.2/tests/ 0000755 0001750 0001750 00000000000 00000000000 015727 5 ustar 00alexandre alexandre ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/tests/__init__.py 0000644 0001750 0001750 00000000000 00000000000 020026 0 ustar 00alexandre alexandre ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/tests/test_extension.py 0000644 0001750 0001750 00000000545 00000000000 021360 0 ustar 00alexandre alexandre from mopidy_somafm import Extension
def test_get_default_config():
ext = Extension()
config = ext.get_default_config()
assert "[somafm]" in config
assert "enabled = true" in config
def test_get_config_schema():
ext = Extension()
schema = ext.get_config_schema()
assert "quality" in schema
assert "encoding" in schema
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/tests/test_somafm.py 0000644 0001750 0001750 00000002655 00000000000 020632 0 ustar 00alexandre alexandre import unittest
from mopidy_somafm.somafm import SomaFMClient
class SomaFMClientTest(unittest.TestCase):
def test_refresh(self):
sfmc = SomaFMClient()
sfmc.refresh("mp3", "fast")
self.assertIsNotNone(sfmc.channels)
self.assertNotEqual(len(sfmc.channels), 0)
def test_refresh_firewall(self):
sfmc = SomaFMClient()
sfmc.refresh("mp3", "firewall")
self.assertIsNotNone(sfmc.channels)
self.assertNotEqual(len(sfmc.channels), 0)
def test_refresh_no_channels(self):
sfmc = SomaFMClient()
sfmc.CHANNELS_URI = ""
sfmc.refresh("mp3", "fast")
self.assertDictEqual(sfmc.channels, {})
self.assertEqual(len(sfmc.channels), 0)
def test_downloadContent(self):
url = "http://api.somafm.com/channels.xml"
sfmc = SomaFMClient()
data = sfmc._downloadContent(url)
self.assertNotEqual(len(data), 0)
def test_extractStreamUrlFromPls(self):
url = "http://somafm.com/groovesalad.pls"
sfmc = SomaFMClient()
data = sfmc.extractStreamUrlFromPls(url)
self.assertNotEqual(len(data), 0)
self.assertNotEqual(data, url)
def test_extractStreamUrlFromPls_unknown(self):
url = "http://somafm.com/noneazerty.pls"
sfmc = SomaFMClient()
data = sfmc.extractStreamUrlFromPls(url)
self.assertNotEqual(len(data), 0)
self.assertEqual(data, url)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1612987304.0
Mopidy-SomaFM-2.0.2/tox.ini 0000644 0001750 0001750 00000000737 00000000000 016107 0 ustar 00alexandre alexandre [tox]
envlist = py37, py38, black, check-manifest, flake8
[testenv]
sitepackages = true
deps = .[test]
commands =
python -m pytest \
--basetemp={envtmpdir} \
--cov=mopidy_somafm --cov-report=term-missing \
{posargs}
[testenv:black]
deps = .[lint]
commands = python -m black --check .
[testenv:check-manifest]
deps = .[lint]
commands = python -m check_manifest
[testenv:flake8]
deps = .[lint]
commands = python -m flake8 --show-source --statistics