mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-13 08:42:59 -07:00
Update Facebook for provider link only
* Custom poster/metadata removed in Graph API v2.9
This commit is contained in:
parent
42a7ae36c2
commit
c27c1379d0
3 changed files with 175 additions and 139 deletions
|
@ -1,12 +1,13 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2010 Facebook
|
||||
# Copyright 2015 Mobolic
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
|
@ -19,8 +20,7 @@
|
|||
This client library is designed to support the Graph API and the
|
||||
official Facebook JavaScript SDK, which is the canonical way to
|
||||
implement Facebook authentication. Read more about the Graph API at
|
||||
http://developers.facebook.com/docs/api. You can download the Facebook
|
||||
JavaScript SDK at http://github.com/facebook/connect-js/.
|
||||
https://developers.facebook.com/docs/graph-api.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -33,9 +33,9 @@ import json
|
|||
import re
|
||||
|
||||
try:
|
||||
from urllib.parse import parse_qs, urlencode
|
||||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
except ImportError:
|
||||
from urlparse import parse_qs
|
||||
from urlparse import parse_qs, urlparse
|
||||
from urllib import urlencode
|
||||
|
||||
from . import version
|
||||
|
@ -43,15 +43,16 @@ from . import version
|
|||
|
||||
__version__ = version.__version__
|
||||
|
||||
|
||||
VALID_API_VERSIONS = ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5"]
|
||||
FACEBOOK_GRAPH_URL = "https://graph.facebook.com/"
|
||||
FACEBOOK_OAUTH_DIALOG_URL = "https://www.facebook.com/dialog/oauth?"
|
||||
VALID_API_VERSIONS = ["2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9"]
|
||||
VALID_SEARCH_TYPES = ["page", "event", "group", "place", "placetopic", "user"]
|
||||
|
||||
|
||||
class GraphAPI(object):
|
||||
"""A client for the Facebook Graph API.
|
||||
|
||||
See http://developers.facebook.com/docs/api for complete
|
||||
documentation for the API.
|
||||
https://developers.facebook.com/docs/graph-api
|
||||
|
||||
The Graph API is made up of the objects in Facebook (e.g., people,
|
||||
pages, events, photos) and the connections between them (e.g.,
|
||||
|
@ -65,11 +66,11 @@ class GraphAPI(object):
|
|||
friends = graph.get_connections(user["id"], "friends")
|
||||
|
||||
You can see a list of all of the objects and connections supported
|
||||
by the API at http://developers.facebook.com/docs/reference/api/.
|
||||
by the API at https://developers.facebook.com/docs/graph-api/reference/.
|
||||
|
||||
You can obtain an access token via OAuth or by using the Facebook
|
||||
JavaScript SDK. See
|
||||
http://developers.facebook.com/docs/authentication/ for details.
|
||||
https://developers.facebook.com/docs/facebook-login for details.
|
||||
|
||||
If you are using the JavaScript SDK, you can use the
|
||||
get_user_from_cookie() method below to get the OAuth access token
|
||||
|
@ -78,13 +79,14 @@ class GraphAPI(object):
|
|||
"""
|
||||
|
||||
def __init__(self, access_token=None, timeout=None, version=None,
|
||||
proxies=None):
|
||||
proxies=None, session=None):
|
||||
# The default version is only used if the version kwarg does not exist.
|
||||
default_version = "2.0"
|
||||
default_version = VALID_API_VERSIONS[0]
|
||||
|
||||
self.access_token = access_token
|
||||
self.timeout = timeout
|
||||
self.proxies = proxies
|
||||
self.session = session or requests.Session()
|
||||
|
||||
if version:
|
||||
version_regex = re.compile("^\d\.\d$")
|
||||
|
@ -101,9 +103,16 @@ class GraphAPI(object):
|
|||
else:
|
||||
self.version = "v" + default_version
|
||||
|
||||
def get_permissions(self, user_id):
|
||||
"""Fetches the permissions object from the graph."""
|
||||
response = self.request(
|
||||
"{0}/{1}/permissions".format(self.version, user_id), {}
|
||||
)["data"]
|
||||
return {x["permission"] for x in response if x["status"] == "granted"}
|
||||
|
||||
def get_object(self, id, **args):
|
||||
"""Fetches the given object from the graph."""
|
||||
return self.request(self.version + "/" + id, args)
|
||||
return self.request("{0}/{1}".format(self.version, id), args)
|
||||
|
||||
def get_objects(self, ids, **args):
|
||||
"""Fetches all of the given object from the graph.
|
||||
|
@ -114,10 +123,39 @@ class GraphAPI(object):
|
|||
args["ids"] = ",".join(ids)
|
||||
return self.request(self.version + "/", args)
|
||||
|
||||
def search(self, type, **args):
|
||||
"""Fetches all objects of a given type from the graph.
|
||||
|
||||
Returns all objects of a given type from the graph as a dict.
|
||||
"""
|
||||
|
||||
if type not in VALID_SEARCH_TYPES:
|
||||
raise GraphAPIError('Valid types are: %s'
|
||||
% ', '.join(VALID_SEARCH_TYPES))
|
||||
|
||||
args["type"] = type
|
||||
return self.request(self.version + "/search/", args)
|
||||
|
||||
def get_connections(self, id, connection_name, **args):
|
||||
"""Fetches the connections for given object."""
|
||||
return self.request(
|
||||
"%s/%s/%s" % (self.version, id, connection_name), args)
|
||||
"{0}/{1}/{2}".format(self.version, id, connection_name), args)
|
||||
|
||||
def get_all_connections(self, id, connection_name, **args):
|
||||
"""Get all pages from a get_connections call
|
||||
|
||||
This will iterate over all pages returned by a get_connections call
|
||||
and yield the individual items.
|
||||
"""
|
||||
while True:
|
||||
page = self.get_connections(id, connection_name, **args)
|
||||
for post in page['data']:
|
||||
yield post
|
||||
next = page.get('paging', {}).get('next')
|
||||
if not next:
|
||||
return
|
||||
args = parse_qs(urlparse(next).query)
|
||||
del args['access_token']
|
||||
|
||||
def put_object(self, parent_object, connection_name, **data):
|
||||
"""Writes the given object to the graph, connected to the given parent.
|
||||
|
@ -133,41 +171,17 @@ class GraphAPI(object):
|
|||
post = feed["data"][0]
|
||||
graph.put_object(post["id"], "comments", message="First!")
|
||||
|
||||
See http://developers.facebook.com/docs/api#publishing for all
|
||||
of the supported writeable objects.
|
||||
|
||||
Certain write operations require extended permissions. For
|
||||
example, publishing to a user's feed requires the
|
||||
"publish_actions" permission. See
|
||||
http://developers.facebook.com/docs/publishing/ for details
|
||||
about publishing permissions.
|
||||
Certain operations require extended permissions. See
|
||||
https://developers.facebook.com/docs/facebook-login/permissions
|
||||
for details about permissions.
|
||||
|
||||
"""
|
||||
assert self.access_token, "Write operations require an access token"
|
||||
return self.request(
|
||||
self.version + "/" + parent_object + "/" + connection_name,
|
||||
"{0}/{1}/{2}".format(self.version, parent_object, connection_name),
|
||||
post_args=data,
|
||||
method="POST")
|
||||
|
||||
def put_wall_post(self, message, attachment={}, profile_id="me"):
|
||||
"""Writes a wall post to the given profile's wall.
|
||||
|
||||
We default to writing to the authenticated user's wall if no
|
||||
profile_id is specified.
|
||||
|
||||
attachment adds a structured attachment to the status message
|
||||
being posted to the Wall. It should be a dictionary of the form:
|
||||
|
||||
{"name": "Link name"
|
||||
"link": "http://www.example.com/",
|
||||
"caption": "{*actor*} posted a new review",
|
||||
"description": "This is a longer description of the attachment",
|
||||
"picture": "http://www.example.com/thumbnail.jpg"}
|
||||
|
||||
"""
|
||||
return self.put_object(profile_id, "feed", message=message,
|
||||
**attachment)
|
||||
|
||||
def put_comment(self, object_id, message):
|
||||
"""Writes the given comment on the given post."""
|
||||
return self.put_object(object_id, "comments", message=message)
|
||||
|
@ -178,11 +192,11 @@ class GraphAPI(object):
|
|||
|
||||
def delete_object(self, id):
|
||||
"""Deletes the object with the given ID from the graph."""
|
||||
self.request(self.version + "/" + id, method="DELETE")
|
||||
self.request("{0}/{1}".format(self.version, id), method="DELETE")
|
||||
|
||||
def delete_request(self, user_id, request_id):
|
||||
"""Deletes the Request with the given ID for the given user."""
|
||||
self.request("%s_%s" % (request_id, user_id), method="DELETE")
|
||||
self.request("{0}_{1}".format(request_id, user_id), method="DELETE")
|
||||
|
||||
def put_photo(self, image, album_path="me/photos", **kwargs):
|
||||
"""
|
||||
|
@ -193,7 +207,7 @@ class GraphAPI(object):
|
|||
|
||||
"""
|
||||
return self.request(
|
||||
self.version + "/" + album_path,
|
||||
"{0}/{1}".format(self.version, album_path),
|
||||
post_args=kwargs,
|
||||
files={"source": image},
|
||||
method="POST")
|
||||
|
@ -202,9 +216,9 @@ class GraphAPI(object):
|
|||
"""Fetches the current version number of the Graph API being used."""
|
||||
args = {"access_token": self.access_token}
|
||||
try:
|
||||
response = requests.request("GET",
|
||||
"https://graph.facebook.com/" +
|
||||
self.version + "/me",
|
||||
response = self.session.request(
|
||||
"GET",
|
||||
FACEBOOK_GRAPH_URL + self.version + "/me",
|
||||
params=args,
|
||||
timeout=self.timeout,
|
||||
proxies=self.proxies)
|
||||
|
@ -228,21 +242,25 @@ class GraphAPI(object):
|
|||
arguments.
|
||||
|
||||
"""
|
||||
args = args or {}
|
||||
|
||||
if args is None:
|
||||
args = dict()
|
||||
if post_args is not None:
|
||||
method = "POST"
|
||||
|
||||
# Add `access_token` to post_args or args if it has not already been
|
||||
# included.
|
||||
if self.access_token:
|
||||
if post_args is not None:
|
||||
# If post_args exists, we assume that args either does not exists
|
||||
# or it does not need `access_token`.
|
||||
if post_args and "access_token" not in post_args:
|
||||
post_args["access_token"] = self.access_token
|
||||
else:
|
||||
elif "access_token" not in args:
|
||||
args["access_token"] = self.access_token
|
||||
|
||||
try:
|
||||
response = requests.request(method or "GET",
|
||||
"https://graph.facebook.com/" +
|
||||
path,
|
||||
response = self.session.request(
|
||||
method or "GET",
|
||||
FACEBOOK_GRAPH_URL + path,
|
||||
timeout=self.timeout,
|
||||
params=args,
|
||||
data=post_args,
|
||||
|
@ -275,21 +293,23 @@ class GraphAPI(object):
|
|||
raise GraphAPIError(result)
|
||||
return result
|
||||
|
||||
def fql(self, query):
|
||||
"""FQL query.
|
||||
|
||||
Example query: "SELECT affiliations FROM user WHERE uid = me()"
|
||||
|
||||
def get_app_access_token(self, app_id, app_secret, offline=False):
|
||||
"""
|
||||
return self.request(self.version + "/" + "fql", {"q": query})
|
||||
|
||||
def get_app_access_token(self, app_id, app_secret):
|
||||
"""Get the application's access token as a string."""
|
||||
Get the application's access token as a string.
|
||||
If offline=True, use the concatenated app ID and secret
|
||||
instead of making an API call.
|
||||
<https://developers.facebook.com/docs/facebook-login/
|
||||
access-tokens#apptokens>
|
||||
"""
|
||||
if offline:
|
||||
return "{0}|{1}".format(app_id, app_secret)
|
||||
else:
|
||||
args = {'grant_type': 'client_credentials',
|
||||
'client_id': app_id,
|
||||
'client_secret': app_secret}
|
||||
|
||||
return self.request("oauth/access_token", args=args)["access_token"]
|
||||
return self.request("{0}/oauth/access_token".format(self.version),
|
||||
args=args)["access_token"]
|
||||
|
||||
def get_access_token_from_code(
|
||||
self, code, redirect_uri, app_id, app_secret):
|
||||
|
@ -305,13 +325,14 @@ class GraphAPI(object):
|
|||
"client_id": app_id,
|
||||
"client_secret": app_secret}
|
||||
|
||||
return self.request("oauth/access_token", args)
|
||||
return self.request(
|
||||
"{0}/oauth/access_token".format(self.version), args)
|
||||
|
||||
def extend_access_token(self, app_id, app_secret):
|
||||
"""
|
||||
Extends the expiration time of a valid OAuth access token. See
|
||||
<https://developers.facebook.com/roadmap/offline-access-removal/
|
||||
#extend_token>
|
||||
<https://developers.facebook.com/docs/facebook-login/access-tokens/
|
||||
expiration-and-extension>
|
||||
|
||||
"""
|
||||
args = {
|
||||
|
@ -321,13 +342,14 @@ class GraphAPI(object):
|
|||
"fb_exchange_token": self.access_token,
|
||||
}
|
||||
|
||||
return self.request("oauth/access_token", args=args)
|
||||
return self.request("{0}/oauth/access_token".format(self.version),
|
||||
args=args)
|
||||
|
||||
def debug_access_token(self, token, app_id, app_secret):
|
||||
"""
|
||||
Gets information about a user access token issued by an app. See
|
||||
<https://developers.facebook.com/docs/facebook-login/access-tokens
|
||||
#debug>
|
||||
<https://developers.facebook.com/docs/facebook-login/
|
||||
access-tokens/debugging-and-error-handling>
|
||||
|
||||
We can generate the app access token by concatenating the app
|
||||
id and secret: <https://developers.facebook.com/docs/
|
||||
|
@ -336,9 +358,9 @@ class GraphAPI(object):
|
|||
"""
|
||||
args = {
|
||||
"input_token": token,
|
||||
"access_token": "%s|%s" % (app_id, app_secret)
|
||||
"access_token": "{0}|{1}".format(app_id, app_secret)
|
||||
}
|
||||
return self.request("/debug_token", args=args)
|
||||
return self.request(self.version + "/" + "debug_token", args=args)
|
||||
|
||||
|
||||
class GraphAPIError(Exception):
|
||||
|
@ -382,10 +404,8 @@ def get_user_from_cookie(cookies, app_id, app_secret):
|
|||
requests to the Graph API. If the user is not logged in, we
|
||||
return None.
|
||||
|
||||
Download the official Facebook JavaScript SDK at
|
||||
http://github.com/facebook/connect-js/. Read more about Facebook
|
||||
authentication at
|
||||
http://developers.facebook.com/docs/authentication/.
|
||||
Read more about Facebook authentication at
|
||||
https://developers.facebook.com/docs/facebook-login.
|
||||
|
||||
"""
|
||||
cookie = cookies.get("fbsr_" + app_id, "")
|
||||
|
@ -435,7 +455,7 @@ def parse_signed_request(signed_request, app_secret):
|
|||
return False
|
||||
|
||||
# HMAC can only handle ascii (byte) strings
|
||||
# http://bugs.python.org/issue5285
|
||||
# https://bugs.python.org/issue5285
|
||||
app_secret = app_secret.encode('ascii')
|
||||
payload = payload.encode('ascii')
|
||||
|
||||
|
@ -449,7 +469,7 @@ def parse_signed_request(signed_request, app_secret):
|
|||
|
||||
|
||||
def auth_url(app_id, canvas_url, perms=None, **kwargs):
|
||||
url = "https://www.facebook.com/dialog/oauth?"
|
||||
url = FACEBOOK_OAUTH_DIALOG_URL
|
||||
kvps = {'client_id': app_id, 'redirect_uri': canvas_url}
|
||||
if perms:
|
||||
kvps['scope'] = ",".join(perms)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2014 Martey Dodoo
|
||||
# Copyright 2015 Mobolic
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
|
@ -14,4 +14,4 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
__version__ = "1.0.0-alpha"
|
||||
__version__ = "3.0.0-alpha"
|
||||
|
|
|
@ -603,14 +603,21 @@ class PrettyMetadata(object):
|
|||
provider = 'Last.fm'
|
||||
return provider
|
||||
|
||||
def get_provider_link(self):
|
||||
def get_provider_link(self, provider=None):
|
||||
provider_link = ''
|
||||
if self.parameters['thetvdb_url']:
|
||||
if provider:
|
||||
if provider == 'plexweb':
|
||||
provider_link = self.get_plex_url()
|
||||
else:
|
||||
provider_link = self.parameters.get(provider + '_url', '')
|
||||
elif self.parameters['thetvdb_url']:
|
||||
provider_link = self.parameters['thetvdb_url']
|
||||
elif self.parameters['themoviedb_url']:
|
||||
provider_link = self.parameters['themoviedb_url']
|
||||
elif self.parameters['imdb_url']:
|
||||
provider_link = self.parameters['imdb_url']
|
||||
elif self.self.parameters['tvmaze_url']:
|
||||
provider_link = self.parameters['tvmaze_url']
|
||||
elif self.parameters['lastfm_url']:
|
||||
provider_link = self.parameters['lastfm_url']
|
||||
return provider_link
|
||||
|
@ -1307,8 +1314,9 @@ class FACEBOOK(Notifier):
|
|||
'group_id': '',
|
||||
'incl_subject': 1,
|
||||
'incl_card': 0,
|
||||
'incl_description': 1,
|
||||
'incl_pmslink': 0
|
||||
'movie_provider': '',
|
||||
'tv_provider': '',
|
||||
'music_provider': ''
|
||||
}
|
||||
|
||||
def _get_authorization(self, app_id='', app_secret='', redirect_uri=''):
|
||||
|
@ -1356,12 +1364,12 @@ class FACEBOOK(Notifier):
|
|||
|
||||
return plexpy.CONFIG.FACEBOOK_TOKEN
|
||||
|
||||
def _post_facebook(self, message=None, attachment=None):
|
||||
def _post_facebook(self, **data):
|
||||
if self.config['group_id']:
|
||||
api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.5')
|
||||
|
||||
try:
|
||||
api.put_wall_post(profile_id=self.config['group_id'], message=message, attachment=attachment)
|
||||
api.put_object(parent_object=self.config['group_id'], connection_name='feed', **data)
|
||||
logger.info(u"PlexPy Notifiers :: {name} notification sent.".format(name=self.NAME))
|
||||
return True
|
||||
except Exception as e:
|
||||
|
@ -1376,41 +1384,29 @@ class FACEBOOK(Notifier):
|
|||
if not subject or not body:
|
||||
return
|
||||
|
||||
attachment = {}
|
||||
if self.config['incl_subject']:
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
|
||||
else:
|
||||
text = body.encode("utf-8")
|
||||
|
||||
data = {'message': text}
|
||||
|
||||
if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'):
|
||||
# Grab formatted metadata
|
||||
pretty_metadata = PrettyMetadata(kwargs['parameters'])
|
||||
media_type = pretty_metadata.media_type
|
||||
poster_url = pretty_metadata.get_poster_url()
|
||||
plex_url = pretty_metadata.get_plex_url()
|
||||
provider_link = pretty_metadata.get_provider_link()
|
||||
caption = pretty_metadata.get_caption()
|
||||
title = pretty_metadata.get_title('\xc2\xb7'.decode('utf8'))
|
||||
description = pretty_metadata.get_description()
|
||||
|
||||
# Build Facebook post attachment
|
||||
if self.config['incl_pmslink']:
|
||||
attachment['link'] = plex_url
|
||||
attachment['caption'] = 'View on Plex Web'
|
||||
elif provider_link:
|
||||
attachment['link'] = provider_link
|
||||
attachment['caption'] = caption
|
||||
if pretty_metadata.media_type == 'movie':
|
||||
provider = self.config['movie_provider']
|
||||
elif pretty_metadata.media_type in ('show', 'season', 'episode'):
|
||||
provider = self.config['tv_provider']
|
||||
elif pretty_metadata.media_type in ('artist', 'album', 'track'):
|
||||
provider = self.config['music_provider']
|
||||
else:
|
||||
attachment['link'] = poster_url
|
||||
provider = None
|
||||
|
||||
attachment['picture'] = poster_url
|
||||
attachment['name'] = title
|
||||
data['link'] = pretty_metadata.get_provider_link(provider)
|
||||
|
||||
if self.config['incl_description'] or media_type in ('artist', 'album', 'track'):
|
||||
attachment['description'] = description
|
||||
else:
|
||||
attachment['description'] = ' '
|
||||
|
||||
if self.config['incl_subject']:
|
||||
return self._post_facebook(subject + '\r\n' + body, attachment=attachment)
|
||||
else:
|
||||
return self._post_facebook(body, attachment=attachment)
|
||||
return self._post_facebook(**data)
|
||||
|
||||
def return_config_options(self):
|
||||
config_option = [{'label': 'Instructions',
|
||||
|
@ -1476,18 +1472,38 @@ class FACEBOOK(Notifier):
|
|||
'description': 'Include an info card with a poster and metadata with the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
},
|
||||
{'label': 'Include Plot Summaries',
|
||||
'value': self.config['incl_description'],
|
||||
'name': 'facebook_incl_description',
|
||||
'description': 'Include a plot summary for movies and TV shows on the info card.',
|
||||
'input_type': 'checkbox'
|
||||
{'label': 'Movie Link Source',
|
||||
'value': self.config['movie_provider'],
|
||||
'name': 'facebook_movie_provider',
|
||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.',
|
||||
'input_type': 'select',
|
||||
'select_options': {'': '',
|
||||
'trakt': 'Trakt.tv',
|
||||
'plexweb': 'Plex Web'
|
||||
}
|
||||
},
|
||||
{'label': 'Include Link to Plex Web',
|
||||
'value': self.config['incl_pmslink'],
|
||||
'name': 'facebook_incl_pmslink',
|
||||
'description': 'Include a link to the media in Plex Web on the info card.<br>'
|
||||
'If disabled, the link will go to IMDB, TVDB, TMDb, or Last.fm instead, if available.',
|
||||
'input_type': 'checkbox'
|
||||
{'label': 'TV Show Link Source',
|
||||
'value': self.config['tv_provider'],
|
||||
'name': 'facebook_tv_provider',
|
||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.',
|
||||
'input_type': 'select',
|
||||
'select_options': {'': '',
|
||||
'thetvdb': 'TheTVDB',
|
||||
'tvmaze': 'TVmaze',
|
||||
'imdb': 'IMDB',
|
||||
'trakt': 'Trakt.tv',
|
||||
'plexweb': 'Plex Web'
|
||||
}
|
||||
},
|
||||
{'label': 'Music Link Source',
|
||||
'value': self.config['music_provider'],
|
||||
'name': 'facebook_music_provider',
|
||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
||||
'input_type': 'select',
|
||||
'select_options': {'': '',
|
||||
'lastfm': 'Last.fm',
|
||||
'plexweb': 'Plex Web'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue