Closed psyciknz closed 5 years ago
The push API is implemented as a long lived future and dispatched on the event loop. So it is basically runs "in the background" by never finishing. My example in the documentation is actually quite wrong and misleading now that I look at it. Not sure what I was thinking. I can't really cook up a correct solution right now, but something like this should work in theory:
import sys
import asyncio
import pyatv
LOOP = asyncio.get_event_loop()
class PushListener:
def playstatus_update(self, updater, playstatus):
# Currently playing in playstatus
print(playstatus)
@staticmethod
def playstatus_error(updater, exception):
# Error in exception
updater.start(initial_delay=10)
@asyncio.coroutine
def print_what_is_playing(loop):
"""Find a device and print what is playing."""
print('Discovering devices on network...')
atvs = yield from pyatv.scan_for_apple_tvs(loop, timeout=5)
if not atvs:
print('no device found', file=sys.stderr)
return
print('Connecting to {}'.format(atvs[0].address))
atv = pyatv.connect_to_apple_tv(atvs[0], loop)
try:
listener = PushListener()
atv.push_updater.listener = listener
atv.push_updater.start()
yield from asyncio.sleep(60)
finally:
# Do not forget to logout
yield from atv.logout()
if __name__ == '__main__':
LOOP.run_until_complete(print_what_is_playing(LOOP))
It should subscribe to updates, sleep for one minute and then shutdown. As I said, not sure if it works but maybe you can use it as a template and make some adjustments?
Bit rough and ready but seems to drop into a loop and puts out a new entry on each video.
import pyatv import asyncio import time
class PushListener:
def playstatus_update(self, updater, playstatus):
# Currently playing in playstatus
print(playstatus)
@staticmethod
def playstatus_error(updater, exception):
# Error in exception
print("error")
updater.start(initial_delay=10)
@asyncio.coroutine def print_what_is_playing(): print("Discovering devices on network...") atvs = yield from pyatv.scan_for_apple_tvs(loop, timeout=5)
if not atvs:
print('No device found', file=sys.stderr)
return
print('Connecting to {}'.format(atvs[0].address))
atv = pyatv.connect_to_apple_tv(atvs[0], loop)
try:
listener = PushListener()
atv.push_updater.listener = listener
atv.push_updater.start()
while True:
yield from asyncio.sleep(60)
print('Aftter yield')
finally:
# Do not forget to logout
print('In Finally')
yield from atv.logout()
if name == "main": loop = asyncio.get_event_loop() asyncio.async(print_what_is_playing())
# asyncio.gather(print_what_is_playing())
#)
try:
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
print('step: loop.close()')
loop.close()
I didn't run it for long enough in one video to see it it kept posting the video name.
On 1 September 2017 at 19:12, Pierre Ståhl notifications@github.com wrote:
The push API is implemented as a long lived future and dispatched on the event loop. So it is basically runs "in the background" by never finishing. My example in the documentation is actually quite wrong and misleading now that I look at it. Not sure what I was thinking. I can't really cook up a correct solution right now, but something like this should work in theory:
import sys import asyncio import pyatv
LOOP = asyncio.get_event_loop()
class PushListener:
def playstatus_update(self, updater, playstatus): # Currently playing in playstatus print(playstatus) @staticmethod def playstatus_error(updater, exception): # Error in exception updater.start(initial_delay=10)
@asyncio.coroutine def print_what_is_playing(loop): """Find a device and print what is playing.""" print('Discovering devices on network...') atvs = yield from pyatv.scan_for_apple_tvs(loop, timeout=5)
if not atvs: print('no device found', file=sys.stderr) return print('Connecting to {}'.format(atvs[0].address)) atv = pyatv.connect_to_apple_tv(atvs[0], loop) try: listener = PushListener() atv.push_updater.listener = listener atv.push_updater.start() yield from asyncio.sleep(60) finally: # Do not forget to logout yield from atv.logout()
if name == 'main': LOOP.run_until_complete(print_what_is_playing(LOOP))
It should subscribe to updates, sleep for one minute and then shutdown. As I said, not sure if it works but maybe you can use it as a template and make some adjustments?
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/postlund/pyatv/issues/107#issuecomment-326507840, or mute the thread https://github.com/notifications/unsubscribe-auth/AFGkc6kJMIHBMq5xfv3Un2MG3wvsd2nlks5sd65ZgaJpZM4PJxRD .
Yeah, that looks about right. Unless you have any other coroutine you want to run, you can just do:
yield from atv.push_updater.start()
and that should basically run forever.
Cool, well done on the code.
So do you know if you can get the app that is playing the media? Looks like the quality of the title parm is pretty limited, ie episode name - will be a pain for me to try and pull from thetvdb.
I'm actually looking if I can push what's playing into trakt.tv - instead of each app doing it.
On 1 September 2017 at 20:43, Pierre Ståhl notifications@github.com wrote:
Yeah, that looks about right. Unless you have any other coroutine you want to run, you can just do:
yield from atv.push_updater.start()
and that should basically run forever.
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/postlund/pyatv/issues/107#issuecomment-326525509, or mute the thread https://github.com/notifications/unsubscribe-auth/AFGkc8SyMQwQd0aH4HBRASeXPRjDwCdNks5sd8PCgaJpZM4PJxRD .
Top! I'll try to update the docs when I get some time and add a proper example as well.
Currently, no. Only the DMAP/DAAP protocol is implemented (mainly used by ATV <= gen 3), and the app concept used in ATV 4 (and later) is not backported to the protocol suite. Instead another protocol, MediaRemote, is used and that is not yet implemented in pyatv
. Not sure if it's possible to extract any information on what apps are running or their metadata, but maybe. I will be working with @jeanregisser and hopefully have support for the MediaRemote protocol soon. But I cannot guarantee any additional metadata because of that. I can of course only provide what the device is exposing.
I want to give a short update here... There is a field in the playing metadata, ceSD, that seems to contain app-specific data. I started s01e08 of "Wet Hot American Summer: Ten Years Later" in Netflix on my ATV3 and got this:
$ atvremote -a --debug playing
...
DEBUG: _get_request: cmst: [container, dmcp.playstatus]
mstt: 200 [uint, dmap.status]
cmsr: 185 [uint, dmcp.serverrevision]
caps: 4 [uint, dacp.playstatus]
cash: 0 [uint, dacp.shufflestate]
carp: 0 [uint, dacp.repeatstate]
cafs: 0 [uint, dacp.fullscreen]
cavs: 0 [uint, dacp.visualizer]
cavc: False [bool, dacp.volumecontrollable]
caas: 2 [uint, dacp.albumshuffle]
caar: 6 [uint, dacp.albumrepeat]
cafe: False [bool, dacp.fullscreenenabled]
cave: False [bool, dacp.dacpvisualizerenabled]
ceQA: 0 [uint, unknown tag]
cann: End Summer Night's Dream [str, daap.nowplayingtrack]
ceSD: b'bplist00\xd1\x01\x02RidX80121154\x08\x0b\x0e\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17' [raw, unknown tag]
cmmk: 2 [uint, dmcp.mediakind]
casc: 1 [uint, unknown tag]
caks: 6 [uint, unknown tag]
cant: 1647627 [uint, dacp.remainingtime]
cast: 1658030 [uint, dacp.tracklength]
casu: 0 [uint, dacp.su]
The data is stored in a binary plist and can be decoded like this:
$ python -c "import plistlib; print(plistlib.loads(b'bplist00\xd1\x01\x02RidX80121154\x08\x0b\x0e\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17'))"
{'id': '80121154'}
Logging into Netflix to my browser and starting the same episode gives me this URL:
https://www.netflix.com/watch/80121154?lots_of_other_metadata_here
Similarly, doing the same thing while watching a youtube video:
$ python -c "import plistlib; print(plistlib.loads(b'bplist00\xd1\x01\x02Rid[_QdPW8JrYzQ\x08\x0b\x0e\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a'))"
{'id': '_QdPW8JrYzQ'}
Which happens to correspond to https://www.youtube.com/watch?v=_QdPW8JrYzQ (which is the correct video).
This information, at least in theory, makes it possible to extract the actual metadata from somewhere else other than the device. For youtube there are libraries but Netflix does not have any open API, so it might be trickier. There are other sources out there, maybe one of those works. But... biggest issue is that I haven't found any metadata about which app the ID belongs to. So, yeah, that's an issue.
But a question to you @psyciknz: Can you try watching something on Netflix or Youtube on your ATV4 and run "atvremote -a --debug playing" and paste the result (as I did above)? It would be interesting to see if it contains the same data.
I've been meaning to ask about that, I did a pip install pyatv, but there's not atvremote on my system as far as I can tell.
It should be available "everywhere" AFAIK. But I would recommend that you set up a venv and install pyatv in that. Something like
virtualenv -p python3.4 venv
source venv/bin/activate
pip install pyatv
atvremote ...
Switch to the python version you use (but at least 3.4).
Ok, found it. Was under some local directory. I'll give that a go tonight
Plex is great (but unnecessary since it goes to trakt already), but includes the show title.
DEBUG: _login_request: mlog: [container, dmap.loginresponse]
mstt: 200 [uint, dmap.status]
mlid: 17 [uint, dmap.sessionid]
INFO: Logged in and got session id 17
DEBUG: GET URL: http://192.168.10.36:3689/ctrl-int/1/playstatusupdate?session-id=17&revision-number=0
DEBUG: _get_request: cmst: [container, dmcp.playstatus]
mstt: 200 [uint, dmap.status]
cmsr: 461 [uint, dmcp.serverrevision]
cafs: 0 [uint, dacp.fullscreen]
cafe: False [bool, dacp.fullscreenenabled]
cave: False [bool, dacp.dacpvisualizerenabled]
cavs: 0 [uint, dacp.visualizer]
cant: 3310201 [uint, dacp.remainingtime]
cast: 3336216 [uint, dacp.tracklength]
caps: 4 [uint, dacp.playstatus]
cash: 0 [uint, dacp.shufflestate]
carp: 0 [uint, dacp.repeatstate]
caar: 6 [uint, dacp.albumrepeat]
caas: 2 [uint, dacp.albumshuffle]
cann: S1 • E4: Cripples, Bastards, and Broken Things [str, daap.nowplayingtrack]
cana: Game of Thrones [str, daap.nowplayingartist]
canl: Season 1 [str, daap.nowplayingalbum]
caks: 1 [uint, unknown tag]
casc: 1 [uint, unknown tag]
cavc: True [bool, dacp.volumecontrollable]
casu: 0 [uint, dacp.su]
Media type: Music
Play state: Playing
Title: S1 • E4: Cripples, Bastards, and Broken Things
Artist: Game of Thrones
Album: Season 1
Position: 26/3336s (0.8%)
Repeat: Off
Shuffle: False
Ill try a streaming service next.
Did you get any result?
I extended the parsing code (https://github.com/postlund/pyatv/commit/003ff3dbf7316630c07c2894614ef8ff72aa7c7b) to support ceSD, so it will appear in a nicer format now on:
DEBUG: _get_request: cmst: [container, dmcp.playstatus]
mstt: 200 [uint, dmap.status]
cmsr: 290 [uint, dmcp.serverrevision]
caps: 4 [uint, dacp.playstatus]
cash: 0 [uint, dacp.shufflestate]
carp: 0 [uint, dacp.repeatstate]
cafs: 0 [uint, dacp.fullscreen]
cavs: 0 [uint, dacp.visualizer]
cavc: False [bool, dacp.volumecontrollable]
caas: 2 [uint, dacp.albumshuffle]
caar: 6 [uint, dacp.albumrepeat]
cafe: False [bool, dacp.fullscreenenabled]
cave: False [bool, dacp.dacpvisualizerenabled]
ceQA: 0 [uint, unknown tag]
cann: 30 Minutes or Less [str, daap.nowplayingtrack]
ceSD: {'id': '70167074'} [bplist, playing metadata]
cmmk: 2 [uint, dmcp.mediakind]
casc: 1 [uint, unknown tag]
caks: 6 [uint, unknown tag]
cant: 2124216 [uint, dacp.remainingtime]
cast: 4981017 [uint, dacp.tracklength]
casu: 0 [uint, dacp.su]
Media type: Music
Play state: Playing
Title: 30 Minutes or Less
Position: 2857/4981s (57.4%)
Repeat: Off
Shuffle: False
I don't have a casd on this service DEBUG: GET URL: http://192.168.10.36:3689/ctrl-int/1/playstatusupdate?session-id=20&revision-number=0
From: Pierre Ståhl notifications@github.com Sent: Friday, September 8, 2017 7:44:45 AM To: postlund/pyatv Cc: psyciknz; Mention Subject: Re: [postlund/pyatv] Can't figure out the push api (#107)
I extended the parsing code (003ff3dhttps://github.com/postlund/pyatv/commit/003ff3dbf7316630c07c2894614ef8ff72aa7c7b) to support ceSD, so it will appear in a nicer format now on:
DEBUG: _get_request: cmst: [container, dmcp.playstatus] mstt: 200 [uint, dmap.status] cmsr: 290 [uint, dmcp.serverrevision] caps: 4 [uint, dacp.playstatus] cash: 0 [uint, dacp.shufflestate] carp: 0 [uint, dacp.repeatstate] cafs: 0 [uint, dacp.fullscreen] cavs: 0 [uint, dacp.visualizer] cavc: False [bool, dacp.volumecontrollable] caas: 2 [uint, dacp.albumshuffle] caar: 6 [uint, dacp.albumrepeat] cafe: False [bool, dacp.fullscreenenabled] cave: False [bool, dacp.dacpvisualizerenabled] ceQA: 0 [uint, unknown tag] cann: 30 Minutes or Less [str, daap.nowplayingtrack] ceSD: {'id': '70167074'} [bplist, playing metadata] cmmk: 2 [uint, dmcp.mediakind] casc: 1 [uint, unknown tag] caks: 6 [uint, unknown tag] cant: 2124216 [uint, dacp.remainingtime] cast: 4981017 [uint, dacp.tracklength] casu: 0 [uint, dacp.su]
Media type: Music Play state: Playing Title: 30 Minutes or Less Position: 2857/4981s (57.4%) Repeat: Off Shuffle: False
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/postlund/pyatv/issues/107#issuecomment-327903730, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AFGkcw8OYaniKFcU3yhf4kRwts2KcUucks5sgEesgaJpZM4PJxRD.
I guess that tvOS maybe skips that one and fills in the other fields in a better manner instead. Good to know!
Hi,
playstatus_error doesn't have to be static, but if you remove the @staticmethod decorator you have to add self as first argument in the argument list to the method.
/Pierre
On Tue, Apr 10, 2018 at 8:40 PM, Serge Wagener notifications@github.com wrote:
Hi, I don't want to hijack this thread / issue, but my problem enters into this issues title. I have the callbacks in the same class where i use pyatv, so i set the callbacks to self like so:
def playstatus_update(self, updater, playstatus): """ Callback for pyatv, is called on currently playing update """ self.update_playing(playstatus) self._loop.create_task(self.update_artwork()) @staticmethod def playstatus_error(updater, exception): """ Callback for pyatv, is called on push update error """ print("PushListener error: {0}".format(exception)) #self.logger.warning("PushListener error: {0}".format(exception)) updater.start(initial_delay=10) def _push_listener_thread_worker(self): """ Thread to run asyncio loop. This avoids blocking the main plugin thread """ asyncio.set_event_loop(self._loop) self._atv.push_updater.listener = self self._atv.push_updater.start() while self._loop.is_running(): pass try: self.logger.debug("Loop running") while True: self._loop.run_until_complete(asyncio.sleep(0.25)) except: self.logger.debug('*** Error in loop.run_forever()') #raise
Push updates work like a charm (meanwhile). I can even insert remote_control tasks into the running loop.
The only thing i can't get to work is the playstatus_error callback. When i ctrl-c my application i guess the following error is when it tries to call the playstatus_error:
2018-04-10 20:00:40 ERROR base_events ATV listener Exception in callback None() handle:
-- base_events.py:default_exception_handler:1258 Traceback (most recent call last): File "/usr/lib/python3.5/asyncio/events.py", line 126, in _run self._callback(*self._args) TypeError: 'NoneType' object is not callable Why does it need to be a static method, and is it possible to get it to work like this, or must it be in a separate class ?
— You are receiving this because you were assigned. Reply to this email directly, view it on GitHub https://github.com/postlund/pyatv/issues/107#issuecomment-380205419, or mute the thread https://github.com/notifications/unsubscribe-auth/AFtH41Lg3sInX2vxJ2RQEChtucN5EY2Dks5tnPyWgaJpZM4PJxRD .
-- Mvh, Pierre Ståhl
I will close this as it will be part of the new documentation in #205.
I've tried your example, but I'm no experienced with python enough to know if it's missing any thing.
I've got the following: import pyatv import asyncio
which now seems to run, as without the if main it'd get a syntax error. But then it just drops out. I assume I can just put in a loop. Would I be correct in that there's not loop/timer in the push_updater, or listener code?