Skip to content

Commit 0e6c6ee

Browse files
committed
[plugin.video.piped] 1.1.0
1 parent 87eff90 commit 0e6c6ee

File tree

9 files changed

+630
-48
lines changed

9 files changed

+630
-48
lines changed

plugin.video.piped/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ An addon which allows you to access any Piped instance, login and manage your pl
1414
- [x] Subtitles
1515
- [x] Pick favourite your Piped instance
1616
- [x] Compatible with Sponsor Block
17-
- [ ] Watch live streams (can be watched once finished, for now)
17+
- [x] Blacklist channels and video titles
18+
- [x] Watch live streams
1819

1920
**Account Features (logged in to a Piped instance)**
2021

@@ -42,4 +43,4 @@ But, you can also choose your favourite Piped instance or host your own and chan
4243
This plugin is neither affiliated with nor endorsed by TeamPiped.
4344

4445
# License
45-
Piped Addon for Kodi is licensed under the AGPL v3 License. See [LICENSE](LICENSE.txt) for details.
46+
Piped Addon for Kodi is licensed under the AGPL v3 License. See [LICENSE](LICENSE.txt) for details.

plugin.video.piped/addon.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="plugin.video.piped" name="Piped" version="1.0.1" provider-name="Syhlx">
2+
<addon id="plugin.video.piped" name="Piped" version="1.1.0" provider-name="Syhlx">
33
<requires>
44
<import addon="xbmc.python" version="3.0.0" />
55
<import addon="inputstream.adaptive" version="19.0.0" />
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import json
2+
from xbmcaddon import Addon
3+
from xbmcvfs import translatePath
4+
from xbmcgui import Dialog
5+
6+
addon = Addon()
7+
blacklist_path: str = f"{translatePath(addon.getAddonInfo('profile'))}/blacklist.json"
8+
9+
def blacklist_load() -> dict:
10+
try:
11+
with open(blacklist_path, 'r') as f: return json.load(f)
12+
except:
13+
return dict()
14+
15+
def blacklist_save(blacklist: dict) -> None:
16+
with open(blacklist_path, 'w+') as f:
17+
json.dump(blacklist, f)
18+
19+
def blacklist_add(channel_id: str, channel_name: str) -> None:
20+
blacklist: dict = blacklist_load()
21+
22+
if channel_id not in blacklist:
23+
blacklist[channel_id] = dict(name = channel_name)
24+
25+
blacklist_save(blacklist)
26+
27+
def blacklist_remove(channel_id: str, prompt: bool=False) -> None:
28+
blacklist: dict = blacklist_load()
29+
30+
if prompt:
31+
if not Dialog().yesno(blacklist[channel_id]['name'], addon.getLocalizedString(30024)):
32+
return
33+
34+
if channel_id in blacklist:
35+
del blacklist[channel_id]
36+
blacklist_save(blacklist)

plugin.video.piped/lib/dash.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
from requests import get
21
import xbmc
32
from xbmcaddon import Addon
43

54
class AutoIncrement():
65
def __init__(self):
7-
self.i: int = 0
6+
self.i: int = -1
87

98
def gen(self):
109
self.i += 1
1110
return self.i
1211

13-
def generate_dash(video_id: str) -> str:
12+
def generate_dash(video_info) -> str:
1413
addon = Addon()
15-
resp = get(f'{addon.getSettingString("instance")}/streams/{video_id}').json()
16-
1714
autoincrement = AutoIncrement()
1815

1916
streams: dict = dict(
@@ -29,7 +26,7 @@ def generate_dash(video_id: str) -> str:
2926
if len(addon.getSettingString("video_codec_priority")) > 1:
3027
for codec in addon.getSettingString("video_codec_priority").split(','): streams['video']['null'][codec]: list = list()
3128

32-
for stream in resp["audioStreams"] + resp["videoStreams"]:
29+
for stream in video_info["audioStreams"] + video_info["videoStreams"]:
3330
media_lang = stream["audioTrackLocale"] if stream["audioTrackLocale"] is not None else 'null'
3431
media_type, _ = stream["mimeType"].split("/")
3532
if 'codec' not in stream or not isinstance(stream["codec"], str): continue
@@ -47,14 +44,14 @@ def generate_dash(video_id: str) -> str:
4744
streams['audio'] = default_audio | streams['audio']
4845

4946
mpd: str = '<?xml version="1.0" encoding="utf-8"?>'
50-
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">'
47+
mpd += f'<MPD schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-main:2011" type="static" mediaPresentationDuration="PT{video_info["duration"]}S">'
5148

5249
mpd += '<Period>'
5350

5451
if addon.getSettingBool("subtitles_load"):
55-
for subtitle in resp["subtitles"]:
52+
for subtitle in video_info["subtitles"]:
5653
mpd += f'<AdaptationSet contentType="text" mimeType="{subtitle["mimeType"]}" segmentAlignment="true" lang="{subtitle["code"]}">'
57-
mpd += '<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>'
54+
mpd += '<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle" />'
5855
mpd += f'<Representation id="caption_{subtitle["code"]}{"_auto" if subtitle["autoGenerated"] else ""}_{autoincrement.gen()}" bandwidth="256">'
5956
mpd += f'<BaseURL>{subtitle["url"].replace("&", "&amp;")}</BaseURL>'
6057
mpd += '</Representation></AdaptationSet>'
@@ -65,19 +62,23 @@ def generate_dash(video_id: str) -> str:
6562
stream_xml: str = ''
6663
for stream in streams[stream_type][stream_lang][stream_format]:
6764
if stream["initEnd"] > 0:
68-
stream_xml += f'<Representation id="{stream["itag"]}_{autoincrement.gen()}" codecs="{stream["codec"]}" bandwidth="{stream["bitrate"]}"'
65+
stream_xml += f'<Representation id="{stream["itag"]}" codecs="{stream["codec"]}" startWithSAP="1" bandwidth="{stream["bitrate"]}"'
66+
if stream_type == 'video':
67+
stream_xml += f' width="{stream["width"]}" height="{stream["height"]}" maxPlayoutRate="1" frameRate="{stream["fps"]}"'
68+
stream_xml += '>'
6969
if stream_type == 'audio':
70-
stream_xml += '><AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>'
71-
elif stream_type == 'video':
72-
stream_xml += f' width="{stream["width"]}" height="{stream["height"]}" maxPlayoutRate="1" frameRate="{stream["fps"]}">'
70+
stream_xml += '<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />'
7371
stream_xml += f'<BaseURL>{stream["url"].replace("&", "&amp;")}</BaseURL>'
7472
stream_xml += f'<SegmentBase indexRange="{stream["indexStart"]}-{stream["indexEnd"]}">'
75-
stream_xml += f'<Initialization range="{stream["initStart"]}-{stream["initEnd"]}"/>'
73+
stream_xml += f'<Initialization range="{stream["initStart"]}-{stream["initEnd"]}" />'
7674
stream_xml += '</SegmentBase></Representation>'
7775

7876
if len(stream_xml) > 0:
79-
mpd += f'<AdaptationSet mimeType="{stream["mimeType"]}" startWithSAP="1" subsegmentAlignment="true"'
80-
mpd += ' scanType="progressive">' if stream_type == 'video' else f' lang="{stream_lang}">'
77+
mpd += f'<AdaptationSet id="{autoincrement.gen()}" mimeType="{stream["mimeType"]}" subsegmentAlignment="true"'
78+
if stream_type != 'video' and stream_lang != 'null':
79+
mpd += f' lang="{stream_lang}"'
80+
mpd += '>'
81+
mpd += '<Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main" />'
8182
mpd += stream_xml
8283
mpd += f'</AdaptationSet>'
8384

plugin.video.piped/lib/sections.py

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import time
22
import json
3-
from re import sub
4-
from urllib.parse import quote
3+
import re
4+
from urllib.parse import quote, unquote
55
from requests import get
66
from xbmcaddon import Addon
77
from xbmcvfs import translatePath
@@ -10,6 +10,7 @@
1010

1111
from lib.authentication import authenticated_request
1212
from lib.history import set_watch_history, mark_as_watched, mark_as_unwatched
13+
from lib.blacklist import blacklist_load, blacklist_add, blacklist_remove
1314
from lib.utils import get_component, human_format
1415

1516
addon = Addon()
@@ -19,15 +20,25 @@
1920
def home() -> None:
2021
folders: list = list()
2122
if addon.getSettingBool('use_login'):
22-
folders.append(('feed', addon.getLocalizedString(30001)))
23-
folders.append(('subscriptions', addon.getLocalizedString(30002)))
24-
folders.append(('playlists',addon.getLocalizedString(30003)))
25-
if addon.getSettingBool('watch_history_enable') and len(addon.getSettingString('watch_history_playlist')) > 0:
23+
if addon.getSettingBool('menu_show_feed'):
24+
folders.append(('feed', addon.getLocalizedString(30001)))
25+
if addon.getSettingBool('menu_show_feed_update'):
26+
folders.append(('updatefeed', addon.getLocalizedString(30020)))
27+
if addon.getSettingBool('menu_show_subscriptions'):
28+
folders.append(('subscriptions', addon.getLocalizedString(30002)))
29+
if addon.getSettingBool('menu_show_playlists'):
30+
folders.append(('playlists',addon.getLocalizedString(30003)))
31+
if addon.getSettingBool('menu_show_watch_history') and addon.getSettingBool('watch_history_enable') and len(addon.getSettingString('watch_history_playlist')) > 0:
2632
folders.append(('watch_history', addon.getLocalizedString(30004)))
27-
28-
folders.append(('trending', addon.getLocalizedString(30005)))
29-
folders.append(('search_select', addon.getLocalizedString(30006)))
30-
folders.append(('settings', addon.getLocalizedString(30007)))
33+
34+
if addon.getSettingBool('menu_show_watch_trending'):
35+
folders.append(('trending', addon.getLocalizedString(30005)))
36+
if addon.getSettingBool('menu_show_watch_blacklist') and addon.getSettingBool('blacklist_channels_enable'):
37+
folders.append(('blacklist_section', addon.getLocalizedString(30600)))
38+
if addon.getSettingBool('menu_show_watch_search'):
39+
folders.append(('search_select', addon.getLocalizedString(30006)))
40+
if addon.getSettingBool('menu_show_watch_settings'):
41+
folders.append(('settings', addon.getLocalizedString(30007)))
3142

3243
for folder in folders:
3344
xbmcplugin.addDirectoryItem(handle=addon_handle, url=f"{addon_url}/{folder[0]}", listitem=xbmcgui.ListItem(folder[1]), isFolder=True)
@@ -39,7 +50,6 @@ def watch(video_id: str) -> None:
3950
path=f'http://localhost:{addon.getSettingInt("http_port")}/watch?v={video_id}',
4051
)
4152
listitem.setProperty('inputstream', 'inputstream.adaptive')
42-
listitem.setProperty('inputstream.adaptive.manifest_type', 'mpd')
4353
listitem.setProperty('piped_video_id', video_id)
4454

4555
xbmcplugin.setResolvedUrl(handle=addon_handle, succeeded=True, listitem=listitem)
@@ -49,32 +59,53 @@ def list_videos(videos: list, hide_watched: bool=False, nextpage: str='') -> Non
4959
watch_history_enabled: bool = addon.getSettingBool('watch_history_enable') and len(addon.getSettingString('watch_history_playlist')) > 0
5060
if watch_history_enabled:
5161
try:
52-
with open(f'{translatePath(addon.getAddonInfo("profile"))}/watch_history.json', 'r') as f:
62+
with open(f'{translatePath(addon.getAddonInfo("profile"))}/watch_history.json', 'r') as f:
5363
history = json.load(f)
5464
except:
5565
pass
5666

67+
blacklist_channels_enabled: bool = addon.getSettingBool('blacklist_channels_enable')
68+
if blacklist_channels_enabled:
69+
blacklist_channels_list = blacklist_load()
70+
71+
blacklist_titles_enabled: bool = addon.getSettingBool('blacklist_titles_enable')
72+
if blacklist_titles_enabled:
73+
blacklist_titles_regex_str: str = addon.getSettingString('blacklist_titles_regex')
74+
if len(blacklist_titles_regex_str) > 0:
75+
blacklist_titles_regex = re.compile(rf"{blacklist_titles_regex_str}", re.IGNORECASE if addon.getSettingBool('blacklist_titles_case_insensitive') else re.NOFLAG)
76+
else:
77+
blacklist_titles_enabled = False
78+
5779
for video in videos:
80+
if blacklist_titles_enabled and 'title' in video and re.match(blacklist_titles_regex, video['title']):
81+
continue
82+
5883
plugin_url: str = f"{addon_url}{video['url'].replace('?v=', '/')}"
5984
video_id: str = get_component(video['url'])['params']['v']
60-
if hide_watched and video_id in history: continue
85+
channel_id: str = video['uploaderUrl'][9:] if ('uploaderUrl' in video and len(str(video['uploaderUrl'])) > 8) else ''
86+
87+
if (hide_watched and video_id in history) or (blacklist_channels_enabled and channel_id in blacklist_channels_list):
88+
continue
89+
6190
if 'uploadedDate' in video and video['uploadedDate'] is not None: date: str = video['uploadedDate']
6291
elif video['uploaded'] > 0: date: str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(video['uploaded'] / 1000))
6392
else: date: str = ''
6493
info: str = f"{video['title']}\n\n{video['uploaderName']}\n\n"
94+
if addon.getSettingBool('show_description') and 'shortDescription' in video and video['shortDescription'] is not None: info += video['shortDescription'] + "\n\n"
6595
if video['views'] >=0: info += f"{addon.getLocalizedString(30008)}: {human_format(video['views'])}\n"
6696
if len(date) > 2: info += f"{addon.getLocalizedString(30009)}: {date}"
6797
listitem = xbmcgui.ListItem(label=video['title'], path=plugin_url)
6898
listitem.setProperty('isplayable', 'true')
6999
listitem.setArt(dict(
70100
thumb = video['thumbnail'],
71-
fanart = video['uploaderAvatar']
101+
fanart = video['thumbnail'].replace('hqdefault.jpg', 'maxresdefault.jpg')
72102
))
73103

74104
tag = listitem.getVideoInfoTag()
75105
tag.setTitle(video['title'])
76106
tag.setPlot(info)
77107
tag.setDuration(video['duration'])
108+
if 'uploaded' in video and video['uploaded'] > 0: tag.setFirstAired(time.strftime('%Y-%m-%d', time.localtime(video['uploaded'] / 1000)))
78109
tag.setFilenameAndPath(plugin_url)
79110
tag.setPath(plugin_url)
80111

@@ -84,6 +115,13 @@ def list_videos(videos: list, hide_watched: bool=False, nextpage: str='') -> Non
84115
context_menu_items.append((addon.getLocalizedString(30011), f"RunPlugin({addon_url}/mark_as_unwatched?id={video_id})"))
85116
else:
86117
context_menu_items.append((addon.getLocalizedString(30012), f"RunPlugin({addon_url}/mark_as_watched?id={video_id})"))
118+
119+
if blacklist_channels_enabled:
120+
if channel_id in blacklist_channels_list:
121+
context_menu_items.append((addon.getLocalizedString(30023), f"RunPlugin({addon_url}/blacklist_remove?id={channel_id})"))
122+
else:
123+
context_menu_items.append((addon.getLocalizedString(30022), f"RunPlugin({addon_url}/blacklist_add?id={channel_id}&name={quote(video['uploaderName'])})"))
124+
87125
listitem.addContextMenuItems(context_menu_items, replaceItems=True)
88126

89127
xbmcplugin.addDirectoryItem(addon_handle, plugin_url, listitem, False)
@@ -96,6 +134,22 @@ def list_videos(videos: list, hide_watched: bool=False, nextpage: str='') -> Non
96134
def feed() -> None:
97135
list_videos(authenticated_request('/feed?authToken=', True), addon.getSettingBool('watch_history_hide_watched_feed'))
98136

137+
def updatefeed() -> None:
138+
instance: str = addon.getSettingString('instance')
139+
140+
channels: list = authenticated_request('/subscriptions')
141+
channelcount: int = len(channels)
142+
143+
progressbar = xbmcgui.DialogProgress()
144+
progressbar.create(addon.getLocalizedString(30021))
145+
146+
for i in range(channelcount):
147+
channel = channels[i]
148+
progressbar.update(int((i + 1) / channelcount * 100), f"{i + 1}/{channelcount} | {channel['name']}")
149+
authenticated_request(channel['url'])
150+
151+
feed()
152+
99153
def list_channels(channels: list, nextpage: str='') -> None:
100154
for channel in channels:
101155
info: str = ''
@@ -136,10 +190,11 @@ def list_playlists(playlists: list, nextpage: str='') -> None:
136190

137191
if 'id' not in playlist:
138192
playlist['id'] = get_component(playlist['url'])['params']['list']
139-
193+
140194
listitem = xbmcgui.ListItem(playlist['name'])
141195
listitem.setArt(dict(
142-
thumb = playlist['thumbnail']
196+
thumb = playlist['thumbnail'],
197+
fanart = playlist['thumbnail'].replace('hqdefault.jpg', 'maxresdefault.jpg')
143198
))
144199

145200
tag = listitem.getVideoInfoTag()
@@ -169,6 +224,19 @@ def playlist(playlist_id: str, hide_watched=None) -> None:
169224
def watch_history() -> None:
170225
playlist(addon.getSettingString('watch_history_playlist'), False)
171226

227+
def blacklist_section() -> None:
228+
blacklist: dict = dict(sorted(blacklist_load().items(), key=lambda x: x[1]['name'].lower()))
229+
230+
for channel_id in blacklist.keys():
231+
listitem = xbmcgui.ListItem(blacklist[channel_id]['name'])
232+
233+
tag = listitem.getVideoInfoTag()
234+
tag.setTitle(blacklist[channel_id]['name'])
235+
236+
xbmcplugin.addDirectoryItem(addon_handle, f"{addon_url}/blacklist_remove?id={channel_id}&prompt=true", listitem, False)
237+
238+
xbmcplugin.endOfDirectory(addon_handle)
239+
172240
def channel(channel_id: str, nextpage: str="") -> None:
173241
instance: str = addon.getSettingString('instance')
174242

@@ -235,9 +303,11 @@ def router(argv: list) -> None:
235303
routes: dict = {
236304
'home': {},
237305
'feed': {},
306+
'updatefeed': {},
238307
'settings': {},
239308
'subscriptions': {},
240309
'playlists': {},
310+
'blacklist_section': {},
241311
'search_select': {},
242312
'search': {
243313
'search_filter': component['params']['search_filter'] if 'search_filter' in component['params'] else '',
@@ -258,17 +328,25 @@ def router(argv: list) -> None:
258328
'mark_as_unwatched': {
259329
'video_id': component['params']['id'] if 'id' in component['params'] else ''
260330
},
331+
'blacklist_add': {
332+
'channel_id': component['params']['id'] if 'id' in component['params'] else '',
333+
'channel_name': unquote(component['params']['name']) if 'name' in component['params'] else ''
334+
},
335+
'blacklist_remove': {
336+
'channel_id': component['params']['id'] if 'id' in component['params'] else '',
337+
'prompt': True if 'prompt' in component['params'] and component['params']['prompt'] == 'true' else False
338+
},
261339
'watch': {
262-
'video_id': sub(r'.*\/', '', component['path'])
340+
'video_id': re.sub(r'.*\/', '', component['path'])
263341
},
264342
'channel': {
265-
'channel_id': sub(r'.*\/', '', component['path']),
343+
'channel_id': re.sub(r'.*\/', '', component['path']),
266344
'nextpage': component['params']['nextpage'] if 'nextpage' in component['params'] else ''
267345
},
268346
}
269347

270-
route: str = sub(r'\/.*', '', component['path'][1:])
348+
route: str = re.sub(r'\/.*', '', component['path'][1:])
271349
if route == '': route = 'home'
272350

273351
if route in routes:
274-
globals()[route](**routes[route])
352+
globals()[route](**routes[route])

0 commit comments

Comments
 (0)