├── icon.png ├── fanart.jpg ├── resources ├── media │ ├── likes.png │ ├── live.png │ ├── channel.png │ ├── channels.png │ ├── dislikes.png │ ├── fanart.jpg │ ├── history.png │ ├── playlist.png │ ├── popular.png │ ├── search.png │ ├── settings.png │ ├── sign_in.png │ ├── sign_out.png │ ├── new_search.png │ ├── new_uploads.png │ ├── watch_later.png │ ├── what_to_watch.png │ └── browse_channels.png ├── __init__.py └── lib │ ├── __init__.py │ ├── youtube_plugin │ ├── kodion │ │ ├── impl │ │ │ ├── xbmc │ │ │ │ ├── __init__.py │ │ │ │ ├── xbmc_plugin_settings.py │ │ │ │ ├── xbmc_progress_dialog.py │ │ │ │ ├── xbmc_progress_dialog_bg.py │ │ │ │ ├── xbmc_player.py │ │ │ │ ├── xbmc_playlist.py │ │ │ │ ├── info_labels.py │ │ │ │ ├── xbmc_context_ui.py │ │ │ │ ├── xbmc_runner.py │ │ │ │ └── xbmc_items.py │ │ │ ├── abstract_provider_runner.py │ │ │ ├── __init__.py │ │ │ ├── abstract_player.py │ │ │ ├── abstract_playlist.py │ │ │ ├── abstract_progress_dialog.py │ │ │ ├── abstract_context_ui.py │ │ │ ├── abstract_context.py │ │ │ └── abstract_settings.py │ │ ├── constants │ │ │ ├── const_paths.py │ │ │ ├── const_content_types.py │ │ │ ├── __init__.py │ │ │ ├── const_localize.py │ │ │ ├── const_settings.py │ │ │ └── const_sort_methods.py │ │ ├── json_store │ │ │ ├── __init__.py │ │ │ ├── api_keys.py │ │ │ ├── json_store.py │ │ │ └── login_tokens.py │ │ ├── items │ │ │ ├── uri_item.py │ │ │ ├── image_item.py │ │ │ ├── favorites_item.py │ │ │ ├── watch_later_item.py │ │ │ ├── next_page_item.py │ │ │ ├── search_item.py │ │ │ ├── __init__.py │ │ │ ├── directory_item.py │ │ │ ├── new_search_item.py │ │ │ ├── search_history_item.py │ │ │ ├── utils.py │ │ │ ├── audio_item.py │ │ │ └── base_item.py │ │ ├── exceptions.py │ │ ├── register_provider_path.py │ │ ├── __init__.py │ │ ├── utils │ │ │ ├── favorite_list.py │ │ │ ├── watch_later_list.py │ │ │ ├── __init__.py │ │ │ ├── ip_api.py │ │ │ ├── search_history.py │ │ │ ├── system_version.py │ │ │ ├── playback_history.py │ │ │ ├── function_cache.py │ │ │ ├── data_cache.py │ │ │ ├── monitor.py │ │ │ └── datetime_parser.py │ │ ├── logger.py │ │ ├── runner.py │ │ ├── debug.py │ │ └── service.py │ ├── refresh.py │ ├── youtube │ │ ├── __init__.py │ │ ├── client │ │ │ └── __init__.py │ │ ├── helper │ │ │ ├── signature │ │ │ │ ├── __init__.py │ │ │ │ └── json_script_engine.py │ │ │ ├── __init__.py │ │ │ ├── yt_old_actions.py │ │ │ ├── yt_subscriptions.py │ │ │ ├── yt_video.py │ │ │ ├── tv.py │ │ │ ├── url_to_item_converter.py │ │ │ ├── url_resolver.py │ │ │ └── yt_login.py │ │ └── youtube_exceptions.py │ └── __init__.py │ ├── startup.py │ ├── default.py │ ├── youtube_registration.py │ ├── youtube_resolver.py │ └── youtube_authentication.py ├── .travis.yml ├── .gitignore ├── README.md └── addon.xml /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/icon.png -------------------------------------------------------------------------------- /fanart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/fanart.jpg -------------------------------------------------------------------------------- /resources/media/likes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/likes.png -------------------------------------------------------------------------------- /resources/media/live.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/live.png -------------------------------------------------------------------------------- /resources/media/channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/channel.png -------------------------------------------------------------------------------- /resources/media/channels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/channels.png -------------------------------------------------------------------------------- /resources/media/dislikes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/dislikes.png -------------------------------------------------------------------------------- /resources/media/fanart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/fanart.jpg -------------------------------------------------------------------------------- /resources/media/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/history.png -------------------------------------------------------------------------------- /resources/media/playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/playlist.png -------------------------------------------------------------------------------- /resources/media/popular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/popular.png -------------------------------------------------------------------------------- /resources/media/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/search.png -------------------------------------------------------------------------------- /resources/media/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/settings.png -------------------------------------------------------------------------------- /resources/media/sign_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/sign_in.png -------------------------------------------------------------------------------- /resources/media/sign_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/sign_out.png -------------------------------------------------------------------------------- /resources/media/new_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/new_search.png -------------------------------------------------------------------------------- /resources/media/new_uploads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/new_uploads.png -------------------------------------------------------------------------------- /resources/media/watch_later.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/watch_later.png -------------------------------------------------------------------------------- /resources/media/what_to_watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/what_to_watch.png -------------------------------------------------------------------------------- /resources/media/browse_channels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdf76/plugin.video.youtube/HEAD/resources/media/browse_channels.png -------------------------------------------------------------------------------- /resources/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | -------------------------------------------------------------------------------- /resources/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | __all__ = ['youtube_plugin'] 12 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | __all__ = [] 12 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/refresh.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | import xbmc 11 | 12 | if __name__ == '__main__': 13 | xbmc.executebuiltin("Container.Refresh") 14 | -------------------------------------------------------------------------------- /resources/lib/startup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from youtube_plugin.kodion import service 12 | 13 | service.run() 14 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .provider import Provider 12 | 13 | __all__ = ['Provider'] 14 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/client/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .youtube import YouTube 12 | 13 | __all__ = ['YouTube'] 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | 5 | before_install: 6 | - | 7 | cd $HOME 8 | git clone https://github.com/xbmc/addon-check 9 | cd $TRAVIS_BUILD_DIR 10 | 11 | install: 12 | - pip install $HOME/addon-check/ 13 | 14 | before_script: 15 | - | 16 | rm -rf LICENSES/ 17 | cd $HOME 18 | 19 | script: 20 | - kodi-addon-checker $TRAVIS_BUILD_DIR --branch=isengard 21 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/constants/const_paths.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | SEARCH = 'kodion/search' 12 | FAVORITES = 'kodion/favorites' 13 | WATCH_LATER = 'kodion/watch_later' 14 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/signature/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from ....youtube.helper.signature.cipher import Cipher 12 | 13 | __all__ = ['Cipher'] 14 | 15 | 16 | -------------------------------------------------------------------------------- /resources/lib/default.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from youtube_plugin.kodion import runner 12 | from youtube_plugin import youtube 13 | 14 | __provider__ = youtube.Provider() 15 | runner.run(__provider__) 16 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/json_store/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | from .json_store import JSONStore 11 | from .api_keys import APIKeyStore 12 | from .login_tokens import LoginTokenStore 13 | 14 | __all__ = ['JSONStore', 'APIKeyStore', 'LoginTokenStore'] 15 | 16 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/uri_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .base_item import BaseItem 12 | 13 | 14 | class UriItem(BaseItem): 15 | def __init__(self, uri): 16 | BaseItem.__init__(self, name=u'', uri=uri) 17 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .resource_manager import ResourceManager 12 | from .url_resolver import UrlResolver 13 | from .url_to_item_converter import UrlToItemConverter 14 | from .utils import extract_urls 15 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/abstract_provider_runner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | 12 | class AbstractProviderRunner(object): 13 | def __init__(self): 14 | pass 15 | 16 | def run(self, provider, context=None): 17 | raise NotImplementedError() 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # other 21 | .idea/ 22 | .settings/ 23 | .project/ 24 | .pydevproject/ 25 | 26 | __pycache__/ 27 | 28 | *.py[cod] 29 | 30 | *.log 31 | 32 | append_to_languages.py 33 | new_strings.txt 34 | convert_settings.py 35 | core.po 36 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/constants/const_content_types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | FILES = 'files' 12 | SONGS = 'songs' 13 | ARTISTS = 'artists' 14 | ALBUMS = 'albums' 15 | MOVIES = 'movies' 16 | TV_SHOWS = 'tvshows' 17 | EPISODES = 'episodes' 18 | VIDEOS = 'videos' 19 | MUSIC_VIDEOS = 'musicvideos' 20 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | 12 | class KodionException(Exception): 13 | def __init__(self, message): 14 | Exception.__init__(self, message) 15 | self._message = message 16 | 17 | def get_message(self): 18 | return self._message 19 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/youtube_exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .. import kodion 12 | 13 | 14 | class LoginException(kodion.KodionException): 15 | pass 16 | 17 | 18 | class YouTubeException(kodion.KodionException): 19 | pass 20 | 21 | 22 | class InvalidGrant(kodion.KodionException): 23 | pass 24 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .xbmc.xbmc_plugin_settings import XbmcPluginSettings as Settings 12 | from .xbmc.xbmc_context import XbmcContext as Context 13 | from .xbmc.xbmc_context_ui import XbmcContextUI as ContextUI 14 | from .xbmc.xbmc_runner import XbmcRunner as Runner 15 | 16 | 17 | __all__ = ['Settings', 'Context', 'ContextUI', 'Runner'] 18 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/abstract_player.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | 12 | class AbstractPlayer(object): 13 | def __init__(self): 14 | pass 15 | 16 | def play(self, playlist_index=-1): 17 | raise NotImplementedError() 18 | 19 | def stop(self): 20 | raise NotImplementedError() 21 | 22 | def pause(self): 23 | raise NotImplementedError() 24 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/constants/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from . import const_settings as setting 12 | from . import const_localize as localize 13 | from . import const_sort_methods as sort_method 14 | from . import const_content_types as content_type 15 | from . import const_paths as paths 16 | 17 | 18 | __all__ = ['setting', 'localize', 'sort_method', 'content_type', 'paths'] 19 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/image_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .base_item import BaseItem 12 | 13 | 14 | class ImageItem(BaseItem): 15 | def __init__(self, name, uri, image=u'', fanart=u''): 16 | BaseItem.__init__(self, name, uri, image, fanart) 17 | self._title = None 18 | 19 | def set_title(self, title): 20 | self._title = title 21 | 22 | def get_title(self): 23 | return self._title 24 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/abstract_playlist.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | 12 | class AbstractPlaylist(object): 13 | def __init__(self): 14 | pass 15 | 16 | def clear(self): 17 | raise NotImplementedError() 18 | 19 | def add(self, base_item): 20 | raise NotImplementedError() 21 | 22 | def shuffle(self): 23 | raise NotImplementedError() 24 | 25 | def unshuffle(self): 26 | raise NotImplementedError() 27 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/register_provider_path.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | 12 | class RegisterProviderPath(object): 13 | def __init__(self, re_path): 14 | self._kodion_re_path = re_path 15 | 16 | def __call__(self, func): 17 | def wrapper(*args, **kwargs): 18 | # only use a wrapper if you need extra code to be run here 19 | return func(*args, **kwargs) 20 | 21 | wrapper.kodion_re_path = self._kodion_re_path 22 | return wrapper 23 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | key_sets = { 12 | 'youtube-tv': { 13 | 'id': 'ODYxNTU2NzA4NDU0LWQ2ZGxtM2xoMDVpZGQ4bnBlazE4azZiZThiYTNvYzY4', 14 | 'key': 'QUl6YVN5QzZmdlpTSkhBN1Z6NWo4akNpS1J0N3RVSU9xakUyTjNn', 15 | 'secret': 'U2JvVmhvRzlzMHJOYWZpeENTR0dLWEFU' 16 | }, 17 | 'provided': { 18 | '0': { 19 | 'id': '', 20 | 'key': '', 21 | 'secret': '' 22 | } 23 | } 24 | } 25 | 26 | __all__ = ['kodion', 'youtube', 'key_sets', 'refresh'] 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Repository Moved 3 | Development will continue at https://github.com/anxdpanic/plugin.video.youtube 4 | 5 | ![](https://raw.githubusercontent.com/Kolifanes/plugin.video.youtube/master/icon.png) 6 | 7 | ![Build Status](https://img.shields.io/travis/jdf76/plugin.video.youtube/master.svg) 8 | ![License](https://img.shields.io/badge/license-GPL--2.0--only-success.svg) 9 | ![Kodi Version](https://img.shields.io/badge/kodi-isengard%2B-success.svg) 10 | ![Contributors](https://img.shields.io/github/contributors/jdf76/plugin.video.youtube.svg) 11 | 12 | ## Links 13 | 14 | * [YouTube](http://www.youtube.com) 15 | * [Support thread](https://ytaddon.page.link/forum) 16 | * [Wiki](https://github.com/jdf76/plugin.video.youtube/wiki) 17 | 18 | --- 19 | 20 | ![](https://i.imgur.com/fzPmDDJ.gif) 21 | 22 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_plugin_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from ..abstract_settings import AbstractSettings 12 | 13 | 14 | class XbmcPluginSettings(AbstractSettings): 15 | def __init__(self, xbmc_addon): 16 | AbstractSettings.__init__(self) 17 | 18 | self._xbmc_addon = xbmc_addon 19 | 20 | def get_string(self, setting_id, default_value=None): 21 | return self._xbmc_addon.getSetting(setting_id) 22 | 23 | def set_string(self, setting_id, value): 24 | self._xbmc_addon.setSetting(setting_id, value) 25 | 26 | def open_settings(self): 27 | self._xbmc_addon.openSetting() 28 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/json_store/api_keys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | from . import JSONStore 11 | 12 | 13 | class APIKeyStore(JSONStore): 14 | def __init__(self): 15 | JSONStore.__init__(self, 'api_keys.json') 16 | 17 | def set_defaults(self): 18 | data = self.get_data() 19 | if 'keys' not in data: 20 | data = {'keys': {'personal': {'api_key': '', 'client_id': '', 'client_secret': ''}, 'developer': {}}} 21 | if 'personal' not in data['keys']: 22 | data['keys']['personal'] = {'api_key': '', 'client_id': '', 'client_secret': ''} 23 | if 'developer' not in data['keys']: 24 | data['keys']['developer'] = {} 25 | self.save(data) 26 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/abstract_progress_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | 12 | class AbstractProgressDialog(object): 13 | def __init__(self, total=100): 14 | self._total = int(total) 15 | self._position = 0 16 | 17 | def get_total(self): 18 | return self._total 19 | 20 | def get_position(self): 21 | return self._position 22 | 23 | def close(self): 24 | raise NotImplementedError() 25 | 26 | def set_total(self, total): 27 | self._total = int(total) 28 | 29 | def update(self, steps=1, text=None): 30 | raise NotImplementedError() 31 | 32 | def is_aborted(self): 33 | raise NotImplementedError() 34 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | # import base exception of kodion directly into the kodion namespace 12 | from .exceptions import KodionException 13 | 14 | # decorator for registering paths for navigating of a provider 15 | from .register_provider_path import RegisterProviderPath 16 | 17 | # Abstract provider for implementation by the user 18 | from .abstract_provider import AbstractProvider 19 | 20 | # import specialized implementation into the kodion namespace 21 | from .impl import Context 22 | 23 | from .constants import * 24 | 25 | from . import logger 26 | 27 | __all__ = ['KodionException', 'RegisterProviderPath', 'AbstractProvider', 'Context', 'utils', 'json_store', 'logger'] 28 | 29 | __version__ = '1.5.4' 30 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/favorites_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .directory_item import DirectoryItem 12 | from .. import constants 13 | 14 | 15 | class FavoritesItem(DirectoryItem): 16 | def __init__(self, context, alt_name=None, image=None, fanart=None): 17 | name = alt_name 18 | if not name: 19 | name = context.localize(constants.localize.FAVORITES) 20 | 21 | if image is None: 22 | image = context.create_resource_path('media/favorites.png') 23 | 24 | DirectoryItem.__init__(self, name, context.create_uri([constants.paths.FAVORITES, 'list']), image=image) 25 | if fanart: 26 | self.set_fanart(fanart) 27 | else: 28 | self.set_fanart(context.get_fanart()) 29 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/watch_later_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .directory_item import DirectoryItem 12 | from .. import constants 13 | 14 | 15 | class WatchLaterItem(DirectoryItem): 16 | def __init__(self, context, alt_name=None, image=None, fanart=None): 17 | name = alt_name 18 | if not name: 19 | name = context.localize(constants.localize.WATCH_LATER) 20 | 21 | if image is None: 22 | image = context.create_resource_path('media/watch_later.png') 23 | 24 | DirectoryItem.__init__(self, name, context.create_uri([constants.paths.WATCH_LATER, 'list']), image=image) 25 | if fanart: 26 | self.set_fanart(fanart) 27 | else: 28 | self.set_fanart(context.get_fanart()) 29 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/constants/const_localize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | SELECT_VIDEO_QUALITY = 30010 12 | 13 | COMMON_PLEASE_WAIT = 30119 14 | 15 | FAVORITES = 30100 16 | FAVORITES_ADD = 30101 17 | FAVORITES_REMOVE = 30108 18 | 19 | SEARCH = 30102 20 | SEARCH_TITLE = 30102 21 | SEARCH_NEW = 30110 22 | SEARCH_RENAME = 30113 23 | SEARCH_REMOVE = 30108 24 | SEARCH_CLEAR = 30120 25 | 26 | SETUP_WIZARD_EXECUTE = 30030 27 | SETUP_VIEW_DEFAULT = 30027 28 | SETUP_VIEW_VIDEOS = 30028 29 | 30 | LIBRARY = 30103 31 | HIGHLIGHTS = 30104 32 | ARCHIVE = 30105 33 | NEXT_PAGE = 30106 34 | 35 | WATCH_LATER = 30107 36 | WATCH_LATER_ADD = 30107 37 | WATCH_LATER_REMOVE = 30108 38 | 39 | LATEST_VIDEOS = 30109 40 | 41 | CONFIRM_DELETE = 30114 42 | CONFIRM_REMOVE = 30115 43 | DELETE_CONTENT = 30116 44 | REMOVE_CONTENT = 30117 45 | 46 | WATCH_LATER_RETRIEVAL_PAGE = 30711 47 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/next_page_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .directory_item import DirectoryItem 12 | from .. import constants 13 | 14 | 15 | class NextPageItem(DirectoryItem): 16 | def __init__(self, context, current_page=1, image=None, fanart=None): 17 | new_params = {} 18 | new_params.update(context.get_params()) 19 | new_params['page'] = str(current_page + 1) 20 | name = context.localize(constants.localize.NEXT_PAGE, 'Next Page') 21 | if name.find('%d') != -1: 22 | name %= current_page + 1 23 | 24 | DirectoryItem.__init__(self, name, context.create_uri(context.get_path(), new_params), image=image) 25 | if fanart: 26 | self.set_fanart(fanart) 27 | else: 28 | self.set_fanart(context.get_fanart()) 29 | 30 | self.next_page = True 31 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/search_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .directory_item import DirectoryItem 12 | from .. import constants 13 | 14 | 15 | class SearchItem(DirectoryItem): 16 | def __init__(self, context, alt_name=None, image=None, fanart=None, location=False): 17 | name = alt_name 18 | if not name: 19 | name = context.localize(constants.localize.SEARCH) 20 | 21 | if image is None: 22 | image = context.create_resource_path('media/search.png') 23 | 24 | params = dict() 25 | if location: 26 | params = {'location': location} 27 | 28 | DirectoryItem.__init__(self, name, context.create_uri([constants.paths.SEARCH, 'list'], params=params), image=image) 29 | if fanart: 30 | self.set_fanart(fanart) 31 | else: 32 | self.set_fanart(context.get_fanart()) 33 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .utils import to_json, from_json, to_jsons 12 | 13 | from .uri_item import UriItem 14 | from .base_item import BaseItem 15 | from .audio_item import AudioItem 16 | from .directory_item import DirectoryItem 17 | from .watch_later_item import WatchLaterItem 18 | from .favorites_item import FavoritesItem 19 | from .search_item import SearchItem 20 | from .new_search_item import NewSearchItem 21 | from .search_history_item import SearchHistoryItem 22 | from .next_page_item import NextPageItem 23 | from .video_item import VideoItem 24 | from .image_item import ImageItem 25 | 26 | 27 | __all__ = ['BaseItem', 'AudioItem', 'DirectoryItem', 'VideoItem', 'ImageItem', 'WatchLaterItem', 'FavoritesItem', 28 | 'SearchItem', 'NewSearchItem', 'SearchHistoryItem', 'NextPageItem', 'UriItem', 29 | 'from_json', 'to_json', 'to_jsons'] 30 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/favorite_list.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .storage import Storage 12 | from .. import items 13 | 14 | 15 | class FavoriteList(Storage): 16 | def __init__(self, filename): 17 | Storage.__init__(self, filename) 18 | 19 | def clear(self): 20 | self._clear() 21 | 22 | def list(self): 23 | result = [] 24 | 25 | for key in self._get_ids(): 26 | data = self._get(key) 27 | item = items.from_json(data[0]) 28 | result.append(item) 29 | 30 | def _sort(_item): 31 | return _item.get_name().upper() 32 | 33 | return sorted(result, key=_sort, reverse=False) 34 | 35 | def add(self, base_item): 36 | item_json_data = items.to_json(base_item) 37 | self._set(base_item.get_id(), item_json_data) 38 | 39 | def remove(self, base_item): 40 | self._remove(base_item.get_id()) 41 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/directory_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .base_item import BaseItem 12 | 13 | 14 | class DirectoryItem(BaseItem): 15 | def __init__(self, name, uri, image=u'', fanart=u''): 16 | BaseItem.__init__(self, name, uri, image, fanart) 17 | self._plot = self.get_name() 18 | self._is_action = False 19 | self._channel_subscription_id = None 20 | 21 | def set_name(self, name): 22 | self._name = name 23 | 24 | def set_plot(self, plot): 25 | self._plot = plot 26 | 27 | def get_plot(self): 28 | return self._plot 29 | 30 | def is_action(self): 31 | return self._is_action 32 | 33 | def set_action(self, value): 34 | if isinstance(value, bool): 35 | self._is_action = value 36 | 37 | def get_channel_subscription_id(self): 38 | return self._channel_subscription_id 39 | 40 | def set_channel_subscription_id(self, value): 41 | self._channel_subscription_id = value 42 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import xbmc 12 | import xbmcaddon 13 | 14 | DEBUG = xbmc.LOGDEBUG 15 | INFO = xbmc.LOGINFO 16 | NOTICE = INFO 17 | WARNING = xbmc.LOGWARNING 18 | ERROR = xbmc.LOGERROR 19 | FATAL = xbmc.LOGFATAL 20 | SEVERE = FATAL 21 | NONE = xbmc.LOGNONE 22 | 23 | _ADDON_ID = 'plugin.video.youtube' 24 | 25 | 26 | def log(text, log_level=DEBUG, addon_id=_ADDON_ID): 27 | if not addon_id: 28 | addon_id = xbmcaddon.Addon().getAddonInfo('id') 29 | log_line = '[%s] %s' % (addon_id, text) 30 | xbmc.log(msg=log_line, level=log_level) 31 | 32 | 33 | def log_debug(text, addon_id=_ADDON_ID): 34 | log(text, DEBUG, addon_id) 35 | 36 | 37 | def log_info(text, addon_id=_ADDON_ID): 38 | log(text, INFO, addon_id) 39 | 40 | 41 | def log_notice(text, addon_id=_ADDON_ID): 42 | log(text, NOTICE, addon_id) 43 | 44 | 45 | def log_warning(text, addon_id=_ADDON_ID): 46 | log(text, WARNING, addon_id) 47 | 48 | 49 | def log_error(text, addon_id=_ADDON_ID): 50 | log(text, ERROR, addon_id) 51 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six import string_types 12 | 13 | import xbmcgui 14 | from ..abstract_progress_dialog import AbstractProgressDialog 15 | 16 | 17 | class XbmcProgressDialog(AbstractProgressDialog): 18 | def __init__(self, heading, text): 19 | AbstractProgressDialog.__init__(self, 100) 20 | self._dialog = xbmcgui.DialogProgress() 21 | self._dialog.create(heading, text) 22 | 23 | # simple reset because KODI won't do it :( 24 | self._position = 1 25 | self.update(steps=-1) 26 | 27 | def close(self): 28 | if self._dialog: 29 | self._dialog.close() 30 | self._dialog = None 31 | 32 | def update(self, steps=1, text=None): 33 | self._position += steps 34 | position = int(float((100.0 // self._total)) * self._position) 35 | 36 | if isinstance(text, string_types): 37 | self._dialog.update(position, text) 38 | else: 39 | self._dialog.update(position) 40 | 41 | def is_aborted(self): 42 | return self._dialog.iscanceled() 43 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog_bg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six import string_types 12 | 13 | import xbmcgui 14 | from ..abstract_progress_dialog import AbstractProgressDialog 15 | 16 | 17 | class XbmcProgressDialogBG(AbstractProgressDialog): 18 | def __init__(self, heading, text): 19 | AbstractProgressDialog.__init__(self, 100) 20 | self._dialog = xbmcgui.DialogProgressBG() 21 | self._dialog.create(heading, text) 22 | 23 | # simple reset because KODI won't do it :( 24 | self._position = 1 25 | self.update(steps=-1) 26 | 27 | def close(self): 28 | if self._dialog: 29 | self._dialog.close() 30 | self._dialog = None 31 | 32 | def update(self, steps=1, text=None): 33 | self._position += steps 34 | position = int((100.0 / float(self._total)) * float(self._position)) 35 | 36 | if isinstance(text, string_types): 37 | self._dialog.update(percent=position, message=text) 38 | else: 39 | self._dialog.update(percent=position) 40 | 41 | def is_aborted(self): 42 | return False 43 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/watch_later_list.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import datetime 12 | 13 | from .storage import Storage 14 | from .. import items 15 | 16 | 17 | class WatchLaterList(Storage): 18 | def __init__(self, filename): 19 | Storage.__init__(self, filename) 20 | 21 | def clear(self): 22 | self._clear() 23 | 24 | def list(self): 25 | result = [] 26 | 27 | for key in self._get_ids(): 28 | data = self._get(key) 29 | item = items.from_json(data[0]) 30 | result.append(item) 31 | 32 | def _sort(video_item): 33 | return video_item.get_date() 34 | 35 | self.sync() 36 | 37 | sorted_list = sorted(result, key=_sort, reverse=False) 38 | return sorted_list 39 | 40 | def add(self, base_item): 41 | now = datetime.datetime.now() 42 | base_item.set_date(now.year, now.month, now.day, now.hour, now.minute, now.second) 43 | 44 | item_json_data = items.to_json(base_item) 45 | self._set(base_item.get_id(), item_json_data) 46 | 47 | def remove(self, base_item): 48 | self._remove(base_item.get_id()) 49 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from . import datetime_parser 12 | from .methods import loose_version 13 | from .methods import * 14 | from .search_history import SearchHistory 15 | from .favorite_list import FavoriteList 16 | from .watch_later_list import WatchLaterList 17 | from .function_cache import FunctionCache 18 | from .access_manager import AccessManager 19 | from .http_server import get_http_server, is_httpd_live, get_client_ip_address 20 | from .monitor import YouTubeMonitor 21 | from .player import YouTubePlayer 22 | from .playback_history import PlaybackHistory 23 | from .data_cache import DataCache 24 | from .system_version import SystemVersion 25 | from . import ip_api 26 | 27 | 28 | __all__ = ['SearchHistory', 'FavoriteList', 'WatchLaterList', 'FunctionCache', 'AccessManager', 29 | 'strip_html_from_text', 'create_path', 'create_uri_path', 'find_best_fit', 'to_unicode', 'to_utf8', 30 | 'datetime_parser', 'select_stream', 'get_http_server', 'is_httpd_live', 'YouTubeMonitor', 31 | 'make_dirs', 'loose_version', 'ip_api', 'PlaybackHistory', 'DataCache', 'get_client_ip_address', 32 | 'SystemVersion', 'find_video_id', 'YouTubePlayer'] 33 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/new_search_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .directory_item import DirectoryItem 12 | from .. import constants 13 | 14 | 15 | class NewSearchItem(DirectoryItem): 16 | def __init__(self, context, alt_name=None, image=None, fanart=None, incognito=False, channel_id='', addon_id='', location=False): 17 | name = alt_name 18 | if not name: 19 | name = context.get_ui().bold(context.localize(constants.localize.SEARCH_NEW)) 20 | 21 | if image is None: 22 | image = context.create_resource_path('media/new_search.png') 23 | 24 | item_params = {} 25 | if addon_id: 26 | item_params.update({'addon_id': addon_id}) 27 | if incognito: 28 | item_params.update({'incognito': incognito}) 29 | if channel_id: 30 | item_params.update({'channel_id': channel_id}) 31 | if location: 32 | item_params.update({'location': location}) 33 | 34 | DirectoryItem.__init__(self, name, context.create_uri([constants.paths.SEARCH, 'input'], params=item_params), image=image) 35 | if fanart: 36 | self.set_fanart(fanart) 37 | else: 38 | self.set_fanart(context.get_fanart()) 39 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/ip_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | import requests 11 | 12 | 13 | class Locator: 14 | 15 | def __init__(self, context): 16 | self._base_url = 'http://ip-api.com' 17 | self._response = dict() 18 | self._context = context 19 | 20 | def response(self): 21 | return self._response 22 | 23 | def locate_requester(self): 24 | request_url = '/'.join([self._base_url, 'json']) 25 | response = requests.get(request_url) 26 | self._response = response.json() 27 | 28 | def success(self): 29 | successful = self.response().get('status', 'fail') == 'success' 30 | if successful: 31 | self._context.log_debug('Location request was successful') 32 | else: 33 | self._context.log_error(self.response().get('message', 'Location request failed with no error message')) 34 | return successful 35 | 36 | def coordinates(self): 37 | lat = None 38 | lon = None 39 | if self.success(): 40 | lat = self._response.get('lat') 41 | lon = self._response.get('lon') 42 | if lat is None or lon is None: 43 | self._context.log_error('No coordinates returned') 44 | return None 45 | else: 46 | self._context.log_debug('Coordinates found') 47 | return lat, lon 48 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/search_history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2019 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import hashlib 12 | 13 | from .storage import Storage 14 | from .methods import to_utf8 15 | 16 | 17 | class SearchHistory(Storage): 18 | def __init__(self, filename, max_items=10): 19 | Storage.__init__(self, filename, max_item_count=max_items) 20 | 21 | def is_empty(self): 22 | return self._is_empty() 23 | 24 | def list(self): 25 | result = [] 26 | 27 | keys = self._get_ids(oldest_first=False) 28 | for i, key in enumerate(keys): 29 | if i >= self._max_item_count: 30 | break 31 | item = self._get(key) 32 | 33 | if item: 34 | result.append(item[0]) 35 | 36 | return result 37 | 38 | def clear(self): 39 | self._clear() 40 | 41 | @staticmethod 42 | def _make_id(search_text): 43 | m = hashlib.md5() 44 | m.update(to_utf8(search_text)) 45 | return m.hexdigest() 46 | 47 | def rename(self, old_search_text, new_search_text): 48 | self.remove(old_search_text) 49 | self.update(new_search_text) 50 | 51 | def remove(self, search_text): 52 | self._remove(self._make_id(search_text)) 53 | 54 | def update(self, search_text): 55 | self._set(self._make_id(search_text), search_text) 56 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/search_history_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from .directory_item import DirectoryItem 12 | from .. import constants 13 | 14 | 15 | class SearchHistoryItem(DirectoryItem): 16 | def __init__(self, context, query, image=None, fanart=None, location=False): 17 | if image is None: 18 | image = context.create_resource_path('media/search.png') 19 | 20 | params = {'q': query} 21 | if location: 22 | params['location'] = location 23 | 24 | DirectoryItem.__init__(self, query, context.create_uri([constants.paths.SEARCH, 'query'], params=params), image=image) 25 | if fanart: 26 | self.set_fanart(fanart) 27 | else: 28 | self.set_fanart(context.get_fanart()) 29 | 30 | context_menu = [(context.localize(constants.localize.SEARCH_REMOVE), 31 | 'RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'remove'], params={'q': query})), 32 | (context.localize(constants.localize.SEARCH_RENAME), 33 | 'RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'rename'], params={'q': query})), 34 | (context.localize(constants.localize.SEARCH_CLEAR), 35 | 'RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'clear']))] 36 | self.set_context_menu(context_menu) 37 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | 12 | class AbstractContextUI(object): 13 | def __init__(self): 14 | pass 15 | 16 | def create_progress_dialog(self, heading, text=None, background=False): 17 | raise NotImplementedError() 18 | 19 | def get_skin_id(self): 20 | raise NotImplementedError() 21 | 22 | def on_keyboard_input(self, title, default='', hidden=False): 23 | raise NotImplementedError() 24 | 25 | def on_numeric_input(self, title, default=''): 26 | raise NotImplementedError() 27 | 28 | def on_yes_no_input(self, title, text): 29 | raise NotImplementedError() 30 | 31 | def on_ok(self, title, text): 32 | raise NotImplementedError() 33 | 34 | def on_remove_content(self, content_name): 35 | raise NotImplementedError() 36 | 37 | def on_select(self, title, items=None): 38 | raise NotImplementedError() 39 | 40 | def open_settings(self): 41 | raise NotImplementedError() 42 | 43 | def show_notification(self, message, header='', image_uri='', time_milliseconds=5000): 44 | raise NotImplementedError() 45 | 46 | @staticmethod 47 | def refresh_container(): 48 | """ 49 | Needs to be implemented by a mock for testing or the real deal. 50 | This will refresh the current container or list. 51 | :return: 52 | """ 53 | raise NotImplementedError() 54 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_player.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import xbmc 12 | from ..abstract_player import AbstractPlayer 13 | 14 | 15 | class XbmcPlayer(AbstractPlayer): 16 | def __init__(self, player_type, context): 17 | AbstractPlayer.__init__(self) 18 | 19 | self._player_type = player_type 20 | if player_type == 'audio': 21 | self._player_type = 'music' 22 | 23 | self._context = context 24 | 25 | def play(self, playlist_index=-1): 26 | """ 27 | We call the player in this way, because 'Player.play(...)' will call the addon again while the instance is 28 | running. This is somehow shitty, because we couldn't release any resources and in our case we couldn't release 29 | the cache. So this is the solution to prevent a locked database (sqlite). 30 | """ 31 | self._context.execute('Playlist.PlayOffset(%s,%d)' % (self._player_type, playlist_index)) 32 | 33 | """ 34 | playlist = None 35 | if self._player_type == 'video': 36 | playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) 37 | elif self._player_type == 'music': 38 | playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) 39 | 40 | if playlist_index >= 0: 41 | xbmc.Player().play(item=playlist, startpos=playlist_index) 42 | else: 43 | xbmc.Player().play(item=playlist) 44 | """ 45 | 46 | def stop(self): 47 | xbmc.Player().stop() 48 | 49 | def pause(self): 50 | xbmc.Player().pause() 51 | 52 | def is_playing(self): 53 | return xbmc.Player().isPlaying() 54 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/signature/json_script_engine.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six.moves import range 12 | 13 | 14 | class JsonScriptEngine(object): 15 | def __init__(self, json_script): 16 | self._json_script = json_script 17 | 18 | def execute(self, signature): 19 | _signature = signature 20 | 21 | _actions = self._json_script['actions'] 22 | for action in _actions: 23 | func = ''.join(['_', action['func']]) 24 | params = action['params'] 25 | 26 | if func == '_return': 27 | break 28 | 29 | for i in range(len(params)): 30 | param = params[i] 31 | if param == '%SIG%': 32 | param = _signature 33 | params[i] = param 34 | break 35 | 36 | method = getattr(self, func) 37 | if method: 38 | _signature = method(*params) 39 | else: 40 | raise Exception("Unknown method '%s'" % func) 41 | 42 | return _signature 43 | 44 | @staticmethod 45 | def _join(signature): 46 | return ''.join(signature) 47 | 48 | @staticmethod 49 | def _list(signature): 50 | return list(signature) 51 | 52 | @staticmethod 53 | def _slice(signature, b): 54 | del signature[b:] 55 | return signature 56 | 57 | @staticmethod 58 | def _splice(signature, a, b): 59 | del signature[a:b] 60 | return signature 61 | 62 | @staticmethod 63 | def _reverse(signature): 64 | return signature[::-1] 65 | 66 | @staticmethod 67 | def _swap(signature, b): 68 | c = signature[0] 69 | signature[0] = signature[b % len(signature)] 70 | signature[b] = c 71 | return signature 72 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/constants/const_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | THUMB_SIZE = 'kodion.thumbnail.size' # (int) 12 | SHOW_FANART = 'kodion.fanart.show' # (bool) 13 | SAFE_SEARCH = 'kodion.safe.search' # (int) 14 | ITEMS_PER_PAGE = 'kodion.content.max_per_page' # (int) 15 | SEARCH_SIZE = 'kodion.search.size' # (int) 16 | CACHE_SIZE = 'kodion.cache.size' # (int) 17 | VIDEO_QUALITY = 'kodion.video.quality' # (int) 18 | VIDEO_QUALITY_ASK = 'kodion.video.quality.ask' # (bool) 19 | USE_DASH = 'kodion.video.quality.mpd' # (bool) 20 | MPD_QUALITY_SELECTION = 'kodion.mpd.quality.selection' # (int) 21 | MPD_30FPS_LIMIT = 'kodion.mpd.limit.30' # (bool) 22 | AUDIO_ONLY = 'kodion.audio_only' # (bool) 23 | AGE_GATE = 'kodion.age.gate' # (bool) 24 | SUBTITLE_LANGUAGE = 'kodion.subtitle.languages.num' # (int) 25 | SUBTITLE_DOWNLOAD = 'kodion.subtitle.download' # (bool) 26 | SETUP_WIZARD = 'kodion.setup_wizard' # (bool) 27 | VERIFY_SSL = 'simple.requests.ssl.verify' # (bool) 28 | LOCATION = 'youtube.location' # (str) 29 | LOCATION_RADIUS = 'youtube.location.radius' # (int) 30 | PLAY_COUNT_MIN_PERCENT = 'kodion.play_count.percent' # (int) 31 | USE_PLAYBACK_HISTORY = 'kodion.playback.history' # (bool) 32 | REMOTE_FRIENDLY_SEARCH = 'youtube.search.remote.friendly' # (bool) 33 | 34 | SUPPORT_ALTERNATIVE_PLAYER = 'kodion.support.alternative_player' # (bool) 35 | ALTERNATIVE_PLAYER_WEB_URLS = 'kodion.alternative_player.web.urls' # (bool) 36 | 37 | ALLOW_DEV_KEYS = 'youtube.allow.dev.keys' # (bool) 38 | 39 | DASH_VIDEOS = 'kodion.mpd.videos' # (bool) 40 | DASH_INCL_HDR = 'kodion.mpd.hdr' # (bool) 41 | DASH_LIVE_STREAMS = 'kodion.mpd.live_streams' # (bool) 42 | 43 | HTTPD_PORT = 'kodion.mpd.proxy.port' # (number) 44 | HTTPD_LISTEN = 'kodion.http.listen' # (string) 45 | HTTPD_WHITELIST = 'kodion.http.ip.whitelist' # (string) 46 | 47 | API_CONFIG_PAGE = 'youtube.api.config.page' # (bool) 48 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/runner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import copy 12 | import timeit 13 | 14 | from .impl import Runner 15 | from .impl import Context 16 | 17 | from . import debug 18 | 19 | __all__ = ['run'] 20 | 21 | __DEBUG_RUNTIME = False 22 | __DEBUG_RUNTIME_SINGLE_FILE = False 23 | 24 | __RUNNER__ = Runner() 25 | 26 | 27 | def run(provider, context=None): 28 | start_time = timeit.default_timer() 29 | 30 | if not context: 31 | context = Context(plugin_id='plugin.video.youtube') 32 | 33 | context.log_debug('Starting Kodion framework by bromix...') 34 | python_version = 'Unknown version of Python' 35 | try: 36 | import platform 37 | 38 | python_version = str(platform.python_version()) 39 | python_version = 'Python %s' % python_version 40 | except: 41 | # do nothing 42 | pass 43 | 44 | version = context.get_system_version() 45 | name = context.get_name() 46 | addon_version = context.get_version() 47 | redacted = '' 48 | context_params = copy.deepcopy(context.get_params()) 49 | if 'api_key' in context_params: 50 | context_params['api_key'] = redacted 51 | if 'client_id' in context_params: 52 | context_params['client_id'] = redacted 53 | if 'client_secret' in context_params: 54 | context_params['client_secret'] = redacted 55 | 56 | context.log_notice('Running: %s (%s) on %s with %s\n\tPath: %s\n\tParams: %s' % 57 | (name, addon_version, version, python_version, 58 | context.get_path(), str(context_params))) 59 | 60 | __RUNNER__.run(provider, context) 61 | provider.tear_down(context) 62 | 63 | elapsed = timeit.default_timer() - start_time 64 | 65 | if __DEBUG_RUNTIME: 66 | debug.runtime(context, addon_version, elapsed, single_file=__DEBUG_RUNTIME_SINGLE_FILE) 67 | 68 | context.log_debug('Shutdown of Kodion after |%s| seconds' % str(round(elapsed, 4))) 69 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/debug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import os 12 | import json 13 | 14 | 15 | def debug_here(host='localhost'): 16 | import sys 17 | 18 | for comp in sys.path: 19 | if comp.find('addons') != -1: 20 | pydevd_path = os.path.normpath(os.path.join(comp, os.pardir, 'script.module.pydevd', 'lib')) 21 | sys.path.append(pydevd_path) 22 | break 23 | 24 | # noinspection PyUnresolvedReferences,PyPackageRequirements 25 | import pydevd 26 | pydevd.settrace(host, stdoutToServer=True, stderrToServer=True) 27 | 28 | 29 | def runtime(context, addon_version, elapsed, single_file=True): 30 | if not single_file: 31 | filename_path_part = context.get_path().lstrip('/').rstrip('/').replace('/', '_') 32 | debug_file_name = 'runtime_%s-%s.json' % (filename_path_part, addon_version) 33 | default_contents = {"runtimes": []} 34 | else: 35 | debug_file_name = 'runtime-%s.json' % addon_version 36 | default_contents = {"runtimes": {}} 37 | 38 | debug_file = os.path.join(context.get_debug_path(), debug_file_name) 39 | with open(debug_file, 'a') as _: 40 | pass # touch 41 | 42 | with open(debug_file, 'r') as f: 43 | contents = f.read() 44 | 45 | with open(debug_file, 'w') as f: 46 | if not contents: 47 | contents = default_contents 48 | else: 49 | contents = json.loads(contents) 50 | if not single_file: 51 | items = contents.get('runtimes', []) 52 | items.append({"path": context.get_path(), "parameters": context.get_params(), "runtime": round(elapsed, 4)}) 53 | contents['runtimes'] = items 54 | else: 55 | items = contents.get('runtimes', {}).get(context.get_path(), []) 56 | items.append({"parameters": context.get_params(), "runtime": round(elapsed, 4)}) 57 | contents['runtimes'][context.get_path()] = items 58 | f.write(json.dumps(contents, indent=4)) 59 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six import string_types 12 | 13 | import json 14 | 15 | from .video_item import VideoItem 16 | from .directory_item import DirectoryItem 17 | from .audio_item import AudioItem 18 | from .image_item import ImageItem 19 | 20 | 21 | def from_json(json_data): 22 | """ 23 | Creates a instance of the given json dump or dict. 24 | :param json_data: 25 | :return: 26 | """ 27 | 28 | def _from_json(_json_data): 29 | mapping = {'VideoItem': lambda: VideoItem(u'', u''), 30 | 'DirectoryItem': lambda: DirectoryItem(u'', u''), 31 | 'AudioItem': lambda: AudioItem(u'', u''), 32 | 'ImageItem': lambda: ImageItem(u'', u'')} 33 | 34 | item = None 35 | item_type = _json_data.get('type', None) 36 | for key in mapping: 37 | if item_type == key: 38 | item = mapping[key]() 39 | break 40 | 41 | if item is None: 42 | return _json_data 43 | 44 | data = _json_data.get('data', {}) 45 | for key in data: 46 | if hasattr(item, key): 47 | setattr(item, key, data[key]) 48 | 49 | return item 50 | 51 | if isinstance(json_data, string_types): 52 | json_data = json.loads(json_data) 53 | return _from_json(json_data) 54 | 55 | 56 | def to_jsons(base_item): 57 | return json.dumps(to_json(base_item)) 58 | 59 | 60 | def to_json(base_item): 61 | """ 62 | Convert the given @base_item to json 63 | :param base_item: 64 | :return: json string 65 | """ 66 | 67 | def _to_json(obj): 68 | if isinstance(obj, dict): 69 | return obj.__dict__ 70 | 71 | mapping = {VideoItem: 'VideoItem', 72 | DirectoryItem: 'DirectoryItem', 73 | AudioItem: 'AudioItem', 74 | ImageItem: 'ImageItem'} 75 | 76 | for key in mapping: 77 | if isinstance(obj, key): 78 | return {'type': mapping[key], 'data': obj.__dict__} 79 | 80 | return obj.__dict__ 81 | 82 | return _to_json(base_item) 83 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import json 12 | 13 | import xbmc 14 | from ..abstract_playlist import AbstractPlaylist 15 | from . import xbmc_items 16 | 17 | 18 | class XbmcPlaylist(AbstractPlaylist): 19 | def __init__(self, playlist_type, context): 20 | AbstractPlaylist.__init__(self) 21 | 22 | self._context = context 23 | self._playlist = None 24 | if playlist_type == 'video': 25 | self._playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) 26 | elif playlist_type == 'audio': 27 | self._playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) 28 | 29 | def clear(self): 30 | self._playlist.clear() 31 | 32 | def add(self, base_item): 33 | item = xbmc_items.to_video_item(self._context, base_item) 34 | if item: 35 | self._playlist.add(base_item.get_uri(), listitem=item) 36 | 37 | def shuffle(self): 38 | self._playlist.shuffle() 39 | 40 | def unshuffle(self): 41 | self._playlist.unshuffle() 42 | 43 | def size(self): 44 | return self._playlist.size() 45 | 46 | def get_items(self): 47 | rpc_request = json.dumps( 48 | { 49 | "jsonrpc": "2.0", 50 | "method": "Playlist.GetItems", 51 | "params": { 52 | "properties": ["title", "file"], 53 | "playlistid": self._playlist.getPlayListId() 54 | }, 55 | "id": 1 56 | }) 57 | 58 | response = json.loads(xbmc.executeJSONRPC(rpc_request)) 59 | 60 | if 'result' in response: 61 | if 'items' in response['result']: 62 | return response['result']['items'] 63 | return [] 64 | else: 65 | if 'error' in response: 66 | message = response['error']['message'] 67 | code = response['error']['code'] 68 | error = 'Requested |%s| received error |%s| and code: |%s|' % (rpc_request, message, code) 69 | else: 70 | error = 'Requested |%s| received error |%s|' % (rpc_request, str(response)) 71 | self._context.log_debug(error) 72 | return [] 73 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/audio_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six.moves import html_parser 12 | 13 | from .base_item import BaseItem 14 | 15 | 16 | class AudioItem(BaseItem): 17 | def __init__(self, name, uri, image=u'', fanart=u''): 18 | BaseItem.__init__(self, name, uri, image, fanart) 19 | self._duration = None 20 | self._track_number = None 21 | self._year = None 22 | self._genre = None 23 | self._album = None 24 | self._artist = None 25 | self._title = self.get_name() 26 | self._rating = None 27 | 28 | def set_rating(self, rating): 29 | self._rating = float(rating) 30 | 31 | def get_rating(self): 32 | return self._rating 33 | 34 | def set_title(self, title): 35 | try: 36 | title = html_parser.HTMLParser().unescape(title) 37 | except html_parser.HTMLParseError as _: 38 | pass 39 | self._title = title 40 | 41 | def get_title(self): 42 | return self._title 43 | 44 | def set_artist_name(self, artist_name): 45 | self._artist = artist_name 46 | 47 | def get_artist_name(self): 48 | return self._artist 49 | 50 | def set_album_name(self, album_name): 51 | self._album = album_name 52 | 53 | def get_album_name(self): 54 | return self._album 55 | 56 | def set_genre(self, genre): 57 | self._genre = genre 58 | 59 | def get_genre(self): 60 | return self._genre 61 | 62 | def set_year(self, year): 63 | self._year = int(year) 64 | 65 | def set_year_from_datetime(self, date_time): 66 | self.set_year(date_time.year) 67 | 68 | def get_year(self): 69 | return self._year 70 | 71 | def set_track_number(self, track_number): 72 | self._track_number = int(track_number) 73 | 74 | def get_track_number(self): 75 | return self._track_number 76 | 77 | def set_duration_from_milli_seconds(self, milli_seconds): 78 | self.set_duration_from_seconds(int(milli_seconds) // 1000) 79 | 80 | def set_duration_from_seconds(self, seconds): 81 | self._duration = int(seconds) 82 | 83 | def set_duration_from_minutes(self, minutes): 84 | self.set_duration_from_seconds(int(minutes) * 60) 85 | 86 | def get_duration(self): 87 | return self._duration 88 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | _xbmc = True 12 | 13 | try: 14 | from xbmcplugin import * 15 | except: 16 | _xbmc = False 17 | _count = 0 18 | 19 | 20 | def _const(name): 21 | if _xbmc: 22 | return eval(name) 23 | else: 24 | global _count 25 | _count += 1 26 | return _count 27 | 28 | 29 | ALBUM = _const('SORT_METHOD_ALBUM') 30 | ALBUM_IGNORE_THE = _const('SORT_METHOD_ALBUM_IGNORE_THE') 31 | ARTIST = _const('SORT_METHOD_ARTIST') 32 | ARTIST_IGNORE_THE = _const('SORT_METHOD_ARTIST_IGNORE_THE') 33 | BIT_RATE = _const('SORT_METHOD_BITRATE') 34 | # CHANNEL = _const('SORT_METHOD_CHANNEL') 35 | # COUNTRY = _const('SORT_METHOD_COUNTRY') 36 | DATE = _const('SORT_METHOD_DATE') 37 | DATE_ADDED = _const('SORT_METHOD_DATEADDED') 38 | # DATE_TAKEN = _const('SORT_METHOD_DATE_TAKEN') 39 | DRIVE_TYPE = _const('SORT_METHOD_DRIVE_TYPE') 40 | DURATION = _const('SORT_METHOD_DURATION') 41 | EPISODE = _const('SORT_METHOD_EPISODE') 42 | FILE = _const('SORT_METHOD_FILE') 43 | # FULL_PATH = _const('SORT_METHOD_FULLPATH') 44 | GENRE = _const('SORT_METHOD_GENRE') 45 | LABEL = _const('SORT_METHOD_LABEL') 46 | # LABEL_IGNORE_FOLDERS = _const('SORT_METHOD_LABEL_IGNORE_FOLDERS') 47 | LABEL_IGNORE_THE = _const('SORT_METHOD_LABEL_IGNORE_THE') 48 | # LAST_PLAYED = _const('SORT_METHOD_LASTPLAYED') 49 | LISTENERS = _const('SORT_METHOD_LISTENERS') 50 | MPAA_RATING = _const('SORT_METHOD_MPAA_RATING') 51 | NONE = _const('SORT_METHOD_NONE') 52 | # PLAY_COUNT = _const('SORT_METHOD_PLAYCOUNT') 53 | PLAYLIST_ORDER = _const('SORT_METHOD_PLAYLIST_ORDER') 54 | PRODUCTION_CODE = _const('SORT_METHOD_PRODUCTIONCODE') 55 | PROGRAM_COUNT = _const('SORT_METHOD_PROGRAM_COUNT') 56 | SIZE = _const('SORT_METHOD_SIZE') 57 | SONG_RATING = _const('SORT_METHOD_SONG_RATING') 58 | STUDIO = _const('SORT_METHOD_STUDIO') 59 | STUDIO_IGNORE_THE = _const('SORT_METHOD_STUDIO_IGNORE_THE') 60 | TITLE = _const('SORT_METHOD_TITLE') 61 | TITLE_IGNORE_THE = _const('SORT_METHOD_TITLE_IGNORE_THE') 62 | TRACK_NUMBER = _const('SORT_METHOD_TRACKNUM') 63 | UNSORTED = _const('SORT_METHOD_UNSORTED') 64 | VIDEO_RATING = _const('SORT_METHOD_VIDEO_RATING') 65 | VIDEO_RUNTIME = _const('SORT_METHOD_VIDEO_RUNTIME') 66 | VIDEO_SORT_TITLE = _const('SORT_METHOD_VIDEO_SORT_TITLE') 67 | VIDEO_SORT_TITLE_IGNORE_THE = _const('SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE') 68 | VIDEO_TITLE = _const('SORT_METHOD_VIDEO_TITLE') 69 | VIDEO_YEAR = _const('SORT_METHOD_VIDEO_YEAR') 70 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/json_store/json_store.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | import os 11 | import json 12 | from copy import deepcopy 13 | 14 | import xbmcaddon 15 | import xbmcvfs 16 | import xbmc 17 | 18 | from .. import logger 19 | 20 | 21 | class JSONStore(object): 22 | def __init__(self, filename): 23 | addon_id = 'plugin.video.youtube' 24 | addon = xbmcaddon.Addon(addon_id) 25 | 26 | try: 27 | self.base_path = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8') 28 | except AttributeError: 29 | self.base_path = xbmc.translatePath(addon.getAddonInfo('profile')) 30 | 31 | self.filename = os.path.join(self.base_path, filename) 32 | 33 | self._data = None 34 | self.load() 35 | self.set_defaults() 36 | 37 | def set_defaults(self): 38 | raise NotImplementedError 39 | 40 | def save(self, data): 41 | if data != self._data: 42 | self._data = deepcopy(data) 43 | if not xbmcvfs.exists(self.base_path): 44 | if not self.make_dirs(self.base_path): 45 | logger.log_debug('JSONStore Save |{filename}| failed to create directories.'.format(filename=self.filename.encode("utf-8"))) 46 | return 47 | with open(self.filename, 'w') as jsonfile: 48 | logger.log_debug('JSONStore Save |{filename}|'.format(filename=self.filename.encode("utf-8"))) 49 | json.dump(self._data, jsonfile, indent=4, sort_keys=True) 50 | 51 | def load(self): 52 | if xbmcvfs.exists(self.filename): 53 | with open(self.filename, 'r') as jsonfile: 54 | data = json.load(jsonfile) 55 | self._data = data 56 | logger.log_debug('JSONStore Load |{filename}|'.format(filename=self.filename.encode("utf-8"))) 57 | else: 58 | self._data = dict() 59 | 60 | def get_data(self): 61 | return deepcopy(self._data) 62 | 63 | @staticmethod 64 | def make_dirs(path): 65 | if not path.endswith('/'): 66 | path = ''.join([path, '/']) 67 | path = xbmc.translatePath(path) 68 | if not xbmcvfs.exists(path): 69 | try: 70 | _ = xbmcvfs.mkdirs(path) 71 | except: 72 | pass 73 | if not xbmcvfs.exists(path): 74 | try: 75 | os.makedirs(path) 76 | except: 77 | pass 78 | return xbmcvfs.exists(path) 79 | 80 | return True 81 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/system_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six.moves import map 12 | from six import string_types 13 | from six import python_2_unicode_compatible 14 | 15 | import json 16 | 17 | import xbmc 18 | 19 | 20 | @python_2_unicode_compatible 21 | class SystemVersion(object): 22 | def __init__(self, version, releasename, appname): 23 | if not isinstance(version, tuple): 24 | self._version = (0, 0, 0, 0) 25 | else: 26 | self._version = version 27 | 28 | if not releasename or not isinstance(releasename, string_types): 29 | self._releasename = 'UNKNOWN' 30 | else: 31 | self._releasename = releasename 32 | 33 | if not appname or not isinstance(appname, string_types): 34 | self._appname = 'UNKNOWN' 35 | else: 36 | self._appname = appname 37 | 38 | try: 39 | json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "Application.GetProperties", ' 40 | '"params": {"properties": ["version", "name"]}, "id": 1 }') 41 | json_query = str(json_query) 42 | json_query = json.loads(json_query) 43 | 44 | version_installed = json_query['result']['version'] 45 | self._version = (version_installed.get('major', 1), version_installed.get('minor', 0)) 46 | self._appname = json_query['result']['name'] 47 | except: 48 | self._version = (1, 0) # Frodo 49 | self._appname = 'Unknown Application' 50 | 51 | self._releasename = 'Unknown Release' 52 | if (19, 0) > self._version >= (18, 0): 53 | self._releasename = 'Leia' 54 | elif self._version >= (17, 0): 55 | self._releasename = 'Krypton' 56 | elif self._version >= (16, 0): 57 | self._releasename = 'Jarvis' 58 | elif self._version >= (15, 0): 59 | self._releasename = 'Isengard' 60 | elif self._version >= (14, 0): 61 | self._releasename = 'Helix' 62 | elif self._version >= (13, 0): 63 | self._releasename = 'Gotham' 64 | elif self._version >= (12, 0): 65 | self._releasename = 'Frodo' 66 | 67 | def __str__(self): 68 | obj_str = "%s (%s-%s)" % (self._releasename, self._appname, '.'.join(map(str, self._version))) 69 | return obj_str 70 | 71 | def get_release_name(self): 72 | return self._releasename 73 | 74 | def get_version(self): 75 | return self._version 76 | 77 | def get_app_name(self): 78 | return self._appname 79 | -------------------------------------------------------------------------------- /resources/lib/youtube_registration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | from base64 import b64encode 11 | from youtube_plugin.kodion.json_store import APIKeyStore 12 | from youtube_plugin.kodion.impl import Context 13 | 14 | 15 | def register_api_keys(addon_id, api_key, client_id, client_secret): 16 | """ 17 | Usage: 18 | 19 | addon.xml 20 | --- 21 | 22 | --- 23 | 24 | .py 25 | --- 26 | import youtube_registration 27 | youtube_registration.register_api_keys(addon_id='plugin.video.example', 28 | api_key='A1zaSyA0b5sTjgxzTzYLmVtradlFVBfSHNOJKS0', 29 | client_id='825419953561-ert5tccq1r0upsuqdf5nm3le39czk23a.apps.googleusercontent.com', 30 | client_secret='Y5cE1IKzJQe1NZ0OsOoEqpu3') 31 | # then use your keys by appending an addon_id param to the plugin url 32 | xbmc.executebuiltin('RunPlugin(plugin://plugin.video.youtube/channel/UCaBf1a-dpIsw8OxqH4ki2Kg/?addon_id=plugin.video.example)') 33 | # addon_id will be passed to all following calls 34 | # also see youtube_authentication.py and youtube_requests.py 35 | --- 36 | 37 | :param addon_id: id of the add-on being registered 38 | :param api_key: YouTube Data v3 API key 39 | :param client_id: YouTube Data v3 Client id 40 | :param client_secret: YouTube Data v3 Client secret 41 | """ 42 | 43 | context = Context(plugin_id='plugin.video.youtube') 44 | 45 | if not addon_id or addon_id == 'plugin.video.youtube': 46 | context.log_error('Register API Keys: |%s| Invalid addon_id' % addon_id) 47 | return 48 | 49 | api_jstore = APIKeyStore() 50 | json_api = api_jstore.get_data() 51 | 52 | access_manager = context.get_access_manager() 53 | 54 | jkeys = json_api['keys']['developer'].get(addon_id, {}) 55 | 56 | api_keys = {'origin': addon_id, 'main': {'system': 'JSONStore', 'key': b64encode(api_key), 'id': b64encode(client_id), 'secret': b64encode(client_secret)}} 57 | if jkeys and jkeys == api_keys: 58 | context.log_debug('Register API Keys: |%s| No update required' % addon_id) 59 | else: 60 | json_api['keys']['developer'][addon_id] = api_keys 61 | api_jstore.save(json_api) 62 | context.log_debug('Register API Keys: |%s| Keys registered' % addon_id) 63 | 64 | developers = access_manager.get_developers() 65 | if not developers.get(addon_id, None): 66 | developers[addon_id] = access_manager.get_new_developer() 67 | access_manager.set_developers(developers) 68 | context.log_debug('Creating developer user: |%s|' % addon_id) 69 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/yt_old_actions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from ... import kodion 12 | 13 | 14 | def _process_play_video(provider, context, re_match): 15 | """ 16 | plugin://plugin.video.youtube/?action=play_video&videoid=[ID] 17 | """ 18 | video_id = context.get_param('videoid', '') 19 | if not video_id: 20 | raise kodion.KodionException('old_actions/play_video: missing video_id') 21 | 22 | context.log_warning('DEPRECATED "%s"' % context.get_uri()) 23 | context.log_warning('USE INSTEAD "plugin://%s/play/?video_id=%s"' % (context.get_id(), video_id)) 24 | new_params = {'video_id': video_id} 25 | new_path = '/play/' 26 | new_context = context.clone(new_path=new_path, new_params=new_params) 27 | return provider.on_play(new_context, re_match) 28 | 29 | 30 | def _process_play_all(provider, context, re_match): 31 | """ 32 | plugin://plugin.video.youtube/?path=/root/video&action=play_all&playlist=PL8_6CHho8Tq4Iie-oNxb-g0ECxIhq3CxW 33 | plugin://plugin.video.youtube/?action=play_all&playlist=PLZRRxQcaEjA5fgfW3a3Q0rzm6NgbmICtg&videoid=qmlYe2KS0-Y 34 | """ 35 | playlist_id = context.get_param('playlist', '') 36 | if not playlist_id: 37 | raise kodion.KodionException('old_actions/play_all: missing playlist_id') 38 | 39 | # optional starting video id of the playlist 40 | video_id = context.get_param('videoid', '') 41 | if video_id: 42 | context.log_warning( 43 | 'USE INSTEAD "plugin://%s/play/?playlist_id=%s&video_id=%s"' % (context.get_id(), playlist_id, video_id)) 44 | else: 45 | context.log_warning('USE INSTEAD "plugin://%s/play/?playlist_id=%s"' % (context.get_id(), playlist_id)) 46 | new_params = {'playlist_id': playlist_id} 47 | new_path = '/play/' 48 | new_context = context.clone(new_path=new_path, new_params=new_params) 49 | return provider.on_play(new_context, re_match) 50 | 51 | 52 | def process_old_action(provider, context, re_match): 53 | """ 54 | if context.get_system_version().get_version() >= (15, 0): 55 | message = u"You're using old YouTube-Plugin calls - please review the log for updated end points starting with Isengard" 56 | context.get_ui().show_notification(message, time_milliseconds=15000) 57 | """ 58 | 59 | action = context.get_param('action', '') 60 | if action == 'play_video': 61 | return _process_play_video(provider, context, re_match) 62 | elif action == 'play_all': 63 | return _process_play_all(provider, context, re_match) 64 | else: 65 | raise kodion.KodionException('old_actions: unknown action "%s"' % action) 66 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/playback_history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | import datetime 11 | import sqlite3 12 | 13 | from six import PY2 14 | # noinspection PyPep8Naming 15 | from six.moves import cPickle as pickle 16 | 17 | from .storage import Storage 18 | 19 | 20 | class PlaybackHistory(Storage): 21 | def __init__(self, filename): 22 | Storage.__init__(self, filename) 23 | 24 | def is_empty(self): 25 | return self._is_empty() 26 | 27 | def get_items(self, keys): 28 | def _decode(obj): 29 | if PY2: 30 | obj = str(obj) 31 | return pickle.loads(obj) 32 | 33 | self._open() 34 | placeholders = ','.join(['?' for _ in keys]) 35 | keys = [str(item) for item in keys] 36 | query = 'SELECT * FROM %s WHERE key IN (%s)' % (self._table_name, placeholders) 37 | query_result = self._execute(False, query, keys) 38 | result = {} 39 | if query_result: 40 | for item in query_result: 41 | values = _decode(item[2]).split(',') 42 | result[str(item[0])] = {'play_count': values[0], 'total_time': values[1], 43 | 'played_time': values[2], 'played_percent': values[3], 44 | 'last_played': item[1]} 45 | 46 | self._close() 47 | return result 48 | 49 | def get_item(self, key): 50 | key = str(key) 51 | query_result = self._get(key) 52 | result = {} 53 | if query_result: 54 | values = query_result[0].split(',') 55 | result[key] = {'play_count': values[0], 'total_time': values[1], 56 | 'played_time': values[2], 'played_percent': values[3], 57 | 'last_played': query_result[1]} 58 | return result 59 | 60 | def clear(self): 61 | self._clear() 62 | 63 | def remove(self, video_id): 64 | self._remove(video_id) 65 | 66 | def update(self, video_id, play_count, total_time, played_time, played_percent): 67 | item = ','.join([str(play_count), str(total_time), str(played_time), str(played_percent)]) 68 | self._set(str(video_id), item) 69 | 70 | def _set(self, item_id, item): 71 | def _encode(obj): 72 | return sqlite3.Binary(pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)) 73 | 74 | self._open() 75 | now = datetime.datetime.now() + datetime.timedelta(microseconds=1) # add 1 microsecond, required for dbapi2 76 | query = 'REPLACE INTO %s (key,time,value) VALUES(?,?,?)' % self._table_name 77 | self._execute(True, query, values=[item_id, now, _encode(item)]) 78 | self._close() 79 | 80 | def _optimize_item_count(self): 81 | pass 82 | 83 | def _optimize_file_size(self): 84 | pass 85 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from datetime import datetime 12 | import time 13 | 14 | from .impl import Context 15 | from ..youtube.provider import Provider 16 | from .utils import YouTubeMonitor 17 | from .utils import YouTubePlayer 18 | 19 | 20 | def strptime(stamp, stamp_fmt): 21 | # noinspection PyUnresolvedReferences 22 | import _strptime 23 | try: 24 | time.strptime('01 01 2012', '%d %m %Y') # dummy call 25 | except: 26 | pass 27 | return time.strptime(stamp, stamp_fmt) 28 | 29 | 30 | def get_stamp_diff(current_stamp): 31 | stamp_format = '%Y-%m-%d %H:%M:%S.%f' 32 | current_datetime = datetime.now() 33 | if not current_stamp: 34 | return 86400 # 24 hrs 35 | try: 36 | stamp_datetime = datetime(*(strptime(current_stamp, stamp_format)[0:6])) 37 | except ValueError: # current_stamp has no microseconds 38 | stamp_format = '%Y-%m-%d %H:%M:%S' 39 | stamp_datetime = datetime(*(strptime(current_stamp, stamp_format)[0:6])) 40 | 41 | time_delta = current_datetime - stamp_datetime 42 | total_seconds = 0 43 | if time_delta: 44 | total_seconds = ((time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) // (10 ** 6) 45 | return total_seconds 46 | 47 | 48 | def run(): 49 | sleep_time = 10 50 | ping_delay_time = 60 51 | ping_timestamp = None 52 | first_run = True 53 | 54 | context = Context(plugin_id='plugin.video.youtube') 55 | 56 | context.log_debug('YouTube service initialization...') 57 | 58 | monitor = YouTubeMonitor() 59 | player = YouTubePlayer(provider=Provider(), context=context) 60 | 61 | # wipe add-on temp folder on updates/restarts (subtitles, and mpd files) 62 | monitor.remove_temp_dir() 63 | 64 | # wipe function cache on updates/restarts (fix cipher related issues on update, valid for one day otherwise) 65 | try: 66 | context.get_function_cache().clear() 67 | except: 68 | # prevent service to failing due to cache related issues 69 | pass 70 | 71 | context.get_ui().clear_home_window_property('abort_requested') 72 | 73 | while not monitor.abortRequested(): 74 | 75 | ping_diff = get_stamp_diff(ping_timestamp) 76 | 77 | if (ping_timestamp is None) or (ping_diff >= ping_delay_time): 78 | ping_timestamp = str(datetime.now()) 79 | 80 | if monitor.httpd and not monitor.ping_httpd(): 81 | monitor.restart_httpd() 82 | 83 | if first_run: 84 | first_run = False 85 | 86 | if monitor.waitForAbort(sleep_time): 87 | break 88 | 89 | context.get_ui().set_home_window_property('abort_requested', 'true') 90 | 91 | player.cleanup_threads(only_ended=False) # clean up any/all playback monitoring threads 92 | 93 | if monitor.httpd: 94 | monitor.shutdown_httpd() # shutdown http server 95 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from ...kodion.items import UriItem 12 | from ... import kodion 13 | from ...youtube.helper import v3 14 | 15 | 16 | def _process_list(provider, context): 17 | result = [] 18 | 19 | page_token = context.get_param('page_token', '') 20 | # no caching 21 | json_data = provider.get_client(context).get_subscription('mine', page_token=page_token) 22 | if not v3.handle_error(provider, context, json_data): 23 | return [] 24 | result.extend(v3.response_to_items(provider, context, json_data)) 25 | 26 | return result 27 | 28 | 29 | def _process_add(provider, context): 30 | listitem_subscription_id = context.get_ui().get_info_label('Container.ListItem(0).Property(subscription_id)') 31 | 32 | subscription_id = context.get_param('subscription_id', '') 33 | if not subscription_id: 34 | if listitem_subscription_id and listitem_subscription_id.lower().startswith('uc'): 35 | subscription_id = listitem_subscription_id 36 | 37 | if subscription_id: 38 | json_data = provider.get_client(context).subscribe(subscription_id) 39 | if not v3.handle_error(provider, context, json_data): 40 | return False 41 | 42 | context.get_ui().show_notification( 43 | context.localize(provider.LOCAL_MAP['youtube.subscribed.to.channel']), 44 | time_milliseconds=2500, 45 | audible=False 46 | ) 47 | 48 | return True 49 | 50 | return False 51 | 52 | 53 | def _process_remove(provider, context): 54 | listitem_subscription_id = context.get_ui().get_info_label('Container.ListItem(0).Property(channel_subscription_id)') 55 | 56 | subscription_id = context.get_param('subscription_id', '') 57 | if not subscription_id and listitem_subscription_id: 58 | subscription_id = listitem_subscription_id 59 | 60 | if subscription_id: 61 | json_data = provider.get_client(context).unsubscribe(subscription_id) 62 | if not v3.handle_error(provider, context, json_data): 63 | return False 64 | 65 | context.get_ui().refresh_container() 66 | 67 | context.get_ui().show_notification( 68 | context.localize(provider.LOCAL_MAP['youtube.unsubscribed.from.channel']), 69 | time_milliseconds=2500, 70 | audible=False 71 | ) 72 | 73 | return True 74 | 75 | return False 76 | 77 | 78 | def process(method, provider, context): 79 | result = [] 80 | 81 | # we need a login 82 | _ = provider.get_client(context) 83 | if not provider.is_logged_in(): 84 | return UriItem(context.create_uri(['sign', 'in'])) 85 | 86 | if method == 'list': 87 | result.extend(_process_list(provider, context)) 88 | elif method == 'add': 89 | return _process_add(provider, context) 90 | elif method == 'remove': 91 | return _process_remove(provider, context) 92 | else: 93 | raise kodion.KodionException("Unknown subscriptions method '%s'" % method) 94 | 95 | return result 96 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/items/base_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six import python_2_unicode_compatible 12 | from six import string_types 13 | from six.moves import html_parser 14 | 15 | import hashlib 16 | import datetime 17 | 18 | 19 | @python_2_unicode_compatible 20 | class BaseItem(object): 21 | VERSION = 3 22 | INFO_DATE = 'date' # (string) iso 8601 23 | 24 | def __init__(self, name, uri, image=u'', fanart=u''): 25 | self._version = BaseItem.VERSION 26 | 27 | try: 28 | self._name = html_parser.HTMLParser().unescape(name) 29 | except html_parser.HTMLParseError as _: 30 | self._name = name 31 | 32 | self._uri = uri 33 | 34 | self._image = u'' 35 | self.set_image(image) 36 | 37 | self._fanart = fanart 38 | self._context_menu = None 39 | self._replace_context_menu = False 40 | self._date = None 41 | 42 | self._next_page = False 43 | 44 | def __str__(self): 45 | name = self._name 46 | uri = self._uri 47 | image = self._image 48 | obj_str = "------------------------------\n'%s'\nURI: %s\nImage: %s\n------------------------------" % (name, uri, image) 49 | return obj_str 50 | 51 | def get_id(self): 52 | """ 53 | Returns a unique id of the item. 54 | :return: unique id of the item. 55 | """ 56 | m = hashlib.md5() 57 | m.update(self._name.encode('utf-8')) 58 | m.update(self._uri.encode('utf-8')) 59 | return m.hexdigest() 60 | 61 | def get_name(self): 62 | """ 63 | Returns the name of the item. 64 | :return: name of the item. 65 | """ 66 | return self._name 67 | 68 | def set_uri(self, uri): 69 | if isinstance(uri, string_types): 70 | self._uri = uri 71 | else: 72 | self._uri = '' 73 | 74 | def get_uri(self): 75 | """ 76 | Returns the path of the item. 77 | :return: path of the item. 78 | """ 79 | return self._uri 80 | 81 | def set_image(self, image): 82 | if image is None: 83 | self._image = '' 84 | else: 85 | self._image = image 86 | 87 | def get_image(self): 88 | return self._image 89 | 90 | def set_fanart(self, fanart): 91 | self._fanart = fanart 92 | 93 | def get_fanart(self): 94 | return self._fanart 95 | 96 | def set_context_menu(self, context_menu, replace=False): 97 | self._context_menu = context_menu 98 | self._replace_context_menu = replace 99 | 100 | def get_context_menu(self): 101 | return self._context_menu 102 | 103 | def replace_context_menu(self): 104 | return self._replace_context_menu 105 | 106 | def set_date(self, year, month, day, hour=0, minute=0, second=0): 107 | date = datetime.datetime(year, month, day, hour, minute, second) 108 | self._date = date.isoformat(sep=' ') 109 | 110 | def set_date_from_datetime(self, date_time): 111 | self.set_date(year=date_time.year, month=date_time.month, day=date_time.day, hour=date_time.hour, 112 | minute=date_time.minute, second=date_time.second) 113 | 114 | def get_date(self): 115 | return self._date 116 | 117 | @property 118 | def next_page(self): 119 | return self._next_page 120 | 121 | @next_page.setter 122 | def next_page(self, value): 123 | self._next_page = bool(value) 124 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/function_cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2019 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from functools import partial 12 | import hashlib 13 | 14 | from .storage import Storage 15 | 16 | 17 | class FunctionCache(Storage): 18 | ONE_MINUTE = 60 19 | ONE_HOUR = 60 * ONE_MINUTE 20 | ONE_DAY = 24 * ONE_HOUR 21 | ONE_WEEK = 7 * ONE_DAY 22 | ONE_MONTH = 4 * ONE_WEEK 23 | 24 | def __init__(self, filename, max_file_size_mb=5): 25 | max_file_size_kb = max_file_size_mb * 1024 26 | Storage.__init__(self, filename, max_file_size_kb=max_file_size_kb) 27 | 28 | self._enabled = True 29 | 30 | def clear(self): 31 | self._clear() 32 | 33 | def enabled(self): 34 | """ 35 | Enables the caching 36 | :return: 37 | """ 38 | self._enabled = True 39 | 40 | def disable(self): 41 | """ 42 | Disable caching e.g. for tests 43 | :return: 44 | """ 45 | self._enabled = False 46 | 47 | @staticmethod 48 | def _create_id_from_func(partial_func): 49 | """ 50 | Creats an id from the given function 51 | :param partial_func: 52 | :return: id for the given function 53 | """ 54 | m = hashlib.md5() 55 | m.update(partial_func.func.__module__.encode('utf-8')) 56 | m.update(partial_func.func.__name__.encode('utf-8')) 57 | m.update(str(partial_func.args).encode('utf-8')) 58 | m.update(str(partial_func.keywords).encode('utf-8')) 59 | return m.hexdigest() 60 | 61 | def _get_cached_data(self, partial_func): 62 | cache_id = self._create_id_from_func(partial_func) 63 | return self._get(cache_id), cache_id 64 | 65 | def get_cached_only(self, func, *args, **keywords): 66 | partial_func = partial(func, *args, **keywords) 67 | 68 | # if caching is disabled call the function 69 | if not self._enabled: 70 | return partial_func() 71 | 72 | # only return before cached data 73 | data, cache_id = self._get_cached_data(partial_func) 74 | if data is not None: 75 | return data[0] 76 | 77 | return None 78 | 79 | def get(self, seconds, func, *args, **keywords): 80 | """ 81 | Returns the cached data of the given function. 82 | :param partial_func: function to cache 83 | :param seconds: time to live in seconds 84 | :param return_cached_only: return only cached data and don't call the function 85 | :return: 86 | """ 87 | 88 | partial_func = partial(func, *args, **keywords) 89 | 90 | # if caching is disabled call the function 91 | if not self._enabled: 92 | return partial_func() 93 | 94 | cached_data = None 95 | cached_time = None 96 | data, cache_id = self._get_cached_data(partial_func) 97 | if data is not None: 98 | cached_data = data[0] 99 | cached_time = data[1] 100 | 101 | diff_seconds = 0 102 | 103 | if cached_time is not None: 104 | # this is so stupid, but we have the function 'total_seconds' only starting with python 2.7 105 | diff_seconds = self.get_seconds_diff(cached_time) 106 | 107 | if cached_data is None or diff_seconds > seconds: 108 | cached_data = partial_func() 109 | self._set(cache_id, cached_data) 110 | 111 | return cached_data 112 | 113 | def _optimize_item_count(self): 114 | # override method from resources/lib/youtube_plugin/kodion/utils/storage.py 115 | # for function cache do not optimize by item count, using database size. 116 | pass 117 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/json_store/login_tokens.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | import uuid 11 | from . import JSONStore 12 | 13 | 14 | # noinspection PyTypeChecker 15 | class LoginTokenStore(JSONStore): 16 | def __init__(self): 17 | JSONStore.__init__(self, 'access_manager.json') 18 | 19 | def set_defaults(self): 20 | data = self.get_data() 21 | if 'access_manager' not in data: 22 | data = {'access_manager': {'users': {'0': {'access_token': '', 'refresh_token': '', 'token_expires': -1, 23 | 'last_key_hash': '', 'name': 'Default', 'watch_later': ' WL', 'watch_history': 'HL'}}}} 24 | if 'users' not in data['access_manager']: 25 | data['access_manager']['users'] = {'0': {'access_token': '', 'refresh_token': '', 'token_expires': -1, 26 | 'last_key_hash': '', 'name': 'Default', 'watch_later': ' WL', 'watch_history': 'HL'}} 27 | if '0' not in data['access_manager']['users']: 28 | data['access_manager']['users']['0'] = {'access_token': '', 'refresh_token': '', 'token_expires': -1, 29 | 'last_key_hash': '', 'name': 'Default', 'watch_later': ' WL', 'watch_history': 'HL'} 30 | if 'current_user' not in data['access_manager']: 31 | data['access_manager']['current_user'] = '0' 32 | if 'last_origin' not in data['access_manager']: 33 | data['access_manager']['last_origin'] = 'plugin.video.youtube' 34 | if 'developers' not in data['access_manager']: 35 | data['access_manager']['developers'] = dict() 36 | 37 | # clean up 38 | if data['access_manager']['current_user'] == 'default': 39 | data['access_manager']['current_user'] = '0' 40 | if 'access_token' in data['access_manager']: 41 | del data['access_manager']['access_token'] 42 | if 'refresh_token' in data['access_manager']: 43 | del data['access_manager']['refresh_token'] 44 | if 'token_expires' in data['access_manager']: 45 | del data['access_manager']['token_expires'] 46 | if 'default' in data['access_manager']: 47 | if (data['access_manager']['default'].get('access_token') or 48 | data['access_manager']['default'].get('refresh_token')) and \ 49 | (not data['access_manager']['users']['0'].get('access_token') and 50 | not data['access_manager']['users']['0'].get('refresh_token')): 51 | if 'name' not in data['access_manager']['default']: 52 | data['access_manager']['default']['name'] = 'Default' 53 | data['access_manager']['users']['0'] = data['access_manager']['default'] 54 | del data['access_manager']['default'] 55 | # end clean up 56 | 57 | current_user = data['access_manager']['current_user'] 58 | if 'watch_later' not in data['access_manager']['users'][current_user]: 59 | data['access_manager']['users'][current_user]['watch_later'] = ' WL' 60 | if 'watch_history' not in data['access_manager']['users'][current_user]: 61 | data['access_manager']['users'][current_user]['watch_history'] = 'HL' 62 | 63 | # ensure all users have uuid 64 | uuids = list() 65 | uuid_update = False 66 | for k in list(data['access_manager']['users'].keys()): 67 | c_uuid = data['access_manager']['users'][k].get('id') 68 | if c_uuid: 69 | uuids.append(c_uuid) 70 | else: 71 | if not uuid_update: 72 | uuid_update = True 73 | 74 | if uuid_update: 75 | for k in list(data['access_manager']['users'].keys()): 76 | c_uuid = data['access_manager']['users'][k].get('id') 77 | if not c_uuid: 78 | g_uuid = uuid.uuid4().hex 79 | while g_uuid in uuids: 80 | g_uuid = uuid.uuid4().hex 81 | data['access_manager']['users'][k]['id'] = g_uuid 82 | # end uuid check 83 | 84 | self.save(data) 85 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/data_cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2019 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six import PY2 12 | # noinspection PyPep8Naming 13 | from six.moves import cPickle as pickle 14 | 15 | import json 16 | import sqlite3 17 | 18 | from datetime import datetime, timedelta 19 | 20 | from .storage import Storage 21 | from .. import logger 22 | 23 | 24 | class DataCache(Storage): 25 | ONE_MINUTE = 60 26 | ONE_HOUR = 60 * ONE_MINUTE 27 | ONE_DAY = 24 * ONE_HOUR 28 | ONE_WEEK = 7 * ONE_DAY 29 | ONE_MONTH = 4 * ONE_WEEK 30 | 31 | def __init__(self, filename, max_file_size_mb=5): 32 | max_file_size_kb = max_file_size_mb * 1024 33 | Storage.__init__(self, filename, max_file_size_kb=max_file_size_kb) 34 | 35 | def is_empty(self): 36 | return self._is_empty() 37 | 38 | def get_items(self, seconds, content_ids): 39 | def _decode(obj): 40 | if PY2: 41 | obj = str(obj) 42 | return pickle.loads(obj) 43 | 44 | current_time = datetime.now() 45 | placeholders = ','.join(['?' for _ in content_ids]) 46 | keys = [str(item) for item in content_ids] 47 | query = 'SELECT * FROM %s WHERE key IN (%s)' % (self._table_name, placeholders) 48 | 49 | self._open() 50 | 51 | query_result = self._execute(False, query, keys) 52 | result = {} 53 | if query_result: 54 | for item in query_result: 55 | cached_time = item[1] 56 | if cached_time is None: 57 | logger.log_error('Data Cache [get_items]: cached_time is None while getting {content_id}'.format(content_id=str(item[0]))) 58 | cached_time = current_time 59 | # this is so stupid, but we have the function 'total_seconds' only starting with python 2.7 60 | diff_seconds = self.get_seconds_diff(cached_time) 61 | if diff_seconds <= seconds: 62 | result[str(item[0])] = json.loads(_decode(item[2])) 63 | 64 | self._close() 65 | return result 66 | 67 | def get_item(self, seconds, content_id): 68 | content_id = str(content_id) 69 | query_result = self._get(content_id) 70 | result = {} 71 | if query_result: 72 | current_time = datetime.now() 73 | cached_time = query_result[1] 74 | if cached_time is None: 75 | logger.log_error('Data Cache [get]: cached_time is None while getting {content_id}'.format(content_id=content_id)) 76 | cached_time = current_time 77 | # this is so stupid, but we have the function 'total_seconds' only starting with python 2.7 78 | diff_seconds = self.get_seconds_diff(cached_time) 79 | if diff_seconds <= seconds: 80 | result[content_id] = json.loads(query_result[0]) 81 | 82 | return result 83 | 84 | def set(self, content_id, item): 85 | self._set(content_id, item) 86 | 87 | def set_all(self, items): 88 | self._set_all(items) 89 | 90 | def clear(self): 91 | self._clear() 92 | 93 | def remove(self, content_id): 94 | self._remove(content_id) 95 | 96 | def update(self, content_id, item): 97 | self._set(str(content_id), json.dumps(item)) 98 | 99 | def _optimize_item_count(self): 100 | pass 101 | 102 | def _set(self, content_id, item): 103 | def _encode(obj): 104 | return sqlite3.Binary(pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)) 105 | 106 | current_time = datetime.now() + timedelta(microseconds=1) 107 | query = 'REPLACE INTO %s (key,time,value) VALUES(?,?,?)' % self._table_name 108 | 109 | self._open() 110 | self._execute(True, query, values=[content_id, current_time, _encode(item)]) 111 | self._close() 112 | 113 | def _set_all(self, items): 114 | def _encode(obj): 115 | return sqlite3.Binary(pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)) 116 | 117 | needs_commit = True 118 | current_time = datetime.now() + timedelta(microseconds=1) 119 | 120 | query = 'REPLACE INTO %s (key,time,value) VALUES(?,?,?)' % self._table_name 121 | 122 | self._open() 123 | 124 | for key in list(items.keys()): 125 | item = items[key] 126 | self._execute(needs_commit, query, values=[key, current_time, _encode(json.dumps(item))]) 127 | needs_commit = False 128 | 129 | self._close() 130 | -------------------------------------------------------------------------------- /resources/lib/youtube_resolver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2017-2019 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | from six import string_types 11 | import re 12 | import json 13 | import requests 14 | 15 | from youtube_plugin.youtube.provider import Provider 16 | from youtube_plugin.kodion.impl import Context 17 | 18 | 19 | def _get_core_components(addon_id=None): 20 | provider = Provider() 21 | if addon_id is not None: 22 | context = Context(params={'addon_id': addon_id}, plugin_id='plugin.video.youtube') 23 | else: 24 | context = Context(plugin_id='plugin.video.youtube') 25 | client = provider.get_client(context=context) 26 | 27 | return provider, context, client 28 | 29 | 30 | def _get_config_and_cookies(client, url): 31 | headers = {'Host': 'www.youtube.com', 32 | 'Connection': 'keep-alive', 33 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36', 34 | 'Accept': '*/*', 35 | 'DNT': '1', 36 | 'Referer': 'https://www.youtube.com', 37 | 'Accept-Encoding': 'gzip, deflate', 38 | 'Accept-Language': 'en-US,en;q=0.8,de;q=0.6'} 39 | 40 | params = {'hl': client.get_language(), 41 | 'gl': client.get_region()} 42 | 43 | if client.get_access_token(): 44 | params['access_token'] = client.get_access_token() 45 | 46 | result = requests.get(url, params=params, headers=headers, verify=client.verify(), allow_redirects=True) 47 | html = result.text 48 | cookies = result.cookies 49 | 50 | _player_config = '{}' 51 | lead = 'ytplayer.config = ' 52 | tail = ';ytplayer.load' 53 | pos = html.find(lead) 54 | if pos >= 0: 55 | html2 = html[pos + len(lead):] 56 | pos = html2.find(tail) 57 | if pos >= 0: 58 | _player_config = html2[:pos] 59 | 60 | blank_config = re.search(r'var blankSwfConfig\s*=\s*(?P{.+?});\s*var fillerData', html) 61 | if not blank_config: 62 | player_config = dict() 63 | else: 64 | try: 65 | player_config = json.loads(blank_config.group('player_config')) 66 | except TypeError: 67 | player_config = dict() 68 | 69 | try: 70 | player_config.update(json.loads(_player_config)) 71 | except TypeError: 72 | pass 73 | 74 | if 'args' not in player_config: 75 | player_config['args'] = dict() 76 | 77 | player_response = player_config['args'].get('player_response', dict()) 78 | if isinstance(player_response, string_types): 79 | try: 80 | player_response = json.loads(player_response) 81 | except TypeError: 82 | player_response = dict() 83 | 84 | player_config['args']['player_response'] = dict() 85 | 86 | result = re.search(r'window\["ytInitialPlayerResponse"\]\s*=\s*\(\s*(?P{.+?})\s*\);', html) 87 | if result: 88 | try: 89 | player_config['args']['player_response'] = json.loads(result.group('player_response')) 90 | except TypeError: 91 | pass 92 | 93 | player_config['args']['player_response'].update(player_response) 94 | 95 | return {'config': player_config, 'cookies': cookies} 96 | 97 | 98 | def resolve(video_id, sort=True, addon_id=None): 99 | """ 100 | 101 | :param video_id: video id / video url 102 | :param sort: sort results by quality highest->lowest 103 | :param addon_id: addon id associated with developer keys to use for requests 104 | :type video_id: str 105 | :type sort: bool 106 | :type addon_id: str 107 | :return: all video items (resolved urls and metadata) for the given video id 108 | :rtype: list of dict 109 | """ 110 | provider, context, client = _get_core_components(addon_id) 111 | matched_id = None 112 | streams = None 113 | 114 | patterns = [r'(?P[\w-]{11})', 115 | r'(?:http)*s*:*[/]{0,2}(?:w{3}\.|m\.)*youtu(?:\.be/|be\.com/' 116 | r'(?:embed/|watch/|v/|.*?[?&/]v=))(?P[\w-]{11}).*'] 117 | 118 | for pattern in patterns: 119 | v_id = re.search(pattern, video_id) 120 | if v_id: 121 | matched_id = v_id.group('video_id') 122 | break 123 | 124 | if matched_id: 125 | streams = client.get_video_streams(context=context, video_id=matched_id) 126 | 127 | if streams is None: 128 | result = _get_config_and_cookies(client, matched_id) 129 | player_config = result.get('config') 130 | cookies = result.get('cookies') 131 | streams = client.get_video_streams(context=context, player_config=player_config, cookies=cookies) 132 | 133 | if sort and streams: 134 | streams = sorted(streams, key=lambda x: x.get('sort', 0), reverse=True) 135 | 136 | return streams 137 | -------------------------------------------------------------------------------- /addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | video 11 | 12 | 13 | 14 | 15 | 16 | [fix] next page now ignores sorting, remains at the end of the directory 17 | [fix] searching, no longer require remote safe search 18 | [fix] notifications for some languages 19 | [chg] only log http server ping failures 20 | 21 | 22 | icon.png 23 | fanart.jpg 24 | 25 | Plugin for YouTube 26 | YouTube is one of the biggest video-sharing websites of the world. 27 | This plugin is not endorsed by Google 28 | תוסף עבור YouTube 29 | YouTube הוא אחד מאתרי שיתוף הווידאו הגדולים בעולם. 30 | Plugin für YouTube 31 | YouTube ist eines der größten Video-Sharing-Websites der Welt. 32 | Wtyczka YouTube 33 | YouTube jest jednym z największych na świecie serwisów udostępniania wideo. 34 | YouTube kiegészítő 35 | A YouTube világ egyik legnagyobb videómegosztó weboldala. 36 | Plugin para YouTube 37 | YouTube es uno de los sitios web más grande del mundo para compartir vídeos. 38 | Este add-on no está respaldado por Google 39 | Plugin para YouTube 40 | YouTube es uno de los más grandes sitios web de intercambio de videos del mundo. 41 | Este add-on no está respaldado por Google 42 | Видеодополнение YouTube 43 | YouTube - популярнейший видеохостинговый сайт, предоставляющий пользователям услуги хранения, доставки и показа видео. 44 | Plugin pour YouTube 45 | YouTube est l'un des plus grands sites de partage vidéos du monde. 46 | YouTube附加元件 47 | 「Youtube」是全世界最大的影片分享網站 48 | YouTube附加元件 49 | 「Youtube」是全世界最大的影片分享網站 50 | 此附加元件未由Google支持 51 | Добавка за YouTube 52 | YouTube е един от най-големите уеб сайтове за споделяне на видео в целия свят. 53 | Πρόσθετο YouTube 54 | Το YouTube είναι μία από τις μεγαλύτερες ιστοσελίδες διαμοιρασμού βίντεο στον κόσμο. 55 | Η παρούσα μικροεφαρμογή δεν έχει υϊοθετηθεί από την Google 56 | Tillegg for YouTube 57 | YouTube er en av verdens største nettsider for videodeling. 58 | 유튜브 플러그인 59 | 유튜브는 세상에서 가장 큰 동영상 공유 사이트 중의 하나입니다. 60 | Plugin pro YouTube 61 | YouTube je jedna z největších webových stránek světa sdílející video. 62 | Tento plugin není schválen společností Google 63 | YouTube için eklenti 64 | YouTube, dünya üzerindeki en büyük video paylaşma platformlarından birisidir 65 | Bu eklenti Google tarafından üretilmemiştir 66 | all 67 | GPL-2.0-only 68 | https://ytaddon.page.link/forum 69 | https://www.youtube.com 70 | ytplugin at datanet dot ws 71 | https://github.com/jdf76/plugin.video.youtube 72 | true 73 | 74 | 75 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/yt_video.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from ... import kodion 12 | from ...youtube.helper import v3 13 | 14 | 15 | def _process_rate_video(provider, context, re_match): 16 | listitem_path = context.get_ui().get_info_label('Container.ListItem(0).FileNameAndPath') 17 | ratings = ['like', 'dislike', 'none'] 18 | 19 | rating_param = context.get_param('rating', '') 20 | if rating_param: 21 | rating_param = rating_param.lower() if rating_param.lower() in ratings else '' 22 | 23 | video_id = context.get_param('video_id', '') 24 | if not video_id: 25 | try: 26 | video_id = re_match.group('video_id') 27 | except IndexError: 28 | if context.is_plugin_path(listitem_path, 'play'): 29 | video_id = kodion.utils.find_video_id(listitem_path) 30 | 31 | if not video_id: 32 | raise kodion.KodionException('video/rate/: missing video_id') 33 | 34 | try: 35 | current_rating = re_match.group('rating') 36 | except IndexError: 37 | current_rating = None 38 | 39 | if not current_rating: 40 | client = provider.get_client(context) 41 | json_data = client.get_video_rating(video_id) 42 | if not v3.handle_error(provider, context, json_data): 43 | return False 44 | 45 | items = json_data.get('items', []) 46 | if items: 47 | current_rating = items[0].get('rating', '') 48 | 49 | rating_items = [] 50 | if not rating_param: 51 | for rating in ratings: 52 | if rating != current_rating: 53 | rating_items.append((context.localize(provider.LOCAL_MAP['youtube.video.rate.%s' % rating]), rating)) 54 | result = context.get_ui().on_select(context.localize(provider.LOCAL_MAP['youtube.video.rate']), rating_items) 55 | else: 56 | if rating_param != current_rating: 57 | result = rating_param 58 | else: 59 | result = -1 60 | 61 | if result != -1: 62 | notify_message = '' 63 | 64 | response = provider.get_client(context).rate_video(video_id, result) 65 | 66 | if response.get('status_code') != 204: 67 | notify_message = context.localize(provider.LOCAL_MAP['youtube.failed']) 68 | 69 | elif response.get('status_code') == 204: 70 | # this will be set if we are in the 'Liked Video' playlist 71 | if context.get_param('refresh_container', '0') == '1': 72 | context.get_ui().refresh_container() 73 | 74 | if result == 'none': 75 | notify_message = context.localize(provider.LOCAL_MAP['youtube.unrated.video']) 76 | elif result == 'like': 77 | notify_message = context.localize(provider.LOCAL_MAP['youtube.liked.video']) 78 | elif result == 'dislike': 79 | notify_message = context.localize(provider.LOCAL_MAP['youtube.disliked.video']) 80 | 81 | if notify_message: 82 | context.get_ui().show_notification( 83 | message=notify_message, 84 | time_milliseconds=2500, 85 | audible=False 86 | ) 87 | 88 | 89 | def _process_more_for_video(provider, context): 90 | video_id = context.get_param('video_id', '') 91 | if not video_id: 92 | raise kodion.KodionException('video/more/: missing video_id') 93 | 94 | items = [] 95 | 96 | is_logged_in = context.get_param('logged_in', '0') 97 | if is_logged_in == '1': 98 | # add video to a playlist 99 | items.append((context.localize(provider.LOCAL_MAP['youtube.video.add_to_playlist']), 100 | 'RunPlugin(%s)' % context.create_uri(['playlist', 'select', 'playlist'], {'video_id': video_id}))) 101 | 102 | 103 | # default items 104 | items.extend([(context.localize(provider.LOCAL_MAP['youtube.related_videos']), 105 | 'Container.Update(%s)' % context.create_uri(['special', 'related_videos'], {'video_id': video_id})), 106 | (context.localize(provider.LOCAL_MAP['youtube.video.comments']), 107 | 'Container.Update(%s)' % context.create_uri(['special', 'parent_comments'], {'video_id': video_id})), 108 | (context.localize(provider.LOCAL_MAP['youtube.video.description.links']), 109 | 'Container.Update(%s)' % context.create_uri(['special', 'description_links'], 110 | {'video_id': video_id}))]) 111 | 112 | if is_logged_in == '1': 113 | # rate a video 114 | refresh_container = context.get_param('refresh_container', '0') 115 | items.append((context.localize(provider.LOCAL_MAP['youtube.video.rate']), 116 | 'RunPlugin(%s)' % context.create_uri(['video', 'rate'], {'video_id': video_id, 117 | 'refresh_container': refresh_container}))) 118 | 119 | result = context.get_ui().on_select(context.localize(provider.LOCAL_MAP['youtube.video.more']), items) 120 | if result != -1: 121 | context.execute(result) 122 | 123 | 124 | def process(method, provider, context, re_match): 125 | if method == 'rate': 126 | return _process_rate_video(provider, context, re_match) 127 | elif method == 'more': 128 | return _process_more_for_video(provider, context) 129 | else: 130 | raise kodion.KodionException("Unknown method '%s'" % method) 131 | -------------------------------------------------------------------------------- /resources/lib/youtube_authentication.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | from youtube_plugin.youtube.provider import Provider 11 | from youtube_plugin.kodion.impl import Context 12 | from youtube_plugin.youtube.helper import yt_login 13 | 14 | # noinspection PyUnresolvedReferences 15 | from youtube_plugin.youtube.youtube_exceptions import LoginException # NOQA 16 | 17 | 18 | SIGN_IN = 'in' 19 | SIGN_OUT = 'out' 20 | 21 | 22 | def __add_new_developer(addon_id): 23 | """ 24 | 25 | :param addon_id: id of the add-on being added 26 | :return: 27 | """ 28 | params = {'addon_id': addon_id} 29 | context = Context(params=params, plugin_id='plugin.video.youtube') 30 | 31 | access_manager = context.get_access_manager() 32 | developers = access_manager.get_developers() 33 | if not developers.get(addon_id, None): 34 | developers[addon_id] = access_manager.get_new_developer() 35 | access_manager.set_developers(developers) 36 | context.log_debug('Creating developer user: |%s|' % addon_id) 37 | 38 | 39 | def __auth(addon_id, mode=SIGN_IN): 40 | """ 41 | 42 | :param addon_id: id of the add-on being signed in 43 | :param mode: SIGN_IN or SIGN_OUT 44 | :return: addon provider, context and client 45 | """ 46 | if not addon_id or addon_id == 'plugin.video.youtube': 47 | context = Context(plugin_id='plugin.video.youtube') 48 | context.log_error('Developer authentication: |%s| Invalid addon_id' % addon_id) 49 | return 50 | __add_new_developer(addon_id) 51 | params = {'addon_id': addon_id} 52 | provider = Provider() 53 | context = Context(params=params, plugin_id='plugin.video.youtube') 54 | 55 | _ = provider.get_client(context=context) # NOQA 56 | logged_in = provider.is_logged_in() 57 | if mode == SIGN_IN: 58 | if logged_in: 59 | return True 60 | else: 61 | provider.reset_client() 62 | yt_login.process(mode, provider, context, sign_out_refresh=False) 63 | elif mode == SIGN_OUT: 64 | if not logged_in: 65 | return True 66 | else: 67 | provider.reset_client() 68 | try: 69 | yt_login.process(mode, provider, context, sign_out_refresh=False) 70 | except: 71 | reset_access_tokens(addon_id) 72 | else: 73 | raise Exception('Unknown mode: |%s|' % mode) 74 | 75 | _ = provider.get_client(context=context) # NOQA 76 | if mode == SIGN_IN: 77 | return provider.is_logged_in() 78 | else: 79 | return not provider.is_logged_in() 80 | 81 | 82 | def sign_in(addon_id): 83 | """ 84 | To use the signed in context, see youtube_registration.py and youtube_requests.py 85 | Usage: 86 | 87 | addon.xml 88 | --- 89 | 90 | --- 91 | 92 | .py 93 | --- 94 | import youtube_registration 95 | import youtube_authentication 96 | 97 | youtube_registration.register_api_keys(addon_id='plugin.video.example', 98 | api_key='A1zaSyA0b5sTjgxzTzYLmVtradlFVBfSHNOJKS0', 99 | client_id='825419953561-ert5tccq1r0upsuqdf5nm3le39czk23a.apps.googleusercontent.com', 100 | client_secret='Y5cE1IKzJQe1NZ0OsOoEqpu3') 101 | 102 | try: 103 | signed_in = youtube_authentication.sign_in(addon_id='plugin.video.example') # refreshes access tokens if already signed in 104 | except youtube_authentication.LoginException as e: 105 | error_message = e.get_message() 106 | # handle error 107 | signed_in = False 108 | 109 | if signed_in: 110 | pass # see youtube_registration.py and youtube_requests.py to use the signed in context 111 | --- 112 | 113 | :param addon_id: id of the add-on being signed in 114 | :return: boolean, True when signed in 115 | """ 116 | 117 | return __auth(addon_id, mode=SIGN_IN) 118 | 119 | 120 | def sign_out(addon_id): 121 | """ 122 | Usage: 123 | 124 | addon.xml 125 | --- 126 | 127 | --- 128 | 129 | .py 130 | --- 131 | import youtube_registration 132 | import youtube_authentication 133 | 134 | youtube_registration.register_api_keys(addon_id='plugin.video.example', 135 | api_key='A1zaSyA0b5sTjgxzTzYLmVtradlFVBfSHNOJKS0', 136 | client_id='825419953561-ert5tccq1r0upsuqdf5nm3le39czk23a.apps.googleusercontent.com', 137 | client_secret='Y5cE1IKzJQe1NZ0OsOoEqpu3') 138 | 139 | signed_out = youtube_authentication.sign_out(addon_id='plugin.video.example') 140 | if signed_out: 141 | pass 142 | --- 143 | 144 | :param addon_id: id of the add-on being signed out 145 | :return: boolean, True when signed out 146 | """ 147 | 148 | return __auth(addon_id, mode=SIGN_OUT) 149 | 150 | 151 | def reset_access_tokens(addon_id): 152 | """ 153 | 154 | :param addon_id: id of the add-on having it's access tokens reset 155 | :return: 156 | """ 157 | if not addon_id or addon_id == 'plugin.video.youtube': 158 | context = Context(plugin_id='plugin.video.youtube') 159 | context.log_error('Developer reset access tokens: |%s| Invalid addon_id' % addon_id) 160 | return 161 | params = {'addon_id': addon_id} 162 | context = Context(params=params, plugin_id='plugin.video.youtube') 163 | 164 | access_manager = context.get_access_manager() 165 | access_manager.update_dev_access_token(addon_id, access_token='', refresh_token='') 166 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from ... import utils 12 | from ...items import * 13 | 14 | 15 | def _process_date(info_labels, param): 16 | if param: 17 | datetime = utils.datetime_parser.parse(param) 18 | datetime = '%02d.%02d.%04d' % (datetime.day, datetime.month, datetime.year) 19 | info_labels['date'] = datetime 20 | 21 | 22 | def _process_int_value(info_labels, name, param): 23 | if param is not None: 24 | info_labels[name] = int(param) 25 | 26 | 27 | def _process_string_value(info_labels, name, param): 28 | if param is not None: 29 | info_labels[name] = param 30 | 31 | 32 | def _process_audio_rating(info_labels, param): 33 | if param is not None: 34 | rating = int(param) 35 | if rating > 5: 36 | rating = 5 37 | if rating < 0: 38 | rating = 0 39 | 40 | info_labels['rating'] = rating 41 | 42 | 43 | def _process_video_dateadded(info_labels, param): 44 | if param is not None and param: 45 | info_labels['dateadded'] = param 46 | 47 | 48 | def _process_video_duration(info_labels, param): 49 | if param is not None: 50 | info_labels['duration'] = '%d' % param 51 | 52 | 53 | def _process_video_rating(info_labels, param): 54 | if param is not None: 55 | rating = float(param) 56 | if rating > 10.0: 57 | rating = 10.0 58 | if rating < 0.0: 59 | rating = 0.0 60 | info_labels['rating'] = rating 61 | 62 | 63 | def _process_date_value(info_labels, name, param): 64 | if param: 65 | date = utils.datetime_parser.parse(param) 66 | date = '%04d-%02d-%02d' % (date.year, date.month, date.day) 67 | info_labels[name] = date 68 | 69 | 70 | def _process_list_value(info_labels, name, param): 71 | if param is not None and isinstance(param, list): 72 | info_labels[name] = param 73 | 74 | 75 | def _process_mediatype(info_labels, name, param): 76 | info_labels[name] = param 77 | 78 | 79 | def _process_last_played(info_labels, name, param): 80 | if param: 81 | try: 82 | info_labels[name] = param.strftime('%Y-%m-%d %H:%M:%S') 83 | except AttributeError: 84 | info_labels[name] = param 85 | 86 | 87 | def create_from_item(base_item): 88 | info_labels = {} 89 | 90 | # 'date' = '09.03.1982' 91 | _process_date(info_labels, base_item.get_date()) 92 | 93 | # Directory 94 | if isinstance(base_item, DirectoryItem): 95 | _process_string_value(info_labels, 'plot', base_item.get_plot()) 96 | 97 | # Image 98 | if isinstance(base_item, ImageItem): 99 | # 'title' = 'Blow Your Head Off' (string) 100 | _process_string_value(info_labels, 'title', base_item.get_title()) 101 | 102 | # Audio 103 | if isinstance(base_item, AudioItem): 104 | # 'duration' = 79 (int) 105 | _process_int_value(info_labels, 'duration', base_item.get_duration()) 106 | 107 | # 'album' = 'Buckle Up' (string) 108 | _process_string_value(info_labels, 'album', base_item.get_album_name()) 109 | 110 | # 'artist' = 'Angerfist' (string) 111 | _process_string_value(info_labels, 'artist', base_item.get_artist_name()) 112 | 113 | # 'rating' = '0' - '5' (string) 114 | _process_audio_rating(info_labels, base_item.get_rating()) 115 | 116 | # Video 117 | if isinstance(base_item, VideoItem): 118 | # mediatype 119 | _process_mediatype(info_labels, 'mediatype', base_item.get_mediatype()) 120 | 121 | # play count 122 | _process_int_value(info_labels, 'playcount', base_item.get_play_count()) 123 | 124 | # studio 125 | _process_string_value(info_labels, 'studio', base_item.get_studio()) 126 | 127 | # 'artist' = [] (list) 128 | _process_list_value(info_labels, 'artist', base_item.get_artist()) 129 | 130 | # 'dateadded' = '2014-08-11 13:08:56' (string) will be taken from 'date' 131 | _process_video_dateadded(info_labels, base_item.get_date()) 132 | 133 | # TODO: starting with Helix this could be seconds 134 | # 'duration' = '3:18' (string) 135 | _process_video_duration(info_labels, base_item.get_duration()) 136 | 137 | _process_last_played(info_labels, 'lastplayed', base_item.get_last_played()) 138 | 139 | # 'rating' = 4.5 (float) 140 | _process_video_rating(info_labels, base_item.get_rating()) 141 | 142 | # 'aired' = '2013-12-12' (string) 143 | _process_date_value(info_labels, 'aired', base_item.get_aired()) 144 | 145 | # 'director' = 'Steven Spielberg' (string) 146 | _process_string_value(info_labels, 'director', base_item.get_director()) 147 | 148 | # 'premiered' = '2013-12-12' (string) 149 | _process_date_value(info_labels, 'premiered', base_item.get_premiered()) 150 | 151 | # 'episode' = 12 (int) 152 | _process_int_value(info_labels, 'episode', base_item.get_episode()) 153 | 154 | # 'season' = 12 (int) 155 | _process_int_value(info_labels, 'season', base_item.get_season()) 156 | 157 | # 'plot' = '...' (string) 158 | _process_string_value(info_labels, 'plot', base_item.get_plot()) 159 | 160 | # 'code' = 'tt3458353' (string) - imdb id 161 | _process_string_value(info_labels, 'code', base_item.get_imdb_id()) 162 | 163 | # 'cast' = [] (list) 164 | _process_list_value(info_labels, 'cast', base_item.get_cast()) 165 | 166 | # Audio and Video 167 | if isinstance(base_item, AudioItem) or isinstance(base_item, VideoItem): 168 | # 'title' = 'Blow Your Head Off' (string) 169 | _process_string_value(info_labels, 'title', base_item.get_title()) 170 | 171 | # 'tracknumber' = 12 (int) 172 | _process_int_value(info_labels, 'tracknumber', base_item.get_track_number()) 173 | 174 | # 'year' = 1994 (int) 175 | _process_int_value(info_labels, 'year', base_item.get_year()) 176 | 177 | # 'genre' = 'Hardcore' (string) 178 | _process_string_value(info_labels, 'genre', base_item.get_genre()) 179 | 180 | return info_labels 181 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/monitor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2018-2018 plugin.video.youtube 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | See LICENSES/GPL-2.0-only for more information. 8 | """ 9 | 10 | from six.moves.urllib.parse import unquote 11 | 12 | import json 13 | import os 14 | import shutil 15 | import threading 16 | 17 | import xbmc 18 | import xbmcaddon 19 | import xbmcvfs 20 | 21 | from ..utils import get_http_server, is_httpd_live 22 | from .. import logger 23 | 24 | 25 | class YouTubeMonitor(xbmc.Monitor): 26 | 27 | # noinspection PyUnusedLocal,PyMissingConstructor 28 | def __init__(self, *args, **kwargs): 29 | self.addon_id = 'plugin.video.youtube' 30 | addon = xbmcaddon.Addon(self.addon_id) 31 | self._whitelist = addon.getSetting('kodion.http.ip.whitelist') 32 | self._httpd_port = int(addon.getSetting('kodion.mpd.proxy.port')) 33 | self._old_httpd_port = self._httpd_port 34 | self._use_httpd = (addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.mpd') == 'true') or \ 35 | (addon.getSetting('youtube.api.config.page') == 'true') 36 | self._httpd_address = addon.getSetting('kodion.http.listen') 37 | self._old_httpd_address = self._httpd_address 38 | self.httpd = None 39 | self.httpd_thread = None 40 | if self.use_httpd(): 41 | self.start_httpd() 42 | del addon 43 | 44 | def onNotification(self, sender, method, data): 45 | if sender == 'plugin.video.youtube' and method.endswith('.check_settings'): 46 | data = json.loads(data) 47 | data = json.loads(unquote(data[0])) 48 | logger.log_debug('onNotification: |check_settings| -> |%s|' % json.dumps(data)) 49 | 50 | _use_httpd = data.get('use_httpd') 51 | _httpd_port = data.get('httpd_port') 52 | _whitelist = data.get('whitelist') 53 | _httpd_address = data.get('httpd_address') 54 | 55 | whitelist_changed = _whitelist != self._whitelist 56 | port_changed = self._httpd_port != _httpd_port 57 | address_changed = self._httpd_address != _httpd_address 58 | 59 | if _whitelist != self._whitelist: 60 | self._whitelist = _whitelist 61 | 62 | if self._use_httpd != _use_httpd: 63 | self._use_httpd = _use_httpd 64 | 65 | if self._httpd_port != _httpd_port: 66 | self._old_httpd_port = self._httpd_port 67 | self._httpd_port = _httpd_port 68 | 69 | if self._httpd_address != _httpd_address: 70 | self._old_httpd_address = self._httpd_address 71 | self._httpd_address = _httpd_address 72 | 73 | if self.use_httpd() and not self.httpd: 74 | self.start_httpd() 75 | elif self.use_httpd() and (port_changed or whitelist_changed or address_changed): 76 | if self.httpd: 77 | self.restart_httpd() 78 | else: 79 | self.start_httpd() 80 | elif not self.use_httpd() and self.httpd: 81 | self.shutdown_httpd() 82 | 83 | elif sender == 'plugin.video.youtube': 84 | logger.log_debug('onNotification: |unknown method|') 85 | 86 | def use_httpd(self): 87 | return self._use_httpd 88 | 89 | def httpd_port(self): 90 | return int(self._httpd_port) 91 | 92 | def httpd_address(self): 93 | return self._httpd_address 94 | 95 | def old_httpd_address(self): 96 | return self._old_httpd_address 97 | 98 | def old_httpd_port(self): 99 | return int(self._old_httpd_port) 100 | 101 | def httpd_port_sync(self): 102 | self._old_httpd_port = self._httpd_port 103 | 104 | def start_httpd(self): 105 | if not self.httpd: 106 | logger.log_debug('HTTPServer: Starting |{ip}:{port}|'.format(ip=self.httpd_address(), 107 | port=str(self.httpd_port()))) 108 | self.httpd_port_sync() 109 | self.httpd = get_http_server(address=self.httpd_address(), port=self.httpd_port()) 110 | if self.httpd: 111 | self.httpd_thread = threading.Thread(target=self.httpd.serve_forever) 112 | self.httpd_thread.daemon = True 113 | self.httpd_thread.start() 114 | sock_name = self.httpd.socket.getsockname() 115 | logger.log_debug('HTTPServer: Serving on |{ip}:{port}|'.format(ip=str(sock_name[0]), 116 | port=str(sock_name[1]))) 117 | 118 | def shutdown_httpd(self): 119 | if self.httpd: 120 | logger.log_debug('HTTPServer: Shutting down |{ip}:{port}|'.format(ip=self.old_httpd_address(), 121 | port=str(self.old_httpd_port()))) 122 | self.httpd_port_sync() 123 | self.httpd.shutdown() 124 | self.httpd.socket.close() 125 | self.httpd_thread.join() 126 | self.httpd_thread = None 127 | self.httpd = None 128 | 129 | def restart_httpd(self): 130 | logger.log_debug('HTTPServer: Restarting... |{old_ip}:{old_port}| -> |{ip}:{port}|' 131 | .format(old_ip=self.old_httpd_address(), old_port=str(self.old_httpd_port()), 132 | ip=self.httpd_address(), port=str(self.httpd_port()))) 133 | self.shutdown_httpd() 134 | self.start_httpd() 135 | 136 | def ping_httpd(self): 137 | return is_httpd_live(port=self.httpd_port()) 138 | 139 | def remove_temp_dir(self): 140 | try: 141 | path = xbmc.translatePath('special://temp/%s' % self.addon_id).decode('utf-8') 142 | except AttributeError: 143 | path = xbmc.translatePath('special://temp/%s' % self.addon_id) 144 | 145 | if os.path.isdir(path): 146 | try: 147 | xbmcvfs.rmdir(path, force=True) 148 | except: 149 | pass 150 | if os.path.isdir(path): 151 | try: 152 | shutil.rmtree(path) 153 | except: 154 | pass 155 | 156 | if os.path.isdir(path): 157 | logger.log_debug('Failed to remove directory: {dir}'.format(dir=path.encode('utf-8'))) 158 | return False 159 | else: 160 | return True 161 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/tv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six import PY2 12 | 13 | from ... import kodion 14 | from ...youtube.helper import utils 15 | from ...kodion.items.video_item import VideoItem 16 | 17 | 18 | def my_subscriptions_to_items(provider, context, json_data, do_filter=False): 19 | result = [] 20 | video_id_dict = {} 21 | 22 | incognito = str(context.get_param('incognito', False)).lower() == 'true' 23 | 24 | filter_list = [] 25 | black_list = False 26 | if do_filter: 27 | black_list = context.get_settings().get_bool('youtube.filter.my_subscriptions_filtered.blacklist', False) 28 | filter_list = context.get_settings().get_string('youtube.filter.my_subscriptions_filtered.list', '') 29 | filter_list = filter_list.replace(', ', ',') 30 | filter_list = filter_list.split(',') 31 | filter_list = [x.lower() for x in filter_list] 32 | 33 | items = json_data.get('items', []) 34 | for item in items: 35 | channel = item['channel'].lower() 36 | channel = channel.replace(',', '') 37 | if PY2: 38 | channel = channel.encode('utf-8', 'ignore') 39 | if not do_filter or (do_filter and (not black_list) and (channel in filter_list)) or \ 40 | (do_filter and black_list and (channel not in filter_list)): 41 | video_id = item['id'] 42 | item_params = {'video_id': video_id} 43 | if incognito: 44 | item_params.update({'incognito': incognito}) 45 | item_uri = context.create_uri(['play'], item_params) 46 | video_item = VideoItem(item['title'], uri=item_uri) 47 | if incognito: 48 | video_item.set_play_count(0) 49 | result.append(video_item) 50 | 51 | video_id_dict[video_id] = video_item 52 | 53 | use_play_data = not incognito and context.get_settings().use_playback_history() 54 | 55 | channel_item_dict = {} 56 | utils.update_video_infos(provider, context, video_id_dict, channel_items_dict=channel_item_dict, use_play_data=use_play_data) 57 | utils.update_fanarts(provider, context, channel_item_dict) 58 | 59 | # next page 60 | next_page_token = json_data.get('next_page_token', '') 61 | if next_page_token or json_data.get('continue', False): 62 | new_params = {} 63 | new_params.update(context.get_params()) 64 | new_params['next_page_token'] = next_page_token 65 | new_params['offset'] = int(json_data.get('offset', 0)) 66 | 67 | new_context = context.clone(new_params=new_params) 68 | 69 | current_page = int(new_context.get_param('page', 1)) 70 | next_page_item = kodion.items.NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) 71 | result.append(next_page_item) 72 | 73 | return result 74 | 75 | 76 | def tv_videos_to_items(provider, context, json_data): 77 | result = [] 78 | video_id_dict = {} 79 | 80 | incognito = str(context.get_param('incognito', False)).lower() == 'true' 81 | 82 | items = json_data.get('items', []) 83 | for item in items: 84 | video_id = item['id'] 85 | item_params = {'video_id': video_id} 86 | if incognito: 87 | item_params.update({'incognito': incognito}) 88 | item_uri = context.create_uri(['play'], item_params) 89 | video_item = VideoItem(item['title'], uri=item_uri) 90 | if incognito: 91 | video_item.set_play_count(0) 92 | 93 | result.append(video_item) 94 | 95 | video_id_dict[video_id] = video_item 96 | 97 | use_play_data = not incognito and context.get_settings().use_playback_history() 98 | 99 | channel_item_dict = {} 100 | utils.update_video_infos(provider, context, video_id_dict, channel_items_dict=channel_item_dict, use_play_data=use_play_data) 101 | utils.update_fanarts(provider, context, channel_item_dict) 102 | 103 | # next page 104 | next_page_token = json_data.get('next_page_token', '') 105 | if next_page_token or json_data.get('continue', False): 106 | new_params = {} 107 | new_params.update(context.get_params()) 108 | new_params['next_page_token'] = next_page_token 109 | new_params['offset'] = int(json_data.get('offset', 0)) 110 | 111 | new_context = context.clone(new_params=new_params) 112 | 113 | current_page = int(new_context.get_param('page', 1)) 114 | next_page_item = kodion.items.NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) 115 | result.append(next_page_item) 116 | 117 | return result 118 | 119 | 120 | def saved_playlists_to_items(provider, context, json_data): 121 | result = [] 122 | playlist_id_dict = {} 123 | 124 | incognito = str(context.get_param('incognito', False)).lower() == 'true' 125 | thumb_size = context.get_settings().use_thumbnail_size() 126 | 127 | items = json_data.get('items', []) 128 | for item in items: 129 | title = item['title'] 130 | channel_id = item['channel_id'] 131 | playlist_id = item['id'] 132 | image = utils.get_thumbnail(thumb_size, item.get('thumbnails', {})) 133 | 134 | item_params = {} 135 | if incognito: 136 | item_params.update({'incognito': incognito}) 137 | 138 | if channel_id: 139 | item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params) 140 | else: 141 | item_uri = context.create_uri(['playlist', playlist_id], item_params) 142 | 143 | playlist_item = kodion.items.DirectoryItem(title, item_uri, image=image) 144 | playlist_item.set_fanart(provider.get_fanart(context)) 145 | result.append(playlist_item) 146 | playlist_id_dict[playlist_id] = playlist_item 147 | 148 | channel_items_dict = {} 149 | utils.update_playlist_infos(provider, context, playlist_id_dict, channel_items_dict) 150 | utils.update_fanarts(provider, context, channel_items_dict) 151 | 152 | # next page 153 | next_page_token = json_data.get('next_page_token', '') 154 | if next_page_token or json_data.get('continue', False): 155 | new_params = {} 156 | new_params.update(context.get_params()) 157 | new_params['next_page_token'] = next_page_token 158 | new_params['offset'] = int(json_data.get('offset', 0)) 159 | 160 | new_context = context.clone(new_params=new_params) 161 | 162 | current_page = int(new_context.get_param('page', 1)) 163 | next_page_item = kodion.items.NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) 164 | result.append(next_page_item) 165 | 166 | return result 167 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six.moves import urllib 12 | 13 | import re 14 | from ...kodion.items import VideoItem, DirectoryItem 15 | from . import utils 16 | 17 | 18 | class UrlToItemConverter(object): 19 | RE_CHANNEL_ID = re.compile(r'^/channel/(?P.+)$') 20 | 21 | def __init__(self, flatten=True): 22 | self._flatten = flatten 23 | 24 | self._video_id_dict = {} 25 | self._video_items = [] 26 | 27 | self._playlist_id_dict = {} 28 | self._playlist_items = [] 29 | self._playlist_ids = [] 30 | 31 | self._channel_id_dict = {} 32 | self._channel_items = [] 33 | self._channel_ids = [] 34 | 35 | def add_url(self, url, provider, context): 36 | url_components = urllib.parse.urlparse(url) 37 | if url_components.hostname.lower() == 'youtube.com' or url_components.hostname.lower() == 'www.youtube.com': 38 | params = dict(urllib.parse.parse_qsl(url_components.query)) 39 | if url_components.path.lower() == '/watch': 40 | video_id = params.get('v', '') 41 | if video_id: 42 | plugin_uri = context.create_uri(['play'], {'video_id': video_id}) 43 | video_item = VideoItem('', plugin_uri) 44 | self._video_id_dict[video_id] = video_item 45 | 46 | playlist_id = params.get('list', '') 47 | if playlist_id: 48 | if self._flatten: 49 | self._playlist_ids.append(playlist_id) 50 | else: 51 | playlist_item = DirectoryItem('', context.create_uri(['playlist', playlist_id])) 52 | playlist_item.set_fanart(provider.get_fanart(context)) 53 | self._playlist_id_dict[playlist_id] = playlist_item 54 | elif url_components.path.lower() == '/playlist': 55 | playlist_id = params.get('list', '') 56 | if playlist_id: 57 | if self._flatten: 58 | self._playlist_ids.append(playlist_id) 59 | else: 60 | playlist_item = DirectoryItem('', context.create_uri(['playlist', playlist_id])) 61 | playlist_item.set_fanart(provider.get_fanart(context)) 62 | self._playlist_id_dict[playlist_id] = playlist_item 63 | elif self.RE_CHANNEL_ID.match(url_components.path): 64 | re_match = self.RE_CHANNEL_ID.match(url_components.path) 65 | channel_id = re_match.group('channel_id') 66 | if self._flatten: 67 | self._channel_ids.append(channel_id) 68 | else: 69 | channel_item = DirectoryItem('', context.create_uri(['channel', channel_id])) 70 | channel_item.set_fanart(provider.get_fanart(context)) 71 | self._channel_id_dict[channel_id] = channel_item 72 | else: 73 | context.log_debug('Unknown path "%s"' % url_components.path) 74 | 75 | def add_urls(self, urls, provider, context): 76 | for url in urls: 77 | self.add_url(url, provider, context) 78 | 79 | def get_items(self, provider, context, title_required=True): 80 | result = [] 81 | 82 | if self._flatten and len(self._channel_ids) > 0: 83 | # remove duplicates 84 | self._channel_ids = list(set(self._channel_ids)) 85 | 86 | channels_item = DirectoryItem(context.get_ui().bold(context.localize(provider.LOCAL_MAP['youtube.channels'])), 87 | context.create_uri(['special', 'description_links'], 88 | {'channel_ids': ','.join(self._channel_ids)}), 89 | context.create_resource_path('media', 'playlist.png')) 90 | channels_item.set_fanart(provider.get_fanart(context)) 91 | result.append(channels_item) 92 | 93 | if self._flatten and len(self._playlist_ids) > 0: 94 | # remove duplicates 95 | self._playlist_ids = list(set(self._playlist_ids)) 96 | 97 | playlists_item = DirectoryItem(context.get_ui().bold(context.localize(provider.LOCAL_MAP['youtube.playlists'])), 98 | context.create_uri(['special', 'description_links'], 99 | {'playlist_ids': ','.join(self._playlist_ids)}), 100 | context.create_resource_path('media', 'playlist.png')) 101 | playlists_item.set_fanart(provider.get_fanart(context)) 102 | result.append(playlists_item) 103 | 104 | if not self._flatten: 105 | result.extend(self.get_channel_items(provider, context)) 106 | 107 | if not self._flatten: 108 | result.extend(self.get_playlist_items(provider, context)) 109 | 110 | # add videos 111 | result.extend(self.get_video_items(provider, context, title_required)) 112 | 113 | return result 114 | 115 | def get_video_items(self, provider, context, title_required=True): 116 | incognito = str(context.get_param('incognito', False)).lower() == 'true' 117 | use_play_data = not incognito 118 | 119 | if len(self._video_items) == 0: 120 | channel_id_dict = {} 121 | utils.update_video_infos(provider, context, self._video_id_dict, None, channel_id_dict, use_play_data=use_play_data) 122 | utils.update_fanarts(provider, context, channel_id_dict) 123 | 124 | for key in self._video_id_dict: 125 | video_item = self._video_id_dict[key] 126 | if not title_required or (title_required and video_item.get_title()): 127 | self._video_items.append(video_item) 128 | 129 | return self._video_items 130 | 131 | def get_playlist_items(self, provider, context): 132 | if len(self._playlist_items) == 0: 133 | channel_id_dict = {} 134 | utils.update_playlist_infos(provider, context, self._playlist_id_dict, channel_id_dict) 135 | utils.update_fanarts(provider, context, channel_id_dict) 136 | 137 | for key in self._playlist_id_dict: 138 | playlist_item = self._playlist_id_dict[key] 139 | if playlist_item.get_name(): 140 | self._playlist_items.append(playlist_item) 141 | 142 | return self._playlist_items 143 | 144 | def get_channel_items(self, provider, context): 145 | if len(self._channel_items) == 0: 146 | channel_id_dict = {} 147 | utils.update_fanarts(provider, context, channel_id_dict) 148 | 149 | for key in self._channel_id_dict: 150 | channel_item = self._channel_id_dict[key] 151 | if channel_item.get_name(): 152 | self._channel_items.append(channel_item) 153 | 154 | return self._channel_items 155 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/url_resolver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six.moves import urllib 12 | 13 | import re 14 | 15 | from ...kodion.utils import FunctionCache 16 | from ...kodion import Context as _Context 17 | import requests 18 | 19 | 20 | class AbstractResolver(object): 21 | def __init__(self): 22 | self._verify = _Context(plugin_id='plugin.video.youtube').get_settings().verify_ssl() 23 | 24 | def supports_url(self, url, url_components): 25 | raise NotImplementedError() 26 | 27 | def resolve(self, url, url_components): 28 | raise NotImplementedError() 29 | 30 | 31 | class YouTubeResolver(AbstractResolver): 32 | RE_USER_NAME = re.compile(r'http(s)?://(www.)?youtube.com/(?P[a-zA-Z0-9]+)$') 33 | 34 | def __init__(self): 35 | AbstractResolver.__init__(self) 36 | 37 | def supports_url(self, url, url_components): 38 | if url_components.hostname == 'www.youtube.com' or url_components.hostname == 'youtube.com': 39 | if url_components.path.lower() in ['/redirect', '/user']: 40 | return True 41 | 42 | if url_components.path.lower().startswith('/user'): 43 | return True 44 | 45 | re_match = self.RE_USER_NAME.match(url) 46 | if re_match: 47 | return True 48 | 49 | return False 50 | 51 | def resolve(self, url, url_components): 52 | def _load_page(_url): 53 | # we try to extract the channel id from the html content. With the channel id we can construct a url we 54 | # already work with. 55 | # https://www.youtube.com/channel/ 56 | try: 57 | headers = {'Cache-Control': 'max-age=0', 58 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 59 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36', 60 | 'DNT': '1', 61 | 'Accept-Encoding': 'gzip, deflate', 62 | 'Accept-Language': 'en-US,en;q=0.8,de;q=0.6'} 63 | response = requests.get(url, headers=headers, verify=self._verify) 64 | if response.status_code == 200: 65 | match = re.search(r'', response.text) 66 | if match: 67 | channel_id = match.group('channel_id') 68 | return 'https://www.youtube.com/channel/%s' % channel_id 69 | except: 70 | # do nothing 71 | pass 72 | 73 | return _url 74 | 75 | if url_components.path.lower() == '/redirect': 76 | params = dict(urllib.parse.parse_qsl(url_components.query)) 77 | return params['q'] 78 | 79 | if url_components.path.lower().startswith('/user'): 80 | return _load_page(url) 81 | 82 | re_match = self.RE_USER_NAME.match(url) 83 | if re_match: 84 | return _load_page(url) 85 | 86 | return url 87 | 88 | 89 | class CommonResolver(AbstractResolver, list): 90 | def __init__(self): 91 | AbstractResolver.__init__(self) 92 | 93 | def supports_url(self, url, url_components): 94 | return True 95 | 96 | def resolve(self, url, url_components): 97 | def _loop(_url, tries=5): 98 | if tries == 0: 99 | return _url 100 | 101 | try: 102 | headers = {'Cache-Control': 'max-age=0', 103 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 104 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36', 105 | 'DNT': '1', 106 | 'Accept-Encoding': 'gzip, deflate', 107 | 'Accept-Language': 'en-US,en;q=0.8,de;q=0.6'} 108 | response = requests.head(_url, headers=headers, verify=self._verify, allow_redirects=False) 109 | if response.status_code == 304: 110 | return url 111 | 112 | if response.status_code in [301, 302, 303]: 113 | headers = response.headers 114 | location = headers.get('location', '') 115 | 116 | # validate the location - some server returned garbage 117 | _url_components = urllib.parse.urlparse(location) 118 | if not _url_components.scheme and not _url_components.hostname: 119 | return url 120 | 121 | # some server return 301 for HEAD requests 122 | # we just compare the new location - if it's equal we can return the url 123 | if location == _url or ''.join([location, '/']) == _url or location == ''.join([_url, '/']): 124 | return _url 125 | 126 | if location: 127 | return _loop(location, tries=tries - 1) 128 | 129 | # just to be sure ;) 130 | location = headers.get('Location', '') 131 | if location: 132 | return _loop(location, tries=tries - 1) 133 | except: 134 | # do nothing 135 | pass 136 | 137 | return _url 138 | 139 | resolved_url = _loop(url) 140 | 141 | return resolved_url 142 | 143 | 144 | class UrlResolver(object): 145 | def __init__(self, context): 146 | self._context = context 147 | self._cache = {} 148 | self._youtube_resolver = YouTubeResolver() 149 | self._resolver = [ 150 | self._youtube_resolver, 151 | CommonResolver() 152 | ] 153 | 154 | def clear(self): 155 | self._context.get_function_cache().clear() 156 | 157 | def _resolve(self, url): 158 | # try one of the resolver 159 | url_components = urllib.parse.urlparse(url) 160 | for resolver in self._resolver: 161 | if resolver.supports_url(url, url_components): 162 | resolved_url = resolver.resolve(url, url_components) 163 | self._cache[url] = resolved_url 164 | 165 | # one last check...sometimes the resolved url is YouTube-specific and can be resolved again or 166 | # simplified. 167 | url_components = urllib.parse.urlparse(resolved_url) 168 | if resolver is not self._youtube_resolver and self._youtube_resolver.supports_url(resolved_url, url_components): 169 | return self._youtube_resolver.resolve(resolved_url, url_components) 170 | 171 | return resolved_url 172 | 173 | def resolve(self, url): 174 | function_cache = self._context.get_function_cache() 175 | resolved_url = function_cache.get(FunctionCache.ONE_DAY, self._resolve, url) 176 | if not resolved_url or resolved_url == '/': 177 | return url 178 | 179 | return resolved_url 180 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/youtube/helper/yt_login.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six.moves import range 12 | 13 | import copy 14 | import json 15 | import time 16 | from ...youtube.youtube_exceptions import LoginException 17 | 18 | 19 | def process(mode, provider, context, sign_out_refresh=True): 20 | addon_id = context.get_param('addon_id', None) 21 | 22 | def _do_logout(): 23 | # we clear the cache, so none cached data of an old account will be displayed. 24 | provider.get_resource_manager(context).clear() 25 | 26 | signout_access_manager = context.get_access_manager() 27 | if addon_id: 28 | if signout_access_manager.developer_has_refresh_token(addon_id): 29 | refresh_tokens = signout_access_manager.get_dev_refresh_token(addon_id).split('|') 30 | refresh_tokens = list(set(refresh_tokens)) 31 | for _refresh_token in refresh_tokens: 32 | provider.get_client(context).revoke(_refresh_token) 33 | else: 34 | if signout_access_manager.has_refresh_token(): 35 | refresh_tokens = signout_access_manager.get_refresh_token().split('|') 36 | refresh_tokens = list(set(refresh_tokens)) 37 | for _refresh_token in refresh_tokens: 38 | provider.get_client(context).revoke(_refresh_token) 39 | 40 | provider.reset_client() 41 | 42 | if addon_id: 43 | signout_access_manager.update_dev_access_token(addon_id, access_token='', refresh_token='') 44 | else: 45 | signout_access_manager.update_access_token(access_token='', refresh_token='') 46 | 47 | def _do_login(_for_tv=False): 48 | _client = provider.get_client(context) 49 | 50 | try: 51 | if _for_tv: 52 | json_data = _client.request_device_and_user_code_tv() 53 | else: 54 | json_data = _client.request_device_and_user_code() 55 | except LoginException: 56 | _do_logout() 57 | raise 58 | 59 | interval = int(json_data.get('interval', 5)) * 1000 60 | if interval > 60000: 61 | interval = 5000 62 | device_code = json_data['device_code'] 63 | user_code = json_data['user_code'] 64 | verification_url = json_data.get('verification_url', 'youtube.com/activate').lstrip('https://www.') 65 | 66 | text = [context.localize(provider.LOCAL_MAP['youtube.sign.go_to']) % context.get_ui().bold(verification_url), 67 | '[CR]%s %s' % (context.localize(provider.LOCAL_MAP['youtube.sign.enter_code']), 68 | context.get_ui().bold(user_code))] 69 | text = ''.join(text) 70 | dialog = context.get_ui().create_progress_dialog( 71 | heading=context.localize(provider.LOCAL_MAP['youtube.sign.in']), text=text, background=False) 72 | 73 | steps = ((10 * 60 * 1000) // interval) # 10 Minutes 74 | dialog.set_total(steps) 75 | for i in range(steps): 76 | dialog.update() 77 | try: 78 | if _for_tv: 79 | json_data = _client.request_access_token_tv(device_code) 80 | else: 81 | json_data = _client.request_access_token(device_code) 82 | except LoginException: 83 | _do_logout() 84 | raise 85 | 86 | log_data = copy.deepcopy(json_data) 87 | if 'access_token' in log_data: 88 | log_data['access_token'] = '' 89 | if 'refresh_token' in log_data: 90 | log_data['refresh_token'] = '' 91 | context.log_debug('Requesting access token: |%s|' % json.dumps(log_data)) 92 | 93 | if 'error' not in json_data: 94 | _access_token = json_data.get('access_token', '') 95 | _expires_in = time.time() + int(json_data.get('expires_in', 3600)) 96 | _refresh_token = json_data.get('refresh_token', '') 97 | dialog.close() 98 | if not _access_token and not _refresh_token: 99 | _expires_in = 0 100 | return _access_token, _expires_in, _refresh_token 101 | 102 | elif json_data['error'] != u'authorization_pending': 103 | message = json_data['error'] 104 | title = '%s: %s' % (context.get_name(), message) 105 | context.get_ui().show_notification(message, title) 106 | context.log_error('Error requesting access token: |%s|' % message) 107 | 108 | if dialog.is_aborted(): 109 | dialog.close() 110 | return '', 0, '' 111 | 112 | context.sleep(interval) 113 | dialog.close() 114 | 115 | if mode == 'out': 116 | _do_logout() 117 | if sign_out_refresh: 118 | context.get_ui().refresh_container() 119 | 120 | elif mode == 'in': 121 | context.get_ui().on_ok(context.localize(provider.LOCAL_MAP['youtube.sign.twice.title']), 122 | context.localize(provider.LOCAL_MAP['youtube.sign.twice.text'])) 123 | 124 | access_token_tv, expires_in_tv, refresh_token_tv = _do_login(_for_tv=True) 125 | # abort tv login 126 | context.log_debug('YouTube-TV Login: Access Token |%s| Refresh Token |%s| Expires |%s|' % 127 | (access_token_tv != '', refresh_token_tv != '', expires_in_tv)) 128 | if not access_token_tv and not refresh_token_tv: 129 | provider.reset_client() 130 | if addon_id: 131 | context.get_access_manager().update_dev_access_token(addon_id, '') 132 | else: 133 | context.get_access_manager().update_access_token('') 134 | context.get_ui().refresh_container() 135 | return 136 | 137 | access_token_kodi, expires_in_kodi, refresh_token_kodi = _do_login(_for_tv=False) 138 | # abort kodi login 139 | context.log_debug('YouTube-Kodi Login: Access Token |%s| Refresh Token |%s| Expires |%s|' % 140 | (access_token_kodi != '', refresh_token_kodi != '', expires_in_kodi)) 141 | if not access_token_kodi and not refresh_token_kodi: 142 | provider.reset_client() 143 | if addon_id: 144 | context.get_access_manager().update_dev_access_token(addon_id, '') 145 | else: 146 | context.get_access_manager().update_access_token('') 147 | context.get_ui().refresh_container() 148 | return 149 | 150 | access_token = '%s|%s' % (access_token_tv, access_token_kodi) 151 | refresh_token = '%s|%s' % (refresh_token_tv, refresh_token_kodi) 152 | expires_in = min(expires_in_tv, expires_in_kodi) 153 | 154 | # we clear the cache, so none cached data of an old account will be displayed. 155 | provider.get_resource_manager(context).clear() 156 | 157 | provider.reset_client() 158 | 159 | if addon_id: 160 | context.get_access_manager().update_dev_access_token(addon_id, access_token, expires_in, refresh_token) 161 | else: 162 | context.get_access_manager().update_access_token(access_token, expires_in, refresh_token) 163 | 164 | context.get_ui().refresh_container() 165 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import xbmc 12 | import xbmcgui 13 | 14 | from ..abstract_context_ui import AbstractContextUI 15 | from .xbmc_progress_dialog import XbmcProgressDialog 16 | from .xbmc_progress_dialog_bg import XbmcProgressDialogBG 17 | from ... import constants 18 | from ... import utils 19 | 20 | 21 | class XbmcContextUI(AbstractContextUI): 22 | def __init__(self, xbmc_addon, context): 23 | AbstractContextUI.__init__(self) 24 | 25 | self._xbmc_addon = xbmc_addon 26 | 27 | self._context = context 28 | self._view_mode = None 29 | 30 | def create_progress_dialog(self, heading, text=None, background=False): 31 | if background and self._context.get_system_version().get_version() > (12, 3): 32 | return XbmcProgressDialogBG(heading, text) 33 | 34 | return XbmcProgressDialog(heading, text) 35 | 36 | def get_skin_id(self): 37 | return xbmc.getSkinDir() 38 | 39 | def on_keyboard_input(self, title, default='', hidden=False): 40 | # fallback for Frodo 41 | if self._context.get_system_version().get_version() <= (12, 3): 42 | keyboard = xbmc.Keyboard(default, title, hidden) 43 | keyboard.doModal() 44 | if keyboard.isConfirmed() and keyboard.getText(): 45 | text = utils.to_unicode(keyboard.getText()) 46 | return True, text 47 | else: 48 | return False, u'' 49 | 50 | # Starting with Gotham (13.X > ...) 51 | dialog = xbmcgui.Dialog() 52 | result = dialog.input(title, utils.to_unicode(default), type=xbmcgui.INPUT_ALPHANUM) 53 | if result: 54 | text = utils.to_unicode(result) 55 | return True, text 56 | 57 | return False, u'' 58 | 59 | def on_numeric_input(self, title, default=''): 60 | dialog = xbmcgui.Dialog() 61 | result = dialog.input(title, str(default), type=xbmcgui.INPUT_NUMERIC) 62 | if result: 63 | return True, int(result) 64 | 65 | return False, None 66 | 67 | def on_yes_no_input(self, title, text, nolabel='', yeslabel=''): 68 | dialog = xbmcgui.Dialog() 69 | return dialog.yesno(title, text, nolabel=nolabel, yeslabel=yeslabel) 70 | 71 | def on_ok(self, title, text): 72 | dialog = xbmcgui.Dialog() 73 | return dialog.ok(title, text) 74 | 75 | def on_remove_content(self, content_name): 76 | text = self._context.localize(constants.localize.REMOVE_CONTENT) % utils.to_unicode(content_name) 77 | return self.on_yes_no_input(self._context.localize(constants.localize.CONFIRM_REMOVE), text) 78 | 79 | def on_delete_content(self, content_name): 80 | text = self._context.localize(constants.localize.DELETE_CONTENT) % utils.to_unicode(content_name) 81 | return self.on_yes_no_input(self._context.localize(constants.localize.CONFIRM_DELETE), text) 82 | 83 | def on_select(self, title, items=None): 84 | if items is None: 85 | items = [] 86 | major_version = self._context.get_system_version().get_version()[0] 87 | if isinstance(items[0], tuple) and len(items[0]) == 4 and major_version <= 16: 88 | items = [(item[0], item[2]) for item in items] 89 | 90 | use_details = (isinstance(items[0], tuple) and len(items[0]) == 4 and major_version > 16) 91 | 92 | _dict = {} 93 | _items = [] 94 | i = 0 95 | for item in items: 96 | if isinstance(item, tuple): 97 | if use_details: 98 | new_item = xbmcgui.ListItem(label=item[0], label2=item[1]) 99 | new_item.setArt({'icon': item[3], 'thumb': item[3]}) 100 | _items.append(new_item) 101 | _dict[i] = item[2] 102 | else: 103 | _dict[i] = item[1] 104 | _items.append(item[0]) 105 | else: 106 | _dict[i] = i 107 | _items.append(item) 108 | 109 | i += 1 110 | 111 | dialog = xbmcgui.Dialog() 112 | if use_details: 113 | result = dialog.select(title, _items, useDetails=use_details) 114 | else: 115 | result = dialog.select(title, _items) 116 | 117 | return _dict.get(result, -1) 118 | 119 | def show_notification(self, message, header='', image_uri='', time_milliseconds=5000, audible=True): 120 | _header = header 121 | if not _header: 122 | _header = self._context.get_name() 123 | _header = utils.to_utf8(_header) 124 | 125 | _image = image_uri 126 | if not _image: 127 | _image = self._context.get_icon() 128 | 129 | try: 130 | _message = utils.to_utf8(message.decode('unicode-escape')) 131 | except UnicodeEncodeError: 132 | _message = utils.to_utf8(message) 133 | 134 | try: 135 | _message = _message.replace(',', ' ') 136 | _message = _message.replace('\n', ' ') 137 | except TypeError: 138 | _message = _message.replace(b',', b' ') 139 | _message = _message.replace(b'\n', b' ') 140 | _message = utils.to_unicode(_message) 141 | _header = utils.to_unicode(_header) 142 | 143 | # xbmc.executebuiltin("Notification(%s, %s, %d, %s)" % (_header, _message, time_milliseconds, _image)) 144 | xbmcgui.Dialog().notification(_header, _message, _image, time_milliseconds, audible) 145 | 146 | def open_settings(self): 147 | self._xbmc_addon.openSettings() 148 | 149 | def refresh_container(self): 150 | script_uri = 'special://home/addons/%s/resources/lib/youtube_plugin/refresh.py' % self._context.get_id() 151 | xbmc.executebuiltin('RunScript(%s)' % script_uri) 152 | 153 | @staticmethod 154 | def get_info_label(value): 155 | return xbmc.getInfoLabel(value) 156 | 157 | @staticmethod 158 | def set_home_window_property(property_id, value): 159 | property_id = ''.join(['plugin.video.youtube-', property_id]) 160 | xbmcgui.Window(10000).setProperty(property_id, value) 161 | 162 | @staticmethod 163 | def get_home_window_property(property_id): 164 | property_id = ''.join(['plugin.video.youtube-', property_id]) 165 | return xbmcgui.Window(10000).getProperty(property_id) or None 166 | 167 | @staticmethod 168 | def clear_home_window_property(property_id): 169 | property_id = ''.join(['plugin.video.youtube-', property_id]) 170 | xbmcgui.Window(10000).clearProperty(property_id) 171 | 172 | @staticmethod 173 | def bold(value): 174 | return ''.join(['[B]', value, '[/B]']) 175 | 176 | @staticmethod 177 | def uppercase(value): 178 | return ''.join(['[UPPERCASE]', value, '[/UPPERCASE]']) 179 | 180 | @staticmethod 181 | def color(color, value): 182 | return ''.join(['[COLOR=', color.lower(), ']', value, '[/COLOR]']) 183 | 184 | def set_focus_next_item(self): 185 | cid = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId() 186 | try: 187 | current_position = int(self.get_info_label('Container.Position')) + 1 188 | self._context.execute('SetFocus(%s,%s)' % (cid, str(current_position))) 189 | except ValueError: 190 | pass 191 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import xbmcgui 12 | import xbmcplugin 13 | 14 | from ..abstract_provider_runner import AbstractProviderRunner 15 | from ...exceptions import KodionException 16 | from ...items import * 17 | from ... import AbstractProvider 18 | from . import info_labels 19 | from . import xbmc_items 20 | 21 | 22 | class XbmcRunner(AbstractProviderRunner): 23 | def __init__(self): 24 | AbstractProviderRunner.__init__(self) 25 | self.handle = None 26 | self.settings = None 27 | 28 | def run(self, provider, context=None): 29 | 30 | self.handle = context.get_handle() 31 | 32 | try: 33 | results = provider.navigate(context) 34 | except KodionException as ex: 35 | if provider.handle_exception(context, ex): 36 | context.log_error(ex.__str__()) 37 | xbmcgui.Dialog().ok("Exception in ContentProvider", ex.__str__()) 38 | xbmcplugin.endOfDirectory(self.handle, succeeded=False) 39 | return 40 | 41 | self.settings = context.get_settings() 42 | 43 | result = results[0] 44 | options = {} 45 | options.update(results[1]) 46 | 47 | if isinstance(result, bool) and not result: 48 | xbmcplugin.endOfDirectory(self.handle, succeeded=False) 49 | elif isinstance(result, VideoItem) or isinstance(result, AudioItem) or isinstance(result, UriItem): 50 | self._set_resolved_url(context, result) 51 | elif isinstance(result, DirectoryItem): 52 | self._add_directory(context, result) 53 | elif isinstance(result, list): 54 | item_count = len(result) 55 | for item in result: 56 | if isinstance(item, DirectoryItem): 57 | self._add_directory(context, item, item_count) 58 | elif isinstance(item, VideoItem): 59 | self._add_video(context, item, item_count) 60 | elif isinstance(item, AudioItem): 61 | self._add_audio(context, item, item_count) 62 | elif isinstance(item, ImageItem): 63 | self._add_image(context, item, item_count) 64 | 65 | xbmcplugin.endOfDirectory( 66 | self.handle, succeeded=True, 67 | updateListing=options.get(AbstractProvider.RESULT_UPDATE_LISTING, False), 68 | cacheToDisc=options.get(AbstractProvider.RESULT_CACHE_TO_DISC, True)) 69 | else: 70 | # handle exception 71 | pass 72 | 73 | def _set_resolved_url(self, context, base_item, succeeded=True): 74 | item = xbmc_items.to_playback_item(context, base_item) 75 | item.setPath(base_item.get_uri()) 76 | xbmcplugin.setResolvedUrl(self.handle, succeeded=succeeded, listitem=item) 77 | 78 | """ 79 | # just to be sure :) 80 | if not isLiveStream: 81 | tries = 100 82 | while tries>0: 83 | xbmc.sleep(50) 84 | if xbmc.Player().isPlaying() and xbmc.getCondVisibility("Player.Paused"): 85 | xbmc.Player().pause() 86 | break 87 | tries-=1 88 | """ 89 | 90 | def _add_directory(self, context, directory_item, item_count=0): 91 | major_version = context.get_system_version().get_version()[0] 92 | 93 | art = {'icon': 'DefaultFolder.png', 94 | 'thumb': directory_item.get_image()} 95 | 96 | if major_version > 17: 97 | item = xbmcgui.ListItem(label=directory_item.get_name(), offscreen=True) 98 | else: 99 | item = xbmcgui.ListItem(label=directory_item.get_name()) 100 | 101 | # only set fanart is enabled 102 | if directory_item.get_fanart() and self.settings.show_fanart(): 103 | art['fanart'] = directory_item.get_fanart() 104 | 105 | if major_version <= 15: 106 | item.setArt(art) 107 | item.setIconImage(art['icon']) 108 | else: 109 | item.setArt(art) 110 | 111 | if directory_item.get_context_menu() is not None: 112 | item.addContextMenuItems(directory_item.get_context_menu(), 113 | replaceItems=directory_item.replace_context_menu()) 114 | 115 | item.setInfo(type='video', infoLabels=info_labels.create_from_item(directory_item)) 116 | item.setPath(directory_item.get_uri()) 117 | 118 | is_folder = True 119 | if directory_item.is_action(): 120 | is_folder = False 121 | item.setProperty('isPlayable', 'false') 122 | 123 | if directory_item.next_page: 124 | item.setProperty('specialSort', 'bottom') 125 | 126 | if directory_item.get_channel_subscription_id(): # make channel_subscription_id property available for keymapping 127 | item.setProperty('channel_subscription_id', directory_item.get_channel_subscription_id()) 128 | 129 | xbmcplugin.addDirectoryItem(handle=self.handle, 130 | url=directory_item.get_uri(), 131 | listitem=item, 132 | isFolder=is_folder, 133 | totalItems=item_count) 134 | 135 | def _add_video(self, context, video_item, item_count=0): 136 | item = xbmc_items.to_video_item(context, video_item) 137 | item.setPath(video_item.get_uri()) 138 | xbmcplugin.addDirectoryItem(handle=self.handle, 139 | url=video_item.get_uri(), 140 | listitem=item, 141 | totalItems=item_count) 142 | 143 | def _add_image(self, context, image_item, item_count): 144 | major_version = context.get_system_version().get_version()[0] 145 | 146 | art = {'icon': 'DefaultPicture.png', 147 | 'thumb': image_item.get_image()} 148 | 149 | if major_version > 17: 150 | item = xbmcgui.ListItem(label=image_item.get_name(), offscreen=True) 151 | else: 152 | item = xbmcgui.ListItem(label=image_item.get_name()) 153 | 154 | if image_item.get_fanart() and self.settings.show_fanart(): 155 | art['fanart'] = image_item.get_fanart() 156 | 157 | if major_version <= 15: 158 | item.setArt(art) 159 | item.setIconImage(art['icon']) 160 | else: 161 | item.setArt(art) 162 | 163 | if image_item.get_context_menu() is not None: 164 | item.addContextMenuItems(image_item.get_context_menu(), replaceItems=image_item.replace_context_menu()) 165 | 166 | item.setInfo(type='picture', infoLabels=info_labels.create_from_item(image_item)) 167 | 168 | item.setPath(image_item.get_uri()) 169 | xbmcplugin.addDirectoryItem(handle=self.handle, 170 | url=image_item.get_uri(), 171 | listitem=item, 172 | totalItems=item_count) 173 | 174 | def _add_audio(self, context, audio_item, item_count): 175 | item = xbmc_items.to_audio_item(context, audio_item) 176 | item.setPath(audio_item.get_uri()) 177 | xbmcplugin.addDirectoryItem(handle=self.handle, 178 | url=audio_item.get_uri(), 179 | listitem=item, 180 | totalItems=item_count) 181 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/abstract_context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | from six.moves import urllib 12 | 13 | import os 14 | 15 | from .. import constants 16 | from .. import logger 17 | from ..utils import * 18 | 19 | 20 | class AbstractContext(object): 21 | def __init__(self, path=u'/', params=None, plugin_name=u'', plugin_id=u''): 22 | if not params: 23 | params = {} 24 | 25 | self._system_version = None 26 | 27 | self._cache_path = None 28 | self._debug_path = None 29 | 30 | self._function_cache = None 31 | self._data_cache = None 32 | self._search_history = None 33 | self._playback_history = None 34 | self._favorite_list = None 35 | self._watch_later_list = None 36 | self._access_manager = None 37 | 38 | self._plugin_name = str(plugin_name) 39 | self._version = 'UNKNOWN' 40 | self._plugin_id = plugin_id 41 | self._path = create_path(path) 42 | self._params = params 43 | self._utils = None 44 | self._view_mode = None 45 | 46 | # create valid uri 47 | self._uri = self.create_uri(self._path, self._params) 48 | 49 | def format_date_short(self, date_obj): 50 | raise NotImplementedError() 51 | 52 | def format_time(self, time_obj): 53 | raise NotImplementedError() 54 | 55 | def get_language(self): 56 | raise NotImplementedError() 57 | 58 | def get_region(self): 59 | raise NotImplementedError() 60 | 61 | def get_cache_path(self): 62 | if not self._cache_path: 63 | self._cache_path = os.path.join(self.get_data_path(), 'kodion') 64 | return self._cache_path 65 | 66 | def get_playback_history(self): 67 | if not self._playback_history: 68 | uuid = self.get_access_manager().get_current_user_id() 69 | db_file = os.path.join(os.path.join(self.get_data_path(), 'playback'), str(uuid)) 70 | self._playback_history = PlaybackHistory(db_file) 71 | return self._playback_history 72 | 73 | def get_data_cache(self): 74 | if not self._data_cache: 75 | max_cache_size_mb = self.get_settings().get_int(constants.setting.CACHE_SIZE, -1) 76 | if max_cache_size_mb <= 0: 77 | max_cache_size_mb = 5 78 | else: 79 | max_cache_size_mb = max_cache_size_mb / 2.0 80 | self._data_cache = DataCache(os.path.join(self.get_cache_path(), 'data_cache'), 81 | max_file_size_mb=max_cache_size_mb) 82 | return self._data_cache 83 | 84 | def get_function_cache(self): 85 | if not self._function_cache: 86 | max_cache_size_mb = self.get_settings().get_int(constants.setting.CACHE_SIZE, -1) 87 | if max_cache_size_mb <= 0: 88 | max_cache_size_mb = 5 89 | else: 90 | max_cache_size_mb = max_cache_size_mb / 2.0 91 | self._function_cache = FunctionCache(os.path.join(self.get_cache_path(), 'cache'), 92 | max_file_size_mb=max_cache_size_mb) 93 | return self._function_cache 94 | 95 | def get_search_history(self): 96 | if not self._search_history: 97 | max_search_history_items = self.get_settings().get_int(constants.setting.SEARCH_SIZE, 50) 98 | self._search_history = SearchHistory(os.path.join(self.get_cache_path(), 'search'), 99 | max_search_history_items) 100 | return self._search_history 101 | 102 | def get_favorite_list(self): 103 | if not self._favorite_list: 104 | self._favorite_list = FavoriteList(os.path.join(self.get_cache_path(), 'favorites')) 105 | return self._favorite_list 106 | 107 | def get_watch_later_list(self): 108 | if not self._watch_later_list: 109 | self._watch_later_list = WatchLaterList(os.path.join(self.get_cache_path(), 'watch_later')) 110 | return self._watch_later_list 111 | 112 | def get_access_manager(self): 113 | if not self._access_manager: 114 | self._access_manager = AccessManager(self) 115 | return self._access_manager 116 | 117 | def get_video_playlist(self): 118 | raise NotImplementedError() 119 | 120 | def get_audio_playlist(self): 121 | raise NotImplementedError() 122 | 123 | def get_video_player(self): 124 | raise NotImplementedError() 125 | 126 | def get_audio_player(self): 127 | raise NotImplementedError() 128 | 129 | def get_ui(self): 130 | raise NotImplementedError() 131 | 132 | def get_system_version(self): 133 | if not self._system_version: 134 | self._system_version = SystemVersion(version='', releasename='', appname='') 135 | 136 | return self._system_version 137 | 138 | def create_uri(self, path=u'/', params=None): 139 | if not params: 140 | params = {} 141 | 142 | uri = create_uri_path(path) 143 | if uri: 144 | uri = "%s://%s%s" % ('plugin', str(self._plugin_id), uri) 145 | else: 146 | uri = "%s://%s/" % ('plugin', str(self._plugin_id)) 147 | 148 | if len(params) > 0: 149 | # make a copy of the map 150 | uri_params = {} 151 | uri_params.update(params) 152 | 153 | # encode in utf-8 154 | for param in uri_params: 155 | if isinstance(params[param], int): 156 | params[param] = str(params[param]) 157 | 158 | uri_params[param] = to_utf8(params[param]) 159 | uri = '?'.join([uri, urllib.parse.urlencode(uri_params)]) 160 | 161 | return uri 162 | 163 | def get_path(self): 164 | return self._path 165 | 166 | def set_path(self, value): 167 | self._path = value 168 | 169 | def get_params(self): 170 | return self._params 171 | 172 | def get_param(self, name, default=None): 173 | return self.get_params().get(name, default) 174 | 175 | def set_param(self, name, value): 176 | self._params[name] = value 177 | 178 | def get_data_path(self): 179 | """ 180 | Returns the path for read/write access of files 181 | :return: 182 | """ 183 | raise NotImplementedError() 184 | 185 | def get_native_path(self): 186 | raise NotImplementedError() 187 | 188 | def get_icon(self): 189 | return os.path.join(self.get_native_path(), 'icon.png') 190 | 191 | def get_fanart(self): 192 | return os.path.join(self.get_native_path(), 'fanart.jpg') 193 | 194 | def create_resource_path(self, *args): 195 | path_comps = [] 196 | for arg in args: 197 | path_comps.extend(arg.split('/')) 198 | path = os.path.join(self.get_native_path(), 'resources', *path_comps) 199 | return path 200 | 201 | def get_uri(self): 202 | return self._uri 203 | 204 | def get_name(self): 205 | return self._plugin_name 206 | 207 | def get_version(self): 208 | return self._version 209 | 210 | def get_id(self): 211 | return self._plugin_id 212 | 213 | def get_handle(self): 214 | raise NotImplementedError() 215 | 216 | def get_settings(self): 217 | raise NotImplementedError() 218 | 219 | def localize(self, text_id, default_text=u''): 220 | raise NotImplementedError() 221 | 222 | def set_content_type(self, content_type): 223 | raise NotImplementedError() 224 | 225 | def add_sort_method(self, *sort_methods): 226 | raise NotImplementedError() 227 | 228 | def log(self, text, log_level=logger.NOTICE): 229 | logger.log(text, log_level, self.get_id()) 230 | 231 | def log_warning(self, text): 232 | self.log(text, logger.WARNING) 233 | 234 | def log_error(self, text): 235 | self.log(text, logger.ERROR) 236 | 237 | def log_notice(self, text): 238 | self.log(text, logger.NOTICE) 239 | 240 | def log_debug(self, text): 241 | self.log(text, logger.DEBUG) 242 | 243 | def log_info(self, text): 244 | self.log(text, logger.INFO) 245 | 246 | def clone(self, new_path=None, new_params=None): 247 | raise NotImplementedError() 248 | 249 | def execute(self, command): 250 | raise NotImplementedError() 251 | 252 | def sleep(self, milli_seconds): 253 | raise NotImplementedError() 254 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/utils/datetime_parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import re 12 | import time 13 | from datetime import date, datetime, timedelta 14 | from datetime import time as dt_time 15 | 16 | from six import PY2 17 | from six import text_type 18 | 19 | from ..exceptions import KodionException 20 | 21 | __RE_MATCH_TIME_ONLY__ = re.compile(r'^(?P[0-9]{2})([:]?(?P[0-9]{2})([:]?(?P[0-9]{2}))?)?$') 22 | __RE_MATCH_DATE_ONLY__ = re.compile(r'^(?P[0-9]{4})[-]?(?P[0-9]{2})[-]?(?P[0-9]{2})$') 23 | __RE_MATCH_DATETIME__ = re.compile(r'^(?P[0-9]{4})[-]?(?P[0-9]{2})[-]?(?P[0-9]{2})["T ](?P[0-9]{2})[:]?(?P[0-9]{2})[:]?(?P[0-9]{2})') 24 | __RE_MATCH_PERIOD__ = re.compile(r'P((?P\d+)Y)?((?P\d+)M)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?') 25 | __RE_MATCH_ABBREVIATED__ = re.compile(r'(\w+), (?P\d+) (?P\w+) (?P\d+) (?P\d+):(?P\d+):(?P\d+)') 26 | 27 | now = datetime.now 28 | 29 | 30 | def py2_utf8(text): 31 | result = text 32 | if PY2 and isinstance(result, text_type): 33 | return result.encode('utf-8', 'ignore') 34 | 35 | return result 36 | 37 | 38 | def parse(datetime_string, localize=True): 39 | _utc_to_local = utc_to_local if localize else lambda x: x 40 | 41 | def _to_int(value): 42 | if value is None: 43 | return 0 44 | return int(value) 45 | 46 | # match time only '00:45:10' 47 | time_only_match = __RE_MATCH_TIME_ONLY__.match(datetime_string) 48 | if time_only_match: 49 | return _utc_to_local(datetime.combine(date.today(), 50 | dt_time(hour=_to_int(time_only_match.group('hour')), 51 | minute=_to_int(time_only_match.group('minute')), 52 | second=_to_int(time_only_match.group('second')))) 53 | ).time() 54 | 55 | # match date only '2014-11-08' 56 | date_only_match = __RE_MATCH_DATE_ONLY__.match(datetime_string) 57 | if date_only_match: 58 | return _utc_to_local(date(_to_int(date_only_match.group('year')), 59 | _to_int(date_only_match.group('month')), 60 | _to_int(date_only_match.group('day')))) 61 | 62 | # full date time 63 | date_time_match = __RE_MATCH_DATETIME__.match(datetime_string) 64 | if date_time_match: 65 | return _utc_to_local(datetime(_to_int(date_time_match.group('year')), 66 | _to_int(date_time_match.group('month')), 67 | _to_int(date_time_match.group('day')), 68 | _to_int(date_time_match.group('hour')), 69 | _to_int(date_time_match.group('minute')), 70 | _to_int(date_time_match.group('second')))) 71 | 72 | # period - at the moment we support only hours, minutes and seconds (e.g. videos and audio) 73 | period_match = __RE_MATCH_PERIOD__.match(datetime_string) 74 | if period_match: 75 | return timedelta(hours=_to_int(period_match.group('hours')), 76 | minutes=_to_int(period_match.group('minutes')), 77 | seconds=_to_int(period_match.group('seconds'))) 78 | 79 | # abbreviated match 80 | abbreviated_match = __RE_MATCH_ABBREVIATED__.match(datetime_string) 81 | if abbreviated_match: 82 | month = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'June': 6, 'Jun': 6, 'July': 7, 'Jul': 7, 'Aug': 8, 83 | 'Sept': 9, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} 84 | return _utc_to_local(datetime(year=_to_int(abbreviated_match.group('year')), 85 | month=month[abbreviated_match.group('month')], 86 | day=_to_int(abbreviated_match.group('day')), 87 | hour=_to_int(abbreviated_match.group('hour')), 88 | minute=_to_int(abbreviated_match.group('minute')), 89 | second=_to_int(abbreviated_match.group('second')))) 90 | 91 | raise KodionException("Could not parse iso 8601 timestamp '%s'" % datetime_string) 92 | 93 | 94 | def get_scheduled_start(datetime_object, localize=True): 95 | start_hour = '{:02d}'.format(datetime_object.hour) 96 | start_minute = '{:<02d}'.format(datetime_object.minute) 97 | start_time = ':'.join([start_hour, start_minute]) 98 | start_date = str(datetime_object.date()) 99 | if localize: 100 | now = datetime.now() 101 | else: 102 | now = datetime.utcnow() 103 | start_date = start_date.replace(str(now.year), '').lstrip('-') 104 | start_date = start_date.replace('-'.join(['{:02d}'.format(now.month), '{:02d}'.format(now.day)]), '') 105 | return start_date, start_time 106 | 107 | 108 | local_timezone_offset = None 109 | 110 | 111 | def utc_to_local(dt): 112 | global local_timezone_offset 113 | if local_timezone_offset is None: 114 | now = time.time() 115 | local_timezone_offset = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now) 116 | 117 | return dt + local_timezone_offset 118 | 119 | 120 | def datetime_to_since(context, dt): 121 | now = datetime.now() 122 | diff = now - dt 123 | yesterday = now - timedelta(days=1) 124 | yyesterday = now - timedelta(days=2) 125 | use_yesterday = total_seconds(now - yesterday) > 10800 126 | today = now.date() 127 | tomorrow = today + timedelta(days=1) 128 | seconds = total_seconds(diff) 129 | 130 | if seconds > 0: 131 | if seconds < 60: 132 | return py2_utf8(context.localize('30676')) 133 | elif 60 <= seconds < 120: 134 | return py2_utf8(context.localize('30677')) 135 | elif 120 <= seconds < 3600: 136 | return py2_utf8(context.localize('30678')) 137 | elif 3600 <= seconds < 7200: 138 | return py2_utf8(context.localize('30679')) 139 | elif 7200 <= seconds < 10800: 140 | return py2_utf8(context.localize('30680')) 141 | elif 10800 <= seconds < 14400: 142 | return py2_utf8(context.localize('30681')) 143 | elif use_yesterday and dt.date() == yesterday.date(): 144 | return ' '.join([py2_utf8(context.localize('30682')), context.format_time(dt)]) 145 | elif dt.date() == yyesterday.date(): 146 | return py2_utf8(context.localize('30683')) 147 | elif 5400 <= seconds < 86400: 148 | return ' '.join([py2_utf8(context.localize('30684')), context.format_time(dt)]) 149 | elif 86400 <= seconds < 172800: 150 | return ' '.join([py2_utf8(context.localize('30682')), context.format_time(dt)]) 151 | else: 152 | seconds *= -1 153 | if seconds < 60: 154 | return py2_utf8(context.localize('30691')) 155 | elif 60 <= seconds < 120: 156 | return py2_utf8(context.localize('30692')) 157 | elif 120 <= seconds < 3600: 158 | return py2_utf8(context.localize('30693')) 159 | elif 3600 <= seconds < 7200: 160 | return py2_utf8(context.localize('30694')) 161 | elif 7200 <= seconds < 10800: 162 | return py2_utf8(context.localize('30695')) 163 | elif dt.date() == today: 164 | return ' '.join([py2_utf8(context.localize('30696')), context.format_time(dt)]) 165 | elif dt.date() == tomorrow: 166 | return ' '.join([py2_utf8(context.localize('30697')), context.format_time(dt)]) 167 | 168 | return ' '.join([context.format_date_short(dt), context.format_time(dt)]) 169 | 170 | 171 | def strptime(s, fmt='%Y-%m-%dT%H:%M:%S.%fZ'): 172 | # noinspection PyUnresolvedReferences 173 | 174 | ms_precision = '.' in s[-5:-1] 175 | if fmt == '%Y-%m-%dT%H:%M:%S.%fZ' and not ms_precision: 176 | fmt = '%Y-%m-%dT%H:%M:%SZ' 177 | elif fmt == '%Y-%m-%dT%H:%M:%SZ' and ms_precision: 178 | fmt = '%Y-%m-%dT%H:%M:%S.%fZ' 179 | 180 | import _strptime 181 | try: 182 | time.strptime('01 01 2012', '%d %m %Y') # dummy call 183 | except: 184 | pass 185 | return datetime(*time.strptime(s, fmt)[:6]) 186 | 187 | 188 | def total_seconds(t_delta): # required for python 2.6 which doesn't have datetime.timedelta.total_seconds 189 | return 24 * 60 * 60 * t_delta.days + t_delta.seconds + (t_delta.microseconds // 1000000.) 190 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/abstract_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import sys 12 | 13 | from .. import constants 14 | from ..logger import log_debug 15 | 16 | 17 | class AbstractSettings(object): 18 | def __init__(self): 19 | object.__init__(self) 20 | 21 | def get_string(self, setting_id, default_value=None): 22 | raise NotImplementedError() 23 | 24 | def set_string(self, setting_id, value): 25 | raise NotImplementedError() 26 | 27 | def open_settings(self): 28 | raise NotImplementedError() 29 | 30 | def get_int(self, setting_id, default_value, converter=None): 31 | if not converter: 32 | def converter(x): 33 | return x 34 | 35 | value = self.get_string(setting_id) 36 | if value is None or value == '': 37 | return default_value 38 | 39 | try: 40 | return converter(int(value)) 41 | except Exception as ex: 42 | log_debug("Failed to get setting '%s' as 'int' (%s)" % setting_id, ex.__str__()) 43 | 44 | return default_value 45 | 46 | def set_int(self, setting_id, value): 47 | self.set_string(setting_id, str(value)) 48 | 49 | def set_bool(self, setting_id, value): 50 | if value: 51 | self.set_string(setting_id, 'true') 52 | else: 53 | self.set_string(setting_id, 'false') 54 | 55 | def get_bool(self, setting_id, default_value): 56 | value = self.get_string(setting_id) 57 | if value is None or value == '': 58 | return default_value 59 | 60 | if value != 'false' and value != 'true': 61 | return default_value 62 | 63 | return value == 'true' 64 | 65 | def get_items_per_page(self): 66 | return self.get_int(constants.setting.ITEMS_PER_PAGE, 50, lambda x: (x + 1) * 5) 67 | 68 | def get_video_quality(self, quality_map_override=None): 69 | vq_dict = {0: 240, 70 | 1: 360, 71 | 2: 480, # 576 seems not to work well 72 | 3: 720, 73 | 4: 1080} 74 | 75 | if quality_map_override is not None: 76 | vq_dict = quality_map_override 77 | 78 | vq = self.get_int(constants.setting.VIDEO_QUALITY, 1) 79 | return vq_dict[vq] 80 | 81 | def ask_for_video_quality(self): 82 | return self.get_bool(constants.setting.VIDEO_QUALITY_ASK, False) 83 | 84 | def show_fanart(self): 85 | return self.get_bool(constants.setting.SHOW_FANART, True) 86 | 87 | def get_search_history_size(self): 88 | return self.get_int(constants.setting.SEARCH_SIZE, 50) 89 | 90 | def is_setup_wizard_enabled(self): 91 | return self.get_bool(constants.setting.SETUP_WIZARD, False) 92 | 93 | def is_support_alternative_player_enabled(self): 94 | return self.get_bool(constants.setting.SUPPORT_ALTERNATIVE_PLAYER, False) 95 | 96 | def alternative_player_web_urls(self): 97 | return self.get_bool(constants.setting.ALTERNATIVE_PLAYER_WEB_URLS, False) 98 | 99 | def use_dash(self): 100 | return self.get_bool(constants.setting.USE_DASH, False) 101 | 102 | def subtitle_languages(self): 103 | return self.get_int(constants.setting.SUBTITLE_LANGUAGE, 0) 104 | 105 | def subtitle_download(self): 106 | return self.get_bool(constants.setting.SUBTITLE_DOWNLOAD, False) 107 | 108 | def audio_only(self): 109 | return self.get_bool(constants.setting.AUDIO_ONLY, False) 110 | 111 | def set_subtitle_languages(self, value): 112 | return self.set_int(constants.setting.SUBTITLE_LANGUAGE, value) 113 | 114 | def set_subtitle_download(self, value): 115 | return self.set_bool(constants.setting.SUBTITLE_DOWNLOAD, value) 116 | 117 | def use_thumbnail_size(self): 118 | size = self.get_int(constants.setting.THUMB_SIZE, 0) 119 | sizes = {0: 'medium', 1: 'high'} 120 | return sizes[size] 121 | 122 | def safe_search(self): 123 | index = self.get_int(constants.setting.SAFE_SEARCH, 0) 124 | values = {0: 'moderate', 1: 'none', 2: 'strict'} 125 | return values[index] 126 | 127 | def age_gate(self): 128 | return self.get_bool(constants.setting.AGE_GATE, True) 129 | 130 | def verify_ssl(self): 131 | verify = self.get_bool(constants.setting.VERIFY_SSL, False) 132 | if sys.version_info <= (2, 7, 9): 133 | verify = False 134 | return verify 135 | 136 | def allow_dev_keys(self): 137 | return self.get_bool(constants.setting.ALLOW_DEV_KEYS, False) 138 | 139 | def use_dash_videos(self): 140 | if not self.use_dash(): 141 | return False 142 | return self.get_bool(constants.setting.DASH_VIDEOS, False) 143 | 144 | def include_hdr(self): 145 | if self.get_mpd_quality() == 'mp4': 146 | return False 147 | return self.get_bool(constants.setting.DASH_INCL_HDR, False) 148 | 149 | def use_dash_live_streams(self): 150 | if not self.use_dash(): 151 | return False 152 | return self.get_bool(constants.setting.DASH_LIVE_STREAMS, False) 153 | 154 | def httpd_port(self): 155 | return self.get_int(constants.setting.HTTPD_PORT, 50152) 156 | 157 | def httpd_listen(self): 158 | ip_address = self.get_string(constants.setting.HTTPD_LISTEN, '0.0.0.0') 159 | try: 160 | ip_address = ip_address.strip() 161 | except AttributeError: 162 | pass 163 | if not ip_address: 164 | ip_address = '0.0.0.0' 165 | return ip_address 166 | 167 | def set_httpd_listen(self, value): 168 | return self.set_string(constants.setting.HTTPD_LISTEN, value) 169 | 170 | def httpd_whitelist(self): 171 | return self.get_string(constants.setting.HTTPD_WHITELIST, '') 172 | 173 | def api_config_page(self): 174 | return self.get_bool(constants.setting.API_CONFIG_PAGE, False) 175 | 176 | def get_location(self): 177 | location = self.get_string(constants.setting.LOCATION, '').replace(' ', '').strip() 178 | coords = location.split(',') 179 | latitude = longitude = None 180 | if len(coords) == 2: 181 | try: 182 | latitude = float(coords[0]) 183 | longitude = float(coords[1]) 184 | if latitude > 90.0 or latitude < -90.0: 185 | latitude = None 186 | if longitude > 180.0 or longitude < -180.0: 187 | longitude = None 188 | except ValueError: 189 | latitude = longitude = None 190 | if latitude and longitude: 191 | return '{lat},{long}'.format(lat=latitude, long=longitude) 192 | else: 193 | return '' 194 | 195 | def set_location(self, value): 196 | self.set_string(constants.setting.LOCATION, value) 197 | 198 | def get_location_radius(self): 199 | return ''.join([str(self.get_int(constants.setting.LOCATION_RADIUS, 500)), 'km']) 200 | 201 | def get_play_count_min_percent(self): 202 | return self.get_int(constants.setting.PLAY_COUNT_MIN_PERCENT, 0) 203 | 204 | def use_playback_history(self): 205 | return self.get_bool(constants.setting.USE_PLAYBACK_HISTORY, False) 206 | 207 | @staticmethod 208 | def __get_mpd_quality_map(): 209 | return { 210 | 0: 240, 211 | 1: 360, 212 | 2: 480, 213 | 3: 720, 214 | 4: 1080, 215 | 5: 1440, 216 | 6: 2160, 217 | 7: 4320, 218 | 8: 'mp4', 219 | 9: 'webm' 220 | } 221 | 222 | def get_mpd_quality(self): 223 | quality_map = self.__get_mpd_quality_map() 224 | quality_enum = self.get_int(constants.setting.MPD_QUALITY_SELECTION, 8) 225 | return quality_map.get(quality_enum, 'mp4') 226 | 227 | def mpd_video_qualities(self): 228 | if not self.use_dash_videos(): 229 | return [] 230 | 231 | quality = self.get_mpd_quality() 232 | 233 | if not isinstance(quality, int): 234 | return quality 235 | 236 | quality_map = self.__get_mpd_quality_map() 237 | qualities = sorted([x for x in list(quality_map.values()) 238 | if isinstance(x, int) and x <= quality], reverse=True) 239 | 240 | return qualities 241 | 242 | def mpd_30fps_limit(self): 243 | if self.include_hdr() or isinstance(self.get_mpd_quality(), str): 244 | return False 245 | return self.get_bool(constants.setting.MPD_30FPS_LIMIT, False) 246 | 247 | def remote_friendly_search(self): 248 | return self.get_bool(constants.setting.REMOTE_FRIENDLY_SEARCH, False) 249 | -------------------------------------------------------------------------------- /resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2014-2016 bromix (plugin.video.youtube) 5 | Copyright (C) 2016-2018 plugin.video.youtube 6 | 7 | SPDX-License-Identifier: GPL-2.0-only 8 | See LICENSES/GPL-2.0-only for more information. 9 | """ 10 | 11 | import xbmcgui 12 | 13 | from ...items import VideoItem, AudioItem, UriItem 14 | from ... import utils 15 | from . import info_labels 16 | 17 | 18 | def to_play_item(context, play_item): 19 | context.log_debug('Converting PlayItem |%s|' % play_item.get_uri()) 20 | 21 | major_version = context.get_system_version().get_version()[0] 22 | 23 | is_strm = str(context.get_param('strm', False)).lower() == 'true' and major_version >= 18 24 | 25 | thumb = play_item.get_image() if play_item.get_image() else u'DefaultVideo.png' 26 | title = play_item.get_title() if play_item.get_title() else play_item.get_name() 27 | fanart = '' 28 | settings = context.get_settings() 29 | if is_strm: 30 | list_item = xbmcgui.ListItem(offscreen=True) 31 | elif major_version > 17: 32 | list_item = xbmcgui.ListItem(label=utils.to_unicode(title), offscreen=True) 33 | else: 34 | list_item = xbmcgui.ListItem(label=utils.to_unicode(title)) 35 | 36 | if not is_strm: 37 | list_item.setProperty('IsPlayable', 'true') 38 | 39 | if play_item.get_fanart() and settings.show_fanart(): 40 | fanart = play_item.get_fanart() 41 | if major_version <= 15: 42 | list_item.setArt({'thumb': thumb, 'fanart': fanart}) 43 | list_item.setIconImage(thumb) 44 | else: 45 | list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) 46 | 47 | if not play_item.use_dash() and not settings.is_support_alternative_player_enabled() and \ 48 | play_item.get_headers() and play_item.get_uri().startswith('http'): 49 | play_item.set_uri('|'.join([play_item.get_uri(), play_item.get_headers()])) 50 | 51 | if settings.is_support_alternative_player_enabled() and \ 52 | settings.alternative_player_web_urls() and \ 53 | not play_item.get_license_key(): 54 | play_item.set_uri('https://www.youtube.com/watch?v={video_id}'.format(video_id=play_item.video_id)) 55 | 56 | if play_item.use_dash() and context.addon_enabled('inputstream.adaptive'): 57 | inputstream_property = 'inputstream' 58 | if major_version < 19: 59 | inputstream_property += 'addon' 60 | 61 | list_item.setContentLookup(False) 62 | list_item.setMimeType('application/xml+dash') 63 | list_item.setProperty(inputstream_property, 'inputstream.adaptive') 64 | list_item.setProperty('inputstream.adaptive.manifest_type', 'mpd') 65 | if play_item.get_headers(): 66 | list_item.setProperty('inputstream.adaptive.stream_headers', play_item.get_headers()) 67 | 68 | if play_item.get_license_key(): 69 | list_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha') 70 | list_item.setProperty('inputstream.adaptive.license_key', play_item.get_license_key()) 71 | 72 | if not is_strm: 73 | if play_item.get_play_count() == 0: 74 | if play_item.get_start_percent(): 75 | list_item.setProperty('StartPercent', play_item.get_start_percent()) 76 | 77 | if play_item.get_start_time(): 78 | list_item.setProperty('StartOffset', play_item.get_start_time()) 79 | 80 | if play_item.subtitles: 81 | list_item.setSubtitles(play_item.subtitles) 82 | 83 | _info_labels = info_labels.create_from_item(play_item) 84 | 85 | # This should work for all versions of XBMC/KODI. 86 | if 'duration' in _info_labels: 87 | duration = _info_labels['duration'] 88 | del _info_labels['duration'] 89 | list_item.addStreamInfo('video', {'duration': duration}) 90 | 91 | list_item.setInfo(type='video', infoLabels=_info_labels) 92 | return list_item 93 | 94 | 95 | def to_video_item(context, video_item): 96 | context.log_debug('Converting VideoItem |%s|' % video_item.get_uri()) 97 | major_version = context.get_system_version().get_version()[0] 98 | thumb = video_item.get_image() if video_item.get_image() else u'DefaultVideo.png' 99 | title = video_item.get_title() if video_item.get_title() else video_item.get_name() 100 | fanart = '' 101 | settings = context.get_settings() 102 | if major_version > 17: 103 | item = xbmcgui.ListItem(label=utils.to_unicode(title), offscreen=True) 104 | else: 105 | item = xbmcgui.ListItem(label=utils.to_unicode(title)) 106 | if video_item.get_fanart() and settings.show_fanart(): 107 | fanart = video_item.get_fanart() 108 | if major_version <= 15: 109 | item.setArt({'thumb': thumb, 'fanart': fanart}) 110 | item.setIconImage(thumb) 111 | else: 112 | item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) 113 | 114 | if video_item.get_context_menu() is not None: 115 | item.addContextMenuItems(video_item.get_context_menu(), replaceItems=video_item.replace_context_menu()) 116 | 117 | item.setProperty('IsPlayable', 'true') 118 | 119 | if not video_item.live: 120 | published_at = video_item.get_aired_utc() 121 | scheduled_start = video_item.get_scheduled_start_utc() 122 | use_dt = scheduled_start or published_at 123 | if use_dt: 124 | local_dt = utils.datetime_parser.utc_to_local(use_dt) 125 | item.setProperty('PublishedSince', 126 | utils.to_unicode(utils.datetime_parser.datetime_to_since(context, local_dt))) 127 | item.setProperty('PublishedLocal', str(local_dt)) 128 | else: 129 | item.setProperty('PublishedSince', context.localize('30539')) 130 | 131 | _info_labels = info_labels.create_from_item(video_item) 132 | 133 | if video_item.get_play_count() == 0: 134 | if video_item.get_start_percent(): 135 | item.setProperty('StartPercent', video_item.get_start_percent()) 136 | 137 | if video_item.get_start_time(): 138 | item.setProperty('StartOffset', video_item.get_start_time()) 139 | 140 | # This should work for all versions of XBMC/KODI. 141 | if 'duration' in _info_labels: 142 | duration = _info_labels['duration'] 143 | del _info_labels['duration'] 144 | item.addStreamInfo('video', {'duration': duration}) 145 | 146 | item.setInfo(type='video', infoLabels=_info_labels) 147 | 148 | if video_item.get_channel_id(): # make channel_id property available for keymapping 149 | item.setProperty('channel_id', video_item.get_channel_id()) 150 | 151 | if video_item.get_subscription_id(): # make subscription_id property available for keymapping 152 | item.setProperty('subscription_id', video_item.get_subscription_id()) 153 | 154 | if video_item.get_playlist_id(): # make playlist_id property available for keymapping 155 | item.setProperty('playlist_id', video_item.get_playlist_id()) 156 | 157 | if video_item.get_playlist_item_id(): # make playlist_item_id property available for keymapping 158 | item.setProperty('playlist_item_id', video_item.get_playlist_item_id()) 159 | 160 | return item 161 | 162 | 163 | def to_audio_item(context, audio_item): 164 | context.log_debug('Converting AudioItem |%s|' % audio_item.get_uri()) 165 | major_version = context.get_system_version().get_version()[0] 166 | thumb = audio_item.get_image() if audio_item.get_image() else u'DefaultAudio.png' 167 | title = audio_item.get_name() 168 | fanart = '' 169 | settings = context.get_settings() 170 | if major_version > 17: 171 | item = xbmcgui.ListItem(label=utils.to_unicode(title), offscreen=True) 172 | else: 173 | item = xbmcgui.ListItem(label=utils.to_unicode(title)) 174 | if audio_item.get_fanart() and settings.show_fanart(): 175 | fanart = audio_item.get_fanart() 176 | if major_version <= 15: 177 | item.setArt({'thumb': thumb, 'fanart': fanart}) 178 | item.setIconImage(thumb) 179 | else: 180 | item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) 181 | 182 | if audio_item.get_context_menu() is not None: 183 | item.addContextMenuItems(audio_item.get_context_menu(), replaceItems=audio_item.replace_context_menu()) 184 | 185 | item.setProperty('IsPlayable', 'true') 186 | 187 | item.setInfo(type='music', infoLabels=info_labels.create_from_item(audio_item)) 188 | return item 189 | 190 | 191 | def to_uri_item(context, base_item): 192 | context.log_debug('Converting UriItem') 193 | major_version = context.get_system_version().get_version()[0] 194 | if major_version > 17: 195 | item = xbmcgui.ListItem(path=base_item.get_uri(), offscreen=True) 196 | else: 197 | item = xbmcgui.ListItem(path=base_item.get_uri()) 198 | item.setProperty('IsPlayable', 'true') 199 | return item 200 | 201 | 202 | def to_playback_item(context, base_item): 203 | if isinstance(base_item, UriItem): 204 | return to_uri_item(context, base_item) 205 | 206 | if isinstance(base_item, AudioItem): 207 | return to_audio_item(context, base_item) 208 | 209 | if isinstance(base_item, VideoItem): 210 | return to_play_item(context, base_item) 211 | 212 | return None 213 | --------------------------------------------------------------------------------