-
Notifications
You must be signed in to change notification settings - Fork 191
Sync support #282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sync support #282
Changes from 9 commits
5432876
edfe9d5
05bd225
3f9673c
1fa3dc4
6051a8a
5ec0a42
1f11bdf
cd2849e
afbf45c
34296c7
57f68ac
59af01d
5860e2c
9383ed0
7fb616e
2f534dd
10226e1
1297724
2be3f5c
ef95e01
e69df4d
4590adc
a46a4aa
f9c881b
a811772
9971435
5a147f8
98d93d3
24c974f
e82e7d7
ee867f5
462a16d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,14 +3,15 @@ | |
| import requests | ||
| import time | ||
| from requests.status_codes import _codes as codes | ||
| from plexapi import BASE_HEADERS, CONFIG, TIMEOUT | ||
| from plexapi import BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_IDENTIFIER, X_PLEX_ENABLE_FAST_CONNECT | ||
| from plexapi import log, logfilter, utils | ||
| from plexapi.base import PlexObject | ||
| from plexapi.exceptions import BadRequest, NotFound | ||
| from plexapi.client import PlexClient | ||
| from plexapi.compat import ElementTree | ||
| from plexapi.library import LibrarySection | ||
| from plexapi.server import PlexServer | ||
| from plexapi.sync import SyncList | ||
| from plexapi.utils import joinArgs | ||
|
|
||
|
|
||
|
|
@@ -378,6 +379,14 @@ def optOut(self, playback=None, library=None): | |
| url = 'https://plex.tv/api/v2/user/privacy' | ||
| return self.query(url, method=self._session.put, params=params) | ||
|
|
||
| def syncItems(self, clientId=None): | ||
| if clientId is None: | ||
| clientId = X_PLEX_IDENTIFIER | ||
|
|
||
| data = self.query(SyncList.key.format(clientId=clientId)) | ||
|
|
||
| return SyncList(self, data) | ||
|
|
||
|
|
||
| class MyPlexUser(PlexObject): | ||
| """ This object represents non-signed in users such as friends and linked | ||
|
|
@@ -698,7 +707,7 @@ def delete(self): | |
| self._server.query(key, self._server._session.delete) | ||
|
|
||
|
|
||
| def _connect(cls, url, token, timeout, results, i): | ||
| def _connect(cls, url, token, timeout, results, i, connected_event=None): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is connected_event used for, can you add a comment in the doc string here about what what is is? |
||
| """ Connects to the specified cls with url and token. Stores the connection | ||
| information to results[i] in a threadsafe way. | ||
| """ | ||
|
|
@@ -707,6 +716,8 @@ def _connect(cls, url, token, timeout, results, i): | |
| device = cls(baseurl=url, token=token, timeout=timeout) | ||
| runtime = int(time.time() - starttime) | ||
| results[i] = (url, token, device, runtime) | ||
| if X_PLEX_ENABLE_FAST_CONNECT and connected_event: | ||
| connected_event.set() | ||
| except Exception as err: | ||
| runtime = int(time.time() - starttime) | ||
| log.error('%s: %s', url, err) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,103 @@ | ||
| # -*- coding: utf-8 -*- | ||
| import requests | ||
| from plexapi import utils | ||
| import plexapi | ||
| from plexapi.exceptions import NotFound | ||
| from plexapi.base import PlexObject, Playable | ||
|
|
||
|
|
||
| class SyncItem(object): | ||
| """ Sync Item. This doesn't current work. """ | ||
| def __init__(self, device, data, servers=None): | ||
| self._device = device | ||
| self._servers = servers | ||
| self._loadData(data) | ||
| def init(replace_provides=False): | ||
| if replace_provides or not plexapi.X_PLEX_PROVIDES: | ||
| plexapi.X_PLEX_PROVIDES = 'sync-target' | ||
| else: | ||
| plexapi.X_PLEX_PROVIDES += ',sync-target' | ||
|
|
||
| plexapi.BASE_HEADERS['X-Plex-Sync-Version'] = '2' | ||
| plexapi.BASE_HEADERS['X-Plex-Provides'] = plexapi.X_PLEX_PROVIDES | ||
|
|
||
| # mimic iPhone SE | ||
| plexapi.X_PLEX_PLATFORM = 'iOS' | ||
| plexapi.X_PLEX_PLATFORM_VERSION = '11.4.1' | ||
| plexapi.X_PLEX_DEVICE = 'iPhone' | ||
|
|
||
| plexapi.BASE_HEADERS['X-Plex-Platform'] = plexapi.X_PLEX_PLATFORM | ||
| plexapi.BASE_HEADERS['X-Plex-Platform-Version'] = plexapi.X_PLEX_PLATFORM_VERSION | ||
| plexapi.BASE_HEADERS['X-Plex-Device'] = plexapi.X_PLEX_DEVICE | ||
| plexapi.BASE_HEADERS['X-Plex-Model'] = '8,4' | ||
| plexapi.BASE_HEADERS['X-Plex-Vendor'] = 'Apple' | ||
|
|
||
|
|
||
| class SyncItem(PlexObject): | ||
| TAG = 'SyncItem' | ||
|
|
||
| def __init__(self, server, data, initpath=None, clientIdentifier=None): | ||
| super().__init__(server, data, initpath) | ||
| self.clientIdentifier = clientIdentifier | ||
|
|
||
| def _loadData(self, data): | ||
| self._data = data | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should be a documentation header on head class in this file explaining all the attributes and why they exist. A good example format is the video.py file. https://github.com/pkkid/python-plexapi/blob/master/plexapi/video.py |
||
| self.id = utils.cast(int, data.attrib.get('id')) | ||
| self.version = utils.cast(int, data.attrib.get('version')) | ||
| self.id = plexapi.utils.cast(int, data.attrib.get('id')) | ||
| self.version = plexapi.utils.cast(int, data.attrib.get('version')) | ||
| self.rootTitle = data.attrib.get('rootTitle') | ||
| self.title = data.attrib.get('title') | ||
| self.metadataType = data.attrib.get('metadataType') | ||
| self.machineIdentifier = data.find('Server').get('machineIdentifier') | ||
| self.status = data.find('Status').attrib.copy() | ||
| self.status = Status(self._server, data.find('Status')) | ||
| self.MediaSettings = data.find('MediaSettings').attrib.copy() | ||
| self.policy = data.find('Policy').attrib.copy() | ||
| self.location = data.find('Location').attrib.copy() | ||
|
|
||
| def server(self): | ||
| server = list(filter(lambda x: x.machineIdentifier == self.machineIdentifier, self._servers)) | ||
| server = list(filter(lambda x: x.clientIdentifier == self.machineIdentifier, self._server.resources())) | ||
| if 0 == len(server): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. List comp please
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you do len(server) == 0 instead, |
||
| raise NotFound('Unable to find server with uuid %s' % self.machineIdentifier) | ||
| return server[0] | ||
|
|
||
| def getMedia(self): | ||
| def getMedia(self, timeout=None): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What up with the timeout? Why not just change the plex.Timeout?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not all operations require 60s+ timeout, and wrapping There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It shouldn't cause any problems. timeout is essentially just telling the requests module when to give up if not getting a response. It's really only useful to change to poor or slow network issues. Having different timeouts for different calls in to the API seems incorrect unless you have a really strong case for it. Perhaps a better option for your case is to update your personal config file which is not checked in. |
||
| server = self.server().connect() | ||
| key = '/sync/items/%s' % self.id | ||
| return server.fetchItems(key) | ||
| return server.fetchItems(key, timeout=timeout) | ||
|
|
||
| def markAsDone(self, sync_id): | ||
| server = self.server().connect() | ||
| url = '/sync/%s/%s/files/%s/downloaded' % ( | ||
| self._device.clientIdentifier, server.machineIdentifier, sync_id) | ||
| server.query(url, method=requests.put) | ||
| def markDownloaded(self, media: Playable): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This need to be support python 2. |
||
| url = '/sync/%s/item/%s/downloaded' % (self.clientIdentifier, media.ratingKey) | ||
| media._server.query(url, method=requests.put) | ||
|
|
||
|
|
||
| class SyncList(PlexObject): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also needs documentation. |
||
| key = 'https://plex.tv/devices/{clientId}/sync_items' | ||
| TAG = 'SyncList' | ||
|
|
||
| def _loadData(self, data): | ||
| self._data = data | ||
| self.clientId = data.attrib.get('clientIdentifier') | ||
| self.items = [] | ||
|
|
||
| for elem in data: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use .iter('SyncItem') here? |
||
| if elem.tag == 'SyncItems': | ||
| for sync_item in elem: | ||
| item = SyncItem(self._server, sync_item, clientIdentifier=self.clientId) | ||
| self.items.append(item) | ||
|
|
||
|
|
||
| class Status(PlexObject): | ||
| TAG = 'Status' | ||
|
|
||
| def _loadData(self, data): | ||
| self._data = data | ||
| self.failureCode = data.attrib.get('failureCode') | ||
| self.failure = data.attrib.get('failure') | ||
| self.state = data.attrib.get('state') | ||
| self.itemsCount = plexapi.utils.cast(int, data.attrib.get('itemsCount')) | ||
| self.itemsCompleteCount = plexapi.utils.cast(int, data.attrib.get('itemsCompleteCount')) | ||
| self.totalSize = plexapi.utils.cast(int, data.attrib.get('totalSize')) | ||
| self.itemsDownloadedCount = plexapi.utils.cast(int, data.attrib.get('itemsDownloadedCount')) | ||
| self.itemsReadyCount = plexapi.utils.cast(int, data.attrib.get('itemsReadyCount')) | ||
| self.itemsSuccessfulCount = plexapi.utils.cast(int, data.attrib.get('itemsSuccessfulCount')) | ||
|
|
||
| def __repr__(self): | ||
| return str(dict( | ||
| itemsCount=self.itemsCount, | ||
| itemsCompleteCount=self.itemsCompleteCount, | ||
| itemsDownloadedCount=self.itemsDownloadedCount, | ||
| itemsReadyCount=self.itemsReadyCount, | ||
| itemsSuccessfulCount=self.itemsSuccessfulCount | ||
| )) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ | |
| import zipfile | ||
| from datetime import datetime | ||
| from getpass import getpass | ||
| from threading import Thread | ||
| import threading | ||
| from tqdm import tqdm | ||
| from plexapi import compat | ||
| from plexapi.exceptions import NotFound | ||
|
|
@@ -152,15 +152,18 @@ def threaded(callback, listargs): | |
| listargs (list): List of lists; \*args to pass each thread. | ||
| """ | ||
| threads, results = [], [] | ||
| connected_event = threading.Event() | ||
| for args in listargs: | ||
| args += [results, len(results)] | ||
| results.append(None) | ||
| threads.append(Thread(target=callback, args=args)) | ||
| threads.append(threading.Thread(target=callback, args=args, kwargs=dict(connected_event=connected_event))) | ||
| threads[-1].setDaemon(True) | ||
| threads[-1].start() | ||
| for thread in threads: | ||
| thread.join() | ||
| return results | ||
| while not connected_event.is_set(): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious, what was the issue that led to this change?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most times I run the my things outside my home network, when the local IP of the server is not available, and it takes annoyingly long time to establish the connection There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense, thanks. |
||
| if all([not t.is_alive() for t in threads]): | ||
| break | ||
| time.sleep(0.05) | ||
| return list(filter(lambda r: r is not None, results)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a list comprehension |
||
|
|
||
|
|
||
| def toDatetime(value, format=None): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please update the configuration.rst file with what X_PLEX_ENABLE_FAST_CONNECT is and when we would want to use it.
https://github.com/pkkid/python-plexapi/blob/master/docs/configuration.rst