Internet radio browser GUI for music/video streams from various directory services.

⌈⌋ ⎇ branch:  streamtuner2


peertube.py at [04648f1c1c]

File channels/peertube.py artifact fc7ec6bb9a part of check-in 04648f1c1c


# 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 resolve_urn does an extra /api/v1/videos/uuid lookup.
#
class peertube (ChannelPlugin):

    # control attributes
    listformat = "href"
    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
        self.status(0.9)
        entries = []
        for video in self.api("videos", params):
            #log.DATA(video)
            entries.append(self.map_data(video))
        #log.EN(json.dumps(entries, indent=4))
        self.status(1.0)
        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):
            self.status(i / 6.0)
            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