Initial commit.
This commit is contained in:
commit
d58b5b94d3
19 changed files with 439 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.conf
|
||||||
|
venv/
|
||||||
|
*.egg-info
|
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
include LICENSE
|
||||||
|
include MANIFEST.in
|
||||||
|
include README.md
|
||||||
|
include mopidy_subidy/ext.conf
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Mopidy Subidy
|
||||||
|
|
||||||
|
A subsonic backend for mopidy.
|
29
mopidy_subidy/__init__.py
Normal file
29
mopidy_subidy/__init__.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from mopidy import ext, config
|
||||||
|
|
||||||
|
__version__ = '0.0.0'
|
||||||
|
|
||||||
|
|
||||||
|
class SubidyExtension(ext.Extension):
|
||||||
|
|
||||||
|
dist_name = 'Mopidy-Subidy'
|
||||||
|
ext_name = 'subidy'
|
||||||
|
version = __version__
|
||||||
|
|
||||||
|
def get_default_config(self):
|
||||||
|
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||||
|
return config.read(conf_file)
|
||||||
|
|
||||||
|
def get_config_schema(self):
|
||||||
|
schema = super(SubidyExtension, self).get_config_schema()
|
||||||
|
schema['url'] = config.String()
|
||||||
|
schema['username'] = config.String()
|
||||||
|
schema['password'] = config.Secret()
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def setup(self, registry):
|
||||||
|
from .backend import SubidyBackend
|
||||||
|
registry.add('backend', SubidyBackend)
|
BIN
mopidy_subidy/__init__.pyc
Normal file
BIN
mopidy_subidy/__init__.pyc
Normal file
Binary file not shown.
16
mopidy_subidy/backend.py
Normal file
16
mopidy_subidy/backend.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from mopidy_subidy import library, playback, playlists, subsonic_api
|
||||||
|
from mopidy import backend
|
||||||
|
import pykka
|
||||||
|
|
||||||
|
class SubidyBackend(pykka.ThreadingActor, backend.Backend):
|
||||||
|
def __init__(self, config, audio):
|
||||||
|
super(SubidyBackend, self).__init__()
|
||||||
|
subidy_config = config['subidy']
|
||||||
|
self.subsonic_api = subsonic_api.SubsonicApi(
|
||||||
|
url=subidy_config['url'],
|
||||||
|
username=subidy_config['username'],
|
||||||
|
password=subidy_config['password'])
|
||||||
|
self.library = library.SubidyLibraryProvider(backend=self)
|
||||||
|
self.playback = playback.SubidyPlaybackProvider(audio=audio, backend=self)
|
||||||
|
#self.playlists = playlists.SubidyPlaylistsProvider(backend=self)
|
||||||
|
self.uri_schemes = ['subidy']
|
BIN
mopidy_subidy/backend.pyc
Normal file
BIN
mopidy_subidy/backend.pyc
Normal file
Binary file not shown.
6
mopidy_subidy/ext.conf
Normal file
6
mopidy_subidy/ext.conf
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[subidy]
|
||||||
|
enabled = true
|
||||||
|
url =
|
||||||
|
username =
|
||||||
|
password =
|
||||||
|
|
57
mopidy_subidy/library.py
Normal file
57
mopidy_subidy/library.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from mopidy import backend, models
|
||||||
|
from mopidy_subidy import uri
|
||||||
|
|
||||||
|
class SubidyLibraryProvider(backend.LibraryProvider):
|
||||||
|
root_directory = models.Ref(uri=uri.ROOT_URI, type=models.Ref.DIRECTORY, name='Subsonic')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SubidyLibraryProvider, self).__init__(*args, **kwargs)
|
||||||
|
self.subsonic_api = self.backend.subsonic_api
|
||||||
|
|
||||||
|
def browse_songs(self,album_id):
|
||||||
|
return self.subsonic_api.get_songs_as_refs(album_id)
|
||||||
|
|
||||||
|
def browse_albums(self, artist_id):
|
||||||
|
return self.subsonic_api.get_albums_as_refs(artist_id)
|
||||||
|
|
||||||
|
def browse_artists(self):
|
||||||
|
return self.subsonic_api.get_artists_as_refs()
|
||||||
|
|
||||||
|
def lookup_song(self, song_id):
|
||||||
|
return self.subsonic_api.find_song_by_id(song_id)
|
||||||
|
|
||||||
|
def lookup_album(self, album_id):
|
||||||
|
return self.subsonic_api.find_album_by_id(album_id)
|
||||||
|
|
||||||
|
def lookup_artist(self, artist_id):
|
||||||
|
return self.subsonic_api.find_artist_by_id(artist_id)
|
||||||
|
|
||||||
|
def browse(self, browse_uri):
|
||||||
|
type = uri.get_type(browse_uri)
|
||||||
|
if browse_uri == uri.ROOT_URI:
|
||||||
|
return self.browse_artists()
|
||||||
|
if type == uri.ARTIST:
|
||||||
|
return self.browse_albums(uri.get_artist_id(browse_uri))
|
||||||
|
if type == uri.ALBUM:
|
||||||
|
return self.browse_songs(uri.get_album_id(browse_uri))
|
||||||
|
|
||||||
|
def lookup_one(self, lookup_uri):
|
||||||
|
type = uri.get_type(lookup_uri)
|
||||||
|
if type == uri.ARTIST:
|
||||||
|
return self.lookup_artist(uri.get_artist_id(lookup_uri))
|
||||||
|
if type == uri.ALBUM:
|
||||||
|
return self.lookup_album(uri.get_album_id(lookup_uri))
|
||||||
|
if type == uri.SONG:
|
||||||
|
return self.lookup_song(uri.get_song_id(lookup_uri))
|
||||||
|
|
||||||
|
def lookup(self, uri=None, uris=None):
|
||||||
|
if uris is not None:
|
||||||
|
return [self.lookup_one(uri) for uri in uris]
|
||||||
|
if uri is not None:
|
||||||
|
return [self.lookup_one(uri)]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def refresh(self, uri):
|
||||||
|
pass
|
||||||
|
def search(self, query=None, uris=None, exact=False):
|
||||||
|
pass
|
BIN
mopidy_subidy/library.pyc
Normal file
BIN
mopidy_subidy/library.pyc
Normal file
Binary file not shown.
10
mopidy_subidy/playback.py
Normal file
10
mopidy_subidy/playback.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from mopidy import backend
|
||||||
|
from mopidy_subidy import uri
|
||||||
|
|
||||||
|
class SubidyPlaybackProvider(backend.PlaybackProvider):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SubidyPlaybackProvider, self).__init__(*args, **kwargs)
|
||||||
|
self.subsonic_api = self.backend.subsonic_api
|
||||||
|
|
||||||
|
def translate_uri(self, translate_uri):
|
||||||
|
return self.subsonic_api.get_song_stream_uri(uri.get_song_id(translate_uri))
|
BIN
mopidy_subidy/playback.pyc
Normal file
BIN
mopidy_subidy/playback.pyc
Normal file
Binary file not shown.
27
mopidy_subidy/playlists.py
Normal file
27
mopidy_subidy/playlists.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from mopidy import backend
|
||||||
|
|
||||||
|
class SubidyPlaylistsProvider(backend.PlaylistsProvider):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SubidyPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||||
|
self.playlists = []
|
||||||
|
|
||||||
|
def as_list(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create(self, name):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete(self, uri):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_items(self, uri):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def lookup(self, uri):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def save(self, playlist):
|
||||||
|
pass
|
BIN
mopidy_subidy/playlists.pyc
Normal file
BIN
mopidy_subidy/playlists.pyc
Normal file
Binary file not shown.
179
mopidy_subidy/subsonic_api.py
Normal file
179
mopidy_subidy/subsonic_api.py
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
from urlparse import urlparse
|
||||||
|
import libsonic
|
||||||
|
import logging
|
||||||
|
import itertools
|
||||||
|
from mopidy.models import Track, Album, Artist, Playlist, Ref
|
||||||
|
from mopidy_subidy import uri
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
RESPONSE_OK = u'ok'
|
||||||
|
UNKNOWN_SONG = u'Unknown Song'
|
||||||
|
UNKNOWN_ALBUM = u'Unknown Album'
|
||||||
|
UNKNOWN_ARTIST = u'Unknown Artist'
|
||||||
|
MAX_SEARCH_RESULTS = 100
|
||||||
|
|
||||||
|
ref_sort_key = lambda ref: ref.name
|
||||||
|
|
||||||
|
class SubsonicApi():
|
||||||
|
def __init__(self, url, username, password):
|
||||||
|
parsed = urlparse(url)
|
||||||
|
self.port = parsed.port if parsed.port else \
|
||||||
|
443 if parsed.scheme == 'https' else 80
|
||||||
|
base_url = parsed.scheme + '://' + parsed.hostname
|
||||||
|
self.connection = libsonic.Connection(
|
||||||
|
base_url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
self.port,
|
||||||
|
parsed.path + '/rest')
|
||||||
|
self.url = url + '/rest'
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
logger.info('Connecting to subsonic server on url %s as user %s' % (url, username))
|
||||||
|
try:
|
||||||
|
self.connection.ping()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Unabled to reach subsonic server: %s' % e)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
def get_song_stream_uri(self, song_id):
|
||||||
|
template = '%s/stream.view?id=%s&u=%s&p=%s&c=mopidy&v=1.14'
|
||||||
|
return template % (self.url, song_id, self.username, self.password)
|
||||||
|
|
||||||
|
def find_artists_by_name(self, artist_name):
|
||||||
|
response = self.connection.search3(artist_name, MAX_SEARCH_RESULTS, 0, 0, 0, 0, 0)
|
||||||
|
if response.get('status') != RESPONSE_OK:
|
||||||
|
return None
|
||||||
|
artists = response.get('searchResult3').get('artist')
|
||||||
|
if artists is not None:
|
||||||
|
return [self.raw_artist_to_artist(artist) for artist in artists]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_tracks_by_name(self, track_name):
|
||||||
|
response = self.connection.search3(track_name, 0, 0, 0, 0, MAX_SEARCH_RESULTS, 0)
|
||||||
|
if response.get('status') != RESPONSE_OK:
|
||||||
|
return None
|
||||||
|
tracks = response.get('searchResult3').get('song')
|
||||||
|
if tracks is not None:
|
||||||
|
return [self.raw_song_to_track(track) for track in tracks]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_albums_by_name(self, album_name):
|
||||||
|
response = self.connection.search3(album_name, 0, 0, MAX_SEARCH_RESULTS, 0, 0, 0)
|
||||||
|
if response.get('status') != RESPONSE_OK:
|
||||||
|
return None
|
||||||
|
albums = response.get('searchResult3').get('album')
|
||||||
|
if albums is not None:
|
||||||
|
return [self.raw_album_to_album(album) for album in albums]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_raw_artists(self):
|
||||||
|
response = self.connection.getIndexes()
|
||||||
|
if response.get('status') != RESPONSE_OK:
|
||||||
|
return None
|
||||||
|
letters = response.get('indexes').get('index')
|
||||||
|
if letters is not None:
|
||||||
|
return [artist for letter in letters for artist in letter.get('artist')]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_song_by_id(self, song_id):
|
||||||
|
response = self.connection.getSong(song_id)
|
||||||
|
if response.get('status') != RESPONSE_OK:
|
||||||
|
return None
|
||||||
|
return self.raw_song_to_track(response.get('song')) if response.get('song') is not None else None
|
||||||
|
|
||||||
|
def find_album_by_id(self, album_id):
|
||||||
|
response = self.connection.getAlbum(album_id)
|
||||||
|
if response.get('status') != RESPONSE_OK:
|
||||||
|
return None
|
||||||
|
return self.raw_album_to_album(response.get('album')) if response.get('album') is not None else None
|
||||||
|
|
||||||
|
def find_artist_by_id(self, artist_id):
|
||||||
|
response = self.connection.getArtist(artist_id)
|
||||||
|
if response.get('status') != RESPONSE_OK:
|
||||||
|
return None
|
||||||
|
return self.raw_artist_to_artist(response.get('artist')) if response.get('artist') is not None else None
|
||||||
|
|
||||||
|
def get_raw_dir(self, parent_id):
|
||||||
|
response = self.connection.getMusicDirectory(parent_id)
|
||||||
|
if response.get('status') != RESPONSE_OK:
|
||||||
|
return None
|
||||||
|
directory = response.get('directory')
|
||||||
|
if directory is not None:
|
||||||
|
return directory.get('child')
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_raw_albums(self, artist_id):
|
||||||
|
return self.get_raw_dir(artist_id)
|
||||||
|
|
||||||
|
def get_raw_songs(self, album_id):
|
||||||
|
return self.get_raw_dir(album_id)
|
||||||
|
|
||||||
|
def get_albums_as_refs(self, artist_id):
|
||||||
|
return sorted([self.raw_album_to_ref(album) for album in self.get_raw_albums(artist_id)], key=ref_sort_key)
|
||||||
|
|
||||||
|
def get_albums_as_albums(self, artist_id):
|
||||||
|
return sorted([self.raw_album_to_album(album) for album in self.get_raw_albums(artist_id)], key=ref_sort_key)
|
||||||
|
|
||||||
|
def get_songs_as_refs(self, album_id):
|
||||||
|
return sorted([self.raw_song_to_ref(song) for song in self.get_raw_songs(album_id)], key=ref_sort_key)
|
||||||
|
|
||||||
|
def get_songs_as_tracks(self, album_id):
|
||||||
|
return sorted([self.raw_song_to_track(song) for song in self.get_raw_songs(album_id)], key=ref_sort_key)
|
||||||
|
|
||||||
|
def get_artists_as_refs(self):
|
||||||
|
return sorted([self.raw_artist_to_ref(artist) for artist in self.get_raw_artists()], key=ref_sort_key)
|
||||||
|
|
||||||
|
def get_artists_as_artists(self):
|
||||||
|
return sorted([self.raw_artist_to_artist(artist) for artist in self.get_raw_artists()], key=lambda artist:artist.name)
|
||||||
|
|
||||||
|
def raw_song_to_ref(self, song):
|
||||||
|
return Ref(
|
||||||
|
name=song.get('title') or UNKNOWN_SONG,
|
||||||
|
uri=uri.get_song_uri(song.get('id')),
|
||||||
|
type=Ref.TRACK)
|
||||||
|
|
||||||
|
def raw_song_to_track(self, song):
|
||||||
|
album_name = song.get('album')
|
||||||
|
album = self.find_albums_by_name(album_name)[0] if album_name is not None else None
|
||||||
|
artist_name = song.get('artist')
|
||||||
|
artist = self.find_artists_by_name(artist_name)[0] if artist_name is not None else None
|
||||||
|
return Track(
|
||||||
|
name=song.get('title') or UNKNOWN_SONG,
|
||||||
|
uri=uri.get_song_uri(song.get('id')),
|
||||||
|
bitrate=song.get('bitRate'),
|
||||||
|
track_no=int(song.get('track')) if song.get('track') else None,
|
||||||
|
date=str(song.get('year')) or 'none',
|
||||||
|
genre=song.get('genre'),
|
||||||
|
length=int(song.get('duration')) * 1000 if song.get('duration') else None,
|
||||||
|
disc_no=int(song.get('discNumber')) if song.get('discNumber') else None,
|
||||||
|
artists=[artist] if artist is not None else None,
|
||||||
|
album=album
|
||||||
|
)
|
||||||
|
def raw_album_to_ref(self, album):
|
||||||
|
return Ref(
|
||||||
|
name=album.get('title') or album.get('name') or UNKNOWN_ALBUM,
|
||||||
|
uri=uri.get_album_uri(album.get('id')),
|
||||||
|
type=Ref.ALBUM)
|
||||||
|
|
||||||
|
def raw_album_to_album(self, album):
|
||||||
|
artist_name = album.get('artist')
|
||||||
|
artist = self.find_artists_by_name(artist_name)[0] if artist_name is not None else None
|
||||||
|
return Album(
|
||||||
|
name=album.get('title') or album.get('name') or UNKNOWN_ALBUM,
|
||||||
|
uri=uri.get_album_uri(album.get('id')),
|
||||||
|
artists=[artist]
|
||||||
|
)
|
||||||
|
|
||||||
|
def raw_artist_to_ref(self, artist):
|
||||||
|
return Ref(
|
||||||
|
name=artist.get('name') or UNKNOWN_ARTIST,
|
||||||
|
uri=uri.get_artist_uri(artist.get('id')),
|
||||||
|
type=Ref.ARTIST)
|
||||||
|
|
||||||
|
def raw_artist_to_artist(self, artist):
|
||||||
|
return Artist(
|
||||||
|
name=artist.get('name') or UNKNOWN_ARTIST,
|
||||||
|
uri=uri.get_artist_uri(artist.get('id'))
|
||||||
|
)
|
BIN
mopidy_subidy/subsonic_api.pyc
Normal file
BIN
mopidy_subidy/subsonic_api.pyc
Normal file
Binary file not shown.
60
mopidy_subidy/uri.py
Normal file
60
mopidy_subidy/uri.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
SONG = 'song'
|
||||||
|
ARTIST = 'artist'
|
||||||
|
PLAYLIST = 'playlist'
|
||||||
|
ALBUM = 'album'
|
||||||
|
PREFIX = 'subidy'
|
||||||
|
ROOT = 'root'
|
||||||
|
|
||||||
|
ROOT_URI = '%s:%s' % (PREFIX, ROOT)
|
||||||
|
|
||||||
|
regex = re.compile(r'(\w+?):(\w+?)(?::|$)(.+?)?$')
|
||||||
|
|
||||||
|
def is_type_result_valid(result):
|
||||||
|
return result is not None and result.group(1) == PREFIX
|
||||||
|
|
||||||
|
def is_id_result_valid(result, type):
|
||||||
|
return is_type_result_valid(result) and result.group(1) == PREFIX and result.group(2) == type
|
||||||
|
|
||||||
|
def get_song_id(uri):
|
||||||
|
result = regex.match(uri)
|
||||||
|
if not is_id_result_valid(result, SONG):
|
||||||
|
return None
|
||||||
|
return result.group(3)
|
||||||
|
|
||||||
|
def get_artist_id(uri):
|
||||||
|
result = regex.match(uri)
|
||||||
|
if not is_id_result_valid(result, ARTIST):
|
||||||
|
return None
|
||||||
|
return result.group(3)
|
||||||
|
|
||||||
|
def get_playlist_id(uri):
|
||||||
|
result = regex.match(uri)
|
||||||
|
if not is_id_result_valid(result, PLAYLIST):
|
||||||
|
return None
|
||||||
|
return result.group(3)
|
||||||
|
|
||||||
|
def get_album_id(uri):
|
||||||
|
result = regex.match(uri)
|
||||||
|
if not is_id_result_valid(result, ALBUM):
|
||||||
|
return None
|
||||||
|
return result.group(3)
|
||||||
|
|
||||||
|
def get_type(uri):
|
||||||
|
result = regex.match(uri)
|
||||||
|
if not is_type_result_valid(result):
|
||||||
|
return None
|
||||||
|
return result.group(2)
|
||||||
|
|
||||||
|
def get_type_uri(type, id):
|
||||||
|
return u'%s:%s:%s' % (PREFIX, type, id)
|
||||||
|
|
||||||
|
def get_artist_uri(id):
|
||||||
|
return get_type_uri(ARTIST, id)
|
||||||
|
|
||||||
|
def get_album_uri(id):
|
||||||
|
return get_type_uri(ALBUM, id)
|
||||||
|
|
||||||
|
def get_song_uri(id):
|
||||||
|
return get_type_uri(SONG, id)
|
BIN
mopidy_subidy/uri.pyc
Normal file
BIN
mopidy_subidy/uri.pyc
Normal file
Binary file not shown.
43
setup.py
Normal file
43
setup.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(filename):
|
||||||
|
content = open(filename).read()
|
||||||
|
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", content))
|
||||||
|
return metadata['version']
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='Mopidy-Subidy',
|
||||||
|
version=get_version('mopidy_subidy/__init__.py'),
|
||||||
|
url='http://github.com/prior99/mopidy-subidy/',
|
||||||
|
license='MIT',
|
||||||
|
author='ptiot99',
|
||||||
|
author_email='fgnodtke@cronosx.de',
|
||||||
|
description='Improved Subsonic extension for Mopidy',
|
||||||
|
long_description=open('README.md').read(),
|
||||||
|
packages=find_packages(exclude=['tests', 'tests.*']),
|
||||||
|
zip_safe=False,
|
||||||
|
include_package_data=True,
|
||||||
|
install_requires=[
|
||||||
|
'setuptools',
|
||||||
|
'Mopidy >= 2.0',
|
||||||
|
'py-sonic',
|
||||||
|
'Pykka >= 1.1'
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
b'mopidy.ext': [
|
||||||
|
'subidy = mopidy_subidy:SubidyExtension',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
classifiers=[
|
||||||
|
'Environment :: No Input/Output (Daemon)',
|
||||||
|
'Intended Audience :: End Users/Desktop',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python :: 2',
|
||||||
|
'Topic :: Multimedia :: Sound/Audio :: Players'
|
||||||
|
]
|
||||||
|
)
|
Loading…
Add table
Reference in a new issue