mirror of
https://github.com/beetbox/beets.git
synced 2025-12-14 20:43:41 +01:00
Added embyupdate plugin
Its a simple plugin that triggers a library refresh after the library got changed. It does the same thing like the plexupdate plugin.
This commit is contained in:
parent
48637f22e9
commit
4b2b9fe2ce
5 changed files with 385 additions and 1 deletions
133
beetsplug/embyupdate.py
Normal file
133
beetsplug/embyupdate.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""Updates the Emby Library whenever the beets library is changed.
|
||||
|
||||
emby:
|
||||
host: localhost
|
||||
port: 8096
|
||||
username: user
|
||||
password: password
|
||||
"""
|
||||
from __future__ import (division, absolute_import, print_function,
|
||||
unicode_literals)
|
||||
|
||||
from beets import config
|
||||
from beets.plugins import BeetsPlugin
|
||||
from urllib import urlencode
|
||||
from urlparse import urljoin, parse_qs, urlsplit, urlunsplit
|
||||
import hashlib
|
||||
import requests
|
||||
|
||||
|
||||
def api_url(host, port, endpoint):
|
||||
"""Returns a joined url.
|
||||
"""
|
||||
joined = urljoin('http://{0}:{1}'.format(host, port), endpoint)
|
||||
scheme, netloc, path, query_string, fragment = urlsplit(joined)
|
||||
query_params = parse_qs(query_string)
|
||||
|
||||
query_params['format'] = ['json']
|
||||
new_query_string = urlencode(query_params, doseq=True)
|
||||
|
||||
return urlunsplit((scheme, netloc, path, new_query_string, fragment))
|
||||
|
||||
|
||||
def password_data(username, password):
|
||||
"""Returns a dict with username and its encoded password.
|
||||
"""
|
||||
return {
|
||||
'username': username,
|
||||
'password': hashlib.sha1(password).hexdigest(),
|
||||
'passwordMd5': hashlib.md5(password).hexdigest()
|
||||
}
|
||||
|
||||
|
||||
def create_headers(user_id, token=None):
|
||||
"""Return header dict that is needed to talk to the Emby API.
|
||||
"""
|
||||
headers = {
|
||||
'Authorization': 'MediaBrowser',
|
||||
'UserId': user_id,
|
||||
'Client': 'other',
|
||||
'Device': 'empy',
|
||||
'DeviceId': 'beets',
|
||||
'Version': '0.0.0'
|
||||
}
|
||||
|
||||
if token:
|
||||
headers['X-MediaBrowser-Token'] = token
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def get_token(host, port, headers, auth_data):
|
||||
"""Return token for a user.
|
||||
"""
|
||||
url = api_url(host, port, '/Users/AuthenticateByName')
|
||||
r = requests.post(url, headers=headers, data=auth_data)
|
||||
|
||||
return r.json().get('AccessToken')
|
||||
|
||||
|
||||
def get_user(host, port, username):
|
||||
"""Return user dict from server or None if there is no user.
|
||||
"""
|
||||
url = api_url(host, port, '/Users/Public')
|
||||
r = requests.get(url)
|
||||
user = [i for i in r.json() if i['Name'] == username]
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class EmbyUpdate(BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(EmbyUpdate, self).__init__()
|
||||
|
||||
# Adding defaults.
|
||||
config['emby'].add({
|
||||
u'host': u'localhost',
|
||||
u'port': 8096
|
||||
})
|
||||
|
||||
self.register_listener('database_change', self.listen_for_db_change)
|
||||
|
||||
def listen_for_db_change(self, lib, model):
|
||||
"""Listens for beets db change and register the update for the end.
|
||||
"""
|
||||
self.register_listener('cli_exit', self.update)
|
||||
|
||||
def update(self, lib):
|
||||
"""When the client exists try to send refresh request to Emby.
|
||||
"""
|
||||
self._log.info(u'Updating Emby library...')
|
||||
|
||||
host = config['emby']['host'].get()
|
||||
port = config['emby']['port'].get()
|
||||
username = config['emby']['username'].get()
|
||||
password = config['emby']['password'].get()
|
||||
|
||||
# Get user information from the Emby API.
|
||||
user = get_user(host, port, username)
|
||||
if not user:
|
||||
self._log.warning(u'User {0} could not be found.'.format(username))
|
||||
return
|
||||
|
||||
# Create Authentication data and headers.
|
||||
auth_data = password_data(username, password)
|
||||
headers = create_headers(user[0]['Id'])
|
||||
|
||||
# Get authentication token.
|
||||
token = get_token(host, port, headers, auth_data)
|
||||
if not token:
|
||||
self._log.warning(
|
||||
u'Couldnt not get token for user {0}'.format(username))
|
||||
return
|
||||
|
||||
# Recreate headers with a token.
|
||||
headers = create_headers(user[0]['Id'], token=token)
|
||||
|
||||
# Trigger the Update.
|
||||
url = api_url(host, port, '/Library/Refresh')
|
||||
r = requests.post(url, headers=headers)
|
||||
if r.status_code != 204:
|
||||
self._log.warning(u'Update could not be triggered')
|
||||
else:
|
||||
self._log.info(u'Update triggered.')
|
||||
|
|
@ -18,7 +18,8 @@ New:
|
|||
various-artists albums. The setting defaults to "Various Artists," the
|
||||
MusicBrainz standard. In order to match MusicBrainz, the
|
||||
:doc:`/plugins/discogs` also adopts the same setting.
|
||||
|
||||
* :doc:`/plugins/embyupdate`: A plugin to trigger a library refresh on a
|
||||
`Emby Server`_ if database changed.
|
||||
|
||||
For developers:
|
||||
|
||||
|
|
@ -56,6 +57,8 @@ Fixes:
|
|||
* :doc:`/plugins/smartplaylist`: More gracefully handle malformed queries and
|
||||
missing configuration.
|
||||
|
||||
.. _Emby Server: http://emby.media
|
||||
|
||||
|
||||
1.3.15 (October 17, 2015)
|
||||
-------------------------
|
||||
|
|
|
|||
33
docs/plugins/embyupdate.rst
Normal file
33
docs/plugins/embyupdate.rst
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
EmbyUpdate Plugin
|
||||
=================
|
||||
|
||||
``embyupdate`` is a plugin that lets you automatically update `Emby`_'s library whenever you change your beets library.
|
||||
|
||||
To use ``embyupdate`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then, you'll probably want to configure the specifics of your Emby server. You can do that using an ``emby:`` section in your ``config.yaml``, which looks like this::
|
||||
|
||||
emby:
|
||||
host: localhost
|
||||
port: 8096
|
||||
username: user
|
||||
password: password
|
||||
|
||||
To use the ``embyupdate`` plugin you need to install the `requests`_ library with::
|
||||
|
||||
pip install requests
|
||||
|
||||
With that all in place, you'll see beets send the "update" command to your Emby server every time you change your beets library.
|
||||
|
||||
.. _Emby: http://emby.media/
|
||||
.. _requests: http://docs.python-requests.org/en/latest/
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
The available options under the ``emby:`` section are:
|
||||
|
||||
- **host**: The Emby server name.
|
||||
Default: ``localhost``
|
||||
- **port**: The Emby server port.
|
||||
Defailt: 8096
|
||||
- **username**: A username of a Emby user that is allowed to refresh the library.
|
||||
- **password**: That users password.
|
||||
|
|
@ -41,6 +41,7 @@ Each plugin has its own set of options that can be defined in a section bearing
|
|||
duplicates
|
||||
echonest
|
||||
embedart
|
||||
embyupdate
|
||||
fetchart
|
||||
fromfilename
|
||||
ftintitle
|
||||
|
|
@ -131,6 +132,7 @@ Path Formats
|
|||
Interoperability
|
||||
----------------
|
||||
|
||||
* :doc:`embyupdate`: Automatically notifies `Emby`_ whenever the beets library changes.
|
||||
* :doc:`importfeeds`: Keep track of imported files via ``.m3u`` playlist file(s) or symlinks.
|
||||
* :doc:`ipfs`: Import libraries from friends and get albums from them via ipfs.
|
||||
* :doc:`mpdupdate`: Automatically notifies `MPD`_ whenever the beets library
|
||||
|
|
@ -143,6 +145,7 @@ Interoperability
|
|||
* :doc:`badfiles`: Check audio file integrity.
|
||||
|
||||
|
||||
.. _Emby: http://emby.media
|
||||
.. _Plex: http://plex.tv
|
||||
|
||||
Miscellaneous
|
||||
|
|
|
|||
212
test/test_embyupdate.py
Normal file
212
test/test_embyupdate.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
from __future__ import (division, absolute_import, print_function,
|
||||
unicode_literals)
|
||||
|
||||
from test._common import unittest
|
||||
from test.helper import TestHelper
|
||||
from beetsplug import embyupdate
|
||||
import responses
|
||||
|
||||
|
||||
class EmbyUpdateTest(unittest.TestCase, TestHelper):
|
||||
def setUp(self):
|
||||
self.setup_beets()
|
||||
self.load_plugins('embyupdate')
|
||||
|
||||
self.config['emby'] = {
|
||||
u'host': u'localhost',
|
||||
u'port': 8096,
|
||||
u'username': u'username',
|
||||
u'password': u'password'
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_beets()
|
||||
self.unload_plugins()
|
||||
|
||||
def test_api_url(self):
|
||||
self.assertEqual(
|
||||
embyupdate.api_url(self.config['emby']['host'].get(),
|
||||
self.config['emby']['port'].get(),
|
||||
'/Library/Refresh'),
|
||||
'http://localhost:8096/Library/Refresh?format=json'
|
||||
)
|
||||
|
||||
def test_password_data(self):
|
||||
self.assertEqual(
|
||||
embyupdate.password_data(self.config['emby']['username'].get(),
|
||||
self.config['emby']['password'].get()),
|
||||
{
|
||||
'username': 'username',
|
||||
'password': '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8',
|
||||
'passwordMd5': '5f4dcc3b5aa765d61d8327deb882cf99'
|
||||
}
|
||||
)
|
||||
|
||||
def test_create_header_no_token(self):
|
||||
self.assertEqual(
|
||||
embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721'),
|
||||
{
|
||||
'Authorization': 'MediaBrowser',
|
||||
'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721',
|
||||
'Client': 'other',
|
||||
'Device': 'empy',
|
||||
'DeviceId': 'beets',
|
||||
'Version': '0.0.0'
|
||||
}
|
||||
)
|
||||
|
||||
def test_create_header_with_token(self):
|
||||
self.assertEqual(
|
||||
embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721',
|
||||
token='abc123'),
|
||||
{
|
||||
'Authorization': 'MediaBrowser',
|
||||
'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721',
|
||||
'Client': 'other',
|
||||
'Device': 'empy',
|
||||
'DeviceId': 'beets',
|
||||
'Version': '0.0.0',
|
||||
'X-MediaBrowser-Token': 'abc123'
|
||||
}
|
||||
)
|
||||
|
||||
@responses.activate
|
||||
def test_get_token(self):
|
||||
body = ('{"User":{"Name":"username", '
|
||||
'"ServerId":"1efa5077976bfa92bc71652404f646ec",'
|
||||
'"Id":"2ec276a2642e54a19b612b9418a8bd3b","HasPassword":true,'
|
||||
'"HasConfiguredPassword":true,'
|
||||
'"HasConfiguredEasyPassword":false,'
|
||||
'"LastLoginDate":"2015-11-09T08:35:03.6357440Z",'
|
||||
'"LastActivityDate":"2015-11-09T08:35:03.6665060Z",'
|
||||
'"Configuration":{"AudioLanguagePreference":"",'
|
||||
'"PlayDefaultAudioTrack":true,"SubtitleLanguagePreference":"",'
|
||||
'"DisplayMissingEpisodes":false,'
|
||||
'"DisplayUnairedEpisodes":false,'
|
||||
'"GroupMoviesIntoBoxSets":false,'
|
||||
'"DisplayChannelsWithinViews":[],'
|
||||
'"ExcludeFoldersFromGrouping":[],"GroupedFolders":[],'
|
||||
'"SubtitleMode":"Default","DisplayCollectionsView":true,'
|
||||
'"DisplayFoldersView":false,"EnableLocalPassword":false,'
|
||||
'"OrderedViews":[],"IncludeTrailersInSuggestions":true,'
|
||||
'"EnableCinemaMode":true,"LatestItemsExcludes":[],'
|
||||
'"PlainFolderViews":[],"HidePlayedInLatest":true,'
|
||||
'"DisplayChannelsInline":false},'
|
||||
'"Policy":{"IsAdministrator":true,"IsHidden":false,'
|
||||
'"IsDisabled":false,"BlockedTags":[],'
|
||||
'"EnableUserPreferenceAccess":true,"AccessSchedules":[],'
|
||||
'"BlockUnratedItems":[],'
|
||||
'"EnableRemoteControlOfOtherUsers":false,'
|
||||
'"EnableSharedDeviceControl":true,'
|
||||
'"EnableLiveTvManagement":true,"EnableLiveTvAccess":true,'
|
||||
'"EnableMediaPlayback":true,'
|
||||
'"EnableAudioPlaybackTranscoding":true,'
|
||||
'"EnableVideoPlaybackTranscoding":true,'
|
||||
'"EnableContentDeletion":false,'
|
||||
'"EnableContentDownloading":true,"EnableSync":true,'
|
||||
'"EnableSyncTranscoding":true,"EnabledDevices":[],'
|
||||
'"EnableAllDevices":true,"EnabledChannels":[],'
|
||||
'"EnableAllChannels":true,"EnabledFolders":[],'
|
||||
'"EnableAllFolders":true,"InvalidLoginAttemptCount":0,'
|
||||
'"EnablePublicSharing":true}},'
|
||||
'"SessionInfo":{"SupportedCommands":[],'
|
||||
'"QueueableMediaTypes":[],"PlayableMediaTypes":[],'
|
||||
'"Id":"89f3b33f8b3a56af22088733ad1d76b3",'
|
||||
'"UserId":"2ec276a2642e54a19b612b9418a8bd3b",'
|
||||
'"UserName":"username","AdditionalUsers":[],'
|
||||
'"ApplicationVersion":"Unknown version",'
|
||||
'"Client":"Unknown app",'
|
||||
'"LastActivityDate":"2015-11-09T08:35:03.6665060Z",'
|
||||
'"DeviceName":"Unknown device","DeviceId":"Unknown device id",'
|
||||
'"SupportsRemoteControl":false,"PlayState":{"CanSeek":false,'
|
||||
'"IsPaused":false,"IsMuted":false,"RepeatMode":"RepeatNone"}},'
|
||||
'"AccessToken":"4b19180cf02748f7b95c7e8e76562fc8",'
|
||||
'"ServerId":"1efa5077976bfa92bc71652404f646ec"}')
|
||||
|
||||
responses.add(responses.POST,
|
||||
('http://localhost:8096'
|
||||
'/Users/AuthenticateByName'),
|
||||
body=body,
|
||||
status=200,
|
||||
content_type='application/json')
|
||||
|
||||
headers = {
|
||||
'Authorization': 'MediaBrowser',
|
||||
'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721',
|
||||
'Client': 'other',
|
||||
'Device': 'empy',
|
||||
'DeviceId': 'beets',
|
||||
'Version': '0.0.0'
|
||||
}
|
||||
|
||||
auth_data = {
|
||||
'username': 'username',
|
||||
'password': '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8',
|
||||
'passwordMd5': '5f4dcc3b5aa765d61d8327deb882cf99'
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
embyupdate.get_token('localhost', 8096, headers, auth_data),
|
||||
'4b19180cf02748f7b95c7e8e76562fc8')
|
||||
|
||||
@responses.activate
|
||||
def test_get_user(self):
|
||||
body = ('[{"Name":"username",'
|
||||
'"ServerId":"1efa5077976bfa92bc71652404f646ec",'
|
||||
'"Id":"2ec276a2642e54a19b612b9418a8bd3b","HasPassword":true,'
|
||||
'"HasConfiguredPassword":true,'
|
||||
'"HasConfiguredEasyPassword":false,'
|
||||
'"LastLoginDate":"2015-11-09T08:35:03.6357440Z",'
|
||||
'"LastActivityDate":"2015-11-09T08:42:39.3693220Z",'
|
||||
'"Configuration":{"AudioLanguagePreference":"",'
|
||||
'"PlayDefaultAudioTrack":true,"SubtitleLanguagePreference":"",'
|
||||
'"DisplayMissingEpisodes":false,'
|
||||
'"DisplayUnairedEpisodes":false,'
|
||||
'"GroupMoviesIntoBoxSets":false,'
|
||||
'"DisplayChannelsWithinViews":[],'
|
||||
'"ExcludeFoldersFromGrouping":[],"GroupedFolders":[],'
|
||||
'"SubtitleMode":"Default","DisplayCollectionsView":true,'
|
||||
'"DisplayFoldersView":false,"EnableLocalPassword":false,'
|
||||
'"OrderedViews":[],"IncludeTrailersInSuggestions":true,'
|
||||
'"EnableCinemaMode":true,"LatestItemsExcludes":[],'
|
||||
'"PlainFolderViews":[],"HidePlayedInLatest":true,'
|
||||
'"DisplayChannelsInline":false},'
|
||||
'"Policy":{"IsAdministrator":true,"IsHidden":false,'
|
||||
'"IsDisabled":false,"BlockedTags":[],'
|
||||
'"EnableUserPreferenceAccess":true,"AccessSchedules":[],'
|
||||
'"BlockUnratedItems":[],'
|
||||
'"EnableRemoteControlOfOtherUsers":false,'
|
||||
'"EnableSharedDeviceControl":true,'
|
||||
'"EnableLiveTvManagement":true,"EnableLiveTvAccess":true,'
|
||||
'"EnableMediaPlayback":true,'
|
||||
'"EnableAudioPlaybackTranscoding":true,'
|
||||
'"EnableVideoPlaybackTranscoding":true,'
|
||||
'"EnableContentDeletion":false,'
|
||||
'"EnableContentDownloading":true,'
|
||||
'"EnableSync":true,"EnableSyncTranscoding":true,'
|
||||
'"EnabledDevices":[],"EnableAllDevices":true,'
|
||||
'"EnabledChannels":[],"EnableAllChannels":true,'
|
||||
'"EnabledFolders":[],"EnableAllFolders":true,'
|
||||
'"InvalidLoginAttemptCount":0,"EnablePublicSharing":true}}]')
|
||||
|
||||
responses.add(responses.GET,
|
||||
'http://localhost:8096/Users/Public',
|
||||
body=body,
|
||||
status=200,
|
||||
content_type='application/json')
|
||||
|
||||
response = embyupdate.get_user('localhost', 8096, 'username')
|
||||
|
||||
self.assertEqual(response[0]['Id'],
|
||||
'2ec276a2642e54a19b612b9418a8bd3b')
|
||||
|
||||
self.assertEqual(response[0]['Name'],
|
||||
'username')
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
||||
if __name__ == b'__main__':
|
||||
unittest.main(defaultTest='suite')
|
||||
Loading…
Reference in a new issue