ADDED channels/peertube.py Index: channels/peertube.py ================================================================== --- channels/peertube.py +++ channels/peertube.py @@ -0,0 +1,234 @@ +# encoding: UTF-8 +# api: streamtuner2 +# title: Peertube +# description: Video browser for Peertube servers. +# type: channel +# version: 0.2 +# priority: extra +# url: https://joinpeertube.org/ +# category: video +# config: +# { name: peertube_srv, type: select, value: "peertube.live", select: "peertube.live|peertube.anarchmusicall.net|hostyour.tv|peertube.co.uk|troo.tubevideo.ploud.fr|video.ploud.fr|peertube.bittube.tv|bittube.video|peertube.video|libretube.net", description: "Primary instance/server to query", category: server } +# priority: default +# png: +# iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAMAAABhEH5lAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3Ccul +# E8AAABO1BMVEUAAAAhHyAnIiAjICDxaA3uZw0eGx13d3dzc3Nwc3YhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAiHyAgHyA5KB7cYQ//bQwe +# HiCbShXwaA3xaA3xaA0hHyAhHyAhHyAAACn2ag3xaA3xaA3xaA3xaA3xaA0gHh8hHyDxaA3xaA3xaA3xaA3xaA0sKislIyQfHR4dGxzxaA3xaA3xaA1paWlubm +# 50dHR5eXnxaA3xaA10dHRzc3Nzc3Nzc3Nzc3PxaA3xaA3xaA1zc3Nzc3Nzc3Nzc3M4eKPzaAvxaA3xaA1zc3Nxc3S9bDfxaA3xaA3xaA1zc3ODcmbkaRf/ZwBz +# c3Nzc3Nyc3RtdHpzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3MhHyDxaA1zc3MAAABrSqpYAAAAZXRSTlMAAAAAAAAAAAAAA4ROBgK56YIX/EXoGfaNOAd+P8 +# SFF/u7SAQl2/u6RwQC7Sba6oMZA31IBfrAQwODRAXDPQK56oMa5oAcAvu7QwQl/L/nhEDEgRr1kjYE6XwdAfq9RwV9FwOGUtq6k/IAAAABYktHRACIBR1IAAAA +# CXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH5AUPCjUlNSD/NQAAAMJJREFUGNNjYODi5uFlZEABfPwCgkKMjKhCqcL8IigKgUKpqaLcYkzMqEKp4hKSUiwoQg +# LSMrJy8iwscCEFRSVlFVU1dQ1NiJgWvzbQRlYd3bQ0PX0DsEJDI2MTNgYWkFCaqZk5SMjC0sqaHSqkZmMLErKzd3B04gALObu4gjW62ae7e3h6eev6+MKMBwql +# p/v5BwQGBcMcARZKDwkNC4c7FSwUERkVzcmAJBQTGxfPgeRtN3uHhERkAQaGpOQUFCUMAJsmJ8NW/rFWAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIwLTA1LTE1VD +# A4OjUzOjM3KzAyOjAw7qvI1wAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMC0wNS0xNVQwODo1MzozNyswMjowMJ/2cGsAAAAASUVORK5CYII= +# depends: bin:youtube-dl +# extraction-method: json +# +# +# Browser for PeerTube instances (configurable in settings) and +# video categories. Even loads video snapshots as favicons, hencewhy +# the display is slightly stretched in comparison to radio channels. +# +# The chosen main server has some influence over sorting, but usually +# includes entries from peered instances. +# +# Currently there's no player for PeerTube /embed/ links, so the +# video/youtube MIME should be combined with `youtuble-dl` even +# for VLC. Else just use the [Station] homepage for the web view. +# +# Per default fetches 200 entries (not conf.max_streams), because +# it's a little slower on some servers. And the standard sorting +# is publication time (newest videos atop). +# + + +from config import * +from channels import * + +import ahttp +import json +import re + + + +# Peertube +# +# /video queries are public, so we don't even need OAuth for most +# of the instances. +# The API and resultsets are also quite simple to work with. +# Apart from the /embed/ url not being useful for direct playback. +# +# API +# +# · https://peer.tube/api/v1/oauth-clients/local +# {"client_id":"3ouzl1…","client_secret":"oLFjYTCjw…"} +# +# · https://peer.tube/api/v1/videos/categories +# {"1":"Music","2":"Films","3":"Vehicles","4":"Art","5":"Sports",…} +# +# · https://peer.tube/api/v1/videos?categoryOneOf=1 +# data[ +# { +# "publishedAt" : "2020-05-10T07:01:18.843Z", +# "embedPath" : "/videos/embed/b60bae06-82bf-4925-b1ef-b8fc6903ae28", +# "originallyPublishedAt" : "2014-03-31T07:00:42.000Z", +# "nsfw" : false, +# "language" : { +# "id" : "te", +# "label" : "Telugu" +# }, +# "createdAt" : "2020-05-10T07:01:18.843Z", +# "privacy" : { +# "label" : "Public", +# "id" : 1 +# }, +# "id" : 184339, +# "views" : 1, +# "duration" : 349, +# "thumbnailPath" : "/static/thumbnails/b60bae06-82bf-4925-b1ef-b8fc6903ae28.jpg", +# "description" : "Lyrics : Lakshmi Valli Devi Bijibilla\nMusic : Kanakesh Rathod\nPublisher : Bijibilla Rama Rao ", +# "dislikes" : 0, +# "uuid" : "b60bae06-82bf-4925-b1ef-b8fc6903ae28", +# "likes" : 0, +# "category" : { +# "label" : "Music", +# "id" : 1 +# }, +# "updatedAt" : "2020-05-10T08:02:35.008Z", +# "licence" : { +# "id" : 7, +# "label" : "Public Domain Dedication" +# }, +# "channel" : { +# "avatar" : null, +# "url" : "https://peertube.slat.org/video-channels/sudhanva_sankirtanam", +# "displayName" : "Sudhanva Sankirtanam", +# "id" : 16607, +# "name" : "sudhanva_sankirtanam", +# "host" : "peertube.slat.org" +# }, +# "isLocal" : false, +# "name" : "Alayam Devaalayam-Kanakesh Rathod", +# "account" : { +# "id" : 25475, +# "name" : "bijibilla_rama_rao", +# "host" : "peertube.slat.org", +# "displayName" : "Bijibilla Rama Rao", +# "url" : "https://peertube.slat.org/accounts/bijibilla_rama_rao", +# "avatar" : null +# }, +# "previewPath" : "/static/previews/b60bae06-82bf-4925-b1ef-b8fc6903ae28.jpg" +# }, +# +# · peertube.mygaia.org/videos/embed/f1208e4f-425f-473b-9449-bc168798d604 +# +# INTERNA +# +# The /embed/ section of the url can sometimes be substituted with: +# · /videos/watch/UUID +# · /static/streaming-playlists/hls/UUID/master.m3u8 +# · /static/webseed/UUID.mp4 +# Though that's sometimes blocked / or not consistently supported on all instances. +# Which is why resoslve_urn does an extra /api/v1/videos/uuid lookup. +# +class peertube (ChannelPlugin): + + # control attributes + listformat = "srv" + has_search = True + audioformat = "video/youtube" + titles = dict( genre="Channel", title="Title", playing="Description", bitrate=False, listeners=False ) + srv = conf.peertube_srv + image_resize = 48 + fixed_size = [48,32] + + categories = [] + catmap = { "Music": "1" } + + + # just a static list for now + def update_categories(self): + cats = self.api("videos/categories") # { "1": "Music" } + self.catmap = dict((k,int(v)) for v,k in cats.items()) # { "Music": 1 } + self.categories = sorted(self.catmap, key=self.catmap.get) # sort by value + + + # retrieve and parse + def update_streams(self, cat, search=None): + if search: + params = { + "tagsOneOf": search, + "count": 100, + "sort": "-name", + "nsfw": "false" + } + elif not cat in self.catmap: + return [] + elif cat: + params = { + "categoryOneOf": self.catmap[cat], + "count": 100, + "sort": "-publishedAt", + "nsfw": "false" + } + + # fetch + map + entries = [] + for video in self.api("videos", params): + #log.DATA(video) + entries.append(self.map_data(video)) + #log.EN(json.dumps(entries, indent=4)) + return entries + + # peertube entry to streamtunter2 dict + def map_data(self, v): + url = "http://" + v["channel"]["host"] + return dict( + uuid = v["uuid"], + genre = v["category"]["label"], + title = v["name"], + playing = re.sub("\s+", " ", v["description"]) if v.get("description") else "", + url = "urn:peertube:{}".format(v["uuid"]), + homepage = url + v["embedPath"].replace("/embed/", "/watch/"), + #homepage = v["channel"]["url"], + format = self.audioformat, + img = url + v["thumbnailPath"] + ) + + + # fetch one or multiple pages from API + def api(self, method, params={}, debug=False, count=200, **kw): + r = [] + for i in range(0, 5): + add = json.loads( + ahttp.get("http://{}/api/v1/{}".format(conf.peertube_srv, method), params, **kw) + ) + if not add.get("data"): + return add + else: + r += add["data"] + params["start"] = 100 * (i+1); + if len(r) >= count: + break + return r + + + # from uuid to downloadUrl + def resolve_urn(self, row): + video = self.api("videos/" + row["uuid"]) + #log.DATA(video) + search = ( + ("streamingPlaylists", "playlistUrl", "audio/mpeg-url"), + ("files", "fileDownloadUrl", "video/mpeg"), + ("files", "fileUrl", "video/mpeg") + ) + for section, entry, format in search: + for row in video.get(section, []): + if row.get(entry): + row["url"] = row[entry] + row["format"] = format + return row + # else use /watch/ url + row["url"] = row["homepage"] + return row + DELETED channels/youtube.py Index: channels/youtube.py ================================================================== --- channels/youtube.py +++ channels/youtube.py @@ -1,333 +0,0 @@ -# encoding: UTF-8 -# api: streamtuner2 -# title: Youtube -# description: Channel, playlist and video browsing for youtube. -# type: channel -# version: 0.3 -# url: http://www.youtube.com/ -# category: video -# 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: GB, description: "Filter by region id.", category: auth } -# { name: youtube_wadsworth, type: boolean, value: 0, description: "Apply Wadsworth constant.", category: filter } -# priority: default -# png: -# iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAYNJREFUOI3Fks9LVFEUxz/nzrPx+WN0xhAUgoT6A6y/wFb+C4IbIQhcBm36H1obVNtoGYS0TFoIQstazBgNBaELQdTx -# vea9uffbwufw3mTRzi8cLnzv955z7vccuG7YXmtyBlgBbgFTQB3Q3/RAHzgD9oHdyMNTg01gshD8DwScCJ7bx+bEN7Cl0Xt5D2aYc//Iq67LYDFHXEamgGZmmd94SHzvPoMoIguerKQZamExykS9kjQIN3eThcdP -# WAiBo/fbHLx5Te/LZzQYgFW6qbsMKEcf+CWRpCm+2aK5ts6drZfMP9okH4/pSxV91NeI4RLmA0mS4ns9JHGaJvzMc1Lpwo3Smyi7wl6FwHmScNzt8mPnA4fv3lLrtJkIHqt+AXvViFPB+JCQ0HQDrTyg127jvu4T -# D3Jqzg0LDLWQ2lYj7oDulmlJZCEwZuD+GGMlRae2eiNqeVgOUA9AAAuAmSEzCq4cKs5TwYvIwzPBJ+A2F2s8XZQcXedL7qwY1neDHa4dvwFfDLdx6YbozgAAAABJRU5ErkJggg== -# depends: bin:youtube-dl -# extraction-method: json -# -# -# 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.. - - -from config import * -from channels import * - -import ahttp -import json - - - -# Youtube -# -# -# 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 -# -class youtube (ChannelPlugin): - - # control attributes - listformat = "url/youtube" - has_search = True - audioformat = "video/youtube" - titles = dict( genre="Channel", title="Title", playing="Playlist", bitrate=False, listeners=False ) - - # API config - service = { - 2: [ "http://gdata.youtube.com/", - # deprecated on 2015-04-20, no /v3/ alternative, pertains "mostPopular" category only - { - "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", "Trailers", "Film & Animation", "Entertainment", "News & Politics"], - "topics", - ["Pop", "Billboard charts", "Rock", "Hip Hop", "Classical", "Soundtrack", "Ambient", - "Jazz", "Blues", "Soul", "Country", "Disco", "Dance", "House", "Trance", "Techno", "Electronica"], - "my channels", - ["Key of Awesome", "Pentatonix"] - ] - # 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 = { - "pop": "/m/064t9", - "billboard charts": "/m/04qf57", - "rock": "/m/06by7", - "dance": "/m/0ggx5q", - "classical": "/m/0ggq0m", - "hip hop": "/m/0glt670", - "soundtrack": "/m/0l14gg", - "ambient": "/m/0fd3y", - "electronica": "/m/0m0jc", - "jazz": "/m/03_d0", - "techno": "/m/07gxw", - "disco": "/m/026z9", - "country": "/m/01lyv", - "blues": "/m/0155w", - "soul": "/m/0gywn", - "trance": "/m/07lnk", - "house": "/m/03mb9", - } - - - # 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, search=None): - - entries = [] - channels = self.categories[self.categories.index("my channels") + 1] - - # plain search request for videos - if search is not None: - for row in self.api("search", type="video", regionCode=conf.youtube_region, q=search): - entries.append( self.wrap3(row, {"genre": ""}) ) - - # Most Popular - elif 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.lower() in self.topic_id: - for row in self.api("search", order="date", regionCode=conf.youtube_region, topicId=self.topic_id[cat.lower()], 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) - - # fetches videos ordered by date - for row in self.api("search", order="date", fields="pageInfo,nextPageToken,items(id,snippet(title,channelTitle,description))", channelId=UC, type="video"): - entries.append( self.wrap3(row, {"genre": cat, "playing": cat}) ) - - # augments with playlist entries - 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) - - # unique entries - e = [] - [e.append(v) for v in entries if v not in e] - entries = e - - # empty entries - else: - return self.placeholder - - # 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, debug=False, **params): - items = [] - - # URL and default parameters - (base_url, defaults) = self.service[ver] - params = dict( list(defaults.items()) + list(params.items()) ) - - # Retrieve data set - while pages > 0: - j = ahttp.get(base_url + method, params=params) - #if debug: - #log.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 len(items) >= int(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( (10 - 1.852 * pages) / 10.5 ) - - 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 = "http://youtu.be/" + id + ("?wadsworth=1" if conf.youtube_wadsworth else ""), - format = self.audioformat, - title = row["snippet"]["title"], - )) - #log.DATA(row) - - # optional values - if "snippet" in row: - if "playing" not in data and "channelTitle" in row["snippet"]: - data["playing"] = row["snippet"]["channelTitle"] - if "description" in row["snippet"] and "description" in row["snippet"]: - data["description"] = row["snippet"]["description"], - #log.UI(data) - - return data - - - # API version 2.0s jsonified XML needs different unpacking: - def wrap2(self, row): - #log.DATA(row) - return dict( - genre = row["category"][1]["term"], - title = row["title"]["$t"], - playing = row["author"][0]["name"]["$t"], - format = self.audioformat, - url = row["content"]["src"].split("?")[0], - homepage = row["media$group"]["media$player"]["url"], - image = row["media$group"]["media$thumbnail"][0]["url"], - ) - - - ADDED contrib/youtube.py Index: contrib/youtube.py ================================================================== --- contrib/youtube.py +++ contrib/youtube.py @@ -0,0 +1,344 @@ +# encoding: UTF-8 +# api: streamtuner2 +# title: Youtube +# description: Channel, playlist and video browsing for youtube. +# type: channel +# version: 0.3 +# url: http://www.youtube.com/ +# category: video +# config: +# { name: youtube_apikey, type: text, value: "", description: "Youtube Data API key", category: auth } +# { 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: GB, description: "Filter by region id.", category: auth } +# { name: youtube_wadsworth, type: boolean, value: 0, description: "Apply Wadsworth constant.", category: filter } +# priority: deprecated +# png: +# iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAYNJREFUOI3Fks9LVFEUxz/nzrPx+WN0xhAUgoT6A6y/wFb+C4IbIQhcBm36H1obVNtoGYS0TFoIQstazBgNBaELQdTx +# vea9uffbwufw3mTRzi8cLnzv955z7vccuG7YXmtyBlgBbgFTQB3Q3/RAHzgD9oHdyMNTg01gshD8DwScCJ7bx+bEN7Cl0Xt5D2aYc//Iq67LYDFHXEamgGZmmd94SHzvPoMoIguerKQZamExykS9kjQIN3eThcdP +# WAiBo/fbHLx5Te/LZzQYgFW6qbsMKEcf+CWRpCm+2aK5ts6drZfMP9okH4/pSxV91NeI4RLmA0mS4ns9JHGaJvzMc1Lpwo3Smyi7wl6FwHmScNzt8mPnA4fv3lLrtJkIHqt+AXvViFPB+JCQ0HQDrTyg127jvu4T +# D3Jqzg0LDLWQ2lYj7oDulmlJZCEwZuD+GGMlRae2eiNqeVgOUA9AAAuAmSEzCq4cKs5TwYvIwzPBJ+A2F2s8XZQcXedL7qwY1neDHa4dvwFfDLdx6YbozgAAAABJRU5ErkJggg== +# depends: bin:youtube-dl +# extraction-method: json +# +# DEPRECATED. You're going to need your own Youtube API key now to use +# this channel. This isn't very much worth the effort, but see: +# https://developers.google.com/youtube/v3/getting-started +# +# WHY? The usage quota has been exceeded (fairly unlikely that it was from +# regular streamtuner2 usage; more plausible that someone looked up the +# existing API key and used it for different purposes). And the Youtube +# "support" people kept demanding a video recording of ST2 usage (due to +# comprehension issues or whatever). Which wasn't a strong indicator for +# a sensible or technical audit/quota usage review. +# +# Which is why there's now a PeerTube channel plugin instead. +# +# Lists recently popular youtube videos from a selection of categories +# or customly configured channel names. +# +# Introduces the faux MIME type "video/youtube" for player and recording +# configuration; both utilizing `youtube-dl`. But VLC can consume Youtube +# URLs directly anyhow. + + +from config import * +from channels import * + +import ahttp +import json + + + +# Youtube +# +# +# 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 +# +class youtube (ChannelPlugin): + + # control attributes + listformat = "url/youtube" + has_search = True + audioformat = "video/youtube" + titles = dict( genre="Channel", title="Title", playing="Playlist", bitrate=False, listeners=False ) + + # API config + service = { + 2: [ "http://gdata.youtube.com/", + # deprecated on 2015-04-20, no /v3/ alternative, pertains "mostPopular" category only + { + "v": 2, + "alt": "json", + "max-results": 50, + } + ], + 3: [ "https://www.googleapis.com/youtube/v3/", + { + "key": conf.get("youtube_apikey", ""), + "maxResults": 50, + "part": "id,snippet", + "fields": "pageInfo,nextPageToken,items(id,snippet(title,thumbnails/default/url,channelTitle))", + } + ] + } + + categories = [ + "mostPopular", + ["Music", "Comedy", "Movies", "Shows", "Trailers", "Film & Animation", "Entertainment", "News & Politics"], + "topics", + ["Pop", "Billboard charts", "Rock", "Hip Hop", "Classical", "Soundtrack", "Ambient", + "Jazz", "Blues", "Soul", "Country", "Disco", "Dance", "House", "Trance", "Techno", "Electronica"], + "my channels", + ["Key of Awesome", "Pentatonix"] + ] + # 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 = { + "pop": "/m/064t9", + "billboard charts": "/m/04qf57", + "rock": "/m/06by7", + "dance": "/m/0ggx5q", + "classical": "/m/0ggq0m", + "hip hop": "/m/0glt670", + "soundtrack": "/m/0l14gg", + "ambient": "/m/0fd3y", + "electronica": "/m/0m0jc", + "jazz": "/m/03_d0", + "techno": "/m/07gxw", + "disco": "/m/026z9", + "country": "/m/01lyv", + "blues": "/m/0155w", + "soul": "/m/0gywn", + "trance": "/m/07lnk", + "house": "/m/03mb9", + } + + + # 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, search=None): + + entries = [] + channels = self.categories[self.categories.index("my channels") + 1] + + # plain search request for videos + if search is not None: + for row in self.api("search", type="video", regionCode=conf.youtube_region, q=search): + entries.append( self.wrap3(row, {"genre": ""}) ) + + # Most Popular + elif 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.lower() in self.topic_id: + for row in self.api("search", order="date", regionCode=conf.youtube_region, topicId=self.topic_id[cat.lower()], 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) + + # fetches videos ordered by date + for row in self.api("search", order="date", fields="pageInfo,nextPageToken,items(id,snippet(title,channelTitle,description))", channelId=UC, type="video"): + entries.append( self.wrap3(row, {"genre": cat, "playing": cat}) ) + + # augments with playlist entries + 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) + + # unique entries + e = [] + [e.append(v) for v in entries if v not in e] + entries = e + + # empty entries + else: + return self.placeholder + + # 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, debug=False, **params): + items = [] + + # URL and default parameters + (base_url, defaults) = self.service[ver] + params = dict( list(defaults.items()) + list(params.items()) ) + + # Retrieve data set + while pages > 0: + j = ahttp.get(base_url + method, params=params) + #if debug: + #log.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 len(items) >= int(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( (10 - 1.852 * pages) / 10.5 ) + + 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 = "http://youtu.be/" + id + ("?wadsworth=1" if conf.youtube_wadsworth else ""), + format = self.audioformat, + title = row["snippet"]["title"], + )) + #log.DATA(row) + + # optional values + if "snippet" in row: + if "playing" not in data and "channelTitle" in row["snippet"]: + data["playing"] = row["snippet"]["channelTitle"] + if "description" in row["snippet"] and "description" in row["snippet"]: + data["description"] = row["snippet"]["description"], + #log.UI(data) + + return data + + + # API version 2.0s jsonified XML needs different unpacking: + def wrap2(self, row): + #log.DATA(row) + return dict( + genre = row["category"][1]["term"], + title = row["title"]["$t"], + playing = row["author"][0]["name"]["$t"], + format = self.audioformat, + url = row["content"]["src"].split("?")[0], + homepage = row["media$group"]["media$player"]["url"], + image = row["media$group"]["media$thumbnail"][0]["url"], + ) + + +