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"],
+ )
+
+
+
|