From d58b5b94d34cabae2c1a3743d837f5cab75cc9c1 Mon Sep 17 00:00:00 2001 From: Frederick Gnodtke Date: Sun, 18 Sep 2016 04:33:46 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 5 + MANIFEST.in | 4 + README.md | 3 + mopidy_subidy/__init__.py | 29 ++++++ mopidy_subidy/__init__.pyc | Bin 0 -> 1608 bytes mopidy_subidy/backend.py | 16 +++ mopidy_subidy/backend.pyc | Bin 0 -> 1148 bytes mopidy_subidy/ext.conf | 6 ++ mopidy_subidy/library.py | 57 +++++++++++ mopidy_subidy/library.pyc | Bin 0 -> 3691 bytes mopidy_subidy/playback.py | 10 ++ mopidy_subidy/playback.pyc | Bin 0 -> 1026 bytes mopidy_subidy/playlists.py | 27 +++++ mopidy_subidy/playlists.pyc | Bin 0 -> 1914 bytes mopidy_subidy/subsonic_api.py | 179 +++++++++++++++++++++++++++++++++ mopidy_subidy/subsonic_api.pyc | Bin 0 -> 10109 bytes mopidy_subidy/uri.py | 60 +++++++++++ mopidy_subidy/uri.pyc | Bin 0 -> 2971 bytes setup.py | 43 ++++++++ 19 files changed, 439 insertions(+) create mode 100644 .gitignore create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 mopidy_subidy/__init__.py create mode 100644 mopidy_subidy/__init__.pyc create mode 100644 mopidy_subidy/backend.py create mode 100644 mopidy_subidy/backend.pyc create mode 100644 mopidy_subidy/ext.conf create mode 100644 mopidy_subidy/library.py create mode 100644 mopidy_subidy/library.pyc create mode 100644 mopidy_subidy/playback.py create mode 100644 mopidy_subidy/playback.pyc create mode 100644 mopidy_subidy/playlists.py create mode 100644 mopidy_subidy/playlists.pyc create mode 100644 mopidy_subidy/subsonic_api.py create mode 100644 mopidy_subidy/subsonic_api.pyc create mode 100644 mopidy_subidy/uri.py create mode 100644 mopidy_subidy/uri.pyc create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fca6bdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +dist/ +*.conf +venv/ +*.egg-info diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..6def079 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE +include MANIFEST.in +include README.md +include mopidy_subidy/ext.conf diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8db34f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Mopidy Subidy + +A subsonic backend for mopidy. diff --git a/mopidy_subidy/__init__.py b/mopidy_subidy/__init__.py new file mode 100644 index 0000000..4120e57 --- /dev/null +++ b/mopidy_subidy/__init__.py @@ -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) diff --git a/mopidy_subidy/__init__.pyc b/mopidy_subidy/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..012719978b0b601611a80011f16a322ab0211d11 GIT binary patch literal 1608 zcmcIkJ8u**5FUH)m5{)JAPp4~#Wq}M=pqyaQBaU#PY7|1wORWvKK3O)Ad!+P{7q_p z06!qij4uI18+Px!GwZSEo6p~D_H%dp`p5T=6>P2ypNAm9e?UXyPe2XO2v`UZe{7U#UYw6XLyGPXwBn^)#q=|f@!_&+S@8<<0Go# z6&eNzJYpx*SV%_T5G;a0WCS;3V+@F%-b|pF=aeY!;_$xfF~^IPDiRdWSu09HFbU-w zN;X3y0IUV9<&BX+7{C>#7jPxvQa~8OdIT5f3gm*;AIAxfSm%stWO@RsIA=*w`* z#=5g@Ao8?zZP}PW%amH$+9*{pVHEF7sS|zVc~dXL78!8v>(W!a@0OFbWm#TL- zfm8(n7bp%%grP);o($GV7!X6q*}-4FO#bj@K7#`co9C40IMO*pS2LT@8|H|ezV!ZU z=X6eohy2qC_Qn;oAUwo@;3wG1SEr^a6Ppx156-qLR^Z%J&IG2m*ruhd1&+UCPK*1v z{bT%l$^+twnDSlS$c>P*<(brf-qR5hDQ`eEQlcbNz z>a%HeBxRi>J7;G8cu~+Hb32w=^K4>(6zI&#`rytv=8lO-hh;+SpJJwso~9xvL2u+w zouE`8qLgYn9jO~w@fu;N+eX3uO3pow{9+t^F))v{7l>GvHcVbWg literal 0 HcmV?d00001 diff --git a/mopidy_subidy/backend.py b/mopidy_subidy/backend.py new file mode 100644 index 0000000..7f39d98 --- /dev/null +++ b/mopidy_subidy/backend.py @@ -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'] diff --git a/mopidy_subidy/backend.pyc b/mopidy_subidy/backend.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6463ca2a12711233ed26cfa5ab460b77496758ce GIT binary patch literal 1148 zcmcIj&2H2%5dNHGv)!dd{HRDPZk%#~eE|ftTv1hJkr269*~D$VO*WD3fL2;g<-|Mk z0GxRMn6cCL1t5wio*$3r`(_gVKANjvKR;C%pP0VyXxWlPO|L)$v;u`d8=;8M#wcR6 z35o=QBO){tv?+=-=rNipiYe*@YKqMPREo_E7ZK_Sz5_SF=VFdJ#wttc7X62wETNs7 zl4$hWlxF7+Xx}&GZe3Q_ZZ;~7w$8dk((JId-Bwq!>~$u{;16@K+SX_8h%BpNt?S)s zFx?^K*i%mMn1liYBNS8@Var!ORmCiDL0rRoTJ{GCN3X!107TeC3`6Yctia-13>RaQ zFc#h$2qWDN45sm<4OW?L*{Z-`Y3s_H;Z)uBLSN>-I&NWrCxba71H74$19`CT5m5;Dx%8&Fyr6h Y2jD+o@iX;0F!qMW!*Yc$h~mTOFO3ENjsO4v literal 0 HcmV?d00001 diff --git a/mopidy_subidy/ext.conf b/mopidy_subidy/ext.conf new file mode 100644 index 0000000..07f3f97 --- /dev/null +++ b/mopidy_subidy/ext.conf @@ -0,0 +1,6 @@ +[subidy] +enabled = true +url = +username = +password = + diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py new file mode 100644 index 0000000..4abbf17 --- /dev/null +++ b/mopidy_subidy/library.py @@ -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 diff --git a/mopidy_subidy/library.pyc b/mopidy_subidy/library.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a504e2b3197dca04d1b92111b0fe1c49a826b828 GIT binary patch literal 3691 zcmcInTW=dh6h6ClY_D^rX@bhN2#Fdf%?m<2AfQdTL`oAhPK9C#R%`Dh*~Ip`GvgLZ zEe|Bm{1E;Vzk&xIc!2YrS;tmD2+^(W8K2oXyK}zpTxPcN+j8@d-+%qKN5!YY|DSWS z+uWu63#d;t2&fa#Af!%60j`GBuh5`Mohpwjqysu#pre4&DqRqr6CHMHlrB)aRpaMm zfAXglvgGRAb(7wi9;OacgFMxJ+X^s;wZ|rN8~ooMce8Y|n{`cMCf}I+ah7V+;|clT zDfhV9U%1l*<8(-;6`r8Ny9Nu4+UCQo*8?A*18tw1z0IBDUqDZpU&b2JQ-E{+vnAh#q6e(5+FWi8QzWmfl(#KgoLTXERg6NoL` zC!PI_rLB{EptnXQ%gvV2qugdLH<`9ugM7qp{J@Ggi&0#R`rf&BMiT|>@TtX`6x;}U zAZgmRF|)=UaPp6F2Xr1>f`p!mxO3q5Si6`>JhpLSW229(1fFVQ9_u9i?syPqX}J)D z!>-AnSnXF|qq0p!HE%)0X7Ec3GqIDY*VzA(ju=$tlj4zw?w zb3O~dnP=8nL%@i=GcmH@c-YVYn8Q+vSBI?j=K}EE(j&I zBz`&W%J&6Ash{U(r)DxovWj zy#p4ZZF93fad*`fo^kJr4+~d#ANb8>j`0Oy{7{r?@}T-0wP?iS8!48|dERPPX3m{@zy#3FB}pc*pkkciM~4d_lCsvBe7cP`|^Y zIqE^XP(P&|ei!O@Szv}3|5-%lHXY0G{&ySnOpY>_>G6F?kJdTJ`3&iGzZNQ|d;Kvd zz#NGvyF$($8>4RVXcmLB0~S+J@<~N<#nZ6Gi(0Tk!BTLO+uCfS$F3&u!NZ){(4K7> zVra?PfnA6XJx1G;k64^;c(4QQUk!tT&tjYyAC0w{sCQYC^w&QmJy)`^NM)-N(>wVX z5}ZoGUTc&8ic>01#&PUGa~$)#!nn^v1r+kOp!AVXC6oe(^U(br^~LtVzB+U$3?|QA zoMuM%_&n;5nNq=|&Pnl;#PSIzN%%J3CGn(yRlSGqeeQ&VFO$C2>JATQM2J+%AsD#> z7g%g7wcu75Rinz>H;SUwdKB)p;N#-u38!m0!+TCpx&4v5IAr@=K+(SPDkZN1F93!3 R?Gx{*PvIvy(r*T9;a^gv7We=F literal 0 HcmV?d00001 diff --git a/mopidy_subidy/playback.py b/mopidy_subidy/playback.py new file mode 100644 index 0000000..3f29f6c --- /dev/null +++ b/mopidy_subidy/playback.py @@ -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)) diff --git a/mopidy_subidy/playback.pyc b/mopidy_subidy/playback.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73614b597c4d41529ef0ce64c85d1c347a56b7ff GIT binary patch literal 1026 zcmcIiO>fjN5FIDkLJO5vs)SU2K@PzF0YDYpOO-t!_F`odckwn~vb~^{dTY=8!(RCV z967*yPWTY#5_z6y?0EdrvM`)11`94m5?EwRElMuV7uba%uRa-^UnYQL&-H5CbCWB z8h#>QL=={`DQkeEGp1cedgu9!#|VBm7*{ZNcnHwib~b2Tz?bkC;CE}^cec{yU~^s@ zGOn8DGCHN(_&_zTKGV(NNd$ieZ}G8j&0=u2cME5R-rLYSYy6_^2Wavf$uUilo}yt= z>cy}xcvqxJN7SjRNR>t^Z(;rvz(O&U1SpVFftxsfmCMsbtoa1q#srO;HrfYg%2uOM zMQr1vzc+0iB`;W9I3~EV^Gz8H58QX5y!sI4S(~VwjHixtWoxt!M_AFi?dx5GFe+2< z)E_4Is5@DZf+rlP!|Xgh`Pk{PcjF78E@M1(ZxnKl<$oW1d}RNFY?SHcD9mg2F_NCE GdGZr3PUw{Y literal 0 HcmV?d00001 diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py new file mode 100644 index 0000000..f5adf8a --- /dev/null +++ b/mopidy_subidy/playlists.py @@ -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 diff --git a/mopidy_subidy/playlists.pyc b/mopidy_subidy/playlists.pyc new file mode 100644 index 0000000000000000000000000000000000000000..357bb12e7bab986c56a32e3ae3b6eeac6c0a5bba GIT binary patch literal 1914 zcmd5-O^?$s5FMvq+l7TiLPGEZP%caV0787+p-K;kO32E_Zr3zvBHP_nrMJk?Cf;>D-;X%`#dXjnDYXW}Sct+iXasC69j zq}lqIf+%87u;olZ?&l%)`N`r6Fl9ty!W%}Ti@LI-#=F{&ylv{>V(py`qpEH=cxORk zxS8qA?3V6Dv(CUMgfOI@DsdV*G2ZbASTQ@7ct+w~2EW=CBx#TbjWb7}LIOTj;D&Lh zD*QlHN$8e&Wm}hrqbR)1qrJyyPjv|T%DaPzGq$wRKG<7a2ESTYhBTkss9m&Gc=Vr% z!DI`G foA#M8q6jtr{0m7ZKZr&N-piYCy@Fmj`P1YNCzYnC literal 0 HcmV?d00001 diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py new file mode 100644 index 0000000..e3fe8ba --- /dev/null +++ b/mopidy_subidy/subsonic_api.py @@ -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')) + ) diff --git a/mopidy_subidy/subsonic_api.pyc b/mopidy_subidy/subsonic_api.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c9cf2b9a608a900414993f351d8b35d04b79926 GIT binary patch literal 10109 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 literal 0 HcmV?d00001 diff --git a/mopidy_subidy/uri.py b/mopidy_subidy/uri.py new file mode 100644 index 0000000..4b5a169 --- /dev/null +++ b/mopidy_subidy/uri.py @@ -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) diff --git a/mopidy_subidy/uri.pyc b/mopidy_subidy/uri.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02b8d01e4df16e531d0eb7266c424ac14f549c46 GIT binary patch literal 2971 zcmc&$?M@p<6ur9!gTY|FO4FoGt0+`PZL!o3sUlGcjhYIH7&H(iSyi&Yv%rGQTD#*U zNaR=YOnH?2%R@wcfcBibUIG#Mg&|(g?48-!*?aECxl{OeY~=0xcaNLW`xS7%hnBuU zXYu!>B@#gJCGf-+B=E%!NKgG8+s@D6&5+wj{4S z2})u|bN`)!0(<)4hol#+i z>TXoOfa=kI_|@G2p~Q!~t_Wc(aZ&2rFvL!4>!`)SAm>}#-L|esyW5dH(zlX0b}66E zrHgZEdIM7RpXWa;%`axR>eAxk@$LEQ0;ZO3H$6OO^U?03rQf1ERudkSB)&;`ANKQT z3+Ky$4@J4S9`Us*?XKUg$A_V6wVTA#DlUfpX;xuw`5`}549e04~rwm$e)aZ@6Nwm;8ax-{8N*nH|6E?EV8_%{{5qtq4 zR8qVqZ+TOIzhc5EMj(oRIS1f}NuQ{Xy$m&>W~d`>sL;&_Bs0X0?orE_5+t<=73(A1 zDeMclkv#);60=yQ!PcmXdJujDmY3>8AaVtH#6Y8?rT-n(C2(_8u8oIuZkoj>!&KLlouVdosLDS0XtIJPUuLyRf z2Rmmw0RQf@04I+wum1Vy;Z-=BIi=1ftpoD+pM*^M(e(Zi!nx^#+DG?$seIa8Bb6d~CS%4dWPp4utMY>%S4+)Aq)3Y}J& zQyfgPK)C{8+CJuE?jo%?ptzs$fz#Ye1|s{Z>P%kf?NC9$y{wyjT+eC*6WNf^GsVB6 zgx06RNWMD78_@JO%v?azS*k|$O`kPAVK@zXDEj-w6eV?BilVvewfaC$o?Utd)q|S` zTM`;7ppG>xP=mS@6~9!#RXn*kD*O-B CuS*dC literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f7c71f7 --- /dev/null +++ b/setup.py @@ -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' + ] +)