Skip to content

Commit

Permalink
[plugin.video.invidoius] 0.2.1+nexus.0
Browse files Browse the repository at this point in the history
A privacy-friendly way of watching YouTube content. Uses the great Invidious
web service's API to do the heavy lifting.

Second nexus repository release from new repository.
  • Loading branch information
petterreinholdtsen committed Aug 10, 2023
1 parent 7caf44d commit fcd559c
Show file tree
Hide file tree
Showing 12 changed files with 713 additions and 0 deletions.
19 changes: 19 additions & 0 deletions plugin.video.invidious/LICENSE.txt
@@ -0,0 +1,19 @@
Copyright (c) 2020 TheAssassin <theassassin@assassinate-you.net>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
45 changes: 45 additions & 0 deletions plugin.video.invidious/README.md
@@ -0,0 +1,45 @@
# [Invidious](https://invidio.us) plugin for [Kodi](https://kodi.tv)

This plugin provides an [Invidious](https://invidio.us) client for [Kodi](https://kodi.tv). Invidious is a privacy-friendly web frontend to YouTube.

**Note:** Currently, this plugin is only tested with Kodi 20 and newer.

## Installation

To keep track with development, it is recommended to install the plugin with git:

```shell script
# please change the destination if necessary
git clone https://github.com/petterreinholdtsen/kodi-invidious-plugin.git ~/.kodi/addons/plugin.video.invidious

# on an embedded device (e.g., an X96 Mini running CoreELEC, you need to clone to /storage/.kodi/addons/plugin.video.invidious
```

You can also download an archive and extract it in the right place instead of using git:

```shell script
# same here: make sure you change to the right directory
cd ~/.kodi/addons/

# download a zip archive
mkdir plugin.video.invidious
wget https://github.com/petterreinholdtsen/kodi-invidious-plugin/archive/master.tar.gz -O - | tar xz --strip-components=1 -C plugin.video.invidious
```

It is strongly recommended to use git instead of an archive, as it makes updating the plugin a lot easier.

You may have to restart Kodi before you can enable the plugin. To enable the plugin, please go to the addons settings, switch to *user plugins* and enable the plugin there.


## To Do

- evaluate using youtube-dl to remove dependency on third-party web service (maybe in a second plugin)
- implement adaptive streaming properly
- be able to open YouTube videos from [NewPipe](https://newpipe.net)
- ~~add support for settings, e.g., to configure Invidious instance~~
- ~~support for adaptive streaming~~
- consider supporting versions < Kodi 18 (e.g., by making the dependency on inputstream-helper optional)
- ~~*trending*, *top*, *popular* etc. video lists (not too important to most people, but Invidious offers endpoints, so why not?)~~
- visit channel of list items (e.g., via the context menu of a video search result)
- save channels in favorite list (for quick access from the main menu)
- support for subtitles
32 changes: 32 additions & 0 deletions plugin.video.invidious/addon.xml
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon id="plugin.video.invidious"
name="Invidious"
version="0.2.1+nexus.0"
provider-name="petterreinholdtsen">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.requests" version="2.22.0"/>
<import addon="script.module.inputstreamhelper" version="0.5.2"/>
</requires>

<extension point="xbmc.python.pluginsource" library="resources/lib/invidious_addon.py">
<provides>video</provides>
</extension>

<extension point="xbmc.python.module" library="resources/lib/"/>
<extension point="xbmc.addon.metadata">
<summary lang="en">Invidious client</summary>
<description lang="en">A privacy-friendly way of watching YouTube content. Uses the great Invidious web service's API to do the heavy lifting.</description>
<disclaimer lang="en">This plugin is not endorsed by Google</disclaimer>
<summary lang="nb">Invidious-klient</summary>
<description lang="nb">En personvernvennlig måte å se YouTube-innhold på. Bruker den flotte Invidious-netttjenestens API for å gjøre det tunge arbeidet.</description>
<disclaimer lang="nb">Denne plugin-modulen er ikke godkjent av Google.</disclaimer>
<language>en nb</language>
<platform>all</platform>
<license>MIT</license>
<source>https://github.com/petterreinholdtsen/kodi-invidious-plugin</source>
<assets>
<icon>resources/icon.png</icon>
</assets>
</extension>
</addon>
Binary file added plugin.video.invidious/resources/icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions plugin.video.invidious/resources/language/Makefile
@@ -0,0 +1,4 @@
# Update translations when the english master changes

update-po:
msgmerge -U --no-wrap resource.language.no_no/strings.po resource.language.en_gb/strings.po
6 changes: 6 additions & 0 deletions plugin.video.invidious/resources/language/README
@@ -0,0 +1,6 @@
To add a new language, create the language folder
(I.e. resource.language.de_de/, resource.language.da_dk/, resource.language.fr_fr/ etc.)
Run "msginit -i resource.language.en_gb/strings.po".
This will procduce an XX.po file, where XX is the current configured Locale language of your system.
Move that file into your language folder, and name the file strings.po.
For french it will be "mv fr.po resource.language.fr_fr/strings.po"
@@ -0,0 +1,57 @@
# Kodi Media Center language file
# Addon Name: YouTube
# Addon id: plugin.video.invidious
# Addon Provider: TheAssassin
msgid ""
msgstr ""
"Project-Id-Version: XBMC-Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: 2015-09-21 11:01+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_GB\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"

msgctxt "#30000"
msgid "No description available"
msgstr ""

msgctxt "#30001"
msgid "Search"
msgstr ""

msgctxt "#30002"
msgid "New search"
msgstr ""

msgctxt "#30003"
msgid "HTTP error"
msgstr ""

msgctxt "#30004"
msgid "Request to Invidious API failed: HTTP status "
msgstr ""

msgctxt "#30005"
msgid "Timeout"
msgstr ""

msgctxt "#30006"
msgid "Request to Invidious API exceeded timeout"
msgstr ""

msgctxt "#30007"
msgid "Instance URL"
msgstr ""

msgctxt "#30008"
msgid "Instance settings"
msgstr ""

msgctxt "#30009"
msgid "Disable Dynamic Adaptive Streaming over HTTP (MPEG-DASH)"
msgstr ""
@@ -0,0 +1,55 @@
# Kodi Media Center language file
# Addon Name: YouTube
# Addon id: plugin.video.invidious
# Addon Provider: TheAssassin
# Johnny A. Solbu, 2023
msgid ""
msgstr ""
"Project-Id-Version: XBMC-Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: 2015-09-21 11:01+0000\n"
"PO-Revision-Date: 2023-08-07 11:19+0200\n"
"Last-Translator: Johnny A. Solbu <johnny@solbu.net>\n"
"Language-Team: Norwegian Bokmal <l10n-no@lister.huftis.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: nb\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.4.2\n"

msgctxt "#30000"
msgid "No description available"
msgstr "Ingen beskrivelse tilgjengelig"

msgctxt "#30001"
msgid "Search"
msgstr "Søk"

msgctxt "#30002"
msgid "Search video titles"
msgstr "Søk etter videotitler"

msgctxt "#30003"
msgid "HTTP error"
msgstr "HTTP-feil"

msgctxt "#30004"
msgid "Request to Invidious API failed: HTTP status "
msgstr "Forespørsel til Invidious-API mislyktes: HTTP-status "

msgctxt "#30005"
msgid "Timeout"
msgstr "Tidsavbrudd"

msgctxt "#30006"
msgid "Request to Invidious API exceeded timeout"
msgstr "Forespørselen til Invidious-API har overskredet tidsavbruddet"

msgctxt "#30007"
msgid "Instance URL"
msgstr "Instans-URL"

msgctxt "#30008"
msgid "Instance settings"
msgstr "Instans-innstillinger"
18 changes: 18 additions & 0 deletions plugin.video.invidious/resources/lib/invidious_addon.py
@@ -0,0 +1,18 @@
import sys

import invidious_plugin

import xbmc
import xbmcplugin


def main():
plugin = invidious_plugin.InvidiousPlugin.from_argv()

xbmcplugin.setContent(plugin.addon_handle, "videos")

return plugin.run()


if __name__ == "__main__":
sys.exit(main())
143 changes: 143 additions & 0 deletions plugin.video.invidious/resources/lib/invidious_api.py
@@ -0,0 +1,143 @@
import time
from collections import namedtuple

import requests
import xbmc
import xbmcaddon

VideoListItem = namedtuple("VideoSearchResult",
[
"type",
"id",
"thumbnail_url",
"heading",
"author",
"description",
"view_count",
"published",
"duration",
]
)

ChannelListItem = namedtuple("ChannelSearchResult",
[
"type",
"id",
"thumbnail_url",
"heading",
"description",
"verified",
"sub_count",
]
)


class InvidiousAPIClient:
def __init__(self, instance_url):
self.instance_url = instance_url.rstrip("/")
self.addon = xbmcaddon.Addon()

def make_get_request(self, *path, **params):
base_url = self.instance_url + "/api/v1/"

url_path = "/".join(path)

while "//" in url_path:
url_path = url_path.replace("//", "/")

assembled_url = base_url + url_path

xbmc.log(f"invidious ========== request {assembled_url} with {params} started ==========", xbmc.LOGDEBUG)
start = time.time()
response = requests.get(assembled_url, params=params, timeout=5)
end = time.time()
xbmc.log(f"invidious ========== request finished in {end - start}s ==========", xbmc.LOGDEBUG)

response.raise_for_status()

return response

def parse_response(self, response):
data = response.json()

# If a channel is opened, the videos are packaged in a dict
# entry "videos".
if "videos" in data:
data = data["videos"]

for item in data:
if item["type"] in ["video", "shortVideo"]:
# Skip videos with no or negative duration.
if not item["lengthSeconds"] > 0:
continue
for thumb in item["videoThumbnails"]:

# high appears to be ~480x360, which is a
# reasonable trade-off works well on 1080p.
if thumb["quality"] == "high":
thumbnail_url = thumb["url"]
break

# as a fallback, we just use the last one in the list
# (which is usually the lowest quality).
else:
thumbnail_url = item["videoThumbnails"][-1]["url"]

yield VideoListItem(
"video",
item["videoId"],
thumbnail_url,
item["title"],
item["author"],
item.get("description", self.addon.getLocalizedString(30000)),
item["viewCount"],
item["published"],
item["lengthSeconds"]
)
elif item["type"] == "channel":
# Grab the highest resolution avatar image
# Usually isn't more than 512x512
thumbnail = sorted(item["authorThumbnails"], key=lambda thumb: thumb["height"], reverse=True)[0]

yield ChannelListItem(
"channel",
item["authorId"],
"https:" + thumbnail["url"],
item["author"],
item["description"],
item["authorVerified"],
item["subCount"],
)
elif item["type"] == 'playlist': # Ignored for now
xbmc.log("invidious skipping playlist from search result!", xbmc.LOGINFO)
xbmc.log(f"invidious playlist entry: {item}", xbmc.LOGDEBUG)
continue
else:
xbmc.log(f'invidious received search result item with unknown response type {item["type"]}.', xbmc.LOGWARNING)

def search(self, *terms):
params = {
"q": " ".join(terms),
"sort_by": "upload_date",
}

response = self.make_get_request("search", **params)

return self.parse_response(response)

def fetch_video_information(self, video_id):
response = self.make_get_request("videos/", video_id)

data = response.json()

return data

def fetch_channel_list(self, channel_id):
response = self.make_get_request(f"channels/videos/{channel_id}")

return self.parse_response(response)

def fetch_special_list(self, special_list_name):
response = self.make_get_request(special_list_name)

return self.parse_response(response)

0 comments on commit fcd559c

Please sign in to comment.