Compare commits
187 Commits
v2.0.0
...
artist-inf
| Author | SHA1 | Date | |
|---|---|---|---|
| 552cd606f5 | |||
| 4980752fd0 | |||
| f587d3d4d6 | |||
| 607bb52158 | |||
| 117175d2cc | |||
| 0db8d3b06c | |||
| 3fb94e3a68 | |||
| 94ceecb245 | |||
| 3b694559e2 | |||
| 3a71d3c107 | |||
| cefe9d617f | |||
| 5fa93a9830 | |||
| f7e3d18b5b | |||
| 1295d078e5 | |||
| 4b1e0ad104 | |||
| 7baa12f06f | |||
| 9d8eaef45b | |||
| 42629ac27f | |||
| 569d028d7c | |||
| 98d7965307 | |||
| d9398ea082 | |||
| 1b708bdbb1 | |||
| ade19135a3 | |||
| 40d4ac737a | |||
| 9be5f646da | |||
| d141895328 | |||
| d85328ffa0 | |||
| b7e734f6d3 | |||
| 54a071a9c7 | |||
| f97b9e1de9 | |||
| 8ca52e6cc8 | |||
| c4c0aa00c6 | |||
| 67bd25496e | |||
| 0932e650f8 | |||
| 57141eed02 | |||
| 2335973433 | |||
| 150b5012da | |||
| 8c4f31db05 | |||
| 596ddaacdb | |||
| c25e5848cf | |||
| 469681bf5e | |||
| ced597b6d2 | |||
| 03da132550 | |||
| 742c5b66fc | |||
| 640157dad0 | |||
| 56dffe70bc | |||
| 34a7c249be | |||
| 67d2302bc4 | |||
| 9fbb67cc3c | |||
| 0ac9d36187 | |||
| 49754bccaa | |||
| 358df44fb1 | |||
| a551df5c8c | |||
| e640d0f81d | |||
| 3aac9d1c54 | |||
| 7564e75f63 | |||
| ed3f7da8b5 | |||
| 194844571c | |||
| 6035ff49b8 | |||
| bc30d36466 | |||
| 445303a2fc | |||
| 2d76c74f42 | |||
| a7f74582de | |||
| 41a747a2b7 | |||
| 2e5d012cae | |||
| 53ecd6e5d1 | |||
| 2efa209227 | |||
| 1703d433a3 | |||
| 17f77aca93 | |||
| a136a7b47e | |||
| b461af0269 | |||
| bf1ee6bd1b | |||
| 79a9b1ebc3 | |||
| 46da611895 | |||
| d524b8cc8e | |||
| 0ddda4676c | |||
| 1d22cd75c1 | |||
| 1fc06bd790 | |||
| e958c5bb2a | |||
| 9d86ff2051 | |||
| 71e208b662 | |||
| 6910755b36 | |||
| aca6ad6e83 | |||
| adc78f9783 | |||
| 0c32547c0d | |||
| 67b5e4ab8c | |||
| 861e7534c6 | |||
| c65548a73b | |||
| f5202f2229 | |||
| 2b3789fe5c | |||
| 92356ab533 | |||
| 1d3181fe4e | |||
| 577181bb73 | |||
| f0649bf2b5 | |||
| b630801150 | |||
| 99d57fa1f9 | |||
| f7ccef89ef | |||
| d90af02d8b | |||
| 4429451454 | |||
| 313b413fc5 | |||
| 30d0e8641e | |||
| b18b8e0094 | |||
| 66ae5ee093 | |||
| 426dcf964f | |||
| ce58b6993f | |||
| f51e5c3c28 | |||
| e966b5bfa2 | |||
| 9e7e542585 | |||
| 10bb432a14 | |||
| e6b00afc27 | |||
| 60ebe928b0 | |||
| 2482e56fdc | |||
| 290fa837b3 | |||
| 0344163cd5 | |||
| 3a0efadebb | |||
| 8c2ffe006c | |||
| ebf82b8218 | |||
| d9a18d41e3 | |||
| c6e9cc92e8 | |||
| 3ed7b231cc | |||
| 7d26333ce0 | |||
| d9210dcaef | |||
| 0cda25e07a | |||
| be07844e38 | |||
| 5484614cc1 | |||
| 7e67edd64a | |||
| 8ce871a9f7 | |||
| 3acb61d605 | |||
| c621458704 | |||
| aaac00f69a | |||
| afea46a9e1 | |||
| fb64a2e6fe | |||
| a029490fe8 | |||
| 4a057ba83c | |||
| ef6ba710e2 | |||
| 4179ca8ada | |||
| fcbcccaeb9 | |||
| fcfee371f8 | |||
| 68a8fec4f8 | |||
| 2285d5ca0b | |||
| 537524d7fe | |||
| 3cb2bc702b | |||
| b66129b448 | |||
| 8bbeabc0dc | |||
| 5117218f86 | |||
| f80b6458a1 | |||
| 8bae5abd88 | |||
| c026d31836 | |||
| f905da4f43 | |||
| 8a786b1eae | |||
| 73c6bd5721 | |||
| 32f706ffbe | |||
| 5c1ba5015b | |||
| 5307f5d8c4 | |||
| ff52a69087 | |||
| 8d3d425089 | |||
| c0a7386ff1 | |||
| 3b9642ea85 | |||
| 52c2c2d2d7 | |||
| 48dc6a555b | |||
| d92a831743 | |||
| 19d39e4da6 | |||
| a9b2f10015 | |||
| d94a6985e2 | |||
| d2ca6218ce | |||
| 99196c32eb | |||
| c08c8d28ee | |||
| a4e8f2376d | |||
| 6fee1db7ec | |||
| ac9160ea2c | |||
| f16f234679 | |||
| 4073737008 | |||
| 03d77f0c83 | |||
| 44418a384e | |||
| b35dfd278d | |||
| 47d0fcbe77 | |||
| 6b4807bbf5 | |||
| fe158c33fb | |||
| a6c18a2eb0 | |||
| 86888b5a7a | |||
| 7be23bfd8d | |||
| 2580050353 | |||
| 81e6bd7ae0 | |||
| 16e8f4996e | |||
| e8aa3c38a8 | |||
| f929cdbdef | |||
| fb01d0eaa9 |
72
CHANGELOG.md
72
CHANGELOG.md
@ -1,5 +1,77 @@
|
||||
# Changelog
|
||||
|
||||
## v3.0.3
|
||||
Released 29th September 2021 (by warwickh)
|
||||
* Added enhanced data collection and display from musicbrainz and wikipedia
|
||||
* Added aqlite storage of artist information to speed loading
|
||||
|
||||
## v3.0.2
|
||||
Released 29th September 2021 (by warwickh)
|
||||
* Removed dependency on future and dateutil
|
||||
* Simpleplugin modified - no longer py2 compatible
|
||||
|
||||
## v3.0.1
|
||||
Released 2nd September 2021 (by warwickh)
|
||||
* Added Navidrome compatibility (remove dependency on integer ids)
|
||||
|
||||
## v3.0.0
|
||||
Released 29th June 2021 (by warwickh)
|
||||
* Basic update to provide Matrix compatility. Not tested on Kodi below v19
|
||||
* Updates simpleplugin to latest version (3.0.0) https://github.com/vlmaksime/script.module.simpleplugin
|
||||
* Moves some legacy simpleplugin static routines into main.py
|
||||
* Removes dependancy on libsonic_extra by moving some walk functions into main.py
|
||||
* Updates libsonic to latest version and adds functions for returning raw url for populating menus
|
||||
* Move to version 3+ for diffferentiation from Leia compatible version
|
||||
|
||||
## v2.0.8
|
||||
Released 29th November 2017 (by Heruwar)
|
||||
* Fixes a security issue where the password is sent as plaintext in the URL query parameters when methods from libsonic_extas are used.
|
||||
Also adds Subsonic hex encoding when using legacy auth.
|
||||
* Adds support for URL paths like https://hostname.com/subsonic as requested in Github issue #17 and also encountered in some of the reports (#14 and #5)
|
||||
* Fixes an error when the password only contains digits, which simpleplugin converts to a Long, which later fails when libsonic tries to salt the password expecting a string.
|
||||
|
||||
## v2.0.7
|
||||
Released 18 April 2017
|
||||
* Added Search (by silascutler)
|
||||
|
||||
## v2.0.6
|
||||
Released 14 January 2017
|
||||
* Upgrade to simpleplugin 2.1.0
|
||||
* Browse/Library menus (file structure vs ID3 tags)
|
||||
* Added multilanguage support
|
||||
|
||||
## v2.0.5
|
||||
Released 15 October 2016
|
||||
* Fixed images when listing entries
|
||||
|
||||
## v2.0.4
|
||||
Released 5 October 2016
|
||||
* Cache (permanently) starred items so we can know everywhere if an item is starred or not without querying it
|
||||
* Colorize starred items
|
||||
* Hide artist in lists if it is unique
|
||||
* Removed list_artist_albums()
|
||||
* Include simpleplugin v2.0.1 as library and remove KODI dependency
|
||||
|
||||
## v2.0.3
|
||||
Released 4 October 2016
|
||||
* Random tracks submenu
|
||||
* Download tracks
|
||||
* Star tracks
|
||||
* Context menu for downloading or marking items as favorite
|
||||
|
||||
## v2.0.2
|
||||
* main code (main.py) fully rewritten
|
||||
* Use SimplePlugin framework (http://romanvm.github.io/script.module.simpleplugin/index.html)
|
||||
* Cache datas
|
||||
|
||||
## v2.0.1
|
||||
* New setting 'albums_per_page'
|
||||
* New menu structure
|
||||
* Albums : Newest / Most played / Rencently played / by Genre
|
||||
* Tracks : Starred / Random by genre / Random by year
|
||||
* Upgraded: libsonic to v.0.5.0 (https://github.com/crustymonkey/py-sonic)
|
||||
* Code cleanup
|
||||
|
||||
## v2.0.0
|
||||
Released 14 September 2015
|
||||
|
||||
|
||||
46
README.md
46
README.md
@ -1,19 +1,49 @@
|
||||
# Subsonic for Kodi
|
||||
Kodi plugin to stream music from Subsonic.
|
||||
# Subsonic
|
||||
Kodi plugin to stream, star and download music from Subsonic/Airsonic/Navidrome (requires Subsonic API compatibility)
|
||||
|
||||
For feature requests / issues:
|
||||
https://github.com/warwickh/plugin.audio.subsonic/issues
|
||||
|
||||
Contributions are welcome:
|
||||
https://github.com/warwickh/plugin.audio.subsonic
|
||||
|
||||
Master branch updated to support Kodi 19 Matrix
|
||||
|
||||
Leia compatible version available in alternate branch
|
||||
|
||||
## Features
|
||||
* Playlist support
|
||||
* Browse by artist, album and genre
|
||||
* Random playlists
|
||||
* Browse by artist, albums (newest/most played/recently played/random), tracks (starred/random), and playlists
|
||||
* Download songs
|
||||
* Star songs
|
||||
* Navidrome compatibility added (please report any issues)
|
||||
* Scrobble to Last.FM
|
||||
* Enhanced data loading from Musicbrainz and wikipedia (switch on in settings and restart)
|
||||
|
||||
## Installation
|
||||
From repository
|
||||
[repository.warwickh-0.9.0.zip](https://github.com/warwickh/repository.warwickh/raw/master/matrix/zips/repository.warwickh/repository.warwickh-0.9.0.zip) (Please report any issues)
|
||||
|
||||
From GitHub
|
||||
* Click the code button and download
|
||||
* Enable unknown sources and install from zip in Kodi
|
||||
|
||||
or
|
||||
* Navigate to your `.kodi/addons/` folder
|
||||
* Clone this repository: `git clone https://github.com/basilfx/plugin.audio.subsonic.git`
|
||||
* Clone this repository: `git clone https://github.com/warwickh/plugin.audio.subsonic.git`
|
||||
* (Re)start Kodi.
|
||||
|
||||
Note: You will need to enter your server settings into the plugin configuration before use
|
||||
|
||||
## TODO
|
||||
* Improve the caching system
|
||||
* Search filter GUI for tracks and albums
|
||||
|
||||
## License
|
||||
See the `LICENSE` file.
|
||||
|
||||
Additional copyright notices:
|
||||
* The original [SubKodi](https://github.com/DarkAllMan/SubKodi) plugin.
|
||||
* [`py-sonic`](https://github.com/crustymonkey/py-sonic) Python module.
|
||||
* [Previous version of this plugin](https://github.com/gordielachance/plugin.audio.subsonic) by gordielachance
|
||||
* [Previous version of this plugin](https://github.com/basilfx/plugin.audio.subsonic) by basilfx
|
||||
* [SimplePlugin](https://github.com/romanvm/script.module.simpleplugin/stargazers) by romanvm now at [SimplePlugin3](https://github.com/vlmaksime/script.module.simpleplugin)
|
||||
* The original [SubKodi](https://github.com/DarkAllMan/SubKodi) plugin
|
||||
* [`py-sonic`](https://github.com/crustymonkey/py-sonic) Python module
|
||||
|
||||
383
addon.py
383
addon.py
@ -1,383 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
|
||||
# Make sure library folder is on the path
|
||||
addon = xbmcaddon.Addon("plugin.audio.subsonic")
|
||||
sys.path.append(xbmc.translatePath(os.path.join(
|
||||
addon.getAddonInfo("path"), "lib")))
|
||||
|
||||
import libsonic_extra
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
"""
|
||||
Plugin container.
|
||||
"""
|
||||
|
||||
def __init__(self, addon_url, addon_handle, addon_args):
|
||||
self.addon_url = addon_url
|
||||
self.addon_handle = addon_handle
|
||||
self.addon_args = addon_args
|
||||
|
||||
# Retrieve plugin settings
|
||||
self.url = addon.getSetting("subsonic_url")
|
||||
self.username = addon.getSetting("username")
|
||||
self.password = addon.getSetting("password")
|
||||
|
||||
self.random_count = addon.getSetting("random_count")
|
||||
self.bitrate = addon.getSetting("bitrate")
|
||||
self.transcode_format = addon.getSetting("transcode_format")
|
||||
|
||||
# Create connection
|
||||
self.connection = libsonic_extra.SubsonicClient(
|
||||
self.url, self.username, self.password)
|
||||
|
||||
def build_url(self, query):
|
||||
"""
|
||||
Create URL for page.
|
||||
"""
|
||||
|
||||
parts = list(urlparse.urlparse(self.addon_url))
|
||||
parts[4] = urllib.urlencode(query)
|
||||
|
||||
return urlparse.urlunparse(parts)
|
||||
|
||||
def route(self):
|
||||
"""
|
||||
Map a Kodi request to certain action. This takes the `mode' query
|
||||
parameter and executed the function in this instance with that name.
|
||||
"""
|
||||
|
||||
mode = self.addon_args.get("mode", ["main_page"])[0]
|
||||
|
||||
if not mode.startswith("_"):
|
||||
getattr(self, mode)()
|
||||
|
||||
def add_track(self, track, show_artist=False):
|
||||
"""
|
||||
Display one track in the list.
|
||||
"""
|
||||
|
||||
url = self.connection.streamUrl(
|
||||
sid=track["id"], maxBitRate=self.bitrate,
|
||||
tformat=self.transcode_format)
|
||||
|
||||
# Create list item
|
||||
if show_artist:
|
||||
title = "%s - %s" % (
|
||||
track.get("artist", "<Unknown>"),
|
||||
track.get("title", "<Unknown>"))
|
||||
else:
|
||||
title = track.get("title", "<Unknown>")
|
||||
|
||||
# Create item
|
||||
li = xbmcgui.ListItem(title)
|
||||
|
||||
# Handle cover art
|
||||
if "coverArt" in track:
|
||||
cover_art_url = self.connection.getCoverArtUrl(track["coverArt"])
|
||||
|
||||
li.setIconImage(cover_art_url)
|
||||
li.setThumbnailImage(cover_art_url)
|
||||
li.setProperty("fanart_image", cover_art_url)
|
||||
|
||||
# Handle metadata
|
||||
li.setProperty("IsPlayable", "true")
|
||||
li.setMimeType(track.get("contentType"))
|
||||
li.setInfo(type="Music", infoLabels={
|
||||
"Artist": track.get("artist"),
|
||||
"Title": track.get("title"),
|
||||
"Year": track.get("year"),
|
||||
"Duration": track.get("duration"),
|
||||
"Genre": track.get("genre"),
|
||||
"TrackNumber": track.get("track")})
|
||||
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li)
|
||||
|
||||
def add_album(self, album, show_artist=False):
|
||||
"""
|
||||
Display one album in the list.
|
||||
"""
|
||||
|
||||
url = self.build_url({
|
||||
"mode": "track_list",
|
||||
"album_id": album["id"]})
|
||||
|
||||
# Create list item
|
||||
if show_artist:
|
||||
title = "%s - %s" % (
|
||||
album.get("artist", "<Unknown>"),
|
||||
album.get("name", "<Unknown>"))
|
||||
else:
|
||||
title = album.get("name", "<Unknown>")
|
||||
|
||||
# Add year if applicable
|
||||
if album.get("year"):
|
||||
title = "%s [%d]" % (title, album.get("year"))
|
||||
|
||||
# Create item
|
||||
li = xbmcgui.ListItem()
|
||||
li.setLabel(title)
|
||||
|
||||
# Handle cover art
|
||||
if "coverArt" in album:
|
||||
cover_art_url = self.connection.getCoverArtUrl(album["coverArt"])
|
||||
|
||||
li.setIconImage(cover_art_url)
|
||||
li.setThumbnailImage(cover_art_url)
|
||||
li.setProperty("fanart_image", cover_art_url)
|
||||
|
||||
# Handle metadata
|
||||
li.setInfo(type="music", infoLabels={
|
||||
"Artist": album.get("artist"),
|
||||
"Album": album.get("name"),
|
||||
"Year": album.get("year")})
|
||||
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
def main_page(self):
|
||||
"""
|
||||
Display main menu.
|
||||
"""
|
||||
|
||||
menu = [
|
||||
{"mode": "starred_list", "foldername": "Starred"},
|
||||
{"mode": "playlists_list", "foldername": "Playlists"},
|
||||
{"mode": "artist_list", "foldername": "Artists"},
|
||||
{"mode": "genre_list", "foldername": "Genres"},
|
||||
{"mode": "random_list", "foldername": "Random songs"}]
|
||||
|
||||
for entry in menu:
|
||||
url = self.build_url(entry)
|
||||
|
||||
li = xbmcgui.ListItem(entry["foldername"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def starred_list(self):
|
||||
"""
|
||||
Display starred songs.
|
||||
"""
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for starred in self.connection.walk_starred():
|
||||
self.add_track(starred, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def playlists_list(self):
|
||||
"""
|
||||
Display playlists.
|
||||
"""
|
||||
|
||||
for playlist in self.connection.walk_playlists():
|
||||
cover_art_url = self.connection.getCoverArtUrl(
|
||||
playlist["coverArt"])
|
||||
url = self.build_url({
|
||||
"mode": "playlist_list", "playlist_id": playlist["id"]})
|
||||
|
||||
li = xbmcgui.ListItem(playlist["name"], iconImage=cover_art_url)
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def playlist_list(self):
|
||||
"""
|
||||
Display playlist tracks.
|
||||
"""
|
||||
|
||||
playlist_id = self.addon_args["playlist_id"][0]
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for track in self.connection.walk_playlist(playlist_id):
|
||||
self.add_track(track, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def genre_list(self):
|
||||
"""
|
||||
Display list of genres menu.
|
||||
"""
|
||||
|
||||
for genre in self.connection.walk_genres():
|
||||
url = self.build_url({
|
||||
"mode": "albums_by_genre_list",
|
||||
"foldername": genre["value"].encode("utf-8")})
|
||||
|
||||
li = xbmcgui.ListItem(genre["value"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def albums_by_genre_list(self):
|
||||
"""
|
||||
Display album list by genre menu.
|
||||
"""
|
||||
|
||||
genre = self.addon_args["foldername"][0].decode("utf-8")
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "albums")
|
||||
|
||||
for album in self.connection.walk_album_list_genre(genre):
|
||||
self.add_album(album, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def artist_list(self):
|
||||
"""
|
||||
Display artist list
|
||||
"""
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "artists")
|
||||
|
||||
for artist in self.connection.walk_artists():
|
||||
cover_art_url = self.connection.getCoverArtUrl(artist["id"])
|
||||
url = self.build_url({
|
||||
"mode": "album_list",
|
||||
"artist_id": artist["id"]})
|
||||
|
||||
li = xbmcgui.ListItem(artist["name"])
|
||||
li.setIconImage(cover_art_url)
|
||||
li.setThumbnailImage(cover_art_url)
|
||||
li.setProperty("fanart_image", cover_art_url)
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def album_list(self):
|
||||
"""
|
||||
Display list of albums for certain artist.
|
||||
"""
|
||||
|
||||
artist_id = self.addon_args["artist_id"][0]
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "albums")
|
||||
|
||||
for album in self.connection.walk_artist(artist_id):
|
||||
self.add_album(album)
|
||||
|
||||
xbmcplugin.addSortMethod(
|
||||
self.addon_handle, xbmcplugin.SORT_METHOD_UNSORTED)
|
||||
xbmcplugin.addSortMethod(
|
||||
self.addon_handle, xbmcplugin.SORT_METHOD_ALBUM)
|
||||
xbmcplugin.addSortMethod(
|
||||
self.addon_handle, xbmcplugin.SORT_METHOD_ARTIST)
|
||||
xbmcplugin.addSortMethod(
|
||||
self.addon_handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def track_list(self):
|
||||
"""
|
||||
Display track list.
|
||||
"""
|
||||
|
||||
album_id = self.addon_args["album_id"][0]
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for track in self.connection.walk_album(album_id):
|
||||
self.add_track(track)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def random_list(self):
|
||||
"""
|
||||
Display random options.
|
||||
"""
|
||||
|
||||
menu = [
|
||||
{"mode": "random_by_genre_list", "foldername": "By genre"},
|
||||
{"mode": "random_by_year_list", "foldername": "By year"}]
|
||||
|
||||
for entry in menu:
|
||||
url = self.build_url(entry)
|
||||
|
||||
li = xbmcgui.ListItem(entry["foldername"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def random_by_genre_list(self):
|
||||
"""
|
||||
Display random genre list.
|
||||
"""
|
||||
|
||||
for genre in self.connection.walk_genres():
|
||||
url = self.build_url({
|
||||
"mode": "random_by_genre_track_list",
|
||||
"foldername": genre["value"].encode("utf-8")})
|
||||
|
||||
li = xbmcgui.ListItem(genre["value"])
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=self.addon_handle, url=url, listitem=li, isFolder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def random_by_genre_track_list(self):
|
||||
"""
|
||||
Display random tracks by genre
|
||||
"""
|
||||
|
||||
genre = self.addon_args["foldername"][0].decode("utf-8")
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for track in self.connection.walk_random_songs(
|
||||
size=self.random_count, genre=genre):
|
||||
self.add_track(track, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
def random_by_year_list(self):
|
||||
"""
|
||||
Display random tracks by year.
|
||||
"""
|
||||
|
||||
from_year = xbmcgui.Dialog().input(
|
||||
"From year", type=xbmcgui.INPUT_NUMERIC)
|
||||
to_year = xbmcgui.Dialog().input(
|
||||
"To year", type=xbmcgui.INPUT_NUMERIC)
|
||||
|
||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
||||
|
||||
for track in self.connection.walk_random_songs(
|
||||
size=self.random_count, from_year=from_year, to_year=to_year):
|
||||
self.add_track(track, show_artist=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Entry point for this plugin. Instantiates a new plugin object and runs the
|
||||
action that is given.
|
||||
"""
|
||||
|
||||
addon_url = sys.argv[0]
|
||||
addon_handle = int(sys.argv[1])
|
||||
addon_args = urlparse.parse_qs(sys.argv[2][1:])
|
||||
|
||||
# Route request to action.
|
||||
Plugin(addon_url, addon_handle, addon_args).route()
|
||||
|
||||
# Start plugin from within Kodi.
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
39
addon.xml
39
addon.xml
@ -1,21 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.audio.subsonic" name="Subsonic" version="2.0.0" provider-name="BasilFX">
|
||||
<addon id="plugin.audio.subsonic" name="Subsonic" version="3.0.3" provider-name="BasilFX,warwickh">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.19.0"/>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="addon.py">
|
||||
<extension point="xbmc.python.pluginsource" library="main.py">
|
||||
<provides>audio</provides>
|
||||
</extension>
|
||||
<extension point="xbmc.service" library="service.py" />
|
||||
<extension point="xbmc.addon.metadata">
|
||||
<summary lang="en">Subsonic music addon for Kodi.</summary>
|
||||
<description lang="en">Subsonic music addon for Kodi. Stream your tunes directly to Kodi.</description>
|
||||
<summary lang="fr">Extension Subsonic pour Kodi.</summary>
|
||||
<summary lang="de">Subsonic Musik Addon für Kodi.</summary>
|
||||
<description lang="en">
|
||||
Stream, star and download your tunes, directly to Kodi !
|
||||
For feature requests / issues:
|
||||
https://github.com/warwickh/plugin.audio.subsonic/issues
|
||||
Contributions are welcome:
|
||||
https://github.com/warwickh/plugin.audio.subsonic
|
||||
</description>
|
||||
<description lang="fr">
|
||||
Jouez, marquez vos favoris et téléchargez votre musique, directement dans Kodi !
|
||||
Pour les demandes et problèmes :
|
||||
https://github.com/warwickh/plugin.audio.subsonic/issues
|
||||
Les contributions sont les bienvenues :
|
||||
https://github.com/warwickh/plugin.audio.subsonic
|
||||
</description>
|
||||
<description lang="de">
|
||||
Streame, bewerte und downloade deine Medien direkt in Kodi !
|
||||
Für neue Eigentschaften oder Fehler:
|
||||
https://github.com/warwickh/plugin.audio.subsonic/issues
|
||||
Beihilfe ist Willkommen:
|
||||
https://github.com/warwickh/plugin.audio.subsonic
|
||||
</description>
|
||||
<assets>
|
||||
<icon>icon.png</icon>
|
||||
<fanart>fanart.jpg</fanart>
|
||||
</assets>
|
||||
<disclaimer lang="en"></disclaimer>
|
||||
<language>en</language>
|
||||
<language>multi</language>
|
||||
<platform>all</platform>
|
||||
<license>MIT</license>
|
||||
<forum></forum>
|
||||
<website>http://www.subsonic.org</website>
|
||||
<source>https://github.com/basilfx/plugin.audio.subsonic</source>
|
||||
<source>https://github.com/warwickh/plugin.audio.subsonic</source>
|
||||
<email></email>
|
||||
</extension>
|
||||
</addon>
|
||||
|
||||
BIN
fanart.jpg
Normal file
BIN
fanart.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
7
lib/dbutils/__init__.py
Normal file
7
lib/dbutils/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""
|
||||
Databse utilities for plugin.audio.subsonic
|
||||
"""
|
||||
|
||||
from .dbutils import *
|
||||
|
||||
__version__ = '0.0.1'
|
||||
118
lib/dbutils/dbutils.py
Normal file
118
lib/dbutils/dbutils.py
Normal file
@ -0,0 +1,118 @@
|
||||
import sqlite3 as sql
|
||||
import time
|
||||
import xbmc
|
||||
|
||||
tbl_artist_info_ddl = str('CREATE TABLE artist_info ('
|
||||
'artist_id TEXT NOT NULL PRIMARY KEY,'
|
||||
'artist_name TEXT,'
|
||||
'artist_info TEXT,'
|
||||
'mb_artist_id TEXT,'
|
||||
'image_url TEXT,'
|
||||
'wikidata_url TEXT,'
|
||||
'wikipedia_url TEXT,'
|
||||
'wikipedia_image TEXT,'
|
||||
'wikipedia_extract TEXT,'
|
||||
'last_update TEXT);')
|
||||
|
||||
class SQLiteDatabase(object):
|
||||
def __init__(self, db_filename):
|
||||
print("Init %s"%db_filename)
|
||||
self.db_filename = db_filename
|
||||
self.conn = None
|
||||
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
try:
|
||||
xbmc.log("Trying connection to the database %s"%self.db_filename, xbmc.LOGINFO)
|
||||
#print("Trying connection to the database %s"%self.db_filename)
|
||||
self.conn = sql.connect(self.db_filename)
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute(str('SELECT SQLITE_VERSION()'))
|
||||
xbmc.log("Connection %s was successful %s"%(self.db_filename, cursor.fetchone()[0]), xbmc.LOGINFO)
|
||||
#print("Connection %s was successful %s"%(self.db_filename, cursor.fetchone()[0]))
|
||||
cursor.row_factory = lambda cursor, row: row[0]
|
||||
cursor.execute(str('SELECT name FROM sqlite_master WHERE type=\'table\' ''AND name NOT LIKE \'sqlite_%\''))
|
||||
list_tables = cursor.fetchall()
|
||||
if not list_tables:
|
||||
# If no tables exist create a new one
|
||||
xbmc.log("Creating Subsonic local DB", xbmc.LOGINFO)
|
||||
#print("Creating Subsonic local DB")
|
||||
cursor.execute(tbl_artist_info_ddl)
|
||||
except sql.Error as e:
|
||||
xbmc.log("SQLite error %s"%e.args[0], xbmc.LOGINFO)
|
||||
#print("SQLite error %s"%e.args[0])
|
||||
|
||||
def get_cursor(self):
|
||||
return self.conn.cursor()
|
||||
|
||||
def run_query(self, query, params=None, cursor=None):
|
||||
#print("Processing query %s params %s"%(str(query),str(params)))
|
||||
try:
|
||||
if cursor is None:
|
||||
cursor = self.get_cursor()
|
||||
if params is not None:
|
||||
cursor.execute(query, params)
|
||||
else:
|
||||
cursor.execute(query)
|
||||
#print("%s rows affected"%cursor.rowcount)
|
||||
return cursor
|
||||
except sql.Error as e:
|
||||
print("SQLite error %s"%e.args[0])
|
||||
except ValueError:
|
||||
pass
|
||||
#print("Error query %s"%str(query))
|
||||
#print("Error query type %s"%type(query))
|
||||
#print("Error params %s"%str(params))
|
||||
#print("Error params type %s"%type(params))
|
||||
|
||||
def get_record_age(self, artist_id):
|
||||
try:
|
||||
last_update = self.get_value(artist_id, 'last_update')
|
||||
record_age = round(time.time())-round(float(last_update[0][0]))
|
||||
return record_age
|
||||
except IndexError:
|
||||
print("No existing record for artist %s" % artist_id)
|
||||
except Exception as e:
|
||||
print("get_record_age failed %s" % e)
|
||||
return 0
|
||||
|
||||
def get_artist_info(self, artist_id):
|
||||
query = 'SELECT * FROM artist_info WHERE artist_id = ?'# %str(artist_id)
|
||||
params = [str(artist_id)]
|
||||
cursor = self.run_query(query, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
def get_all(self):
|
||||
query = 'SELECT * FROM artist_info'
|
||||
#params = [str(artist_id)]
|
||||
cursor = self.run_query(query)#, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
def update_value(self, artist_id, field_name, field_value):
|
||||
success = False
|
||||
query = 'UPDATE artist_info SET %s = ?, last_update = ? WHERE artist_id = ?' % str(field_name)
|
||||
params = [str(field_value), str(time.time()), str(artist_id)]
|
||||
cursor = self.run_query(query, params)
|
||||
if (cursor.rowcount == 0):
|
||||
query = 'INSERT OR IGNORE INTO artist_info (artist_id, %s, last_update) VALUES (?, ?, ?)' % str(field_name)
|
||||
params = [str(artist_id), str(field_value), str(time.time())]
|
||||
cursor = self.run_query(query, params)
|
||||
try:
|
||||
self.conn.commit()
|
||||
success = True
|
||||
except Exception as e:
|
||||
print("Exception %s"%e)
|
||||
pass
|
||||
return success
|
||||
|
||||
def get_value(self, artist_id, field_name):
|
||||
query = 'SELECT %s FROM artist_info WHERE artist_id = ?' % str(field_name)
|
||||
params = [str(artist_id)]
|
||||
cursor = self.run_query(query, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
def close(self):
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
|
||||
@ -1,32 +1,24 @@
|
||||
"""
|
||||
This file is part of py-sonic.
|
||||
|
||||
py-sonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
py-sonic 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with py-sonic. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
For information on method calls, see 'pydoc libsonic.connection'
|
||||
|
||||
----------
|
||||
Basic example:
|
||||
----------
|
||||
|
||||
import libsonic
|
||||
|
||||
conn = libsonic.Connection('http://localhost' , 'admin' , 'password')
|
||||
print conn.ping()
|
||||
|
||||
"""
|
||||
|
||||
from connection import *
|
||||
from .connection import *
|
||||
|
||||
__version__ = '0.3.4'
|
||||
__version__ = '0.7.9'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,433 +0,0 @@
|
||||
import urllib
|
||||
import urlparse
|
||||
import libsonic
|
||||
|
||||
|
||||
def force_list(value):
|
||||
"""
|
||||
Coerce the input value to a list.
|
||||
|
||||
If `value` is `None`, return an empty list. If it is a single value, create
|
||||
a new list with that element on index 0.
|
||||
|
||||
:param value: Input value to coerce.
|
||||
:return: Value as list.
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
return []
|
||||
elif type(value) == list:
|
||||
return value
|
||||
else:
|
||||
return [value]
|
||||
|
||||
|
||||
class SubsonicClient(libsonic.Connection):
|
||||
"""
|
||||
Extend `libsonic.Connection` with new features and fix a few issues.
|
||||
|
||||
- Parse URL for host and port for constructor.
|
||||
- Make sure API results are of of uniform type.
|
||||
- Provide methods to intercept URL of binary requests.
|
||||
- Add order property to playlist items.
|
||||
- Add conventient `walk_*' methods to iterate over the API responses.
|
||||
"""
|
||||
|
||||
def __init__(self, url, username, password):
|
||||
"""
|
||||
Construct a new SubsonicClient.
|
||||
|
||||
:param str url: Full URL (including scheme) of the Subsonic server.
|
||||
:param str username: Username of the server.
|
||||
:param str password: Password of the server.
|
||||
"""
|
||||
|
||||
self.intercept_url = False
|
||||
|
||||
# Parse Subsonic URL
|
||||
parts = urlparse.urlparse(url)
|
||||
scheme = parts.scheme or "http"
|
||||
|
||||
# Make sure there is hostname
|
||||
if not parts.hostname:
|
||||
raise ValueError("Expected hostname for URL: %s" % url)
|
||||
|
||||
# Validate scheme
|
||||
if scheme not in ("http", "https"):
|
||||
raise ValueError("Unexpected scheme '%s' for URL: %s" % (
|
||||
scheme, url))
|
||||
|
||||
# Pick a default port
|
||||
host = "%s://%s" % (scheme, parts.hostname)
|
||||
port = parts.port or {"http": 80, "https": 443}[scheme]
|
||||
|
||||
# Invoke original constructor
|
||||
super(SubsonicClient, self).__init__(
|
||||
host, username, password, port=port)
|
||||
|
||||
def getIndexes(self, *args, **kwargs):
|
||||
"""
|
||||
Improve the getIndexes method. Ensures IDs are integers.
|
||||
"""
|
||||
|
||||
def _artists_iterator(artists):
|
||||
for artist in force_list(artists):
|
||||
artist["id"] = int(artist["id"])
|
||||
yield artist
|
||||
|
||||
def _index_iterator(index):
|
||||
for index in force_list(index):
|
||||
index["artist"] = list(_artists_iterator(index.get("artist")))
|
||||
yield index
|
||||
|
||||
def _children_iterator(children):
|
||||
for child in force_list(children):
|
||||
child["id"] = int(child["id"])
|
||||
|
||||
if "parent" in child:
|
||||
child["parent"] = int(child["parent"])
|
||||
if "coverArt" in child:
|
||||
child["coverArt"] = int(child["coverArt"])
|
||||
if "artistId" in child:
|
||||
child["artistId"] = int(child["artistId"])
|
||||
if "albumId" in child:
|
||||
child["albumId"] = int(child["albumId"])
|
||||
|
||||
yield child
|
||||
|
||||
response = super(SubsonicClient, self).getIndexes(*args, **kwargs)
|
||||
response["indexes"] = response.get("indexes", {})
|
||||
response["indexes"]["index"] = list(
|
||||
_index_iterator(response["indexes"].get("index")))
|
||||
response["indexes"]["child"] = list(
|
||||
_children_iterator(response["indexes"].get("child")))
|
||||
|
||||
return response
|
||||
|
||||
def getPlaylists(self, *args, **kwargs):
|
||||
"""
|
||||
Improve the getPlaylists method. Ensures IDs are integers.
|
||||
"""
|
||||
|
||||
def _playlists_iterator(playlists):
|
||||
for playlist in force_list(playlists):
|
||||
playlist["id"] = int(playlist["id"])
|
||||
yield playlist
|
||||
|
||||
response = super(SubsonicClient, self).getPlaylists(*args, **kwargs)
|
||||
response["playlists"]["playlist"] = list(
|
||||
_playlists_iterator(response["playlists"].get("playlist")))
|
||||
|
||||
return response
|
||||
|
||||
def getPlaylist(self, *args, **kwargs):
|
||||
"""
|
||||
Improve the getPlaylist method. Ensures IDs are integers and add an
|
||||
order property to each entry.
|
||||
"""
|
||||
|
||||
def _entries_iterator(entries):
|
||||
for order, entry in enumerate(force_list(entries), start=1):
|
||||
entry["id"] = int(entry["id"])
|
||||
entry["order"] = order
|
||||
yield entry
|
||||
|
||||
response = super(SubsonicClient, self).getPlaylist(*args, **kwargs)
|
||||
response["playlist"]["entry"] = list(
|
||||
_entries_iterator(response["playlist"].get("entry")))
|
||||
|
||||
return response
|
||||
|
||||
def getArtists(self, *args, **kwargs):
|
||||
"""
|
||||
Improve the getArtists method. Ensures IDs are integers.
|
||||
"""
|
||||
|
||||
def _artists_iterator(artists):
|
||||
for artist in force_list(artists):
|
||||
artist["id"] = int(artist["id"])
|
||||
yield artist
|
||||
|
||||
def _index_iterator(index):
|
||||
for index in force_list(index):
|
||||
index["artist"] = list(_artists_iterator(index.get("artist")))
|
||||
yield index
|
||||
|
||||
response = super(SubsonicClient, self).getArtists(*args, **kwargs)
|
||||
response["artists"] = response.get("artists", {})
|
||||
response["artists"]["index"] = list(
|
||||
_index_iterator(response["artists"].get("index")))
|
||||
|
||||
return response
|
||||
|
||||
def getArtist(self, *args, **kwargs):
|
||||
"""
|
||||
Improve the getArtist method. Ensures IDs are integers.
|
||||
"""
|
||||
|
||||
def _albums_iterator(albums):
|
||||
for album in force_list(albums):
|
||||
album["id"] = int(album["id"])
|
||||
|
||||
if "artistId" in album:
|
||||
album["artistId"] = int(album["artistId"])
|
||||
|
||||
yield album
|
||||
|
||||
response = super(SubsonicClient, self).getArtist(*args, **kwargs)
|
||||
response["artist"]["album"] = list(
|
||||
_albums_iterator(response["artist"].get("album")))
|
||||
|
||||
return response
|
||||
|
||||
def getMusicDirectory(self, *args, **kwargs):
|
||||
"""
|
||||
Improve the getMusicDirectory method. Ensures IDs are integers.
|
||||
"""
|
||||
|
||||
def _children_iterator(children):
|
||||
for child in force_list(children):
|
||||
child["id"] = int(child["id"])
|
||||
|
||||
if "parent" in child:
|
||||
child["parent"] = int(child["parent"])
|
||||
if "coverArt" in child:
|
||||
child["coverArt"] = int(child["coverArt"])
|
||||
if "artistId" in child:
|
||||
child["artistId"] = int(child["artistId"])
|
||||
if "albumId" in child:
|
||||
child["albumId"] = int(child["albumId"])
|
||||
|
||||
yield child
|
||||
|
||||
response = super(SubsonicClient, self).getMusicDirectory(
|
||||
*args, **kwargs)
|
||||
response["directory"]["child"] = list(
|
||||
_children_iterator(response["directory"].get("child")))
|
||||
|
||||
return response
|
||||
|
||||
def getAlbum(self, *args, **kwargs):
|
||||
"""
|
||||
Improve the getAlbum method. Ensures the IDs are real integers.
|
||||
"""
|
||||
|
||||
def _songs_iterator(songs):
|
||||
for song in force_list(songs):
|
||||
song["id"] = int(song["id"])
|
||||
yield song
|
||||
|
||||
response = super(SubsonicClient, self).getAlbum(*args, **kwargs)
|
||||
response["album"]["song"] = list(
|
||||
_songs_iterator(response["album"].get("song")))
|
||||
|
||||
return response
|
||||
|
||||
def getAlbumList2(self, *args, **kwargs):
|
||||
"""
|
||||
Improve the getAlbumList2 method. Ensures the IDs are real integers.
|
||||
"""
|
||||
|
||||
def _album_iterator(albums):
|
||||
for album in force_list(albums):
|
||||
album["id"] = int(album["id"])
|
||||
yield album
|
||||
|
||||
response = super(SubsonicClient, self).getAlbumList2(*args, **kwargs)
|
||||
response["albumList2"]["album"] = list(
|
||||
_album_iterator(response["albumList2"].get("album")))
|
||||
|
||||
return response
|
||||
|
||||
def getStarred(self, *args, **kwargs):
|
||||
"""
|
||||
Improve the getStarred method. Ensures the IDs are real integers.
|
||||
"""
|
||||
|
||||
def _song_iterator(songs):
|
||||
for song in force_list(songs):
|
||||
song["id"] = int(song["id"])
|
||||
yield song
|
||||
|
||||
response = super(SubsonicClient, self).getStarred(*args, **kwargs)
|
||||
response["starred"]["song"] = list(
|
||||
_song_iterator(response["starred"].get("song")))
|
||||
|
||||
return response
|
||||
|
||||
def getCoverArtUrl(self, *args, **kwargs):
|
||||
"""
|
||||
Return an URL to the cover art.
|
||||
"""
|
||||
|
||||
self.intercept_url = True
|
||||
url = self.getCoverArt(*args, **kwargs)
|
||||
self.intercept_url = False
|
||||
|
||||
return url
|
||||
|
||||
def streamUrl(self, *args, **kwargs):
|
||||
"""
|
||||
Return an URL to the file to stream.
|
||||
"""
|
||||
|
||||
self.intercept_url = True
|
||||
url = self.stream(*args, **kwargs)
|
||||
self.intercept_url = False
|
||||
|
||||
return url
|
||||
|
||||
def _doBinReq(self, *args, **kwargs):
|
||||
"""
|
||||
Intercept request URL to provide the URL of the item that is requested.
|
||||
|
||||
If the URL is intercepted, the request is not executed. A username and
|
||||
password is added to provide direct access to the stream.
|
||||
"""
|
||||
|
||||
if self.intercept_url:
|
||||
parts = list(urlparse.urlparse(
|
||||
args[0].get_full_url() + "?" + args[0].data))
|
||||
parts[4] = dict(urlparse.parse_qsl(parts[4]))
|
||||
parts[4].update({"u": self.username, "p": self.password})
|
||||
parts[4] = urllib.urlencode(parts[4])
|
||||
|
||||
return urlparse.urlunparse(parts)
|
||||
else:
|
||||
return super(SubsonicClient, self)._doBinReq(*args, **kwargs)
|
||||
|
||||
def walk_index(self):
|
||||
"""
|
||||
Request Subsonic's index and iterate each item.
|
||||
"""
|
||||
|
||||
response = self.getIndexes()
|
||||
|
||||
for index in response["indexes"]["index"]:
|
||||
for index in index["artist"]:
|
||||
for item in self.walk_directory(index["id"]):
|
||||
yield item
|
||||
|
||||
for child in response["indexes"]["child"]:
|
||||
if child.get("isDir"):
|
||||
for child in self.walk_directory(child["id"]):
|
||||
yield child
|
||||
else:
|
||||
yield child
|
||||
|
||||
def walk_playlists(self):
|
||||
"""
|
||||
Request Subsonic's playlists and iterate over each item.
|
||||
"""
|
||||
|
||||
response = self.getPlaylists()
|
||||
|
||||
for child in response["playlists"]["playlist"]:
|
||||
yield child
|
||||
|
||||
def walk_playlist(self, playlist_id):
|
||||
"""
|
||||
Request Subsonic's playlist items and iterate over each item.
|
||||
"""
|
||||
|
||||
response = self.getPlaylist(playlist_id)
|
||||
|
||||
for child in response["playlist"]["entry"]:
|
||||
yield child
|
||||
|
||||
def walk_starred(self):
|
||||
"""
|
||||
Request Subsonic's starred songs and iterate over each item.
|
||||
"""
|
||||
|
||||
response = self.getStarred()
|
||||
|
||||
for song in response["starred"]["song"]:
|
||||
yield song
|
||||
|
||||
def walk_directory(self, directory_id):
|
||||
"""
|
||||
Request a Subsonic music directory and iterate over each item.
|
||||
"""
|
||||
|
||||
response = self.getMusicDirectory(directory_id)
|
||||
|
||||
for child in response["directory"]["child"]:
|
||||
if child.get("isDir"):
|
||||
for child in self.walk_directory(child["id"]):
|
||||
yield child
|
||||
else:
|
||||
yield child
|
||||
|
||||
def walk_artist(self, artist_id):
|
||||
"""
|
||||
Request a Subsonic artist and iterate over each album.
|
||||
"""
|
||||
|
||||
response = self.getArtist(artist_id)
|
||||
|
||||
for child in response["artist"]["album"]:
|
||||
yield child
|
||||
|
||||
def walk_artists(self):
|
||||
"""
|
||||
Request all artists and iterate over each item.
|
||||
"""
|
||||
|
||||
response = self.getArtists()
|
||||
|
||||
for index in response["artists"]["index"]:
|
||||
for artist in index["artist"]:
|
||||
yield artist
|
||||
|
||||
def walk_genres(self):
|
||||
"""
|
||||
Request all genres and iterate over each item.
|
||||
"""
|
||||
|
||||
response = self.getGenres()
|
||||
|
||||
for genre in response["genres"]["genre"]:
|
||||
yield genre
|
||||
|
||||
def walk_album_list_genre(self, genre):
|
||||
"""
|
||||
Request all albums for a given genre and iterate over each album.
|
||||
"""
|
||||
|
||||
offset = 0
|
||||
|
||||
while True:
|
||||
response = self.getAlbumList2(
|
||||
ltype="byGenre", genre=genre, size=500, offset=offset)
|
||||
|
||||
if not response["albumList2"]["album"]:
|
||||
break
|
||||
|
||||
for album in response["albumList2"]["album"]:
|
||||
yield album
|
||||
|
||||
offset += 500
|
||||
|
||||
def walk_album(self, album_id):
|
||||
"""
|
||||
Request an alum and iterate over each item.
|
||||
"""
|
||||
|
||||
response = self.getAlbum(album_id)
|
||||
|
||||
for song in response["album"]["song"]:
|
||||
yield song
|
||||
|
||||
def walk_random_songs(self, size, genre=None, from_year=None,
|
||||
to_year=None):
|
||||
"""
|
||||
Request random songs by genre and/or year and iterate over each song.
|
||||
"""
|
||||
|
||||
response = self.getRandomSongs(
|
||||
size=size, genre=genre, fromYear=from_year, toYear=to_year)
|
||||
|
||||
for song in response["randomSongs"]["song"]:
|
||||
yield song
|
||||
6
lib/musicbrainz/__init__.py
Normal file
6
lib/musicbrainz/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""
|
||||
Musicbrainz utilities for plugin.audio.subsonic
|
||||
"""
|
||||
from .mbconnection import *
|
||||
|
||||
__version__ = '0.0.1'
|
||||
146
lib/musicbrainz/mbconnection.py
Normal file
146
lib/musicbrainz/mbconnection.py
Normal file
@ -0,0 +1,146 @@
|
||||
import os
|
||||
import json
|
||||
import urllib.request
|
||||
from urllib.parse import urlencode
|
||||
import xml.etree.ElementTree as ET
|
||||
import re
|
||||
|
||||
urllib.request.install_opener(urllib.request.build_opener(urllib.request.HTTPSHandler))
|
||||
|
||||
class MBConnection(object):
|
||||
def __init__(self, lang = "en"):
|
||||
self._lang = lang
|
||||
self._baseUrl = "https://musicbrainz.org/ws/2"
|
||||
self._wikipediaBaseUrl = "https://{}.wikipedia.org/wiki".format(self._lang)
|
||||
self._wikidataBaseUrl = "https://www.wikidata.org/wiki/Special:EntityData"
|
||||
self._opener = self._getOpener()
|
||||
|
||||
def _getOpener(self):
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPSHandler)
|
||||
return opener
|
||||
|
||||
def search(self, entity, query, limit=None, offset=None):
|
||||
viewName = '%s' % entity
|
||||
q = self._getQueryDict({'query': query, 'limit': limit, 'offset': offset})
|
||||
req = self._getRequest(self._baseUrl, viewName, q)
|
||||
res = self._doInfoReqXml(req)
|
||||
return res
|
||||
|
||||
def get_wiki_extract(self, title):
|
||||
try:
|
||||
if('http' in title):
|
||||
#accepts search text or full url https://en.wikipedia.org/wiki/Alex_Lloyd
|
||||
pattern = 'wikipedia.org/wiki/(.+)'
|
||||
title = re.search(pattern, title).group(1)
|
||||
viewName = 'api.php'
|
||||
q = self._getQueryDict({'format' : 'json', 'action' : 'query', 'prop' : 'extracts', 'redirects' : 1, 'titles' : title})
|
||||
req = self._getRequest(self._wikipediaBaseUrl[:-3], viewName, q, 'exintro&explaintext&')
|
||||
res = self._doInfoReqJson(req)
|
||||
pages = res['query']['pages']
|
||||
extract = list(pages.values())[0]['extract']
|
||||
return extract
|
||||
except Exception as e:
|
||||
print("get_artist_wikpedia failed %s"%e)
|
||||
return
|
||||
|
||||
#https://en.wikipedia.org/w/api.php?exintro&explaintext&format=json&action=query&prop=extracts&redirects=1&titles=%C3%89milie_Simon
|
||||
def get_wiki_image(self, title):
|
||||
try:
|
||||
if('http' in title):
|
||||
#accepts search text or full url https://en.wikipedia.org/wiki/Alex_Lloyd
|
||||
pattern = 'wikipedia.org/wiki/(.+)'
|
||||
title = re.search(pattern, title).group(1)
|
||||
viewName = 'api.php'
|
||||
q = self._getQueryDict({'format' : 'json', 'action' : 'query', 'prop' : 'pageimages', 'pithumbsize' : 800, 'titles' : title})
|
||||
req = self._getRequest(self._wikipediaBaseUrl[:-3], viewName, q)
|
||||
res = self._doInfoReqJson(req)
|
||||
pages = res['query']['pages']
|
||||
print(res['query']['pages'])
|
||||
image_url = list(pages.values())[0]['thumbnail']['source']
|
||||
return image_url
|
||||
except Exception as e:
|
||||
print("get_wiki_image failed %s"%e)
|
||||
return
|
||||
|
||||
def get_artist_id(self, query):
|
||||
try:
|
||||
dres = self.search('artist', query)
|
||||
artist_list = dres.find('{http://musicbrainz.org/ns/mmd-2.0#}artist-list')
|
||||
artist = artist_list.find('{http://musicbrainz.org/ns/mmd-2.0#}artist')
|
||||
return artist.attrib['id']
|
||||
except Exception as e:
|
||||
print("get_artist_id failed %s"%e)
|
||||
return
|
||||
|
||||
def get_relation(self, artist_id, rel_type):
|
||||
try:
|
||||
viewName = 'artist/%s' % artist_id
|
||||
q = self._getQueryDict({'inc': "url-rels"})
|
||||
req = self._getRequest(self._baseUrl, viewName, q)
|
||||
res = self._doInfoReqXml(req)
|
||||
for relation in res.iter('{http://musicbrainz.org/ns/mmd-2.0#}relation'):
|
||||
if relation.attrib['type'] == rel_type:
|
||||
return relation.find('{http://musicbrainz.org/ns/mmd-2.0#}target').text
|
||||
except Exception as e:
|
||||
print("get_artist_image failed %s"%e)
|
||||
return
|
||||
|
||||
def get_artist_image(self, artist_id):
|
||||
try:
|
||||
image = self.get_relation(artist_id, 'image')
|
||||
return image
|
||||
except Exception as e:
|
||||
print("get_artist_image failed %s"%e)
|
||||
return
|
||||
|
||||
def get_artist_wikpedia(self, artist_id):
|
||||
wikidata_url = self.get_relation(artist_id, 'wikidata')
|
||||
pattern = 'www.wikidata.org/wiki/(Q\d+)'
|
||||
try:
|
||||
wikidata_ref = re.search(pattern, wikidata_url).group(1)
|
||||
viewName = '%s.rdf' % wikidata_ref
|
||||
q = self._getQueryDict({})
|
||||
req = self._getRequest(self._wikidataBaseUrl, viewName, q )
|
||||
res = self._doInfoReqXml(req)
|
||||
for item in res.iter('{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description'):
|
||||
try:
|
||||
url = item.attrib['{http://www.w3.org/1999/02/22-rdf-syntax-ns#}about']
|
||||
if self._wikipediaBaseUrl in url:
|
||||
#print(urlencode(url))
|
||||
#print((url.encode().decode('unicode-escape')))
|
||||
return urllib.parse.unquote(url)
|
||||
except KeyError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print("get_artist_wikpedia failed %s"%e)
|
||||
return
|
||||
|
||||
def _getQueryDict(self, d):
|
||||
"""
|
||||
Given a dictionary, it cleans out all the values set to None
|
||||
"""
|
||||
for k, v in list(d.items()):
|
||||
if v is None:
|
||||
del d[k]
|
||||
return d
|
||||
|
||||
def _getRequest(self, baseUrl, viewName, query={}, prefix=""):
|
||||
qdict = {}
|
||||
qdict.update(query)
|
||||
url = '%s/%s' % (baseUrl, viewName)
|
||||
if(prefix!='' or qdict!={}):
|
||||
url += "?%s%s" % (prefix, urlencode(qdict))
|
||||
print("UseGET URL %s" % (url))
|
||||
req = urllib.request.Request(url)
|
||||
return req
|
||||
|
||||
def _doInfoReqXml(self, req):
|
||||
res = urllib.request.urlopen(req)
|
||||
data = res.read().decode('utf-8')
|
||||
dres = ET.fromstring(data)
|
||||
return dres
|
||||
|
||||
def _doInfoReqJson(self, req):
|
||||
res = urllib.request.urlopen(req)
|
||||
dres = json.loads(res.read().decode('utf-8'))
|
||||
return dres
|
||||
4
lib/simpleplugin/__init__.py
Normal file
4
lib/simpleplugin/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
#v2.1.0
|
||||
#https://github.com/romanvm/script.module.simpleplugin/releases
|
||||
|
||||
from .simpleplugin import *
|
||||
1354
lib/simpleplugin/simpleplugin.py
Normal file
1354
lib/simpleplugin/simpleplugin.py
Normal file
File diff suppressed because it is too large
Load Diff
182
resources/language/English/strings.po
Normal file
182
resources/language/English/strings.po
Normal file
@ -0,0 +1,182 @@
|
||||
# XBMC Media Center language file
|
||||
# Addon Name: Subsonic
|
||||
# Addon id: plugin.audio.subsonic
|
||||
# Addon Provider:
|
||||
# Addon Translate: Moshkopp
|
||||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "General"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Server"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30002"
|
||||
msgid "Server URL"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Display"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Albums per page"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Tracks per page (ignored in albums & playlists)"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30009"
|
||||
msgid "Download folder"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Streaming"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "Transcode format"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "Bitrate"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30013"
|
||||
msgid "Advanced Settings"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "API version"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Allow self signed certificates"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Cache (in minutes)"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Cache data time"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Library"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Albums"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Tracks"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Playlists"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Newest albums"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Most played albums"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Recently played albums"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30026"
|
||||
msgid "Random albums"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30029"
|
||||
msgid "Next page"
|
||||
msgstr ""
|
||||
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Back to Menu"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30031"
|
||||
msgid "Item has been unstarred."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30032"
|
||||
msgid "Item has been starred!"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30033"
|
||||
msgid "Star on Subsonic"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30034"
|
||||
msgid "Unstar on Subsonic"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30035"
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30036"
|
||||
msgid "Starred tracks"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30037"
|
||||
msgid "Random tracks"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30038"
|
||||
msgid "Browse"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30039"
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30040"
|
||||
msgid "useGET"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30041"
|
||||
msgid "legacyauth"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30042"
|
||||
msgid "port"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30043"
|
||||
msgid "Merge album folders"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Scrobble to Last.FM"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Enhanced Information"
|
||||
msgstr ""
|
||||
182
resources/language/French/strings.po
Normal file
182
resources/language/French/strings.po
Normal file
@ -0,0 +1,182 @@
|
||||
# XBMC Media Center language file
|
||||
# Addon Name: Subsonic
|
||||
# Addon id: plugin.audio.subsonic
|
||||
# Addon Provider:
|
||||
# Addon Translate: Gordie
|
||||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "General"
|
||||
msgstr "Général"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Server"
|
||||
msgstr "Serveur"
|
||||
|
||||
msgctxt "#30002"
|
||||
msgid "Server URL"
|
||||
msgstr "URL du serveur"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Username"
|
||||
msgstr "Nom d'utilisateur"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Display"
|
||||
msgstr "Affichage"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Albums per page"
|
||||
msgstr "Albums par page"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Tracks per page (ignored in albums & playlists)"
|
||||
msgstr "Pistes par page (ignoré dans les albums & listes de lecture)"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Download"
|
||||
msgstr "Télécharger"
|
||||
|
||||
msgctxt "#30009"
|
||||
msgid "Download folder"
|
||||
msgstr "Répertoire de téléchargement"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Streaming"
|
||||
msgstr "Diffusion"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "Transcode format"
|
||||
msgstr "Format de transcodage"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "Bitrate"
|
||||
msgstr "Bitrate"
|
||||
|
||||
msgctxt "#30013"
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Paramètres avancés"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "API version"
|
||||
msgstr "Version de l'API"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Allow self signed certificates"
|
||||
msgstr "Autoriser les certificats auto-signés"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Cache (in minutes)"
|
||||
msgstr "Cache (en minutes)"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Cache datas time"
|
||||
msgstr "Durée du cache pour les données"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Library"
|
||||
msgstr "Bibliothèque"
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Albums"
|
||||
msgstr "Albums"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Tracks"
|
||||
msgstr "Pistes"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Playlists"
|
||||
msgstr "Playlists"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Newest albums"
|
||||
msgstr "Nouveaux albums"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Most played albums"
|
||||
msgstr "Albums les plus joués"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Recently played albums"
|
||||
msgstr "Albums joués récemment"
|
||||
|
||||
msgctxt "#30026"
|
||||
msgid "Random albums"
|
||||
msgstr "Albums au hasard"
|
||||
|
||||
msgctxt "#30029"
|
||||
msgid "Next page"
|
||||
msgstr "Page suivante"
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Back to Menu"
|
||||
msgstr "Retour au menu"
|
||||
|
||||
msgctxt "#30031"
|
||||
msgid "Item has been unstarred."
|
||||
msgstr "Cet élément a été retiré des favoris"
|
||||
|
||||
msgctxt "#30032"
|
||||
msgid "Item has been starred!"
|
||||
msgstr "Cet élément a été ajouté aux favoris !"
|
||||
|
||||
msgctxt "#30033"
|
||||
msgid "Star on Subsonic"
|
||||
msgstr "Ajouter aux favoris Subsonic"
|
||||
|
||||
msgctxt "#30034"
|
||||
msgid "Unstar on Subsonic"
|
||||
msgstr "Retirer des favoris Subsonic"
|
||||
|
||||
msgctxt "#30035"
|
||||
msgid "Download"
|
||||
msgstr "Télécharger"
|
||||
|
||||
msgctxt "#30036"
|
||||
msgid "Starred tracks"
|
||||
msgstr "Pistes favorites"
|
||||
|
||||
msgctxt "#30037"
|
||||
msgid "Random tracks"
|
||||
msgstr "Pistes au hasard"
|
||||
|
||||
msgctxt "#30038"
|
||||
msgid "Browse"
|
||||
msgstr "Parcourir"
|
||||
|
||||
msgctxt "#30039"
|
||||
msgid "Search"
|
||||
msgstr "Rechercher"
|
||||
|
||||
|
||||
msgctxt "#30040"
|
||||
msgid "useGET"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30041"
|
||||
msgid "legacyauth"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30042"
|
||||
msgid "port"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30043"
|
||||
msgid "Merge album folders"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Scrobble to Last.FM"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Enhanced Information"
|
||||
msgstr ""
|
||||
181
resources/language/German/strings.po
Normal file
181
resources/language/German/strings.po
Normal file
@ -0,0 +1,181 @@
|
||||
# XBMC Media Center language file
|
||||
# Addon Name: Subsonic
|
||||
# Addon id: plugin.audio.subsonic
|
||||
# Addon Provider:
|
||||
# Addon Translate: Moshkopp
|
||||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "General"
|
||||
msgstr "Allgemein"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Server"
|
||||
msgstr "Server"
|
||||
|
||||
msgctxt "#30002"
|
||||
msgid "Server URL"
|
||||
msgstr "Serveradresse"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Username"
|
||||
msgstr "Benutzername"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Display"
|
||||
msgstr "Anzeige"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Albums per page"
|
||||
msgstr "Alben pro Seite"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Tracks per page (ignored in albums & playlists)"
|
||||
msgstr "Lieder pro Seite (wird in Alben und Playlisten ignoriert)"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Download"
|
||||
msgstr "Download"
|
||||
|
||||
msgctxt "#30009"
|
||||
msgid "Download folder"
|
||||
msgstr "Download Verzeichnis"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Streaming"
|
||||
msgstr "Übertragung"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "Transcode format"
|
||||
msgstr "Umwandlungs Format"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "Bitrate"
|
||||
msgstr "Bitrate"
|
||||
|
||||
msgctxt "#30013"
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Erweitert"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "API version"
|
||||
msgstr "API Version"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Allow self signed certificates"
|
||||
msgstr "Erlaube eigensignierte Zertifikate"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Cache (in minutes)"
|
||||
msgstr "Speicher (in Minuten)"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Cache datas time"
|
||||
msgstr "Speicher Daten Zeit"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Library"
|
||||
msgstr "Bibliothek"
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Albums"
|
||||
msgstr "Alben"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Tracks"
|
||||
msgstr "Lieder"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Playlists"
|
||||
msgstr "Playlisten"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Newest albums"
|
||||
msgstr "Neueste Alben"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Most played albums"
|
||||
msgstr "Häufig gehörte Alben"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Recently played albums"
|
||||
msgstr "Zuletzt gehörte Alben"
|
||||
|
||||
msgctxt "#30026"
|
||||
msgid "Random albums"
|
||||
msgstr "Zufällige Alben"
|
||||
|
||||
msgctxt "#30029"
|
||||
msgid "Next page"
|
||||
msgstr "Nächste Seite"
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Back to Menu"
|
||||
msgstr "Hauptmenü"
|
||||
|
||||
msgctxt "#30031"
|
||||
msgid "Item has been unstarred."
|
||||
msgstr "Bewertung entfernt"
|
||||
|
||||
msgctxt "#30032"
|
||||
msgid "Item has been starred!"
|
||||
msgstr "Bewertung hinzugefügt"
|
||||
|
||||
msgctxt "#30033"
|
||||
msgid "Star on Subsonic"
|
||||
msgstr "Bewerten auf Subsonic"
|
||||
|
||||
msgctxt "#30034"
|
||||
msgid "Unstar on Subsonic"
|
||||
msgstr "Löschen auf Subsonic"
|
||||
|
||||
msgctxt "#30035"
|
||||
msgid "Download"
|
||||
msgstr "Herunterladen"
|
||||
|
||||
msgctxt "#30036"
|
||||
msgid "Starred tracks"
|
||||
msgstr "Lieblings lieder"
|
||||
|
||||
msgctxt "#30037"
|
||||
msgid "Random tracks"
|
||||
msgstr "Zufällig lieder"
|
||||
|
||||
msgctxt "#30038"
|
||||
msgid "Browse"
|
||||
msgstr "Durchsuchen"
|
||||
|
||||
msgctxt "#30039"
|
||||
msgid "Search"
|
||||
msgstr "Suche"
|
||||
|
||||
msgctxt "#30040"
|
||||
msgid "useGET"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30041"
|
||||
msgid "legacyauth"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30042"
|
||||
msgid "port"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30043"
|
||||
msgid "Merge album folders"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Scrobble to Last.FM"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Enhanced Information"
|
||||
msgstr ""
|
||||
@ -1,9 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<settings>
|
||||
<setting id="subsonic_url" type="text" label="URL" default="http://demo.subsonic.org"/>
|
||||
<setting id="username" type="text" label="Username" default="guest3"/>
|
||||
<setting id="password" type="text" option="hidden" label="Password" default="guest"/>
|
||||
<setting id="random_count" type="labelenum" label="Random songs" values="10|15|20|25"/>
|
||||
<setting id="transcode_format" type="labelenum" label="Transcode format" values="mp3|raw|flv|ogg"/>
|
||||
<setting id="bitrate" type="labelenum" label="Bitrate" values="320|256|224|192|160|128|112|96|80|64|56|48|40|32"/>
|
||||
<!-- GENERAL -->
|
||||
<category label="30000">
|
||||
<setting label="30001" type="lsep" />
|
||||
<setting label="30002" id="subsonic_url" type="text" default="http://demo.subsonic.org"/>
|
||||
<setting label="30042" id="port" type="text" default="80"/>
|
||||
<setting label="30003" id="username" type="text" default="guest3"/>
|
||||
<setting label="30004" id="password" type="text" option="hidden" default="guest"/>
|
||||
<setting label="30005" type="lsep" />
|
||||
<setting label="30006" id="albums_per_page" type="labelenum" default="50" values="10|25|50|100|250|500"/>
|
||||
<setting label="30007" id="tracks_per_page" type="labelenum" default="100" values="10|25|50|100|250|500"/>
|
||||
<setting label="30008" type="lsep" />
|
||||
<setting label="30009" id="download_folder" type="folder" source="auto" option="writeable"/>
|
||||
<setting label="30010" type="lsep" />
|
||||
<setting label="30011" id="transcode_format_streaming" type="labelenum" values="mp3|raw|flv|ogg"/>
|
||||
<setting label="30012" id="bitrate_streaming" type="labelenum" default="0" values="320|256|224|192|160|128|112|96|80|64|56|48|40|32|0"/>
|
||||
</category>
|
||||
|
||||
<!-- ADVANCED -->
|
||||
<category label="30013">
|
||||
<setting label="30001" type="lsep" />
|
||||
<setting label="30014" id="apiversion" type="labelenum" values="1.11.0|1.12.0|1.13.0|1.14.0|1.15.0|1.16.0" default="1.15.0"/>
|
||||
<setting label="30016" id="insecure" type="bool" default="false" />
|
||||
<setting label="30040" id="useget" type="bool" default="true" />
|
||||
<setting label="30041" id="legacyauth" type="bool" default="false" />
|
||||
<setting label="30017" type="lsep" />
|
||||
<setting label="30018" id="cachetime" type="labelenum" default="3600" values="1|5|15|30|60|120|180|720|1440|3600"/>
|
||||
<setting label="30043" id="merge" type="bool" default="false" />
|
||||
<setting label="30044" id="scrobble" type="bool" default="false" />
|
||||
<setting label="30045" id="enhanced_info" type="bool" default="false" />
|
||||
|
||||
</category>
|
||||
</settings>
|
||||
|
||||
220
service.py
Normal file
220
service.py
Normal file
@ -0,0 +1,220 @@
|
||||
import re
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
import os
|
||||
import xbmcaddon
|
||||
import time
|
||||
import random
|
||||
|
||||
# Add the /lib folder to sys
|
||||
sys.path.append(xbmcvfs.translatePath(os.path.join(xbmcaddon.Addon("plugin.audio.subsonic").getAddonInfo("path"), "lib")))
|
||||
|
||||
import dbutils
|
||||
import libsonic
|
||||
import musicbrainz
|
||||
|
||||
connection = None
|
||||
db = None
|
||||
mb = None
|
||||
|
||||
serviceEnabled = True
|
||||
|
||||
refresh_age = 86400#3600 #multiple of random to age info records - needs some validation
|
||||
check_freq_init = 30 #How often to run a refresh cycle - needs some validation - updates afer first load to
|
||||
check_freq_refresh = 86400
|
||||
|
||||
|
||||
db_filename = "subsonic_sqlite.db"
|
||||
|
||||
last_db_check = 0
|
||||
current_artist_index = 0
|
||||
|
||||
from simpleplugin import Plugin
|
||||
from simpleplugin import Addon
|
||||
|
||||
# Create plugin instance
|
||||
plugin = Plugin()
|
||||
|
||||
try:
|
||||
enhancedInfo = Addon().get_setting('enhanced_info')
|
||||
except:
|
||||
enhancedInfo = False
|
||||
|
||||
try:
|
||||
scrobbleEnabled = Addon().get_setting('scrobble')
|
||||
except:
|
||||
scrobbleEnabled = False
|
||||
|
||||
scrobbled = False
|
||||
|
||||
def check_address_format():
|
||||
address = Addon().get_setting('subsonic_url')
|
||||
port = Addon().get_setting('port')
|
||||
if len(address.split(":"))>2:
|
||||
found_port = address.split(":")[2]
|
||||
plugin.log("Found port %s in address %s, splitting"%(found_port, address))
|
||||
plugin.log("Changing port from %s to %s"%(port, found_port))
|
||||
Addon().set_setting('port', int(found_port))
|
||||
Addon().set_setting('subsonic_url', "%s:%s"%(address.split(":")[0],address.split(":")[1]))
|
||||
|
||||
def popup(text, time=5000, image=None):
|
||||
title = plugin.addon.getAddonInfo('name')
|
||||
icon = plugin.addon.getAddonInfo('icon')
|
||||
xbmc.executebuiltin('Notification(%s, %s, %d, %s)' % (title, text, time, icon))
|
||||
|
||||
def get_connection():
|
||||
global connection
|
||||
if connection==None:
|
||||
connected = False
|
||||
try:
|
||||
connection = libsonic.Connection(
|
||||
baseUrl=Addon().get_setting('subsonic_url'),
|
||||
username=Addon().get_setting('username', convert=False),
|
||||
password=Addon().get_setting('password', convert=False),
|
||||
port=Addon().get_setting('port'),
|
||||
apiVersion=Addon().get_setting('apiversion'),
|
||||
insecure=Addon().get_setting('insecure'),
|
||||
legacyAuth=Addon().get_setting('legacyauth'),
|
||||
useGET=Addon().get_setting('useget'),
|
||||
appName="Kodi-Subsonic",
|
||||
)
|
||||
connected = connection.ping()
|
||||
except:
|
||||
pass
|
||||
|
||||
if connected==False:
|
||||
popup('Connection error')
|
||||
return False
|
||||
|
||||
return connection
|
||||
|
||||
def get_mb():
|
||||
global mb
|
||||
mb = musicbrainz.MBConnection()
|
||||
return mb
|
||||
|
||||
def get_db():
|
||||
global db
|
||||
db_path = os.path.join(plugin.profile_dir, db_filename)
|
||||
plugin.log("Getting DB %s"%db_path)
|
||||
try:
|
||||
db = dbutils.SQLiteDatabase(db_path)
|
||||
except Exception as e:
|
||||
plugin.log("Connecting to DB failed: %s"%e)
|
||||
return db
|
||||
|
||||
def refresh_artist(artist_id):
|
||||
db = get_db()
|
||||
connection = get_connection()
|
||||
artist = connection.getArtist(artist_id)#['subsonic-response']
|
||||
artist_name = artist['artist']['name']
|
||||
db.update_value(artist_id, 'artist_name', artist_name)
|
||||
artist_info = connection.getArtistInfo2(artist_id)
|
||||
try:
|
||||
artist_info = artist_info['artistInfo2']['biography']
|
||||
#pattern = '<a target=\'_blank\' href="https://www.last.fm/music/Afrojack">Read more on Last.fm</a>
|
||||
artist_info = re.sub('<a.*?</a>', '', artist_info)
|
||||
plugin.log("subbed: %s"%artist_info)
|
||||
except:
|
||||
artist_info = ""
|
||||
if(enhancedInfo):
|
||||
mb = get_mb()
|
||||
mb_artist_id = mb.get_artist_id(artist_name)
|
||||
artist_image_url = mb.get_artist_image(mb_artist_id)
|
||||
wikipedia_url = mb.get_artist_wikpedia(mb_artist_id)
|
||||
artist_wiki_extract = mb.get_wiki_extract(wikipedia_url)
|
||||
wikipedia_image = mb.get_wiki_image(wikipedia_url)
|
||||
db.update_value(artist_id, 'artist_info', artist_info)
|
||||
db.update_value(artist_id, 'mb_artist_id', mb_artist_id)
|
||||
db.update_value(artist_id, 'image_url', artist_image_url)
|
||||
db.update_value(artist_id, 'wikipedia_url', wikipedia_url)
|
||||
db.update_value(artist_id, 'wikipedia_extract', artist_wiki_extract)
|
||||
db.update_value(artist_id, 'wikipedia_image', wikipedia_image)
|
||||
|
||||
def check_db_status():
|
||||
global last_db_check
|
||||
global check_freq_init
|
||||
global current_artist_index
|
||||
try:
|
||||
if(time.time()-check_freq_init > last_db_check): #Won't check on every run uses check_freq_init
|
||||
plugin.log("DB check starting %s %s" % (time.time(), last_db_check))
|
||||
db = get_db()
|
||||
connection = get_connection()
|
||||
response = connection.getArtists()
|
||||
current_index_content = response["artists"]["index"][current_artist_index] #Completes refresh for alpha index
|
||||
plugin.log("Starting info load for index %s"%current_index_content['name'])
|
||||
for artist in current_index_content["artist"]:
|
||||
artist_id = artist['id']
|
||||
record_age = db.get_record_age(artist_id)
|
||||
rnd_age = random.randint(1,111)*refresh_age
|
||||
#plugin.log("Record age %s vs %s for %s"%(record_age, rnd_age, artist_id))
|
||||
if(not record_age or (record_age > rnd_age)):
|
||||
#plugin.log("Refreshing %s" % (artist_id))
|
||||
refresh_artist(artist_id)
|
||||
#plugin.log("Refresh complete for %s" % (artist_id))
|
||||
plugin.log("Finished info loading for index %s"%current_index_content['name'])
|
||||
current_artist_index+=1
|
||||
if(current_artist_index>=len(response["artists"]["index"])): #init load complete go to daily check freq
|
||||
plugin.log("Finished info loading for all alpha index")
|
||||
current_artist_index=0
|
||||
check_freq_init = check_freq_refresh
|
||||
plugin.log("check_freq_init is now %s"%check_freq_init)
|
||||
last_db_check = time.time()
|
||||
except Exception as e:
|
||||
plugin.log("Refresh check failed %s"%e)
|
||||
|
||||
return
|
||||
|
||||
def check_player_status():
|
||||
global scrobbled
|
||||
if (scrobbleEnabled and xbmc.getCondVisibility("Player.HasMedia")):
|
||||
try:
|
||||
currentFileName = xbmc.getInfoLabel("Player.Filenameandpath")
|
||||
currentFileProgress = xbmc.getInfoLabel("Player.Progress")
|
||||
pattern = re.compile(r'plugin:\/\/plugin\.audio\.subsonic\/\?action=play_track&id=(.*?)&')
|
||||
currentTrackId = re.findall(pattern, currentFileName)[0]
|
||||
#xbmc.log("Name %s Id %s Progress %s"%(currentFileName,currentTrackId,currentFileProgress), xbmc.LOGDEBUG)
|
||||
if (int(currentFileProgress)<50):
|
||||
scrobbled = False
|
||||
elif (int(currentFileProgress)>=50 and scrobbled == False):
|
||||
xbmc.log("Scrobbling Track Id %s"%(currentTrackId), xbmc.LOGDEBUG)
|
||||
success = scrobble_track(currentTrackId)
|
||||
if success:
|
||||
scrobbled = True
|
||||
else:
|
||||
pass
|
||||
except IndexError:
|
||||
plugin.log ("Not a Subsonic track")
|
||||
scrobbled = True
|
||||
except Exception as e:
|
||||
xbmc.log("Subsonic scrobble check failed %e"%e, xbmc.LOGINFO)
|
||||
return
|
||||
|
||||
def scrobble_track(track_id):
|
||||
connection = get_connection()
|
||||
if connection==False:
|
||||
return False
|
||||
res = connection.scrobble(track_id)
|
||||
#xbmc.log("response %s"%(res), xbmc.LOGINFO)
|
||||
if res['status'] == 'ok':
|
||||
popup("Scrobbled track")
|
||||
return True
|
||||
else:
|
||||
popup("Scrobble failed")
|
||||
xbmc.log("Scrobble failed", xbmc.LOGERROR)
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if serviceEnabled:
|
||||
check_address_format()
|
||||
monitor = xbmc.Monitor()
|
||||
xbmc.log("Subsonic service started", xbmc.LOGINFO)
|
||||
popup("Subsonic service started")
|
||||
while not monitor.abortRequested():
|
||||
if monitor.waitForAbort(10):
|
||||
break
|
||||
check_player_status()
|
||||
check_db_status()
|
||||
else:
|
||||
plugin.log("Subsonic service not enabled")
|
||||
Reference in New Issue
Block a user