Skip to content

Commit 51e0cff

Browse files
committed
Changes how things display in the app, and some code changes to accommodate that.
Root category now includes the active episodes (loads quickly), and the active episodes playlist can now be played at once (slower!).
1 parent 4adaaa6 commit 51e0cff

5 files changed

+90
-37
lines changed

README.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Play [Overcast](https://overcast.fm/) podcasts on your Sonos.
22

3-
* Fast!
3+
* Fast!
44
* Remembers last played position
55
* Syncs play position back to the Overcast service every 30 seconds or when paused/stopped
66
* Removes episodes from Overcast when completed
@@ -16,9 +16,10 @@ Usage:
1616
- SID - some unique SID (255 works if you've not done this before
1717
- Service Name - some name, `overcast` works
1818
- Endpoint URL and Secure Endpoint URL: `http://<YOUR_IP>:8140/overcast-sonos`
19+
- Presentation map: version = 1, Uri = `http://<YOUR_IP>:8140/presentation_map`
1920
- Authentication SOAP header policy: "Anonymous"
20-
- Check the boxes in the example picture
21+
- Check the boxes in the [example screenshot](./customsd_example.png)
2122

2223
1. In your Sonos controller, go to "Add Music Service", then add the service above.
2324

24-
See the [TODO list](./TODO.md) for some nice-to-haves .
25+
See the [TODO list](./TODO.md) for some nice-to-haves.
-296 KB
Binary file not shown.

customsd_example.png

332 KB
Loading

overcast-sonos.py

+70-20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,32 @@
88
logging.basicConfig(level=logging.DEBUG)
99
log = logging.getLogger('overcast-sonos')
1010

11+
list_active_episodes_in_root = True
12+
allow_all_active_episodes_as_playlist = True
13+
14+
15+
class customSOAPHandler(SOAPHandler):
16+
17+
def do_GET(self):
18+
log.debug('PATH ==> %s', self.path)
19+
if self.path == '/presentation_map':
20+
self.send_response(200)
21+
self.send_header('Content-type', 'text/xml')
22+
self.end_headers()
23+
self.wfile.write('''<?xml version="1.0" encoding="UTF-8"?>
24+
<Presentation>
25+
<PresentationMap type="DisplayType">
26+
<RootNodeDisplayType>
27+
<DisplayMode>LIST</DisplayMode>
28+
</RootNodeDisplayType>
29+
</PresentationMap>
30+
</Presentation>
31+
''')
32+
return
33+
else:
34+
return SOAPHandler.do_GET(self)
35+
36+
1137
dispatcher = SoapDispatcher('overcast-sonos',
1238
location='http://localhost:8140/',
1339
namespace='http://www.sonos.com/Services/1.1',
@@ -63,30 +89,51 @@ def getSessionId(username, password):
6389
###
6490

6591

66-
def getMetadata(id, index, count):
67-
log.debug('at=getMetadata id=%s index=%s count=%s', id, index, count)
92+
def getMetadata(id, index, count, recursive=False):
93+
log.debug('at=getMetadata id=%s index=%s count=%s recursive=%s', id, index, count, recursive)
6894

6995
if id == 'root':
70-
response = {'getMetadataResult': [
71-
{'index': 0, 'count': 2, 'total': 2},
72-
{'mediaCollection': {
73-
'id': 'episodes',
74-
'title': 'All Active Episodes',
75-
'itemType': 'container',
76-
'canPlay': False,
77-
'albumArtURI': 'http://is2.mzstatic.com/image/thumb/Purple62/v4/22/c7/93/22c793a7-55a8-e72b-756c-90641e7b96d4/source/175x175bb.jpg',
78-
}},
96+
response = {'getMetadataResult': []}
97+
response['getMetadataResult'].append(
7998
{'mediaCollection': {
8099
'id': 'podcasts',
81100
'title': 'Podcasts',
82-
'itemType': 'container',
101+
'itemType': 'albumList',
83102
'canPlay': False,
84103
'albumArtURI': 'http://is2.mzstatic.com/image/thumb/Purple62/v4/22/c7/93/22c793a7-55a8-e72b-756c-90641e7b96d4/source/175x175bb.jpg',
85-
}},
86-
]}
104+
}})
105+
response['getMetadataResult'].append(
106+
{'mediaCollection': {
107+
'id': 'episodes',
108+
'title': 'All Active Episodes',
109+
'itemType': 'playlist',
110+
'canPlay': allow_all_active_episodes_as_playlist,
111+
'albumArtURI': 'http://is2.mzstatic.com/image/thumb/Purple62/v4/22/c7/93/22c793a7-55a8-e72b-756c-90641e7b96d4/source/175x175bb.jpg',
112+
}})
113+
if list_active_episodes_in_root:
114+
all_episodes = overcast.get_active_episodes()
115+
episodes = all_episodes[index:index+count]
116+
response['getMetadataResult'].append({'index': index, 'count': len(episodes) + 2, 'total': len(all_episodes) + 2})
117+
for episode in episodes:
118+
response['getMetadataResult'].append({
119+
'mediaMetadata': {
120+
'id': 'episodes/' + episode['id'],
121+
'title': episode['podcast_title'] + " - " + episode['title'],
122+
'mimeType': episode['audio_type'],
123+
'itemType': 'track',
124+
'trackMetadata': {
125+
'artist': episode['podcast_title'],
126+
'album': episode['podcast_title'],
127+
'albumArtist': episode['podcast_title'],
128+
'albumArtURI': episode['albumArtURI'],
129+
'genreId': 'podcast',
130+
'canResume': True,
131+
}
132+
}
133+
})
87134

88135
elif id == 'episodes':
89-
all_episodes = overcast.get_active_episodes()
136+
all_episodes = overcast.get_active_episodes(get_details=recursive)
90137
episodes = all_episodes[index:index+count]
91138
response = {'getMetadataResult': [{'index': index, 'count': len(episodes), 'total': len(all_episodes)}]}
92139
for episode in episodes:
@@ -101,6 +148,7 @@ def getMetadata(id, index, count):
101148
'albumArtist': episode['podcast_title'],
102149
'albumArtURI': episode['albumArtURI'],
103150
'genreId': 'podcast',
151+
'duration': episode['duration'],
104152
'canResume': True,
105153
}
106154
}
@@ -115,7 +163,7 @@ def getMetadata(id, index, count):
115163
'id': 'podcasts/' + podcast['id'],
116164
'title': podcast['title'],
117165
'albumArtURI': podcast['albumArtURI'],
118-
'itemType': 'container',
166+
'itemType': 'album',
119167
'canPlay': False,
120168
}})
121169

@@ -152,7 +200,7 @@ def getMetadata(id, index, count):
152200
dispatcher.register_function(
153201
'getMetadata', getMetadata,
154202
returns={'getMetadataResult': {'index': int, 'count': int, 'total': int, 'mediaCollection': mediaCollection}},
155-
args={'id': str, 'index': int, 'count': int}
203+
args={'id': str, 'index': int, 'count': int, 'recursive': bool}
156204
)
157205

158206
###
@@ -161,6 +209,7 @@ def getMetadata(id, index, count):
161209
def getMediaMetadata(id):
162210
log.debug('at=getMediaMetadata id=%s', id)
163211
_, episode_id = id.rsplit('/', 1)
212+
log.debug('at=getMediaMetadata episode_id=%s', episode_id)
164213
episode = overcast.get_episode_detail(episode_id)
165214
response = {'getMediaMetadataResult': {
166215
'mediaMetadata': {
@@ -204,7 +253,7 @@ def getMediaURI(id):
204253
'offsetMillis': episode['offsetMillis']
205254
},
206255
}
207-
log.debug('at=getMediaMetadata response=%s', response)
256+
log.debug('at=getMediaURI response=%s', response)
208257
return response
209258

210259

@@ -246,7 +295,7 @@ def reportPlaySeconds(id, seconds, offsetMillis, contextId):
246295
)
247296

248297

249-
def reportPlayStatus(id, status, contextId, offsetMillis):
298+
def reportPlayStatus(id, status, offsetMillis, contextId):
250299
episode_id = id.rsplit('/', 1)[-1]
251300
log.debug('at=reportPlayStatus and id=%s, status=%s, contextId=%s, offsetMillis=%d, episode_id=%s', id, status, contextId, offsetMillis, episode_id)
252301
episode = overcast.get_episode_detail(episode_id)
@@ -273,8 +322,9 @@ def setPlayedSeconds(id, seconds, offsetMillis, contextId):
273322
args={'id': str, 'seconds': int, 'offsetMillis': int, 'contextId': str}
274323
)
275324

325+
276326
if __name__ == '__main__':
277327
log.info('at=start')
278-
httpd = HTTPServer(("", 8140), SOAPHandler)
328+
httpd = HTTPServer(("", 8140), customSOAPHandler)
279329
httpd.dispatcher = dispatcher
280330
httpd.serve_forever()

overcast.py

+16-14
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,23 @@ def __init__(self, email, password):
2626
def _get_html(self, url):
2727
return lxml.html.fromstring(self.session.get(url).content)
2828

29-
def get_active_episodes(self):
29+
def get_active_episodes(self, get_details=False):
30+
active_episodes = []
3031
doc = self._get_html('https://overcast.fm/podcasts')
31-
return [
32-
# NOTE: If the hardcoded audio_type causes any problems, just uncomment the line below and comment out the dictionary below it.
33-
# self.get_episode_detail(cell.attrib['href'])
34-
{
35-
'id': urlparse.urljoin('https://overcast.fm', cell.attrib['href']).lstrip('/'),
36-
'title': cell.cssselect('div.titlestack div.title')[0].text_content(),
37-
'audio_type': 'audio/mpeg',
38-
'podcast_title': cell.cssselect('div.titlestack div.caption2')[0].text_content(),
39-
'albumArtURI': cell.cssselect('img')[0].attrib['src'],
40-
}
41-
for cell in doc.cssselect('a.episodecell')
42-
if 'href' in cell.attrib
43-
]
32+
for cell in doc.cssselect('a.episodecell'):
33+
if 'href' in cell.attrib:
34+
if get_details:
35+
active_episodes.append(self.get_episode_detail(cell.attrib['href']))
36+
else:
37+
active_episodes.append({
38+
'id': urlparse.urljoin('https://overcast.fm', cell.attrib['href']).lstrip('/'),
39+
'title': cell.cssselect('div.titlestack div.title')[0].text_content(),
40+
'audio_type': 'audio/mpeg',
41+
'podcast_title': cell.cssselect('div.titlestack div.caption2')[0].text_content(),
42+
'albumArtURI': cell.cssselect('img')[0].attrib['src'],
43+
'duration': -1,
44+
})
45+
return active_episodes
4446

4547
def get_episode_detail(self, episode_id):
4648
episode_href = urlparse.urljoin('https://overcast.fm', episode_id)

0 commit comments

Comments
 (0)