Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
77a961e
Fixed station and show name grabbing
chrisfu Jul 31, 2015
6079455
Corrected xpaths for show detail parsing
chrisfu Aug 3, 2015
25093f1
Fixed the case when show_id is missing
solvek Jan 12, 2016
5619b65
Merge pull request #1 from solvek/master
chrisfu Jan 14, 2016
1c53468
1. Added art 2. Extended readme 3. Tried to support more urls from se…
solvek Jan 15, 2016
77c7691
Reverted some code
solvek Jan 15, 2016
568390f
Listing user's stations
solvek Jan 15, 2016
5eac2e8
1. Fixed wordings in readme 2. Updated art image
solvek Jan 15, 2016
922b68d
Removed unnecessary http call
solvek Jan 15, 2016
e4d2f78
Completely rewrote service. Now it is much more reliable, faster and …
solvek Jan 16, 2016
a2c730f
Rewrote station listing
solvek Jan 16, 2016
d4cfa8b
Normalized url version
solvek Jan 16, 2016
c006fba
Commented url normalizing
solvek Jan 16, 2016
98b2736
Fixed the case when my stations showed too many times
solvek Jan 16, 2016
cfbfed9
Removed unneeded image
solvek Jan 16, 2016
20b21b0
Better work with json
solvek Jan 16, 2016
572d508
Merge pull request #1 from chrisfu/master
solvek Jan 16, 2016
a6e0e78
Playing castom streams
solvek Jan 18, 2016
68c54f1
Merge branch 'master' of github.com:solvek/TuneIn.bundle
solvek Jan 18, 2016
71e8414
Fixed playing of podcasts/programs
solvek Jan 18, 2016
f7186b3
Better art
solvek Jan 18, 2016
389c386
Merge pull request #3 from solvek/master
chrisfu Jan 18, 2016
d269277
Small fix in readme
solvek Jan 22, 2016
21ebe6b
Changed streams requesting form
solvek Feb 1, 2016
8ff4b5b
Merge pull request #2 from chrisfu/master
sander1 Feb 1, 2016
5d80d9b
Convert icon to jpeg; scale artwork to 1280x720.
sander1 Feb 1, 2016
3560588
Merge branch 'master' of github.com:plexinc-plugins/TuneIn.bundle
solvek Feb 1, 2016
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
119 changes: 87 additions & 32 deletions Contents/Code/__init__.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,112 @@
ROOT_MENU = 'http://opml.radiotime.com/Browse.ashx?formats=mp3,aac'

ART = 'art-default.jpg'
ICON = 'icon-default.jpg'

# STATION_URL = 'http://tunein.com/radio/-%s/'
USER_URL = 'http://opml.radiotime.com/Browse.ashx?c=presets&partnerId=RadioTime&username=%s'
MY_STATIONS = 'tunein://mystations'
CUSTOM_URL_PREFIX = 'tunein://play?'

####################################################################################################
def Start():

ObjectContainer.title1 = 'TuneIn'
HTTP.CacheTime = 300
ObjectContainer.art = R(ART)
DirectoryObject.art = R(ART)
DirectoryObject.thumb = R(ICON)

####################################################################################################
def ValidatePrefs():

return True

####################################################################################################
@handler('/music/tunein', 'TuneIn')
@route('/music/tunein/menu')
def Menu(url=ROOT_MENU, title='', outline_text=''):
def Menu(url=ROOT_MENU, title='', xml = None):

oc = ObjectContainer(title2=title)
root = XML.ElementFromURL(url).xpath('//body')[0]

if len(root.xpath('./outline[@URL and not(@type="audio")]')) > 0:
for item in root.xpath('./outline[@URL]'):
if url == ROOT_MENU and not xml:
my_stations = L('My Stations')
oc.add(DirectoryObject(
key = Callback(Menu, url=MY_STATIONS, title=my_stations),
title = my_stations
))
oc.add(PrefsObject(
title = L('Preferences...'),
)
)

oc.add(DirectoryObject(
key = Callback(Menu, url=item.get('URL'), title=item.get('text')),
title = item.get('text'),
thumb = Resource.ContentsOfURLWithFallback('')
))
if url == MY_STATIONS:
username = Prefs['username']

if len(root.xpath('./outline[@text and not(@URL) and not(@key="related")]')) > 0 and outline_text == '':
for item in root.xpath('./outline[@text and not(@URL) and not(@key="related")]'):
if not username:
oc.message = L('Please specify user name in the TuneIn preferences')
return oc

if item.get('text') == 'This program is not available':
continue
url = USER_URL % username

oc.add(DirectoryObject(
key = Callback(Menu, url=url, title=item.get('text'), outline_text=item.get('text')),
title = item.get('text'),
thumb = Resource.ContentsOfURLWithFallback('')
))
if xml:
subitems = XML.ElementFromString(xml)
else:
subitems = XML.ElementFromURL(url).xpath('//body')[0]

# Log.Debug('Subitems: '+str(subitems))
Log.Debug('Subitems count: '+str(len(subitems)))

if outline_text != '':
for item in root.xpath('./outline[@text="%s"]/outline' % outline_text):
for item in subitems:
typ = item.get('type')
local_url = item.get('URL')
text = item.get('text')
image = item.get('image')
key = item.get('key')
subtext = item.get('subtext')
# station_id = item.get('guide_id')
itemAttr = item.get('item')

if item.get('type') == 'link':

oc.add(DirectoryObject(
key = Callback(Menu, url=item.get('URL'), title=item.get('text')),
title = item.get('text'),
thumb = Resource.ContentsOfURLWithFallback(item.get('image'))
))
if key in ['unavailable', 'related']:
continue

elif item.get('type') == 'audio':
if itemAttr == 'url':
data = {
'url' : local_url,
'title': text,
'summary': subtext,
'image': image
}

oc.add(TrackObject(
url = item.get('URL'),
title = item.get('text'),
thumb = Resource.ContentsOfURLWithFallback(item.get('image'))
))
oc.add(TrackObject(
url = CUSTOM_URL_PREFIX+String.Encode(JSON.StringFromObject(data)),
title = text,
summary = subtext,
source_title = 'TuneIn',
thumb = Resource.ContentsOfURLWithFallback(image)
))
elif typ == 'audio':
oc.add(TrackObject(
url = local_url,
# url = STATION_URL % station_id,
title = text,
summary = subtext,
source_title = 'TuneIn',
thumb = Resource.ContentsOfURLWithFallback(image)
))
elif typ == 'link':
oc.add(DirectoryObject(
key = Callback(Menu, url=local_url, title=text),
title = text,
thumb = Resource.ContentsOfURLWithFallback('')
))
else:
# Log.Debug('Current item: '+str(item))
oc.add(DirectoryObject(
key = Callback(Menu, title=text, xml = XML.StringFromElement(item)),
title = text,
thumb = Resource.ContentsOfURLWithFallback('')
))

return oc
8 changes: 8 additions & 0 deletions Contents/DefaultPrefs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"id": "username",
"type": "text",
"label": "TuneIn username",
"default": ""
}
]
Binary file added Contents/Resources/art-default.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Contents/Resources/icon-default.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions Contents/Services/ServiceInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<key>URLPatterns</key>
<array>
<string>^http://opml\.radiotime\.com/Tune\.ashx</string>
<string>^http://tunein\.com/radio/.*-(s|p)\d+/$</string>
<string>^tunein://</string>
</array>
</dict>
</dict>
Expand Down
184 changes: 134 additions & 50 deletions Contents/Services/URL/TuneIn/ServiceCode.pys
Original file line number Diff line number Diff line change
@@ -1,82 +1,166 @@
DETAILS_URL = 'http://opml.radiotime.com/Describe.ashx?id=%s'
STATION_DETAILS_URL = 'http://opml.radiotime.com/Describe.ashx?c=nowplaying&id=%s'
URL_STATION = 'http://tunein.com/radio/-%s/'

RE_ID = Regex('id=(?P<id>(s|p)\d+)')
RE_ENTRIES = Regex('NumberOfEntries=(?P<entries>\d+)')
RE_STREAM = Regex('File%s=(?P<stream>.*)$')
RE_IDS = [
Regex('-(?P<id>(s|p)\d+)/'),
Regex('id=(?P<id>(s|p)\d+)')
]

CUSTOM_URL_PREFIX = 'tunein://play?'

####################################################################################################
def MetadataObjectForURL(url):

station_id = RE_ID.search(url).group('id')
station_details_page = XML.ElementFromURL(STATION_DETAILS_URL % station_id)

station = station_details_page.xpath("//outline[@key='station']/text()")[0]

show_id = station_details_page.xpath("//outline[@key='show']/@guide_id")[0]
show_detail_page = XML.ElementFromURL(DETAILS_URL % show_id)
Log.Debug("Requesting metadata for url: %s", url)

try: title = show_detail_page.xpath("//show/title/text()")[0]
except: title = None
customStream = ParseCustomUrl(url)

try: hosts = show_detail_page.xpath("//show/hosts/text()")[0]
except: hosts = None
if customStream:
title = customStream['title']
summary = customStream['summary']
playing = summary
image_url = customStream['image']
else:
station = TuneInStation(url)

try: thumb = show_detail_page.xpath("//show/logo/text()")[0]
except: thumb = ''

try: genre = show_detail_page.xpath("//show/genre_id/text()")[0]
except: genre = None
title = station.title
summary = station.title
playing = station.playing
image_url = station.image_url

return TrackObject(
artist = station,
album = title,
title = hosts,
thumb = Resource.ContentsOfURLWithFallback(thumb)
artist = title,
album = playing,
title = summary,
summary = playing,
source_title = 'TuneIn',
thumb = Resource.ContentsOfURLWithFallback(image_url)
)

####################################################################################################
def MediaObjectsForURL(url):

return [
MediaObject(
parts = [
PartObject(key = Callback(PlayTrack, url=url))
]
)
]
return [MediaObject(
# container = container,
# audio_codec = audio_codec,
# bitrate = bitrate,
# audio_channels = 2,
parts = [PartObject(key = Callback(PlayTrack, url=url))]
)]

####################################################################################################
def PlayTrack(url):

details = XML.ElementFromURL(url + '&c=ebrowse')
Log(XML.StringFromElement(details))
customStream = ParseCustomUrl(url)

if customStream:
return Redirect(customStream['url'])

station = TuneInStation(url)

stream_url = details.xpath("//outline[@type='audio']/@URL")[0]
Log(stream_url)
best_reliability = -1
for stream in station.loadStreams():
reliability = int(stream['Reliability'])
if reliability > best_reliability:
best_reliability = reliability
stream_url = stream['Url']

Log(HTTP.Request(stream_url).content.strip())
return Redirect(HTTP.Request(stream_url).content.strip())
# stream_url = HTTP.Request(m3u8).content.strip()

# Log("m3u8 url: %s, audio stream: %s" %(m3u8, stream_url))

return Redirect(stream_url)

####################################################################################################
def parse_m3u(content):
def GetStationId(url):

streams = []
for re in RE_IDS:
m = re.search(url)
if m:
return m.group('id')

for item in content.split('\n'):
return None

if item.startswith('#') == False:
streams.append(item.strip())
####################################################################################################
def UrlRequest(url):

return streams
return HTTP.Request(
url,
headers={
'x-requested-with': 'XMLHttpRequest',
'Accept' : 'application/json, text/javascript, */*; q=0.01'}
).content

####################################################################################################
def parse_pls(content):
def ParseCustomUrl(url):

if not url.startswith(CUSTOM_URL_PREFIX):
return None

param = url[len(CUSTOM_URL_PREFIX):]

# Log.Debug("Url: %s, param encoded: %s", url, param)

s = String.Decode(param)
# Log.Debug("param decoded: %s", s)

return JSON.ObjectFromString(s)

####################################################################################################
class TuneInStation:

def __init__(self, url='', id=''):
if not id:
id = GetStationId(url)

text = UrlRequest(URL_STATION % id)

# Log.Debug("Requested url: %s, response: %s", url, text)

payload = JSON.ObjectFromString(text)['payload']

if 'Station' in payload:
self.js = payload['Station']
else:
self.js = payload['Program']

self.broadcast = self.js['broadcast']
self.echo = self.broadcast['EchoData']

# Log.Debug('Station id: %s, json: %s' % (id, self.js))
# print('Station id: %s, json: %s' % (id, self.js))

@property
def id(self):
return self.echo['targetGuideId']

@property
def title(self):
return self.broadcast['Title']

@property
def description(self):
return self.js['description']

@property
def image_url(self):
return self.broadcast['Logo']

@property
def summary(self):
return self.echo['subtitle']

@property
def playing(self):
return self.broadcast['SongPlayingTitle']

streams = []
entries = RE_ENTRIES.search(content).group('entries')
@property
def stream_url(self):
url = self.broadcast['StreamUrl']
if url.startswith('//'):
url = 'http:'+url;
return url

for i in range(1, entries+1):
streams.append(RE_STREAM.search(content).group('stream'))
def loadStreams(self):
text = UrlRequest(self.stream_url)[2:-2]
return JSON.ObjectFromString(text)['Streams']

return streams
Loading