Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5432876
[sync] initial commit
worried-networking Aug 19, 2018
edfe9d5
fix populating of `state` field in sync.Status
worried-networking Aug 20, 2018
05bd225
[connection] add posibliity to return first established connection fa…
worried-networking Aug 24, 2018
3f9673c
[base] add timeout argument to PlexObject.fetchItems()
worried-networking Aug 24, 2018
1fa3dc4
[sync] add timeout arg to SyncItem.getMedia()
worried-networking Aug 24, 2018
6051a8a
[sync] fix marking media as downloaded
worried-networking Aug 24, 2018
5ec0a42
[sync] pass clientIdentifier to created SyncItem()
worried-networking Aug 24, 2018
1f11bdf
[sync] override __repr__() for sync.Status
worried-networking Aug 24, 2018
cd2849e
fix after @mikes-nasuni`s review
worried-networking Aug 24, 2018
afbf45c
fix python2 compatibility
worried-networking Aug 24, 2018
34296c7
get rid of sync.init()
worried-networking Aug 24, 2018
57f68ac
use list comprehension
worried-networking Aug 24, 2018
59af01d
remove timeout from PlexObject.fetchItems()
worried-networking Aug 25, 2018
5860e2c
fix SyncItem under python 2.7
worried-networking Aug 28, 2018
9383ed0
fix __doc__ in sync module
worried-networking Aug 28, 2018
7fb616e
revert myplex._connect() back to it`s original state
worried-networking Sep 1, 2018
2f534dd
improve sync docs
worried-networking Sep 1, 2018
10226e1
get rid of PlexObjects where not needed
worried-networking Sep 2, 2018
1297724
add X-Plex-Sync-Version=2 to headers
worried-networking Sep 2, 2018
2be3f5c
add sync() method into Video, LibrarySection and MyPlexAccount
worried-networking Sep 2, 2018
ef95e01
add SyncItem.delete()
worried-networking Sep 3, 2018
e69df4d
add sync.Policy.create()
worried-networking Sep 3, 2018
4590adc
use self._default_sync_title instead of _prettyfilename as default title
worried-networking Sep 3, 2018
a46a4aa
let the tests begin
worried-networking Sep 3, 2018
f9c881b
add items for refreshing synclists to PlexServer
worried-networking Sep 3, 2018
a811772
fix sync tests
worried-networking Sep 3, 2018
9971435
sync for everybody!
worried-networking Sep 3, 2018
5a147f8
add TODO doctring for Audio._defaultSyncTitle()
worried-networking Sep 4, 2018
98d93d3
SyncItems tag may be presented only once, there is no need for loop
worried-networking Sep 4, 2018
24c974f
add more TODO docstrings
worried-networking Sep 4, 2018
e82e7d7
hello docs
worried-networking Sep 4, 2018
ee867f5
remove relative import
worried-networking Sep 6, 2018
462a16d
remove unused variable from tests/test_sync.py
worried-networking Sep 6, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plexapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
VERSION = '3.0.6'
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)

Copy link
Copy Markdown

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


# Plex Header Configuation
X_PLEX_PROVIDES = CONFIG.get('header.provides', 'controller')
Expand Down
4 changes: 2 additions & 2 deletions plexapi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,12 @@ def fetchItem(self, ekey, cls=None, **kwargs):
clsname = cls.__name__ if cls else 'None'
raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs))

def fetchItems(self, ekey, cls=None, **kwargs):
def fetchItems(self, ekey, cls=None, timeout=None, **kwargs):
""" Load the specified key to find and build all items with the specified tag
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
on how this is used.
"""
data = self._server.query(ekey)
data = self._server.query(ekey, timeout=timeout)
return self.findItems(data, cls, ekey, **kwargs)

def findItems(self, data, cls=None, initpath=None, **kwargs):
Expand Down
5 changes: 5 additions & 0 deletions plexapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ def _loadData(self, data):
self.indexes = data.attrib.get('indexes')
self.key = data.attrib.get('key')
self.size = cast(int, data.attrib.get('size'))
self.decision = data.attrib.get('decision')
self.optimizedForStreaming = cast(bool, data.attrib.get('optimizedForStreaming'))
self.syncItemId = cast(int, data.attrib.get('syncItemId'))
self.syncState = data.attrib.get('syncState')
self.videoProfile = data.attrib.get('videoProfile')
self.streams = self._buildStreams(data)

def _buildStreams(self, data):
Expand Down
15 changes: 13 additions & 2 deletions plexapi/myplex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.
"""
Expand All @@ -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)
Expand Down
97 changes: 79 additions & 18 deletions plexapi/sync.py
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List comp please

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What up with the timeout? Why not just change the plex.Timeout?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all operations require 60s+ timeout, and wrapping getMedia() calls with changing timeout back-and-forth would look strange.
You think that just globally change TIMEOUT in my code would be more than enough and wouldn't cause any strange problems?

@ghost ghost Aug 24, 2018

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You think that just globally change TIMEOUT in my code would be more than enough and wouldn't cause any strange problems?

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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
))
13 changes: 8 additions & 5 deletions plexapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, what was the issue that led to this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a list comprehension



def toDatetime(value, format=None):
Expand Down