import groovy.json.JsonSlurper import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.util.Spliterators.AbstractDoubleSpliterator import java.util.concurrent.ForkJoinPool.DefaultForkJoinWorkerThreadFactory import org.serviio.library.metadata.* import org.serviio.library.online.* import org.serviio.util.* /** * WebResource extractor plugin for tunein.com. * * @author Petr Nejedly * */ class TuneIn extends WebResourceUrlExtractor { final VALID_FEED_URL = '^(?:https?://)?(?:www\\.)?tunein\\.com/(radio/|search/\\?query=).+' String getExtractorName() { return 'Tune In' } boolean extractorMatches(URL feedUrl) { return feedUrl ==~ VALID_FEED_URL } int getVersion() { 8 } String urlGetText(URL url) { String html = url.getText() for (int i=0; i<10; ++i) { //sometimes redirected to tune in beta site??? if (html.startsWith("Object moved")) { Thread.currentThread().sleep(500); html = url.getText() } else { return html; } } return html } WebResourceContainer extractItems(URL resourceUrl, int maxItems) { String html = urlGetText(resourceUrl) def titleMatcher = html =~ '(?s)([^<|]+)' String pageTitle = titleMatcher[0][1].trim() //println "pagetitle = "+pageTitle def stations = html.split(/(?s)\s*/).find { it.startsWith("Stations") } List items = [] String rowDelim = '
]+>([^<]+)' title = stationMatcher[0][1] //println title } if (url.contains("-s")) { WebResourceItem item = new WebResourceItem(title: title, additionalInfo: ['stationUrl':url]) items << item } } } return new WebResourceContainer(title: pageTitle, items: items) } ContentURLContainer extractUrl(WebResourceItem item, PreferredQuality requestedQuality) { String stationTitle = item.title String stationUrl = item.getAdditionalInfo()['stationUrl'] assert stationUrl != null def idMatcher = stationUrl =~ '-(s\\d+)'; def id = idMatcher[0][1]; def url = "https://opml.radiotime.com/Tune.ashx?id=${id}&render=json&formats=mp3,aac,ogg&partnerId=RadioTime"; def json = new JsonSlurper().parse(new URL(url)) //println(json) Map jsonResult = (Map) json; List streams = jsonResult['body'] streams = streams.findAll { it -> it['element'] == 'audio' && it['url'].startsWith('http') } streams = streams.sort { it -> it['bitrate'].toInteger() } Map selectedStream = streams.last() def streamUrl = selectedStream.url if (selectedStream['playlist_type']) streamUrl = getUrlFromPlaylist(new URL(selectedStream.url)); return new ContentURLContainer(fileType: MediaFileType.AUDIO, contentUrl: streamUrl, live: true) } private String getStreamUrl(Map jsonResult) { List streams = jsonResult['Streams'] // only get streams that are represented as playlists and are live streams streams = streams.findAll { it -> it['Type'] == 'Live' && ['MP3','Windows','AAC','Flash'].contains(it['MediaType']) && it['Url'].indexOf('adType') == -1 } if( streams.size() > 0 ) { // ignore quality, audio bitrate is too low to make any difference, deliver the best available streams = streams.sort { it -> it['Bandwidth'].toInteger() } Map selectedStream = streams.last() String streamUrl = selectedStream['Url'] boolean hasPlaylist = Boolean.valueOf (selectedStream['HasPlaylist']) if(hasPlaylist && streamUrl.startsWith("http")) { return getUrlFromPlaylist(new URL(streamUrl)) } else { return streamUrl } } else { return null } } /** * Supports m3u playlists ATM */ protected getUrlFromPlaylist(URL playlistUrl) { assert playlistUrl != null String playlist = playlistUrl.getText() if(playlist.toLowerCase().startsWith(" -1 || url.indexOf('.pls') > -1) { return getUrlFromPlaylist(new URL(url)) } return url } } static void main(args) { // this is just to test TuneIn extractor = new TuneIn() if (args.length == 1) { WebResourceContainer container = extractor.extractItems(new URL(args[0]), 5) println container container.getItems().each { ContentURLContainer result = extractor.extractUrl(it, PreferredQuality.MEDIUM) println result } System.exit(0); } assert extractor.extractorMatches( new URL("https://tunein.com/radio/London-United-Kingdom-r100780/?qlc=1") ) assert !extractor.extractorMatches( new URL("http://google.com/feeds/api/standardfeeds/top_rated?time=today") ) WebResourceContainer container = extractor.extractItems( new URL("https://tunein.com/radio/Capital-London-958-s16534/"), -1) // with embedded streams //WebResourceContainer container = extractor.extractItems( new URL("https://tunein.com/radio/London-United-Kingdom-r100780/?qlc=1"), -1) // with embedded streams //WebResourceContainer container = extractor.extractItems( new URL("http://tunein.com/radio/search/05301/"), -1) // with external streams //WebResourceContainer container = extractor.extractItems( new URL("http://tunein.com/radio/Christmas-Holiday-Favorites-s257618/"), -1) //WebResourceContainer container = extractor.extractItems( new URL("http://tunein.com/radio/Christmas-Radio-s249952/"), -1) //WebResourceContainer container = extractor.extractItems( new URL("https://tunein.com/search/?query=kiss"), -1) //WebResourceContainer container = extractor.extractItems( new URL("http://tunein.com/search/?query=doowop"), -1) //WebResourceContainer container = extractor.extractItems( new URL("https://tunein.com/radio/Radio-Mizrahit-s171349/"), -1) //WebResourceContainer container = extractor.extractItems( new URL("https://tunein.com/radio/RussianFM-s118244/"), -1) //WebResourceContainer container = extractor.extractItems( new URL("https://tunein.com/radio/Non-Stop-Radio-1030-s14201/"), -1) println container container.getItems().each { ContentURLContainer result = extractor.extractUrl(it, PreferredQuality.MEDIUM) println result } } }