Repository: kolsys/YouTubeTV.bundle
Branch: master
Commit: 93c231df79e4
Files: 25
Total size: 103.0 KB
Directory structure:
gitextract_uz02c5iz/
├── CNAME
├── Contents/
│ ├── Code/
│ │ ├── __init__.py
│ │ └── updater.py
│ ├── DefaultPrefs.json
│ ├── Info.plist
│ ├── Services/
│ │ ├── ServiceInfo.plist
│ │ ├── Shared Code/
│ │ │ ├── jsinterp.pys
│ │ │ └── video.pys
│ │ └── URL/
│ │ └── YouTubeTV/
│ │ ├── ServiceCode.pys
│ │ └── ServicePrefs.json
│ └── Strings/
│ ├── da.json
│ ├── de.json
│ ├── en.json
│ ├── es.json
│ ├── fr-BE.json
│ ├── fr.json
│ ├── hu.json
│ ├── it.json
│ ├── nl.json
│ ├── pl.json
│ ├── ru.json
│ └── sv.json
├── LICENSE
├── README.md
└── _config.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: CNAME
================================================
kolsys.ml
================================================
FILE: Contents/Code/__init__.py
================================================
# -*- coding: utf-8 -*-
# Copyright (c) 2014, KOL
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the <organization> nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from urllib import urlencode
from time import time
from updater import Updater
Video = SharedCodeService.video
PREFIX = '/video/youtubetv'
ART = 'art-default.jpg'
ICON = 'icon-default.png'
TITLE = u'%s' % L('Title')
YT_CLIENT_ID = (
'383749313750-e0fj400djq4lukahnfjfqg6ckdbets63'
'.apps.googleusercontent.com'
)
YT_SECRET = 'rHHvL6tgl8Ej9KngduayT2ce'
YT_SCOPE = 'https://www.googleapis.com/auth/youtube'
YT_VERSION = 'v3'
ICONS = {
'likes': R('heart-full.png'),
'favorites': R('star-2.png'),
'uploads': R('outbox-2.png'),
'watchHistory': R('revert.png'),
'watchLater': R('clock.png'),
'subscriptions': R('podcast-2.png'),
'browseChannels': R('grid-2.png'),
'playlists': R('list.png'),
'whatToWhatch': R('home.png'),
'account': R('user-2.png'),
'categories': R('store.png'),
'options': R('settings.png'),
'suggestions': R('tag.png'),
'remove': R('bin.png'),
'next': R('arrow-right.png'),
'offline': R('power.png'),
'search': R('search.png'),
}
YT_EDITABLE = {
'watchLater': L('watchLater'),
'likes': L('I like this'),
'favorites': L('Add to favorites'),
}
YT_FEEDS = {
'_SB': {'u': 'feed/subscriptions', 'title': L('My Subscriptions')},
'HL': {'u': 'feed/history', 'title': L('watchHistory')},
'WL': {'u': 'playlist', 'title': L('watchLater')},
}
###############################################################################
# Init
###############################################################################
Plugin.AddViewGroup(
'details',
viewMode='InfoList',
type=ViewType.List,
summary=SummaryTextType.Long
)
def Start():
HTTP.CacheTime = CACHE_1HOUR
ValidatePrefs()
def ValidatePrefs():
loc = GetLanguage()
if Core.storage.file_exists(Core.storage.abs_path(
Core.storage.join_path(
Core.bundle_path,
'Contents',
'Strings',
'%s.json' % loc
)
)):
Locale.DefaultLocale = loc
else:
Locale.DefaultLocale = 'en-us'
###############################################################################
# Video
###############################################################################
@handler(PREFIX, TITLE, thumb=ICON)
def MainMenu(complete=False, offline=False):
oc = ObjectContainer(title2=TITLE, no_cache=True, replace_parent=False)
if offline:
ResetToken()
if not CheckToken():
oc.add(DirectoryObject(
key=Callback(Authorization),
title=u'%s' % L('Authorize'),
thumb=ICONS['options'],
))
if complete:
oc.header = L('Authorize')
oc.message = L('You must enter code for continue')
return oc
Updater(PREFIX+'/update', oc)
oc.add(DirectoryObject(
key=Callback(Feed, oid='_SB'),
title=u'%s' % L('My Subscriptions'),
thumb=ICONS['subscriptions'],
))
oc.add(DirectoryObject(
key=Callback(Category, title=L('What to Watch')),
title=u'%s' % L('What to Watch'),
thumb=ICONS['whatToWhatch'],
))
oc.add(DirectoryObject(
key=Callback(Playlists, uid='me', title=L('Playlists')),
title=u'%s' % L('Playlists'),
thumb=ICONS['playlists'],
))
oc.add(DirectoryObject(
key=Callback(Categories, title=L('Categories'), c_type='video'),
title=u'%s' % L('Categories'),
thumb=ICONS['categories'],
))
oc.add(DirectoryObject(
key=Callback(Categories, title=L('Browse channels'), c_type='guide'),
title=u'%s' % L('Browse channels'),
thumb=ICONS['browseChannels'],
))
oc.add(DirectoryObject(
key=Callback(Channel, oid='me', title=L('My channel')),
title=u'%s' % L('My channel'),
thumb=ICONS['account'],
))
FillChannelInfo(oc, 'me', ('watchLater', 'watchHistory', 'likes'))
oc.add(InputDirectoryObject(
key=Callback(
Search,
s_type='video',
title=u'%s' % L('Search Video')
),
title=u'%s' % L('Search'), prompt=u'%s' % L('Search Video'),
thumb=ICONS['search']
))
return AddSubscriptions(oc, uid='me')
@route(PREFIX + '/feed')
def Feed(oid, offset=None):
if not CheckToken():
return NoContents()
params = {
'access_token': Dict['access_token'],
'ajax': 1,
}
if offset:
path = 'feed'
params['action_continuation'] = 1
params.update(JSON.ObjectFromString(offset))
else:
path = YT_FEEDS[oid]['u']
if YT_FEEDS[oid]['u'] == 'playlist':
params['list'] = oid
path = YT_FEEDS[oid]['u']
try:
res = JSON.ObjectFromString(HTTP.Request(
'https://m.youtube.com/%s?%s' % (path, urlencode(params)),
headers={
'User-Agent': Video.USER_AGENT
}
).content[4:])['content']
except:
return NoContents()
if 'single_column_browse_results' in res:
for item in res['single_column_browse_results']['tabs']:
if 'selected' in item and item['selected'] is True:
res = item['content']
break
elif 'section_list' in res and len(res['section_list']['contents']):
for item in res['section_list']['contents']:
if item['item_type'] == 'playlist_video_list':
res = item
break
elif 'contents' in item:
for subitem in item['contents']:
if subitem['item_type'] == 'playlist_video_list':
res = subitem
break
else:
continue
break
elif 'continuation_contents' in res:
res = res['continuation_contents']
else:
return NoContents()
if not 'contents' in res or not len(res['contents']):
return NoContents()
ids = []
if 'continuations' in res and len(res['continuations']):
continuations = res['continuations']
else:
continuations = None
for item in res['contents']:
if 'continuations' in item and len(item['continuations']):
continuations = item['continuations']
vid = Video.GetFeedVid(item)
if vid is not None:
ids.append(vid)
continue
for subitem in item['contents']:
vid = Video.GetFeedVid(subitem)
if vid is not None:
ids.append(vid)
if not len(ids):
return NoContents()
oc = ObjectContainer(title2=u'%s' % YT_FEEDS[oid]['title'])
chunk_size = 50
extended = Prefs['my_subscriptions_extened'] if oid == '_SB' else Prefs['playlists_extened']
[AddVideos(
oc,
ApiGetVideos(ids=ids[i:i + chunk_size]),
extended=extended
) for i in xrange(0, len(ids), chunk_size)]
if continuations is None:
return oc
# Add offset
for item in continuations:
if item['item_type'] == 'next_continuation_data':
oc.add(NextPageObject(
key=Callback(
Feed,
oid=oid,
offset=JSON.StringFromObject({
'itct': item['click_tracking_params'],
'ctoken': item['continuation'],
}),
),
title=u'%s' % L('Next page'),
thumb=ICONS['next']
))
break
return oc
@route(PREFIX + '/video/view')
def VideoView(vid, **kwargs):
return URLService.MetadataObjectForURL(
url=Video.GetServiceURL(vid, Dict['access_token'], GetLanguage()),
in_container=True
)
@route(PREFIX + '/video/info')
def VideoInfo(vid, pl_item_id=None):
oc = ObjectContainer()
res = ApiGetVideos(ids=[vid])
AddVideos(oc, res, title=L('Play video'))
if not len(oc):
return NoContents()
item = res['items'][0]
oc.title2 = u'%s' % item['snippet']['localized']['title']
oc.add(DirectoryObject(
key=Callback(
Channel,
oid=item['snippet']['channelId'],
title=item['snippet']['channelTitle']
),
title=u'%s' % item['snippet']['channelTitle'],
thumb=ICONS['account'],
))
oc.add(DirectoryObject(
key=Callback(
Search,
title=L('Related videos'),
query=None,
relatedToVideoId=item['id']
),
title=u'%s' % L('Related videos'),
thumb=ICONS['suggestions'],
))
for key, title in YT_EDITABLE.items():
oc.add(DirectoryObject(
key=Callback(PlaylistAdd, aid=item['id'], key=key),
title=u'%s' % title,
thumb=ICONS[key],
))
if pl_item_id:
oc.add(DirectoryObject(
key=Callback(PlaylistRemove, pl_item_id=pl_item_id),
title=u'%s' % L('Remove from playlist'),
thumb=ICONS['remove'],
))
return AddItemsFromDescription(
oc,
item['snippet']['localized']['description']
)
@route(PREFIX + '/channels')
def Channels(oid, title, offset=None):
res = ApiRequest('channels', ApiGetParams(
categoryId=oid,
hl=GetLanguage(),
limit=Prefs['items_per_page'],
offset=offset
))
if not res or not len(res['items']):
return NoContents()
oc = ObjectContainer(
title2=u'%s' % title,
replace_parent=bool(offset)
)
for item in res['items']:
cid = item['id']
item = item['snippet']
oc.add(DirectoryObject(
key=Callback(
Channel,
oid=cid,
title=item['title']
),
title=u'%s' % item['title'],
summary=u'%s' % item['description'],
thumb=GetThumbFromSnippet(item),
))
if 'nextPageToken' in res:
oc.add(NextPageObject(
key=Callback(
Channels,
oid=oid,
title=title,
offset=res['nextPageToken'],
),
title=u'%s' % L('Next page'),
thumb=ICONS['next']
))
return oc
@route(PREFIX + '/channel')
def Channel(oid, title):
oc = ObjectContainer(
title2=u'%s' % title
)
# Add standart menu
FillChannelInfo(oc, oid)
if oid == 'me':
oc.add(DirectoryObject(
key=Callback(MainMenu, offline=True),
title=u'%s' % L('Sign out'),
thumb=ICONS['offline'],
))
return oc
oc.add(DirectoryObject(
key=Callback(
Subscriptions,
title=u'%s - %s' % (title, L('Subscriptions')),
uid=oid
),
title=u'%s' % L('Subscriptions'),
thumb=ICONS['subscriptions'],
))
oc.add(InputDirectoryObject(
key=Callback(
Search,
s_type='video',
channelId=oid,
title=u'%s' % L('Search Channel')
),
title=u'%s' % L('Search'), prompt=u'%s' % L('Search Channel'),
thumb=ICONS['search']
))
AddPlaylists(oc, uid=oid)
return oc
@route(PREFIX + '/user')
def User(username):
res = ApiRequest('channels', ApiGetParams(
forUsername=username,
hl=GetLanguage()
))
if not res or not len(res['items']):
return NoContents()
item = res['items'][0]
return Channel(item['id'], item['snippet']['localized']['title'])
@route(PREFIX + '/categories')
def Categories(title, c_type):
res = ApiRequest('%sCategories' % c_type, ApiGetParams(
regionCode=GetRegion(),
hl=GetLanguage()
))
if not res or not len(res['items']):
return NoContents()
oc = ObjectContainer(
title2=u'%s' % title
)
if c_type == 'guide':
c_callback = Channels
oc.add(InputDirectoryObject(
key=Callback(
Search,
s_type='channel',
title=u'%s' % L('Search channels')
),
title=u'%s' % L('Search'), prompt=u'%s' % L('Search channels'),
thumb=ICONS['search']
))
else:
c_callback = Category
for item in res['items']:
oc.add(DirectoryObject(
key=Callback(
c_callback,
title=item['snippet']['title'],
oid=item['id']
),
title=u'%s' % item['snippet']['title']
))
return oc
@route(PREFIX + '/category')
def Category(title, oid=0, offset=None):
oc = ObjectContainer(
title2=u'%s' % title,
replace_parent=bool(offset)
)
res = ApiGetVideos(
chart='mostPopular',
limit=Prefs['items_per_page'],
offset=offset,
regionCode=GetRegion(),
videoCategoryId=oid
)
AddVideos(oc, res, extended=Prefs['category_extened'])
if not len(oc):
return NoContents()
if 'nextPageToken' in res:
oc.add(NextPageObject(
key=Callback(
Category,
title=oc.title2,
oid=oid,
offset=res['nextPageToken'],
),
title=u'%s' % L('Next page'),
thumb=ICONS['next']
))
return oc
@route(PREFIX + '/playlists')
def Playlists(uid, title, offset=None):
oc = ObjectContainer(
title2=u'%s' % title,
replace_parent=bool(offset)
)
if not offset and uid == 'me':
FillChannelInfo(oc, uid, ('watchLater', 'likes', 'favorites'))
oc.add(InputDirectoryObject(
key=Callback(
Search,
s_type='playlist',
title=u'%s' % L('Search playlists')
),
title=u'%s' % L('Search'), prompt=u'%s' % L('Search playlists'),
thumb=ICONS['search']
))
return AddPlaylists(oc, uid=uid, offset=offset)
@route(PREFIX + '/playlist')
def Playlist(oid, title, can_edit=False, offset=None):
if oid in YT_FEEDS:
return Feed(oid)
res = ApiRequest('playlistItems', ApiGetParams(
part='contentDetails',
playlistId=oid,
offset=offset,
limit=Prefs['items_per_page']
))
if not res or not len(res['items']):
return NoContents()
oc = ObjectContainer(
title2=u'%s' % title,
replace_parent=bool(offset)
)
ids = []
pl_map = {}
can_edit = can_edit and can_edit != 'False'
for item in res['items']:
ids.append(item['contentDetails']['videoId'])
if can_edit:
pl_map[item['contentDetails']['videoId']] = item['id']
AddVideos(
oc,
ApiGetVideos(ids=ids),
extended=Prefs['playlists_extened'],
pl_map=pl_map
)
if 'nextPageToken' in res:
oc.add(NextPageObject(
key=Callback(
Playlist,
title=oc.title2,
oid=oid,
can_edit=can_edit,
offset=res['nextPageToken'],
),
title=u'%s' % L('Next page'),
thumb=ICONS['next']
))
return oc
@route(PREFIX + '/playlist/add')
def PlaylistAdd(aid, key=None, oid=None, a_type='video'):
if key is not None:
items = ApiGetChannelInfo('me')['playlists']
if key in items:
oid = items[key]
if not oid:
return ErrorMessage()
res = ApiRequest('playlistItems', {'part': 'snippet'}, data={
'snippet': {
'playlistId': oid,
'resourceId': {
'kind': 'youtube#'+a_type,
a_type+'Id': aid,
}
}
})
if not res:
return ErrorMessage()
return SuccessMessage()
def PlaylistRemove(pl_item_id):
if ApiRequest('playlistItems', {'id': pl_item_id}, rmethod='DELETE'):
return SuccessMessage()
return ErrorMessage()
@route(PREFIX + '/subscriptions')
def Subscriptions(uid, title, offset=None):
oc = ObjectContainer(
title2=u'%s' % L('Subscriptions'),
replace_parent=bool(offset)
)
return AddSubscriptions(oc, uid=uid, offset=offset)
def AddVideos(oc, res, title=None, extended=False, pl_map={}):
if not res or not len(res['items']):
return oc
for item in res['items']:
snippet = item['snippet']
duration = Video.ParseDuration(
item['contentDetails']['duration']
)*1000
summary = u'%s\n%s' % (snippet['channelTitle'], snippet['description'])
if extended:
pl_item_id = pl_map[item['id']] if item['id'] in pl_map else None
oc.add(DirectoryObject(
key=Callback(VideoInfo, vid=item['id'], pl_item_id=pl_item_id),
title=u'%s' % snippet['title'],
summary=summary,
thumb=GetThumbFromSnippet(snippet),
duration=duration,
))
else:
oc.add(VideoClipObject(
key=Callback(VideoView, vid=item['id']),
rating_key=Video.GetServiceURL(item['id']),
title=u'%s' % snippet['title'] if title is None else title,
summary=summary,
thumb=GetThumbFromSnippet(snippet),
duration=duration,
originally_available_at=Datetime.ParseDate(
snippet['publishedAt']
).date(),
items=URLService.MediaObjectsForURL(
Video.GetServiceURL(item['id'], Dict['access_token'])
)
))
return oc
def FillChannelInfo(oc, uid, pl_types=None):
info = ApiGetChannelInfo(uid)
if info['banner'] is not None:
oc.art = info['banner']
if not info['playlists']:
return oc
if pl_types is not None:
items = dict(filter(
lambda v: v[0] in pl_types,
info['playlists'].items()
))
else:
items = info['playlists']
for key in sorted(
items,
key=lambda v: v != 'uploads'
):
oc.add(DirectoryObject(
key=Callback(
Playlist,
oid=items[key],
title=L(key),
can_edit=uid == 'me' and key in YT_EDITABLE
),
title=u'%s' % L(key),
thumb=ICONS[key] if key in ICONS else None,
))
return oc
def AddPlaylists(oc, uid, offset=None):
res = ApiRequest('playlists', ApiGetParams(
uid=uid,
limit=GetLimitForOC(oc),
offset=offset,
hl=GetLanguage()
))
if res:
if 'items' in res:
for item in res['items']:
oid = item['id']
item = item['snippet']
oc.add(DirectoryObject(
key=Callback(
Playlist,
oid=oid,
title=item['localized']['title'],
can_edit=uid == 'me'
),
title=u'%s' % item['localized']['title'],
summary=u'%s' % item['localized']['description'],
thumb=GetThumbFromSnippet(item),
))
if 'nextPageToken' in res:
oc.add(NextPageObject(
key=Callback(
Playlists,
uid=uid,
title=oc.title2,
offset=res['nextPageToken'],
),
title=u'%s' % L('More playlists'),
thumb=ICONS['next']
))
if not len(oc):
return NoContents()
return oc
def AddSubscriptions(oc, uid, offset=None):
res = ApiRequest('subscriptions', ApiGetParams(
uid=uid,
limit=GetLimitForOC(oc),
offset=offset,
order=str(Prefs['subscriptions_order']).lower()
))
if res:
if 'items' in res:
for item in res['items']:
item = item['snippet']
oc.add(DirectoryObject(
key=Callback(
Channel,
oid=item['resourceId']['channelId'],
title=item['title']
),
title=u'%s' % item['title'],
summary=u'%s' % item['description'],
thumb=GetThumbFromSnippet(item),
))
if 'nextPageToken' in res:
offset = res['nextPageToken']
oc.add(NextPageObject(
key=Callback(
Subscriptions,
uid=uid,
title=oc.title2,
offset=offset,
),
title=u'%s' % L('More subscriptions'),
thumb=ICONS['next']
))
if not len(oc):
return NoContents()
return oc
def AddItemsFromDescription(oc, description):
links = Video.ParseLinksFromDescription(description)
if not len(links):
return oc;
for (ext_title, url) in links:
ext_title = ext_title.strip()
if '/user/' in url:
oc.add(DirectoryObject(
key=Callback(User, username=Video.GetOID(url)),
title=u'[*] %s' % ext_title,
))
continue
elif '/channel/' in url:
oc.add(DirectoryObject(
key=Callback(Channel, oid=Video.GetOID(url), title=ext_title),
title=u'[*] %s' % ext_title,
))
continue
try:
ext_vid = URLService.NormalizeURL(url)
except:
continue
if ext_vid is None:
continue
if 'playlist?' in ext_vid:
ext_vid = ext_vid[ext_vid.rfind('list=')+5:]
oc.add(DirectoryObject(
key=Callback(Playlist, oid=ext_vid, title=ext_title),
title=u'[*] %s' % ext_title
))
else:
ext_vid = Video.GetOID(ext_vid)
oc.add(DirectoryObject(
key=Callback(VideoInfo, vid=ext_vid),
title=u'[*] %s' % ext_title,
thumb=Video.GetThumb(ext_vid)
))
return oc
def Search(query=None, title=L('Search'), s_type='video', offset=0, **kwargs):
if not query and not kwargs:
return NoContents()
is_video = s_type == 'video'
res = ApiRequest('search', ApiGetParams(
part='id' if is_video else 'snippet',
q=query,
type=s_type,
regionCode=GetRegion(),
videoDefinition='high' if is_video and Prefs['search_hd'] else '',
offset=offset,
limit=Prefs['items_per_page'],
**kwargs
))
if not res or not len(res['items']):
return NoContents()
oc = ObjectContainer(
title2=u'%s' % title,
replace_parent=bool(offset)
)
if is_video:
ids = []
for item in res['items']:
ids.append(item['id']['videoId'])
AddVideos(oc, ApiGetVideos(ids=ids), extended=Prefs['search_extened'])
else:
s_callback = Channel if s_type == 'channel' else Playlist
s_key = s_type+'Id'
for item in res['items']:
oid = item['id'][s_key]
item = item['snippet']
oc.add(DirectoryObject(
key=Callback(
s_callback,
title=item['title'],
oid=oid
),
title=u'%s' % item['title'],
summary=u'%s' % item['description'],
thumb=GetThumbFromSnippet(item),
))
if 'nextPageToken' in res:
oc.add(NextPageObject(
key=Callback(
Search,
query=query,
title=oc.title2,
s_type=s_type,
offset=res['nextPageToken'],
**kwargs
),
title=u'%s' % L('Next page'),
thumb=ICONS['next']
))
return oc
@route(PREFIX + '/authorization')
def Authorization():
code = None
if CheckAccessData('device_code'):
code = Dict['user_code']
url = Dict['verification_url']
else:
res = OAuthRequest({'scope': YT_SCOPE}, 'device/code')
if res:
code = res['user_code']
url = res['verification_url']
StoreAccessData(res)
if code:
oc = ObjectContainer(
view_group='details',
no_cache=True,
objects=[
DirectoryObject(
key=Callback(MainMenu, complete=True),
title=u'%s' % F('codeIs', code),
summary=u'%s' % F('enterCodeSite', code, url),
tagline=url,
),
DirectoryObject(
key=Callback(MainMenu, complete=True),
title=u'%s' % L('Authorize'),
summary=u'%s' % L('Complete authorization'),
),
]
)
return oc
return ObjectContainer(
header=u'%s' % L('Error'),
message=u'%s' % L('Service temporarily unavailable')
)
###############################################################################
# Common
###############################################################################
def NoContents():
return ObjectContainer(
header=u'%s' % L('Error'),
message=u'%s' % L('No entries found')
)
def SuccessMessage():
return ObjectContainer(
header=u'%s' % L('Success'),
message=u'%s' % L('Action complete')
)
def ErrorMessage():
return ObjectContainer(
header=u'%s' % L('Error'),
message=u'%s' % L('An error has occurred')
)
def NotImplemented(**kwargs):
return ObjectContainer(
header=u'%s' % L('Not Implemented'),
message=u'%s' % L('This function not implemented yet')
)
def GetRegion():
return Prefs['region'].split('/')[1]
def GetLanguage():
return Prefs['language'].split('/')[1]
def GetLimitForOC(oc):
ret = int(Prefs['items_per_page'])-len(oc)
return 8 if ret <= 0 else ret
def GetThumbFromSnippet(snippet):
try:
return snippet['thumbnails']['high']['url']
except:
return ''
def ApiGetVideos(ids=[], title=None, extended=False, **kwargs):
return ApiRequest('videos', ApiGetParams(
part='snippet,contentDetails',
hl=GetLanguage(),
id=','.join(ids),
**kwargs
))
def ApiGetChannelInfo(uid):
res = ApiRequest('channels', ApiGetParams(
part='contentDetails,brandingSettings',
hl=GetLanguage(),
uid=uid,
id=uid if uid != 'me' else None
))
ret = {
'playlists': {},
'banner': None
}
if res and res['items']:
res = res['items'][0]
ret['playlists'] = res['contentDetails']['relatedPlaylists']
try:
ret['banner'] = res['brandingSettings']['image']['bannerTvHighImageUrl']
except:
pass
return ret
def ApiRequest(method, params, data=None, rmethod=None):
if not CheckToken():
return None
params['access_token'] = Dict['access_token']
is_change = data or rmethod == 'DELETE'
try:
res = HTTP.Request(
'https://www.googleapis.com/youtube/%s/%s?%s' % (
YT_VERSION,
method,
urlencode(params)
),
headers={'Content-Type': 'application/json; charset=UTF-8'},
data=None if not data else JSON.StringFromObject(data),
method=rmethod,
cacheTime=0 if is_change else CACHE_1HOUR
).content
except Exception as e:
Log.Debug(str(e))
return None
if is_change:
HTTP.ClearCache()
return True
try:
res = JSON.ObjectFromString(res)
except:
return None
if 'error' in res:
return None
return res
def ApiGetParams(part='snippet', offset=None, limit=None, uid=None, **kwargs):
params = {
'part': part,
}
if uid is not None:
if uid == 'me':
params['mine'] = 'true'
else:
params['channelId'] = uid
if offset:
params['pageToken'] = offset
if limit:
params['maxResults'] = limit
params.update(filter(lambda v: v[1], kwargs.items()))
return params
def CheckToken():
if CheckAccessData('access_token'):
return True
if 'refresh_token' in Dict:
res = OAuthRequest({
'refresh_token': Dict['refresh_token'],
'grant_type': 'refresh_token',
})
if res:
StoreAccessData(res)
return True
if CheckAccessData('device_code'):
res = OAuthRequest({
'code': Dict['device_code'],
'grant_type': 'http://oauth.net/grant_type/device/1.0',
})
if res:
StoreAccessData(res)
return True
return False
def ResetToken():
del Dict['access_token']
del Dict['refresh_token']
del Dict['device_code']
Dict.Reset()
Dict.Save()
def OAuthRequest(params, rtype='token'):
params['client_id'] = YT_CLIENT_ID
if rtype == 'token':
params['client_secret'] = YT_SECRET
try:
res = JSON.ObjectFromURL(
'https://accounts.google.com/o/oauth2/' + rtype,
values=params,
cacheTime=0
)
if 'error' in res:
res = False
except:
res = False
return res
def CheckAccessData(key):
return (key in Dict and Dict['expires'] >= int(time()))
def StoreAccessData(data):
if 'expires_in' in data:
data['expires'] = int(time()) + int(data['expires_in'])
for key, val in data.items():
Dict[key] = val
================================================
FILE: Contents/Code/updater.py
================================================
# -*- coding: utf-8 -*-
#
# Plex Plugin Updater
# $Id$
#
# Universal plugin updater module for Plex Server Channels that
# implement automatic plugins updates from remote config.
# Support Github API by default
#
# https://github.com/kolsys/plex-channel-updater
#
# Copyright (c) 2014, KOL
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the <organization> nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
KEY_PLIST_VERSION = 'CFBundleVersion'
KEY_PLIST_URL = 'PlexPluginVersionUrl'
KEY_DATA_VERSION = 'tag_name'
KEY_DATA_DESC = 'body'
KEY_DATA_ZIPBALL = 'zipball_url'
CHECK_INTERVAL = CACHE_1HOUR * 12
class Updater:
info = None
update = None
def __init__(self, prefix, oc):
if self.InitBundleInfo() and self.IsUpdateAvailable():
Route.Connect(prefix, self.DoUpdate)
oc.add(DirectoryObject(
key=Callback(self.DoUpdate),
title=u'%s' % F(
'Update available: %s',
self.update['version']
),
summary=u'%s\n%s' % (L(
'Install latest version of the channel.'
), self.update['info']),
))
def NormalizeVersion(self, version):
if version[:1] == 'v':
version = version[1:]
return version
def ParseVersion(self, version):
try:
return tuple(map(int, (version.split('.'))))
except:
# String comparison by default
return version
def IsUpdateAvailable(self):
try:
info = JSON.ObjectFromURL(
self.info['url'],
cacheTime=CHECK_INTERVAL,
timeout=5
)
version = self.NormalizeVersion(info[KEY_DATA_VERSION])
dist_url = info[KEY_DATA_ZIPBALL]
except:
return False
if self.ParseVersion(version) > self.ParseVersion(
self.info['version']
):
self.update = {
'version': version,
'url': dist_url,
'info': info[KEY_DATA_DESC] if KEY_DATA_DESC in info else '',
}
return bool(self.update)
def InitBundleInfo(self):
try:
plist = Plist.ObjectFromString(Core.storage.load(
Core.storage.abs_path(
Core.storage.join_path(
Core.bundle_path,
'Contents',
'Info.plist'
)
)
))
self.info = {
'version': plist[KEY_PLIST_VERSION],
'url': plist[KEY_PLIST_URL],
}
except:
pass
return bool(self.info)
def DoUpdate(self):
try:
zip_data = Archive.ZipFromURL(self.update['url'])
bundle_path = Core.storage.abs_path(Core.bundle_path)
for name in zip_data.Names():
data = zip_data[name]
parts = name.split('/')
shifted = Core.storage.join_path(*parts[1:])
full = Core.storage.join_path(bundle_path, shifted)
if '/.' in name:
continue
if name.endswith('/'):
Core.storage.ensure_dirs(full)
else:
Core.storage.save(full, data)
del zip_data
return ObjectContainer(
header=u'%s' % L('Success'),
message=u'%s' % F(
'Channel updated to version %s',
self.update['version']
)
)
except Exception as e:
return ObjectContainer(
header=u'%s' % L('Error'),
message=u'%s' % e
)
================================================
FILE: Contents/DefaultPrefs.json
================================================
[
{
"id": "my_subscriptions_extened",
"type": "bool",
"label": "Extended video info in My subscriptions",
"default": "false",
},
{
"id": "playlists_extened",
"type": "bool",
"label": "Extended video info in Playlists",
"default": "false",
},
{
"id": "category_extened",
"type": "bool",
"label": "Extended video info in Categories",
"default": "true",
},
{
"id": "search_extened",
"type": "bool",
"label": "Extended video info in Search",
"default": "true",
},
{
"id": "search_hd",
"type": "bool",
"label": "Search HD only",
"default": "true",
},
{
"id": "items_per_page",
"type": "enum",
"label": "Items per page",
"values": ["16", "32", "48"],
"default": "16",
},
{
"id": "subscriptions_order",
"type": "enum",
"label": "Subscriptions list sorting order",
"values": ["Relevance", "Alphabetical"],
"default": "Relevance",
},
{
"id": "region",
"type": "enum",
"label": "Preferred Region for Videos and channels",
"default": "Worldwide (All)/US",
"values": [
"Worldwide (All)/US",
"Algeria/DZ",
"Argentina/AR",
"Australia/AU",
"Austria/AT",
"Bahrain/BH",
"Belgium/BE",
"Bosnia and Herzegovina/BA",
"Brazil/BR",
"Bulgaria/BG",
"Canada/CA",
"Chile/CL",
"Colombia/CO",
"Croatia/HR",
"Czech Republic/CZ",
"Denmark/DK",
"Egypt/EG",
"Estonia/EE",
"Finland/FI",
"France/FR",
"Germany/DE",
"Ghana/GH",
"Greece/GR",
"Hong Kong/HK",
"Hungary/HU",
"India/IN",
"Indonesia/ID",
"Ireland/IE",
"Israel/IL",
"Italy/IT",
"Japan/JP",
"Jordan/JO",
"Kenya/KE",
"Kuwait/KW",
"Latvia/LV",
"Lebanon/LB",
"Libya/LY",
"Lithuania/LT",
"Macedonia/MK",
"Malaysia/MY",
"Mexico/MX",
"Montenegro/ME",
"Morocco/MA",
"Netherlands/NL",
"New Zealand/NZ",
"Nigeria/NG",
"Norway/NO",
"Oman/OM",
"Peru/PE",
"Philippines/PH",
"Poland/PL",
"Portugal/PT",
"Puerto Rico/PR",
"Qatar/QA",
"Romania/RO",
"Russia/RU",
"Saudi Arabia/SA",
"Senegal/SN",
"Serbia/RS",
"Singapore/SG",
"Slovakia/SK",
"Slovenia/SI",
"South Africa/ZA",
"South Korea/KR",
"Spain/ES",
"Sweden/SE",
"Switzerland/CH",
"Taiwan/TW",
"Thailand/TH",
"Tunisia/TN",
"Turkey/TR",
"Uganda/UG",
"Ukraine/UA",
"United Arab Emirates/AE",
"United Kingdom/GB",
"Vietnam/VN",
"Yemen/YE"
]
},
{
"id": "language",
"type": "enum",
"label": "Preferred language for Videos and channels",
"default": "English/en",
"values": [
"Afrikaans/af",
"Albanian/sq",
"Amharic/am",
"Arabic/ar",
"Armenian/hy",
"Azerbaijani/az",
"Basque/eu",
"Bengali/bn",
"Bulgarian/bg",
"Catalan/ca",
"Chinese (Hong Kong)/zh-HK",
"Chinese (Taiwan)/zh-TW",
"Chinese/zh-CN",
"Croatian/hr",
"Czech/cs",
"Danish/da",
"Dutch/nl",
"English (United Kingdom)/en-GB",
"English/en",
"Estonian/et",
"Filipino/fil",
"Finnish/fi",
"French (Canada)/fr-CA",
"French/fr",
"Galician/gl",
"Georgian/ka",
"German/de",
"Greek/el",
"Gujarati/gu",
"Hebrew/iw",
"Hindi/hi",
"Hungarian/hu",
"Icelandic/is",
"Indonesian/id",
"Italian/it",
"Japanese/ja",
"Kannada/kn",
"Kazakh/kk",
"Khmer/km",
"Korean/ko",
"Kyrgyz/ky",
"Lao/lo",
"Latvian/lv",
"Lithuanian/lt",
"Macedonian/mk",
"Malay/ms",
"Malayalam/ml",
"Marathi/mr",
"Mongolian/mn",
"Myanmar (Burmese)/my",
"Nepali/ne",
"Norwegian/no",
"Persian/fa",
"Polish/pl",
"Portuguese (Brazil)/pt",
"Portuguese (Portugal)/pt-PT",
"Punjabi/pa",
"Romanian/ro",
"Russian/ru",
"Serbian/sr",
"Sinhala/si",
"Slovak/sk",
"Slovenian/sl",
"Spanish (Latin America)/es-419",
"Spanish (Spain)/es",
"Swahili/sw",
"Swedish/sv",
"Tamil/ta",
"Telugu/te",
"Thai/th",
"Turkish/tr",
"Ukrainian/uk",
"Urdu/ur",
"Uzbek/uz",
"Vietnamese/vi",
"Zulu/zu",
]
}
]
================================================
FILE: Contents/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>com.plexapp.plugins.youtube.tv</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>AAPL</string>
<key>CFBundleSignature</key>
<string>hook</string>
<key>CFBundleVersion</key>
<string>4.13</string>
<key>PlexPluginVersionUrl</key>
<string>https://go.kolsys.cf/1FuE3dz</string>
<key>PlexPluginCodePolicy</key>
<string>Elevated</string>
<key>PlexClientPlatforms</key>
<string>*</string>
<key>PlexFrameworkVersion</key>
<string>2</string>
<key>PlexMediaContainer</key>
<array>
<string>MP4</string>
</array>
<key>PlexVideoCodec</key>
<array>
<string>H.264</string>
</array>
<key>PlexAudioCodec</key>
<array>
<string>AAC</string>
</array>
<key>PlexPluginConsoleLogging</key>
<string>1</string>
<key>PlexPluginDevMode</key>
<string>0</string>
</dict>
</plist>
================================================
FILE: Contents/Services/ServiceInfo.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>URL</key>
<dict>
<key>YouTubeTV</key>
<dict>
<key>URLPatterns</key>
<array>
<string>^(https?:)?//tv\.youtube\.plugins\.plex\.com/.+</string>
<string>^(https?:)?//(www\.|m\.)?youtu(be(\.googleapis)?\.com|\.be)/(?!account(_|\?)?|artist(/|\?)|blog\?|categories\?|channels\?|charts|embed/(__videoid__|videoseries\?)|playlist\?|p/|profile\?|results\?|subscribe_widget|\?tab=).+</string>
</array>
</dict>
</dict>
</dict>
</plist>
================================================
FILE: Contents/Services/Shared Code/jsinterp.pys
================================================
# -*- coding: utf-8 -*-
#
# Based on youtube-dl lib
# https://github.com/rg3/youtube-dl/blob/master/youtube_dl/jsinterp.py
# Copyright (c) 2015, KOL
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the <organization> nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTLICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
import json
import operator
import re
OPERATORS = [
('|', operator.or_),
('^', operator.xor),
('&', operator.and_),
('>>', operator.rshift),
('<<', operator.lshift),
('-', operator.sub),
('+', operator.add),
('%', operator.mod),
('/', operator.truediv),
('*', operator.mul),
]
ASSIGNOPERATORS = [(op + '=', opfunc) for op, opfunc in OPERATORS]
ASSIGNOPERATORS.append(('=', lambda cur, right: right))
NAME_RE = r'[a-zA-Z_$][a-zA-Z_$0-9]*'
class JSInterpreter(object):
def __init__(self, code, objects=None):
if objects is None:
objects = {}
self.code = code
self.p_functions = {}
self.p_objects = objects
def interpret_statement(self, stmt, local_vars, allow_recursion=100):
if allow_recursion < 0:
raise Exception('Recursion limit reached')
should_abort = False
stmt = stmt.lstrip()
stmt_m = re.match(r'var\s', stmt)
if stmt_m:
expr = stmt[len(stmt_m.group(0)):]
else:
return_m = re.match(r'return(?:\s+|$)', stmt)
if return_m:
expr = stmt[len(return_m.group(0)):]
should_abort = True
else:
# Try interpreting it as an expression
expr = stmt
v = self.interpret_expression(expr, local_vars, allow_recursion)
return v, should_abort
def interpret_expression(self, expr, local_vars, allow_recursion):
expr = expr.strip()
if expr == '': # Empty expression
return None
if expr.startswith('('):
parens_count = 0
for m in re.finditer(r'[()]', expr):
if m.group(0) == '(':
parens_count += 1
else:
parens_count -= 1
if parens_count == 0:
sub_expr = expr[1:m.start()]
sub_result = self.interpret_expression(
sub_expr, local_vars, allow_recursion)
remaining_expr = expr[m.end():].strip()
if not remaining_expr:
return sub_result
else:
expr = json.dumps(sub_result) + remaining_expr
break
else:
raise Exception('Premature end of parens in %r' % expr)
for op, opfunc in ASSIGNOPERATORS:
m = re.match(r'''(?x)
(?P<out>%s)(?:\[(?P<index>[^\]]+?)\])?
\s*%s
(?P<expr>.*)$''' % (NAME_RE, re.escape(op)), expr)
if not m:
continue
right_val = self.interpret_expression(
m.group('expr'), local_vars, allow_recursion - 1)
if m.groupdict().get('index'):
lvar = local_vars[m.group('out')]
idx = self.interpret_expression(
m.group('index'), local_vars, allow_recursion)
assert isinstance(idx, int)
cur = lvar[idx]
val = opfunc(cur, right_val)
lvar[idx] = val
return val
else:
cur = local_vars.get(m.group('out'))
val = opfunc(cur, right_val)
local_vars[m.group('out')] = val
return val
if expr.isdigit():
return int(expr)
var_m = re.match(
r'(?!if|return|true|false)(?P<name>%s)$' % NAME_RE,
expr)
if var_m:
return local_vars[var_m.group('name')]
try:
return json.loads(expr)
except ValueError:
pass
m = re.match(
# r'(?P<var>%s)\.(?P<member>[^(]+)(?:\(+(?P<args>[^()]*)\))?$' % NAME_RE,
r'(?P<var>%s)(?:\.(?P<member>[^(]+)|\[[\'\"](?P<member_str>[^(]+)[\'\"]\])(?:\(+(?P<args>[^()]*)\))?$' % NAME_RE,
expr)
if m:
variable = m.group('var')
member = m.group('member')
if not member:
member = m.group('member_str')
arg_str = m.group('args')
if variable in local_vars:
obj = local_vars[variable]
else:
if variable not in self.p_objects:
self.p_objects[variable] = self.extract_object(variable)
obj = self.p_objects[variable]
if arg_str is None:
# Member access
if member == 'length':
return len(obj)
return obj[member]
assert expr.endswith(')')
# Function call
if arg_str == '':
argvals = tuple()
else:
argvals = tuple([
self.interpret_expression(v, local_vars, allow_recursion)
for v in arg_str.split(',')])
if member == 'split':
assert argvals == ('',)
return list(obj)
if member == 'join':
assert len(argvals) == 1
return argvals[0].join(obj)
if member == 'reverse':
assert len(argvals) == 0
obj.reverse()
return obj
if member == 'slice':
assert len(argvals) == 1
return obj[argvals[0]:]
if member == 'splice':
assert isinstance(obj, list)
index, howMany = argvals
res = []
for i in range(index, min(index + howMany, len(obj))):
res.append(obj.pop(index))
return res
return obj[member](argvals)
m = re.match(
r'(?P<in>%s)\[(?P<idx>.+)\]$' % NAME_RE, expr)
if m:
val = local_vars[m.group('in')]
idx = self.interpret_expression(
m.group('idx'), local_vars, allow_recursion - 1)
return val[idx]
for op, opfunc in OPERATORS:
m = re.match(r'(?P<x>.+?)%s(?P<y>.+)' % re.escape(op), expr)
if not m:
continue
x, abort = self.interpret_statement(
m.group('x'), local_vars, allow_recursion - 1)
if abort:
raise Exception(
'Premature left-side return of %s in %r' % (op, expr))
y, abort = self.interpret_statement(
m.group('y'), local_vars, allow_recursion - 1)
if abort:
raise Exception(
'Premature right-side return of %s in %r' % (op, expr))
return opfunc(x, y)
m = re.match(
r'^(?P<func>%s)\((?P<args>[a-zA-Z0-9_$,]+)\)$' % NAME_RE, expr)
if m:
fname = m.group('func')
argvals = tuple([
int(v) if v.isdigit() else local_vars[v]
for v in m.group('args').split(',')])
if fname not in self.p_functions:
self.p_functions[fname] = self.extract_function(fname)
return self.p_functions[fname](argvals)
raise Exception('Unsupported JS expression %r' % expr)
def extract_object(self, objname):
obj = {}
obj_m = re.search(
(r'(?:var\s+)?%s\s*=\s*\{' % re.escape(objname)) +
r'\s*(?P<fields>[\'\"]?([a-zA-Z$0-9]+[\'\"]?\s*:\s*function\(.*?\)\s*\{.*?\}\s*,*\s*)*)' +
r'\}\s*;',
self.code)
fields = obj_m.group('fields')
# Currently, it only supports function definitions
fields_m = re.finditer(
r'[\'\"]?(?P<key>[a-zA-Z$0-9]+)[\'\"]?\s*:\s*function'
r'\((?P<args>[a-z,]+)\){(?P<code>[^}]+)}',
fields)
for f in fields_m:
argnames = f.group('args').split(',')
obj[f.group('key')] = self.build_function(argnames, f.group('code'))
return obj
def extract_function(self, funcname):
func_m = re.search(
r'''(?x)
(?:function\s+%s|[{;,\s]%s\s*=\s*function)\s*
\((?P<args>[^)]*)\)\s*
\{(?P<code>[^}]+)\}''' % (
re.escape(funcname), re.escape(funcname)),
self.code)
if func_m is None:
raise Exception('Could not find JS function %r' % funcname)
argnames = func_m.group('args').split(',')
return self.build_function(argnames, func_m.group('code'))
def call_function(self, funcname, *args):
f = self.extract_function(funcname)
return f(args)
def build_function(self, argnames, code):
def resf(args):
local_vars = dict(zip(argnames, args))
for stmt in code.split(';'):
res, abort = self.interpret_statement(stmt, local_vars)
if abort:
break
return res
return resf
================================================
FILE: Contents/Services/Shared Code/video.pys
================================================
# -*- coding: utf-8 -*-
# Copyright (c) 2014, KOL
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the <organization> nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTLICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from datetime import timedelta
from jsinterp import JSInterpreter
DEFINITIONS = {
'sd': (36, 18),
'hd': (22, 18, 36),
}
RESOLUTIONS = {
22: 720,
18: 360,
36: 240,
160: 144,
133: 240,
134: 360,
135: 480,
136: 720,
298: 720.60,
137: 1080,
299: 1080.60,
}
AUDIO = {
141: 256,
140: 128,
139: 48,
}
MAX_RESOLUTION = 22
USER_AGENT = (
'Mozilla/5.0 (iPad; CPU OS 7_0_4 like Mac OS X) '
'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 '
'Mobile/11B554a Safari/9537.54'
)
RE_DATA = Regex('ytplayer.config\s*=\s*({(?:.+)});\s*(?:ytplayer.web_player_context_config|ytplayer.load)\s*=')
def GetServiceURL(vid, token='', hl=''):
return 'http://tv.youtube.plugins.plex.com/%s%s%s%s%s' % (
vid,
'&access_token=' if token else '',
token,
'&hl=' if hl else '',
hl
)
def GetMetaUrlByServiceURL(url):
return 'https://www.youtube.com/watch?v=%s' % GetOID(url)
def GetThumb(vid):
return 'https://i.ytimg.com/vi/%s/hqdefault.jpg' % vid
def GetVideoData(url):
try:
res = HTML.ElementFromURL(GetMetaUrlByServiceURL(url))
except:
raise Ex.MediaNotAvailable
ret = {}
try:
ret = JSON.ObjectFromString(RE_DATA.search(
res.xpath(
'//script[contains(., "ytplayer.config")]'
)[0].text_content()
).group(1))
except Exception as e:
Log.Error('Cannot get video data: %s' % e)
raise Ex.MediaNotAvailable
try:
cont = res.xpath('//div[@id="watch7-container"]')[0]
ret['date_published'] = cont.xpath(
'//meta[@itemprop="datePublished"]'
)[0].get('content')
cont = cont.xpath(
'//div[@id="watch-description-text"]'
)[0]
for br in cont.xpath('*//br'):
br.tail = '\n' + br.tail if br.tail else '\n'
ret['description'] = cont.text_content()
except:
pass
return ret
def GetOID(url):
return url[url.rfind('/')+1:]
def MetaFromInfo(item):
try:
return item['args']
except:
return None
def GetFeedVid(item):
if 'video_id' in item:
return item['video_id']
if 'encrypted_id' in item:
return item['encrypted_id']
if item['item_type'] == 'shelf':
return item['content']['items'][0]['encrypted_id']
return None
def GetVideoUrls(url):
info = GetVideoData(url)
meta = MetaFromInfo(info)
try:
player_url = info['assets']['js']
except:
player_url = None
ret = {}
# Live stream
if 'hlsvp' in meta and meta['hlsvp']:
Log.Debug('Parse playlist')
try:
res = HTTP.Request(meta['hlsvp']).content.splitlines()
except:
res = None
if res:
fmt_re = Regex('^#EXT-X-STREAM-INF.+,RESOLUTION=[0-9]+x([0-9]+),')
index = range(len(res))
res_map = dict(map(reversed, RESOLUTIONS.items()))
for i in index:
match = fmt_re.search(res[i])
if match:
resolution = int(match.group(1))
i = i+1
if resolution in res_map:
ret[res_map[resolution]] = res[i]
# Next line does need to processing
index.remove(i)
# Normal video
elif 'url_encoded_fmt_stream_map' in meta:
# High definition
key = 'url_encoded_fmt_stream_map'
if 'adaptive_fmts' and IsAdaptiveSupport():
key = 'adaptive_fmts'
for item in meta[key].split(','):
item = dict(map(
lambda x: (x[0], x[1][0]),
String.ParseQueryString(item).items()
))
itag = int(item['itag'])
if itag in RESOLUTIONS or itag in AUDIO:
ret[itag] = GetUrlFromStream(item, player_url)
elif 'player_response' in meta:
player_response = JSON.ObjectFromString(meta['player_response'])
if 'streamingData' in player_response:
streaming_data = player_response['streamingData']
if 'formats' in streaming_data:
for item in streaming_data['formats']:
itag = item['itag']
if itag in RESOLUTIONS or itag in AUDIO:
ret[itag] = GetUrlFromStream(item, player_url)
if not len(ret):
raise Ex.MediaNotAvailable
return ret
def ParseLinksFromDescription(text):
re = Regex(
(
r'^(.+)[\s\n]*(https?://(?:www\.|m\.)?youtu(?:be\.com|\.be)/'
'(?!account(?:_|\?)?|artist(?:/|\?)|blog\?|categories\?|'
'channels\?|charts|embed/(?:__videoid__|videoseries\?)|p/|'
'profile\?|results\?|subscribe_widget|\?tab=).+)'
),
Regex.MULTILINE
)
return re.findall(text)
def GetUrlFromStream(item, player_url):
ret = item['url']
if player_url and 's' in item:
ret = '%s&signature=%s' % (ret, DecryptSignature(item['s'], player_url))
return ret
def IsAdaptiveSupport():
return False
def DecryptSignature(s, player_url):
"""Turn the encrypted s field into a working signature"""
try: # hasattr workaround
len(DecryptSignature.cache)
except:
DecryptSignature.cache = {}
def signature_cache_id(example_sig):
return '.'.join(str(len(part)) for part in example_sig.split('.'))
def parse_sig_js(jscode):
# These regexes are pulled from the youtube-dl source. Credit to rg3
sig_regexes = (r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(',
r'yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*c\s*&&\s*d\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
r'\bc\s*&&\s*d\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
r'\bc\s*&&\s*d\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(')
searches = [Regex(rx).search(jscode) for rx in sig_regexes]
for search in searches:
if not search is None:
funcname = search.group('sig')
break
jsi = JSInterpreter(jscode)
initial_function = jsi.extract_function(funcname)
return lambda s: initial_function([s])
if player_url.startswith('//'):
player_url = 'https:' + player_url
elif player_url.startswith('/'):
player_url = 'https://www.youtube.com' + player_url
try:
player_id = (player_url, signature_cache_id(s))
if player_id not in DecryptSignature.cache:
code = HTTP.Request(player_url).content
DecryptSignature.cache[player_id] = parse_sig_js(code)
return DecryptSignature.cache[player_id](s)
except Exception as e:
Log.Error('Cannot decrypt signature: %s' % e)
return ''
def ParseDuration(durationstr):
'''
Original on https://bitbucket.org/nielsenb/aniso8601
'''
def parse_duration_element(durationstr, elementstr):
#Extracts the specified portion of a duration, for instance, given:
#durationstr = 'T4H5M6.1234S'
#elementstr = 'H'
#
#returns 4
#
#Note that the string must start with a character, so its assumed the
#full duration string would be split at the 'T'
durationstartindex = 0
durationendindex = durationstr.find(elementstr)
for characterindex in xrange(durationendindex - 1, 0, -1):
if durationstr[characterindex].isalpha() == True:
durationstartindex = characterindex
break
durationstartindex += 1
if ',' in durationstr:
#Replace the comma with a 'full-stop'
durationstr = durationstr.replace(',', '.')
return float(durationstr[durationstartindex:durationendindex])
#durationstr can be of the form PnYnMnDTnHnMnS or PnW
#Make sure only the lowest order element has decimal precision
if durationstr.count('.') > 1:
raise ValueError('String is not a valid ISO8601 duration.')
elif durationstr.count('.') == 1:
#There should only ever be 1 letter after a decimal if there is more
#then one, the string is invalid
lettercount = 0;
for character in durationstr.split('.')[1]:
if character.isalpha() == True:
lettercount += 1
if lettercount > 1:
raise ValueError('String is not a valid ISO8601 duration.')
#Parse the elements of the duration
if durationstr.find('T') == -1:
if durationstr.find('Y') != -1:
years = parse_duration_element(durationstr, 'Y')
else:
years = 0
if durationstr.find('M') != -1:
months = parse_duration_element(durationstr, 'M')
else:
months = 0
if durationstr.find('W') != -1:
weeks = parse_duration_element(durationstr, 'W')
else:
weeks = 0
if durationstr.find('D') != -1:
days = parse_duration_element(durationstr, 'D')
else:
days = 0
#No hours, minutes or seconds
hours = 0
minutes = 0
seconds = 0
else:
firsthalf = durationstr[:durationstr.find('T')]
secondhalf = durationstr[durationstr.find('T'):]
if firsthalf.find('Y') != -1:
years = parse_duration_element(firsthalf, 'Y')
else:
years = 0
if firsthalf.find('M') != -1:
months = parse_duration_element(firsthalf, 'M')
else:
months = 0
if durationstr.find('W') != -1:
weeks = parse_duration_element(durationstr, 'W')
else:
weeks = 0
if firsthalf.find('D') != -1:
days = parse_duration_element(firsthalf, 'D')
else:
days = 0
if secondhalf.find('H') != -1:
hours = parse_duration_element(secondhalf, 'H')
else:
hours = 0
if secondhalf.find('M') != -1:
minutes = parse_duration_element(secondhalf, 'M')
else:
minutes = 0
if secondhalf.find('S') != -1:
seconds = parse_duration_element(secondhalf, 'S')
else:
seconds = 0
#Note that weeks can be handled without conversion to days
totaldays = years * 365 + months * 30 + days
return int(timedelta(
weeks=weeks,
days=totaldays,
hours=hours,
minutes=minutes,
seconds=seconds
).total_seconds())
================================================
FILE: Contents/Services/URL/YouTubeTV/ServiceCode.pys
================================================
# -*- coding: utf-8 -*-
# Copyright (c) 2014, KOL
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the <organization> nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from urlparse import urlparse
import video as Video
RE_PATH_1 = Regex('^/(watch|details)(_popup)?')
RE_PATH_2 = Regex('^/[^/]+$')
RE_VID_ID = Regex('/(v|e(mbed)?)/(v/)?(?P<id>.{11})')
RE_VID_PID = Regex('pid=(.{11})')
RE_VIDEO_ID = Regex('"video_id":\s"([^"]+)')
############################################################################
def NormalizeURL(url):
''' This modified version from Services.bundle '''
if 'tv.youtube.plugins.plex.com' in url:
return url
video_id = None
if isinstance(url, basestring):
url = urlparse(url)
domain = url.netloc
# http://www.youtube.com/?v=lgTssWW2Qw4&feature=player_embedded
# http://www.youtube.com/watch?v=lgTssWW2Qw4&feature=player_embedded
# http://www.youtube.com/details?v=pjyfMCTAqKU
if url.path == '/' or RE_PATH_1.match(url.path):
qs = String.ParseQueryString(url.query)
if 'v' in qs:
video_id = qs['v'][0][0:11]
# http://youtu.be/vXhmyyXFd5I
elif domain == 'youtu.be':
video_id = url.path[1:12]
# http://www.youtube.com/user/andyverg#p/a/u/0/HTGOoQNYGL4
elif (url.path[0:6] == '/user/' or RE_PATH_2.match(url.path)) and url.fragment != '':
video_id = url.fragment.split('/')[-1]
# http://www.youtube.com/user/fujitvlive?feature=lb&v=eYpAUMZLXqo
elif url.path[0:6] == '/user/' and "v=" in url.query:
qs = String.ParseQueryString(url.query)
if 'v' in qs:
video_id = qs['v'][0][0:11]
# http://www.youtube.com/v/taaSlWjKdDY
# http://www.youtube.com/e/NO00y65njH0
# http://www.youtube.com/embed/nd5WGLWNllA
elif url.path[0:3] == '/v/' or url.path[0:3] == '/e/' or url.path[0:7] == '/embed/':
video_id = RE_VID_ID.search(url.path).group('id')
# http://www.youtube.com/my_subscriptions?pid=nCgQDjiotG0&show_edit=1&feature=uploadeemail_edit
elif url.path[0:17] == '/my_subscriptions' and url.query != '':
video_id = RE_VID_PID.findall(url.query)[0]
# http://www.youtube.com/movie/the-last-man-on-earth
elif url.path[0:7] == '/movie/':
url = HTML.ElementFromURL(url.geturl()).xpath(
'//a[contains(@href, "watch-now-button")]'
)
if url:
url = urlparse(url[0].get('href'))
qs = String.ParseQueryString(url.query)
if 'v' in qs:
video_id = qs['v'][0][0:11]
if video_id is None:
page = HTTP.Request(url.geturl()).content
id = HTML.ElementFromString(page).xpath(
'//div[@data-video-id]/@data-video-id'
)
if id:
video_id = id[0]
else:
id = RE_VIDEO_ID.search(page)
if id:
video_id = id.group(1)
else:
return None
return Video.GetServiceURL(video_id)
def MetadataObjectForURL(url):
item = Video.GetVideoData(url)
meta = Video.MetaFromInfo(item)
if not meta:
raise Ex.MediaNotAvailable
return VideoClipObject(
title=u'%s' % meta['title'] if 'title' in meta else '',
rating_key=url.split('&')[0],
summary=u'%s\n%s' % (
meta['author'] if 'author' in meta else '',
item['description'] if 'description' in item else '',
),
thumb=Video.GetThumb(meta['video_id'] if 'video_id' in meta else ''),
rating=(float(meta['avg_rating'])*2 if 'avg_rating' in meta else 0.0),
tags=(item['keywords'] if 'keywords' in item else '').split(','),
duration=int(meta['length_seconds'])*1000 if 'length_seconds' in meta else None,
originally_available_at=Datetime.ParseDate(
item['date_published']
) if 'date_published' in item else None,
)
def MediaObjectsForURL(url, definition='hd'):
definitions = Video.DEFINITIONS[definition]
if Prefs['play_highest']:
definitions = (Video.MAX_RESOLUTION,)
return [
MediaObject(
parts=[
PartObject(key=Callback(PlayVideo, url=url, fmt=key))
],
video_resolution=Video.RESOLUTIONS[key],
container=Container.MP4,
video_codec=VideoCodec.H264,
audio_codec=AudioCodec.AAC,
optimized_for_streaming=True
) for key in definitions
]
@indirect
def PlayVideo(url, fmt):
info = Video.GetVideoUrls(url)
fmt = int(fmt)
if fmt not in info:
Log.Debug('Get default format')
for fmt, v in sorted(
Video.RESOLUTIONS.items(),
lambda x,y: cmp(x[1], y[1]),
reverse=True
):
if fmt in info:
break
Log.Debug('Play itag: %d' % fmt)
# Live stream
if info[fmt][-5:] == '.m3u8':
info[fmt] = HTTPLiveStreamURL(info[fmt])
return IndirectResponse(VideoClipObject, key=info[fmt])
================================================
FILE: Contents/Services/URL/YouTubeTV/ServicePrefs.json
================================================
[
{
"id": "play_highest",
"type": "bool",
"label": "Always play highest quality",
"default": "false",
},
]
================================================
FILE: Contents/Strings/da.json
================================================
{
"Error": "Fejl",
"Next page": "Næste side",
"No entries found": "Intet fundet",
"Search": "Søg",
"Search Video": "Søg Video",
"Success": "Success",
"Title": "YouTubeTV",
"Search HD only": "Søg kun efter HD",
"Items per page": "Elementer pr. side",
"likes": "Liked videoer",
"favorites": "Fortrukne",
"uploads": "Videos",
"watchHistory": "Historie",
"watchLater": "Se senere",
"Authorize": "Godkend",
"You must enter code for continue": "Du skal indtaste en kode for at fortsætte",
"My Subscriptions": "Mine abbonementer",
"What to Watch": "Hvad skal der ses",
"Playlists": "Afspille liste",
"My channel": "Min kanal",
"Channels": "Kanaler",
"Categories": "Kategorier",
"Subscriptions": "Abbonementer",
"More playlists": "Flere afspille lister",
"More subscriptions": "Flere abbonementer",
"Complete authorization": "Færdigør godkendelse",
"Service temporarily unavailable": "Service er midlertidigt ude af drift",
"Preferred Region for Videos and channels": "Foretrukken region for Videoer og kanaler",
"Preferred language for Videos and channels": "Fortrukken sprog for videoer og kanaler",
"Search playlists": "Søg afspille liste",
"Search channels": "Søg kanaler",
"Browse channels": "Gennemse kanaler",
"enterCodeSite": "Venligst, indtast kode \"%s\" i %s",
"codeIs": "Kode: %s",
"Play video": "Afspil video",
"Related videos": "Relaterede videoer",
"I like this": "Synes godt om",
"Add to favorites": "Tilføj til fortrukne",
"Extended video info in My subscriptions": "Udvidet video info i mine abbonementer",
"Extended video info in Playlists": "Udvidet video info i afspille lister",
"Extended video info in Categories": "Udvidet video information for kategorier",
"Extended video info in Search": "Udvidet video info i søgninger",
"Update available: %s": "Opdatering tilgængelig: %s",
"Install latest version of the channel.": "Installer seneste version af denne kanal.",
"Channel updated to version %s": "Kanalen er opdateret til version %s",
"Remove from playlist": "Fjern fra afspille kø",
"Action complete": "Opgave fuldført",
"An error has occurred": "Der er opstået en fejl",
"Sign out": "Log ud",
"Subscriptions list sorting order": "Abonnements liste sorterings order "
}
================================================
FILE: Contents/Strings/de.json
================================================
{
"Error": "Fehler",
"Next page": "Nächste Seite",
"No entries found": "Keine Einträge gefunden",
"Search": "Suche",
"Search Video": "Suche Video",
"Success": "Erfolg",
"Title": "YouTubeTV",
"Search HD only": "Nur HD suchen",
"Items per page": "Einträge pro Seite",
"likes": "Positiv bewertete Videos",
"favorites": "Favoriten",
"uploads": "Videos",
"watchHistory": "Verlauf",
"watchLater": "Später ansehen",
"Authorize": "Genehmigen",
"You must enter code for continue": "Zum fortfahren Code eingeben",
"My Subscriptions": "Meine Abos",
"What to Watch": "Empfohlene Videos ",
"Playlists": "Playlists",
"My channel": "Mein Kanal",
"Channels": "Kanäle",
"Categories": "Kategorien",
"Subscriptions": "Abos",
"More playlists": "Mehr Playlists",
"More subscriptions": "Mehr Abonnements",
"Complete authorization": "Genehmigung abschließen ",
"Service temporarily unavailable": "Service temporär nicht verfügbar",
"Preferred Region for Videos and channels": "Bevorzugte Region für Videos und Kanäle",
"Preferred language for Videos and channels": "Bevorzugte Sprache für Videos und Kanäle",
"Search playlists": "Suche Playlists",
"Search channels": "Suche Kanäle",
"Browse channels": "Kanäle durchsuchen",
"enterCodeSite": "Bitte Code \"%s\" in %s eingeben",
"codeIs": "Code: %s",
"Play video": "Video abspielen",
"Related videos": "Videovorschläge",
"I like this": "Mag Ich",
"Add to favorites": "Zu Favoriten hinzufügen",
"Extended video info in My subscriptions": "Erweiterte Video Info in \"Meine Abos\"",
"Extended video info in Playlists": "Erweiterte Video Info in \"Playlists\"",
"Extended video info in Categories": "Erweiterte Video Info in \"Kategorien\"",
"Extended video info in Search": "Erweiterte Video Info in \"Suche\"",
"Update available: %s": "Update verfügbar: %s",
"Install latest version of the channel.": "Installiere die aktuelle Version des Kanals.",
"Channel updated to version %s": "Kanal Aktualisierung auf Version %s",
"Remove from playlist": "Von Playlist entfernen",
"Action complete": "Aktion vollständig",
"An error has occurred": "Ein Fehler ist aufgetreten",
"Sign out": "Abmelden",
"Subscriptions list sorting order": "Sortierreihenfolge meiner Abos"
}
================================================
FILE: Contents/Strings/en.json
================================================
{
"Error": "Error",
"Next page": "Next page",
"No entries found": "No entries found",
"Search": "Search",
"Search Video": "Search Video",
"Success": "Success",
"Title": "YouTubeTV",
"Search HD only": "Search HD only",
"Items per page": "Items per page",
"likes": "Liked videos",
"favorites": "Favorites",
"uploads": "Videos",
"watchHistory": "History",
"watchLater": "Watch Later",
"Authorize": "Authorize",
"You must enter code for continue": "You must enter code for continue",
"My Subscriptions": "My Subscriptions",
"What to Watch": "What to Watch",
"Playlists": "Playlists",
"My channel": "My channel",
"Channels": "Channels",
"Categories": "Categories",
"Subscriptions": "Subscriptions",
"More playlists": "More playlists",
"More subscriptions": "More subscriptions",
"Complete authorization": "Complete authorization",
"Service temporarily unavailable": "Service temporarily unavailable",
"Preferred Region for Videos and channels": "Preferred Region for Videos and channels",
"Preferred language for Videos and channels": "Preferred language for Videos and channels",
"Search playlists": "Search playlists",
"Search channels": "Search channels",
"Browse channels": "Browse channels",
"enterCodeSite": "Please, enter code \"%s\" in %s",
"codeIs": "Code: %s",
"Play video": "Play video",
"Related videos": "Related videos",
"I like this": "I like this",
"Add to favorites": "Add to favorites",
"Extended video info in My subscriptions": "Extended video info in My subscriptions",
"Extended video info in Playlists": "Extended video info in Playlists",
"Extended video info in Categories": "Extended video info in Categories",
"Extended video info in Search": "Extended video info in Search",
"Update available: %s": "Update available: %s",
"Install latest version of the channel.": "Install latest version of the channel.",
"Channel updated to version %s": "Channel updated to version %s",
"Remove from playlist": "Remove from playlist",
"Action complete": "Action complete",
"An error has occurred": "An error has occurred",
"Sign out": "Sign out",
"Subscriptions list sorting order": "Subscriptions list sorting order",
"Always play highest quality": "Always play highest quality",
"Search Channel": "Search Channel"
}
================================================
FILE: Contents/Strings/es.json
================================================
{
"Error": "Error",
"Next page": "Página siguiente",
"No entries found": "No se han encontrado entradas",
"Search": "Búsqueda",
"Search Video": "Buscar vídeo",
"Success": "Éxito",
"Title": "YouTubeTV",
"Search HD only": "Buscar solo en HD",
"Items per page": "Objetos por página",
"likes": "Vídeos que te gustan",
"favorites": "Favoritos",
"uploads": "Vídeos",
"watchHistory": "Historia",
"watchLater": "Ver más tarde",
"Authorize": "Autorizar",
"You must enter code for continue": "Tienes que introducir un código para continuar",
"My Subscriptions": "Mis subscriptores",
"What to Watch": "Qué ver",
"Playlists": "Lista de reproducción",
"My channel": "Mi canal",
"Channels": "Canales",
"Categories": "Categorías",
"Subscriptions": "Subscripciones",
"More playlists": "Más listas de reproducción",
"More subscriptions": "Más suscripciones",
"Complete authorization": "Completar autorización",
"Service temporarily unavailable": "Servicio no disponible temporalmente",
"Preferred Region for Videos and channels": "Región preferida para vídeos y canales",
"Preferred language for Videos and channels": "Idioma preferido para Vídeos y canales",
"Search playlists": "Buscar listas de reproducción",
"Search channels": "Buscar canales",
"Browse channels": "Explorar canales",
"enterCodeSite": "Por favor, introduce el código \"%s\" en %s",
"codeIs": "Código: %s",
"Play video": "Reproducir vídeo",
"Related videos": "Vídeos relacionados",
"I like this": "Me gusta esto",
"Add to favorites": "Añadir a favoritos",
"Extended video info in My subscriptions": "Información ampliada del vídeo en Mis suscripciones",
"Extended video info in Playlists": "Información ampliada del vídeo en Listas de reproducción",
"Extended video info in Categories": "Información ampliada del vídeo en Categorías",
"Extended video info in Search": "Información ampliada del vídeo en Buscar",
"Update available: %s": "Actualización disponible: %s",
"Install latest version of the channel.": "Instalar la última versión del canal.",
"Channel updated to version %s": "Canal actualizado a la versión %s",
"Remove from playlist": "Eliminar de la lista de reproducción",
"Action complete": "Completar acción",
"An error has occurred": "Ha ocurrido un error",
"Sign out": "Cerrar sesión",
"Subscriptions list sorting order": "Orden de clasificación de la lista de suscripciones"
}
================================================
FILE: Contents/Strings/fr-BE.json
================================================
{
"Error": "Erreur",
"Next page": "Page suivante",
"No entries found": "Aucune entrée n'a été trouvée",
"Search": "Rechercher",
"Search Video": "Rechercher une vidéo",
"Success": "Succès",
"Title": "YouTubeTV",
"Search HD only": "Rechercher uniquement en haute définition",
"Items per page": "Nombre d'éléments par page",
"likes": "Vidéos que vous aimez",
"favorites": "Favoris",
"uploads": "Vidéos",
"watchHistory": "Historique",
"watchLater": "Á regarder plus tard",
"Authorize": "Autoriser",
"You must enter code for continue": "Vous devez introduire un code pour pouvoir continuer",
"My Subscriptions": "Mes abonnements",
"What to Watch": "Que regarder ?",
"Playlists": "Listes de lecture",
"My channel": "Ma chaîne",
"Channels": "Chaînes",
"Categories": "Catégories",
"Subscriptions": "Abonnements",
"More playlists": "Plus de listes de lecture",
"More subscriptions": "Plus d'abonnements",
"Complete authorization": "Finaliser l'autorisation",
"Service temporarily unavailable": "Service temporairement indisponible",
"Preferred Region for Videos and channels": "Région de prédilection pour les vidéos et les chaînes",
"Preferred language for Videos and channels": "Langue de prédilection pour les vidéos et les chaînes",
"Search playlists": "Rechercher dans les listes de lecture",
"Search channels": "Rechercher dans les chaînes",
"Browse channels": "Parcourir les chaînes",
"enterCodeSite": "Veuillez introduire le code \"%s\" sur %s",
"codeIs": "Code : %s",
"Play video": "Lire la vidéo",
"Related videos": "Vidéos suggérées",
"I like this": "J'aime ce contenu",
"Add to favorites": "Ajouter aux favoris",
"Extended video info in My subscriptions": "Informations étendues pour les vidéos de mes abonnements",
"Extended video info in Playlists": "Informations étendues pour les vidéos des listes de lecture",
"Extended video info in Categories": "Informations étendues pour les vidéos des catégories",
"Extended video info in Search": "Informations étendues pour les vidéos de mes recherches",
"Update available: %s": "Mise à jour disponible : %s",
"Install latest version of the channel.": "Installer la dernière version de la chaîne.",
"Channel updated to version %s": "Chaîne mise à jour vers la version %s",
"Remove from playlist": "Supprimer de la liste de lecture",
"Action complete": "Action terminée",
"An error has occurred": "Une erreur est survenue",
"Sign out": "Déconnexion",
"Subscriptions list sorting order": "Ordre de tri de la liste des abonnements",
"Always play highest quality": "Toujours utiliser la meilleurs qualité",
"Search Channel": "Rechercher dans les chaînes"
}
================================================
FILE: Contents/Strings/fr.json
================================================
{
"Error": "Erreur",
"Next page": "Page suivante",
"No entries found": "Aucune entrée n'a été trouvée",
"Search": "Rechercher",
"Search Video": "Rechercher une vidéo",
"Success": "Succès",
"Title": "YouTubeTV",
"Search HD only": "Rechercher uniquement en haute définition",
"Items per page": "Nombre d'éléments par page",
"likes": "Vidéos que vous aimez",
"favorites": "Favoris",
"uploads": "Vidéos",
"watchHistory": "Historique",
"watchLater": "Á regarder plus tard",
"Authorize": "Autoriser",
"You must enter code for continue": "Vous devez introduire un code pour pouvoir continuer",
"My Subscriptions": "Mes abonnements",
"What to Watch": "Que regarder ?",
"Playlists": "Listes de lecture",
"My channel": "Ma chaîne",
"Channels": "Chaînes",
"Categories": "Catégories",
"Subscriptions": "Abonnements",
"More playlists": "Plus de listes de lecture",
"More subscriptions": "Plus d'abonnements",
"Complete authorization": "Finaliser l'autorisation",
"Service temporarily unavailable": "Service temporairement indisponible",
"Preferred Region for Videos and channels": "Région de prédilection pour les vidéos et les chaînes",
"Preferred language for Videos and channels": "Langue de préférence pour les vidéos et les chaînes",
"Search playlists": "Rechercher dans les listes de lecture",
"Search channels": "Rechercher dans les chaînes",
"Browse channels": "Parcourir les chaînes",
"enterCodeSite": "Veuillez introduire le code \"%s\" sur %s",
"codeIs": "Code : %s",
"Play video": "Lire la vidéo",
"Related videos": "Vidéos suggérées",
"I like this": "J'aime ce contenu",
"Add to favorites": "Ajouter aux favoris",
"Extended video info in My subscriptions": "Informations étendues pour les vidéos de mes abonnements",
"Extended video info in Playlists": "Informations étendues pour les vidéos des listes de lecture",
"Extended video info in Categories": "Informations étendues pour les vidéos des catégories",
"Extended video info in Search": "Informations étendues pour les vidéos de mes recherches",
"Update available: %s": "Mise à jour disponible : %s",
"Install latest version of the channel.": "Installer la dernière version de la chaîne.",
"Channel updated to version %s": "Chaîne mise à jour vers la version %s",
"Remove from playlist": "Supprimer de la liste de lecture",
"Action complete": "Action terminée",
"An error has occurred": "Une erreur est survenue",
"Sign out": "Déconnexion",
"Subscriptions list sorting order": "Ordre de tri de la liste des abonnements",
"Always play highest quality": "Toujours utiliser la meilleurs qualité",
"Search Channel": "Rechercher dans les chaînes"
}
================================================
FILE: Contents/Strings/hu.json
================================================
{
"Error": "Hiba",
"Next page": "Következő oldal",
"No entries found": "Nem találhatók bejegyzések",
"Search": "Keresés",
"Search Video": "Videó keresés",
"Success": "Sikeres",
"Title": "YouTubeTV",
"Search HD only": "Csak HD keresés",
"Items per page": "Elem laponként",
"likes": "Hivatkozott videók",
"favorites": "Kedvencek",
"uploads": "Videók",
"watchHistory": "Előzmények",
"watchLater": "Megnézés később",
"Authorize": "Engedélyez",
"You must enter code for continue": "Írja be a kódot a folytatáshoz",
"My Subscriptions": "Saját előfizetések",
"What to Watch": "Mit néz",
"Playlists": "Lejátszási lista",
"My channel": "Saját csatorna",
"Channels": "Csatornák",
"Categories": "Kategóriák",
"Subscriptions": "Előfizetések",
"More playlists": "További lejátszási listák",
"More subscriptions": "További előfizetések",
"Complete authorization": "Teljes engedélyezés",
"Service temporarily unavailable": "A szolgáltatás átmenetileg nem elérhető",
"Preferred Region for Videos and channels": "Preferált régiók a videók és a csatornák között",
"Preferred language for Videos and channels": "Preferált nyelvek videóknál és csatornáknál",
"Search playlists": "Lejátszási listák keresése",
"Search channels": "Csatornák keresése",
"Browse channels": "Böngészés a csatornák közt",
"enterCodeSite": "Kérjük írja be a kódot \"%s\" itt %s",
"codeIs": "Kód: %s",
"Play video": "Videó lejátszása",
"Related videos": "Hasonló videók",
"I like this": "Ez tetszik",
"Add to favorites": "Hozzáadás a Kedvencekhez",
"Extended video info in My subscriptions": "Kibővített videó információ a Saját előfizetésekben",
"Extended video info in Playlists": "Kibővített videó információ a Lejátszási listákban",
"Extended video info in Categories": "Kibővített videó információ a Kategóriákban",
"Extended video info in Search": "Kibővített videó információ a Keresésekben",
"Update available: %s": "Frissítés elérhető: %s",
"Install latest version of the channel.": "A csatorna legújabb verziójának telepítése",
"Channel updated to version %s": "A csatorna frissítve %s verzióra",
"Remove from playlist": "Eltávolítás a lejátszási listáról",
"Action complete": "Művelet befejezve",
"An error has occurred": "Hiba történt",
"Sign out": "Kijelentkezés",
"Subscriptions list sorting order": "Előfizetői lista rendezés"
}
================================================
FILE: Contents/Strings/it.json
================================================
{
"Error": "Errore",
"Next page": "Pagina successiva",
"No entries found": "Nessuna voce trovata",
"Search": "Cerca",
"Search Video": "Cerca video",
"Success": "Successo",
"Title": "YouTubeTV",
"Search HD only": "Cerca solo in HD",
"Items per page": "Voci per pagina",
"likes": "Video correlati",
"favorites": "Favoriti",
"uploads": "Video",
"watchHistory": "Cronologia",
"watchLater": "Guarda più tardi",
"Authorize": "Autorizza",
"You must enter code for continue": "Devi inserire il codice per proseguire",
"My Subscriptions": "Le mie iscrizioni",
"What to Watch": "Cosa guardare",
"Playlists": "Playlists",
"My channel": "Il mio canale",
"Channels": "Canali",
"Categories": "Categorie",
"Subscriptions": "Iscrizioni",
"More playlists": "Altre playlists",
"More subscriptions": "Altre iscrizioni",
"Complete authorization": "Completa autorizzazione",
"Service temporarily unavailable": "Servizio temporaneamente non disponibile",
"Preferred Region for Videos and channels": "Regione preferita per video e canali",
"Preferred language for Videos and channels": "Lingua preferita per i Video e i canali",
"Search playlists": "Cerca playlists",
"Search channels": "Cerca canali",
"Browse channels": "Sfoglia i canali",
"enterCodeSite": "Per favore, inserire il codice \"%s\" in %s",
"codeIs": "Codice: %s",
"Play video": "Play video",
"Related videos": "Video correlati",
"I like this": "Mi piace",
"Add to favorites": "Aggiungi ai preferiti",
"Extended video info in My subscriptions": "Info estese dei video in Le mie iscrizioni",
"Extended video info in Playlists": "Info estese dei video in Playlists",
"Extended video info in Categories": "Info estese dei video in Categorie",
"Extended video info in Search": "Info estese in Cerca",
"Update available: %s": "Aggiornamento disponibile: %s",
"Install latest version of the channel.": "Installa l'ultima versione del canale.",
"Channel updated to version %s": "Canale aggiornato alla versione %s",
"Remove from playlist": "Rimuovi dalla playlist",
"Action complete": "Azione completata",
"An error has occurred": "Si è verificato un errore",
"Sign out": "Disconnetti",
"Subscriptions list sorting order": "Ordinamento della lista delle iscrizioni"
}
================================================
FILE: Contents/Strings/nl.json
================================================
{
"Error": "Fout",
"Next page": "Volgende pagina",
"No entries found": "Geen items gevonden",
"Search": "Zoeken",
"Search Video": "Video zoeken",
"Success": "Gelukt",
"Title": "YouTubeTV",
"Search HD only": "Zoek alleen HD",
"Items per page": "Items per pagina",
"likes": "Video's die ik leuk vind",
"favorites": "Favorieten",
"uploads": "Video's",
"watchHistory": "Geschiedenis",
"watchLater": "Later bekijken",
"Authorize": "Machtigen",
"You must enter code for continue": "Voer de code in om door te gaan",
"My Subscriptions": "Mijn abonnementen",
"What to Watch": "Wat je kunt bekijken",
"Playlists": "Afspeellijsten",
"My channel": "Mijn kanaal",
"Channels": "Kanalen",
"Categories": "Categorieën",
"Subscriptions": "Abonnementen",
"More playlists": "Meer afspeellijsten",
"More subscriptions": "Meer abonnementen",
"Complete authorization": "Machtiging voltooien",
"Service temporarily unavailable": "Service tijdelijk niet beschikbaar",
"Preferred Region for Videos and channels": "Voorkeursregio voor video's en kanalen",
"Preferred language for Videos and channels": "Voorkeurstaal voor video's en kanalen",
"Search playlists": "Zoek afspeellijsten",
"Search channels": "Zoek kanalen",
"Browse channels": "Browse door kanalen",
"enterCodeSite": "Voer code \"%s\" in %s",
"codeIs": "Code: %s",
"Play video": "Speel video af",
"Related videos": "Gerelateerde video's",
"I like this": "Ik vind dit leuk",
"Add to favorites": "Toevoegen aan favorieten",
"Extended video info in My subscriptions": "Uitgebreide video info in Mijn abonnementen",
"Extended video info in Playlists": "Uitgebreide video info in Afspeellijsten",
"Extended video info in Categories": "Uitgebreide video info in Zoekresultaten",
"Extended video info in Search": "Uitgebreide video info in Zoekresultaten",
"Update available: %s": "Update beschikbaar: %s",
"Install latest version of the channel.": "Installeer meest recente versie van dit kanaal",
"Channel updated to version %s": "Kanaal is geüpdatet naar versie %s",
"Remove from playlist": " Verwijder uit afspeellijst",
"Action complete": "Actie voltooid",
"An error has occurred": "Er is een fout opgetreden",
"Sign out": "Uitloggen",
"Subscriptions list sorting order": "Subscriptions list sorting order"
}
================================================
FILE: Contents/Strings/pl.json
================================================
{
"Error": "Błąd",
"Next page": "Następna strona",
"No entries found": "Nie znaleziono wpisów",
"Search": "Szukaj",
"Search Video": "Szukaj filmów",
"Success": "Sukces",
"Title": "YouTubeTV",
"Search HD only": "Szukaj tylko w HD",
"Items per page": "Ilość na stronie",
"likes": "Polubione Filmy",
"favorites": "Ulubione",
"uploads": "Filmy",
"watchHistory": "Historia",
"watchLater": "Obejrzyj później",
"Authorize": "Autoryzuj",
"You must enter code for continue": "Musisz wprowadzić kod żeby kontynuować",
"My Subscriptions": "Moje subscrybcje",
"What to Watch": "Co oglądać",
"Playlists": "Lista odtwarzania",
"My channel": "Mój kanał",
"Channels": "Kanały",
"Categories": "Categorie",
"Subscriptions": "Subskrybcja",
"More playlists": "Więcej list odtwarzania",
"More subscriptions": "Więcej Subskrypcji ",
"Complete authorization": "Pełna autoryzacja ",
"Service temporarily unavailable": "Serwis chwilowo niedostępny",
"Preferred Region for Videos and channels": "Preferowany Region dla wideo i kanałów ",
"Preferred language for Videos and channels": "Preferowany język dla Filmów i Kanałów ",
"Search playlists": "Szukaj list odtwarzania",
"Search channels": "Szukaj kanałow",
"Browse channels": "Przeglądaj kanały",
"enterCodeSite": "Proszę wprowadzić kod \"%s\" w %s",
"codeIs": "Kod: %s",
"Play video": "Odtwórz",
"Related videos": "Powiązane filmy",
"I like this": "Lubie to",
"Add to favorites": "Dodaj do ulubionych",
"Extended video info in My subscriptions": "Rozszerzona informacja o wideo w Moich Subscrybcjach",
"Extended video info in Playlists": "Rozszerzona informacja o wideo w Liscie Odtwarzania",
"Extended video info in Categories": "Rozrzeszona informacja o wideo w Kategoriach",
"Extended video info in Search": "Rozszerzona informacja o wyszukiwanych filmach",
"Update available: %s": "Aktualizacja dostępna %s",
"Install latest version of the channel.": "Zainstaluj najnowsza wersje kanału.",
"Channel updated to version %s": "Kanał uaktualniony do wersji %s",
"Remove from playlist": "Usuń z listy odtwarzania",
"Action complete": "Zakończono",
"An error has occurred": "Wystąpił błąd",
"Sign out": "Wyloguj się",
"Subscriptions list sorting order": "Posortowana lista subscrybcji"
}
================================================
FILE: Contents/Strings/ru.json
================================================
{
"Error": "Ошибка",
"Next page": "Далее",
"No entries found": "Ничего не нашлось",
"Search": "Поиск",
"Search Video": "Поиск видеозаписей",
"Success": "Успех",
"Title": "YouTubeTV",
"Search HD only": "Искать только в HD",
"Items per page": "Элементов на страницу",
"likes": "Понравившиеся",
"favorites": "Избранное",
"uploads": "Видео",
"watchHistory": "Просмотренные",
"watchLater": "Посмотреть позже",
"Authorize": "Авторизация",
"You must enter code for continue": "Вы должны ввести код для продолжения",
"My Subscriptions": "Мои подписки",
"What to Watch": "Рекомендации",
"Playlists": "Плейлисты",
"My channel": "Мой канал",
"Channels": "Каналы",
"Categories": "Каталог",
"Subscriptions": "Подписки",
"More playlists": "Еще плейлисты",
"More subscriptions": "Еще подписки",
"Complete authorization": "Завершить авторизацию",
"Service temporarily unavailable": "Сервис временно недоступен",
"Preferred Region for Videos and channels": "Ваш регион",
"Preferred language for Videos and channels": "Ваш язык",
"Search playlists": "Искать плейлисты",
"Search channels": "Поиск каналов",
"Browse channels": "Каталог каналов",
"enterCodeSite": "Пожалуйста, введите код \"%s\" на сайте %s",
"codeIs": "Код: %s",
"Play video": "Просмотр",
"Related videos": "Похожие видео",
"I like this": "Мне понравилось",
"Add to favorites": "Добавить в избранное",
"Extended video info in My subscriptions": "Расширенная информация по видео в Моих подписках",
"Extended video info in Playlists": "Расширенная информация по видео в Плейлистах",
"Extended video info in Categories": "Расширенная информация по видео в Каталоге",
"Extended video info in Search": "Расширенная информация по видео в Поиске",
"Update available: %s": "Доступно обновление: %s",
"Install latest version of the channel.": "Установить последнюю версию плагина.",
"Channel updated to version %s": "Плагин обновлен до версии %s",
"Remove from playlist": "Удалить из плейлиста",
"Action complete": "Действие выполнено",
"An error has occurred": "Произошла ошибка",
"Sign out": "Выйти",
"Subscriptions list sorting order": "Порядок сортировки Подписок",
"Always play highest quality": "Всегда проигрывать максимальное качество",
"Search Channel": "Поиск на канале"
}
================================================
FILE: Contents/Strings/sv.json
================================================
{
"Error": "Fel",
"Next page": "Nästa sida",
"No entries found": "Inga objekt hittades",
"Search": "Sök",
"Search Video": "Sök video",
"Success": "Det lyckades",
"Title": "YouTubeTV",
"Search HD only": "Sök HD enbart",
"Items per page": "Objekt per sida",
"likes": "Gillade videos",
"favorites": "Favoriter",
"uploads": "Videos",
"watchHistory": "Historik",
"watchLater": "Titta senare",
"Authorize": "Godkänn",
"You must enter code for continue": "Du måste skriva in koden för att fortsätta",
"My Subscriptions": "Mina prenumerationer",
"What to Watch": "Intressanta videoklipp",
"Playlists": "Spellistor",
"My channel": "Min kanal",
"Channels": "Kanaler",
"Categories": "Kategorier",
"Subscriptions": "Prenumerationer",
"More playlists": "Mer spellistor",
"More subscriptions": "Mer prenumerationer",
"Complete authorization": "Slutför godkännande",
"Service temporarily unavailable": "Tjänsten tillfälligt otillgänglig",
"Preferred Region for Videos and channels": "Föredragen region för videos och kanaler",
"Preferred language for Videos and channels": "Föredraget språk för videos och kanaler",
"Search playlists": "Sök spellistor",
"Search channels": "Sök kanaler",
"Browse channels": "Bläddra kanaler",
"enterCodeSite": "Vänligen skriv in kod \"%s\" i %s",
"codeIs": "Kod: %s",
"Play video": "Spela upp video",
"Related videos": "Liknande videos",
"I like this": "Gilla detta",
"Add to favorites": "Lägg till favoriter",
"Extended video info in My subscriptions": "Mer videoinformation i Mina prenumerationer",
"Extended video info in Playlists": "Mer videoinformation i Spellistor",
"Extended video info in Categories": "Mer videoinformation i Kategorier",
"Extended video info in Search": "Mer videoinformation i Sök",
"Update available: %s": "Uppdatering tillgänglig: %s",
"Install latest version of the channel.": "Installera senaste versionen av kanalen.",
"Channel updated to version %s": "Kanalen uppdaterad till version %s",
"Remove from playlist": "Ta bort från spellista",
"Action complete": "Åtgärd genomförd",
"An error has occurred": "Ett fel har inträffat",
"Sign out": "Logga ut",
"Subscriptions list sorting order": "Sorteringsordning för prenumerationer"
}
================================================
FILE: LICENSE
================================================
Copyright (c) 2014, KOL
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the {organization} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
YouTubeTV Plex Plugin
=====
This is fast and easy alternative [YouTube](http://youtube.com/) plugin for [Plex Media Server](http://plex.tv/).
Current status
-------
* Support categories
* Support subscriptions
* Support playlists
* Support channels catalogue and video categories browsing
* Search playlists, channels and videos
* Support live streams
Features
-------
* Much faster than original Plex YouTube plugin
* Full YouTube API v3 support
* Does not require Google login and password
* Navigation like YouTube Apps
* Pagination and regional settings
* Russian, Swedish, Danish, Dutch, French, French (Belgium), Hungarian, Italian, Spanish, German, Polish localization
* YouTube content localization by region preferences
* "Like" and "Watch later" video functions including edit function
* Videos and playlists from videos description
* Navigation by videos, playlists and channels from video description
* Update function
Installation
-------
You can install it using the [instruction](https://support.plex.tv/hc/en-us/articles/201187656-How-do-I-manually-install-a-channel-)
-------
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=27YXJGA4W5VP4&lc=RU&item_name=KOL%27s%20Plex%20YouTubeTV%20Plugin&item_number=YouTubeTV%2ebundle¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted "Donate for project support") [](https://money.yandex.ru/embed/shop.xml?account=410012666604862&quickpay=shop&payment-type-choice=on&mobile-payment-type-choice=on&writer=seller&targets=%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%BA%D0%B0+Plex+YouTubeTV+Plugin&targets-hint=&default-sum=300&button-text=03&comment=on&hint=%D0%92%D0%B0%D1%88%D0%B8+%D0%BF%D0%BE%D0%B6%D0%B5%D0%BB%D0%B0%D0%BD%D0%B8%D1%8F&successURL=https%3A%2F%2Fgithub.com%2Fkolsys%2FYouTubeTV.bundle "Поддержкать проект")
Help with localization
-------
[YouTubeTV on Transifex](https://www.transifex.com/kolsys/youtubetv/)
================================================
FILE: _config.yml
================================================
theme: jekyll-theme-slate
gitextract_uz02c5iz/ ├── CNAME ├── Contents/ │ ├── Code/ │ │ ├── __init__.py │ │ └── updater.py │ ├── DefaultPrefs.json │ ├── Info.plist │ ├── Services/ │ │ ├── ServiceInfo.plist │ │ ├── Shared Code/ │ │ │ ├── jsinterp.pys │ │ │ └── video.pys │ │ └── URL/ │ │ └── YouTubeTV/ │ │ ├── ServiceCode.pys │ │ └── ServicePrefs.json │ └── Strings/ │ ├── da.json │ ├── de.json │ ├── en.json │ ├── es.json │ ├── fr-BE.json │ ├── fr.json │ ├── hu.json │ ├── it.json │ ├── nl.json │ ├── pl.json │ ├── ru.json │ └── sv.json ├── LICENSE ├── README.md └── _config.yml
SYMBOL INDEX (47 symbols across 2 files)
FILE: Contents/Code/__init__.py
function Start (line 92) | def Start():
function ValidatePrefs (line 97) | def ValidatePrefs():
function MainMenu (line 117) | def MainMenu(complete=False, offline=False):
function Feed (line 180) | def Feed(oid, offset=None):
function VideoView (line 294) | def VideoView(vid, **kwargs):
function VideoInfo (line 302) | def VideoInfo(vid, pl_item_id=None):
function Channels (line 357) | def Channels(oid, title, offset=None):
function Channel (line 404) | def Channel(oid, title):
function User (line 444) | def User(username):
function Categories (line 459) | def Categories(title, c_type):
function Category (line 500) | def Category(title, oid=0, offset=None):
function Playlists (line 533) | def Playlists(uid, title, offset=None):
function Playlist (line 555) | def Playlist(oid, title, can_edit=False, offset=None):
function PlaylistAdd (line 608) | def PlaylistAdd(aid, key=None, oid=None, a_type='video'):
function PlaylistRemove (line 633) | def PlaylistRemove(pl_item_id):
function Subscriptions (line 641) | def Subscriptions(uid, title, offset=None):
function AddVideos (line 649) | def AddVideos(oc, res, title=None, extended=False, pl_map={}):
function FillChannelInfo (line 688) | def FillChannelInfo(oc, uid, pl_types=None):
function AddPlaylists (line 723) | def AddPlaylists(oc, uid, offset=None):
function AddSubscriptions (line 767) | def AddSubscriptions(oc, uid, offset=None):
function AddItemsFromDescription (line 809) | def AddItemsFromDescription(oc, description):
function Search (line 856) | def Search(query=None, title=L('Search'), s_type='video', offset=0, **kw...
function Authorization (line 922) | def Authorization():
function NoContents (line 964) | def NoContents():
function SuccessMessage (line 971) | def SuccessMessage():
function ErrorMessage (line 978) | def ErrorMessage():
function NotImplemented (line 985) | def NotImplemented(**kwargs):
function GetRegion (line 992) | def GetRegion():
function GetLanguage (line 996) | def GetLanguage():
function GetLimitForOC (line 1000) | def GetLimitForOC(oc):
function GetThumbFromSnippet (line 1005) | def GetThumbFromSnippet(snippet):
function ApiGetVideos (line 1012) | def ApiGetVideos(ids=[], title=None, extended=False, **kwargs):
function ApiGetChannelInfo (line 1021) | def ApiGetChannelInfo(uid):
function ApiRequest (line 1045) | def ApiRequest(method, params, data=None, rmethod=None):
function ApiGetParams (line 1084) | def ApiGetParams(part='snippet', offset=None, limit=None, uid=None, **kw...
function CheckToken (line 1104) | def CheckToken():
function ResetToken (line 1129) | def ResetToken():
function OAuthRequest (line 1137) | def OAuthRequest(params, rtype='token'):
function CheckAccessData (line 1156) | def CheckAccessData(key):
function StoreAccessData (line 1160) | def StoreAccessData(data):
FILE: Contents/Code/updater.py
class Updater (line 47) | class Updater:
method __init__ (line 51) | def __init__(self, prefix, oc):
method NormalizeVersion (line 65) | def NormalizeVersion(self, version):
method ParseVersion (line 70) | def ParseVersion(self, version):
method IsUpdateAvailable (line 77) | def IsUpdateAvailable(self):
method InitBundleInfo (line 101) | def InitBundleInfo(self):
method DoUpdate (line 121) | def DoUpdate(self):
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (115K chars).
[
{
"path": "CNAME",
"chars": 9,
"preview": "kolsys.ml"
},
{
"path": "Contents/Code/__init__.py",
"chars": 31568,
"preview": "# -*- coding: utf-8 -*-\n\n# Copyright (c) 2014, KOL\n# All rights reserved.\n\n# Redistribution and use in source and binary"
},
{
"path": "Contents/Code/updater.py",
"chars": 5217,
"preview": "# -*- coding: utf-8 -*-\n#\n# Plex Plugin Updater\n# $Id$\n#\n# Universal plugin updater module for Plex Server Channels that"
},
{
"path": "Contents/DefaultPrefs.json",
"chars": 5674,
"preview": "[\n {\n \"id\": \"my_subscriptions_extened\",\n \"type\": \"bool\",\n \"label\": \"Extended video info in My su"
},
{
"path": "Contents/Info.plist",
"chars": 1171,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/Pr"
},
{
"path": "Contents/Services/ServiceInfo.plist",
"chars": 635,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.c"
},
{
"path": "Contents/Services/Shared Code/jsinterp.pys",
"chars": 10665,
"preview": "# -*- coding: utf-8 -*-\n#\n# Based on youtube-dl lib\n# https://github.com/rg3/youtube-dl/blob/master/youtube_dl/jsinterp."
},
{
"path": "Contents/Services/Shared Code/video.pys",
"chars": 12258,
"preview": "# -*- coding: utf-8 -*-\n\n# Copyright (c) 2014, KOL\n# All rights reserved.\n\n# Redistribution and use in source and binary"
},
{
"path": "Contents/Services/URL/YouTubeTV/ServiceCode.pys",
"chars": 6421,
"preview": "# -*- coding: utf-8 -*-\n\n# Copyright (c) 2014, KOL\n# All rights reserved.\n\n# Redistribution and use in source and binary"
},
{
"path": "Contents/Services/URL/YouTubeTV/ServicePrefs.json",
"chars": 146,
"preview": "[\n {\n \"id\": \"play_highest\",\n \"type\": \"bool\",\n \"label\": \"Always play highest quality\",\n \"d"
},
{
"path": "Contents/Strings/da.json",
"chars": 2239,
"preview": "{\n\t\"Error\": \"Fejl\",\n\t\"Next page\": \"Næste side\",\n\t\"No entries found\": \"Intet fundet\",\n\t\"Search\": \"Søg\",\n\t\"Search Video\": "
},
{
"path": "Contents/Strings/de.json",
"chars": 2234,
"preview": "{\n\t\"Error\": \"Fehler\",\n\t\"Next page\": \"Nächste Seite\",\n\t\"No entries found\": \"Keine Einträge gefunden\",\n\t\"Search\": \"Suche\","
},
{
"path": "Contents/Strings/en.json",
"chars": 2278,
"preview": "{\n\t\"Error\": \"Error\",\n\t\"Next page\": \"Next page\",\n\t\"No entries found\": \"No entries found\",\n\t\"Search\": \"Search\",\n\t\"Search V"
},
{
"path": "Contents/Strings/es.json",
"chars": 2396,
"preview": "{\n\t\"Error\": \"Error\",\n\t\"Next page\": \"Página siguiente\",\n\t\"No entries found\": \"No se han encontrado entradas\",\n\t\"Search\": "
},
{
"path": "Contents/Strings/fr-BE.json",
"chars": 2652,
"preview": "{\n\t\"Error\": \"Erreur\",\n\t\"Next page\": \"Page suivante\",\n\t\"No entries found\": \"Aucune entrée n'a été trouvée\",\n\t\"Search\": \"R"
},
{
"path": "Contents/Strings/fr.json",
"chars": 2650,
"preview": "{\n\t\"Error\": \"Erreur\",\n\t\"Next page\": \"Page suivante\",\n\t\"No entries found\": \"Aucune entrée n'a été trouvée\",\n\t\"Search\": \"R"
},
{
"path": "Contents/Strings/hu.json",
"chars": 2348,
"preview": "{\n\t\"Error\": \"Hiba\",\n\t\"Next page\": \"Következő oldal\",\n\t\"No entries found\": \"Nem találhatók bejegyzések\",\n\t\"Search\": \"Kere"
},
{
"path": "Contents/Strings/it.json",
"chars": 2252,
"preview": "{\n\t\"Error\": \"Errore\",\n\t\"Next page\": \"Pagina successiva\",\n\t\"No entries found\": \"Nessuna voce trovata\",\n\t\"Search\": \"Cerca\""
},
{
"path": "Contents/Strings/nl.json",
"chars": 2293,
"preview": "{\n\t\"Error\": \"Fout\",\n\t\"Next page\": \"Volgende pagina\",\n\t\"No entries found\": \"Geen items gevonden\",\n\t\"Search\": \"Zoeken\",\n\t\""
},
{
"path": "Contents/Strings/pl.json",
"chars": 2264,
"preview": "{\n\t\"Error\": \"Błąd\",\n\t\"Next page\": \"Następna strona\",\n\t\"No entries found\": \"Nie znaleziono wpisów\",\n\t\"Search\": \"Szukaj\",\n"
},
{
"path": "Contents/Strings/ru.json",
"chars": 2272,
"preview": "{\n\t\"Error\": \"Ошибка\",\n\t\"Next page\": \"Далее\",\n\t\"No entries found\": \"Ничего не нашлось\",\n\t\"Search\": \"Поиск\",\n\t\"Search Vide"
},
{
"path": "Contents/Strings/sv.json",
"chars": 2231,
"preview": "{\n\t\"Error\": \"Fel\",\n\t\"Next page\": \"Nästa sida\",\n\t\"No entries found\": \"Inga objekt hittades\",\n\t\"Search\": \"Sök\",\n\t\"Search V"
},
{
"path": "LICENSE",
"chars": 1480,
"preview": "Copyright (c) 2014, KOL\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodific"
},
{
"path": "README.md",
"chars": 2127,
"preview": "YouTubeTV Plex Plugin\n=====\n\nThis is fast and easy alternative [YouTube](http://youtube.com/) plugin for [Plex Media Ser"
},
{
"path": "_config.yml",
"chars": 25,
"preview": "theme: jekyll-theme-slate"
}
]
About this extraction
This page contains the full source code of the kolsys/YouTubeTV.bundle GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (103.0 KB), approximately 27.1k tokens, and a symbol index with 47 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.