165 lines
7.8 KiB
Python
165 lines
7.8 KiB
Python
import warnings
|
|
from xml.etree import ElementTree
|
|
import requests
|
|
from django.forms import ValidationError
|
|
from urllib.parse import urlsplit, urlunsplit, urlencode
|
|
from django.utils.translation import gettext_lazy as _
|
|
from common.logger import log
|
|
|
|
|
|
class MediaServerError(Exception):
|
|
'''
|
|
Raised when a back-end error occurs.
|
|
'''
|
|
pass
|
|
|
|
|
|
class MediaServer:
|
|
|
|
TIMEOUT = 0
|
|
HELP = ''
|
|
|
|
def __init__(self, mediaserver_instance):
|
|
self.object = mediaserver_instance
|
|
|
|
def validate(self):
|
|
raise NotImplementedError('MediaServer.validate() must be implemented')
|
|
|
|
def update(self):
|
|
raise NotImplementedError('MediaServer.update() must be implemented')
|
|
|
|
|
|
class PlexMediaServer(MediaServer):
|
|
|
|
TIMEOUT = 5
|
|
|
|
HELP = _('<p>To connect your TubeSync sevrer to your Plex Media Server you will '
|
|
'need to enter the details of your Plex server below.</p>'
|
|
'<p>The <strong>host</strong> can be either an IP address or valid hostname.</p>'
|
|
'<p>The <strong>port</strong> number must be between 1 and 65536.</p>'
|
|
'<p>The <strong>token</strong> is a Plex access token to your Plex server. You can find '
|
|
'out how to get a Plex access token <a href="https://support.plex.tv/'
|
|
'articles/204059436-finding-an-authentication-token-x-plex-token/" '
|
|
'target="_blank">here</a>.</p>'
|
|
'<p>The <strong>libraries</strong> is a comma-separated list of Plex '
|
|
'library or section IDs, you can find out how to get your library or '
|
|
'section IDs <a href="https://support.plex.tv/articles/201242707-plex-'
|
|
'media-scanner-via-command-line/#toc-1" target="_blank">here</a>.</p>')
|
|
|
|
def make_request(self, uri='/', params={}):
|
|
headers = {'User-Agent': 'TubeSync'}
|
|
token = self.object.loaded_options['token']
|
|
params['X-Plex-Token'] = token
|
|
base_parts = urlsplit(self.object.url)
|
|
qs = urlencode(params)
|
|
url = urlunsplit((base_parts.scheme, base_parts.netloc, uri, qs, ''))
|
|
if self.object.verify_https:
|
|
log.debug(f'[plex media server] Making HTTP GET request to: {url}')
|
|
return requests.get(url, headers=headers, verify=True,
|
|
timeout=self.TIMEOUT)
|
|
else:
|
|
# If not validating SSL, given this is likely going to be for an internal
|
|
# or private network, that Plex issues certs *.hash.plex.direct and that
|
|
# the warning won't ever been sensibly seen in the HTTPS logs, hide it
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
return requests.get(url, headers=headers, verify=False,
|
|
timeout=self.TIMEOUT)
|
|
|
|
def validate(self):
|
|
'''
|
|
A Plex server requires a host, port, access token and a comma-separated
|
|
list if library IDs.
|
|
'''
|
|
# Check all the required values are present
|
|
if not self.object.host:
|
|
raise ValidationError('Plex Media Server requires a "host"')
|
|
if not self.object.port:
|
|
raise ValidationError('Plex Media Server requires a "port"')
|
|
try:
|
|
port = int(self.object.port)
|
|
except (TypeError, ValueError):
|
|
raise ValidationError('Plex Media Server "port" must be an integer')
|
|
if port < 1 or port > 65535:
|
|
raise ValidationError('Plex Media Server "port" must be between 1 '
|
|
'and 65535')
|
|
options = self.object.loaded_options
|
|
if 'token' not in options:
|
|
raise ValidationError('Plex Media Server requires a "token"')
|
|
token = options['token'].strip()
|
|
if 'token' not in options:
|
|
raise ValidationError('Plex Media Server requires a "token"')
|
|
if 'libraries' not in options:
|
|
raise ValidationError('Plex Media Server requires a "libraries"')
|
|
libraries = options['libraries'].strip().split(',')
|
|
for position, library in enumerate(libraries):
|
|
library = library.strip()
|
|
try:
|
|
int(library)
|
|
except (TypeError, ValueError):
|
|
raise ValidationError(f'Plex Media Server library ID "{library}" at '
|
|
f'position {position+1} must be an integer')
|
|
# Test the details work by requesting a summary page from the Plex server
|
|
try:
|
|
response = self.make_request('/library/sections')
|
|
except Exception as e:
|
|
raise ValidationError(f'Failed to make a test connection to your Plex '
|
|
f'Media Server at "{self.object.host}:'
|
|
f'{self.object.port}", the error was "{e}". Check '
|
|
'your host and port are correct.') from e
|
|
if response.status_code != 200:
|
|
check_token = ''
|
|
if 400 <= response.status_code < 500:
|
|
check_token = (' A 4XX error could mean your access token is being '
|
|
'rejected. Check your token is correct.')
|
|
raise ValidationError(f'Your Plex Media Server returned an invalid HTTP '
|
|
f'status code, expected 200 but received '
|
|
f'{response.status_code}.' + check_token)
|
|
try:
|
|
parsed_response = ElementTree.fromstring(response.content)
|
|
except Exception as e:
|
|
raise ValidationError(f'Your Plex Media Server returned unexpected data, '
|
|
f'expected valid XML but parsing it as XML caused '
|
|
f'the error "{e}"')
|
|
# Seems we have a valid library sections page, get the library IDs
|
|
remote_libraries = {}
|
|
try:
|
|
for parent in parsed_response.getiterator('MediaContainer'):
|
|
for d in parent:
|
|
library_id = d.attrib['key']
|
|
library_name = d.attrib['title']
|
|
remote_libraries[library_id] = library_name
|
|
except Exception as e:
|
|
raise ValidationError(f'Your Plex Media Server returned unexpected data, '
|
|
f'the XML it returned could not be parsed and the '
|
|
f'error was "{e}"')
|
|
# Validate the library IDs
|
|
remote_libraries_desc = []
|
|
for remote_library_id, remote_library_name in remote_libraries.items():
|
|
remote_libraries_desc.append(f'"{remote_library_name}" with ID '
|
|
f'"{remote_library_id}"')
|
|
remote_libraries_str = ', '.join(remote_libraries_desc)
|
|
for library_id in libraries:
|
|
library_id = library_id.strip()
|
|
if library_id not in remote_libraries:
|
|
raise ValidationError(f'One or more of your specified library IDs do '
|
|
f'not exist on your Plex Media Server. Your '
|
|
f'valid libraries are: {remote_libraries_str}')
|
|
# All good!
|
|
return True
|
|
|
|
def update(self):
|
|
# For each section / library ID pop off a request to refresh it
|
|
libraries = self.object.loaded_options.get('libraries', '')
|
|
for library_id in libraries.split(','):
|
|
library_id = library_id.strip()
|
|
uri = f'/library/sections/{library_id}/refresh'
|
|
response = self.make_request(uri)
|
|
if response.status_code != 200:
|
|
raise MediaServerError(f'Failed to refresh library "{library_id}" on '
|
|
f'Plex server "{self.object.url}", expected a '
|
|
f'200 status code but got '
|
|
f'{response.status_code}. Check your media '
|
|
f'server details.')
|
|
return True
|