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


class Audio(PlexPartialObject):
Expand All @@ -23,6 +24,9 @@ class Audio(PlexPartialObject):
updatedAt (datatime): Datetime this item was updated.
viewCount (int): Count of times this item was accessed.
"""

METADATA_TYPE = 'track'

def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
Expand Down Expand Up @@ -57,6 +61,46 @@ def url(self, part):
""" Returns the full URL for this audio item. Typically used for getting a specific track. """
return self._server.url(part, includeToken=True) if part else None

def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
return self.title

def sync(self, bitrate, client=None, clientId=None, limit=None, title=None):
""" Add current audio (artist, album or track) as sync item for specified device.
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.

Parameters:
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
module :mod:`plexapi.sync`.
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
:func:`plexapi.myplex.MyPlexAccount.sync`.
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
limit (int): maximum count of items to sync, unlimited if `None`.
title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be
generated from metadata of current media.

Returns:
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
"""

from plexapi.sync import SyncItem, Policy, MediaSettings

myplex = self._server.myPlexAccount()
sync_item = SyncItem(self._server, None)
sync_item.title = title if title else self._defaultSyncTitle()
sync_item.rootTitle = self.title
sync_item.contentType = self.listType
sync_item.metadataType = self.METADATA_TYPE
sync_item.machineIdentifier = self._server.machineIdentifier

section = self._server.library.sectionByID(self.librarySectionID)

sync_item.location = 'library://%s/item/%s' % (section.uuid, quote_plus(self.key))
sync_item.policy = Policy.create(limit)
sync_item.mediaSettings = MediaSettings.createMusic(bitrate)

return myplex.sync(sync_item, client=client, clientId=clientId)


@utils.registerPlexObject
class Artist(Audio):
Expand Down Expand Up @@ -225,6 +269,10 @@ def download(self, savepath=None, keep_orginal_name=False, **kwargs):
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
return filepaths

def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
return '%s - %s' % (self.parentTitle, self.title)


@utils.registerPlexObject
class Track(Audio, Playable):
Expand Down Expand Up @@ -302,3 +350,7 @@ def album(self):
def artist(self):
""" Return this track's :class:`~plexapi.audio.Artist`. """
return self.fetchItem(self.grandparentKey)

def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title)
1 change: 1 addition & 0 deletions plexapi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ def reset_base_headers():
'X-Plex-Device': plexapi.X_PLEX_DEVICE,
'X-Plex-Device-Name': plexapi.X_PLEX_DEVICE_NAME,
'X-Plex-Client-Identifier': plexapi.X_PLEX_IDENTIFIER,
'X-Plex-Sync-Version': '2',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added this because it doesn't require anything from the client, it just reports to the server the we can work with Sync. Basically without this header you can't add new SyncItems.

}
223 changes: 223 additions & 0 deletions plexapi/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,82 @@ def _cleanSearchSort(self, sort):
raise BadRequest('Unknown sort dir: %s' % sdir)
return '%s:%s' % (lookup[scol], sdir)

def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, sort=None, libtype=None,
**kwargs):
""" Add current library section as sync item for specified device.
See description of :func:`~plexapi.library.LibrarySection.search()` for details about filtering / sorting
and :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.

Parameters:
policy (:class:`plexapi.sync.Policy`): policy of syncing the media (how many items to sync and process
watched media or not), generated automatically when method
called on specific LibrarySection object.
mediaSettings (:class:`plexapi.sync.MediaSettings`): Transcoding settings used for the media, generated
automatically when method called on specific
LibrarySection object.
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
:func:`plexapi.myplex.MyPlexAccount.sync`.
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be
generated from metadata of current media.
sort (str): formatted as `column:dir`; column can be any of {`addedAt`, `originallyAvailableAt`,
`lastViewedAt`, `titleSort`, `rating`, `mediaHeight`, `duration`}. dir can be `asc` or
`desc`.
libtype (str): Filter results to a specific libtype (`movie`, `show`, `episode`, `artist`, `album`,
`track`).

Returns:
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.

Raises:
:class:`plexapi.exceptions.BadRequest`: when the library is not allowed to sync

Example:

.. code-block:: python

from plexapi import myplex
from plexapi.sync import Policy, MediaSettings, VIDEO_QUALITY_3_MBPS_720p

c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('Movies')
policy = Policy('count', unwatched=True, value=1)
media_settings = MediaSettings.create(VIDEO_QUALITY_3_MBPS_720p)
section.sync(target, policy, media_settings, title='Next best movie', sort='rating:desc')

"""
from plexapi.sync import SyncItem

if not self.allowSync:
raise BadRequest('The requested library is not allowed to sync')

args = {}
for category, value in kwargs.items():
args[category] = self._cleanSearchFilter(category, value, libtype)
if sort is not None:
args['sort'] = self._cleanSearchSort(sort)
if libtype is not None:
args['type'] = utils.searchType(libtype)

myplex = self._server.myPlexAccount()
sync_item = SyncItem(self._server, None)
sync_item.title = title if title else self.title
sync_item.rootTitle = self.title
sync_item.contentType = self.CONTENT_TYPE
sync_item.metadataType = self.METADATA_TYPE
sync_item.machineIdentifier = self._server.machineIdentifier

key = '/library/sections/%s/all' % self.key

sync_item.location = 'library://%s/directory/%s' % (self.uuid, quote_plus(key + utils.joinArgs(args)))
sync_item.policy = policy
sync_item.mediaSettings = mediaSettings

return myplex.sync(client=client, clientId=clientId, sync_item=sync_item)


class MovieSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
Expand All @@ -564,11 +640,48 @@ class MovieSection(LibrarySection):
'mediaHeight', 'duration')
TAG = 'Directory'
TYPE = 'movie'
METADATA_TYPE = 'movie'
CONTENT_TYPE = 'video'

def collection(self, **kwargs):
""" Returns a list of collections from this library section. """
return self.search(libtype='collection', **kwargs)

def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
""" Add current Movie library section as sync item for specified device.
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.

Parameters:
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
:mod:`plexapi.sync` module.
limit (int): maximum count of movies to sync, unlimited if `None`.
unwatched (bool): if `True` watched videos wouldn't be synced.

Returns:
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.

Example:

.. code-block:: python

from plexapi import myplex
from plexapi.sync import VIDEO_QUALITY_3_MBPS_720p

c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('Movies')
section.sync(VIDEO_QUALITY_3_MBPS_720p, client=target, limit=1, unwatched=True,
title='Next best movie', sort='rating:desc')

"""
from plexapi.sync import Policy, MediaSettings
kwargs['mediaSettings'] = MediaSettings.createVideo(videoQuality)
kwargs['policy'] = Policy.create(limit, unwatched)
return super(MovieSection, self).sync(**kwargs)


class ShowSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing tv shows.
Expand All @@ -587,6 +700,8 @@ class ShowSection(LibrarySection):
'rating', 'unwatched')
TAG = 'Directory'
TYPE = 'show'
METADATA_TYPE = 'episode'
CONTENT_TYPE = 'video'

def searchShows(self, **kwargs):
""" Search for a show. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
Expand All @@ -608,6 +723,41 @@ def collection(self, **kwargs):
""" Returns a list of collections from this library section. """
return self.search(libtype='collection', **kwargs)

def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
""" Add current Show library section as sync item for specified device.
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.

Parameters:
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
:mod:`plexapi.sync` module.
limit (int): maximum count of episodes to sync, unlimited if `None`.
unwatched (bool): if `True` watched videos wouldn't be synced.

Returns:
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.

Example:

.. code-block:: python

from plexapi import myplex
from plexapi.sync import VIDEO_QUALITY_3_MBPS_720p

c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('TV-Shows')
section.sync(VIDEO_QUALITY_3_MBPS_720p, client=target, limit=1, unwatched=True,
title='Next unwatched episode')

"""
from plexapi.sync import Policy, MediaSettings
kwargs['mediaSettings'] = MediaSettings.createVideo(videoQuality)
kwargs['policy'] = Policy.create(limit, unwatched)
return super(ShowSection, self).sync(**kwargs)


class MusicSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing music artists.
Expand All @@ -625,6 +775,9 @@ class MusicSection(LibrarySection):
TAG = 'Directory'
TYPE = 'artist'

CONTENT_TYPE = 'audio'
METADATA_TYPE = 'track'

def albums(self):
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
key = '/library/sections/%s/albums' % self.key
Expand All @@ -646,6 +799,40 @@ def collection(self, **kwargs):
""" Returns a list of collections from this library section. """
return self.search(libtype='collection', **kwargs)

def sync(self, bitrate, limit=None, **kwargs):
""" Add current Music library section as sync item for specified device.
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.

Parameters:
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
module :mod:`plexapi.sync`.
limit (int): maximum count of tracks to sync, unlimited if `None`.

Returns:
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.

Example:

.. code-block:: python

from plexapi import myplex
from plexapi.sync import AUDIO_BITRATE_320_KBPS

c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('Music')
section.sync(AUDIO_BITRATE_320_KBPS, client=target, limit=100, sort='addedAt:desc',
title='New music')

"""
from plexapi.sync import Policy, MediaSettings
kwargs['mediaSettings'] = MediaSettings.createMusic(bitrate)
kwargs['policy'] = Policy.create(limit)
return super(MusicSection, self).sync(**kwargs)


class PhotoSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing photos.
Expand All @@ -661,6 +848,8 @@ class PhotoSection(LibrarySection):
ALLOWED_SORT = ('addedAt',)
TAG = 'Directory'
TYPE = 'photo'
CONTENT_TYPE = 'photo'
METADATA_TYPE = 'photo'

def searchAlbums(self, title, **kwargs):
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
Expand All @@ -672,6 +861,40 @@ def searchPhotos(self, title, **kwargs):
key = '/library/sections/%s/all?type=13' % self.key
return self.fetchItems(key, title=title)

def sync(self, resolution, limit=None, **kwargs):
""" Add current Music library section as sync item for specified device.
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting and
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.

Parameters:
resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the
module :mod:`plexapi.sync`.
limit (int): maximum count of tracks to sync, unlimited if `None`.

Returns:
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.

Example:

.. code-block:: python

from plexapi import myplex
from plexapi.sync import PHOTO_QUALITY_HIGH

c = myplex.MyPlexAccount()
target = c.device('Plex Client')
sync_items_wd = c.syncItems(target.clientIdentifier)
srv = c.resource('Server Name').connect()
section = srv.library.section('Photos')
section.sync(PHOTO_QUALITY_HIGH, client=target, limit=100, sort='addedAt:desc',
title='Fresh photos')

"""
from plexapi.sync import Policy, MediaSettings
kwargs['mediaSettings'] = MediaSettings.createPhoto(resolution)
kwargs['policy'] = Policy.create(limit)
return super(PhotoSection, self).sync(**kwargs)


class FilterChoice(PlexObject):
""" Represents a single filter choice. These objects are gathered when using filters
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
Loading