-
Notifications
You must be signed in to change notification settings - Fork 455
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4445 from syhlx/matrix
[plugin.video.piped] 1.0.0
- Loading branch information
Showing
21 changed files
with
1,572 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Piped Addon for Kodi | ||
|
||
[![AGPL v3](https://shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html) | ||
|
||
An addon which allows you to access any Piped instance, login and manage your playlists and watch history. | ||
|
||
## Features: | ||
|
||
**Basic Features** | ||
|
||
- [x] Watch videos (obviously!) | ||
- [x] Search for videos, channels and playlists | ||
- [x] Multi-language audio | ||
- [x] Subtitles | ||
- [x] Pick favourite your Piped instance | ||
- [x] Compatible with Sponsor Block | ||
- [ ] Watch live streams (can be watched once finished, for now) | ||
|
||
**Account Features (logged in to a Piped instance)** | ||
|
||
- [x] Personal Feed | ||
- [x] Subscriptions | ||
- [x] Playlists | ||
- [x] Optional: Watch History (use a playlist as watch history) | ||
- [x] Optional: Hide already watched videos in each section | ||
|
||
## Watch history | ||
|
||
To enable the watch history: After logging in from the settings, go to your playlists and choose "**Set as watch history**" from the context menu on the playlist you wish to use as your watch history. | ||
|
||
## Piped instances | ||
|
||
The official Piped instance is set by default. | ||
|
||
But, you can also choose your favourite Piped instance or host your own and change it in the settings. | ||
|
||
- List of public Piped Instances: [Public Piped instances](https://github.com/TeamPiped/Piped/wiki/Instances) | ||
- Host your own instance: [Self-Hosting](https://docs.piped.video/docs/self-hosting/) | ||
|
||
## Disclaimer | ||
|
||
This plugin is neither affiliated with nor endorsed by TeamPiped. | ||
|
||
# License | ||
Piped Addon for Kodi is licensed under the AGPL v3 License. See [LICENSE](LICENSE.txt) for details. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
<addon id="plugin.video.piped" name="Piped" version="1.0.0" provider-name="Syhlx"> | ||
<requires> | ||
<import addon="xbmc.python" version="3.0.0" /> | ||
<import addon="inputstream.adaptive" version="19.0.0" /> | ||
<import addon="script.module.requests" version="2.27.0" /> | ||
</requires> | ||
<extension point="xbmc.service" library="service.py" /> | ||
<extension point="xbmc.python.pluginsource" library="default.py"> | ||
<provides>video</provides> | ||
</extension> | ||
<extension point="xbmc.addon.metadata"> | ||
<summary lang="en_GB">Piped Addon for Kodi</summary> | ||
<description lang="en_GB">An addon which allows you to access any Piped instance, login and manage your playlists and watch history</description> | ||
<disclaimer lang="en_GB">This plugin is neither affiliated with nor endorsed by TeamPiped</disclaimer> | ||
<platform>all</platform> | ||
<license>AGPL-3.0-only</license> | ||
<source>https://github.com/syhlx/plugin.video.piped</source> | ||
<news>None</news> | ||
<assets> | ||
<icon>resources/icon.png</icon> | ||
<fanart>resources/fanart.jpg</fanart> | ||
</assets> | ||
</extension> | ||
</addon> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import sys | ||
from xbmcplugin import setContent | ||
|
||
from lib.sections import router | ||
|
||
setContent(int(sys.argv[1]), 'videos') | ||
|
||
router(sys.argv) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from xbmcaddon import Addon | ||
from xbmcgui import Dialog | ||
|
||
def get_auth_token() -> str: | ||
addon = Addon() | ||
if not addon.getSettingBool('use_login'): return '' | ||
|
||
if len(addon.getSettingString('auth_token')) > 0: | ||
return addon.getSettingString('auth_token') | ||
else: | ||
error_msg: str = '' | ||
try: | ||
result = requests.post(f'{addon.getSettingString("instance")}/login', json = dict( | ||
username = addon.getSettingString('username'), | ||
password = addon.getSettingString('password') | ||
)) | ||
|
||
error_msg = result.text | ||
|
||
auth_token = result.json()['token'] | ||
addon.setSettingString('auth_token', auth_token) | ||
|
||
return auth_token | ||
except: | ||
Dialog().ok(addon.getLocalizedString(30016), error_msg) | ||
return '' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
from requests import get | ||
import xbmc | ||
from xbmcaddon import Addon | ||
|
||
def generate_dash(video_id: str) -> str: | ||
addon = Addon() | ||
resp = get(f'{addon.getSettingString("instance")}/streams/{video_id}').json() | ||
|
||
streams: dict = dict( | ||
audio = dict(), | ||
video = dict(), | ||
) | ||
|
||
default_audio: dict = dict() | ||
prefer_original_lang: bool = addon.getSettingBool("audio_prefer_original_lang") | ||
preferred_lang: str = addon.getSettingString("audio_custom_lang") if addon.getSettingString("audio_custom_lang") and not addon.getSettingBool("audio_prefer_kodi_lang") else xbmc.getLanguage(xbmc.ISO_639_1) | ||
|
||
for stream in resp["audioStreams"] + resp["videoStreams"]: | ||
media_lang = stream["audioTrackLocale"] if stream["audioTrackLocale"] is not None else 'null' | ||
media_type, media_format = stream["mimeType"].split("/") | ||
if media_type in ['audio', 'video'] and "googlevideo" in stream["url"]: | ||
if media_type == 'audio' and ((prefer_original_lang and stream["audioTrackType"] == "ORIGINAL") or (not prefer_original_lang and media_lang[:2] == preferred_lang)): | ||
if not default_audio.__contains__(media_lang): default_audio[media_lang]: dict = dict() | ||
if not default_audio[media_lang].__contains__(media_format): default_audio[media_lang][media_format]: list = list() | ||
default_audio[media_lang][media_format].append(stream) | ||
else: | ||
if not streams[media_type].__contains__(media_lang): streams[media_type][media_lang]: dict = dict() | ||
if not streams[media_type][media_lang].__contains__(media_format): streams[media_type][media_lang][media_format]: list = list() | ||
streams[media_type][media_lang][media_format].append(stream) | ||
|
||
streams['audio'] = default_audio | streams['audio'] | ||
|
||
mpd: str = '<?xml version="1.0" encoding="utf-8"?>' | ||
mpd += f'<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:full:2011" minBufferTime="PT1.5S" type="static" mediaPresentationDuration="PT{resp["duration"]}S">' | ||
|
||
mpd += '<Period>' | ||
|
||
if addon.getSettingBool("subtitles_load"): | ||
for subtitle in resp["subtitles"]: | ||
mpd += f'<AdaptationSet contentType="text" mimeType="{subtitle["mimeType"]}" segmentAlignment="true" lang="{subtitle["code"]}">' | ||
mpd += '<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>' | ||
mpd += f'<Representation id="caption_{subtitle["code"]}{"_auto" if subtitle["autoGenerated"] else ""}" bandwidth="256">' | ||
mpd += f'<BaseURL>{subtitle["url"].replace("&", "&")}</BaseURL>' | ||
mpd += '</Representation></AdaptationSet>' | ||
|
||
for stream_type in ['audio', 'video']: | ||
for stream_lang in streams[stream_type]: | ||
for stream_format in streams[stream_type][stream_lang]: | ||
stream_xml: str = '' | ||
for stream in streams[stream_type][stream_lang][stream_format]: | ||
if stream["initEnd"] > 0: | ||
stream_xml += f'<Representation id="{stream["itag"]}" codecs="{stream["codec"]}" bandwidth="{stream["bitrate"]}"' | ||
if stream_type == 'audio': | ||
stream_xml += '><AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>' | ||
elif stream_type == 'video': | ||
stream_xml += f' width="{stream["width"]}" height="{stream["height"]}" maxPlayoutRate="1" frameRate="{stream["fps"]}">' | ||
stream_xml += f'<BaseURL>{stream["url"].replace("&", "&")}</BaseURL>' | ||
stream_xml += f'<SegmentBase indexRange="{stream["indexStart"]}-{stream["indexEnd"]}">' | ||
stream_xml += f'<Initialization range="{stream["initStart"]}-{stream["initEnd"]}"/>' | ||
stream_xml += '</SegmentBase></Representation>' | ||
|
||
if len(stream_xml) > 0: | ||
mpd += f'<AdaptationSet mimeType="{stream_type}/{stream_format}" startWithSAP="1" subsegmentAlignment="true"' | ||
mpd += ' scanType="progressive">' if stream_type == 'video' else f' lang="{stream_lang}">' | ||
mpd += stream_xml | ||
mpd += f'</AdaptationSet>' | ||
|
||
mpd += '</Period></MPD>' | ||
|
||
return mpd |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import json | ||
from requests import get, post | ||
from xbmcaddon import Addon | ||
from xbmcvfs import translatePath | ||
|
||
from lib.authentication import get_auth_token | ||
|
||
def set_watch_history(playlist_id: str) -> None: | ||
addon = Addon() | ||
addon.setSettingBool('watch_history_enable', True) | ||
addon.setSettingString('watch_history_playlist', playlist_id) | ||
|
||
def mark_as_watched(video_id: str) -> None: | ||
addon = Addon() | ||
profile_path: str = translatePath(addon.getAddonInfo("profile")) | ||
instance: str = addon.getSettingString('instance') | ||
auth_token: str = get_auth_token() | ||
playlist_id: str = addon.getSettingString('watch_history_playlist') | ||
|
||
videos = get(f'{instance}/playlists/{playlist_id}').json()['relatedStreams'] | ||
index: int = -1 | ||
|
||
for i in range(len(videos)): | ||
if videos[i]['url'] == f"/watch?v={video_id}": | ||
index = i | ||
break | ||
|
||
if index == -1: | ||
post(f'{instance}/user/playlists/add', json = dict( | ||
playlistId = playlist_id, | ||
videoId = video_id | ||
), headers={'Authorization': auth_token}) | ||
|
||
try: | ||
with open(f'{profile_path}/watch_history.json', 'r') as f: | ||
history = json.load(f) | ||
history.insert(0, video_id) | ||
|
||
with open(f'{profile_path}/watch_history.json', 'w+') as f: | ||
json.dump(history, f) | ||
except: | ||
pass | ||
|
||
def mark_as_unwatched(video_id: str) -> None: | ||
addon = Addon() | ||
profile_path: str = translatePath(addon.getAddonInfo("profile")) | ||
instance: str = addon.getSettingString('instance') | ||
auth_token: str = get_auth_token() | ||
playlist_id: str = addon.getSettingString('watch_history_playlist') | ||
|
||
videos: list = get(f'{instance}/playlists/{playlist_id}').json()['relatedStreams'] | ||
|
||
for i in range(len(videos)): | ||
if videos[i]['url'] == f"/watch?v={video_id}": | ||
post(f'{instance}/user/playlists/remove', json = dict( | ||
playlistId = playlist_id, | ||
index = i | ||
), headers={'Authorization': auth_token}) | ||
break | ||
|
||
try: | ||
with open(f'{profile_path}/watch_history.json', 'r') as f: | ||
history = json.load(f) | ||
history.remove(video_id) | ||
|
||
with open(f'{profile_path}/watch_history.json', 'w+') as f: | ||
json.dump(history, f) | ||
except: | ||
pass |
Oops, something went wrong.