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