Merge pull request #1317 from MoojMidge/nexus-unofficial

Nexus unofficial v7.3.0+beta.7
This commit is contained in:
MoojMidge 2025-10-19 15:08:37 +11:00 committed by GitHub
commit 174727a6ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 648 additions and 476 deletions

View file

@ -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"/>

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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):
"""

View file

@ -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:

View file

@ -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,

View file

@ -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()

View file

@ -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:

View file

@ -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,
},
},
))

View file

@ -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)

View file

@ -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]

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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):

View file

@ -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:

View file

@ -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:

View file

@ -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',

View file

@ -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}',

View file

@ -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'

View file

@ -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)

View file

@ -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')

View file

@ -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')

View file

@ -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],

View file

@ -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:

View file

@ -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: