Skip to content

Conversation

@andrey-yantsen
Copy link
Contributor

@andrey-yantsen andrey-yantsen commented Aug 19, 2018

Hi there! I've started working on Mobile Sync support. If anybody has any comments on the code, or just any feature requests — feel free to tell :)

At the beginning the code can do few useful things:

  1. Initialise the "sync" env: tell the plex.tv that the client supports syncing, and the client can be used as sync-target
  2. Unfortunately I have to "fake" used platform/device info, because transcoding is depends on this values
  3. And finally you can download converted files, by iterating over media parts and looking for ones with required syncItemId

The reason why I'm started this is simple: I have a WD My Passport Pro which has the Plex onboard and I'd like to have my "travel" library in sync with the main one. I've already started the project for this (andrey-yantsen/plexiglas) but for now there is nothing to see there.

fix #134

@Hellowlol
Copy link
Collaborator

Hi! Thanks for the PR!

How do you actually sync the items? Can you add a test?

I would expect that there was a sync function inside the client so you could just pass a list of objects and that would be it.

Any info needed for the transcode can be picked up there this way we don’t need to mess with the init method.

@andrey-yantsen
Copy link
Contributor Author

Just to clarify: the PR is not finished now, it's an early alpha version :)

How do you actually sync the items?

The list of "items-to-sync" is controlled in Plex Web, you can add any item to the list for a specific device.

In the following code block I will show the way to download all suitable content (actually I've not yet reached the "downloading" stage in my project, so I'm not sure if this really working now):

from plexapi import sync, utils
from plexapi.myplex import MyPlexAccount
sync.init()
plex = MyPlexAccount()

for item in plex.syncItems().items:
    if item.status.state != 'complete':
        continue

    for media in item.getMedia():
        for part in media.iterParts():
            filename = '%s.%s' % (item._prettyfilename(), part.container)
            url = item._server.url(part.key)
            filepath = utils.download(url, token=plex.authenticationToken, filename=filename, savepath=os.getcwd(),
                                      session=item.server()._session, showstatus=True)

Can you add a test?

Yep, of course, on some latter stage :)

I would expect that there was a sync function inside the client so you could just pass a list of objects and that would be it.
Any info needed for the transcode can be picked up there this way we don’t need to mess with the init method.

As far as I can see the only way for Plex to detect transcoding options, is thru target platform. Here is what I see in the Plex logs with real platform info:

ERROR - Unable to find client profile for device; platform=Darwin, platformVersion=17.7.0, device=
Darwin, model=

And the process goes smoothly when I mimic my iPhone SE.

Also here is the request which should be sent to add an item to sync list:

POST https://plex.tv/devices/779755E8-54E2-4D0F-A7D9-E2FF8171CC55/sync_items?X-Plex-Device-Name=Yantsen%20-%20iPhone%20An HTTP/1.1

Content-Type:	application/x-www-form-urlencoded

SyncItem[Location][uri]:        library://ac0a6a57-522b-4dcf-9cb8-0a58b16c6726/item/%2Flibrary%2Fmetadata%2F2428
SyncItem[MediaSettings][audioBoost]:100
SyncItem[MediaSettings][maxVideoBitrate]:4000
SyncItem[MediaSettings][musicBitrate]:192
SyncItem[MediaSettings][photoQuality]:74
SyncItem[MediaSettings][photoResolution]:1920x1080
SyncItem[MediaSettings][subtitleSize]:100
SyncItem[MediaSettings][videoQuality]:100
SyncItem[MediaSettings][videoResolution]:1280x720
SyncItem[Policy][scope]:        all
SyncItem[Policy][unwatched]:    0
SyncItem[Policy][value]:        0
SyncItem[contentType]:          video
SyncItem[machineIdentifier]:    607c8c938b50eef734456f8b9da94b5d02339ce5
SyncItem[metadataType]:         movie
SyncItem[rootTitle]:            Test
SyncItem[title]:                Test1

As you can see there are nothing here about container and/or supported codecs list, only basic/common transcoding stuff.

@andrey-yantsen andrey-yantsen changed the title [WIP] Sync support [WIP] [Do not merge] Sync support Aug 20, 2018
@Hellowlol
Copy link
Collaborator

You dont really need to do anything to make the sync happens. Seems you just upload zhe shit to a queue, and the client handles the rest from there.

Anyway to send a sync command should be as easy as:

inside client.py


def sync_item(self, item): 
    #magic happens here.
    # Maybe grab stuff that you need from the client like platform etc
    # name, title from item etc.

   

Looking forward to test the pr :)

@andrey-yantsen
Copy link
Contributor Author

andrey-yantsen commented Aug 20, 2018

Seems you just upload zhe shit to a queue, and the client handles the rest from there.

Yep, adding new sync-item to some other client is easy, and you don't have to call sync.init() for it. And it's not supported by this PR now :)

My goal is to support "sync-target": when you run the script on some random computer, select sync-items in your Plex Web and the required media magically being copied to this computer, in the suitable format.

In my case I have an external HDD and I'd like to always have some movies / tv shows (converted to mp4 and not in 4k format), e.g. to be able to watch something while I'm on a plane.

@Hellowlol
Copy link
Collaborator

Hellowlol commented Aug 20, 2018

I dont think syncing to computers is supported by plex at all. Do you just want to copy things from another shared plex server to your own? Or sync to a actual client?

Anyway if you just want to download stuff or transcode to a computer thats already supported.

@andrey-yantsen
Copy link
Contributor Author

I dont think syncing to computers is supported by plex at all.

It's not supported out-of-the-box, but we're the engineers, and we can force software to do what we want! :)

Do you just want to copy things from another shared plex server to your own?

I want to copy optimised copies of things from any plex server to an external hdd

@Hellowlol
Copy link
Collaborator

Hellowlol commented Aug 20, 2018

Oh, thats easy. Its pretty much supported out of the box.

See the docs for.

https://python-plexapi.readthedocs.io/en/latest/modules/video.html?highlight=getStreamURL%20#plexapi.video.Movie.download

note the comment about **kwargs – Additional options passed into getStreamURL().
You can use this to transcode.

here was a example on how i use it from one my my own projects.

https://github.com/Hellowlol/plex-cli/blob/master/plexcli/cli.py#L126

@andrey-yantsen
Copy link
Contributor Author

andrey-yantsen commented Aug 20, 2018

Yep, I saw this option, I plan to support it in my project, but it's not on my priority list...

getSteamURL() not that powerful, easy and cute like Mobile Sync: e.g. with Sync you can mark to sync "3 unwatched episodes", "3 unwatched movies with highest rating", "3 unwatched movies released recently", and so on. And all those media would be automatically rotated while you marking something as watched and when new releases become available.

@Hellowlol
Copy link
Collaborator

I think im missing something here. Do you expect to do this from the plex ui?
If would be easy to get movies/episodes based on some condition run them in some cron script.

@andrey-yantsen
Copy link
Contributor Author

andrey-yantsen commented Aug 20, 2018

Do you expect to do this from the plex ui?

You're exactly right :) https://support.plex.tv/articles/201082477-quick-guide-to-mobile-sync/

If would be easy to get movies/episodes based on some condition run them in some cron script.

Man... I just like the Mobile Sync approach, where you don't have to code the filters by yourself, but you can easily choose the required things with UI and this UI doesn't require any dev-knowledge at all, even my wife would be able to use it! :D

@Hellowlol
Copy link
Collaborator

Hellowlol commented Aug 20, 2018

Oh, i think i get it now. You faking so plexapi is detected as a client in the plex webui... imo that should be done with your own project.

Nice idea btw!

@andrey-yantsen
Copy link
Contributor Author

imo that should be done with your own project.

You mean sync.init()? You're right, I'll replace it with a note in the docs.

plexapi/base.py Outdated
on how this is used.
"""
data = self._server.query(ekey)
timeout = kwargs.pop('timeout', None)
Copy link

Choose a reason for hiding this comment

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

This should probably be added as a real keyword arg timeout=None in the function definition and not pulled out of kwargs.



def _connect(cls, url, token, timeout, results, i):
def _connect(cls, url, token, timeout, fast, results, i, connected_event=None):
Copy link

Choose a reason for hiding this comment

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

If X_PLEX_ENABLE_FAST_CONNECT is a global configuration option. Should we instead always use fast=True or false based on that, rather than passing it in as an argument?

plexapi/utils.py Outdated
for thread in threads:
thread.join()
return results
while not connected_event.is_set():
Copy link

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

Choose a reason for hiding this comment

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

Makes sense, thanks.

@andrey-yantsen
Copy link
Contributor Author

@mikes-nasuni thank you for the fast reaction, I've made the changes which you've suggested :)

plexapi/utils.py Outdated
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
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

plexapi/sync.py Outdated
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
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.

plexapi/sync.py Outdated
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
Collaborator

Choose a reason for hiding this comment

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

List comp please

plexapi/sync.py Outdated
return server[0]

def getMedia(self):
def getMedia(self, timeout=None):
Copy link
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
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?

Copy link

@ghost ghost Aug 24, 2018

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.

""" Returns an instance of :class:`~plexapi.sync.SyncItems` for specified client
Arguments:
clientId (str): an identifier of a client for which you need to get SyncItems. Would be set to current
Copy link
Collaborator

Choose a reason for hiding this comment

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

allow for both a client and a clientid as a string.

""" Adds specified sync item for the client
Arguments:
client (:class:`~plexapi.myplex.MyPlexDevice`): pass
Copy link
Collaborator

Choose a reason for hiding this comment

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

doc what this client, sync_item is used for.

return '%s%s' % (self._baseurl, key)

def refreshSynclist(self):
return self.query('/sync/refreshSynclists', self._session.put)
Copy link
Collaborator

Choose a reason for hiding this comment

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

add missing doc string

return self.query('/sync/refreshSynclists', self._session.put)

def refreshContent(self):
return self.query('/sync/refreshContent', self._session.put)
Copy link
Collaborator

Choose a reason for hiding this comment

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

add missing doc string

plexapi/sync.py Outdated
status (:class:`~plexapi.sync.Status`): current status of the sync
mediaSettings (:class:`~plexapi.sync.MediaSettings`): media transcoding settings used for the item
policy (:class:`~plexapi.sync.Policy`): the policy of which media to sync
location (str): unknown
Copy link
Collaborator

Choose a reason for hiding this comment

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

self.clientidentifier is not listed as a attribute.

plexapi/sync.py Outdated
""" Returns :class:`~plexapi.myplex.MyPlexResource` with server of current item.
"""
server = [s for s in self._server.resources() if s.clientIdentifier == self.machineIdentifier]
if 0 == len(server):
Copy link
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,

media._server.query(url, method=requests.put)

def delete(self):
url = SyncList.key.format(clientId=self.clientIdentifier)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Missing doc string

plexapi/sync.py Outdated
self.clientId = data.attrib.get('clientIdentifier')
self.items = []

for elem in data:
Copy link
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?

plexapi/video.py Outdated
return self._prettyfilename()

def sync(self, client, policy, media_settings, title=None):
""" Add current video as sync item for specified device.
Copy link
Collaborator

Choose a reason for hiding this comment

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

add docs for args, and keywork arguments.

MAX_RETRIES = 3


def get_new_synclist(account, client_id, old_sync_items=[]):
Copy link
Collaborator

Choose a reason for hiding this comment

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

old_sync_items is mutable, is this intended?

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 already rewrote this part locally, anyway :D

@andrey-yantsen
Copy link
Contributor Author

Heyhey, @Hellowlol, thank you for the review! I've pushed another pile of code, where I've done few things:

  1. Fixed part of your review request
  2. Marked all (I hope that I marked them all) as TODO, to not miss it later
  3. Simplified initialisation of sync.MediaSettings in order to get this working easily with other content types
  4. Added sync() method to audio, photo & playlists
  5. Each sync-related method now can accept client as an object, or clientId as a string
  6. Covered this stuff with creepy tests

In order to get sync/tests for photos working PR #292 need to be applied.

@Hellowlol
Copy link
Collaborator

Let me know when this is ready, i want to squash and merge this before i do the small prs.

@andrey-yantsen
Copy link
Contributor Author

Yep, sure. I hope to finish this by tomorrow.

@andrey-yantsen andrey-yantsen changed the title [WIP] [Do not merge] Sync support Sync support Sep 4, 2018
@andrey-yantsen
Copy link
Contributor Author

@Hellowlol here goes the docs :) Anything else you'd like to be changed?

@Hellowlol
Copy link
Collaborator

Thanks for the doc changes. I havnt really looked at the code before now. There is a lot of repetitions regarding the sync methods. Anyway we could simplify this?

@andrey-yantsen
Copy link
Contributor Author

Uh... From the top of my head — I can add a mixin Syncable with some basic implementation of the method, but, IMO, because of different content types (video, audio, photo) require different options, and it's possible to sync so many different, not directly related in OOP to each other, stuff (library, playlist, movie, tv-show episode, etc.) there are still be some repetition...

@Hellowlol Do you have any better ideas?

@Hellowlol
Copy link
Collaborator

Hellowlol commented Sep 6, 2018

Using a mixin was my thought too. If there are too many options we might just leave it as it is. Anyway are you done?

@andrey-yantsen
Copy link
Contributor Author

Yep, it's done I think. Probably I'll find something else later, but for now I have nothing to add :)

@Hellowlol
Copy link
Collaborator

Awesome, thanks!

@Hellowlol
Copy link
Collaborator

Can you fix the inline imports so the style is consistent? We don't seem to use any other relative imports,

@andrey-yantsen
Copy link
Contributor Author

Yep, sure, I've just pushed it

@Hellowlol Hellowlol merged commit 54b26fd into pushingkarmaorg:master Sep 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Sync

2 participants