DELETED channels/google.png Index: channels/google.png ================================================================== --- channels/google.png +++ channels/google.png cannot compute difference between binary files DELETED channels/google.py Index: channels/google.py ================================================================== --- channels/google.py +++ channels/google.py @@ -1,168 +0,0 @@ -# -# encoding: ISO-8859-1 -# api: streamtuner2 -# title: Google stations -# description: Looks up web radio homepages from DMOZ/Google directory. -# type: channel -# category: web -# priority: deprecated -# version: 0.2 -# depends: channels, re, http -# author: Mario, original: Jean-Yves Lefort -# -# This is a plugun from streamtuner1. It has been rewritten for the -# more mundane plugin API of streamtuner2 - reimplementing ST seemed -# to much work. -# Also it has been rewritten to query DMOZ directly. Google required -# the use of fake User-Agents for access, and the structure on DMOZ -# is simpler (even if less HTML-compliant). DMOZ probably is kept -# more up-to-date as well. -# PS: we need to check out //musicmoz.org/ -# - - -# Copyright (c) 2003, 2004 Jean-Yves Lefort -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of Jean-Yves Lefort nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND -# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - - -import re, os, gtk -from channels import * -from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities -import ahttp as http - - -### constants ################################################################# - - -GOOGLE_DIRECTORY_ROOT = "http://www.dmoz.org" -CATEGORIES_URL_POSTFIX = "/Arts/Radio/Internet/" -#GOOGLE_DIRECTORY_ROOT = "http://directory.google.com" -#CATEGORIES_URL_POSTFIX = "/Top/Arts/Music/Sound_Files/MP3/Streaming/Stations/" -GOOGLE_STATIONS_HOME = GOOGLE_DIRECTORY_ROOT + CATEGORIES_URL_POSTFIX - -"""
  • Jazz""" -re_category = re.compile('()([^:]+?)()@', re.I|re.M) - -#re_stream = re.compile('^(.*)') -#re_description = re.compile('^
    (.*?)') -"""
  • Atlanta Blue Sky - Rock and alternative streaming audio. Live real-time requests.""" -re_stream_desc = re.compile('
  • \s*([^<>]+)\s*( - )?\s*([^<>\n\r]+)', re.M|re.I) - - -###### - - -# Google Stations is actually now DMOZ Stations -class google(ChannelPlugin): - - # description - title = "Google" - module = "google" - homepage = GOOGLE_STATIONS_HOME - - # config data - config = [ -# {"name": "theme", "type": "text", "value":"Tactile", "description":"Streamtuner2 theme; no this isn't a google-specific option. But you know, the plugin options are a new toy."}, -# {"name": "flag2", "type": "boolean", "value":1, "description":"oh see, an unused checkbox"} - ] - - - # category map - categories = ['Google/DMOZ Stations', 'Alternative', 'Ambient', 'Classical', 'College', 'Country', 'Dance', 'Experimental', 'Gothic', 'Industrial', 'Jazz', 'Local', 'Lounge', 'Metal', 'New Age', 'Oldies', 'Old-Time Radio', 'Pop', 'Punk', 'Rock', '80s', 'Soundtracks', 'Talk', 'Techno', 'Urban', 'Variety', 'World'] - catmap = [('Google/DMOZ Stations', '__main', '/Arts/Radio/Internet/'), ['Alternative', 'Alternative', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Alternative/'], ['Ambient', 'Ambient', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Ambient/'], ['Classical', 'Classical', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Classical/'], ['College', 'College', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/College/'], ['Country', 'Country', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Country/'], ['Dance', 'Dance', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Dance/'], ['Experimental', 'Experimental', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Experimental/'], ['Gothic', 'Gothic', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Gothic/'], ['Industrial', 'Industrial', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Industrial/'], ['Jazz', 'Jazz', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Jazz/'], ['Local', 'Local', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Local/'], ['Lounge', 'Lounge', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Lounge/'], ['Metal', 'Metal', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Metal/'], ['New Age', 'New Age', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/New_Age/'], ['Oldies', 'Oldies', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Oldies/'], ['Old-Time Radio', 'Old-Time Radio', '/Arts/Radio/Formats/Old-Time_Radio/Streaming_MP3_Stations/'], ['Pop', 'Pop', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Pop/'], ['Punk', 'Punk', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Punk/'], ['Rock', 'Rock', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/'], ['80s', '80s', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/80s/'], ['Soundtracks', 'Soundtracks', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Soundtracks/'], ['Talk', 'Talk', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Talk/'], ['Techno', 'Techno', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Dance/Techno/'], ['Urban', 'Urban', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Urban/'], ['Variety', 'Variety', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Variety/'], ['World', 'World', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/World/']] - - - #def __init__(self, parent): - # #self.update_categories() - # ChannelPlugin.__init__(self, parent) - - - # refresh category list - def update_categories(self): - - # interim data structure for categories (label, google-id/name, url) - categories = [ - ("Google/DMOZ Stations", "__main", "/Arts/Radio/Internet/"), - ] - - # fetch and extract list - html = http.get(GOOGLE_DIRECTORY_ROOT + CATEGORIES_URL_POSTFIX) - - for row in re_category.findall(html): - if row: - name = entity_decode(row[2]) - label = name - - href = entity_decode(row[0]) - if href[0] != "/": - href = CATEGORIES_URL_POSTFIX + href - - categories.append([label, name, href]) - - # return - self.catmap = categories - self.categories = [x[0] for x in categories] - pass - # actually saving this into _categories and _catmap.json would be nice - # ... - - - - # download links from dmoz listing - def update_streams(self, cat, force=0): - - # result list - ls = [] - - # get //dmoz.org/HREF for category name - try: - (label, name, href) = [x for x in self.catmap if x[0]==cat][0] - except: - return ls # wrong category - - # download - html = http.get(GOOGLE_DIRECTORY_ROOT + href) - - # filter - for row in re_stream_desc.findall(html): - - if row: - row = { - "homepage": entity_decode(row[0]), - "title": entity_decode(row[1]), - "playing": entity_decode(row[3]), - } - ls.append(row) - - - # final list for current category - return ls - - - ADDED channels/youtube.png Index: channels/youtube.png ================================================================== --- channels/youtube.png +++ channels/youtube.png cannot compute difference between binary files ADDED channels/youtube.py Index: channels/youtube.py ================================================================== --- channels/youtube.py +++ channels/youtube.py @@ -0,0 +1,317 @@ +# encoding: UTF-8 +# api: streamtuner2 +# title: Youtube +# description: Channel, playlist and video browsing for youtube. +# type: channel +# version: 0.1 +# category: video +# priority: optional +# suggests: youtube-dl +# requires: ahttp +# +# +# Lists recently popular youtube videos by category or channels. +# +# Introduces the faux MIME type "video/youtube" for player and recording +# configuration; both utilizing `youtube-dl`. But VLC can consume Youtube +# URLs directly anyhow. +# +# For now custom channel names must be configured in the settings dialog +# text entry, and applied using Channel > Update categories.. +# +# +# INTERNA +# +# The Youtube v3.0 API is quite longwinded. Here the .api() call shadows +# a few of the details. +# While .wrap3() unpacks the various variations of where the video IDs +# get hidden in the result sets. +# Google uses some quote/billing algorithm for all queries. It seems +# sufficient for Streamtuner2 for now, as the fields= JSON filter strips +# a lot of uneeded data. (Clever idea, but probably incurs more processing +# effort on Googles servers than it actually saves bandwidth, but hey..) +# +# +# EXAMPLES +# +# api("videos", chart="mostPopular") +# api("search", chart="mostPopular", videoCategoryId=10, order="date", type="video") +# api("channels", categoryId=10) +# api("search", topicId="/m/064t9", type="video") +# +# Discovery +# +# videoCat Music id= 10 +# guideCat Music id= GCTXVzaWM channelid= UCBR8-60-B28hp2BmDPdntcQ +# topicId Music mid= /m/0kpv0g +# +# + +from config import * +from channels import * + +import ahttp +import json + + + +# Youtube +class youtube (ChannelPlugin): + + # description + title = "Youtube" + module = "youtube" + homepage = "http://www.youtube.com/" + listformat = "url/youtube" + fmt = "video/youtube" + titles = dict( genre="Channel", title="Title", playing="Playlist", bitrate=False, listeners=False ) + + # API config + service = { + 2: [ "http://gdata.youtube.com/", + { + "v": 2, + "alt": "json", + "max-results": 50, + } + ], + 3: [ "https://www.googleapis.com/youtube/v3/", + { + "key": "AIzaSyAkbLSLn1VgsdFXCJjjdZtLd6W8RqtL4Ag", + "maxResults": 50, + "part": "id,snippet", + "fields": "pageInfo,nextPageToken,items(id,snippet(title,thumbnails/default/url,channelTitle))", + } + ] + } + + categories = [ + "mostPopular", + ["Music", "Comedy", "Movies", "Shows", "Short Movies", "Trailers", "Film & Animation", "Entertainment", "News & Politics", "Sci-Fi/Fantasy"], + "topics", + ["music", "pop", "music video"], + "my channels", + ["Key of Awesome", "Pentatonix"] + ] + + # plugin settings + config = [ + { + "name": "youtube_channels", + "type": "text", + "value": "Key Of Awesome, Pentatonix", + "description": "Preferred channels to list videos from.", + "category": "select", + }, + { + "name": "youtube_region", + "type": "select", + "select": "=No Region|AR=Argentina|AU=Australia|AT=Austria|BE=Belgium|BR=Brazil|CA=Canada|CL=Chile|CO=Colombia|CZ=Czech Republic|EG=Egypt|FR=France|DE=Germany|GB=Great Britain|HK=Hong Kong|HU=Hungary|IN=India|IE=Ireland|IL=Israel|IT=Italy|JP=Japan|JO=Jordan|MY=Malaysia|MX=Mexico|MA=Morocco|NL=Netherlands|NZ=New Zealand|PE=Peru|PH=Philippines|PL=Poland|RU=Russia|SA=Saudi Arabia|SG=Singapore|ZA=South Africa|KR=South Korea|ES=Spain|SE=Sweden|CH=Switzerland|TW=Taiwan|AE=United Arab Emirates|US=United States", + "value": "UK", + "description": "Filter by region id.", + "category": "auth", + }, + ] + + # from GET https://www.googleapis.com/youtube/v3/videoCategories?part=id%2Csnippet& + videocat_id = { + "Film & Animation": 1, + "Autos & Vehicles": 2, + "Music": 10, + "Pets & Animals": 15, + "Sports": 17, + "Short Movies": 18, + "Travel & Events": 19, + "Gaming": 20, + "Videoblogging": 21, + "People & Blogs": 22, + "Comedy": 34, + "Entertainment": 24, + "News & Politics": 25, + "Howto & Style": 26, + "Education": 27, + "Science & Technology": 28, + "Nonprofits & Activism": 29, + "Movies": 30, + "Anime/Animation": 31, + "Action/Adventure": 32, + "Classics": 33, + "Documentary": 35, + "Drama": 36, + "Family": 37, + "Foreign": 38, + "Horror": 39, + "Sci-Fi/Fantasy": 40, + "Thriller": 41, + "Shorts": 42, + "Shows": 43, + "Trailers": 44, + } + # Freebase topics + topic_id = { + "music": "/m/0kpv0g", + "pop": "/m/064t9", + "music video": "/m/05k5h7g", + } + + + # just a static list for now + def update_categories(self): + i = self.categories.index("my channels") + 1 + self.categories[i] = [ title.strip() for title in conf.youtube_channels.split(",") ] + + + # retrieve and parse + def update_streams(self, cat, force=0, search=None): + + entries = [] + channels = self.categories[self.categories.index("my channels") + 1] + + # Most Popular + if cat == "mostPopular": + #for row in self.api("feeds/api/standardfeeds/%s/most_popular"%conf.youtube_region, ver=2): + # entries.append(self.wrap2(row)) + for row in self.api("videos", chart="mostPopular", regionCode=conf.youtube_region): + entries.append( self.wrap3(row, {"genre": "mostPopular"}) ) + + # Categories + elif cat in self.videocat_id: + for row in self.api("search", chart="mostPopular", videoCategoryId=self.videocat_id[cat], order="date", type="video"): + entries.append( self.wrap3(row, {"genre": cat}) ) + + # Topics + elif cat in self.topic_id: + for row in self.api("search", order="date", regionCode=conf.youtube_region, topicId=self.topic_id[cat], type="video"): + entries.append( self.wrap3(row, {"genre": cat}) ) + + # My Channels + # - searches channel id for given title + # - iterates over playlist + # - then over playlistitems to get videos + elif cat in channels: + # channel id, e.g. UCEmCXnbNYz-MOtXi3lZ7W1Q + UC = self.channel_id(cat) + + # playlist + for i,playlist in enumerate(self.api("playlists", fields="items(id,snippet/title)", channelId=UC, maxResults=15)): + + # items (videos) + for row in self.api("playlistItems", playlistId=playlist["id"], fields="items(snippet(title,resourceId/videoId,description))"): + entries.append(self.wrap3(row, {"genre": cat, "playing": playlist["snippet"]["title"]})) + + self.update_streams_partially_done(entries) + self.parent.status(i / 15.0) + + # plain search request for videos + elif search is not None: + for row in self.api("search", type="video", regionCode=conf.youtube_region, q=search): + entries.append( self.wrap3(row, {"genre": ""}) ) + + # empty entries + else: + entries = [dict(title="Placeholder for subcategories", genre="./.", playing="./.", url="http://youtube.com/")] + + # done + return entries + + + + # Search for channel name: + def channel_id(self, title): + id = self.channel2id.get(title) + if not id: + data = self.api("search", part="id", type="channel", q=title) + if data: + id = data[0]["id"]["channelId"] + self.channel2id[title] = id + return id + channel2id = {} + + + + #-- Retrieve Youtube API query results + # + def api(self, method, ver=3, pages=5, **params): + items = [] + + # URL and default parameters + (base_url, defaults) = self.service[ver] + params = dict(defaults.items() + params.items()) + + # Retrieve data set + while pages > 0: + j = ahttp.get(base_url + method, params=params) + __print__(dbg.DATA, j) + if j: + # json decode + data = json.loads(j) + + # extract items + if "items" in data: + items += data["items"] + elif "feed" in data: + items += data["feed"]["entry"] + else: + pages = 0 + + # Continue to load results? + if items >= conf.max_streams: + pages = 0 + elif "pageInfo" in data and data["pageInfo"]["totalResults"] < 50: + pages = 0 + elif "nextPageToken" in data: + params["pageToken"] = data["nextPageToken"] + pages -= 1 + else: + pages = 0 + self.parent.status(pages / 10.0) + + return items + + + + # Wrap API 3.0 result into streams row + def wrap3(self, row, data): + + # Video id + if "id" in row: + # plain /video queries + id = row["id"] + # for /search queries + if type(row["id"]) is dict: + id = id["videoId"] + # for /playlistItems + elif "resourceId" in row["snippet"]: + id = row["snippet"]["resourceId"]["videoId"] + + data.update(dict( + url = "http://youtube.com/v/" + id, + homepage = "https://youtube.com/watch?v=" + id, + format = self.fmt, + title = row["snippet"]["title"], + )) + + # optional values + if "playing" not in data: + data["playing"] = row["snippet"]["channelTitle"] + if "description" in row["snippet"]: + data["description"] = row["snippet"]["description"], + + return data + + + # API version 2.0s jsonified XML needs different unpacking: + def wrap2(self, row): + __print__(dbg.DATA, row) + return dict( + genre = row["category"][1]["term"], + title = row["title"]["$t"], + playing = row["author"][0]["name"]["$t"], + format = self.fmt, + url = row["content"]["src"].split("?")[0], + homepage = row["media$group"]["media$player"]["url"], + image = row["media$group"]["media$thumbnail"][0]["url"], + ) + + +