mirror of
https://github.com/anxdpanic/plugin.video.youtube.git
synced 2025-12-06 02:30:50 -08:00
Merge pull request #1317 from MoojMidge/nexus-unofficial
Nexus unofficial v7.3.0+beta.7
This commit is contained in:
commit
174727a6ea
32 changed files with 648 additions and 476 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.youtube" name="YouTube" version="7.3.0+beta.6" provider-name="anxdpanic, bromix, MoojMidge">
|
||||
<addon id="plugin.video.youtube" name="YouTube" version="7.3.0+beta.7" provider-name="anxdpanic, bromix, MoojMidge">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
<import addon="script.module.requests" version="2.27.1"/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,27 @@
|
|||
## v7.3.0+beta.7
|
||||
### Fixed
|
||||
- Only add playable items to playlist when adding related items
|
||||
- Fix using invalid default end limit with Playlist.GetItems JSONRPC method
|
||||
- Fix conversion of SRT subtitles to WebVTT #1256
|
||||
- Workaround playback failure of progressive streams
|
||||
- Fix re-sorting live search lists
|
||||
- Disable use of custom thumbnail urls #1245
|
||||
- Workaround addon service not starting prior to plugin invocation #1298
|
||||
- Fix unofficial version using localised sort order #1309
|
||||
- Fix parsing of logged_in query parameter
|
||||
|
||||
### Changed
|
||||
- Ignore player request failures that may incorrectly indicate a need to sign-in #1312
|
||||
- Include playlist_id listitem property for items from virtual playlists
|
||||
|
||||
### New
|
||||
- Add refresh to context menu of playlists
|
||||
- Allow watch urls from music.youtube.com to be directly handled by the addon
|
||||
- Allow urls from www.youtubekids.com to be directly handled by the addon
|
||||
|
||||
## v7.3.0+beta.6
|
||||
### Fixed
|
||||
Fix typo in YouTubePlayerClient error hook
|
||||
- Fix typo in YouTubePlayerClient error hook
|
||||
|
||||
## v7.3.0+beta.5
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: XBMC-Addons\n"
|
||||
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
|
||||
"Report-Msgid-Bugs-To: translations@kodi.tv\n"
|
||||
"POT-Creation-Date: 2015-09-21 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-08-22 19:29+0000\n"
|
||||
"PO-Revision-Date: 2025-10-11 06:29+0000\n"
|
||||
"Last-Translator: Massimo Pissarello <mapi68@gmail.com>\n"
|
||||
"Language-Team: Italian <https://kodi.weblate.cloud/projects/kodi-add-ons-video/plugin-video-youtube/it_it/>\n"
|
||||
"Language: it_it\n"
|
||||
|
|
@ -16,7 +16,7 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.13\n"
|
||||
"X-Generator: Weblate 5.13.3\n"
|
||||
|
||||
msgctxt "Addon Summary"
|
||||
msgid "Plugin for YouTube"
|
||||
|
|
@ -238,7 +238,7 @@ msgstr "Disconnessione"
|
|||
|
||||
msgctxt "#30113"
|
||||
msgid "Related to \"%s\""
|
||||
msgstr ""
|
||||
msgstr "Correlato a \"%s\""
|
||||
|
||||
msgctxt "#30114"
|
||||
msgid "Confirm delete"
|
||||
|
|
@ -258,7 +258,7 @@ msgstr "Rimuovere \"%s\"?"
|
|||
|
||||
msgctxt "#30118"
|
||||
msgid "Links from \"%s\""
|
||||
msgstr ""
|
||||
msgstr "Link da \"%s\""
|
||||
|
||||
msgctxt "#30119"
|
||||
msgid "Please wait..."
|
||||
|
|
@ -490,7 +490,7 @@ msgstr "Completa tutte le richieste di accesso"
|
|||
|
||||
msgctxt "#30547"
|
||||
msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly."
|
||||
msgstr ""
|
||||
msgstr "Potrebbe esserti richiesto di effettuare l'accesso e di abilitare l'accesso a più applicazioni affinché questo add-on possa funzionare correttamente."
|
||||
|
||||
msgctxt "#30548"
|
||||
msgid ""
|
||||
|
|
@ -650,19 +650,19 @@ msgstr "Blacklist"
|
|||
|
||||
msgctxt "#30587"
|
||||
msgid "Hide \"Playlists\" folder"
|
||||
msgstr ""
|
||||
msgstr "Nascondi cartella \"Playlist\""
|
||||
|
||||
msgctxt "#30588"
|
||||
msgid "Hide \"Search\" folder"
|
||||
msgstr ""
|
||||
msgstr "Nascondi cartella \"Ricerca\""
|
||||
|
||||
msgctxt "#30589"
|
||||
msgid "Hide \"Shorts\" folder"
|
||||
msgstr ""
|
||||
msgstr "Nascondi cartella \"Video brevi\""
|
||||
|
||||
msgctxt "#30590"
|
||||
msgid "Hide \"Live\" folder"
|
||||
msgstr ""
|
||||
msgstr "Nascondi cartella \"Live\""
|
||||
|
||||
msgctxt "#30591"
|
||||
msgid "Thumbnail size"
|
||||
|
|
@ -690,7 +690,7 @@ msgstr "Severo"
|
|||
|
||||
msgctxt "#30597"
|
||||
msgid "Hide \"Members only\" folder"
|
||||
msgstr ""
|
||||
msgstr "Nascondi cartella \"Solo per i membri\""
|
||||
|
||||
msgctxt "#30598"
|
||||
msgid "Large (4:3)"
|
||||
|
|
@ -798,7 +798,7 @@ msgstr "Installa InputStream Helper"
|
|||
|
||||
msgctxt "#30624"
|
||||
msgid "Members only"
|
||||
msgstr ""
|
||||
msgstr "Solo per i membri"
|
||||
|
||||
msgctxt "#30625"
|
||||
msgid "InputStream Helper is already installed."
|
||||
|
|
@ -1534,7 +1534,7 @@ msgstr "Usa nome del canale come"
|
|||
|
||||
msgctxt "#30808"
|
||||
msgid "Hide items from listings"
|
||||
msgstr ""
|
||||
msgstr "Nascondi elementi dagli elenchi"
|
||||
|
||||
msgctxt "#30809"
|
||||
msgid "All upcoming videos"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ msgstr ""
|
|||
"Project-Id-Version: XBMC-Addons\n"
|
||||
"Report-Msgid-Bugs-To: translations@kodi.tv\n"
|
||||
"POT-Creation-Date: 2015-09-21 11:01+0000\n"
|
||||
"PO-Revision-Date: 2025-10-04 08:29+0000\n"
|
||||
"Last-Translator: Alexey <signfinder@gmail.com>\n"
|
||||
"PO-Revision-Date: 2025-10-11 06:29+0000\n"
|
||||
"Last-Translator: Dmitry Petrov <dimakrm361@gmail.com>\n"
|
||||
"Language-Team: Russian <https://kodi.weblate.cloud/projects/kodi-add-ons-video/plugin-video-youtube/ru_ru/>\n"
|
||||
"Language: ru_ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
|
@ -265,11 +265,11 @@ msgstr "Пожалуйста, подождите..."
|
|||
|
||||
msgctxt "#30120"
|
||||
msgid "Confirm clear"
|
||||
msgstr ""
|
||||
msgstr "Подтвердить отчиску"
|
||||
|
||||
msgctxt "#30121"
|
||||
msgid "Clear %s?"
|
||||
msgstr ""
|
||||
msgstr "Очистить %s?"
|
||||
|
||||
# YouTube
|
||||
# empty strings from id 30121 to 30199
|
||||
|
|
@ -385,15 +385,15 @@ msgstr "Добавить в..."
|
|||
|
||||
msgctxt "#30521"
|
||||
msgid "Delete requests cache database"
|
||||
msgstr ""
|
||||
msgstr "Удалить базу данных с кешом запросов"
|
||||
|
||||
msgctxt "#30522"
|
||||
msgid "Clear requests cache"
|
||||
msgstr ""
|
||||
msgstr "Удалить кеш запросов"
|
||||
|
||||
msgctxt "#30523"
|
||||
msgid "requests cache"
|
||||
msgstr ""
|
||||
msgstr "запросов в кеше"
|
||||
|
||||
msgctxt "#30524"
|
||||
msgid "Select language"
|
||||
|
|
@ -405,11 +405,11 @@ msgstr "Выберите регион"
|
|||
|
||||
msgctxt "#30526"
|
||||
msgid "Setup Wizard"
|
||||
msgstr ""
|
||||
msgstr "Мастер настройки"
|
||||
|
||||
msgctxt "#30527"
|
||||
msgid "Language and Region"
|
||||
msgstr ""
|
||||
msgstr "Язык и регион"
|
||||
|
||||
msgctxt "#30528"
|
||||
msgid "Rate..."
|
||||
|
|
@ -457,7 +457,7 @@ msgstr "Непонравившееся видео"
|
|||
|
||||
msgctxt "#30539"
|
||||
msgid "Play recently added"
|
||||
msgstr ""
|
||||
msgstr "Воспроизвести недавно добавленное"
|
||||
|
||||
msgctxt "#30540"
|
||||
msgid ""
|
||||
|
|
@ -465,7 +465,7 @@ msgstr ""
|
|||
|
||||
msgctxt "#30541"
|
||||
msgid "Show channel name and video details in description"
|
||||
msgstr ""
|
||||
msgstr "Отображать название канала и детали видео в описании"
|
||||
|
||||
msgctxt "#30542"
|
||||
msgid "rtmpe streams are not supported"
|
||||
|
|
@ -481,15 +481,15 @@ msgstr "Больше ссылок из описания"
|
|||
|
||||
msgctxt "#30545"
|
||||
msgid "No videos found."
|
||||
msgstr ""
|
||||
msgstr "Видео не найдено."
|
||||
|
||||
msgctxt "#30546"
|
||||
msgid "Please complete all login prompts"
|
||||
msgstr ""
|
||||
msgstr "Пожалуйста заполните все формы авторизации"
|
||||
|
||||
msgctxt "#30547"
|
||||
msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly."
|
||||
msgstr ""
|
||||
msgstr "Вас могут запросить войти и дать доступ нескольким приложениям, так дополнение сможет функционировать корректно."
|
||||
|
||||
msgctxt "#30548"
|
||||
msgid ""
|
||||
|
|
@ -497,7 +497,7 @@ msgstr ""
|
|||
|
||||
msgctxt "#30549"
|
||||
msgid "No streams found"
|
||||
msgstr ""
|
||||
msgstr "Трансляции не найдены"
|
||||
|
||||
msgctxt "#30550"
|
||||
msgid ""
|
||||
|
|
@ -549,15 +549,15 @@ msgstr ""
|
|||
|
||||
msgctxt "#30562"
|
||||
msgid "View all"
|
||||
msgstr ""
|
||||
msgstr "Посмотреть всё"
|
||||
|
||||
msgctxt "#30563"
|
||||
msgid "Plugin execution timeout"
|
||||
msgstr ""
|
||||
msgstr "Время выполнения плагина"
|
||||
|
||||
msgctxt "#30564"
|
||||
msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable."
|
||||
msgstr ""
|
||||
msgstr "Для целей тестирования - принудительно остановить выполнение плагина через заданное время. Установите на 0 секунд (по-умолчанию) для выключения."
|
||||
|
||||
msgctxt "#30565"
|
||||
msgid ""
|
||||
|
|
@ -577,11 +577,11 @@ msgstr "Удалить из списка Отложенного просмотр
|
|||
|
||||
msgctxt "#30569"
|
||||
msgid "Are you sure you want to remove \"%s\" as your Watch Later list?"
|
||||
msgstr ""
|
||||
msgstr "Вы уверены что хотите убрать \"%s\" из списка \"Посмотреть позже\"?"
|
||||
|
||||
msgctxt "#30570"
|
||||
msgid "Are you sure you want to replace your current Watch Later list with \"%s\"?"
|
||||
msgstr ""
|
||||
msgstr "Вы уверены что хотите заменить список \"Посмотреть позже\" с \"%s\"?"
|
||||
|
||||
msgctxt "#30571"
|
||||
msgid "Set as History"
|
||||
|
|
@ -609,7 +609,7 @@ msgstr "Ошибка"
|
|||
|
||||
msgctxt "#30577"
|
||||
msgid "Smallest (4:3)"
|
||||
msgstr ""
|
||||
msgstr "Наименьшее (4:3)"
|
||||
|
||||
msgctxt "#30578"
|
||||
msgid "Force SSL certificate verification"
|
||||
|
|
@ -617,7 +617,7 @@ msgstr "Принудительная проверка SSL сертификато
|
|||
|
||||
msgctxt "#30579"
|
||||
msgid "InputStream.Adaptive is activated in the YouTube settings, however the add-on has been disabled. Would you like to enable InputStream.Adaptive now?"
|
||||
msgstr ""
|
||||
msgstr "InputStream.Adaptive активирован в настройках YouTube, но дополнение выключено. Хотите включить InputStream.Adaptive сейчас?"
|
||||
|
||||
msgctxt "#30580"
|
||||
msgid "Reset access manager"
|
||||
|
|
@ -649,19 +649,19 @@ msgstr "Черный список"
|
|||
|
||||
msgctxt "#30587"
|
||||
msgid "Hide \"Playlists\" folder"
|
||||
msgstr ""
|
||||
msgstr "Скрыть папку \"Плейлисты\""
|
||||
|
||||
msgctxt "#30588"
|
||||
msgid "Hide \"Search\" folder"
|
||||
msgstr ""
|
||||
msgstr "Скрыть папку \"Поиск\""
|
||||
|
||||
msgctxt "#30589"
|
||||
msgid "Hide \"Shorts\" folder"
|
||||
msgstr ""
|
||||
msgstr "Скрыть папку \"Shorts\""
|
||||
|
||||
msgctxt "#30590"
|
||||
msgid "Hide \"Live\" folder"
|
||||
msgstr ""
|
||||
msgstr "Скрыть папку \"Прямой эфир\""
|
||||
|
||||
msgctxt "#30591"
|
||||
msgid "Thumbnail size"
|
||||
|
|
@ -669,11 +669,11 @@ msgstr "Размер иконок"
|
|||
|
||||
msgctxt "#30592"
|
||||
msgid "Small (16:9)"
|
||||
msgstr ""
|
||||
msgstr "Маленький (16:9)"
|
||||
|
||||
msgctxt "#30593"
|
||||
msgid "Medium (4:3)"
|
||||
msgstr ""
|
||||
msgstr "Средний (4:3)"
|
||||
|
||||
msgctxt "#30594"
|
||||
msgid "Safe search"
|
||||
|
|
@ -689,11 +689,11 @@ msgstr "Строгий"
|
|||
|
||||
msgctxt "#30597"
|
||||
msgid "Hide \"Members only\" folder"
|
||||
msgstr ""
|
||||
msgstr "Скрыть папку \"Только для платных подписчиков\""
|
||||
|
||||
msgctxt "#30598"
|
||||
msgid "Large (4:3)"
|
||||
msgstr ""
|
||||
msgstr "Большой (4:3)"
|
||||
|
||||
msgctxt "#30599"
|
||||
msgid "Failed to enable personal API keys. Missing: %s"
|
||||
|
|
@ -701,7 +701,7 @@ msgstr "Ошибка включения персонального API ключ
|
|||
|
||||
msgctxt "#30600"
|
||||
msgid "Largest (16:9)"
|
||||
msgstr ""
|
||||
msgstr "Крупный (16:9)"
|
||||
|
||||
msgctxt "#30601"
|
||||
msgid "%s with Original/%s fallback"
|
||||
|
|
@ -753,31 +753,31 @@ msgstr "Повтор"
|
|||
|
||||
msgctxt "#30613"
|
||||
msgid "Add to %s"
|
||||
msgstr ""
|
||||
msgstr "Добавить в %s"
|
||||
|
||||
msgctxt "#30614"
|
||||
msgid "Remove from %s"
|
||||
msgstr ""
|
||||
msgstr "Убрать из %s"
|
||||
|
||||
msgctxt "#30615"
|
||||
msgid "Added to %s"
|
||||
msgstr ""
|
||||
msgstr "Добавлено в %s"
|
||||
|
||||
msgctxt "#30616"
|
||||
msgid "Removed from %s"
|
||||
msgstr ""
|
||||
msgstr "Убрано из %s"
|
||||
|
||||
msgctxt "#30617"
|
||||
msgid "InputStream.Adaptive"
|
||||
msgstr ""
|
||||
msgstr "InputStream.Adaptive"
|
||||
|
||||
msgctxt "#30618"
|
||||
msgid "Stream redirect"
|
||||
msgstr ""
|
||||
msgstr "Переадресация потока"
|
||||
|
||||
msgctxt "#30619"
|
||||
msgid "Enable to reduce resource usage on less powerful devices, but could lead to IP bans. Use at own risk."
|
||||
msgstr ""
|
||||
msgstr "Включить для снижения потребления ресурсов в маломощных устройствах, но это может привести к блокировке по IP. Используйте на свой страх и риск."
|
||||
|
||||
msgctxt "#30620"
|
||||
msgid "Port %s already in use. Cannot start http server."
|
||||
|
|
@ -797,7 +797,7 @@ msgstr "Установить InputStream Helper"
|
|||
|
||||
msgctxt "#30624"
|
||||
msgid "Members only"
|
||||
msgstr ""
|
||||
msgstr "Только для платных подписчиков"
|
||||
|
||||
msgctxt "#30625"
|
||||
msgid "InputStream Helper is already installed."
|
||||
|
|
@ -893,15 +893,15 @@ msgstr "Завершенный прямой эфир"
|
|||
|
||||
msgctxt "#30648"
|
||||
msgid "API Key is incorrect. Settings > API > API Key"
|
||||
msgstr ""
|
||||
msgstr "Неверный API ключ. Настройки > API > Ключ API (Key)"
|
||||
|
||||
msgctxt "#30649"
|
||||
msgid "Client Id is incorrect. Settings > API > API Id"
|
||||
msgstr ""
|
||||
msgstr "Неверный идентификатор клиента. Настройки > API >Идентификатор клиента (API Id)"
|
||||
|
||||
msgctxt "#30650"
|
||||
msgid "Client Secret is incorrect. Settings > API > API Secret"
|
||||
msgstr ""
|
||||
msgstr "Неверный секретный код клиента. Настройки > API > Секретный код клиента (API Secret)"
|
||||
|
||||
msgctxt "#30651"
|
||||
msgid "Location"
|
||||
|
|
@ -937,7 +937,7 @@ msgstr "Введите имя пользователя"
|
|||
|
||||
msgctxt "#30659"
|
||||
msgid "User is now \"%s\""
|
||||
msgstr ""
|
||||
msgstr "Выбран пользователь \"%s\""
|
||||
|
||||
msgctxt "#30660"
|
||||
msgid "Users"
|
||||
|
|
@ -961,15 +961,15 @@ msgstr "Смена пользователя"
|
|||
|
||||
msgctxt "#30665"
|
||||
msgid "Switch to \"%s\" now?"
|
||||
msgstr ""
|
||||
msgstr "Переключить на \"%s\" сейчас?"
|
||||
|
||||
msgctxt "#30666"
|
||||
msgid "\"%s\" removed"
|
||||
msgstr ""
|
||||
msgstr "\"%s\" удален"
|
||||
|
||||
msgctxt "#30667"
|
||||
msgid "Renamed \"%s\" to \"%s\""
|
||||
msgstr ""
|
||||
msgstr "\"%s\" переименован на \"%s\""
|
||||
|
||||
msgctxt "#30668"
|
||||
msgid "Play count minimum percent"
|
||||
|
|
@ -977,11 +977,11 @@ msgstr "Минимальный процент отметки просмотра"
|
|||
|
||||
msgctxt "#30669"
|
||||
msgid "%s removed"
|
||||
msgstr ""
|
||||
msgstr "%s удален"
|
||||
|
||||
msgctxt "#30670"
|
||||
msgid "Added %s"
|
||||
msgstr ""
|
||||
msgstr "Добавлен %s"
|
||||
|
||||
msgctxt "#30671"
|
||||
msgid "Clear playback history"
|
||||
|
|
@ -989,7 +989,7 @@ msgstr "Очистить историю просмотра"
|
|||
|
||||
msgctxt "#30672"
|
||||
msgid "Delete playback history database"
|
||||
msgstr ""
|
||||
msgstr "Удалить историю просмотров"
|
||||
|
||||
msgctxt "#30673"
|
||||
msgid "playback history"
|
||||
|
|
@ -1001,7 +1001,7 @@ msgstr ""
|
|||
|
||||
msgctxt "#30675"
|
||||
msgid "Use local playback history (watched, resume tracking)"
|
||||
msgstr ""
|
||||
msgstr "Использовать локальную историю просмотров (просмотренные, трекер времени)"
|
||||
|
||||
msgctxt "#30676"
|
||||
msgid "Just now"
|
||||
|
|
@ -1053,7 +1053,7 @@ msgstr "кеш данных"
|
|||
|
||||
msgctxt "#30688"
|
||||
msgid "Use MPEG-DASH for videos"
|
||||
msgstr ""
|
||||
msgstr "Использовать MPEG-DASH для видео"
|
||||
|
||||
msgctxt "#30689"
|
||||
msgid "Use for live streams"
|
||||
|
|
@ -1061,7 +1061,7 @@ msgstr "Использовать для прямого эфира"
|
|||
|
||||
msgctxt "#30690"
|
||||
msgid "InputStream.Adaptive >= 2.0.12 is required for adaptive live streams"
|
||||
msgstr ""
|
||||
msgstr "Требуется InputStream.Adaptive >= 2.0.12 для прямых трансляций"
|
||||
|
||||
msgctxt "#30691"
|
||||
msgid "Airing now"
|
||||
|
|
@ -1117,7 +1117,7 @@ msgstr ""
|
|||
|
||||
msgctxt "#30704"
|
||||
msgid "Use YouTube website urls with default player"
|
||||
msgstr ""
|
||||
msgstr "Использовать ссылки YouTube с проигрывателем по-умолчанию"
|
||||
|
||||
msgctxt "#30705"
|
||||
msgid "Download subtitles"
|
||||
|
|
@ -1137,11 +1137,11 @@ msgstr "Воспроизвести аудио"
|
|||
|
||||
msgctxt "#30709"
|
||||
msgid "WebVTT subtitles"
|
||||
msgstr ""
|
||||
msgstr "WebVTT субтитры"
|
||||
|
||||
msgctxt "#30710"
|
||||
msgid "TTML subtitles"
|
||||
msgstr ""
|
||||
msgstr "TTML субтитры"
|
||||
|
||||
msgctxt "#30711"
|
||||
msgid ""
|
||||
|
|
@ -1153,11 +1153,11 @@ msgstr "Оценивать видео в плейлистах"
|
|||
|
||||
msgctxt "#30713"
|
||||
msgid "Prefer dubbed audio over original audio"
|
||||
msgstr ""
|
||||
msgstr "Предпочитать дубляж аудио вместо оригинала"
|
||||
|
||||
msgctxt "#30714"
|
||||
msgid "Prefer automatically translated dubbed audio over original audio"
|
||||
msgstr ""
|
||||
msgstr "Предпочитать автоматически переведенное аудио дубляж вместо оригинального аудио"
|
||||
|
||||
msgctxt "#30715"
|
||||
msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube."
|
||||
|
|
@ -1185,19 +1185,19 @@ msgstr "Отписан от канала"
|
|||
|
||||
msgctxt "#30721"
|
||||
msgid "Enable Panoramic/180/360/VR video"
|
||||
msgstr ""
|
||||
msgstr "Включить Панорамные/180/360/VR видео"
|
||||
|
||||
msgctxt "#30722"
|
||||
msgid "Enable HDR video"
|
||||
msgstr ""
|
||||
msgstr "Включить HDR видео"
|
||||
|
||||
msgctxt "#30723"
|
||||
msgid "Proxy is required for MPEG-DASH VODs (see Advanced > HTTP Server)[CR]HDR and >1080p video requires InputStream.Adaptive >= 2.3.14"
|
||||
msgstr ""
|
||||
msgstr "Требуется прокси для MPEG-DASH VODs (смотрите HTTP сервер)[CR]HDR >1080p видео требуют nputStream.Adaptive >= 2.3.14"
|
||||
|
||||
msgctxt "#30724"
|
||||
msgid "Enable high framerate video"
|
||||
msgstr ""
|
||||
msgstr "Включить видео с высокой частотой кадров"
|
||||
|
||||
msgctxt "#30725"
|
||||
msgid "1440p (QHD)"
|
||||
|
|
@ -1209,15 +1209,15 @@ msgstr "Загрузки"
|
|||
|
||||
msgctxt "#30727"
|
||||
msgid "Enable H.264 video"
|
||||
msgstr ""
|
||||
msgstr "Включить H.264 видео"
|
||||
|
||||
msgctxt "#30728"
|
||||
msgid "Enable VP9 video"
|
||||
msgstr ""
|
||||
msgstr "Включить VP9 видео"
|
||||
|
||||
msgctxt "#30729"
|
||||
msgid "Prefer lower resolution streams for unselected codecs"
|
||||
msgstr ""
|
||||
msgstr "Предпочитать потоки в низком разрешении для не выбранных кодеков"
|
||||
|
||||
msgctxt "#30730"
|
||||
msgid "Play (Ask for quality)"
|
||||
|
|
@ -1245,43 +1245,43 @@ msgstr "Изменен"
|
|||
|
||||
msgctxt "#30736"
|
||||
msgid "Shorts"
|
||||
msgstr ""
|
||||
msgstr "Shorts"
|
||||
|
||||
msgctxt "#30737"
|
||||
msgid "Shorts - Max duration"
|
||||
msgstr ""
|
||||
msgstr "Shorts - Максимальная длительность"
|
||||
|
||||
msgctxt "#30738"
|
||||
msgid "Enable spatial audio"
|
||||
msgstr ""
|
||||
msgstr "Включить пространственный звук"
|
||||
|
||||
msgctxt "#30739"
|
||||
msgid "Subscribers"
|
||||
msgstr ""
|
||||
msgstr "Подписчики"
|
||||
|
||||
msgctxt "#30740"
|
||||
msgid "HLS"
|
||||
msgstr ""
|
||||
msgstr "HLS"
|
||||
|
||||
msgctxt "#30741"
|
||||
msgid "Multi-stream HLS"
|
||||
msgstr ""
|
||||
msgstr "Многопоточный HLS"
|
||||
|
||||
msgctxt "#30742"
|
||||
msgid "Adaptive HLS"
|
||||
msgstr ""
|
||||
msgstr "Адаптивный HLS"
|
||||
|
||||
msgctxt "#30743"
|
||||
msgid "MPEG-DASH"
|
||||
msgstr ""
|
||||
msgstr "MPEG-DASH"
|
||||
|
||||
msgctxt "#30744"
|
||||
msgid "Original"
|
||||
msgstr ""
|
||||
msgstr "Оригинал"
|
||||
|
||||
msgctxt "#30745"
|
||||
msgid "Dubbed"
|
||||
msgstr ""
|
||||
msgstr "Дубляж"
|
||||
|
||||
msgctxt "#30746"
|
||||
msgid "Descriptive"
|
||||
|
|
@ -1293,79 +1293,79 @@ msgstr ""
|
|||
|
||||
msgctxt "#30748"
|
||||
msgid "Stream features"
|
||||
msgstr ""
|
||||
msgstr "Функции потоков"
|
||||
|
||||
msgctxt "#30749"
|
||||
msgid "Enable AV1 video"
|
||||
msgstr ""
|
||||
msgstr "Включить AV1 видео"
|
||||
|
||||
msgctxt "#30750"
|
||||
msgid "Enable Vorbis audio"
|
||||
msgstr ""
|
||||
msgstr "Включить Vorbis аудио"
|
||||
|
||||
msgctxt "#30751"
|
||||
msgid "Enable Opus audio"
|
||||
msgstr ""
|
||||
msgstr "Включить Opus аудио"
|
||||
|
||||
msgctxt "#30752"
|
||||
msgid "Enable AAC audio"
|
||||
msgstr ""
|
||||
msgstr "Включить AAC аудио"
|
||||
|
||||
msgctxt "#30753"
|
||||
msgid "Enable surround sound audio"
|
||||
msgstr ""
|
||||
msgstr "Включить пространственное аудио"
|
||||
|
||||
msgctxt "#30754"
|
||||
msgid "Enable AC-3 audio"
|
||||
msgstr ""
|
||||
msgstr "Включить AC-3 аудио"
|
||||
|
||||
msgctxt "#30755"
|
||||
msgid "Enable EAC-3 audio"
|
||||
msgstr ""
|
||||
msgstr "Включить EAC-3 аудио"
|
||||
|
||||
msgctxt "#30756"
|
||||
msgid "Enable DTS audio"
|
||||
msgstr ""
|
||||
msgstr "Включить DTS аудио"
|
||||
|
||||
msgctxt "#30757"
|
||||
msgid "Remove similar/duplicate streams"
|
||||
msgstr ""
|
||||
msgstr "Убирать похожие/дублирующие потоки"
|
||||
|
||||
msgctxt "#30758"
|
||||
msgid "Stream selection"
|
||||
msgstr ""
|
||||
msgstr "Выбор потока"
|
||||
|
||||
msgctxt "#30759"
|
||||
msgid "Quality selection"
|
||||
msgstr ""
|
||||
msgstr "Выбор качества"
|
||||
|
||||
msgctxt "#30760"
|
||||
msgid "Automatic + Quality selection"
|
||||
msgstr ""
|
||||
msgstr "Автоматически + Выбор качества"
|
||||
|
||||
msgctxt "#30761"
|
||||
msgid "Update playback history on Youtube"
|
||||
msgstr ""
|
||||
msgstr "Обновить историю просмотра в Youtube"
|
||||
|
||||
msgctxt "#30762"
|
||||
msgid "Multi-language"
|
||||
msgstr ""
|
||||
msgstr "Многоязычный"
|
||||
|
||||
msgctxt "#30763"
|
||||
msgid "Multi-audio"
|
||||
msgstr ""
|
||||
msgstr "Мульти-аудио"
|
||||
|
||||
msgctxt "#30764"
|
||||
msgid "Requests connect timeout"
|
||||
msgstr ""
|
||||
msgstr "Таймаут запросов"
|
||||
|
||||
msgctxt "#30765"
|
||||
msgid "Requests read timeout"
|
||||
msgstr ""
|
||||
msgstr "Таймаут на чтение запросов"
|
||||
|
||||
msgctxt "#30766"
|
||||
msgid "Premieres"
|
||||
msgstr ""
|
||||
msgstr "Премьеры"
|
||||
|
||||
msgctxt "#30767"
|
||||
msgid "Views"
|
||||
|
|
@ -1373,67 +1373,67 @@ msgstr "Просмотры"
|
|||
|
||||
msgctxt "#30768"
|
||||
msgid "Disable high framerate video at maximum video quality"
|
||||
msgstr ""
|
||||
msgstr "Выключить высокую частоту на максимальном качестве видео"
|
||||
|
||||
msgctxt "#30769"
|
||||
msgid "Clear Watch Later list"
|
||||
msgstr ""
|
||||
msgstr "Очистить список \"Посмотреть позже\""
|
||||
|
||||
msgctxt "#30770"
|
||||
msgid "Are you sure you want to clear your Watch Later list?"
|
||||
msgstr ""
|
||||
msgstr "Вы уверены что хотите очистить ваш список Посмотреть позже\"?"
|
||||
|
||||
msgctxt "#30771"
|
||||
msgid "Disable fractional framerate hinting"
|
||||
msgstr ""
|
||||
msgstr "Выключить метку дробной частоты кадров"
|
||||
|
||||
msgctxt "#30772"
|
||||
msgid "Disable all framerate hinting"
|
||||
msgstr ""
|
||||
msgstr "Включить все метки частоты кадров"
|
||||
|
||||
msgctxt "#30773"
|
||||
msgid "Show video details in video lists"
|
||||
msgstr ""
|
||||
msgstr "Отображать детали видео в списках видео"
|
||||
|
||||
msgctxt "#30774"
|
||||
msgid "All available"
|
||||
msgstr ""
|
||||
msgstr "Все доступные"
|
||||
|
||||
msgctxt "#30775"
|
||||
msgid "%s (translation)"
|
||||
msgstr ""
|
||||
msgstr "%s (перевод)"
|
||||
|
||||
msgctxt "#30776"
|
||||
msgid "Ask + Automatic + Quality selection"
|
||||
msgstr ""
|
||||
msgstr "Спрашивать + Автоматически + Выбор качества"
|
||||
|
||||
msgctxt "#30777"
|
||||
msgid "Views for %s (%s)"
|
||||
msgstr ""
|
||||
msgstr "Просмотры для %s (%s)"
|
||||
|
||||
msgctxt "#30778"
|
||||
msgid "Import old playback history?"
|
||||
msgstr ""
|
||||
msgstr "Импортировать старую историю просмотра?"
|
||||
|
||||
msgctxt "#30779"
|
||||
msgid "Import old search history?"
|
||||
msgstr ""
|
||||
msgstr "Импортировать старую историю поиска?"
|
||||
|
||||
msgctxt "#30780"
|
||||
msgid "Clear local watch later list"
|
||||
msgstr ""
|
||||
msgstr "Очистить локальный список \"Посмотреть позже\""
|
||||
|
||||
msgctxt "#30781"
|
||||
msgid "Delete watch later database"
|
||||
msgstr ""
|
||||
msgstr "Удалить базу данных \"Посмотреть позже\""
|
||||
|
||||
msgctxt "#30782"
|
||||
msgid "local watch later list"
|
||||
msgstr ""
|
||||
msgstr "локальный список \"Посмотреть позже\""
|
||||
|
||||
msgctxt "#30783"
|
||||
msgid "settings to recommended values"
|
||||
msgstr ""
|
||||
msgstr "настройки к рекомендованным значениям"
|
||||
|
||||
msgctxt "#30784"
|
||||
msgid "listings to show minimal details"
|
||||
|
|
@ -1441,147 +1441,147 @@ msgstr ""
|
|||
|
||||
msgctxt "#30785"
|
||||
msgid "performance settings"
|
||||
msgstr ""
|
||||
msgstr "настройки производительности"
|
||||
|
||||
msgctxt "#30786"
|
||||
msgid "Choose device capabilities"
|
||||
msgstr ""
|
||||
msgstr "Выберите возможности устройства"
|
||||
|
||||
msgctxt "#30787"
|
||||
msgid "720p, H.264 only | Limited or older devices"
|
||||
msgstr ""
|
||||
msgstr "720p, только H.264| Ограниченные или старые устройства"
|
||||
|
||||
msgctxt "#30788"
|
||||
msgid "1080p/30 fps | Raspberry Pi 3, or similar"
|
||||
msgstr ""
|
||||
msgstr "1080p/30 fps | Raspberry Pi 3, или подобные"
|
||||
|
||||
msgctxt "#30789"
|
||||
msgid "4K/30 fps or 1080p/60 fps, HDR if compatible | Raspberry Pi 5, or similar"
|
||||
msgstr ""
|
||||
msgstr "4K/30 fps or 1080p/60 fps, HDR если совместимо | Raspberry Pi 5, или подобные"
|
||||
|
||||
msgctxt "#30790"
|
||||
msgid "4K/60 fps, HDR if compatible | Fire TV Cube Gen 2, Shield TV, Fire TV Stick 4K Gen 1, or similar"
|
||||
msgstr ""
|
||||
msgstr "4K/60 fps, HDR если совместимо | Fire TV Cube Gen 2, Shield TV, Fire TV Stick 4K Gen 1, или подобные"
|
||||
|
||||
msgctxt "#30791"
|
||||
msgid "4K/60 fps, HDR, using AV1 | Fire TV Cube Gen 3, Fire TV Stick 4K Max, Vero V, or similar"
|
||||
msgstr ""
|
||||
msgstr "4K/60 fps, HDR, с кодеком AV1 | Fire TV Cube Gen 3, Fire TV Stick 4K Max, Vero V, или подобные"
|
||||
|
||||
msgctxt "#30792"
|
||||
msgid "8K/60 fps, HDR, using AV1 | Modern device or PC with full capabilities"
|
||||
msgstr ""
|
||||
msgstr "8K/60 fps, HDR, с кодеком AV1 | Современные устройства или ПК с полными возможностями"
|
||||
|
||||
msgctxt "#30793"
|
||||
msgid "Views count display colour"
|
||||
msgstr ""
|
||||
msgstr "Цвет счётчика просмотров"
|
||||
|
||||
msgctxt "#30794"
|
||||
msgid "Subscriber/Likes count display colour"
|
||||
msgstr ""
|
||||
msgstr "Цвет счётчика подписчиков или лайков"
|
||||
|
||||
msgctxt "#30795"
|
||||
msgid "Videos/Comments count display colour"
|
||||
msgstr ""
|
||||
msgstr "Цвет счётчика комментариев или просмотров"
|
||||
|
||||
msgctxt "#30796"
|
||||
msgid "1080p/60 fps | Raspberry Pi 4, or similar"
|
||||
msgstr ""
|
||||
msgstr "1080p/60 fps | Raspberry Pi 4, или подобные"
|
||||
|
||||
msgctxt "#30797"
|
||||
msgid "1080p/30 fps or 720p/30 fps, H.264 only | Raspberry Pi 1/2, or similar"
|
||||
msgstr ""
|
||||
msgstr "1080p/30 fps or 720p/30 fps, только H.264| Raspberry Pi 1/2, или подобные"
|
||||
|
||||
msgctxt "#30798"
|
||||
msgid "Clear bookmarks list"
|
||||
msgstr ""
|
||||
msgstr "Очистить список закладок"
|
||||
|
||||
msgctxt "#30799"
|
||||
msgid "Delete bookmarks database"
|
||||
msgstr ""
|
||||
msgstr "Удалить базу данных закладок"
|
||||
|
||||
msgctxt "#30800"
|
||||
msgid "bookmarks list"
|
||||
msgstr ""
|
||||
msgstr "список закладок"
|
||||
|
||||
msgctxt "#30801"
|
||||
msgid "Clear Bookmarks list"
|
||||
msgstr ""
|
||||
msgstr "Очистить список закладок"
|
||||
|
||||
msgctxt "#30802"
|
||||
msgid "Are you sure you want to clear your Bookmarks list?"
|
||||
msgstr ""
|
||||
msgstr "Вы уверены что хотите очистить список ваших закладок?"
|
||||
|
||||
msgctxt "#30803"
|
||||
msgid "Bookmark %s"
|
||||
msgstr ""
|
||||
msgstr "Добавить в закладки"
|
||||
|
||||
msgctxt "#30804"
|
||||
msgid "Use YouTube website urls with external player"
|
||||
msgstr ""
|
||||
msgstr "Использовать ссылки YouTube c внешним проигрывателем"
|
||||
|
||||
msgctxt "#30805"
|
||||
msgid "Use MPEG-DASH with external player"
|
||||
msgstr ""
|
||||
msgstr "Использовать MPEG-DASH с внешним проигрывателем"
|
||||
|
||||
msgctxt "#30806"
|
||||
msgid "Jump to page..."
|
||||
msgstr ""
|
||||
msgstr "Перепрыгнуть на страницу..."
|
||||
|
||||
msgctxt "#30807"
|
||||
msgid "Use channel name as"
|
||||
msgstr ""
|
||||
msgstr "Использовать имя канала как"
|
||||
|
||||
msgctxt "#30808"
|
||||
msgid "Hide items from listings"
|
||||
msgstr ""
|
||||
msgstr "Убрать элементы из списка"
|
||||
|
||||
msgctxt "#30809"
|
||||
msgid "All upcoming videos"
|
||||
msgstr ""
|
||||
msgstr "Все предстоящие видео"
|
||||
|
||||
msgctxt "#30810"
|
||||
msgid "All previously streamed (completed) videos"
|
||||
msgstr ""
|
||||
msgstr "Все предыдущие записи трансляций"
|
||||
|
||||
msgctxt "#30811"
|
||||
msgid "Filter Live folders"
|
||||
msgstr ""
|
||||
msgstr "Фильтровать папки трансляций"
|
||||
|
||||
msgctxt "#30812"
|
||||
msgid "Clear subscription feed history"
|
||||
msgstr ""
|
||||
msgstr "Очистить историю ленты подписок"
|
||||
|
||||
msgctxt "#30813"
|
||||
msgid "Delete subscription feed history database"
|
||||
msgstr ""
|
||||
msgstr "Очистить базу данных ленты подписок"
|
||||
|
||||
msgctxt "#30814"
|
||||
msgid "feed history"
|
||||
msgstr ""
|
||||
msgstr "лента подписок"
|
||||
|
||||
msgctxt "#30815"
|
||||
msgid "Go back..."
|
||||
msgstr ""
|
||||
msgstr "Вернуться назад..."
|
||||
|
||||
msgctxt "#30816"
|
||||
msgid "List is empty.[CR][CR]Refresh from context menu or try again later."
|
||||
msgstr ""
|
||||
msgstr "Список пуст.[CR][CR]Обновите из контекстного меню или попробуйте снова."
|
||||
|
||||
msgctxt "#30817"
|
||||
msgid "Refresh settings.xml"
|
||||
msgstr ""
|
||||
msgstr "Обновить settings.xml"
|
||||
|
||||
msgctxt "#30818"
|
||||
msgid "Are you sure you want to refresh settings.xml?"
|
||||
msgstr ""
|
||||
msgstr "Вы уверены что хотите обновить settings.xml?"
|
||||
|
||||
msgctxt "#30819"
|
||||
msgid "Play from start"
|
||||
msgstr ""
|
||||
msgstr "Начать с начала"
|
||||
|
||||
msgctxt "#30820"
|
||||
msgid "Podcast"
|
||||
msgstr ""
|
||||
msgstr "Подкаст"
|
||||
|
||||
# empty strings 30019
|
||||
#~ msgctxt "#30020"
|
||||
|
|
|
|||
|
|
@ -53,9 +53,18 @@ VALUE_TO_STR = {
|
|||
1: 'true',
|
||||
}
|
||||
|
||||
YOUTUBE_HOSTNAMES = frozenset((
|
||||
'youtube.com',
|
||||
'www.youtube.com',
|
||||
'm.youtube.com',
|
||||
'www.youtubekids.com',
|
||||
'music.youtube.com',
|
||||
))
|
||||
|
||||
# Flags
|
||||
ABORT_FLAG = 'abort_requested'
|
||||
BUSY_FLAG = 'busy'
|
||||
SERVICE_RUNNING_FLAG = 'service_monitor_running'
|
||||
WAIT_END_FLAG = 'builtin_completed'
|
||||
TRAKT_PAUSE_FLAG = 'script.trakt.paused'
|
||||
|
||||
|
|
@ -218,10 +227,12 @@ __all__ = (
|
|||
# Const values
|
||||
'BOOL_FROM_STR',
|
||||
'VALUE_TO_STR',
|
||||
'YOUTUBE_HOSTNAMES',
|
||||
|
||||
# Flags
|
||||
'ABORT_FLAG',
|
||||
'BUSY_FLAG',
|
||||
'SERVICE_RUNNING_FLAG',
|
||||
'TRAKT_PAUSE_FLAG',
|
||||
'WAIT_END_FLAG',
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ for name, label_id, sort_by in methods:
|
|||
if sort_by is not None:
|
||||
SORT_ID_MAPPING.update((
|
||||
(name, sort_by),
|
||||
(xbmc.getLocalizedString(label_id).lower(), sort_by),
|
||||
(xbmc.getLocalizedString(label_id), sort_by),
|
||||
(sort_method, sort_by if sort_method else 0),
|
||||
))
|
||||
|
||||
|
|
@ -99,6 +99,11 @@ SORT_ID_MAPPING.update((
|
|||
(CONTENT.HISTORY.join(('__', '__')), SORT.LASTPLAYED),
|
||||
))
|
||||
|
||||
SORT_DIR = {
|
||||
xbmc.getLocalizedString(584): 'ascending',
|
||||
xbmc.getLocalizedString(585): 'descending',
|
||||
}
|
||||
|
||||
# Label mask token details:
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/utils/LabelFormatter.cpp#L33-L105
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ class AbstractContext(object):
|
|||
'visitor',
|
||||
))
|
||||
_STRING_BOOL_PARAMS = frozenset((
|
||||
'logged_in',
|
||||
'reload_path',
|
||||
))
|
||||
_STRING_INT_PARAMS = frozenset((
|
||||
|
|
@ -213,7 +214,7 @@ class AbstractContext(object):
|
|||
self.parse_params(params)
|
||||
|
||||
self._uri = None
|
||||
self._path = path
|
||||
self._path = None
|
||||
self._path_parts = []
|
||||
self.set_path(path, force=True)
|
||||
|
||||
|
|
@ -540,7 +541,12 @@ class AbstractContext(object):
|
|||
value = unquote(value)
|
||||
try:
|
||||
if param in self._BOOL_PARAMS:
|
||||
parsed_value = BOOL_FROM_STR.get(str(value), False)
|
||||
parsed_value = BOOL_FROM_STR.get(
|
||||
str(value),
|
||||
bool(value)
|
||||
if param in self._STRING_BOOL_PARAMS else
|
||||
False
|
||||
)
|
||||
elif param in self._INT_PARAMS:
|
||||
parsed_value = int(
|
||||
(BOOL_FROM_STR.get(str(value), value) or 0)
|
||||
|
|
@ -682,7 +688,7 @@ class AbstractContext(object):
|
|||
def tear_down(self):
|
||||
pass
|
||||
|
||||
def ipc_exec(self, target, timeout=None, payload=None):
|
||||
def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@ from ...compatibility import (
|
|||
from ...constants import (
|
||||
ABORT_FLAG,
|
||||
ADDON_ID,
|
||||
BUSY_FLAG,
|
||||
CHANNEL_ID,
|
||||
CONTENT,
|
||||
FOLDER_NAME,
|
||||
PLAYLIST_ID,
|
||||
PLAY_FORCE_AUDIO,
|
||||
SERVICE_IPC,
|
||||
SERVICE_RUNNING_FLAG,
|
||||
SORT,
|
||||
URI,
|
||||
VIDEO_ID,
|
||||
|
|
@ -963,6 +965,8 @@ class XbmcContext(AbstractContext):
|
|||
attrs = (
|
||||
'_ui',
|
||||
'_playlist',
|
||||
'_api_store',
|
||||
'_access_manager',
|
||||
)
|
||||
for attr in attrs:
|
||||
try:
|
||||
|
|
@ -971,7 +975,15 @@ class XbmcContext(AbstractContext):
|
|||
except AttributeError:
|
||||
pass
|
||||
|
||||
def ipc_exec(self, target, timeout=None, payload=None):
|
||||
def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False):
|
||||
if not XbmcContextUI.get_property(SERVICE_RUNNING_FLAG, as_bool=True):
|
||||
msg = 'Service IPC - Monitor has not started'
|
||||
XbmcContextUI.set_property(SERVICE_RUNNING_FLAG, BUSY_FLAG)
|
||||
if raise_exc:
|
||||
raise RuntimeError(msg)
|
||||
self.log.warning_trace(msg)
|
||||
return None
|
||||
|
||||
data = {'target': target, 'response_required': bool(timeout)}
|
||||
if payload:
|
||||
data.update(payload)
|
||||
|
|
|
|||
|
|
@ -139,14 +139,26 @@ def media_play_using(context, video_id=VIDEO_ID_INFOLABEL):
|
|||
)
|
||||
|
||||
|
||||
def refresh_listing(context):
|
||||
def refresh_listing(context, path=None, params=None):
|
||||
if path is None:
|
||||
path = (PATHS.ROUTE, context.get_path(),)
|
||||
elif isinstance(path, tuple):
|
||||
path = (PATHS.ROUTE,) + path
|
||||
else:
|
||||
path = (PATHS.ROUTE, path,)
|
||||
if params is None:
|
||||
params = context.get_params()
|
||||
return (
|
||||
context.localize('refresh'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, context.get_path(),),
|
||||
dict(context.get_params(),
|
||||
refresh=context.refresh_requested(force=True, on=True)),
|
||||
path,
|
||||
dict(params,
|
||||
refresh=context.refresh_requested(
|
||||
force=True,
|
||||
on=True,
|
||||
params=params,
|
||||
)),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -829,7 +841,7 @@ def search_sort_by(context, params, order):
|
|||
),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, PATHS.SEARCH, 'query',),
|
||||
(PATHS.ROUTE, context.get_path(),),
|
||||
params=dict(params,
|
||||
order=order,
|
||||
page=1,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,12 @@ class AccessManager(JSONStore):
|
|||
}
|
||||
|
||||
def __init__(self, context):
|
||||
self._user = None
|
||||
self._last_origin = None
|
||||
super(AccessManager, self).__init__('access_manager.json', context)
|
||||
|
||||
def init(self):
|
||||
super(AccessManager, self).init()
|
||||
access_manager_data = self._data['access_manager']
|
||||
self._user = access_manager_data.get('current_user', 0)
|
||||
self._last_origin = access_manager_data.get('last_origin', ADDON_ID)
|
||||
|
|
@ -204,7 +209,8 @@ class AccessManager(JSONStore):
|
|||
Returns users
|
||||
:return: users
|
||||
"""
|
||||
return self._data['access_manager'].get('users', {})
|
||||
data = self._data if self._loaded else self.get_data()
|
||||
return data['access_manager'].get('users', {})
|
||||
|
||||
def add_user(self, username='', user=None):
|
||||
"""
|
||||
|
|
@ -546,7 +552,8 @@ class AccessManager(JSONStore):
|
|||
Returns developers
|
||||
:return: dict, developers
|
||||
"""
|
||||
return self._data['access_manager'].get('developers', {})
|
||||
data = self._data if self._loaded else self.get_data()
|
||||
return data['access_manager'].get('developers', {})
|
||||
|
||||
def add_new_developer(self, addon_id):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -38,9 +38,17 @@ class JSONStore(object):
|
|||
self.filepath = None
|
||||
|
||||
self._context = context
|
||||
self._loaded = False
|
||||
self._data = {}
|
||||
self.load(stacklevel=3)
|
||||
self.set_defaults()
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
if self.load(stacklevel=4):
|
||||
self._loaded = True
|
||||
self.set_defaults()
|
||||
else:
|
||||
self.set_defaults(reset=True)
|
||||
return self._loaded
|
||||
|
||||
def set_defaults(self, reset=False):
|
||||
raise NotImplementedError
|
||||
|
|
@ -81,6 +89,7 @@ class JSONStore(object):
|
|||
FILE_WRITE,
|
||||
timeout=5,
|
||||
payload={'filepath': filepath},
|
||||
raise_exc=True,
|
||||
)
|
||||
if response is False:
|
||||
raise IOError
|
||||
|
|
@ -92,7 +101,7 @@ class JSONStore(object):
|
|||
else:
|
||||
with open(filepath, mode='w', encoding='utf-8') as file:
|
||||
file.write(to_unicode(_data))
|
||||
except (IOError, OSError):
|
||||
except (RuntimeError, IOError, OSError):
|
||||
self.log.exception(('Access error', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
|
|
@ -119,6 +128,7 @@ class JSONStore(object):
|
|||
FILE_READ,
|
||||
timeout=5,
|
||||
payload={'filepath': filepath},
|
||||
raise_exc=True,
|
||||
) is not False:
|
||||
data = self._context.get_ui().get_property(
|
||||
'-'.join((FILE_READ, filepath)),
|
||||
|
|
@ -135,17 +145,23 @@ class JSONStore(object):
|
|||
data,
|
||||
object_pairs_hook=(self._process_data if process else None),
|
||||
)
|
||||
except (IOError, OSError):
|
||||
except (RuntimeError, IOError, OSError):
|
||||
self.log.exception(('Access error', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
except (TypeError, ValueError):
|
||||
self.log.exception(('Invalid data', 'Data: {data!r}'),
|
||||
data=data,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_data(self, process=True, fallback=True, stacklevel=2):
|
||||
if not self._loaded:
|
||||
self.init()
|
||||
data = self._data
|
||||
|
||||
try:
|
||||
if not data:
|
||||
raise ValueError
|
||||
|
|
@ -160,7 +176,9 @@ class JSONStore(object):
|
|||
if fallback:
|
||||
self.set_defaults(reset=True)
|
||||
return self.get_data(process=process, fallback=False)
|
||||
raise exc
|
||||
if self._loaded:
|
||||
raise exc
|
||||
return data
|
||||
|
||||
def load_data(self, data, process=True, stacklevel=2):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ class KodiLogger(logging.Logger):
|
|||
**kwargs
|
||||
)
|
||||
|
||||
def waring_trace(self, msg, *args, **kwargs):
|
||||
def warning_trace(self, msg, *args, **kwargs):
|
||||
if self.isEnabledFor(WARNING):
|
||||
self._log(
|
||||
WARNING,
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ from ..compatibility import urlsplit, xbmc, xbmcgui
|
|||
from ..constants import (
|
||||
ACTION,
|
||||
ADDON_ID,
|
||||
BOOL_FROM_STR,
|
||||
BUSY_FLAG,
|
||||
CHECK_SETTINGS,
|
||||
CONTAINER_FOCUS,
|
||||
CONTAINER_ID,
|
||||
|
|
@ -112,81 +110,11 @@ class ServiceMonitor(xbmc.Monitor):
|
|||
xbmcgui.Window(10000).setProperty(_property_id, value)
|
||||
return value
|
||||
|
||||
def get_property(self,
|
||||
property_id,
|
||||
stacklevel=2,
|
||||
process=None,
|
||||
log_value=None,
|
||||
log_process=None,
|
||||
raw=False,
|
||||
as_bool=False,
|
||||
default=False):
|
||||
_property_id = property_id if raw else '-'.join((ADDON_ID, property_id))
|
||||
value = xbmcgui.Window(10000).getProperty(_property_id)
|
||||
if log_value is None:
|
||||
log_value = value
|
||||
if log_process:
|
||||
log_value = log_process(log_value)
|
||||
self.log.debug_trace('Get property {property_id!r}: {value!r}',
|
||||
property_id=property_id,
|
||||
value=log_value,
|
||||
stacklevel=stacklevel)
|
||||
if process:
|
||||
value = process(value)
|
||||
return BOOL_FROM_STR.get(value, default) if as_bool else value
|
||||
|
||||
def pop_property(self,
|
||||
property_id,
|
||||
stacklevel=2,
|
||||
process=None,
|
||||
log_value=None,
|
||||
log_process=None,
|
||||
raw=False,
|
||||
as_bool=False,
|
||||
default=False):
|
||||
_property_id = property_id if raw else '-'.join((ADDON_ID, property_id))
|
||||
window = xbmcgui.Window(10000)
|
||||
value = window.getProperty(_property_id)
|
||||
if value:
|
||||
window.clearProperty(_property_id)
|
||||
if process:
|
||||
value = process(value)
|
||||
if log_value is None:
|
||||
log_value = value
|
||||
if log_value and log_process:
|
||||
log_value = log_process(log_value)
|
||||
self.log.debug_trace('Pop property {property_id!r}: {value!r}',
|
||||
property_id=property_id,
|
||||
value=log_value,
|
||||
stacklevel=stacklevel)
|
||||
return BOOL_FROM_STR.get(value, default) if as_bool else value
|
||||
|
||||
def clear_property(self, property_id, stacklevel=2, raw=False):
|
||||
self.log.debug_trace('Clear property {property_id!r}',
|
||||
property_id=property_id,
|
||||
stacklevel=stacklevel)
|
||||
_property_id = property_id if raw else '-'.join((ADDON_ID, property_id))
|
||||
xbmcgui.Window(10000).clearProperty(_property_id)
|
||||
return None
|
||||
|
||||
def refresh_container(self, deferred=False):
|
||||
if deferred:
|
||||
def refresh_container(self, force=False):
|
||||
if force:
|
||||
self.refresh = False
|
||||
if self.get_property(REFRESH_CONTAINER) == BUSY_FLAG:
|
||||
self.set_property(REFRESH_CONTAINER)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
return
|
||||
|
||||
container = self._context.get_ui().get_container()
|
||||
if not container['is_plugin'] or not container['is_loaded']:
|
||||
self.log.debug('No plugin container loaded - cancelling refresh')
|
||||
return
|
||||
if container['is_active']:
|
||||
self.set_property(REFRESH_CONTAINER)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
else:
|
||||
self.set_property(REFRESH_CONTAINER, BUSY_FLAG)
|
||||
self.log.debug('Plugin container not active - deferring refresh')
|
||||
refreshed = self._context.get_ui().refresh_container(force=force)
|
||||
if refreshed is None:
|
||||
self.refresh = True
|
||||
|
||||
def onNotification(self, sender, method, data):
|
||||
|
|
@ -317,7 +245,7 @@ class ServiceMonitor(xbmc.Monitor):
|
|||
response = False
|
||||
else:
|
||||
with write_access:
|
||||
content = self.pop_property(
|
||||
content = self._context.get_ui().pop_property(
|
||||
'-'.join((FILE_WRITE, filepath)),
|
||||
log_value='<redacted>',
|
||||
)
|
||||
|
|
@ -347,52 +275,11 @@ class ServiceMonitor(xbmc.Monitor):
|
|||
elif event == CONTAINER_FOCUS:
|
||||
if data:
|
||||
data = json.loads(data)
|
||||
if not data:
|
||||
return
|
||||
|
||||
context = self._context
|
||||
ui = context.get_ui()
|
||||
|
||||
container = ui.get_container()
|
||||
if not all(container.values()):
|
||||
return
|
||||
|
||||
container_id = data.get(CONTAINER_ID)
|
||||
if container_id is None:
|
||||
container_id = container['id']
|
||||
elif not container_id:
|
||||
return
|
||||
if not isinstance(container_id, int):
|
||||
try:
|
||||
container_id = int(container_id)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
|
||||
position = data.get(CONTAINER_POSITION)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
if ui.get_container_bool(HAS_PARENT, container_id):
|
||||
offset = 0
|
||||
else:
|
||||
offset = -1
|
||||
|
||||
if not isinstance(position, int):
|
||||
if position == 'next':
|
||||
position = ui.get_container_info(CURRENT_ITEM, container_id)
|
||||
offset += 1
|
||||
elif position == 'previous':
|
||||
position = ui.get_container_info(CURRENT_ITEM, container_id)
|
||||
offset -= 1
|
||||
try:
|
||||
position = int(position)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
|
||||
context.execute('SetFocus({0},{1},absolute)'.format(
|
||||
container_id,
|
||||
position + offset,
|
||||
))
|
||||
if data:
|
||||
self._context.get_ui().focus_container(
|
||||
container_id=data.get(CONTAINER_ID),
|
||||
position=data.get(CONTAINER_POSITION),
|
||||
)
|
||||
|
||||
elif event == RELOAD_ACCESS_MANAGER:
|
||||
self._context.reload_access_manager()
|
||||
|
|
|
|||
|
|
@ -174,14 +174,14 @@ class XbmcPlaylistPlayer(AbstractPlaylistPlayer):
|
|||
def get_items(self, properties=None, start=0, end=-1, dumps=False):
|
||||
if properties is None:
|
||||
properties = ('title', 'file')
|
||||
limits = {'start': start}
|
||||
if end != -1:
|
||||
limits['end'] = end
|
||||
response = jsonrpc(method='Playlist.GetItems',
|
||||
params={
|
||||
'properties': properties,
|
||||
'playlistid': self._playlist.getPlayListId(),
|
||||
'limits': {
|
||||
'start': start,
|
||||
'end': end,
|
||||
},
|
||||
'limits': limits,
|
||||
})
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -374,7 +374,12 @@ class XbmcPlugin(AbstractPlugin):
|
|||
listitem=item,
|
||||
)
|
||||
elif options.get(provider.FORCE_REFRESH):
|
||||
_post_run_action = ui.refresh_container
|
||||
_post_run_action = (
|
||||
context.send_notification,
|
||||
{
|
||||
'method': REFRESH_CONTAINER,
|
||||
},
|
||||
)
|
||||
else:
|
||||
if context.is_plugin_path(
|
||||
ui.get_container_info(FOLDER_URI, container_id=None)
|
||||
|
|
@ -411,20 +416,8 @@ class XbmcPlugin(AbstractPlugin):
|
|||
if any(sync_items):
|
||||
context.send_notification(SYNC_LISTITEM, sync_items)
|
||||
|
||||
if forced and is_same_path and (not played_video_id or route):
|
||||
container = ui.get_property(CONTAINER_ID)
|
||||
position = ui.get_property(CONTAINER_POSITION)
|
||||
if container and position:
|
||||
post_run_actions.append((
|
||||
context.send_notification,
|
||||
{
|
||||
'method': CONTAINER_FOCUS,
|
||||
'data': {
|
||||
CONTAINER_ID: container,
|
||||
CONTAINER_POSITION: position,
|
||||
},
|
||||
},
|
||||
))
|
||||
container = ui.get_property(CONTAINER_ID)
|
||||
position = ui.get_property(CONTAINER_POSITION)
|
||||
|
||||
# set alternative view mode
|
||||
view_manager = ui.get_view_manager()
|
||||
|
|
@ -438,22 +431,30 @@ class XbmcPlugin(AbstractPlugin):
|
|||
|
||||
if is_same_path:
|
||||
sort_method = kwargs.get(SORT_METHOD)
|
||||
if sort_method:
|
||||
sort_dir = kwargs.get(SORT_DIR)
|
||||
if sort_method and sort_dir:
|
||||
post_run_actions.append((
|
||||
view_manager.apply_sort_method,
|
||||
{
|
||||
'context': context,
|
||||
SORT_METHOD: sort_method,
|
||||
SORT_DIR: sort_dir,
|
||||
CONTAINER_POSITION: position if forced else None,
|
||||
},
|
||||
))
|
||||
position = None
|
||||
|
||||
sort_dir = kwargs.get(SORT_DIR)
|
||||
if sort_dir:
|
||||
if (container and position
|
||||
and (forced or position == 'current')
|
||||
and (not played_video_id or route)):
|
||||
post_run_actions.append((
|
||||
view_manager.apply_sort_dir,
|
||||
context.send_notification,
|
||||
{
|
||||
'context': context,
|
||||
SORT_DIR: sort_dir,
|
||||
'method': CONTAINER_FOCUS,
|
||||
'data': {
|
||||
CONTAINER_ID: container,
|
||||
CONTAINER_POSITION: position,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -69,11 +69,8 @@ def run(context=_context,
|
|||
log.verbose_logging = False
|
||||
profiler.disable()
|
||||
|
||||
old_path, old_params = context.parse_uri(
|
||||
ui.get_container_info(FOLDER_URI, container_id=None),
|
||||
parse_params=False,
|
||||
)
|
||||
old_path = old_path.rstrip('/')
|
||||
old_path = context.get_path().rstrip('/')
|
||||
old_uri = ui.get_container_info(FOLDER_URI, container_id=None)
|
||||
context.init()
|
||||
current_path = context.get_path().rstrip('/')
|
||||
current_params = context.get_original_params()
|
||||
|
|
@ -81,13 +78,28 @@ def run(context=_context,
|
|||
|
||||
new_params = {}
|
||||
new_kwargs = {}
|
||||
params = context.get_params()
|
||||
|
||||
params = context.get_params()
|
||||
refresh = context.refresh_requested(params=params)
|
||||
is_same_path = refresh != 0 and current_path == old_path
|
||||
forced = (current_handle != -1
|
||||
and (old_path == PATHS.PLAY
|
||||
or (is_same_path and current_params == old_params)))
|
||||
was_playing = old_path == PATHS.PLAY
|
||||
is_same_path = current_path == old_path
|
||||
|
||||
if was_playing or is_same_path or refresh:
|
||||
old_path, old_params = context.parse_uri(
|
||||
old_uri,
|
||||
parse_params=False,
|
||||
)
|
||||
old_path = old_path.rstrip('/')
|
||||
is_same_path = current_path == old_path
|
||||
if was_playing and current_handle != -1:
|
||||
forced = True
|
||||
elif is_same_path and current_params == old_params:
|
||||
forced = True
|
||||
else:
|
||||
forced = False
|
||||
else:
|
||||
forced = False
|
||||
|
||||
if forced:
|
||||
refresh = context.refresh_requested(force=True, off=True, params=params)
|
||||
new_params['refresh'] = refresh if refresh else 0
|
||||
|
|
@ -97,14 +109,14 @@ def run(context=_context,
|
|||
or ui.get_infolabel('Container.SortMethod')
|
||||
)
|
||||
if sort_method:
|
||||
new_kwargs[SORT_METHOD] = sort_method.lower()
|
||||
new_kwargs[SORT_METHOD] = sort_method
|
||||
|
||||
sort_dir = (
|
||||
params.get(SORT_DIR)
|
||||
or ui.get_infolabel('Container.SortOrder')
|
||||
)
|
||||
if sort_dir:
|
||||
new_kwargs[SORT_DIR] = sort_dir.lower()
|
||||
new_kwargs[SORT_DIR] = sort_dir
|
||||
|
||||
if new_params:
|
||||
context.set_params(**new_params)
|
||||
|
|
|
|||
|
|
@ -145,7 +145,10 @@ def _config_actions(context, action, *_args):
|
|||
base_kodi_language = kodi_language.partition('-')[0]
|
||||
|
||||
json_data = client.get_supported_languages(kodi_language)
|
||||
items = json_data.get('items') or DEFAULT_LANGUAGES['items']
|
||||
if json_data:
|
||||
items = json_data.get('items') or DEFAULT_LANGUAGES['items']
|
||||
else:
|
||||
items = DEFAULT_LANGUAGES['items']
|
||||
|
||||
selected_language = [None]
|
||||
|
||||
|
|
@ -184,7 +187,10 @@ def _config_actions(context, action, *_args):
|
|||
return
|
||||
|
||||
json_data = client.get_supported_regions(language=language_id)
|
||||
items = json_data.get('items') or DEFAULT_REGIONS['items']
|
||||
if json_data:
|
||||
items = json_data.get('items') or DEFAULT_REGIONS['items']
|
||||
else:
|
||||
items = DEFAULT_REGIONS['items']
|
||||
|
||||
selected_region = [None]
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from .constants import (
|
|||
ABORT_FLAG,
|
||||
ARTIST,
|
||||
BOOKMARK_ID,
|
||||
BUSY_FLAG,
|
||||
CHANNEL_ID,
|
||||
CONTAINER_ID,
|
||||
CONTAINER_POSITION,
|
||||
|
|
@ -24,6 +25,7 @@ from .constants import (
|
|||
PLAYLIST_ITEM_ID,
|
||||
PLAY_COUNT,
|
||||
PLUGIN_SLEEPING,
|
||||
SERVICE_RUNNING_FLAG,
|
||||
SUBSCRIPTION_ID,
|
||||
TEMP_PATH,
|
||||
TITLE,
|
||||
|
|
@ -67,6 +69,9 @@ def run():
|
|||
localize = context.localize
|
||||
|
||||
clear_property(ABORT_FLAG)
|
||||
if ui.get_property(SERVICE_RUNNING_FLAG) == BUSY_FLAG:
|
||||
monitor.refresh_container()
|
||||
set_property(SERVICE_RUNNING_FLAG)
|
||||
|
||||
# wipe add-on temp folder on updates/restarts (subtitles, and mpd files)
|
||||
rm_dir(TEMP_PATH)
|
||||
|
|
@ -173,7 +178,7 @@ def run():
|
|||
monitor.interrupt = True
|
||||
|
||||
if monitor.refresh and all(container.values()):
|
||||
monitor.refresh_container(deferred=True)
|
||||
monitor.refresh_container(force=True)
|
||||
break
|
||||
|
||||
if (monitor.interrupt
|
||||
|
|
@ -240,6 +245,7 @@ def run():
|
|||
break
|
||||
|
||||
set_property(ABORT_FLAG)
|
||||
clear_property(SERVICE_RUNNING_FLAG)
|
||||
|
||||
# clean up any/all playback monitoring threads
|
||||
player.cleanup_threads(only_ended=False)
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ class AbstractContextUI(object):
|
|||
def on_busy():
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def refresh_container():
|
||||
def refresh_container(self, force=False, stacklevel=None):
|
||||
"""
|
||||
Needs to be implemented by a mock for testing or the real deal.
|
||||
This will refresh the current container or list.
|
||||
|
|
@ -71,6 +70,9 @@ class AbstractContextUI(object):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def focus_container(self, container_id=None, position=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def get_infobool(name):
|
||||
raise NotImplementedError()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,13 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
|
||||
from ... import logging
|
||||
from ...compatibility import xbmc
|
||||
from ...constants import CONTENT, SORT, SORT_DIR, SORT_METHOD
|
||||
from ...constants import (
|
||||
CONTAINER_POSITION,
|
||||
CONTENT,
|
||||
SORT,
|
||||
SORT_DIR,
|
||||
SORT_METHOD,
|
||||
)
|
||||
|
||||
|
||||
class ViewManager(object):
|
||||
|
|
@ -267,54 +273,70 @@ class ViewManager(object):
|
|||
@classmethod
|
||||
def apply_sort_method(cls, context, **kwargs):
|
||||
execute = context.execute
|
||||
get_infolabel = xbmc.getInfoLabel
|
||||
get_infobool = xbmc.getCondVisibility
|
||||
|
||||
sort_method = (kwargs.get(SORT_METHOD)
|
||||
or CONTENT.VIDEO_CONTENT.join(('__', '__'))).lower()
|
||||
sort_method = (
|
||||
kwargs.get(SORT_METHOD)
|
||||
or CONTENT.VIDEO_CONTENT.join(('__', '__'))
|
||||
)
|
||||
sort_id = SORT.SORT_ID_MAPPING.get(sort_method)
|
||||
if sort_id is None:
|
||||
cls.log.warning('Unknown sort method: %r', sort_method)
|
||||
return
|
||||
|
||||
_sort_method = get_infolabel('Container.SortMethod').lower()
|
||||
_sort_id = SORT.SORT_ID_MAPPING.get(_sort_method)
|
||||
# Check if hardcoded sort method to ID mapping has changed
|
||||
if not xbmc.getCondVisibility('Container.SortMethod(%s)' % _sort_id):
|
||||
cls.log.warning('Sort method mismatch: {method!r} ID is not {id}',
|
||||
method=_sort_method,
|
||||
id=_sort_id)
|
||||
sort_dir = kwargs.get(SORT_DIR)
|
||||
_sort_dir = SORT.SORT_DIR.get(sort_dir)
|
||||
if _sort_dir is None:
|
||||
cls.log.warning('Invalid sort direction: %r', sort_dir)
|
||||
return
|
||||
|
||||
position = kwargs.get(CONTAINER_POSITION)
|
||||
if position is not None:
|
||||
context.get_ui().focus_container(position=position)
|
||||
|
||||
# Workaround for Container.SetSortMethod failing for some sort methods
|
||||
num_attempts_remaining = 3
|
||||
while num_attempts_remaining and not context.sleep(0.1):
|
||||
cls.log.debug('Applying sort method: {method!r} ({id})',
|
||||
method=sort_method,
|
||||
id=sort_id)
|
||||
num_attempts = 0
|
||||
while num_attempts < 4:
|
||||
# Workaround for Container.SetSortMethod(0) being a noop
|
||||
# https://github.com/xbmc/xbmc/blob/7e1a55cb861342cd9062745161d88aca08dcead1/xbmc/windows/GUIMediaWindow.cpp#L502
|
||||
if sort_id == 0:
|
||||
# Sort by track number to reset sort order to default order
|
||||
_sort_id = SORT.SORT_ID_MAPPING.get(SORT.TRACKNUM)
|
||||
execute('Container.SetSortMethod(%s)' % _sort_id)
|
||||
if not num_attempts % 2:
|
||||
_sort_method = 'TRACKNUM'
|
||||
_sort_id = SORT.SORT_ID_MAPPING.get(_sort_method)
|
||||
sort_action = 'Container.SetSortMethod(%s)' % _sort_id
|
||||
# Then switch to previous sort method which is default/unsorted
|
||||
# as per the order set in XbmcContext.apply_content
|
||||
execute('Container.PreviousSortMethod')
|
||||
else:
|
||||
_sort_method = 'UNSORTED'
|
||||
_sort_id = SORT.SORT_ID_MAPPING.get(_sort_method)
|
||||
sort_action = 'Container.PreviousSortMethod'
|
||||
else:
|
||||
execute('Container.SetSortMethod(%s)' % sort_id)
|
||||
if get_infolabel('Container.SortMethod').lower() == sort_method:
|
||||
break
|
||||
num_attempts_remaining -= 1
|
||||
else:
|
||||
cls.log.warning('Unable to apply sort method: {method!r} ({id})',
|
||||
method=sort_method,
|
||||
id=sort_id)
|
||||
_sort_method = sort_method
|
||||
_sort_id = sort_id
|
||||
sort_action = 'Container.SetSortMethod(%s)' % _sort_id
|
||||
|
||||
@classmethod
|
||||
def apply_sort_dir(cls, context, **kwargs):
|
||||
sort_dir = kwargs.get(SORT_DIR, 'ascending')
|
||||
if not xbmc.getCondVisibility('Container.SortDirection(%s)' % sort_dir):
|
||||
cls.log.debug('Applying sort direction: %r', sort_dir)
|
||||
# This builtin should be Container.SortDirection but has been broken
|
||||
# since Kodi v16
|
||||
# https://github.com/xbmc/xbmc/commit/ac870b64b16dfd0fc2bd0496c14529cf6d563f41
|
||||
context.execute('Container.SetSortDirection')
|
||||
cls.log.debug('Applying sort method: {method!r} ({id})',
|
||||
method=_sort_method,
|
||||
id=_sort_id)
|
||||
execute(sort_action)
|
||||
context.sleep(0.1)
|
||||
|
||||
if not get_infobool('Container.SortDirection(%s)' % _sort_dir):
|
||||
cls.log.debug('Applying sort direction: %r', sort_dir)
|
||||
# This builtin should be Container.SortDirection but has been
|
||||
# broken since Kodi v16
|
||||
# https://github.com/xbmc/xbmc/commit/ac870b64b16dfd0fc2bd0496c14529cf6d563f41
|
||||
execute('Container.SetSortDirection')
|
||||
context.sleep(0.1)
|
||||
|
||||
num_attempts += 1
|
||||
|
||||
if get_infobool('Container.SortMethod(%s)' % sort_id):
|
||||
break
|
||||
else:
|
||||
cls.log.warning('Unable to apply sorting:'
|
||||
' {sort_method!r} ({sort_id}) {sort_dir!r}',
|
||||
sort_method=sort_method,
|
||||
sort_id=sort_id,
|
||||
sort_dir=sort_dir)
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ from ...compatibility import string_type, xbmc, xbmcgui
|
|||
from ...constants import (
|
||||
ADDON_ID,
|
||||
BOOL_FROM_STR,
|
||||
BUSY_FLAG,
|
||||
CONTAINER_FOCUS,
|
||||
CONTAINER_ID,
|
||||
CONTAINER_LISTITEM_INFO,
|
||||
CONTAINER_LISTITEM_PROP,
|
||||
CONTAINER_POSITION,
|
||||
CURRENT_CONTAINER_INFO,
|
||||
CURRENT_ITEM,
|
||||
HAS_FILES,
|
||||
HAS_FOLDERS,
|
||||
HAS_PARENT,
|
||||
|
|
@ -200,8 +202,76 @@ class XbmcContextUI(AbstractContextUI):
|
|||
def on_busy():
|
||||
return XbmcBusyDialog()
|
||||
|
||||
def refresh_container(self):
|
||||
self._context.send_notification(REFRESH_CONTAINER)
|
||||
def refresh_container(self, force=False, stacklevel=None):
|
||||
if force:
|
||||
if self.get_property(REFRESH_CONTAINER) == BUSY_FLAG:
|
||||
self.set_property(REFRESH_CONTAINER)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
return True
|
||||
|
||||
stacklevel = 2 if stacklevel is None else stacklevel + 1
|
||||
|
||||
container = self.get_container()
|
||||
if not container['is_plugin'] or not container['is_loaded']:
|
||||
self.log.debug('No plugin container loaded - cancelling refresh',
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
|
||||
if container['is_active']:
|
||||
self.set_property(REFRESH_CONTAINER)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
return True
|
||||
|
||||
self.set_property(REFRESH_CONTAINER, BUSY_FLAG)
|
||||
self.log.debug('Plugin container not active - deferring refresh',
|
||||
stacklevel=stacklevel)
|
||||
return None
|
||||
|
||||
def focus_container(self, container_id=None, position=None):
|
||||
if position is None:
|
||||
return
|
||||
|
||||
container = self.get_container()
|
||||
if not all(container.values()):
|
||||
return
|
||||
|
||||
if container_id is None:
|
||||
container_id = container['id']
|
||||
elif not container_id:
|
||||
return
|
||||
|
||||
if not isinstance(container_id, int):
|
||||
try:
|
||||
container_id = int(container_id)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
|
||||
if self.get_container_bool(HAS_PARENT, container_id):
|
||||
offset = 0
|
||||
else:
|
||||
offset = -1
|
||||
|
||||
if not isinstance(position, int):
|
||||
if position == 'next':
|
||||
position = self.get_container_info(CURRENT_ITEM, container_id)
|
||||
offset += 1
|
||||
elif position == 'previous':
|
||||
position = self.get_container_info(CURRENT_ITEM, container_id)
|
||||
offset -= 1
|
||||
elif position == 'current':
|
||||
position = (
|
||||
self.get_property(CONTAINER_POSITION)
|
||||
or self.get_container_info(CURRENT_ITEM, container_id)
|
||||
)
|
||||
try:
|
||||
position = int(position)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
|
||||
xbmc.executebuiltin('SetFocus({0},{1},absolute)'.format(
|
||||
container_id,
|
||||
position + offset,
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def get_infobool(name, _bool=xbmc.getCondVisibility):
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
|
||||
from datetime import timedelta
|
||||
from math import floor, log
|
||||
from re import MULTILINE, compile as re_compile
|
||||
from re import DOTALL, compile as re_compile
|
||||
|
||||
from ..compatibility import byte_string_type
|
||||
|
||||
|
|
@ -103,10 +103,10 @@ def timedelta_to_timestamp(delta, offset=None, multiplier=1.0):
|
|||
def _srt_to_vtt(content,
|
||||
srt_re=re_compile(
|
||||
br'\d+[\r\n]'
|
||||
br'(?P<start>\d+:\d+:\d+,\d+) --> '
|
||||
br'(?P<end>\d+:\d+:\d+,\d+)[\r\n]'
|
||||
br'(?P<text>.+)(?=[\r\n]{2,})',
|
||||
flags=MULTILINE,
|
||||
br'(?P<start>[\d:,]+) --> '
|
||||
br'(?P<end>[\d:,]+)[\r\n]'
|
||||
br'(?P<text>.+?)[\r\n][\r\n]',
|
||||
flags=DOTALL,
|
||||
)):
|
||||
subtitle_iter = srt_re.finditer(content)
|
||||
try:
|
||||
|
|
@ -136,6 +136,7 @@ def _srt_to_vtt(content,
|
|||
except StopIteration:
|
||||
if subtitle == next_subtitle:
|
||||
break
|
||||
subtitle = None
|
||||
next_subtitle = None
|
||||
|
||||
if next_subtitle and end > next_start:
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
'browseEndpoint',
|
||||
'browseId',
|
||||
),
|
||||
'playlist_id': (
|
||||
'tileRenderer',
|
||||
'onSelectCommand',
|
||||
'watchEndpoint',
|
||||
'playlistId',
|
||||
),
|
||||
'continuation': (
|
||||
'contents',
|
||||
'tvBrowseRenderer',
|
||||
|
|
@ -1072,12 +1078,16 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
if playlist_id_upper == 'HL':
|
||||
browse_id = 'FEhistory'
|
||||
json_path = self.JSON_PATHS['tv_grid']
|
||||
response_type = 'videos'
|
||||
else:
|
||||
browse_id = 'VL' + playlist_id_upper
|
||||
json_path = self.JSON_PATHS['tv_playlist']
|
||||
response_type = 'playlistItems'
|
||||
|
||||
return self.get_browse_items(
|
||||
browse_id=browse_id,
|
||||
playlist_id=playlist_id,
|
||||
response_type=response_type,
|
||||
client='tv',
|
||||
do_auth=True,
|
||||
page_token=page_token,
|
||||
|
|
@ -1315,6 +1325,7 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
def get_browse_items(self,
|
||||
browse_id=None,
|
||||
channel_id=None,
|
||||
playlist_id=None,
|
||||
skip_ids=None,
|
||||
params=None,
|
||||
route=None,
|
||||
|
|
@ -1340,7 +1351,12 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
'youtube#playlistListResponse',
|
||||
'youtube#playlist',
|
||||
'contentId',
|
||||
)
|
||||
),
|
||||
'playlistItems': (
|
||||
'youtube#playlistItemListResponse',
|
||||
'youtube#playlistItem',
|
||||
'contentId',
|
||||
),
|
||||
},
|
||||
data=None,
|
||||
client=None,
|
||||
|
|
@ -1448,6 +1464,13 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
)
|
||||
if skip_ids and _channel_id in skip_ids:
|
||||
continue
|
||||
if playlist_id:
|
||||
_playlist_id = playlist_id
|
||||
else:
|
||||
_playlist_id = self.json_traverse(
|
||||
item,
|
||||
json_path.get('playlist_id'),
|
||||
)
|
||||
items.append({
|
||||
'kind': item_kind,
|
||||
'id': item_id,
|
||||
|
|
@ -1470,6 +1493,7 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
),
|
||||
),
|
||||
'channelId': _channel_id,
|
||||
'playlistId': _playlist_id,
|
||||
}
|
||||
})
|
||||
if not items:
|
||||
|
|
|
|||
|
|
@ -18,20 +18,9 @@ from ...kodion import logging
|
|||
class YouTubeLoginClient(YouTubeRequestClient):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ANDROID_CLIENT_AUTH_URL = 'https://android.clients.google.com/auth'
|
||||
DOMAIN_SUFFIX = '.apps.googleusercontent.com'
|
||||
DEVICE_CODE_URL = 'https://accounts.google.com/o/oauth2/device/code'
|
||||
REVOKE_URL = 'https://accounts.google.com/o/oauth2/revoke'
|
||||
SERVICE_URLS = 'oauth2:' + 'https://www.googleapis.com/auth/'.join((
|
||||
'youtube '
|
||||
'youtube.force-ssl '
|
||||
'plus.me '
|
||||
'emeraldsea.mobileapps.doritos.cookie '
|
||||
'plus.stream.read '
|
||||
'plus.stream.write '
|
||||
'plus.pages.manage '
|
||||
'identity.plus.page.impersonation',
|
||||
))
|
||||
TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
|
||||
TOKEN_TYPES = {
|
||||
0: 'tv',
|
||||
|
|
|
|||
|
|
@ -790,6 +790,31 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
'-1': ('original', 'main', -6),
|
||||
}
|
||||
|
||||
FAILURE_REASONS = {
|
||||
'abort': frozenset((
|
||||
'country',
|
||||
'not available',
|
||||
)),
|
||||
'auth': frozenset((
|
||||
'not a bot',
|
||||
'please sign in',
|
||||
)),
|
||||
'reauth': frozenset((
|
||||
'confirm your age',
|
||||
'inappropriate',
|
||||
'member',
|
||||
)),
|
||||
'retry': frozenset((
|
||||
'try again later',
|
||||
'unavailable',
|
||||
'unknown',
|
||||
)),
|
||||
'skip': frozenset((
|
||||
'error code: 6',
|
||||
'latest version',
|
||||
)),
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
context,
|
||||
clients=None,
|
||||
|
|
@ -826,18 +851,18 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
self._auth_client = {}
|
||||
self._client_groups = (
|
||||
('custom', clients if clients else ()),
|
||||
('auth_enabled_initial_request', (
|
||||
('auth_enabled|initial_request|no_playable_streams', (
|
||||
'tv_embed',
|
||||
'tv_unplugged',
|
||||
'tv',
|
||||
)),
|
||||
('auth_disabled_kids_vp9_avc1', (
|
||||
('auth_disabled|kids|av1|vp9|vp9.2|avc1|stereo_sound|multi_audio', (
|
||||
'ios_testsuite_params',
|
||||
)),
|
||||
('auth_disabled_kids_av1_avc1', (
|
||||
('auth_disabled|kids|av1|vp9.2|avc1|surround_sound|multi_audio', (
|
||||
'android_testsuite_params',
|
||||
)),
|
||||
('auth_enabled_no_kids', (
|
||||
('auth_enabled|no_kids|av1|vp9.2|avc1|surround_sound', (
|
||||
'android_vr',
|
||||
)),
|
||||
('mpd', (
|
||||
|
|
@ -1285,7 +1310,11 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
else:
|
||||
new_url = url
|
||||
|
||||
new_url = self._process_url_params(new_url, mpd=False)
|
||||
new_url = self._process_url_params(new_url,
|
||||
mpd=False,
|
||||
headers=headers,
|
||||
referrer=None,
|
||||
visitor_data=None)
|
||||
if not new_url:
|
||||
continue
|
||||
|
||||
|
|
@ -1622,26 +1651,7 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
responses = {}
|
||||
stream_list = {}
|
||||
|
||||
abort_reasons = {
|
||||
'country',
|
||||
'not available',
|
||||
}
|
||||
reauth_reasons = {
|
||||
'confirm your age',
|
||||
'inappropriate',
|
||||
'please sign in',
|
||||
'not a bot',
|
||||
'member',
|
||||
}
|
||||
skip_reasons = {
|
||||
'latest version',
|
||||
'error code: 6',
|
||||
}
|
||||
retry_reasons = {
|
||||
'try again later',
|
||||
'unavailable',
|
||||
'unknown',
|
||||
}
|
||||
fail = self.FAILURE_REASONS
|
||||
abort = False
|
||||
|
||||
logged_in = self.logged_in
|
||||
|
|
@ -1670,17 +1680,17 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
for name, clients in self._client_groups:
|
||||
if not clients:
|
||||
continue
|
||||
if name == 'auth_enabled_initial_request':
|
||||
if name == 'mpd' and not use_mpd:
|
||||
continue
|
||||
if name == 'ask' and use_mpd and not ask_for_quality:
|
||||
continue
|
||||
if name.startswith('auth_enabled|initial_request'):
|
||||
if visitor_data and not logged_in:
|
||||
continue
|
||||
allow_skip = False
|
||||
client_data['_auth_requested'] = True
|
||||
else:
|
||||
allow_skip = True
|
||||
if name == 'mpd' and not use_mpd:
|
||||
continue
|
||||
if name == 'ask' and use_mpd and not ask_for_quality:
|
||||
continue
|
||||
|
||||
exclude_retry = set()
|
||||
restart = None
|
||||
|
|
@ -1792,8 +1802,17 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
video_id=video_id,
|
||||
client=_client_name,
|
||||
has_auth=_has_auth)
|
||||
compare_reason = _reason.lower()
|
||||
if any(why in compare_reason for why in reauth_reasons):
|
||||
fail_reason = _reason.lower()
|
||||
if any(why in fail_reason for why in fail['auth']):
|
||||
if _has_auth:
|
||||
restart = False
|
||||
elif restart is None and logged_in:
|
||||
client_data['_auth_requested'] = True
|
||||
restart = True
|
||||
else:
|
||||
continue
|
||||
break
|
||||
elif any(why in fail_reason for why in fail['reauth']):
|
||||
if _client.get('_auth_required') == 'ignore_fail':
|
||||
continue
|
||||
elif client_data.get('_auth_required'):
|
||||
|
|
@ -1803,13 +1822,13 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
client_data['_auth_required'] = True
|
||||
restart = True
|
||||
break
|
||||
if any(why in compare_reason for why in abort_reasons):
|
||||
elif any(why in fail_reason for why in fail['abort']):
|
||||
abort = True
|
||||
break
|
||||
if any(why in compare_reason for why in skip_reasons):
|
||||
elif any(why in fail_reason for why in fail['skip']):
|
||||
if allow_skip:
|
||||
break
|
||||
if any(why in compare_reason for why in retry_reasons):
|
||||
elif any(why in fail_reason for why in fail['retry']):
|
||||
continue
|
||||
else:
|
||||
self.log.warning('Unknown playabilityStatus: {status!r}',
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from re import compile as re_compile
|
|||
|
||||
from ...kodion import logging
|
||||
from ...kodion.compatibility import parse_qsl, unescape, urlencode, urlsplit
|
||||
from ...kodion.constants import YOUTUBE_HOSTNAMES
|
||||
from ...kodion.network import BaseRequestsClass
|
||||
|
||||
|
||||
|
|
@ -63,16 +64,16 @@ class YouTubeResolver(AbstractResolver):
|
|||
r'|(?P<is_clip>"clipConfig":\{)'
|
||||
r'|("startTimeMs":"(?P<start_time>\d+)")'
|
||||
r'|("endTimeMs":"(?P<end_time>\d+)")')
|
||||
_RE_MUSIC_VIDEO_ID = re_compile(r'"INITIAL_ENDPOINT":.+?videoId\\":\\"'
|
||||
r'(?P<video_id>[^\\"]+)'
|
||||
r'\\"')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(YouTubeResolver, self).__init__(*args, **kwargs)
|
||||
|
||||
def supports_url(self, url, url_components):
|
||||
if url_components.hostname not in {
|
||||
'www.youtube.com',
|
||||
'youtube.com',
|
||||
'm.youtube.com',
|
||||
}:
|
||||
hostname = url_components.hostname
|
||||
if hostname not in YOUTUBE_HOSTNAMES:
|
||||
return False
|
||||
|
||||
path = url_components.path.lower()
|
||||
|
|
@ -91,10 +92,14 @@ class YouTubeResolver(AbstractResolver):
|
|||
'/redirect',
|
||||
'/shorts',
|
||||
'/supported_browsers',
|
||||
'/watch',
|
||||
)):
|
||||
return 'HEAD'
|
||||
|
||||
if path.startswith('/watch'):
|
||||
if hostname.startswith('music.'):
|
||||
return 'GET'
|
||||
return 'HEAD'
|
||||
|
||||
# user channel in the form of youtube.com/username
|
||||
path = path.strip('/').split('/', 1)
|
||||
return 'GET' if len(path) == 1 and path[0] else False
|
||||
|
|
@ -192,7 +197,17 @@ class YouTubeResolver(AbstractResolver):
|
|||
query=urlencode(new_params)
|
||||
).geturl()
|
||||
|
||||
# we try to extract the channel id from the html content
|
||||
# try to extract the real videoId from the html content
|
||||
elif method == 'GET' and url_components.hostname.startswith('music.'):
|
||||
match = self._RE_MUSIC_VIDEO_ID.search(response_text)
|
||||
if match:
|
||||
params = dict(parse_qsl(url_components.query))
|
||||
params['v'] = match.group('video_id')
|
||||
return url_components._replace(
|
||||
query=urlencode(params)
|
||||
).geturl()
|
||||
|
||||
# try to extract the channel id from the html content
|
||||
# With the channel id we can construct a URL we already work with
|
||||
# https://www.youtube.com/channel/<CHANNEL_ID>
|
||||
elif method == 'GET':
|
||||
|
|
@ -217,11 +232,7 @@ class CommonResolver(AbstractResolver):
|
|||
super(CommonResolver, self).__init__(*args, **kwargs)
|
||||
|
||||
def supports_url(self, url, url_components):
|
||||
if url_components.hostname in {
|
||||
'www.youtube.com',
|
||||
'youtube.com',
|
||||
'm.youtube.com',
|
||||
}:
|
||||
if url_components.hostname in YOUTUBE_HOSTNAMES:
|
||||
return False
|
||||
return 'HEAD'
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ from ...kodion.constants import (
|
|||
START,
|
||||
VIDEO_ID,
|
||||
VIDEO_IDS,
|
||||
YOUTUBE_HOSTNAMES,
|
||||
)
|
||||
from ...kodion.items import DirectoryItem, UriItem, VideoItem
|
||||
from ...kodion.utils.convert_format import duration_to_seconds
|
||||
|
|
@ -41,11 +42,6 @@ class UrlToItemConverter(object):
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
RE_PATH_ID = re_compile(r'/[^/]*?[/@](?P<id>[^/?#]+)', IGNORECASE)
|
||||
VALID_HOSTNAMES = {
|
||||
'youtube.com',
|
||||
'www.youtube.com',
|
||||
'm.youtube.com',
|
||||
}
|
||||
|
||||
def __init__(self, flatten=True):
|
||||
self._flatten = flatten
|
||||
|
|
@ -67,7 +63,7 @@ class UrlToItemConverter(object):
|
|||
def add_url(self, url):
|
||||
parsed_url = urlsplit(url)
|
||||
if (not parsed_url.hostname
|
||||
or parsed_url.hostname.lower() not in self.VALID_HOSTNAMES):
|
||||
or parsed_url.hostname.lower() not in YOUTUBE_HOSTNAMES):
|
||||
self.log.debug('Unknown hostname "{hostname}" in url "{url}"',
|
||||
hostname=parsed_url.hostname,
|
||||
url=url)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,13 @@ from ...kodion.constants import (
|
|||
PATHS,
|
||||
PLAYLIST_ID,
|
||||
)
|
||||
from ...kodion.items import AudioItem, CommandItem, DirectoryItem, menu_items
|
||||
from ...kodion.items import (
|
||||
AudioItem,
|
||||
CommandItem,
|
||||
DirectoryItem,
|
||||
MediaItem,
|
||||
menu_items,
|
||||
)
|
||||
from ...kodion.utils.convert_format import friendly_number, strip_html_from_text
|
||||
from ...kodion.utils.datetime import (
|
||||
get_scheduled_start,
|
||||
|
|
@ -401,7 +407,8 @@ def update_playlist_items(provider, context, playlist_id_dict,
|
|||
show_details = settings.show_detailed_description()
|
||||
item_count_color = settings.get_label_color('itemCount')
|
||||
|
||||
fanart_type = context.get_param(FANART_TYPE)
|
||||
params = context.get_params()
|
||||
fanart_type = params.get(FANART_TYPE)
|
||||
if fanart_type is None:
|
||||
fanart_type = settings.fanart_selection()
|
||||
thumb_size = settings.get_thumbnail_size()
|
||||
|
|
@ -443,6 +450,7 @@ def update_playlist_items(provider, context, playlist_id_dict,
|
|||
cxm_play_recently_added = menu_items.playlist_play_recently_added(context)
|
||||
cxm_view_playlist = menu_items.playlist_view(context)
|
||||
cxm_play_shuffled_playlist = menu_items.playlist_shuffle(context)
|
||||
cxm_refresh_listing = menu_items.refresh_listing(context, path, params)
|
||||
cxm_remove_saved_playlist = menu_items.playlist_remove_from_library(context)
|
||||
cxm_save_playlist = (
|
||||
menu_items.playlist_save_to_library(context)
|
||||
|
|
@ -585,6 +593,7 @@ def update_playlist_items(provider, context, playlist_id_dict,
|
|||
cxm_play_recently_added,
|
||||
cxm_view_playlist,
|
||||
cxm_play_shuffled_playlist,
|
||||
cxm_refresh_listing,
|
||||
cxm_separator,
|
||||
cxm_save_playlist,
|
||||
menu_items.bookmark_add(
|
||||
|
|
@ -1238,6 +1247,7 @@ if PREFER_WEBP_THUMBS:
|
|||
THUMB_URL = 'https://i.ytimg.com/vi_webp/{0}/{1}{2}.webp'
|
||||
else:
|
||||
THUMB_URL = 'https://i.ytimg.com/vi/{0}/{1}{2}.jpg'
|
||||
RE_CUSTOM_THUMB = re_compile(r'_custom_[0-9]')
|
||||
THUMB_TYPES = {
|
||||
'default': {
|
||||
'name': 'default',
|
||||
|
|
@ -1326,10 +1336,17 @@ def get_thumbnail(thumb_size, thumbnails, default_thumb=None):
|
|||
url = (thumbnail[1] if is_dict else thumbnail).get('url')
|
||||
if not url:
|
||||
return default_thumb
|
||||
if PREFER_WEBP_THUMBS and '/vi_webp/' not in url and '?' not in url:
|
||||
url = url.replace('/vi/', '/vi_webp/', 1).replace('.jpg', '.webp', 1)
|
||||
if url.startswith('//'):
|
||||
url = 'https:' + url
|
||||
if '?' in url:
|
||||
url = urlsplit(url)
|
||||
url = url._replace(
|
||||
netloc='i.ytimg.com',
|
||||
path=RE_CUSTOM_THUMB.sub('', url.path),
|
||||
query=None,
|
||||
).geturl()
|
||||
elif PREFER_WEBP_THUMBS and '/vi_webp/' not in url:
|
||||
url = url.replace('/vi/', '/vi_webp/', 1).replace('.jpg', '.webp', 1)
|
||||
return url
|
||||
|
||||
|
||||
|
|
@ -1360,6 +1377,7 @@ def add_related_video_to_playlist(provider, context, client, v3, video_id):
|
|||
next_item = next((
|
||||
item for item in result_items
|
||||
if (item
|
||||
and isinstance(item, MediaItem)
|
||||
and not any((
|
||||
item.get_uri() == playlist_item.get('file')
|
||||
or item.get_name() == playlist_item.get('title')
|
||||
|
|
|
|||
|
|
@ -364,8 +364,12 @@ def _process_list_response(provider,
|
|||
item.available = yt_item.get('_available', False)
|
||||
|
||||
elif kind_type == 'playlistitem':
|
||||
playlist_item_id = item_id
|
||||
video_id = snippet['resourceId']['videoId']
|
||||
video_id = snippet.get('resourceId', {}).get('videoId')
|
||||
if video_id:
|
||||
playlist_item_id = item_id
|
||||
else:
|
||||
video_id = item_id
|
||||
playlist_item_id = None
|
||||
channel_id = (snippet.get('videoOwnerChannelId')
|
||||
or snippet.get('channelId'))
|
||||
playlist_id = snippet.get('playlistId')
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ def _play_stream(provider, context):
|
|||
ask_for_quality = settings.ask_for_video_quality()
|
||||
if ui.pop_property(PLAY_PROMPT_QUALITY) and not screensaver:
|
||||
ask_for_quality = True
|
||||
elif ui.pop_property(PLAY_FORCE_AUDIO):
|
||||
if ui.pop_property(PLAY_FORCE_AUDIO):
|
||||
audio_only = True
|
||||
else:
|
||||
audio_only = settings.audio_only()
|
||||
|
|
@ -97,6 +97,7 @@ def _play_stream(provider, context):
|
|||
|
||||
if not streams:
|
||||
ui.show_notification(context.localize('error.no_streams_found'))
|
||||
logging.debug('No streams found')
|
||||
return False
|
||||
|
||||
stream = _select_stream(
|
||||
|
|
@ -106,7 +107,6 @@ def _play_stream(provider, context):
|
|||
audio_only=audio_only,
|
||||
use_mpd=use_mpd,
|
||||
)
|
||||
|
||||
if stream is None:
|
||||
return False
|
||||
|
||||
|
|
@ -345,7 +345,6 @@ def _select_stream(context,
|
|||
|
||||
stream_list.sort(key=_stream_sort, reverse=True)
|
||||
num_streams = len(stream_list)
|
||||
ask_for_quality = ask_for_quality and num_streams >= 1
|
||||
|
||||
if logging.debugging:
|
||||
def _default_NA():
|
||||
|
|
@ -362,7 +361,7 @@ def _select_stream(context,
|
|||
idx=idx,
|
||||
stream=defaultdict(_default_NA, stream))
|
||||
|
||||
if ask_for_quality:
|
||||
if ask_for_quality and num_streams > 1:
|
||||
selected_stream = context.get_ui().on_select(
|
||||
context.localize('select_video_quality'),
|
||||
[stream['title'] for stream in stream_list],
|
||||
|
|
|
|||
|
|
@ -242,11 +242,13 @@ def _process_disliked_videos(provider, context, client):
|
|||
|
||||
def _process_live_events(provider, context, client, event_type='live'):
|
||||
# TODO: cache result
|
||||
params = context.get_params()
|
||||
json_data = client.get_live_events(
|
||||
event_type=event_type,
|
||||
order='date' if event_type == 'upcoming' else 'viewCount',
|
||||
page_token=context.get_param('page_token', ''),
|
||||
location=context.get_param('location', False),
|
||||
order=params.get('order',
|
||||
'date' if event_type == 'upcoming' else 'viewCount'),
|
||||
page_token=params.get('page_token', ''),
|
||||
location=params.get('location', False),
|
||||
after={'days': 3} if event_type == 'completed' else None,
|
||||
)
|
||||
if not json_data:
|
||||
|
|
|
|||
|
|
@ -1488,13 +1488,14 @@ class Provider(AbstractProvider):
|
|||
# watch later
|
||||
if settings_bool(settings.SHOW_WATCH_LATER, True):
|
||||
if watch_later_id:
|
||||
path = (
|
||||
(PATHS.VIRTUAL_PLAYLIST, watch_later_id)
|
||||
if watch_later_id.lower() == 'wl' else
|
||||
(PATHS.MY_PLAYLIST, watch_later_id)
|
||||
)
|
||||
watch_later_item = DirectoryItem(
|
||||
localize('watch_later'),
|
||||
create_uri(
|
||||
(PATHS.VIRTUAL_PLAYLIST, watch_later_id)
|
||||
if watch_later_id.lower() == 'wl' else
|
||||
(PATHS.MY_PLAYLIST, watch_later_id)
|
||||
),
|
||||
create_uri(path),
|
||||
image='{media}/watch_later.png',
|
||||
)
|
||||
context_menu = [
|
||||
|
|
@ -1510,6 +1511,9 @@ class Provider(AbstractProvider):
|
|||
menu_items.playlist_shuffle(
|
||||
context, watch_later_id
|
||||
),
|
||||
menu_items.refresh_listing(
|
||||
context, path, {}
|
||||
),
|
||||
]
|
||||
watch_later_item.add_context_menu(context_menu)
|
||||
result.append(watch_later_item)
|
||||
|
|
@ -1541,9 +1545,10 @@ class Provider(AbstractProvider):
|
|||
playlists = resource_manager.get_related_playlists('mine')
|
||||
if playlists and 'likes' in playlists:
|
||||
liked_list_id = playlists['likes'] or 'LL'
|
||||
path = (PATHS.VIRTUAL_PLAYLIST, liked_list_id)
|
||||
liked_videos_item = DirectoryItem(
|
||||
localize('video.liked'),
|
||||
create_uri((PATHS.VIRTUAL_PLAYLIST, liked_list_id)),
|
||||
create_uri(path),
|
||||
image='{media}/likes.png',
|
||||
)
|
||||
context_menu = [
|
||||
|
|
@ -1559,6 +1564,9 @@ class Provider(AbstractProvider):
|
|||
menu_items.playlist_shuffle(
|
||||
context, liked_list_id
|
||||
),
|
||||
menu_items.refresh_listing(
|
||||
context, path, {}
|
||||
),
|
||||
]
|
||||
liked_videos_item.add_context_menu(context_menu)
|
||||
result.append(liked_videos_item)
|
||||
|
|
@ -1575,13 +1583,14 @@ class Provider(AbstractProvider):
|
|||
# history
|
||||
if settings_bool(settings.SHOW_HISTORY, True):
|
||||
if history_id:
|
||||
path = (
|
||||
(PATHS.VIRTUAL_PLAYLIST, history_id)
|
||||
if history_id.lower() == 'hl' else
|
||||
(PATHS.MY_PLAYLIST, history_id)
|
||||
)
|
||||
watch_history_item = DirectoryItem(
|
||||
localize('history'),
|
||||
create_uri(
|
||||
(PATHS.VIRTUAL_PLAYLIST, history_id)
|
||||
if history_id.lower() == 'hl' else
|
||||
(PATHS.MY_PLAYLIST, history_id)
|
||||
),
|
||||
create_uri(path),
|
||||
image='{media}/history.png',
|
||||
)
|
||||
context_menu = [
|
||||
|
|
@ -1597,6 +1606,9 @@ class Provider(AbstractProvider):
|
|||
menu_items.playlist_shuffle(
|
||||
context, history_id
|
||||
),
|
||||
menu_items.refresh_listing(
|
||||
context, path, {}
|
||||
),
|
||||
]
|
||||
watch_history_item.add_context_menu(context_menu)
|
||||
result.append(watch_history_item)
|
||||
|
|
@ -2210,7 +2222,6 @@ class Provider(AbstractProvider):
|
|||
attrs = (
|
||||
'_resource_manager',
|
||||
'_client',
|
||||
'_api_check',
|
||||
)
|
||||
for attr in attrs:
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue