diff --git a/plugin.video.youtube/addon.xml b/plugin.video.youtube/addon.xml index 2c3ef4bec7..4a6f9ef03c 100644 --- a/plugin.video.youtube/addon.xml +++ b/plugin.video.youtube/addon.xml @@ -1,5 +1,5 @@ - + @@ -27,6 +27,7 @@ https://github.com/anxdpanic/plugin.video.youtube true Добавка за YouTube + Dodatak za YouTube Plugin pro YouTube Plugin til YouTube Plugin für YouTube @@ -38,6 +39,7 @@ YouTube lisamoodul YouTube-lisäosa Plugin pour YouTube + Breiseán do YouTube תוסף עבור YouTube Dodatak za YouTube YouTube kiegészítő @@ -58,6 +60,7 @@ 油管插件 YouTube附加元件 YouTube е един от най-големите уеб сайтове за споделяне на видео в целия свят. + YouTube je jedna od najvećih svjetskih web stranica za dijeljenje videozapisa. YouTube je jedna z největších webových stránek světa sdílející video. YouTube er et af verdens største videodelingswebsteder. YouTube ist eines der größten Video-Sharing-Websites der Welt. @@ -86,6 +89,7 @@ YouTube là một trong những trang web chia sẻ video lớn nhất trên thế giới. 油管是世界上最大的视频分享网站之一。 Youtube 是全世界最大的影片分享網站。 + Ovaj dodatak nije odobren od strane Googlea Tento plugin není schválen společností Google Dette plugin er ikke godkendt af Google Dieses Plugin ist nicht von Google befürwortet @@ -108,34 +112,14 @@ Tento doplnok nie je schválený spoločnosťou Google Denna insticksmodul är inte godkänd av Google Bu eklenti Google tarafından üretilmemiştir - Цей плагін не має відношення до компанії Google + Цей додаток не має відношення до компанії Google Plugin này không được xác nhận bởi Google 此插件未被谷歌认可 此附加元件未由Google支持 - ## v7.2.0.3 + ## v7.3.0.1 ### Fixed -- Fix performance regression with channel filters -- Fix regression preventing video playback from Kodi Info dialog -- Fix exception in youtube.helper.v3.response_to_items when using Kodi 18 (Python 2) -- Fix exception in YouTubeRequestClient.json_traverse when using Kodi 18 (Python 2) -- Fix handling of custom Watch Later playlist in add to playlist dialog -- Fix Liked video playlist not loading -- Fix typo preventing some video only searches from working -- Fix incorrect deletion of provider parameter in 10c3758 -- Fix possible hangs in multiple busy dialog workaround -- Fix misidentifying channels in channel filter -- Attempt to avoid race condition where user input occurs prior to window rerouting -- Fix incorrectly referencing channels as playlist in My Subscriptions after db44928 -- Improve handling of invalid playlists in My Subscriptions after db44928 -- Update YouTube.get_related_videos and YouTube.get_related_for_home -- Fix error with listing comments if number of replies is 1000+ -- Fix regression in opening playlists of logged in user -- Fix regressions in checking channel filters -- Clear old function cache values on initial run after install -- Fix My Subscriptions not loading old or previously accessed content -- Manually redirect video server requests if required -- Improve logging of client ID -- Fix possible exception that may occur if video thumbnails fail to update -- Fix unsubscribing from My Subscriptions -- Fix folders for channel shorts and live stream +- Workaround crash caused by non-thread safe behaviour of sqlite connection when used as context manager + +### Changed +- Delay gc collection rather than disabling diff --git a/plugin.video.youtube/changelog.txt b/plugin.video.youtube/changelog.txt index 82931cdc6a..351f6297fa 100644 --- a/plugin.video.youtube/changelog.txt +++ b/plugin.video.youtube/changelog.txt @@ -1,3 +1,453 @@ +## v7.3.0.1 +### Fixed +- Workaround crash caused by non-thread safe behaviour of sqlite connection when used as context manager #1346 + +### Changed +- Delay gc collection rather than disabling + +## v7.3.0 +### Fixed +- Revert change to busy polling interval #1339 +- Prune invalid entries from DB when closing connection #1331 +- Fix regressions with SQLite db operations #1331 +- Disable label masks being used in Kodi 18 #1327 +- Python 2 compatibility workaround for lack of timeout when trying to acquire an RLock #1327 +- More expansive handling of inconsistent urllib3 exception re-raising +- Fix regression in handling audio only setting after d154325c5b672dccc6a17413063cfdeb32256ffd +- Fix comments not using correct sort methods +- Fix incorrectly using playlist cache entries that have been invalidated by playlist modification +- Fix some context menu actions failing for video item bookmarks +- Ensure listings and items added by the addon have correct sort order +- Fix resetting client region when playing media with subtitles enabled +- 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 +- Fix typo in YouTubePlayerClient error hook +- Fix not resolving single playable items when using the uri2addon plugin endpoint #1300 +- Correctly check whether access tokens are available to be used for player requests +- Fix not correctly resetting client instance +- Dont restore container position on forced refresh when playback ends +- Better handle urllib3 re-raising low level errors but sometimes not +- Ignore unused parameters in item constructors #1282 +- Various misc fixes for focus and position loss on refresh +- Fix possible unnecessary listing refresh after playlist action +- Don't check items added to non music or video playlists +- Re-enable setting for displaying saved playlists #1023 +- Fix exceptions with using non-existent request response as context manager #1279 +- Use different default player client request which provides more captions in response #1250 +- Exclude retrying player clients that do not support authentication if authentication is required #1273 +- Only request authenticated player request once, if not otherwise required #1273 +- Fix not updating breadcrumb after certain context menu actions +- Fix setting focus on items in listing when parent item is not shown #1012 +- Reduce CPU usage of service runner loop when idle +- Simplify window history fallback for search inputs #1070 #1266 +- Fix MPD quality selection #1268 +- Fix stream feature for disabling HFR at max resolution #539 +- Don't re-raise BrokenPipeError in RequestHandler.handle_one_request #1259 +- Fix including details in label2 mask when video details in listings is disabled #1265 +- Fix incorrect modification of custom thumbnails #1245 +- Refresh stale cached entries if new player data is available #1259 +- Disable use of captions from clients that are sometimes aggressively rate limited #1250 +- Switch browse client for recommended videos #1254 +- Ignore failing player requests that require signing in but won't accept OAuth2 authentication #1254 +- Improve querying of GUI info to work with widgets and custom windows #1243 +- Fix using locale specific abbreviations for weekday and month in If-Modified-Since header #1246 +- Workaround various Kodi 18 and Python 2 issues #1246 +- Fix inconsistencies between item IDs used as params that could result in exceptions +- Fix generated page token not working for first page in listing +- Don't replace non-standard JPEG thumbnails with WebP thumbnails #1245 +- Fix not parsing infolabels used in plugin url path #1239 #1243 +- Fix not parsing infolabels used as plugin url query params #1239 +- Fix unnecessarily processing window properties #1238 +- Workaround for distributions that patch Kodi to disable System.InternetState #1224 +- Workaround issue with search not returning items if no search query is used +- Misc updates to try and prevent some GVS requests from failing #1222 +- Fix not correctly updating channel details of subscriptions #1226 +- Redact IP address in stream urls when not using InputStream.Adaptive +- Fix implementation of logging module debugging property +- Python 2 compatibility fix to automatically handle timezones when determining timestamps +- Fix not skipping invalid items in plugin list response +- Fix not showing Create bookmark item if bookmarks list is empty +- Python 2 compatibility fixes for pretty print logging and lack of datetime.datetime.timestamp +- Fix typo resulting in spatial audio streams not being correctly enabled/disabled +- Ensure DELETE API requests are properly authorised #1226 +- Update XbmcContext.localize to handle string interpolation #1225 +- Attempt to mitigate possible memory leaks associated with use of Python Requests +- Ensure plugin folders in channel listings are not processed as YouTube items +- Fix not refreshing cached data when not logged in #1224 +- Fix potentially misidentifying vp9.2 video streams #1222 +- Fix not fully redacting logged stream data +- Fix clip start and end time parsing +- Fix possible exception when fetching subtitles after http server fails to wakeup +- Improve concurrent JSON file IO operations #1218 +- Change pruning of sqlite databases to avoid potential deadlocks #1161 +- Avoid potential deadlock in v3._process_list_response #1161 +- Improve listing pre and post fill methods #1161 +- Fix repeating previous V1 browse/next requests when no continuation is available #1161 +- Fix typo in allowable search parameters +- Ensure best available quality is used for thumbnails as fanart #1212 +- Fix double forward slash in channel url in description +- Fix toggling watched status on non-forced refresh +- Ensure logged in status is properly (re)set when client is reset #1210 +- Don't rely on dict insertion order for client groups #1185 +- Ensure authenticated requests are used only when necessary #1210 #1196 +- Workaround playlistNotFound error in Related Videos #1210 #1196 +- Fix stream quality checks incorrectly identifying 480p streams as 360p +- Add workarounds for various Kodi/ISA subtitle incompatibilities and formatting issues #489 #1147 +- Identify API requests requiring authentication +- Fix incorrect page number used when post filling listings +- Prevent unnecessary API requests when refreshing listing that uses channel filters +- Improve loading of malformed/partial access_manager.json #1173 + +### Changed +- Improve robustness of fetching recommended and related videos +- Improve workarounds for SQLite concurrency issues +- Remove possibly invalid access token if an authentication error occurs +- Better organise and use standard labels for http server address and port settings +- Try to make http server IP address selection even more obvious when running Setup Wizard #1320 +- Improve logging of errors caused by localised strings that have been incorrectly translated +- Improve offline access to cached data +- Updates to SQLite database lock handling +- Ignore player request failures that may incorrectly indicate a need to sign-in #1312 +- Include playlist_id listitem property for items from virtual playlists +- Don't list users own playlists in listing of saved playlists +- Allow sign-in when partially logged in without needing to sign-out +- Identify if user is only partially logged in +- Use persistent visitor data where possible except when incognito +- Allow additional query parameters to be inherited from parent listing #1282 +- Improve process for initial player request if remote history not enabled #1273 +- Disable unusable player clients #1273 +- Disable multiple busy dialog crash workarounds in Kodi 22 +- Include visitorData in subtitle request headers along with referer #1250 +- Revert use of WEBP thumbnails #1245 +- Improve notification of player request errors #1254 #1262 + - Add notification if reCaptcha check is required +- Improve workarounds for failing authorised player requests #1254 +- Allow plugin url query parameters to hide folders in search and channel playlist listing #1251 +- Use WebP thumbnails instead of JPEG thumbnails +- Update Setup Wizard for the various new Stream Features that have been added +- Don't cache playback associated requests +- Bypass requests cache when a listing is refreshed +- Update order and wording of prompts used when adding custom bookmark +- Allow bookmarking channel of videos within Bookmarks list +- Return empty listing on failure to parse YouTube url +- Update known itag details +- Do not show notification when automatically removing played video from Watch Later list +- Add visitor ID to stream headers #1222 +- Improve logging of adaptive streams from player requests #1222 +- Identify and de-prioritise DRC audio streams +- Specifically use elapsed time for plugin profiling if available +- Use new BookmarkItem class for custom bookmarks to allow updating details like normal bookmarks +- Default to list as path command if not given when navigating to various internal listings +- Re-enable http server idle shutdown for all platforms +- Update default cache size to 50 MiB +- Enable caching of YouTube virtual lists #1220 +- Improve fetching of cached playlist items #1220 +- Allow for use of last known good value if JSON file read IPC timeout occurs #1220 +- Limit JSON-RPC usage when interacting with Kodi playlist #1220 +- Improve syncing local history with Kodi play count +- api_keys.json not backwards compatible with older addon versions +- Set default live stream type to adaptive HLS +- Allow saving of play count and last played date for live streams +- Disable script.trakt scrobbling when playing YouTube videos +- Allow failure of unauthorised player requests to be ignored #1211 +- Try to prevent misuse of strm files #1208 +- Update thumbnails size settings and selection logic #1204 +- Only show folders in channel playlists listing on first page and if not hidden +- Improve cache performance and reliability +- Improve management of user data stored as JSON files + +### 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 +- Add support for listing members only content of channels +- Add selections to hide various folders from listings #1282 +- Add support for additional OAuth2 client to allow playback of age restricted videos #1273 +- Add dubbed audio preferences to stream features #1036 #1228 #1232 +- Add support for directly adding YouTube URL as custom bookmark +- Add support for 3D/VR video and spatial audio in stream selections +- Add Setup Wizard steps to change custom Watch History and Watch Later playlists to internal YouTube lists #1210 +- Add support for viewing YouTube history list (HL) +- Add support for podcasts in related videos #1161 +- Add notification on successfully adding item to local Watch Later list #1210 +- Add support for saving/removing playlists to/from YouTube library (Saved Playlists) #1023 +- Add support for removing liked videos from YouTube Liked videos list +- Add support for auto removing videos from YouTube Watch Later list after playback #1210 +- Add support for removing videos from YouTube Watch Later list #1210 +- Add support for adding videos to YouTube Watch Later list #1210 +- Initial support for viewing Saved Playlists #1023 +- Initial support for viewing YouTube Watch Later list (WL) #1210 +- Implement request cache +- Add ability to create custom bookmarks #1208 +- Add subtitle type selection to stream features +- Add plugin execution timeout to forcibly terminate execution after set time limit #1161 +- Improve addon logging using customised Python logging module + +## v7.3.0+beta.10 +### Fixed +- Prune invalid entries from DB when closing connection #1331 +- Fix regressions with SQLite db operations #1331 + +## v7.3.0+beta.9 +### Fixed +- Disable label masks being used in Kodi 18 #1327 +- Python 2 compatibility workaround for lack of timeout when trying to acquire an RLock #1327 +- More expansive handling of inconsistent urllib3 exception re-raising + +### Changed +- Improve robustness of fetching recommended and related videos +- Improve workarounds for SQLite concurrency issues +- Remove possibly invalid access token if an authentication error occurs +- Better organise and use standard labels for http server address and port settings +- Try to make http server IP address selection even more obvious when running Setup Wizard #1320 +- Improve logging of errors caused by localised strings that have been incorrectly translated + +## v7.3.0+beta.8 +### Fixed +- Fix regression in handling audio only setting after d154325c5b672dccc6a17413063cfdeb32256ffd +- Fix comments not using correct sort methods +- Fix incorrectly using playlist cache entries that have been invalidated by playlist modification +- Fix some context menu actions failing for video item bookmarks +- Ensure listings and items added by the addon have correct sort order +- Fix resetting client region when playing media with subtitles enabled + +### Changed +- Improve offline access to cached data +- Updates to SQLite database lock handling + +## 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 + +## v7.3.0+beta.5 +### Fixed +- Fix not resolving single playable items when using the uri2addon plugin endpoint #1300 +- Correctly check whether access tokens are available to be used for player requests +- Fix not correctly resetting client instance +- Dont restore container position on forced refresh when playback ends +- Better handle urllib3 re-raising low level errors but sometimes not +- Ignore unused parameters in item constructors #1282 + +### Changed +- Don't list users own playlists in listing of saved playlists +- Allow sign-in when partially logged in without needing to sign-out +- Identify if user is only partially logged in +- Use persistent visitor data where possible except when incognito + +## v7.3.0+beta.4 +### Fixed +- Various misc fixes for focus and position loss on refresh +- Fix possible unnecessary listing refresh after playlist action +- Don't check items added to non music or video playlists +- Re-enable setting for displaying saved playlists #1023 +- Fix exceptions with using non-existent request response as context manager #1279 +- Use different default player client request which provides more captions in response #1250 +- Exclude retrying player clients that do not support authentication if authentication is required #1273 +- Only request authenticated player request once, if not otherwise required #1273 +- Fix not updating breadcrumb after certain context menu actions +- Fix setting focus on items in listing when parent item is not shown #1012 +- Reduce CPU usage of service runner loop when idle +- Simplify window history fallback for search inputs #1070 #1266 +- Fix MPD quality selection #1268 +- Fix stream feature for disabling HFR at max resolution #539 +- Don't re-raise BrokenPipeError in RequestHandler.handle_one_request #1259 +- Fix including details in label2 mask when video details in listings is disabled #1265 +- Fix incorrect modification of custom thumbnails #1245 +- Refresh stale cached entries if new player data is available #1259 +- Disable use of captions from clients that are sometimes aggressively rate limited #1250 + +### Changed +- Allow additional query parameters to be inherited from parent listing #1282 +- Improve process for initial player request if remote history not enabled #1273 +- Disable unusable player clients #1273 +- Disable multiple busy dialog crash workarounds in Kodi 22 +- Include visitorData in subtitle request headers along with referer #1250 +- Revert use of WEBP thumbnails #1245 +- Improve notification of player request errors #1254 #1262 + - Add notification if reCaptcha check is required +- Improve workarounds for failing authorised player requests #1254 + +### New +- Add support for listing members only content of channels +- Add selections to hide various folders from listings #1282 +- Add support for additional OAuth2 client to allow playback of age restricted videos #1273 + +## v7.3.0+beta.3 +### Fixed +- Switch browse client for recommended videos #1254 +- Ignore failing player requests that require signing in but won't accept OAuth2 authentication #1254 +- Improve querying of GUI info to work with widgets and custom windows #1243 +- Fix using locale specific abbreviations for weekday and month in If-Modified-Since header #1246 +- Workaround various Kodi 18 and Python 2 issues #1246 +- Fix inconsistencies between item IDs used as params that could result in exceptions +- Fix generated page token not working for first page in listing +- Don't replace non-standard JPEG thumbnails with WebP thumbnails #1245 +- Fix not parsing infolabels used in plugin url path #1239 #1243 + +### Changed +- Allow plugin url query parameters to hide folders in search and channel playlist listing #1251 + +## v7.3.0+beta.2 +### Fixed +- Fix not parsing infolabels used as plugin url query params #1239 +- Fix unnecessarily processing window properties #1238 + +## v7.3.0+beta.1 +### Fixed +- Workaround for distributions that patch Kodi to disable System.InternetState #1224 +- Workaround issue with search not returning items if no search query is used +- Misc updates to try and prevent some GVS requests from failing #1222 + +### Changed +- Use WebP thumbnails instead of JPEG thumbnails +- Update Setup Wizard for the various new Stream Features that have been added +- Don't cache playback associated requests +- Bypass requests cache when a listing is refreshed +- Update order and wording of prompts used when adding custom bookmark + +### New +- Add dubbed audio preferences to stream features #1036 #1228 #1232 + +## v7.3.0+alpha.5 +### Fixed +- Fix not correctly updating channel details of subscriptions #1226 +- Redact IP address in stream urls when not using InputStream.Adaptive +- Fix implementation of logging module debugging property +- Python 2 compatibility fix to automatically handle timezones when determining timestamps +- Fix not skipping invalid items in plugin list response + +## v7.3.0+alpha.4 +### Fixed +- Fix not showing Create bookmark item if bookmarks list is empty +- Python 2 compatibility fixes for pretty print logging and lack of datetime.datetime.timestamp +- Fix typo resulting in spatial audio streams not being correctly enabled/disabled +- Ensure DELETE API requests are properly authorised #1226 +- Update XbmcContext.localize to handle string interpolation #1225 +- Attempt to mitigate possible memory leaks associated with use of Python Requests + +### Changed +- Allow bookmarking channel of videos within Bookmarks list +- Return empty listing on failure to parse YouTube url +- Update known itag details + +### New +- Add support for directly adding YouTube URL as custom bookmark + +## v7.3.0+alpha.3 +### Fixed +- Ensure plugin folders in channel listings are not processed as YouTube items +- Fix not refreshing cached data when not logged in #1224 +- Fix potentially misidentifying vp9.2 video streams #1222 +- Fix not fully redacting logged stream data + +### Changed +- Do not show notification when automatically removing played video from Watch Later list +- Add visitor ID to stream headers #1222 +- Improve logging of adaptive streams from player requests #1222 +- Identify and de-prioritise DRC audio streams +- Specifically use elapsed time for plugin profiling if available +- Use new BookmarkItem class for custom bookmarks to allow updating details like normal bookmarks +- Default to list as path command if not given when navigating to various internal listings + +### New +- Add support for 3D/VR video and spatial audio in stream selections + +## v7.3.0+alpha.2 +### Fixed +- Fix clip start and end time parsing +- Fix possible exception when fetching subtitles after http server fails to wakeup + +### Changed +- Re-enable http server idle shutdown for all platforms +- Update default cache size to 50 MiB +- Enable caching of YouTube virtual lists #1220 +- Improve fetching of cached playlist items #1220 +- Allow for use of last known good value if JSON file read IPC timeout occurs #1220 +- Limit JSON-RPC usage when interacting with Kodi playlist #1220 + +### New +- Add Setup Wizard steps to change custom Watch History and Watch Later playlists to internal YouTube lists #1210 + +## v7.3.0+alpha.1 +### Fixed +- Improve concurrent JSON file IO operations #1218 +- Change pruning of sqlite databases to avoid potential deadlocks #1161 +- Avoid potential deadlock in v3._process_list_response #1161 +- Improve listing pre and post fill methods #1161 +- Fix repeating previous V1 browse/next requests when no continuation is available #1161 +- Fix typo in allowable search parameters +- Ensure best available quality is used for thumbnails as fanart #1212 +- Fix double forward slash in channel url in description +- Fix toggling watched status on non-forced refresh +- Ensure logged in status is properly (re)set when client is reset #1210 +- Don't rely on dict insertion order for client groups #1185 +- Ensure authenticated requests are used only when necessary #1210 #1196 +- Workaround playlistNotFound error in Related Videos #1210 #1196 +- Fix stream quality checks incorrectly identifying 480p streams as 360p +- Add workarounds for various Kodi/ISA subtitle incompatibilities and formatting issues #489 #1147 +- Identify API requests requiring authentication +- Fix incorrect page number used when post filling listings +- Prevent unnecessary API requests when refreshing listing that uses channel filters +- Improve loading of malformed/partial access_manager.json #1173 + +### Changed +- Improve syncing local history with Kodi play count +- api_keys.json not backwards compatible with older addon versions +- Set default live stream type to adaptive HLS +- Allow saving of play count and last played date for live streams +- Disable script.trakt scrobbling when playing YouTube videos +- Allow failure of unauthorised player requests to be ignored #1211 +- Try to prevent misuse of strm files #1208 +- Update thumbnails size settings and selection logic #1204 +- Only show folders in channel playlists listing on first page and if not hidden +- Improve cache performance and reliability +- Improve management of user data stored as JSON files + +### New +- Add support for viewing YouTube history list (HL) +- Add support for podcasts in related videos #1161 +- Add notification on successfully adding item to local Watch Later list #1210 +- Add support for saving/removing playlists to/from YouTube library (Saved Playlists) #1023 +- Add support for removing liked videos from YouTube Liked videos list +- Add support for auto removing videos from YouTube Watch Later list after playback #1210 +- Add support for removing videos from YouTube Watch Later list #1210 +- Add support for adding videos to YouTube Watch Later list #1210 +- Initial support for viewing Saved Playlists #1023 +- Initial support for viewing YouTube Watch Later list (WL) #1210 +- Implement request cache +- Add ability to create custom bookmarks #1208 +- Add subtitle type selection to stream features +- Add plugin execution timeout to forcibly terminate execution after set time limit #1161 +- Improve addon logging using customised Python logging module + ## v7.2.0.3 ### Fixed - Fix performance regression with channel filters diff --git a/plugin.video.youtube/resources/__init__.py b/plugin.video.youtube/resources/__init__.py index f800be6022..ff15db69df 100644 --- a/plugin.video.youtube/resources/__init__.py +++ b/plugin.video.youtube/resources/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/language/resource.language.af_za/strings.po b/plugin.video.youtube/resources/language/resource.language.af_za/strings.po index 3e06b4e6ad..d4f8054837 100644 --- a/plugin.video.youtube/resources/language/resource.language.af_za/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.af_za/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.am_et/strings.po b/plugin.video.youtube/resources/language/resource.language.am_et/strings.po index 83f041af7f..6a2ab1ca3a 100644 --- a/plugin.video.youtube/resources/language/resource.language.am_et/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.am_et/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.ar_sa/strings.po b/plugin.video.youtube/resources/language/resource.language.ar_sa/strings.po index 854ed1f537..7dcbd3d07d 100644 --- a/plugin.video.youtube/resources/language/resource.language.ar_sa/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ar_sa/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.ast_es/strings.po b/plugin.video.youtube/resources/language/resource.language.ast_es/strings.po index 4042e497b4..130b7a23c6 100644 --- a/plugin.video.youtube/resources/language/resource.language.ast_es/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ast_es/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.az_az/strings.po b/plugin.video.youtube/resources/language/resource.language.az_az/strings.po index 908e3e73a2..59e3c802eb 100644 --- a/plugin.video.youtube/resources/language/resource.language.az_az/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.az_az/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.be_by/strings.po b/plugin.video.youtube/resources/language/resource.language.be_by/strings.po index 77dc02a3a7..af5987f7ae 100644 --- a/plugin.video.youtube/resources/language/resource.language.be_by/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.be_by/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.bg_bg/strings.po b/plugin.video.youtube/resources/language/resource.language.bg_bg/strings.po index 1403937a89..440dd20eaf 100644 --- a/plugin.video.youtube/resources/language/resource.language.bg_bg/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.bg_bg/strings.po @@ -106,8 +106,8 @@ msgid "144p" msgstr "" msgctxt "#30020" -msgid "Allow 3D" -msgstr "Разреши 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -150,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -216,11 +216,11 @@ msgid "Watch Later" msgstr "За гледане по-късно" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "Изход" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Премахване на \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -384,15 +384,15 @@ msgid "Add to..." msgstr "Добавяне към..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -460,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Възпроизведи с..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -488,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -552,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -608,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -648,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -668,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -688,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -700,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -752,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -784,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -976,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -996,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1136,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1152,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1172,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1184,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1252,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1532,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1583,6 +1583,14 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Разреши 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Възпроизведи с..." + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Не са намерени повече връзки." diff --git a/plugin.video.youtube/resources/language/resource.language.bs_ba/strings.po b/plugin.video.youtube/resources/language/resource.language.bs_ba/strings.po index d0faf34b2a..767370b67f 100644 --- a/plugin.video.youtube/resources/language/resource.language.bs_ba/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.bs_ba/strings.po @@ -7,27 +7,27 @@ msgstr "" "Project-Id-Version: XBMC-Addons\n" "Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2024-04-12 14:59+0000\n" -"Last-Translator: Anonymous \n" +"PO-Revision-Date: 2025-08-30 13:29+0000\n" +"Last-Translator: SecularSteve \n" "Language-Team: Bosnian (Bosnia and Herzegovina) \n" "Language: bs_ba\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 5.4.3\n" +"X-Generator: Weblate 5.13\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" -msgstr "" +msgstr "Dodatak za YouTube" msgctxt "Addon Description" msgid "YouTube is one of the biggest video-sharing websites of the world." -msgstr "" +msgstr "YouTube je jedna od najvećih svjetskih web stranica za dijeljenje videozapisa." msgctxt "Addon Disclaimer" msgid "This plugin is not endorsed by Google" -msgstr "" +msgstr "Ovaj dodatak nije odobren od strane Googlea" # msgctxt "Addon Summary" # msgid "Plugin for YouTube" @@ -50,151 +50,150 @@ msgstr "" msgctxt "#30003" msgid "YouTube" -msgstr "" +msgstr "YouTube" # empty strings from id 30004 to 30006 msgctxt "#30007" msgid "Use InputStream.Adaptive" -msgstr "" +msgstr "Koristite InputStream.Adaptive" msgctxt "#30008" msgid "Configure InputStream.Adaptive" -msgstr "" +msgstr "Konfigurišite InputStream.Adaptive" msgctxt "#30009" msgid "Always ask for the video quality" -msgstr "" +msgstr "Uvijek pitajte za kvalitet videa" msgctxt "#30010" msgid "Maximum video quality" -msgstr "" +msgstr "Maksimalni kvalitet videa" msgctxt "#30011" msgid "480p" -msgstr "" +msgstr "480p" msgctxt "#30012" msgid "720p (HD)" -msgstr "" +msgstr "720p (HD)" msgctxt "#30013" msgid "1080p (FHD)" -msgstr "" +msgstr "1080p (FHD)" msgctxt "#30014" msgid "2160p (4K)" -msgstr "" +msgstr "2160p (4K)" msgctxt "#30015" msgid "4320p (8K)" -msgstr "" +msgstr "4320p (8K)" msgctxt "#30016" msgid "240p" -msgstr "" +msgstr "240p" msgctxt "#30017" msgid "360p" -msgstr "" +msgstr "360p" msgctxt "#30018" msgid "1080p Live / 720p (HD)" -msgstr "" +msgstr "1080p Uživo / 720p (HD)" msgctxt "#30019" msgid "144p" -msgstr "" +msgstr "144p" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "" +msgid "Enable Stereoscopic 3D video" +msgstr "Omogući stereoskopski 3D video" msgctxt "#30021" msgid "Show fanart" -msgstr "" +msgstr "Prikaži fanart" msgctxt "#30022" msgid "Items per page" -msgstr "" +msgstr "Stavke po stranici" msgctxt "#30023" msgid "Search history size" -msgstr "" +msgstr "Veličina historije pretraživanja" msgctxt "#30024" msgid "Cache size (MB)" -msgstr "" +msgstr "Veličina keš memorije (MB)" msgctxt "#30025" msgid "Enable Setup Wizard" -msgstr "" +msgstr "Omogući čarobnjaka za podešavanje" msgctxt "#30026" msgid "Override views" -msgstr "" +msgstr "Prekriži prikaze" msgctxt "#30027" msgid "View: Default" -msgstr "" +msgstr "Prikaz: Zadano" msgctxt "#30028" msgid "View: Episodes" -msgstr "" +msgstr "Prikaz: Epizode" msgctxt "#30029" msgid "View: Movies" -msgstr "" +msgstr "Prikaz: Filmovi" msgctxt "#30030" msgid "Configure %s?" -msgstr "" +msgstr "Konfigurirati %s?" msgctxt "#30031" -msgid "" -msgstr "" +msgid "Requests cache size (MB)" +msgstr "Veličina keš memorije zahtjeva (MB)" msgctxt "#30032" msgid "View: TV Shows" -msgstr "" +msgstr "Prikaz: TV emisije" msgctxt "#30033" msgid "View: Songs" -msgstr "" +msgstr "Prikaz: Pjesme" msgctxt "#30034" msgid "View: Artists" -msgstr "" +msgstr "Prikaz: Umjetnici" msgctxt "#30035" msgid "View: Albums" -msgstr "" +msgstr "Prikaz: Albumi" msgctxt "#30036" msgid "Support alternative player" -msgstr "" +msgstr "Podrži alternativnog igrača" msgctxt "#30037" msgid "Custom Watch Later playlist id" -msgstr "" +msgstr "ID prilagođene plejliste za gledanje kasnije" msgctxt "#30038" msgid "Custom History playlist id" -msgstr "" +msgstr "ID prilagođene liste za reprodukciju historije" # Kodion Common # empty strings from id 30039 to 30099 msgctxt "#30100" msgid "Bookmarks" -msgstr "" +msgstr "Obilješci" msgctxt "#30101" msgid "Bookmark" -msgstr "" +msgstr "Označi" msgctxt "#30102" msgid "Search" -msgstr "" +msgstr "Pretraga" msgctxt "#30103" msgid "" @@ -206,89 +205,89 @@ msgstr "" msgctxt "#30105" msgid "filtered" -msgstr "" +msgstr "filtrirano" msgctxt "#30106" msgid "Next page: %d" -msgstr "" +msgstr "Sljedeća stranica: %d" msgctxt "#30107" msgid "Watch Later" -msgstr "" +msgstr "Gledaj kasnije" msgctxt "#30108" -msgid "" -msgstr "" +msgid "Enter the name of the bookmark" +msgstr "Unesite naziv oznake" msgctxt "#30109" -msgid "" -msgstr "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "Unesite važeći YouTube ili dodatak URL za oznaku" msgctxt "#30110" msgid "New Search" -msgstr "" +msgstr "Nova pretraga" msgctxt "#30111" msgid "Sign In" -msgstr "" +msgstr "Prijavi se" msgctxt "#30112" msgid "Sign Out" -msgstr "" +msgstr "Odjava" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" msgid "Confirm delete" -msgstr "" +msgstr "Potvrdi brisanje" msgctxt "#30115" msgid "Confirm remove" -msgstr "" +msgstr "Potvrdi uklanjanje" msgctxt "#30116" msgid "Delete \"%s\"?" -msgstr "" +msgstr "Izbrisati \"%s\"?" msgctxt "#30117" msgid "Remove \"%s\"?" -msgstr "" +msgstr "Ukloniti \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" msgid "Please wait..." -msgstr "" +msgstr "Molimo pričekajte..." msgctxt "#30120" msgid "Confirm clear" -msgstr "" +msgstr "Potvrdi brisanje" msgctxt "#30121" msgid "Clear %s?" -msgstr "" +msgstr "Obrisati %s?" # YouTube # empty strings from id 30121 to 30199 msgctxt "#30200" msgid "API" -msgstr "" +msgstr "API" msgctxt "#30201" msgid "API Key" -msgstr "" +msgstr "API ključ" msgctxt "#30202" msgid "API Id" -msgstr "" +msgstr "ID API-ja" msgctxt "#30203" msgid "API Secret" -msgstr "" +msgstr "Tajna API-ja" msgctxt "#30204" msgid "" @@ -301,128 +300,128 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" -msgstr "" +msgid "%s failed" +msgstr "%s nije uspjelo" msgctxt "#30501" -msgid "" -msgstr "" +msgid "Edit %s" +msgstr "Uredi %s" msgctxt "#30502" msgid "Go to %s" -msgstr "" +msgstr "Idi na %s" msgctxt "#30503" msgid "Channel fanart" -msgstr "" +msgstr "Umjetnost obožavatelja kanala" msgctxt "#30504" msgid "Subscriptions" -msgstr "" +msgstr "Pretplate" msgctxt "#30505" msgid "Unsubscribe" -msgstr "" +msgstr "Otpretplati se" msgctxt "#30506" msgid "Subscribe" -msgstr "" +msgstr "Pretplati se" msgctxt "#30507" msgid "My Channel" -msgstr "" +msgstr "Moj kanal" msgctxt "#30508" msgid "Liked Videos" -msgstr "" +msgstr "Sviđa mi se videozapisi" msgctxt "#30509" msgid "History" -msgstr "" +msgstr "Historija" msgctxt "#30510" msgid "My Subscriptions" -msgstr "" +msgstr "Moje pretplate" msgctxt "#30511" msgid "Queue video" -msgstr "" +msgstr "Video u redu čekanja" msgctxt "#30512" msgid "Browse Channels" -msgstr "" +msgstr "Pregledaj kanale" msgctxt "#30513" msgid "Trending" -msgstr "" +msgstr "U trendu" msgctxt "#30514" msgid "Related Videos" -msgstr "" +msgstr "Povezani videozapisi" msgctxt "#30515" msgid "Auto-Remove from Watch Later" -msgstr "" +msgstr "Automatsko uklanjanje iz rubrike Gledaj kasnije" msgctxt "#30516" msgid "Folders" -msgstr "" +msgstr "Folderi" msgctxt "#30517" msgid "Subscribe to %s" -msgstr "" +msgstr "Pretplatite se na %s" msgctxt "#30518" msgid "Feeds" -msgstr "" +msgstr "Feedovi" msgctxt "#30519" msgid "and enter the following code:" -msgstr "" +msgstr "i unesite sljedeći kod:" msgctxt "#30520" msgid "Add to..." -msgstr "" +msgstr "Dodaj u..." msgctxt "#30521" -msgid "" -msgstr "" +msgid "Delete requests cache database" +msgstr "Brisanje keš baze podataka zahtjeva" msgctxt "#30522" -msgid "" -msgstr "" +msgid "Clear requests cache" +msgstr "Obriši keš zahtjeva" msgctxt "#30523" -msgid "" -msgstr "" +msgid "requests cache" +msgstr "keš zahtjeva" msgctxt "#30524" msgid "Select language" -msgstr "" +msgstr "Odaberite jezik" msgctxt "#30525" msgid "Select region" -msgstr "" +msgstr "Odaberite regiju" msgctxt "#30526" msgid "Setup Wizard" -msgstr "" +msgstr "Čarobnjak za podešavanje" msgctxt "#30527" msgid "Language and Region" -msgstr "" +msgstr "Jezik i regija" msgctxt "#30528" msgid "Rate..." -msgstr "" +msgstr "Ocijeni..." msgctxt "#30529" msgid "I like this" -msgstr "" +msgstr "Sviđa mi se ovo" msgctxt "#30530" msgid "I dislike this" -msgstr "" +msgstr "Ne sviđa mi se ovo" msgctxt "#30531" msgid "" @@ -434,7 +433,7 @@ msgstr "" msgctxt "#30533" msgid "Reverse" -msgstr "" +msgstr "Obrnuto" msgctxt "#30534" msgid "" @@ -442,35 +441,35 @@ msgstr "" msgctxt "#30535" msgid "Select the order of the playlist" -msgstr "" +msgstr "Odaberite redoslijed popisa za reprodukciju" msgctxt "#30536" msgid "Updating Playlist..." -msgstr "" +msgstr "Ažuriranje plejliste..." msgctxt "#30537" msgid "Play from here" -msgstr "" +msgstr "Reproduciraj odavde" msgctxt "#30538" msgid "Disliked Videos" -msgstr "" +msgstr "Nesviđa mi se videozapisi" msgctxt "#30539" msgid "Play recently added" -msgstr "" +msgstr "Nedavno dodana reprodukcija" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" -msgstr "" +msgstr "Prikaži naziv kanala i detalje videa u opisu" msgctxt "#30542" msgid "rtmpe streams are not supported" -msgstr "" +msgstr "rtmpe streamovi nisu podržani" msgctxt "#30543" msgid "" @@ -478,18 +477,18 @@ msgstr "" msgctxt "#30544" msgid "More Links from the description" -msgstr "" +msgstr "Više linkova iz opisa" msgctxt "#30545" msgid "No videos found." -msgstr "" +msgstr "Nije pronađen nijedan video." msgctxt "#30546" msgid "Please complete all login prompts" -msgstr "" +msgstr "Molimo Vas da ispunite sve upite za prijavu" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -498,7 +497,7 @@ msgstr "" msgctxt "#30549" msgid "No streams found" -msgstr "" +msgstr "Nisu pronađeni streamovi" msgctxt "#30550" msgid "" @@ -506,39 +505,39 @@ msgstr "" msgctxt "#30551" msgid "Recommendations" -msgstr "" +msgstr "Preporuke" msgctxt "#30552" msgid "Maintenance" -msgstr "" +msgstr "Održavanje" msgctxt "#30553" msgid "Delete function cache database" -msgstr "" +msgstr "Obriši bazu podataka keš memorije funkcija" msgctxt "#30554" msgid "Delete search history database" -msgstr "" +msgstr "Obriši bazu podataka historije pretraživanja" msgctxt "#30555" msgid "Clear function cache" -msgstr "" +msgstr "Obriši keš memoriju funkcija" msgctxt "#30556" msgid "Clear search history" -msgstr "" +msgstr "Obriši historiju pretraživanja" msgctxt "#30557" msgid "function cache" -msgstr "" +msgstr "keš za funkciju" msgctxt "#30558" msgid "search history" -msgstr "" +msgstr "historija pretraživanja" msgctxt "#30559" msgid "Delete settings.xml" -msgstr "" +msgstr "Obriši settings.xml" msgctxt "#30560" msgid "" @@ -550,15 +549,15 @@ msgstr "" msgctxt "#30562" msgid "View all" -msgstr "" +msgstr "Prikaži sve" msgctxt "#30563" -msgid "" -msgstr "" +msgid "Plugin execution timeout" +msgstr "Vremensko ograničenje izvršavanja dodatka" msgctxt "#30564" -msgid "" -msgstr "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "Samo za testiranje - prisilno prekinite izvršavanje dodatka nakon postavljenog vremenskog ograničenja. Postavite na 0 sekundi (zadano) da biste onemogućili." msgctxt "#30565" msgid "" @@ -570,255 +569,255 @@ msgstr "" msgctxt "#30567" msgid "Set as Watch Later" -msgstr "" +msgstr "Postavi kao Gledaj kasnije" msgctxt "#30568" msgid "Remove as Watch Later" -msgstr "" +msgstr "Ukloni kao Gledaj kasnije" msgctxt "#30569" msgid "Are you sure you want to remove \"%s\" as your Watch Later list?" -msgstr "" +msgstr "Jeste li sigurni da želite ukloniti \"%s\" sa svoje liste Gledaj kasnije?" msgctxt "#30570" msgid "Are you sure you want to replace your current Watch Later list with \"%s\"?" -msgstr "" +msgstr "Jeste li sigurni da želite zamijeniti svoju trenutnu listu Gledaj kasnije sa \"%s\"?" msgctxt "#30571" msgid "Set as History" -msgstr "" +msgstr "Postavi kao historiju" msgctxt "#30572" msgid "Remove as History" -msgstr "" +msgstr "Ukloni kao historiju" msgctxt "#30573" msgid "Are you sure you want to remove \"%s\" as your History list?" -msgstr "" +msgstr "Jeste li sigurni da želite ukloniti \"%s\" sa svoje liste historije?" msgctxt "#30574" msgid "Are you sure you want to replace your current History list with \"%s\"?" -msgstr "" +msgstr "Jeste li sigurni da želite zamijeniti svoju trenutnu listu historije sa \"%s\"?" msgctxt "#30575" msgid "Succeeded" -msgstr "" +msgstr "Uspjelo je" msgctxt "#30576" msgid "Failed" -msgstr "" +msgstr "Nije uspjelo" msgctxt "#30577" -msgid "" -msgstr "" +msgid "Smallest (4:3)" +msgstr "Najmanji (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" -msgstr "" +msgstr "Prisilna verifikacija SSL certifikata" 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 je aktiviran u postavkama YouTubea, međutim, dodatak je onemogućen. Želite li sada omogućiti InputStream.Adaptive?" msgctxt "#30580" msgid "Reset access manager" -msgstr "" +msgstr "Resetiraj upravitelja pristupa" msgctxt "#30581" msgid "Are you sure you want to reset access manager?" -msgstr "" +msgstr "Jeste li sigurni da želite resetirati upravitelja pristupa?" msgctxt "#30582" msgid "Autoplay suggested videos" -msgstr "" +msgstr "Automatska reprodukcija predloženih videozapisa" msgctxt "#30583" msgid "Filters can be channel names separated by a comma eg. 'The Best Channel,The 2nd Best Channel', and/or custom video filters in the form '{ATTR}{OP}{VALUE}' eg. '{duration}{>=}{180}{artists_string}{=}{\"The Best Channel\"},{duration}{<}{180}'" -msgstr "" +msgstr "Filteri mogu biti nazivi kanala odvojeni zarezom, npr. 'Najbolji kanal, Drugi najbolji kanal', i/ili prilagođeni video filteri u obliku '{ATTR}{OP}{VALUE}', npr. '{duration}{>=}{180}{artists_string}{=}{\"Najbolji kanal\"},{duration}{<}{180}'" msgctxt "#30584" msgid "My Subscriptions (Filtered)" -msgstr "" +msgstr "Moje pretplate (filtrirane)" msgctxt "#30585" msgid "Enable Blacklist to exclude channel names in Filters from My Subscriptions, disable to include channel names" -msgstr "" +msgstr "Omogućite Crnu listu da biste isključili nazive kanala u Filterima iz Mojih pretplata, onemogućite da biste uključili nazive kanala" msgctxt "#30586" msgid "Blacklist" -msgstr "" +msgstr "Crna lista" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" msgid "Thumbnail size" -msgstr "" +msgstr "Veličina sličice" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "" +msgid "Small (16:9)" +msgstr "Mali (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "" +msgid "Medium (4:3)" +msgstr "Srednje (4:3)" msgctxt "#30594" msgid "Safe search" -msgstr "" +msgstr "Sigurna pretraga" msgctxt "#30595" msgid "Moderate" -msgstr "" +msgstr "Nadzirite" msgctxt "#30596" msgid "Strict" -msgstr "" +msgstr "Strogo" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" -msgstr "" +msgid "Large (4:3)" +msgstr "Veliko (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" -msgstr "" +msgstr "Omogućavanje ličnih API ključeva nije uspjelo. Nedostaje: %s" msgctxt "#30600" -msgid "" -msgstr "" +msgid "Largest (16:9)" +msgstr "Najveći (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" -msgstr "" +msgstr "%s s originalnim/%s rezervnim" msgctxt "#30602" msgid "No auto-generated" -msgstr "" +msgstr "Nije automatski generirano" msgctxt "#30603" msgid "Age gate" -msgstr "" +msgstr "Starosna granica" msgctxt "#30604" msgid "Allow offensive content" -msgstr "" +msgstr "Dozvoli uvredljiv sadržaj" msgctxt "#30605" msgid "Quick Search" -msgstr "" +msgstr "Brza pretraga" msgctxt "#30606" msgid "Quick Search (Incognito)" -msgstr "" +msgstr "Brza pretraga (anonimno)" msgctxt "#30607" msgid "Audio only" -msgstr "" +msgstr "Samo zvuk" msgctxt "#30608" msgid "Allow developer keys" -msgstr "" +msgstr "Dozvoli ključeve za programere" msgctxt "#30609" msgid "Clear watch history" -msgstr "" +msgstr "Obriši historiju gledanja" msgctxt "#30610" msgid "This will clear your account's watch history from all devices. You can't undo this." -msgstr "" +msgstr "Ovim će se izbrisati historija gledanja vašeg računa sa svih uređaja. Ovu radnju ne možete poništiti." msgctxt "#30611" msgid "Saved Playlists" -msgstr "" +msgstr "Sačuvane liste za reprodukciju" msgctxt "#30612" msgid "Retry" -msgstr "" +msgstr "Pokušaj ponovo" msgctxt "#30613" -msgid "" -msgstr "" +msgid "Add to %s" +msgstr "Dodaj u %s" msgctxt "#30614" -msgid "" -msgstr "" +msgid "Remove from %s" +msgstr "Ukloni iz %s" msgctxt "#30615" -msgid "" -msgstr "" +msgid "Added to %s" +msgstr "Dodano u %s" msgctxt "#30616" -msgid "" -msgstr "" +msgid "Removed from %s" +msgstr "Uklonjeno iz %s" msgctxt "#30617" msgid "InputStream.Adaptive" -msgstr "" +msgstr "InputStream.Adaptive" msgctxt "#30618" msgid "Stream redirect" -msgstr "" +msgstr "Preusmjeravanje streama" msgctxt "#30619" msgid "Enable to reduce resource usage on less powerful devices, but could lead to IP bans. Use at own risk." -msgstr "" +msgstr "Omogućite smanjenje korištenja resursa na manje moćnim uređajima, ali može dovesti do zabrane IP adresa. Koristite na vlastitu odgovornost." msgctxt "#30620" msgid "Port %s already in use. Cannot start http server." -msgstr "" +msgstr "Port %s je već u upotrebi. Nije moguće pokrenuti http server." msgctxt "#30621" -msgid "" -msgstr "" +msgid "Verbose" +msgstr "Opširno" msgctxt "#30622" msgid "Purchases" -msgstr "" +msgstr "Kupovine" msgctxt "#30623" msgid "Install InputStream Helper" -msgstr "" +msgstr "Instalirajte InputStream pomoćnika" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" msgid "InputStream Helper is already installed." -msgstr "" +msgstr "InputStream Helper je već instaliran." msgctxt "#30626" msgid "Delete temporary files" -msgstr "" +msgstr "Izbriši privremene datoteke" msgctxt "#30627" msgid "Rate video after watching" -msgstr "" +msgstr "Ocijenite video nakon gledanja" msgctxt "#30628" msgid "HTTP Server" -msgstr "" +msgstr "HTTP server" msgctxt "#30629" msgid "IP whitelist (comma delimited)" -msgstr "" +msgstr "Bijela IP lista (razdvojena zarezom)" msgctxt "#30630" msgid "" @@ -826,291 +825,291 @@ msgstr "" msgctxt "#30631" msgid "Successfully updated: %s" -msgstr "" +msgstr "Uspješno ažurirano: %s" msgctxt "#30632" msgid "Enable API configuration page" -msgstr "" +msgstr "Omogući stranicu za konfiguraciju API-ja" msgctxt "#30633" msgid "http://:/youtube/api (see Advanced > HTTP Server)" -msgstr "" +msgstr "http:// : /youtube/api (pogledajte Napredno > HTTP server)" msgctxt "#30634" msgid "YouTube Add-on API Configuration" -msgstr "" +msgstr "Konfiguracija API-ja za YouTube dodatak" msgctxt "#30635" msgid "No changes detected, API keys were not updated." -msgstr "" +msgstr "Nisu otkrivene promjene, API ključevi nisu ažurirani." msgctxt "#30636" msgid "Personal API keys are enabled." -msgstr "" +msgstr "Lični API ključevi su omogućeni." msgctxt "#30637" msgid "Personal API keys are disabled." -msgstr "" +msgstr "Lični API ključevi su onemogućeni." msgctxt "#30638" msgid "Bookmark this page to quickly add your keys in the future." -msgstr "" +msgstr "Označite ovu stranicu kako biste brzo dodali svoje ključeve u budućnosti." msgctxt "#30639" msgid "Found personal API keys in api_keys.json, would you like to restore them? Choosing no will overwrite them." -msgstr "" +msgstr "Pronađeni su lični API ključevi u api_keys.json, želite li ih vratiti? Odabirom opcije \"ne\" prepisat ćete ih." msgctxt "#30640" msgid "Restore" -msgstr "" +msgstr "Povratite" msgctxt "#30641" msgid "Delete api_keys.json" -msgstr "" +msgstr "Obriši api_keys.json" msgctxt "#30642" msgid "Delete access_manager.json" -msgstr "" +msgstr "Obriši datoteku access_manager.json" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" msgid "Select listen IP" -msgstr "" +msgstr "Odaberite IP adresu za slušanje" msgctxt "#30645" msgid "Refresh after watching" -msgstr "" +msgstr "Osvježi nakon gledanja" msgctxt "#30646" msgid "Upcoming Live" -msgstr "" +msgstr "Predstojeći prijenos uživo" msgctxt "#30647" msgid "Completed Live" -msgstr "" +msgstr "Završeno uživo" msgctxt "#30648" msgid "API Key is incorrect. Settings > API > API Key" -msgstr "" +msgstr "API ključ je netačan. Postavke > API > API ključ" msgctxt "#30649" msgid "Client Id is incorrect. Settings > API > API Id" -msgstr "" +msgstr "ID klijenta je netačan. Postavke > API > ID API-ja" msgctxt "#30650" msgid "Client Secret is incorrect. Settings > API > API Secret" -msgstr "" +msgstr "Tajni podatak klijenta je netačan. Postavke > API > Tajni podatak API-ja" msgctxt "#30651" msgid "Location" -msgstr "" +msgstr "Lokacija" msgctxt "#30652" msgid "Location radius (km)" -msgstr "" +msgstr "Radijus lokacije (km)" msgctxt "#30653" msgid "My Location using IP geolocation lookup" -msgstr "" +msgstr "Moja lokacija korištenjem pretrage geolokacije IP adrese" msgctxt "#30654" msgid "My Location" -msgstr "" +msgstr "Moja lokacija" msgctxt "#30655" msgid "Switch User" -msgstr "" +msgstr "Promijeni korisnika" msgctxt "#30656" msgid "New user" -msgstr "" +msgstr "Novi korisnik" msgctxt "#30657" msgid "Unnamed" -msgstr "" +msgstr "Neimenovano" msgctxt "#30658" msgid "Enter a name for this user" -msgstr "" +msgstr "Unesite ime za ovog korisnika" msgctxt "#30659" msgid "User is now \"%s\"" -msgstr "" +msgstr "Korisnik je sada \"%s\"" msgctxt "#30660" msgid "Users" -msgstr "" +msgstr "Korisnici" msgctxt "#30661" msgid "Add a user" -msgstr "" +msgstr "Dodaj korisnika" msgctxt "#30662" msgid "Remove a user" -msgstr "" +msgstr "Uklonite korisnika" msgctxt "#30663" msgid "Rename a user" -msgstr "" +msgstr "Preimenuj korisnika" msgctxt "#30664" msgid "Switch user" -msgstr "" +msgstr "Promijeni korisnika" msgctxt "#30665" msgid "Switch to \"%s\" now?" -msgstr "" +msgstr "Prebaciti se na \"%s\" sada?" msgctxt "#30666" msgid "\"%s\" removed" -msgstr "" +msgstr "\"%s\" je uklonjeno" msgctxt "#30667" msgid "Renamed \"%s\" to \"%s\"" -msgstr "" +msgstr "Preimenovano iz \"%s\" u \"%s\"" msgctxt "#30668" msgid "Play count minimum percent" -msgstr "" +msgstr "Minimalni postotak broja reprodukcija" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "" +msgid "%s removed" +msgstr "%s uklonjeno" msgctxt "#30670" -msgid "Mark watched" -msgstr "" +msgid "Added %s" +msgstr "Dodano %s" msgctxt "#30671" msgid "Clear playback history" -msgstr "" +msgstr "Obriši historiju reprodukcije" msgctxt "#30672" msgid "Delete playback history database" -msgstr "" +msgstr "Obriši bazu podataka historije reprodukcije" msgctxt "#30673" msgid "playback history" -msgstr "" +msgstr "historija reprodukcije" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" -msgstr "" +msgstr "Koristi lokalnu historiju reprodukcije (gledano, nastaviti praćenje)" msgctxt "#30676" msgid "Just now" -msgstr "" +msgstr "Upravo sada" msgctxt "#30677" msgid "A minute ago" -msgstr "" +msgstr "Prije minute" msgctxt "#30678" msgid "Recently" -msgstr "" +msgstr "Nedavno" msgctxt "#30679" msgid "An hour ago" -msgstr "" +msgstr "Prije sat vremena" msgctxt "#30680" msgid "Two hours ago" -msgstr "" +msgstr "Prije dva sata" msgctxt "#30681" msgid "Three hours ago" -msgstr "" +msgstr "Prije tri sata" msgctxt "#30682" msgid "Yesterday at" -msgstr "" +msgstr "Jučer u" msgctxt "#30683" msgid "Two days ago" -msgstr "" +msgstr "Prije dva dana" msgctxt "#30684" msgid "Today at" -msgstr "" +msgstr "Danas u" msgctxt "#30685" msgid "Delete data cache database" -msgstr "" +msgstr "Izbriši bazu podataka keš memorije" msgctxt "#30686" msgid "Clear data cache" -msgstr "" +msgstr "Obriši keš podatke" msgctxt "#30687" msgid "data cache" -msgstr "" +msgstr "keš podataka" msgctxt "#30688" msgid "Use MPEG-DASH for videos" -msgstr "" +msgstr "Koristite MPEG-DASH za videozapise" msgctxt "#30689" msgid "Use for live streams" -msgstr "" +msgstr "Koristi za prijenose uživo" msgctxt "#30690" msgid "InputStream.Adaptive >= 2.0.12 is required for adaptive live streams" -msgstr "" +msgstr "Za adaptivne prijenose uživo potreban je InputStream.Adaptive >= 2.0.12" msgctxt "#30691" msgid "Airing now" -msgstr "" +msgstr "Emitira se sada" msgctxt "#30692" msgid "In a minute" -msgstr "" +msgstr "Za minutu" msgctxt "#30693" msgid "Airing soon" -msgstr "" +msgstr "Uskoro u emitiranju" msgctxt "#30694" msgid "In over an hour" -msgstr "" +msgstr "Za više od sat vremena" msgctxt "#30695" msgid "In over two hours" -msgstr "" +msgstr "Za više od dva sata" msgctxt "#30696" msgid "Airing today at" -msgstr "" +msgstr "Emituje se danas u" msgctxt "#30697" msgid "Tomorrow at" -msgstr "" +msgstr "Sutra u" msgctxt "#30698" msgid "Check my IP" -msgstr "" +msgstr "Provjeri moju IP adresu" msgctxt "#30699" msgid "HTTP server is not running" -msgstr "" +msgstr "HTTP server ne radi" msgctxt "#30700" msgid "Client IP is %s" -msgstr "" +msgstr "IP adresa klijenta je %s" msgctxt "#30701" msgid "Failed to obtain client IP" -msgstr "" +msgstr "Nije uspjelo dobijanje IP adrese klijenta" msgctxt "#30702" msgid "Play with subtitles" -msgstr "" +msgstr "Reproduciraj s titlovima" msgctxt "#30703" msgid "" @@ -1118,31 +1117,31 @@ msgstr "" msgctxt "#30704" msgid "Use YouTube website urls with default player" -msgstr "" +msgstr "Koristite URL-ove YouTube web stranice sa zadanim playerom" msgctxt "#30705" msgid "Download subtitles" -msgstr "" +msgstr "Preuzmi titlove" msgctxt "#30706" msgid "Download subtitles before starting playback? (Default: No)" -msgstr "" +msgstr "Preuzmi titlove prije početka reprodukcije? (Zadano: Ne)" msgctxt "#30707" msgid "Untitled" -msgstr "" +msgstr "Bez naslova" msgctxt "#30708" msgid "Play audio only" -msgstr "" +msgstr "Reprodukuj samo zvuk" msgctxt "#30709" -msgid "" -msgstr "" +msgid "WebVTT subtitles" +msgstr "WebVTT titlovi" msgctxt "#30710" -msgid "" -msgstr "" +msgid "TTML subtitles" +msgstr "TTML titlovi" msgctxt "#30711" msgid "" @@ -1150,436 +1149,448 @@ msgstr "" msgctxt "#30712" msgid "Rate videos in playlists" -msgstr "" +msgstr "Ocijenite videozapise na plejlistama" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "" +msgid "Prefer dubbed audio over original audio" +msgstr "Preferiraj sinkronizirani zvuk u odnosu na originalni zvuk" msgctxt "#30714" -msgid "Added to playlist" -msgstr "" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "Preferiraj automatski prevedeni sinkronizirani audio u odnosu na originalni audio" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "Koristiti internu listu YouTubea za historiju gledanja?[CR][CR]Zahtijeva prijavu putem dodatka i aktiviranje praćenja historije na YouTubeu." msgctxt "#30716" msgid "Liked video" -msgstr "" +msgstr "Sviđa mi se video" msgctxt "#30717" msgid "Disliked video" -msgstr "" +msgstr "Nesviđa mi se video" msgctxt "#30718" -msgid "Rating removed" -msgstr "" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "Koristiti YouTube internu listu za Gledaj kasnije?[CR][CR]Zahtijeva prijavu putem dodatka." msgctxt "#30719" msgid "Subscribed to channel" -msgstr "" +msgstr "Pretplaćeni ste na kanal" msgctxt "#30720" msgid "Unsubscribed from channel" -msgstr "" +msgstr "Odjavljeni ste s kanala" msgctxt "#30721" -msgid "" -msgstr "" +msgid "Enable Panoramic/180/360/VR video" +msgstr "Omogući panoramski/180/360/VR video" msgctxt "#30722" msgid "Enable HDR video" -msgstr "" +msgstr "Omogući HDR video" 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 "Proxy je potreban za MPEG-DASH VOD-ove (pogledajte Napredno > HTTP Server)[CR]HDR i >1080p video zahtijevaju InputStream.Adaptive >= 2.3.14" msgctxt "#30724" msgid "Enable high framerate video" -msgstr "" +msgstr "Omogući video s visokom brzinom kadrova" msgctxt "#30725" msgid "1440p (QHD)" -msgstr "" +msgstr "1440p (QHD)" msgctxt "#30726" msgid "Uploads" -msgstr "" +msgstr "Otpremanja" msgctxt "#30727" msgid "Enable H.264 video" -msgstr "" +msgstr "Omogući H.264 video" msgctxt "#30728" msgid "Enable VP9 video" -msgstr "" +msgstr "Omogući VP9 video" msgctxt "#30729" msgid "Prefer lower resolution streams for unselected codecs" -msgstr "" +msgstr "Preferiraj streamove niže rezolucije za neodabrane kodeke" msgctxt "#30730" msgid "Play (Ask for quality)" -msgstr "" +msgstr "Igrajte (Tražite kvalitet)" msgctxt "#30731" msgid "The YouTube add-on now requires that you use your own API keys.[CR]For more information see the wiki: [B]https://ytaddon.page.link/keys[/B][CR][CR]Sorry for the inconvenience." -msgstr "" +msgstr "Dodatak za YouTube sada zahtijeva korištenje vlastitih API ključeva.[CR]Za više informacija pogledajte wiki: [B]https://ytaddon.page.link/keys[/B][CR][CR]Žao nam je zbog neugodnosti." msgctxt "#30732" msgid "Comments" -msgstr "" +msgstr "Komentari" msgctxt "#30733" msgid "Likes" -msgstr "" +msgstr "Sviđanja" msgctxt "#30734" msgid "Replies" -msgstr "" +msgstr "Odgovori" msgctxt "#30735" msgid "Edited" -msgstr "" +msgstr "Uređeno" msgctxt "#30736" msgid "Shorts" -msgstr "" +msgstr "Kratke hlače" msgctxt "#30737" msgid "Shorts - Max duration" -msgstr "" +msgstr "Kratki filmovi - Maksimalno trajanje" msgctxt "#30738" -msgid "" -msgstr "" +msgid "Enable spatial audio" +msgstr "Omogući prostorni zvuk" msgctxt "#30739" msgid "Subscribers" -msgstr "" +msgstr "Pretplatnici" msgctxt "#30740" msgid "HLS" -msgstr "" +msgstr "HLS" msgctxt "#30741" msgid "Multi-stream HLS" -msgstr "" +msgstr "Višestruki HLS" msgctxt "#30742" msgid "Adaptive HLS" -msgstr "" +msgstr "Adaptivni HLS" msgctxt "#30743" msgid "MPEG-DASH" -msgstr "" +msgstr "MPEG-DASH" msgctxt "#30744" msgid "Original" -msgstr "" +msgstr "Original" msgctxt "#30745" msgid "Dubbed" -msgstr "" +msgstr "Sinhronizovano" msgctxt "#30746" msgid "Descriptive" -msgstr "" +msgstr "Opisno" msgctxt "#30747" msgid "Alternate" -msgstr "" +msgstr "Alternativni" msgctxt "#30748" msgid "Stream features" -msgstr "" +msgstr "Funkcije streama" msgctxt "#30749" msgid "Enable AV1 video" -msgstr "" +msgstr "Omogući AV1 video" msgctxt "#30750" msgid "Enable Vorbis audio" -msgstr "" +msgstr "Omogući Vorbis audio" msgctxt "#30751" msgid "Enable Opus audio" -msgstr "" +msgstr "Omogući Opus audio" msgctxt "#30752" msgid "Enable AAC audio" -msgstr "" +msgstr "Omogući AAC zvuk" msgctxt "#30753" msgid "Enable surround sound audio" -msgstr "" +msgstr "Omogući surround zvuk" msgctxt "#30754" msgid "Enable AC-3 audio" -msgstr "" +msgstr "Omogući AC-3 zvuk" msgctxt "#30755" msgid "Enable EAC-3 audio" -msgstr "" +msgstr "Omogući EAC-3 zvuk" msgctxt "#30756" msgid "Enable DTS audio" -msgstr "" +msgstr "Omogući DTS zvuk" msgctxt "#30757" msgid "Remove similar/duplicate streams" -msgstr "" +msgstr "Ukloni slične/duplicirane streamove" msgctxt "#30758" msgid "Stream selection" -msgstr "" +msgstr "Odabir streama" msgctxt "#30759" msgid "Quality selection" -msgstr "" +msgstr "Odabir kvalitete" msgctxt "#30760" msgid "Automatic + Quality selection" -msgstr "" +msgstr "Automatski + odabir kvalitete" msgctxt "#30761" msgid "Update playback history on Youtube" -msgstr "" +msgstr "Ažuriranje historije reprodukcije na YouTubeu" msgctxt "#30762" msgid "Multi-language" -msgstr "" +msgstr "Višejezičnost" msgctxt "#30763" msgid "Multi-audio" -msgstr "" +msgstr "Višestruki audio" msgctxt "#30764" msgid "Requests connect timeout" -msgstr "" +msgstr "Zahtjevi za povezivanje su prekinuli vrijeme" msgctxt "#30765" msgid "Requests read timeout" -msgstr "" +msgstr "Zahtjevi za čitanje su istekli" msgctxt "#30766" msgid "Premieres" -msgstr "" +msgstr "Premijere" msgctxt "#30767" msgid "Views" -msgstr "" +msgstr "Pregledi" msgctxt "#30768" msgid "Disable high framerate video at maximum video quality" -msgstr "" +msgstr "Onemogući video s visokom brzinom sličica u sekundi pri maksimalnom kvalitetu videa" msgctxt "#30769" msgid "Clear Watch Later list" -msgstr "" +msgstr "Obriši listu Gledaj kasnije" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" -msgstr "" +msgstr "Jeste li sigurni da želite obrisati svoju listu \"Gledaj kasnije\"?" msgctxt "#30771" msgid "Disable fractional framerate hinting" -msgstr "" +msgstr "Onemogući hintovanje frakcijskog broja sličica u sekundi" msgctxt "#30772" msgid "Disable all framerate hinting" -msgstr "" +msgstr "Onemogući sve nagovještaje o brzini kadrova" msgctxt "#30773" msgid "Show video details in video lists" -msgstr "" +msgstr "Prikaži detalje o videu u listama videa" msgctxt "#30774" msgid "All available" -msgstr "" +msgstr "Sve dostupno" msgctxt "#30775" msgid "%s (translation)" -msgstr "" +msgstr "%s (prijevod)" msgctxt "#30776" msgid "Ask + Automatic + Quality selection" -msgstr "" +msgstr "Pitaj + Automatski + Odabir kvalitete" msgctxt "#30777" msgid "Views for %s (%s)" -msgstr "" +msgstr "Pregledi za %s (%s)" msgctxt "#30778" msgid "Import old playback history?" -msgstr "" +msgstr "Uvesti staru historiju reprodukcije?" msgctxt "#30779" msgid "Import old search history?" -msgstr "" +msgstr "Uvesti staru historiju pretraživanja?" msgctxt "#30780" msgid "Clear local watch later list" -msgstr "" +msgstr "Obriši lokalnu listu za kasnije gledanje" msgctxt "#30781" msgid "Delete watch later database" -msgstr "" +msgstr "Obriši bazu podataka za kasnije gledanje" msgctxt "#30782" msgid "local watch later list" -msgstr "" +msgstr "lokalna lista za kasnije gledanje" msgctxt "#30783" msgid "settings to recommended values" -msgstr "" +msgstr "postavke na preporučene vrijednosti" msgctxt "#30784" msgid "listings to show minimal details" -msgstr "" +msgstr "oglasi za prikaz minimalnih detalja" msgctxt "#30785" msgid "performance settings" -msgstr "" +msgstr "postavke performansi" msgctxt "#30786" msgid "Choose device capabilities" -msgstr "" +msgstr "Odaberite mogućnosti uređaja" msgctxt "#30787" msgid "720p, H.264 only | Limited or older devices" -msgstr "" +msgstr "Samo 720p, H.264 | Ograničeni ili stariji uređaji" msgctxt "#30788" msgid "1080p/30 fps | Raspberry Pi 3, or similar" -msgstr "" +msgstr "1080p/30 fps | Raspberry Pi 3 ili slično" msgctxt "#30789" msgid "4K/30 fps or 1080p/60 fps, HDR if compatible | Raspberry Pi 5, or similar" -msgstr "" +msgstr "4K/30 fps ili 1080p/60 fps, HDR ako je kompatibilan | Raspberry Pi 5 ili slično" 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 ako je kompatibilan | Fire TV Cube Gen 2, Shield TV, Fire TV Stick 4K Gen 1 ili slično" 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, korištenjem AV1 | Fire TV Cube Gen 3, Fire TV Stick 4K Max, Vero V ili slično" msgctxt "#30792" msgid "8K/60 fps, HDR, using AV1 | Modern device or PC with full capabilities" -msgstr "" +msgstr "8K/60 fps, HDR, korištenje AV1 | Moderni uređaj ili računar sa svim mogućnostima" msgctxt "#30793" msgid "Views count display colour" -msgstr "" +msgstr "Boja prikaza broja pregleda" msgctxt "#30794" msgid "Subscriber/Likes count display colour" -msgstr "" +msgstr "Boja prikaza broja pretplatnika/lajkova" msgctxt "#30795" msgid "Videos/Comments count display colour" -msgstr "" +msgstr "Boja prikaza broja videa/komentara" msgctxt "#30796" msgid "1080p/60 fps | Raspberry Pi 4, or similar" -msgstr "" +msgstr "1080p/60 fps | Raspberry Pi 4 ili slično" msgctxt "#30797" msgid "1080p/30 fps or 720p/30 fps, H.264 only | Raspberry Pi 1/2, or similar" -msgstr "" +msgstr "1080p/30 fps ili 720p/30 fps, samo H.264 | Raspberry Pi 1/2 ili slično" msgctxt "#30798" msgid "Clear bookmarks list" -msgstr "" +msgstr "Obriši listu oznaka" msgctxt "#30799" msgid "Delete bookmarks database" -msgstr "" +msgstr "Obriši bazu podataka oznaka" msgctxt "#30800" msgid "bookmarks list" -msgstr "" +msgstr "lista oznaka" msgctxt "#30801" msgid "Clear Bookmarks list" -msgstr "" +msgstr "Obriši listu oznaka" msgctxt "#30802" msgid "Are you sure you want to clear your Bookmarks list?" -msgstr "" +msgstr "Jeste li sigurni da želite obrisati listu oznaka?" msgctxt "#30803" msgid "Bookmark %s" -msgstr "" +msgstr "Označi %s" msgctxt "#30804" msgid "Use YouTube website urls with external player" -msgstr "" +msgstr "Koristite URL-ove YouTube web stranice s vanjskim playerom" msgctxt "#30805" msgid "Use MPEG-DASH with external player" -msgstr "" +msgstr "Koristite MPEG-DASH sa eksternim plejerom" msgctxt "#30806" msgid "Jump to page..." -msgstr "" +msgstr "Skoči na stranicu..." msgctxt "#30807" msgid "Use channel name as" -msgstr "" +msgstr "Koristi naziv kanala kao" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" msgid "All upcoming videos" -msgstr "" +msgstr "Svi nadolazeći videozapisi" msgctxt "#30810" msgid "All previously streamed (completed) videos" -msgstr "" +msgstr "Svi prethodno strimovani (završeni) videozapisi" msgctxt "#30811" msgid "Filter Live folders" -msgstr "" +msgstr "Filtriraj žive mape" msgctxt "#30812" msgid "Clear subscription feed history" -msgstr "" +msgstr "Obriši historiju feeda pretplata" msgctxt "#30813" msgid "Delete subscription feed history database" -msgstr "" +msgstr "Obriši bazu podataka historije pretplata" msgctxt "#30814" msgid "feed history" -msgstr "" +msgstr "historija feeda" msgctxt "#30815" msgid "Go back..." -msgstr "" +msgstr "Vrati se..." msgctxt "#30816" msgid "List is empty.[CR][CR]Refresh from context menu or try again later." -msgstr "" +msgstr "Lista je prazna.[CR][CR]Osvježite iz kontekstnog menija ili pokušajte ponovo kasnije." msgctxt "#30817" msgid "Refresh settings.xml" -msgstr "" +msgstr "Osvježi settings.xml" msgctxt "#30818" msgid "Are you sure you want to refresh settings.xml?" -msgstr "" +msgstr "Jeste li sigurni da želite osvježiti datoteku settings.xml?" msgctxt "#30819" msgid "Play from start" -msgstr "" +msgstr "Reproduciraj od početka" msgctxt "#30820" msgid "Podcast" -msgstr "" +msgstr "Podcast" + +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Slušajte putem IP-a" + +#~ msgctxt "#30547" +#~ msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +#~ msgstr "Možda će vam biti zatraženo da omogućite dvije aplikacije kako bi YouTube ispravno funkcionirao." + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Sakrij videozapise iz oglasa" diff --git a/plugin.video.youtube/resources/language/resource.language.ca_es/strings.po b/plugin.video.youtube/resources/language/resource.language.ca_es/strings.po index 87886bcecc..aab88da66a 100644 --- a/plugin.video.youtube/resources/language/resource.language.ca_es/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ca_es/strings.po @@ -7,15 +7,15 @@ msgstr "" "Project-Id-Version: XBMC-Addons\n" "Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2024-04-12 14:59+0000\n" -"Last-Translator: Christian Gade \n" +"PO-Revision-Date: 2025-08-30 13:29+0000\n" +"Last-Translator: Elvis Gallegos \n" "Language-Team: Catalan (Spain) \n" "Language: ca_es\n" "MIME-Version: 1.0\n" "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.4.3\n" +"X-Generator: Weblate 5.13\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,12 +1184,12 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" -msgstr "" +msgid "Enable Panoramic/180/360/VR video" +msgstr "Habilita el vídeo Panoràmic/180/360/VR" msgctxt "#30722" msgid "Enable HDR video" -msgstr "" +msgstr "Habilita el vídeo 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" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.cs_cz/strings.po b/plugin.video.youtube/resources/language/resource.language.cs_cz/strings.po index 56a118da4a..a45fe83cc8 100644 --- a/plugin.video.youtube/resources/language/resource.language.cs_cz/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.cs_cz/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Povolit 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "Shlédnout později" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Odhlásit" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Přesunout \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "Přidat do..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Přehrát pomocí..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "Neúspěšně" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Černý seznam" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Přidat do mého předplatného" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Odstranit z mého předplatného" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Přidáno do mého předplatného" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Odstraněno z mého předplatného" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Poměr stran náhledu" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "16:9" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "4:3" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "Přesné" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Aktualizováno: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Chyba povolení osobního API KEY. Chybí: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "Opakovat" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Port %s je již použiván. Nelze spustit HTTP Server." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Instalovat InputStream Helper" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Odstranit access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Poslech z IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Minimálně přehrávané (%)" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Označit jako nesledované" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Označit jako sledované" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "Historie přehrávání" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Obnovit bod" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "Přehrát pouze zvuk" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Hodnocená videa v seznamu" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Přidáno do 'Shlédnout později'" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Přidáno do seznamu" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Odstraněno ze seznamu" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "Video, které se mně nelíbí" msgctxt "#30718" -msgid "Rating removed" -msgstr "Hodnocení odstraněno" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Nepředplacené z kanálu" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,75 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Poslech z IP" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Povolit 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Přehrát pomocí..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Přidat do mého předplatného" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Odstranit z mého předplatného" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Přidáno do mého předplatného" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Odstraněno z mého předplatného" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "16:9" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "4:3" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Aktualizováno: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Označit jako nesledované" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Označit jako sledované" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Obnovit bod" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Přidáno do 'Shlédnout později'" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Přidáno do seznamu" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Odstraněno ze seznamu" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Hodnocení odstraněno" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Žádné další odkazy nenalezeny." diff --git a/plugin.video.youtube/resources/language/resource.language.cy_gb/strings.po b/plugin.video.youtube/resources/language/resource.language.cy_gb/strings.po index 24c1872754..f56b7204d5 100644 --- a/plugin.video.youtube/resources/language/resource.language.cy_gb/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.cy_gb/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.da_dk/strings.po b/plugin.video.youtube/resources/language/resource.language.da_dk/strings.po index 2dd8c0602e..d94995eace 100644 --- a/plugin.video.youtube/resources/language/resource.language.da_dk/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.da_dk/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Tillad 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "Se senere" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Log ud" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Fjern \"(%s)\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "Tilføj til..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Afspil med..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "Mislykkedes" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Sortliste" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Tilføj til filteret Mine abonnementer" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Fjern fra filteret Mine abonnementer" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Tilføjet til filteret Mine abonnementer" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Fjernet fra filteret Mine abonnementer" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Størrelse på miniaturebillede" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Medium (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Høj (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "Streng" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Opdateret: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Kunne ikke aktivere personlige API-nøgler. Mangler: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "Forsøg igen" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Port %s er allerede i brug. Kan ikke starte http-serveren." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Installer InputStream Helper" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Slet access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Lyt på IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Minimum procent for afspilningsantal" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Marker som ikke set" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Marker som set" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "afspilningshistorik" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Nulstil genoptagelsesposition" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "Afspil kun lyd" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Bedøm videoer i playlister" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Føj til 'Se senere'" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Føjet til playliste" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Fjernet fra playliste" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "Video du ikke synes godt om" msgctxt "#30718" -msgid "Rating removed" -msgstr "Bedømmelse fjernet" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Afmeldte abonnement på kanalen" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,75 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Lyt på IP" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Tillad 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Afspil med..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Tilføj til filteret Mine abonnementer" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Fjern fra filteret Mine abonnementer" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Tilføjet til filteret Mine abonnementer" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Fjernet fra filteret Mine abonnementer" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Medium (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Høj (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Opdateret: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Marker som ikke set" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Marker som set" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Nulstil genoptagelsesposition" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Føj til 'Se senere'" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Føjet til playliste" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Fjernet fra playliste" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Bedømmelse fjernet" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Ingen yderligere links fundet." diff --git a/plugin.video.youtube/resources/language/resource.language.de_de/strings.po b/plugin.video.youtube/resources/language/resource.language.de_de/strings.po index bf10d9cbe0..ea3d1b4011 100644 --- a/plugin.video.youtube/resources/language/resource.language.de_de/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.de_de/strings.po @@ -5,9 +5,9 @@ msgid "" msgstr "" "Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2025-05-11 19:28+0000\n" +"PO-Revision-Date: 2025-09-28 15:29+0000\n" "Last-Translator: Kai Sommerfeld \n" "Language-Team: German \n" "Language: de_de\n" @@ -15,7 +15,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.11.3\n" +"X-Generator: Weblate 5.13.3\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "144p" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "3D erlauben" +msgid "Enable Stereoscopic 3D video" +msgstr "Stereoskopisches 3D-Video aktivieren" msgctxt "#30021" msgid "Show fanart" @@ -151,8 +150,8 @@ msgid "Configure %s?" msgstr "%s konfigurieren?" msgctxt "#30031" -msgid "" -msgstr "" +msgid "Requests cache size (MB)" +msgstr "Cachegröße für Anfragen (MB)" msgctxt "#30032" msgid "View: TV Shows" @@ -217,12 +216,12 @@ msgid "Watch Later" msgstr "Später ansehen" msgctxt "#30108" -msgid "" -msgstr "" +msgid "Enter the name of the bookmark" +msgstr "Name des Lesezeichens eingeben" msgctxt "#30109" -msgid "" -msgstr "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "Eine gültige YouTube- oder Plugin-URL für das Lesezeichen eingeben" msgctxt "#30110" msgid "New Search" @@ -237,8 +236,8 @@ msgid "Sign Out" msgstr "Abmelden" msgctxt "#30113" -msgid "" -msgstr "" +msgid "Related to \"%s\"" +msgstr "Verwandt mit „%s“" msgctxt "#30114" msgid "Confirm delete" @@ -257,8 +256,8 @@ msgid "Remove \"%s\"?" msgstr "Soll \"%s\" entfernt werden?" msgctxt "#30118" -msgid "" -msgstr "" +msgid "Links from \"%s\"" +msgstr "Links von „%s“" msgctxt "#30119" msgid "Please wait..." @@ -301,12 +300,12 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" -msgstr "" +msgid "%s failed" +msgstr "%s ist fehlgeschlagen" msgctxt "#30501" -msgid "" -msgstr "" +msgid "Edit %s" +msgstr "%s bearbeiten" msgctxt "#30502" msgid "Go to %s" @@ -385,16 +384,16 @@ msgid "Add to..." msgstr "Hinzufügen zu ..." msgctxt "#30521" -msgid "" -msgstr "" +msgid "Delete requests cache database" +msgstr "Datenbank für Cache für Anfragen löschen" msgctxt "#30522" -msgid "" -msgstr "" +msgid "Clear requests cache" +msgstr "Cache für Anfragen löschen" msgctxt "#30523" -msgid "" -msgstr "" +msgid "requests cache" +msgstr "Cache für Anfragen" msgctxt "#30524" msgid "Select language" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "Zuletzt Hinzugefügte wiedergeben" msgctxt "#30540" -msgid "Play with..." -msgstr "Wiedergeben mit ..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,8 +488,8 @@ msgid "Please complete all login prompts" msgstr "Bitte alle Anmeldeaufforderungen abschliessen" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." -msgstr "Es kann zum Aktivieren on zwei Anwendungen aufgefordert werden, damit YouTube korrekt funktioniert." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +msgstr "Es kann Aufforderungen zum Anmelden und Aktivieren des Zugriffs auf mehrere Applikationen geben, damit dieses Add-on funktioniert." msgctxt "#30548" msgid "" @@ -498,7 +497,7 @@ msgstr "" msgctxt "#30549" msgid "No streams found" -msgstr "" +msgstr "Keine Streams gefunden" msgctxt "#30550" msgid "" @@ -553,12 +552,12 @@ msgid "View all" msgstr "Alle ansehen" msgctxt "#30563" -msgid "" -msgstr "" +msgid "Plugin execution timeout" +msgstr "Zeitüberschreitung bei Ausführung des Plugins" msgctxt "#30564" -msgid "" -msgstr "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "Nur zum Testen: Nach Überschreiten des eingestellten Zeitlimits Ausführung des Plugins hart beenden. Auf 0 Sekunden setzen zum Deaktivieren." msgctxt "#30565" msgid "" @@ -609,8 +608,8 @@ msgid "Failed" msgstr "Fehlgeschlagen" msgctxt "#30577" -msgid "" -msgstr "" +msgid "Smallest (4:3)" +msgstr "Kleinste (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Blacklist" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Zu Meine-Abonnements-Filter hinzufügen" +msgid "Hide \"Playlists\" folder" +msgstr "Ordner „Wiedergabelisten“ verbergen" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Aus Meine-Abonnements-Filter entfernen" +msgid "Hide \"Search\" folder" +msgstr "Ordner „Suchen“ verbergen" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Zu Meine-Abonnements-Filter hinzugefügt" +msgid "Hide \"Shorts\" folder" +msgstr "Ordner „Kurze Videos“ verbergen" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Aus Meine-Abonnements-Filter entfernt" +msgid "Hide \"Live\" folder" +msgstr "Ordner „Live-Videos“ verbergen" msgctxt "#30591" msgid "Thumbnail size" msgstr "Vorschaubildgröße" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Mittel (16:9)" +msgid "Small (16:9)" +msgstr "Klein (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Hoch (4:3)" +msgid "Medium (4:3)" +msgstr "Mittel (4:3)" msgctxt "#30594" msgid "Safe search" @@ -689,20 +688,20 @@ msgid "Strict" msgstr "Streng" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Aktualisiert: %s" +msgid "Hide \"Members only\" folder" +msgstr "Ordner „Nur für Mitglieder“ verbergen" msgctxt "#30598" -msgid "" -msgstr "" +msgid "Large (4:3)" +msgstr "Gross (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" msgstr "Aktivierung persönlicher API-Schlüssel fehlgeschlagen. Folgendes fehlt: %s" msgctxt "#30600" -msgid "" -msgstr "" +msgid "Largest (16:9)" +msgstr "Grösste (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" @@ -753,20 +752,20 @@ msgid "Retry" msgstr "Erneut versuchen" msgctxt "#30613" -msgid "" -msgstr "" +msgid "Add to %s" +msgstr "Zu %s hinzufügen" msgctxt "#30614" -msgid "" -msgstr "" +msgid "Remove from %s" +msgstr "Aus %s entfernen" msgctxt "#30615" -msgid "" -msgstr "" +msgid "Added to %s" +msgstr "Zu %s hinzugefügt" msgctxt "#30616" -msgid "" -msgstr "" +msgid "Removed from %s" +msgstr "Aus %s entfernt" msgctxt "#30617" msgid "InputStream.Adaptive" @@ -785,8 +784,8 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Port %s wird bereits genutzt. HTTP-Server kann nicht gestartet werden." msgctxt "#30621" -msgid "" -msgstr "" +msgid "Verbose" +msgstr "Ausführlich" msgctxt "#30622" msgid "Purchases" @@ -797,8 +796,8 @@ msgid "Install InputStream Helper" msgstr "InputStream Helper installieren" msgctxt "#30624" -msgid "" -msgstr "" +msgid "Members only" +msgstr "Nur für Mitglieder" msgctxt "#30625" msgid "InputStream Helper is already installed." @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "access_manager.json löschen" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Auf IP reagieren" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Minimaler Prozentwert für Wiedergabezähler" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Als ungesehen markieren" +msgid "%s removed" +msgstr "%s entfernt" msgctxt "#30670" -msgid "Mark watched" -msgstr "Als gesehen markieren" +msgid "Added %s" +msgstr "%s hinzugefügt" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "Wiedergabeverlauf" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Wiedergabepunkt zurücksetzen" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,12 +1136,12 @@ msgid "Play audio only" msgstr "Nur Audio wiedergeben" msgctxt "#30709" -msgid "" -msgstr "" +msgid "WebVTT subtitles" +msgstr "WebVTT-Untertitel" msgctxt "#30710" -msgid "" -msgstr "" +msgid "TTML subtitles" +msgstr "TTML-Untertitel" msgctxt "#30711" msgid "" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Videos in Wiedergabelisten bewerten" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Zu „Später ansehen“ hinzufügen" +msgid "Prefer dubbed audio over original audio" +msgstr "Synchronisierten Ton gegenüber Originalton bevorzugen" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Zur Wiedergabeliste hinzugefügt" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "Automatisch übersetzten Ton gegenüber Originalton bevorzugen" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Aus Wiedergabeliste entfernt" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "YouTube-interne Liste für Gesehen-Historie verwenden?[CR][CR]Anmelden via Addon und Aktivieren von Historienverfolgung in YouTube sind dafür nötig." msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "Negativ bewertete Videos" msgctxt "#30718" -msgid "Rating removed" -msgstr "Bewertung entfernt" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "YouTube-interne Liste für „Später ansehen“ verwenden?[CR][CR]Anmelden via Addon ist dafür nötig." msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,8 +1184,8 @@ msgid "Unsubscribed from channel" msgstr "Kanal nicht mehr abonniert" msgctxt "#30721" -msgid "" -msgstr "" +msgid "Enable Panoramic/180/360/VR video" +msgstr "Panorama/180/360/VR Video aktivieren" msgctxt "#30722" msgid "Enable HDR video" @@ -1253,8 +1252,8 @@ msgid "Shorts - Max duration" msgstr "Kurzclips - max. Länge" msgctxt "#30738" -msgid "" -msgstr "" +msgid "Enable spatial audio" +msgstr "Spatial Audio aktivieren" msgctxt "#30739" msgid "Subscribers" @@ -1533,8 +1532,8 @@ msgid "Use channel name as" msgstr "Kanalnamen benutzen als" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "Videos in Listen verstecken" +msgid "Hide items from listings" +msgstr "Listeneinträge verbergen" msgctxt "#30809" msgid "All upcoming videos" @@ -1584,6 +1583,83 @@ msgctxt "#30820" msgid "Podcast" msgstr "Podcast" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Auf IP reagieren" + +#~ msgctxt "#30547" +#~ msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +#~ msgstr "Es kann zum Aktivieren on zwei Anwendungen aufgefordert werden, damit YouTube korrekt funktioniert." + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Videos in Listen verstecken" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "3D erlauben" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Wiedergeben mit ..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Zu Meine-Abonnements-Filter hinzufügen" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Aus Meine-Abonnements-Filter entfernen" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Zu Meine-Abonnements-Filter hinzugefügt" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Aus Meine-Abonnements-Filter entfernt" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Mittel (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Hoch (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Aktualisiert: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Als ungesehen markieren" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Als gesehen markieren" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Wiedergabepunkt zurücksetzen" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Zu „Später ansehen“ hinzufügen" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Zur Wiedergabeliste hinzugefügt" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Aus Wiedergabeliste entfernt" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Bewertung entfernt" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Keine weiteren Links gefunden." diff --git a/plugin.video.youtube/resources/language/resource.language.el_gr/strings.po b/plugin.video.youtube/resources/language/resource.language.el_gr/strings.po index 7bbfa5873b..4e797d988e 100644 --- a/plugin.video.youtube/resources/language/resource.language.el_gr/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.el_gr/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Επιτρέψτε το 3Δ" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "Δείτε αργότερα" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Έξοδος" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Αφαίρεση \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "Προσθέστε στο..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Αναπαραγωγή με..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "Αποτυχία" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Μαύρη λίστα" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Προσθήκη στο φίλτρο των συνδρομών μου" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Αφαίρεση από το φίλτρο των συνδρομών μου" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Προσθήκη φίλτρο των συνδρομών μου" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Αφαίρεση από το φίλτρο των εγγραφών μου" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Μέγεθος εικονιδίων προεπισκόπησης" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Μέτριο (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Υψηλό (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "Αυστηρή" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Ενημερωθηκε: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Αποτυχία ενεργοποίησης προσωπικών κλειδιών API. Λείπει: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "Ξαναπροσπάθησε" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Η θύρα %s χρησιμοποιείται ήδη. Αδυναμία έναρξης εξυπηρετητή http." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Εγκατάσταση βοηθού InputStream" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Διαγραφή access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Ακρόαση στην IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Ελάχιστο ποσοστό καταμέτρησης αναπαραγωγής" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Σημείωση ως μη προβεβλημένο" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Σημείωση ως προβεβλημένο" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "ιστορικό αναπαραγωγής" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Επαναφορά σημείου συνέχισης" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "Αναπαραγωγή μόνο του ήχου" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Βαθμολόγηση βίντεο σε λίστες αναπαραγωγής" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Προσθήκη στο 'Παρακολουθήστε αργότερα'" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Έγινε η προσθήκη σε λίστα αναπαραγωγής" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Αφαίρεση από λίστα αναπαραγωγής" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "Μη αρεστό βίντεο" msgctxt "#30718" -msgid "Rating removed" -msgstr "Βαθμολόγηση αφαιρέθηκε" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Έγινε διαγραφή από το κανάλι" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,75 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Ακρόαση στην IP" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Επιτρέψτε το 3Δ" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Αναπαραγωγή με..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Προσθήκη στο φίλτρο των συνδρομών μου" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Αφαίρεση από το φίλτρο των συνδρομών μου" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Προσθήκη φίλτρο των συνδρομών μου" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Αφαίρεση από το φίλτρο των εγγραφών μου" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Μέτριο (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Υψηλό (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Ενημερωθηκε: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Σημείωση ως μη προβεβλημένο" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Σημείωση ως προβεβλημένο" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Επαναφορά σημείου συνέχισης" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Προσθήκη στο 'Παρακολουθήστε αργότερα'" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Έγινε η προσθήκη σε λίστα αναπαραγωγής" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Αφαίρεση από λίστα αναπαραγωγής" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Βαθμολόγηση αφαιρέθηκε" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Δεν βρέθηκαν άλλοι σύνδεσμοι." diff --git a/plugin.video.youtube/resources/language/resource.language.en_au/strings.po b/plugin.video.youtube/resources/language/resource.language.en_au/strings.po index cc83b11a07..a8a7f48c3b 100644 --- a/plugin.video.youtube/resources/language/resource.language.en_au/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.en_au/strings.po @@ -106,7 +106,7 @@ msgid "144p" msgstr "" msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -150,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -216,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -384,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -460,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -488,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -552,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -608,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -648,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -668,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -688,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -700,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -752,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -784,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -976,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -996,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1136,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1152,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1172,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1184,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1252,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1532,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.en_gb/strings.po b/plugin.video.youtube/resources/language/resource.language.en_gb/strings.po index 256e8fa179..3fe9220227 100644 --- a/plugin.video.youtube/resources/language/resource.language.en_gb/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.en_gb/strings.po @@ -109,7 +109,7 @@ msgid "144p" msgstr "" msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -153,7 +153,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -220,11 +220,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -240,7 +240,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -260,7 +260,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -306,11 +306,11 @@ msgstr "" # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -390,15 +390,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -466,7 +466,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -494,7 +494,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -558,11 +558,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -614,7 +614,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -654,19 +654,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -674,11 +674,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -694,11 +694,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -706,7 +706,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -758,19 +758,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -790,7 +790,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -802,7 +802,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -878,7 +878,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -982,11 +982,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -1002,7 +1002,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1142,11 +1142,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1158,15 +1158,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1178,7 +1178,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1190,7 +1190,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1258,7 +1258,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1538,7 +1538,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.en_nz/strings.po b/plugin.video.youtube/resources/language/resource.language.en_nz/strings.po index 28e472a3ad..dfd15c291f 100644 --- a/plugin.video.youtube/resources/language/resource.language.en_nz/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.en_nz/strings.po @@ -106,7 +106,7 @@ msgid "144p" msgstr "" msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -150,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -216,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -384,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -460,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -488,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -552,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -608,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -648,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -668,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -688,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -700,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -752,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -784,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -976,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -996,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1136,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1152,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1172,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1184,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1252,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1532,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.en_us/strings.po b/plugin.video.youtube/resources/language/resource.language.en_us/strings.po index e3a08ba485..aaf1c00ad4 100644 --- a/plugin.video.youtube/resources/language/resource.language.en_us/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.en_us/strings.po @@ -106,7 +106,7 @@ msgid "144p" msgstr "" msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -150,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -216,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -384,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -460,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -488,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -552,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -608,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -648,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -668,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -688,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -700,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -752,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -784,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -976,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -996,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1136,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1152,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1172,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1184,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1252,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1532,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.eo/strings.po b/plugin.video.youtube/resources/language/resource.language.eo/strings.po index 57c769db66..30ddb4c64a 100644 --- a/plugin.video.youtube/resources/language/resource.language.eo/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.eo/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "144p" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Permesi 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "Ĉu konfiguri %s?" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "Spekti poste" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Elsaluti" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "Aldoni al..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Ludi per..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "Malsukcesis" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "Nigra listo" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,12 +668,12 @@ msgid "Thumbnail size" msgstr "Grando de miniaturoj" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Meza (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Granda (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,23 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Permesi 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Ludi per..." + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Meza (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Granda (4:3)" + #~ msgctxt "#30106" #~ msgid "Next Page (%d)" #~ msgstr "Sekva paĝo (%d)" diff --git a/plugin.video.youtube/resources/language/resource.language.es_ar/strings.po b/plugin.video.youtube/resources/language/resource.language.es_ar/strings.po index ccf114668b..761f2c095d 100644 --- a/plugin.video.youtube/resources/language/resource.language.es_ar/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.es_ar/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.es_es/strings.po b/plugin.video.youtube/resources/language/resource.language.es_es/strings.po index f9a7803b3c..8d346e7ba1 100644 --- a/plugin.video.youtube/resources/language/resource.language.es_es/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.es_es/strings.po @@ -5,9 +5,9 @@ msgid "" msgstr "" "Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2025-05-12 21:19+0000\n" +"PO-Revision-Date: 2025-09-23 09:09+0000\n" "Last-Translator: Alfonso Cachero \n" "Language-Team: Spanish (Spain) \n" "Language: es_es\n" @@ -15,7 +15,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.11.3\n" +"X-Generator: Weblate 5.13.3\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "144p" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Permitir 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "Activar vídeo estereoscópico 3D" msgctxt "#30021" msgid "Show fanart" @@ -151,8 +150,8 @@ msgid "Configure %s?" msgstr "¿Configurar %s?" msgctxt "#30031" -msgid "" -msgstr " " +msgid "Requests cache size (MB)" +msgstr "Tamaño de caché de peticiones (MB)" msgctxt "#30032" msgid "View: TV Shows" @@ -217,12 +216,12 @@ msgid "Watch Later" msgstr "Ver más tarde" msgctxt "#30108" -msgid "" -msgstr " " +msgid "Enter the name of the bookmark" +msgstr "Introduce el nombre del marcador" msgctxt "#30109" -msgid "" -msgstr " " +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "Introduce una URL de YouTube o plugin válida para el marcador" msgctxt "#30110" msgid "New Search" @@ -237,8 +236,8 @@ msgid "Sign Out" msgstr "Cerrar sesión" msgctxt "#30113" -msgid "" -msgstr " " +msgid "Related to \"%s\"" +msgstr "Relacionado con \"%s\"" msgctxt "#30114" msgid "Confirm delete" @@ -257,8 +256,8 @@ msgid "Remove \"%s\"?" msgstr "¿Está seguro de que desea borrar \"%s\"?" msgctxt "#30118" -msgid "" -msgstr " " +msgid "Links from \"%s\"" +msgstr "Enlaces de \"%s\"" msgctxt "#30119" msgid "Please wait..." @@ -301,12 +300,12 @@ msgstr " " # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" -msgstr " " +msgid "%s failed" +msgstr "%s fallado" msgctxt "#30501" -msgid "" -msgstr " " +msgid "Edit %s" +msgstr "Editar %s" msgctxt "#30502" msgid "Go to %s" @@ -385,16 +384,16 @@ msgid "Add to..." msgstr "Añadir a..." msgctxt "#30521" -msgid "" -msgstr " " +msgid "Delete requests cache database" +msgstr "Borrar base de datos de caché de peticiones" msgctxt "#30522" -msgid "" -msgstr " " +msgid "Clear requests cache" +msgstr "Limpiar caché de peticiones" msgctxt "#30523" -msgid "" -msgstr " " +msgid "requests cache" +msgstr "caché de peticiones" msgctxt "#30524" msgid "Select language" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "Reproducir lo añadido recientemente" msgctxt "#30540" -msgid "Play with..." -msgstr "Abrir con..." +msgid "" +msgstr " " msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,8 +488,8 @@ msgid "Please complete all login prompts" msgstr "Por favor, complete todas las peticiones de inicio de sesión" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." -msgstr "Puede que se pidan activar dos aplicaciones para que YouTube funcione correctamente." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +msgstr "Se pedirá inicio de sesión y habilitar acceso a múltiples aplicaciones para que este addon funcione correctamente." msgctxt "#30548" msgid "" @@ -553,12 +552,12 @@ msgid "View all" msgstr "Ver todo" msgctxt "#30563" -msgid "" -msgstr " " +msgid "Plugin execution timeout" +msgstr "Tiempo máximo de espera de ejecución de plugin" msgctxt "#30564" -msgid "" -msgstr " " +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "Sólo para pruebas - Termina la ejecución del plugin al pasar ese tiempo. 0 segundos (por defecto) para desactivarlo." msgctxt "#30565" msgid "" @@ -609,8 +608,8 @@ msgid "Failed" msgstr "¡Vaya! Algo ha fallado" msgctxt "#30577" -msgid "" -msgstr " " +msgid "Smallest (4:3)" +msgstr "El más pequeño (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Lista negra" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Añadir filtro a 'Mis Suscripciones'" +msgid "Hide \"Playlists\" folder" +msgstr "Ocultar carpeta \"Listas\"" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Borrar filtro de 'Mis Suscripciones'" +msgid "Hide \"Search\" folder" +msgstr "Ocultar carpeta \"Buscar\"" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Añadido a filtro de 'Mis Suscripciones'" +msgid "Hide \"Shorts\" folder" +msgstr "Ocultar carpeta \"Cortos\"" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Borrado filtro de 'Mis Suscripciones'" +msgid "Hide \"Live\" folder" +msgstr "Ocultar carpeta \"Directo\"" msgctxt "#30591" msgid "Thumbnail size" msgstr "Tamaño de miniaturas" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Medio (16:9)" +msgid "Small (16:9)" +msgstr "Pequeño (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Alto (4:3)" +msgid "Medium (4:3)" +msgstr "Mediano (4:3)" msgctxt "#30594" msgid "Safe search" @@ -689,20 +688,20 @@ msgid "Strict" msgstr "Estricto" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Actualizado: %s" +msgid "Hide \"Members only\" folder" +msgstr "Ocultar carpeta \"Solo miembros\"" msgctxt "#30598" -msgid "" -msgstr " " +msgid "Large (4:3)" +msgstr "Grande (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" msgstr "Error al habilitar claves API personales. No encontrado: %s" msgctxt "#30600" -msgid "" -msgstr " " +msgid "Largest (16:9)" +msgstr "El más grande (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" @@ -753,20 +752,20 @@ msgid "Retry" msgstr "Volver a intentarlo" msgctxt "#30613" -msgid "" -msgstr " " +msgid "Add to %s" +msgstr "Añadir a %s" msgctxt "#30614" -msgid "" -msgstr " " +msgid "Remove from %s" +msgstr "Quitar de %s" msgctxt "#30615" -msgid "" -msgstr " " +msgid "Added to %s" +msgstr "Añadido a %s" msgctxt "#30616" -msgid "" -msgstr " " +msgid "Removed from %s" +msgstr "Quitado de %s" msgctxt "#30617" msgid "InputStream.Adaptive" @@ -785,8 +784,8 @@ msgid "Port %s already in use. Cannot start http server." msgstr "El puerto %s ya esta en uso. No se ha podido iniciar el servidor HTTP." msgctxt "#30621" -msgid "" -msgstr " " +msgid "Verbose" +msgstr "Detallado" msgctxt "#30622" msgid "Purchases" @@ -797,8 +796,8 @@ msgid "Install InputStream Helper" msgstr "Instalar InputStream Helper" msgctxt "#30624" -msgid "" -msgstr " " +msgid "Members only" +msgstr "Solo miembros" msgctxt "#30625" msgid "InputStream Helper is already installed." @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Eliminar access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "IP a la escucha" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Porcentaje mínimo para contar como reproducido" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Marcar como no visto" +msgid "%s removed" +msgstr "%s quitado" msgctxt "#30670" -msgid "Mark watched" -msgstr "Marcar como visto" +msgid "Added %s" +msgstr "Añadido %s" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "historial de reproducción" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Restablecer punto de reanudación" +msgid "" +msgstr " " msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,12 +1136,12 @@ msgid "Play audio only" msgstr "Reproducir sólo audio" msgctxt "#30709" -msgid "" -msgstr " " +msgid "WebVTT subtitles" +msgstr "Subtítulos WebVTT" msgctxt "#30710" -msgid "" -msgstr " " +msgid "TTML subtitles" +msgstr "Subtítulos TTML" msgctxt "#30711" msgid "" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Valorar vídeos en listas de reproducción" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Añadido a Ver más tarde" +msgid "Prefer dubbed audio over original audio" +msgstr "Preferir audio doblado en lugar del original" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Añadido a lista de reproducción" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "Preferir automáticamente audio doblado traducido en lugar del original" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Borrado de la lista de reproducción" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "¿Usar lista interna de YouTube Historial de Visualizaciones?[CR][CR]Requiere iniciar sesión en el addon y activar el seguimiento de historial en YouTube." msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "No me gusto el vídeo" msgctxt "#30718" -msgid "Rating removed" -msgstr "Valoración eliminada" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "¿Usar lista interna de YouTube Ver Mas Tarde?[CR][CR]Requiere iniciar sesión en el addon." msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,8 +1184,8 @@ msgid "Unsubscribed from channel" msgstr "Desuscrito del canal" msgctxt "#30721" -msgid "" -msgstr " " +msgid "Enable Panoramic/180/360/VR video" +msgstr "Activar vídeo Panorámico/180/360/VR" msgctxt "#30722" msgid "Enable HDR video" @@ -1253,8 +1252,8 @@ msgid "Shorts - Max duration" msgstr "Cortos - Duración máxima" msgctxt "#30738" -msgid "" -msgstr " " +msgid "Enable spatial audio" +msgstr "Activar audio espacial" msgctxt "#30739" msgid "Subscribers" @@ -1533,8 +1532,8 @@ msgid "Use channel name as" msgstr "Usar el nombre del canal como" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "Ocultar vídeos de listados" +msgid "Hide items from listings" +msgstr "Ocultar elementos de listados" msgctxt "#30809" msgid "All upcoming videos" @@ -1584,6 +1583,205 @@ msgctxt "#30820" msgid "Podcast" msgstr "Podcast" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "IP a la escucha" + +#~ msgctxt "#30113" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30118" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30547" +#~ msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +#~ msgstr "Puede que se pidan activar dos aplicaciones para que YouTube funcione correctamente." + +#~ msgctxt "#30587" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30588" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30589" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30590" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30597" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30624" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Ocultar vídeos de listados" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Permitir 3D" + +#~ msgctxt "#30031" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30108" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30109" +#~ msgid "" +#~ msgstr " " + +# YouTube +# empty strings from id 30206 to 30499 +#~ msgctxt "#30500" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30501" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30521" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30522" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30523" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Abrir con..." + +#~ msgctxt "#30563" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30564" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30577" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Añadir filtro a 'Mis Suscripciones'" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Borrar filtro de 'Mis Suscripciones'" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Añadido a filtro de 'Mis Suscripciones'" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Borrado filtro de 'Mis Suscripciones'" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Medio (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Alto (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Actualizado: %s" + +#~ msgctxt "#30598" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30600" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30613" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30614" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30615" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30616" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30621" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Marcar como no visto" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Marcar como visto" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Restablecer punto de reanudación" + +#~ msgctxt "#30709" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30710" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Añadido a Ver más tarde" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Añadido a lista de reproducción" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Borrado de la lista de reproducción" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Valoración eliminada" + +#~ msgctxt "#30721" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30738" +#~ msgid "" +#~ msgstr " " + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "No se encontraron más enlaces." diff --git a/plugin.video.youtube/resources/language/resource.language.es_mx/strings.po b/plugin.video.youtube/resources/language/resource.language.es_mx/strings.po index 82e5d2301e..708fb09aea 100644 --- a/plugin.video.youtube/resources/language/resource.language.es_mx/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.es_mx/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Permitir 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "Ver más tarde" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Cerrar sesión" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "¿Estás seguro de que quieres borrar \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "Añadir a..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Reproducir con..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "Ocurrió un error" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Lista negra" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Añadir filtro a 'Mis suscripciones'" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Eliminar filtro de 'Mis suscripciones'" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Añadido a filtro de 'Mis suscripciones'" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Eliminado de filtro de 'Mis suscripciones'" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Tamaño de miniaturas" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Medio (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Alto (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "Estricto" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Actualizado: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Error al habilitar claves API personales. Falta: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "Volver a intentarlo" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "El puerto %s ya esta en uso. No se ha podido iniciar el servidor HTTP." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Instalar InputStream Helper" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Eliminar access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Escuchando en IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Porcentaje mínimo total para marcar como visto" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Marcar como no visto" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Marcar como visto" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "historial de reproducción" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Restablecer punto de reanudación de reproducción" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "Reproducir audio solamente" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Calificar videos en listas de reproducción" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Añadido a Ver más tarde" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Añadido a lista de reproducción" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Eliminado de lista de reproducción" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "No te gusta el video" msgctxt "#30718" -msgid "Rating removed" -msgstr "Calificación eliminada" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Des-sucrito del canal" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,75 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Escuchando en IP" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Permitir 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Reproducir con..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Añadir filtro a 'Mis suscripciones'" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Eliminar filtro de 'Mis suscripciones'" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Añadido a filtro de 'Mis suscripciones'" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Eliminado de filtro de 'Mis suscripciones'" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Medio (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Alto (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Actualizado: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Marcar como no visto" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Marcar como visto" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Restablecer punto de reanudación de reproducción" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Añadido a Ver más tarde" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Añadido a lista de reproducción" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Eliminado de lista de reproducción" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Calificación eliminada" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "No se encontraron enlaces." diff --git a/plugin.video.youtube/resources/language/resource.language.et_ee/strings.po b/plugin.video.youtube/resources/language/resource.language.et_ee/strings.po index 3056c20809..4b615b03b7 100644 --- a/plugin.video.youtube/resources/language/resource.language.et_ee/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.et_ee/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.eu_es/strings.po b/plugin.video.youtube/resources/language/resource.language.eu_es/strings.po index d3b703c26c..5e2b048d2d 100644 --- a/plugin.video.youtube/resources/language/resource.language.eu_es/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.eu_es/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.fa_af/strings.po b/plugin.video.youtube/resources/language/resource.language.fa_af/strings.po index 3640cdf9df..755b958a41 100644 --- a/plugin.video.youtube/resources/language/resource.language.fa_af/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.fa_af/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.fa_ir/strings.po b/plugin.video.youtube/resources/language/resource.language.fa_ir/strings.po index f96479ade5..961783aff7 100644 --- a/plugin.video.youtube/resources/language/resource.language.fa_ir/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.fa_ir/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.fi_fi/strings.po b/plugin.video.youtube/resources/language/resource.language.fi_fi/strings.po index 0ee7f872af..c2c5445bf0 100644 --- a/plugin.video.youtube/resources/language/resource.language.fi_fi/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.fi_fi/strings.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" "PO-Revision-Date: 2025-05-29 22:48+0000\n" "Last-Translator: Oskari Lavinto \n" @@ -106,8 +106,8 @@ msgid "144p" msgstr "144p" msgctxt "#30020" -msgid "Allow 3D" -msgstr "Salli 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -150,7 +150,7 @@ msgid "Configure %s?" msgstr "Määritetäänkö %s?" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -216,11 +216,11 @@ msgid "Watch Later" msgstr "Katso myöhemmin" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "Kirjaudu ulos" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Poistetaanko \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -384,15 +384,15 @@ msgid "Add to..." msgstr "Lisää soittolistalle..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -460,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Toista soittimella..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -488,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -552,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -608,7 +608,7 @@ msgid "Failed" msgstr "Epäonnistui" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -648,32 +648,32 @@ msgid "Blacklist" msgstr "Estolista" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Lisää \"Omat tilaukset\" -suodattimeen" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Poista \"Omat tilaukset\" -suodattimesta" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Lisätty \"Omat tilaukset\" -suodattimeen" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Poistettu \"Omat tilaukset\" -suodattimesta" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Pienoiskuvien koko" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Keskitasoinen (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Korkea (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -688,11 +688,11 @@ msgid "Strict" msgstr "Tiukka" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Päivitetty: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -700,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Henkilökohtaisten API-avainten käyttöönotto epäonnistui. Puuttuu: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -752,19 +752,19 @@ msgid "Retry" msgstr "Yritä uudelleen" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -784,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Portti %s on jo käytössä. HTTP-palvelimen käynnistys ei onnistu." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Asenna InputStream Helper -lisäosa" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Poista \"access_manager.json\" -tiedosto" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Kuunneltava IP-osoite" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -976,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Toistetuksi merkinnän vähimmäisprosentti" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Merkitse katsomattomaksi" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Merkitse katsotuksi" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -996,8 +996,8 @@ msgid "playback history" msgstr "toistohistoria" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Unohda katselutilanne" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1136,11 +1136,11 @@ msgid "Play audio only" msgstr "Toista vain ääni" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1152,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Arvioi videot toistettaessa soittolistoja" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Lisättiin \"Katso myöhemmin\" -listalle" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Lisättiin soittolistalle" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Poistettiin soittolistalta" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1172,8 +1172,8 @@ msgid "Disliked video" msgstr "Ei-tykätty video" msgctxt "#30718" -msgid "Rating removed" -msgstr "Arvio poistettiin" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1184,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Kanavan tilaus lopetettiin" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1252,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1532,8 +1532,8 @@ msgid "Use channel name as" msgstr "Käytä kanvan nimeä" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "Piilota videot listauksista" +msgid "Hide items from listings" +msgstr "" msgctxt "#30809" msgid "All upcoming videos" @@ -1583,6 +1583,78 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Kuunneltava IP-osoite" + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Piilota videot listauksista" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Salli 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Toista soittimella..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Lisää \"Omat tilaukset\" -suodattimeen" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Poista \"Omat tilaukset\" -suodattimesta" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Lisätty \"Omat tilaukset\" -suodattimeen" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Poistettu \"Omat tilaukset\" -suodattimesta" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Keskitasoinen (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Korkea (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Päivitetty: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Merkitse katsomattomaksi" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Merkitse katsotuksi" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Unohda katselutilanne" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Lisättiin \"Katso myöhemmin\" -listalle" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Lisättiin soittolistalle" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Poistettiin soittolistalta" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Arvio poistettiin" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Enempää linkkejä ei löytynyt." diff --git a/plugin.video.youtube/resources/language/resource.language.fo_fo/strings.po b/plugin.video.youtube/resources/language/resource.language.fo_fo/strings.po index d42cf2de2b..8a3cb6a0f1 100644 --- a/plugin.video.youtube/resources/language/resource.language.fo_fo/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.fo_fo/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.fr_ca/strings.po b/plugin.video.youtube/resources/language/resource.language.fr_ca/strings.po index 15897584ea..7133a79251 100644 --- a/plugin.video.youtube/resources/language/resource.language.fr_ca/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.fr_ca/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.fr_fr/strings.po b/plugin.video.youtube/resources/language/resource.language.fr_fr/strings.po index 715c12434f..df787dc9ba 100644 --- a/plugin.video.youtube/resources/language/resource.language.fr_fr/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.fr_fr/strings.po @@ -106,8 +106,8 @@ msgid "144p" msgstr "144p" msgctxt "#30020" -msgid "Allow 3D" -msgstr "Autoriser la 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -150,7 +150,7 @@ msgid "Configure %s?" msgstr "Configurer %s ?" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -216,11 +216,11 @@ msgid "Watch Later" msgstr "Regarder plus tard" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "Déconnexion" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Supprimer \"%s\" ?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -384,15 +384,15 @@ msgid "Add to..." msgstr "Ajouter à..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -460,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Lire avec..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -488,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -552,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -608,7 +608,7 @@ msgid "Failed" msgstr "Échoué" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -648,32 +648,32 @@ msgid "Blacklist" msgstr "Liste noire" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Ajouter à ma liste d'abonnement filtré" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Supprimer de mes abonnements filtré" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Ajouté pour mes abonnements filtré" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Supprimer de mes abonnements filtré" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Taille des vignettes" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Moyennes (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Hautes (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -688,11 +688,11 @@ msgid "Strict" msgstr "Stricte" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Actualisé: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -700,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Impossible d'activer les clés API personnelles. Disparu : %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -752,19 +752,19 @@ msgid "Retry" msgstr "Recommencer" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -784,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Le port %s est déjà en cours d'utilisation. Impossible de démarrer le serveur http." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Installer l'aide d'InputStream" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Supprimer access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Écoute sur IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -976,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Pourcentage minimum du nombre de lectures" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Marquer comme non vu" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Marquer comme vu" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -996,8 +996,8 @@ msgid "playback history" msgstr "historique de lecture" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Réinitialiser le point de reprise de lecture" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1136,11 +1136,11 @@ msgid "Play audio only" msgstr "Lire seulement l'audio" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1152,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Noter les vidéos des playlists" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Ajouté à \"À regarder plus tard\"" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Ajouté à la liste de lecture" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Supprimé de la liste de lecture" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1172,8 +1172,8 @@ msgid "Disliked video" msgstr "Vidéo que je n'aime pas" msgctxt "#30718" -msgid "Rating removed" -msgstr "Note supprimée" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1184,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Désabonné de la chaîne" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1252,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1532,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1583,6 +1583,74 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Écoute sur IP" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Autoriser la 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Lire avec..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Ajouter à ma liste d'abonnement filtré" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Supprimer de mes abonnements filtré" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Ajouté pour mes abonnements filtré" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Supprimer de mes abonnements filtré" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Moyennes (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Hautes (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Actualisé: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Marquer comme non vu" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Marquer comme vu" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Réinitialiser le point de reprise de lecture" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Ajouté à \"À regarder plus tard\"" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Ajouté à la liste de lecture" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Supprimé de la liste de lecture" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Note supprimée" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Aucun autre lien trouvé." diff --git a/plugin.video.youtube/resources/language/resource.language.gl_es/strings.po b/plugin.video.youtube/resources/language/resource.language.gl_es/strings.po index 5a974d073f..4db576a8e7 100644 --- a/plugin.video.youtube/resources/language/resource.language.gl_es/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.gl_es/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.he_il/strings.po b/plugin.video.youtube/resources/language/resource.language.he_il/strings.po index bedff267d2..71b1e3f899 100644 --- a/plugin.video.youtube/resources/language/resource.language.he_il/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.he_il/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "אפשר 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "צפה בהמשך" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "התנתק" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "הסר \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "הוסף אל..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "נגן עם..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "נכשל" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "רשימה שחורה" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "הוסף למסנן המנויים שלי" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "הסר מתוך מסנן המנויים שלי" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "נוסף למסנן המנויים שלי" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "הוסר ממסנן המנויים שלי" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "גודל תמונה ממוזערת" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "בינוני (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "גבוה (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "קפדני" msgctxt "#30597" -msgid "Updated: %s" -msgstr "עודכן: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "נכשל בהפעלת מפתח ה-API האישי. חסר: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,43 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "אפשר 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "נגן עם..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "הוסף למסנן המנויים שלי" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "הסר מתוך מסנן המנויים שלי" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "נוסף למסנן המנויים שלי" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "הוסר ממסנן המנויים שלי" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "בינוני (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "גבוה (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "עודכן: %s" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "לא נמצאו קישורים נוספים." diff --git a/plugin.video.youtube/resources/language/resource.language.hi_in/strings.po b/plugin.video.youtube/resources/language/resource.language.hi_in/strings.po index 7f437cb1a7..395edbf221 100644 --- a/plugin.video.youtube/resources/language/resource.language.hi_in/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.hi_in/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.hr_hr/strings.po b/plugin.video.youtube/resources/language/resource.language.hr_hr/strings.po index dfa2fed540..e96c2c63a6 100644 --- a/plugin.video.youtube/resources/language/resource.language.hr_hr/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.hr_hr/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Dopusti 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "Gledat ću kasnije" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Odjava" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Ukloni \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "Dodaj u..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Gledaj s..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "Neuspješno" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Popis zabranjenih" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Dodaj u filter Moje pretplate" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Ukloni iz filtra Moje pretplate" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Dodano u filter Moje pretplate" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Uklonjeno iz filtra Moje pretplate" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Veličina minijatura" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Srednja (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Velika (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "Potpuna" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Nadopunjeno: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Neuspjelo omogućavanje osobnih API ključeva. Nedostaje: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "Pokušaj ponovno" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Ulaz %s se već koristi. Nemoguće pokretanje http poslužitelja." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Instaliraj InputStream Helper" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Obriši access_manager.json datoteku" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Osluškuj IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Najmanji postotak za označavanje kao reproducirano" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Označi kao neodgledano" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Označi kao pogledano" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "povijest reprodukcije" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Poništi vrijeme nastavka gledanja" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "Slušaj samo zvuk" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Ocijeni videozapise u popisu izvođenja" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Dodano u Gledat ću kasnije" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Dodano u popis izvođenja" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Uklonjeno iz popisa izvođenja" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "Video koji mi se ne sviđa" msgctxt "#30718" -msgid "Rating removed" -msgstr "Ocjena je uklonjena" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Prekinuta je pretplata na kanal" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,75 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Osluškuj IP" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Dopusti 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Gledaj s..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Dodaj u filter Moje pretplate" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Ukloni iz filtra Moje pretplate" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Dodano u filter Moje pretplate" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Uklonjeno iz filtra Moje pretplate" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Srednja (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Velika (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Nadopunjeno: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Označi kao neodgledano" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Označi kao pogledano" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Poništi vrijeme nastavka gledanja" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Dodano u Gledat ću kasnije" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Dodano u popis izvođenja" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Uklonjeno iz popisa izvođenja" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Ocjena je uklonjena" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Nema pronađenih daljnjih poveznica." diff --git a/plugin.video.youtube/resources/language/resource.language.hu_hu/strings.po b/plugin.video.youtube/resources/language/resource.language.hu_hu/strings.po index e347c869ca..78ac3d32d3 100644 --- a/plugin.video.youtube/resources/language/resource.language.hu_hu/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.hu_hu/strings.po @@ -106,8 +106,8 @@ msgid "144p" msgstr "" msgctxt "#30020" -msgid "Allow 3D" -msgstr "3D engedélyezés" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -150,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -216,11 +216,11 @@ msgid "Watch Later" msgstr "Megnézendő videók" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "Kijelentkezés" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Eltávolítja ezt: \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -384,15 +384,15 @@ msgid "Add to..." msgstr "Hozzáadás..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -460,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Lejátszás..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -488,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -552,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -608,7 +608,7 @@ msgid "Failed" msgstr "Nem sikerült" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -648,32 +648,32 @@ msgid "Blacklist" msgstr "Feketelista" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Hozzáadás a szűrt Felíratkozásokhoz" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Eltávolítás a szűrt Felíratkozások közül" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Hozzáadva a szűrt Felíratkozásokhoz" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Eltávolítva a szűrt Felíratkozások közül" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Bélyegkép méret" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Közepes (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Magas (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -688,11 +688,11 @@ msgid "Strict" msgstr "Szigorú" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Frissítve: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -700,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Nem sikerült engedélyezni a személyes API-kulcsokat. Hiányzik: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -752,19 +752,19 @@ msgid "Retry" msgstr "Próbálja meg újra" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -784,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "%s port használatban van. Nem lehet elindítani a http kiszolgálót." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "InputStream Helper telepítés" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Az access_manager.json törlése" msgctxt "#30643" -msgid "Listen on IP" -msgstr "IP cím figyelése" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -976,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Lejátszások száma minimum százalék" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Megjelölés nem megtekintettként" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Megjelölés megtekintettként" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -996,8 +996,8 @@ msgid "playback history" msgstr "lejátszási előzmények" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Folytatási pont visszaállítása" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1136,11 +1136,11 @@ msgid "Play audio only" msgstr "Csak hang lejátszás" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1152,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Videók értékelése a lejátszási listákban" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Hozzáadva a megnézendő videókhoz" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Hozzáadva a lejátszási listához" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Eltávolítva a lejátszási listáról" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1172,8 +1172,8 @@ msgid "Disliked video" msgstr "Nem kedvelt video" msgctxt "#30718" -msgid "Rating removed" -msgstr "Értékelés törölve" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1184,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Leiratkozott a csatornáról" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1252,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1532,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1583,6 +1583,74 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "IP cím figyelése" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "3D engedélyezés" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Lejátszás..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Hozzáadás a szűrt Felíratkozásokhoz" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Eltávolítás a szűrt Felíratkozások közül" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Hozzáadva a szűrt Felíratkozásokhoz" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Eltávolítva a szűrt Felíratkozások közül" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Közepes (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Magas (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Frissítve: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Megjelölés nem megtekintettként" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Megjelölés megtekintettként" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Folytatási pont visszaállítása" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Hozzáadva a megnézendő videókhoz" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Hozzáadva a lejátszási listához" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Eltávolítva a lejátszási listáról" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Értékelés törölve" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Nem találhatók további hivatkozások." diff --git a/plugin.video.youtube/resources/language/resource.language.hy_am/strings.po b/plugin.video.youtube/resources/language/resource.language.hy_am/strings.po index 35f91dc781..cb0132a7ab 100644 --- a/plugin.video.youtube/resources/language/resource.language.hy_am/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.hy_am/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.id_id/strings.po b/plugin.video.youtube/resources/language/resource.language.id_id/strings.po index 7b22278673..e5acf81a67 100644 --- a/plugin.video.youtube/resources/language/resource.language.id_id/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.id_id/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Izinkan 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "Nonton Nanti" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Keluar" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Hapus \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "Tambahkan ke..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Putar dengan..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "Gagal" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Daftar hitam" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Tambahkan ke filter Langgananku" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Hapus dari filter Langgananku" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Ditambahkan ke filter Langgananku" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Dihapus dari filter Langgananku" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Ukuran Thumbnail" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Sedang (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Tinggi (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "Ketat" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Diperbarui: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Gagal mengaktifkan kunci API pribadi. Tidak ada: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "Coba lagi" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Port %s sudah digunakan. Tidak dapat memulai server http." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Instal Pembantu InputStream" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Hapus access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Dengarkan di IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Mainkan menghitung persentase minimum" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Tandai belum ditonton" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Tandai ditonton" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "riwayat pemutaran" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Setel ulang titik resume" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "Putar hanya audio" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Beri nilai video dalam daftar putar" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Ditambahkan ke Tonton Nanti" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Ditambahkan ke daftar putar" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Dihapus dari daftar putar" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "Video tak disukai" msgctxt "#30718" -msgid "Rating removed" -msgstr "Nilai dihapus" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Berhenti berlangganan dari saluran" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,75 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Dengarkan di IP" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Izinkan 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Putar dengan..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Tambahkan ke filter Langgananku" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Hapus dari filter Langgananku" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Ditambahkan ke filter Langgananku" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Dihapus dari filter Langgananku" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Sedang (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Tinggi (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Diperbarui: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Tandai belum ditonton" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Tandai ditonton" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Setel ulang titik resume" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Ditambahkan ke Tonton Nanti" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Ditambahkan ke daftar putar" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Dihapus dari daftar putar" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Nilai dihapus" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Tidak ada tautan lebih lanjut yang ditemukan." diff --git a/plugin.video.youtube/resources/language/resource.language.is_is/strings.po b/plugin.video.youtube/resources/language/resource.language.is_is/strings.po index b95d7c54da..1897de95d8 100644 --- a/plugin.video.youtube/resources/language/resource.language.is_is/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.is_is/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.it_it/strings.po b/plugin.video.youtube/resources/language/resource.language.it_it/strings.po index 45d9def490..6b34a9dcba 100644 --- a/plugin.video.youtube/resources/language/resource.language.it_it/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.it_it/strings.po @@ -8,7 +8,7 @@ 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-05-10 22:41+0000\n" +"PO-Revision-Date: 2025-11-15 23:52+0000\n" "Last-Translator: Massimo Pissarello \n" "Language-Team: Italian \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.11.3\n" +"X-Generator: Weblate 5.14.3\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -106,10 +106,9 @@ msgctxt "#30019" msgid "144p" msgstr "144p" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Consenti 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "Abilita video 3D stereoscopico" msgctxt "#30021" msgid "Show fanart" @@ -152,8 +151,8 @@ msgid "Configure %s?" msgstr "Configurare %s?" msgctxt "#30031" -msgid "" -msgstr " " +msgid "Requests cache size (MB)" +msgstr "Dimensione cache richieste (MB)" msgctxt "#30032" msgid "View: TV Shows" @@ -218,12 +217,12 @@ msgid "Watch Later" msgstr "Guarda più tardi" msgctxt "#30108" -msgid "" -msgstr " " +msgid "Enter the name of the bookmark" +msgstr "Inserisci nome del segnalibro" msgctxt "#30109" -msgid "" -msgstr " " +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "Inserisci un URL YouTube o un plugin valido per il segnalibro" msgctxt "#30110" msgid "New Search" @@ -238,8 +237,8 @@ msgid "Sign Out" msgstr "Disconnessione" msgctxt "#30113" -msgid "" -msgstr " " +msgid "Related to \"%s\"" +msgstr "Correlato a \"%s\"" msgctxt "#30114" msgid "Confirm delete" @@ -258,8 +257,8 @@ msgid "Remove \"%s\"?" msgstr "Rimuovere \"%s\"?" msgctxt "#30118" -msgid "" -msgstr " " +msgid "Links from \"%s\"" +msgstr "Link da \"%s\"" msgctxt "#30119" msgid "Please wait..." @@ -302,12 +301,12 @@ msgstr " " # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" -msgstr " " +msgid "%s failed" +msgstr "%s non riuscito" msgctxt "#30501" -msgid "" -msgstr " " +msgid "Edit %s" +msgstr "Modifica %s" msgctxt "#30502" msgid "Go to %s" @@ -386,16 +385,16 @@ msgid "Add to..." msgstr "Aggiungi a..." msgctxt "#30521" -msgid "" -msgstr " " +msgid "Delete requests cache database" +msgstr "Elimina richieste dalla cache del database" msgctxt "#30522" -msgid "" -msgstr " " +msgid "Clear requests cache" +msgstr "Pulisci cache delle richieste" msgctxt "#30523" -msgid "" -msgstr " " +msgid "requests cache" +msgstr "cache delle richieste" msgctxt "#30524" msgid "Select language" @@ -462,8 +461,8 @@ msgid "Play recently added" msgstr "Riproduci aggiunti recentemente" msgctxt "#30540" -msgid "Play with..." -msgstr "Riproduci con..." +msgid "" +msgstr " " msgctxt "#30541" msgid "Show channel name and video details in description" @@ -490,8 +489,8 @@ msgid "Please complete all login prompts" msgstr "Completa tutte le richieste di accesso" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." -msgstr "Potrebbe esserti richiesto di abilitare due applicazioni affinché YouTube funzioni correttamente." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +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 "" @@ -554,12 +553,12 @@ msgid "View all" msgstr "Visualizza tutto" msgctxt "#30563" -msgid "" -msgstr " " +msgid "Plugin execution timeout" +msgstr "Timeout esecuzione plugin" msgctxt "#30564" -msgid "" -msgstr " " +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "Solo a scopo di test: termina forzatamente l'esecuzione del plugin dopo il limite di tempo impostato. Imposta su 0 secondi (impostazione predefinita) per disabilitare." msgctxt "#30565" msgid "" @@ -610,8 +609,8 @@ msgid "Failed" msgstr "Operazione fallita" msgctxt "#30577" -msgid "" -msgstr " " +msgid "Smallest (4:3)" +msgstr "Più piccolo (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" @@ -650,32 +649,32 @@ msgid "Blacklist" msgstr "Blacklist" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Aggiungi al filtro Le mie iscrizioni" +msgid "Hide \"Playlists\" folder" +msgstr "Nascondi cartella \"Playlist\"" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Rimuovi dal filtro Le mie iscrizioni" +msgid "Hide \"Search\" folder" +msgstr "Nascondi cartella \"Ricerca\"" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Aggiungi al filtro Le mie iscrizioni" +msgid "Hide \"Shorts\" folder" +msgstr "Nascondi cartella \"Video brevi\"" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Rimuovi dal filtro Le mie iscrizioni" +msgid "Hide \"Live\" folder" +msgstr "Nascondi cartella \"Live\"" msgctxt "#30591" msgid "Thumbnail size" msgstr "Dimensioni miniature" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Media (16:9)" +msgid "Small (16:9)" +msgstr "Piccolo (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Grande (4:3)" +msgid "Medium (4:3)" +msgstr "Medio (4:3)" msgctxt "#30594" msgid "Safe search" @@ -690,20 +689,20 @@ msgid "Strict" msgstr "Severo" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Aggiornato: %s" +msgid "Hide \"Members only\" folder" +msgstr "Nascondi cartella \"Solo per i membri\"" msgctxt "#30598" -msgid "" -msgstr " " +msgid "Large (4:3)" +msgstr "Grande (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" msgstr "Impossibile abilitare chiavi API personali. Manca: %s" msgctxt "#30600" -msgid "" -msgstr " " +msgid "Largest (16:9)" +msgstr "Più grande (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" @@ -754,20 +753,20 @@ msgid "Retry" msgstr "Riprova" msgctxt "#30613" -msgid "" -msgstr " " +msgid "Add to %s" +msgstr "Aggiungi a %s" msgctxt "#30614" -msgid "" -msgstr " " +msgid "Remove from %s" +msgstr "Rimuovi da %s" msgctxt "#30615" -msgid "" -msgstr " " +msgid "Added to %s" +msgstr "Aggiunto a %s" msgctxt "#30616" -msgid "" -msgstr " " +msgid "Removed from %s" +msgstr "Rimosso da %s" msgctxt "#30617" msgid "InputStream.Adaptive" @@ -786,8 +785,8 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Porta %s già in uso. Impossibile avviare il server http." msgctxt "#30621" -msgid "" -msgstr " " +msgid "Verbose" +msgstr "Prolisso" msgctxt "#30622" msgid "Purchases" @@ -798,8 +797,8 @@ msgid "Install InputStream Helper" msgstr "Installa InputStream Helper" msgctxt "#30624" -msgid "" -msgstr " " +msgid "Members only" +msgstr "Solo per i membri" msgctxt "#30625" msgid "InputStream Helper is already installed." @@ -874,8 +873,8 @@ msgid "Delete access_manager.json" msgstr "Elimina access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Ascolta su IP" +msgid "" +msgstr " " msgctxt "#30644" msgid "Select listen IP" @@ -978,12 +977,12 @@ msgid "Play count minimum percent" msgstr "Percentuale minima conteggio riproduzioni" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Segna come non visto" +msgid "%s removed" +msgstr "%s rimosso" msgctxt "#30670" -msgid "Mark watched" -msgstr "Segna come visto" +msgid "Added %s" +msgstr "Aggiunto %s" msgctxt "#30671" msgid "Clear playback history" @@ -998,8 +997,8 @@ msgid "playback history" msgstr "cronologia riprodotti" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Reimposta punto di ripresa" +msgid "" +msgstr " " msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1138,12 +1137,12 @@ msgid "Play audio only" msgstr "Riproduci solo audio" msgctxt "#30709" -msgid "" -msgstr " " +msgid "WebVTT subtitles" +msgstr "Sottotitoli WebVTT" msgctxt "#30710" -msgid "" -msgstr " " +msgid "TTML subtitles" +msgstr "Sottotitoli TTML" msgctxt "#30711" msgid "" @@ -1154,16 +1153,16 @@ msgid "Rate videos in playlists" msgstr "Valuta i video nelle playlist" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Aggiunto a Guarda più tardi" +msgid "Prefer dubbed audio over original audio" +msgstr "Preferisci l'audio doppiato rispetto all'audio originale" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Aggiunto alla playlist" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "Preferisci l'audio doppiato tradotto automaticamente rispetto all'audio originale" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Rimosso dalla playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "Usare l'elenco interno di YouTube per la cronologia visualizzazioni?[CR][CR]Richiede l'accesso tramite l'add-on e l'attivazione del monitoraggio della cronologia su YouTube." msgctxt "#30716" msgid "Liked video" @@ -1174,8 +1173,8 @@ msgid "Disliked video" msgstr "Video non piaciuti" msgctxt "#30718" -msgid "Rating removed" -msgstr "Valutazione rimossa" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "Usare l'elenco interno di YouTube per Guarda più tardi?[CR][CR]Richiede l'accesso tramite l'add-on." msgctxt "#30719" msgid "Subscribed to channel" @@ -1186,8 +1185,8 @@ msgid "Unsubscribed from channel" msgstr "Iscrizione al canale cancellata" msgctxt "#30721" -msgid "" -msgstr " " +msgid "Enable Panoramic/180/360/VR video" +msgstr "Abilita video panoramico/180/360/VR" msgctxt "#30722" msgid "Enable HDR video" @@ -1254,8 +1253,8 @@ msgid "Shorts - Max duration" msgstr "Video brevi - Durata max" msgctxt "#30738" -msgid "" -msgstr " " +msgid "Enable spatial audio" +msgstr "Abilita audio spaziale" msgctxt "#30739" msgid "Subscribers" @@ -1534,8 +1533,8 @@ msgid "Use channel name as" msgstr "Usa nome del canale come" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "Nascondi video dagli elenchi" +msgid "Hide items from listings" +msgstr "Nascondi elementi dagli elenchi" msgctxt "#30809" msgid "All upcoming videos" @@ -1585,6 +1584,205 @@ msgctxt "#30820" msgid "Podcast" msgstr "Podcast" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Ascolta su IP" + +#~ msgctxt "#30113" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30118" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30547" +#~ msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +#~ msgstr "Potrebbe esserti richiesto di abilitare due applicazioni affinché YouTube funzioni correttamente." + +#~ msgctxt "#30587" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30588" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30589" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30590" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30597" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30624" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Nascondi video dagli elenchi" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Consenti 3D" + +#~ msgctxt "#30031" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30108" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30109" +#~ msgid "" +#~ msgstr " " + +# YouTube +# empty strings from id 30206 to 30499 +#~ msgctxt "#30500" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30501" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30521" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30522" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30523" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Riproduci con..." + +#~ msgctxt "#30563" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30564" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30577" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Aggiungi al filtro Le mie iscrizioni" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Rimuovi dal filtro Le mie iscrizioni" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Aggiungi al filtro Le mie iscrizioni" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Rimuovi dal filtro Le mie iscrizioni" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Media (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Grande (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Aggiornato: %s" + +#~ msgctxt "#30598" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30600" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30613" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30614" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30615" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30616" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30621" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Segna come non visto" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Segna come visto" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Reimposta punto di ripresa" + +#~ msgctxt "#30709" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30710" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Aggiunto a Guarda più tardi" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Aggiunto alla playlist" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Rimosso dalla playlist" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Valutazione rimossa" + +#~ msgctxt "#30721" +#~ msgid "" +#~ msgstr " " + +#~ msgctxt "#30738" +#~ msgid "" +#~ msgstr " " + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Nessun ulteriore collegamento trovato." diff --git a/plugin.video.youtube/resources/language/resource.language.ja_jp/strings.po b/plugin.video.youtube/resources/language/resource.language.ja_jp/strings.po index f9b486c7ce..8be8245a49 100644 --- a/plugin.video.youtube/resources/language/resource.language.ja_jp/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ja_jp/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.kn_in/strings.po b/plugin.video.youtube/resources/language/resource.language.kn_in/strings.po deleted file mode 100644 index 78a2122670..0000000000 --- a/plugin.video.youtube/resources/language/resource.language.kn_in/strings.po +++ /dev/null @@ -1,1585 +0,0 @@ -# Kodi Media Center language file -# Addon Name: YouTube -# Addon id: plugin.video.youtube -# Addon Provider: bromix -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2024-04-12 15:00+0000\n" -"Last-Translator: Anonymous \n" -"Language-Team: Kannada (India) \n" -"Language: kn_in\n" -"MIME-Version: 1.0\n" -"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.4.3\n" - -msgctxt "Addon Summary" -msgid "Plugin for YouTube" -msgstr "" - -msgctxt "Addon Description" -msgid "YouTube is one of the biggest video-sharing websites of the world." -msgstr "" - -msgctxt "Addon Disclaimer" -msgid "This plugin is not endorsed by Google" -msgstr "" - -# msgctxt "Addon Summary" -# msgid "Plugin for YouTube" -# msgstr "" -# msgctxt "Addon Description" -# msgid "YouTube is a one of the biggest video-sharing websites of the world." -# msgstr "" -# Kodion Settings -msgctxt "#30000" -msgid "" -msgstr "" - -msgctxt "#30001" -msgid "" -msgstr "" - -msgctxt "#30002" -msgid "" -msgstr "" - -msgctxt "#30003" -msgid "YouTube" -msgstr "" - -# empty strings from id 30004 to 30006 -msgctxt "#30007" -msgid "Use InputStream.Adaptive" -msgstr "" - -msgctxt "#30008" -msgid "Configure InputStream.Adaptive" -msgstr "" - -msgctxt "#30009" -msgid "Always ask for the video quality" -msgstr "" - -msgctxt "#30010" -msgid "Maximum video quality" -msgstr "" - -msgctxt "#30011" -msgid "480p" -msgstr "" - -msgctxt "#30012" -msgid "720p (HD)" -msgstr "" - -msgctxt "#30013" -msgid "1080p (FHD)" -msgstr "" - -msgctxt "#30014" -msgid "2160p (4K)" -msgstr "" - -msgctxt "#30015" -msgid "4320p (8K)" -msgstr "" - -msgctxt "#30016" -msgid "240p" -msgstr "" - -msgctxt "#30017" -msgid "360p" -msgstr "" - -msgctxt "#30018" -msgid "1080p Live / 720p (HD)" -msgstr "" - -msgctxt "#30019" -msgid "144p" -msgstr "" - -# empty strings 30019 -msgctxt "#30020" -msgid "Allow 3D" -msgstr "" - -msgctxt "#30021" -msgid "Show fanart" -msgstr "" - -msgctxt "#30022" -msgid "Items per page" -msgstr "" - -msgctxt "#30023" -msgid "Search history size" -msgstr "" - -msgctxt "#30024" -msgid "Cache size (MB)" -msgstr "" - -msgctxt "#30025" -msgid "Enable Setup Wizard" -msgstr "" - -msgctxt "#30026" -msgid "Override views" -msgstr "" - -msgctxt "#30027" -msgid "View: Default" -msgstr "" - -msgctxt "#30028" -msgid "View: Episodes" -msgstr "" - -msgctxt "#30029" -msgid "View: Movies" -msgstr "" - -msgctxt "#30030" -msgid "Configure %s?" -msgstr "" - -msgctxt "#30031" -msgid "" -msgstr "" - -msgctxt "#30032" -msgid "View: TV Shows" -msgstr "" - -msgctxt "#30033" -msgid "View: Songs" -msgstr "" - -msgctxt "#30034" -msgid "View: Artists" -msgstr "" - -msgctxt "#30035" -msgid "View: Albums" -msgstr "" - -msgctxt "#30036" -msgid "Support alternative player" -msgstr "" - -msgctxt "#30037" -msgid "Custom Watch Later playlist id" -msgstr "" - -msgctxt "#30038" -msgid "Custom History playlist id" -msgstr "" - -# Kodion Common -# empty strings from id 30039 to 30099 -msgctxt "#30100" -msgid "Bookmarks" -msgstr "" - -msgctxt "#30101" -msgid "Bookmark" -msgstr "" - -msgctxt "#30102" -msgid "Search" -msgstr "" - -msgctxt "#30103" -msgid "" -msgstr "" - -msgctxt "#30104" -msgid "" -msgstr "" - -msgctxt "#30105" -msgid "filtered" -msgstr "" - -msgctxt "#30106" -msgid "Next page: %d" -msgstr "" - -msgctxt "#30107" -msgid "Watch Later" -msgstr "" - -msgctxt "#30108" -msgid "" -msgstr "" - -msgctxt "#30109" -msgid "" -msgstr "" - -msgctxt "#30110" -msgid "New Search" -msgstr "" - -msgctxt "#30111" -msgid "Sign In" -msgstr "" - -msgctxt "#30112" -msgid "Sign Out" -msgstr "" - -msgctxt "#30113" -msgid "" -msgstr "" - -msgctxt "#30114" -msgid "Confirm delete" -msgstr "" - -msgctxt "#30115" -msgid "Confirm remove" -msgstr "" - -msgctxt "#30116" -msgid "Delete \"%s\"?" -msgstr "" - -msgctxt "#30117" -msgid "Remove \"%s\"?" -msgstr "" - -msgctxt "#30118" -msgid "" -msgstr "" - -msgctxt "#30119" -msgid "Please wait..." -msgstr "" - -msgctxt "#30120" -msgid "Confirm clear" -msgstr "" - -msgctxt "#30121" -msgid "Clear %s?" -msgstr "" - -# YouTube -# empty strings from id 30121 to 30199 -msgctxt "#30200" -msgid "API" -msgstr "" - -msgctxt "#30201" -msgid "API Key" -msgstr "" - -msgctxt "#30202" -msgid "API Id" -msgstr "" - -msgctxt "#30203" -msgid "API Secret" -msgstr "" - -msgctxt "#30204" -msgid "" -msgstr "" - -msgctxt "#30205" -msgid "" -msgstr "" - -# YouTube -# empty strings from id 30206 to 30499 -msgctxt "#30500" -msgid "" -msgstr "" - -msgctxt "#30501" -msgid "" -msgstr "" - -msgctxt "#30502" -msgid "Go to %s" -msgstr "" - -msgctxt "#30503" -msgid "Channel fanart" -msgstr "" - -msgctxt "#30504" -msgid "Subscriptions" -msgstr "" - -msgctxt "#30505" -msgid "Unsubscribe" -msgstr "" - -msgctxt "#30506" -msgid "Subscribe" -msgstr "" - -msgctxt "#30507" -msgid "My Channel" -msgstr "" - -msgctxt "#30508" -msgid "Liked Videos" -msgstr "" - -msgctxt "#30509" -msgid "History" -msgstr "" - -msgctxt "#30510" -msgid "My Subscriptions" -msgstr "" - -msgctxt "#30511" -msgid "Queue video" -msgstr "" - -msgctxt "#30512" -msgid "Browse Channels" -msgstr "" - -msgctxt "#30513" -msgid "Trending" -msgstr "" - -msgctxt "#30514" -msgid "Related Videos" -msgstr "" - -msgctxt "#30515" -msgid "Auto-Remove from Watch Later" -msgstr "" - -msgctxt "#30516" -msgid "Folders" -msgstr "" - -msgctxt "#30517" -msgid "Subscribe to %s" -msgstr "" - -msgctxt "#30518" -msgid "Feeds" -msgstr "" - -msgctxt "#30519" -msgid "and enter the following code:" -msgstr "" - -msgctxt "#30520" -msgid "Add to..." -msgstr "" - -msgctxt "#30521" -msgid "" -msgstr "" - -msgctxt "#30522" -msgid "" -msgstr "" - -msgctxt "#30523" -msgid "" -msgstr "" - -msgctxt "#30524" -msgid "Select language" -msgstr "" - -msgctxt "#30525" -msgid "Select region" -msgstr "" - -msgctxt "#30526" -msgid "Setup Wizard" -msgstr "" - -msgctxt "#30527" -msgid "Language and Region" -msgstr "" - -msgctxt "#30528" -msgid "Rate..." -msgstr "" - -msgctxt "#30529" -msgid "I like this" -msgstr "" - -msgctxt "#30530" -msgid "I dislike this" -msgstr "" - -msgctxt "#30531" -msgid "" -msgstr "" - -msgctxt "#30532" -msgid "" -msgstr "" - -msgctxt "#30533" -msgid "Reverse" -msgstr "" - -msgctxt "#30534" -msgid "" -msgstr "" - -msgctxt "#30535" -msgid "Select the order of the playlist" -msgstr "" - -msgctxt "#30536" -msgid "Updating Playlist..." -msgstr "" - -msgctxt "#30537" -msgid "Play from here" -msgstr "" - -msgctxt "#30538" -msgid "Disliked Videos" -msgstr "" - -msgctxt "#30539" -msgid "Play recently added" -msgstr "" - -msgctxt "#30540" -msgid "Play with..." -msgstr "" - -msgctxt "#30541" -msgid "Show channel name and video details in description" -msgstr "" - -msgctxt "#30542" -msgid "rtmpe streams are not supported" -msgstr "" - -msgctxt "#30543" -msgid "" -msgstr "" - -msgctxt "#30544" -msgid "More Links from the description" -msgstr "" - -msgctxt "#30545" -msgid "No videos found." -msgstr "" - -msgctxt "#30546" -msgid "Please complete all login prompts" -msgstr "" - -msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." -msgstr "" - -msgctxt "#30548" -msgid "" -msgstr "" - -msgctxt "#30549" -msgid "No streams found" -msgstr "" - -msgctxt "#30550" -msgid "" -msgstr "" - -msgctxt "#30551" -msgid "Recommendations" -msgstr "" - -msgctxt "#30552" -msgid "Maintenance" -msgstr "" - -msgctxt "#30553" -msgid "Delete function cache database" -msgstr "" - -msgctxt "#30554" -msgid "Delete search history database" -msgstr "" - -msgctxt "#30555" -msgid "Clear function cache" -msgstr "" - -msgctxt "#30556" -msgid "Clear search history" -msgstr "" - -msgctxt "#30557" -msgid "function cache" -msgstr "" - -msgctxt "#30558" -msgid "search history" -msgstr "" - -msgctxt "#30559" -msgid "Delete settings.xml" -msgstr "" - -msgctxt "#30560" -msgid "" -msgstr "" - -msgctxt "#30561" -msgid "" -msgstr "" - -msgctxt "#30562" -msgid "View all" -msgstr "" - -msgctxt "#30563" -msgid "" -msgstr "" - -msgctxt "#30564" -msgid "" -msgstr "" - -msgctxt "#30565" -msgid "" -msgstr "" - -msgctxt "#30566" -msgid "" -msgstr "" - -msgctxt "#30567" -msgid "Set as Watch Later" -msgstr "" - -msgctxt "#30568" -msgid "Remove as Watch Later" -msgstr "" - -msgctxt "#30569" -msgid "Are you sure you want to remove \"%s\" as your Watch Later list?" -msgstr "" - -msgctxt "#30570" -msgid "Are you sure you want to replace your current Watch Later list with \"%s\"?" -msgstr "" - -msgctxt "#30571" -msgid "Set as History" -msgstr "" - -msgctxt "#30572" -msgid "Remove as History" -msgstr "" - -msgctxt "#30573" -msgid "Are you sure you want to remove \"%s\" as your History list?" -msgstr "" - -msgctxt "#30574" -msgid "Are you sure you want to replace your current History list with \"%s\"?" -msgstr "" - -msgctxt "#30575" -msgid "Succeeded" -msgstr "" - -msgctxt "#30576" -msgid "Failed" -msgstr "" - -msgctxt "#30577" -msgid "" -msgstr "" - -msgctxt "#30578" -msgid "Force SSL certificate verification" -msgstr "" - -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 "" - -msgctxt "#30580" -msgid "Reset access manager" -msgstr "" - -msgctxt "#30581" -msgid "Are you sure you want to reset access manager?" -msgstr "" - -msgctxt "#30582" -msgid "Autoplay suggested videos" -msgstr "" - -msgctxt "#30583" -msgid "Filters can be channel names separated by a comma eg. 'The Best Channel,The 2nd Best Channel', and/or custom video filters in the form '{ATTR}{OP}{VALUE}' eg. '{duration}{>=}{180}{artists_string}{=}{\"The Best Channel\"},{duration}{<}{180}'" -msgstr "" - -msgctxt "#30584" -msgid "My Subscriptions (Filtered)" -msgstr "" - -msgctxt "#30585" -msgid "Enable Blacklist to exclude channel names in Filters from My Subscriptions, disable to include channel names" -msgstr "" - -msgctxt "#30586" -msgid "Blacklist" -msgstr "" - -msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "" - -msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "" - -msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "" - -msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "" - -msgctxt "#30591" -msgid "Thumbnail size" -msgstr "" - -msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "" - -msgctxt "#30593" -msgid "High (4:3)" -msgstr "" - -msgctxt "#30594" -msgid "Safe search" -msgstr "" - -msgctxt "#30595" -msgid "Moderate" -msgstr "" - -msgctxt "#30596" -msgid "Strict" -msgstr "" - -msgctxt "#30597" -msgid "Updated: %s" -msgstr "" - -msgctxt "#30598" -msgid "" -msgstr "" - -msgctxt "#30599" -msgid "Failed to enable personal API keys. Missing: %s" -msgstr "" - -msgctxt "#30600" -msgid "" -msgstr "" - -msgctxt "#30601" -msgid "%s with Original/%s fallback" -msgstr "" - -msgctxt "#30602" -msgid "No auto-generated" -msgstr "" - -msgctxt "#30603" -msgid "Age gate" -msgstr "" - -msgctxt "#30604" -msgid "Allow offensive content" -msgstr "" - -msgctxt "#30605" -msgid "Quick Search" -msgstr "" - -msgctxt "#30606" -msgid "Quick Search (Incognito)" -msgstr "" - -msgctxt "#30607" -msgid "Audio only" -msgstr "" - -msgctxt "#30608" -msgid "Allow developer keys" -msgstr "" - -msgctxt "#30609" -msgid "Clear watch history" -msgstr "" - -msgctxt "#30610" -msgid "This will clear your account's watch history from all devices. You can't undo this." -msgstr "" - -msgctxt "#30611" -msgid "Saved Playlists" -msgstr "" - -msgctxt "#30612" -msgid "Retry" -msgstr "" - -msgctxt "#30613" -msgid "" -msgstr "" - -msgctxt "#30614" -msgid "" -msgstr "" - -msgctxt "#30615" -msgid "" -msgstr "" - -msgctxt "#30616" -msgid "" -msgstr "" - -msgctxt "#30617" -msgid "InputStream.Adaptive" -msgstr "" - -msgctxt "#30618" -msgid "Stream redirect" -msgstr "" - -msgctxt "#30619" -msgid "Enable to reduce resource usage on less powerful devices, but could lead to IP bans. Use at own risk." -msgstr "" - -msgctxt "#30620" -msgid "Port %s already in use. Cannot start http server." -msgstr "" - -msgctxt "#30621" -msgid "" -msgstr "" - -msgctxt "#30622" -msgid "Purchases" -msgstr "" - -msgctxt "#30623" -msgid "Install InputStream Helper" -msgstr "" - -msgctxt "#30624" -msgid "" -msgstr "" - -msgctxt "#30625" -msgid "InputStream Helper is already installed." -msgstr "" - -msgctxt "#30626" -msgid "Delete temporary files" -msgstr "" - -msgctxt "#30627" -msgid "Rate video after watching" -msgstr "" - -msgctxt "#30628" -msgid "HTTP Server" -msgstr "" - -msgctxt "#30629" -msgid "IP whitelist (comma delimited)" -msgstr "" - -msgctxt "#30630" -msgid "" -msgstr "" - -msgctxt "#30631" -msgid "Successfully updated: %s" -msgstr "" - -msgctxt "#30632" -msgid "Enable API configuration page" -msgstr "" - -msgctxt "#30633" -msgid "http://:/youtube/api (see Advanced > HTTP Server)" -msgstr "" - -msgctxt "#30634" -msgid "YouTube Add-on API Configuration" -msgstr "" - -msgctxt "#30635" -msgid "No changes detected, API keys were not updated." -msgstr "" - -msgctxt "#30636" -msgid "Personal API keys are enabled." -msgstr "" - -msgctxt "#30637" -msgid "Personal API keys are disabled." -msgstr "" - -msgctxt "#30638" -msgid "Bookmark this page to quickly add your keys in the future." -msgstr "" - -msgctxt "#30639" -msgid "Found personal API keys in api_keys.json, would you like to restore them? Choosing no will overwrite them." -msgstr "" - -msgctxt "#30640" -msgid "Restore" -msgstr "" - -msgctxt "#30641" -msgid "Delete api_keys.json" -msgstr "" - -msgctxt "#30642" -msgid "Delete access_manager.json" -msgstr "" - -msgctxt "#30643" -msgid "Listen on IP" -msgstr "" - -msgctxt "#30644" -msgid "Select listen IP" -msgstr "" - -msgctxt "#30645" -msgid "Refresh after watching" -msgstr "" - -msgctxt "#30646" -msgid "Upcoming Live" -msgstr "" - -msgctxt "#30647" -msgid "Completed Live" -msgstr "" - -msgctxt "#30648" -msgid "API Key is incorrect. Settings > API > API Key" -msgstr "" - -msgctxt "#30649" -msgid "Client Id is incorrect. Settings > API > API Id" -msgstr "" - -msgctxt "#30650" -msgid "Client Secret is incorrect. Settings > API > API Secret" -msgstr "" - -msgctxt "#30651" -msgid "Location" -msgstr "" - -msgctxt "#30652" -msgid "Location radius (km)" -msgstr "" - -msgctxt "#30653" -msgid "My Location using IP geolocation lookup" -msgstr "" - -msgctxt "#30654" -msgid "My Location" -msgstr "" - -msgctxt "#30655" -msgid "Switch User" -msgstr "" - -msgctxt "#30656" -msgid "New user" -msgstr "" - -msgctxt "#30657" -msgid "Unnamed" -msgstr "" - -msgctxt "#30658" -msgid "Enter a name for this user" -msgstr "" - -msgctxt "#30659" -msgid "User is now \"%s\"" -msgstr "" - -msgctxt "#30660" -msgid "Users" -msgstr "" - -msgctxt "#30661" -msgid "Add a user" -msgstr "" - -msgctxt "#30662" -msgid "Remove a user" -msgstr "" - -msgctxt "#30663" -msgid "Rename a user" -msgstr "" - -msgctxt "#30664" -msgid "Switch user" -msgstr "" - -msgctxt "#30665" -msgid "Switch to \"%s\" now?" -msgstr "" - -msgctxt "#30666" -msgid "\"%s\" removed" -msgstr "" - -msgctxt "#30667" -msgid "Renamed \"%s\" to \"%s\"" -msgstr "" - -msgctxt "#30668" -msgid "Play count minimum percent" -msgstr "" - -msgctxt "#30669" -msgid "Mark unwatched" -msgstr "" - -msgctxt "#30670" -msgid "Mark watched" -msgstr "" - -msgctxt "#30671" -msgid "Clear playback history" -msgstr "" - -msgctxt "#30672" -msgid "Delete playback history database" -msgstr "" - -msgctxt "#30673" -msgid "playback history" -msgstr "" - -msgctxt "#30674" -msgid "Reset resume point" -msgstr "" - -msgctxt "#30675" -msgid "Use local playback history (watched, resume tracking)" -msgstr "" - -msgctxt "#30676" -msgid "Just now" -msgstr "" - -msgctxt "#30677" -msgid "A minute ago" -msgstr "" - -msgctxt "#30678" -msgid "Recently" -msgstr "" - -msgctxt "#30679" -msgid "An hour ago" -msgstr "" - -msgctxt "#30680" -msgid "Two hours ago" -msgstr "" - -msgctxt "#30681" -msgid "Three hours ago" -msgstr "" - -msgctxt "#30682" -msgid "Yesterday at" -msgstr "" - -msgctxt "#30683" -msgid "Two days ago" -msgstr "" - -msgctxt "#30684" -msgid "Today at" -msgstr "" - -msgctxt "#30685" -msgid "Delete data cache database" -msgstr "" - -msgctxt "#30686" -msgid "Clear data cache" -msgstr "" - -msgctxt "#30687" -msgid "data cache" -msgstr "" - -msgctxt "#30688" -msgid "Use MPEG-DASH for videos" -msgstr "" - -msgctxt "#30689" -msgid "Use for live streams" -msgstr "" - -msgctxt "#30690" -msgid "InputStream.Adaptive >= 2.0.12 is required for adaptive live streams" -msgstr "" - -msgctxt "#30691" -msgid "Airing now" -msgstr "" - -msgctxt "#30692" -msgid "In a minute" -msgstr "" - -msgctxt "#30693" -msgid "Airing soon" -msgstr "" - -msgctxt "#30694" -msgid "In over an hour" -msgstr "" - -msgctxt "#30695" -msgid "In over two hours" -msgstr "" - -msgctxt "#30696" -msgid "Airing today at" -msgstr "" - -msgctxt "#30697" -msgid "Tomorrow at" -msgstr "" - -msgctxt "#30698" -msgid "Check my IP" -msgstr "" - -msgctxt "#30699" -msgid "HTTP server is not running" -msgstr "" - -msgctxt "#30700" -msgid "Client IP is %s" -msgstr "" - -msgctxt "#30701" -msgid "Failed to obtain client IP" -msgstr "" - -msgctxt "#30702" -msgid "Play with subtitles" -msgstr "" - -msgctxt "#30703" -msgid "" -msgstr "" - -msgctxt "#30704" -msgid "Use YouTube website urls with default player" -msgstr "" - -msgctxt "#30705" -msgid "Download subtitles" -msgstr "" - -msgctxt "#30706" -msgid "Download subtitles before starting playback? (Default: No)" -msgstr "" - -msgctxt "#30707" -msgid "Untitled" -msgstr "" - -msgctxt "#30708" -msgid "Play audio only" -msgstr "" - -msgctxt "#30709" -msgid "" -msgstr "" - -msgctxt "#30710" -msgid "" -msgstr "" - -msgctxt "#30711" -msgid "" -msgstr "" - -msgctxt "#30712" -msgid "Rate videos in playlists" -msgstr "" - -msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "" - -msgctxt "#30714" -msgid "Added to playlist" -msgstr "" - -msgctxt "#30715" -msgid "Removed from playlist" -msgstr "" - -msgctxt "#30716" -msgid "Liked video" -msgstr "" - -msgctxt "#30717" -msgid "Disliked video" -msgstr "" - -msgctxt "#30718" -msgid "Rating removed" -msgstr "" - -msgctxt "#30719" -msgid "Subscribed to channel" -msgstr "" - -msgctxt "#30720" -msgid "Unsubscribed from channel" -msgstr "" - -msgctxt "#30721" -msgid "" -msgstr "" - -msgctxt "#30722" -msgid "Enable HDR video" -msgstr "" - -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 "" - -msgctxt "#30724" -msgid "Enable high framerate video" -msgstr "" - -msgctxt "#30725" -msgid "1440p (QHD)" -msgstr "" - -msgctxt "#30726" -msgid "Uploads" -msgstr "" - -msgctxt "#30727" -msgid "Enable H.264 video" -msgstr "" - -msgctxt "#30728" -msgid "Enable VP9 video" -msgstr "" - -msgctxt "#30729" -msgid "Prefer lower resolution streams for unselected codecs" -msgstr "" - -msgctxt "#30730" -msgid "Play (Ask for quality)" -msgstr "" - -msgctxt "#30731" -msgid "The YouTube add-on now requires that you use your own API keys.[CR]For more information see the wiki: [B]https://ytaddon.page.link/keys[/B][CR][CR]Sorry for the inconvenience." -msgstr "" - -msgctxt "#30732" -msgid "Comments" -msgstr "" - -msgctxt "#30733" -msgid "Likes" -msgstr "" - -msgctxt "#30734" -msgid "Replies" -msgstr "" - -msgctxt "#30735" -msgid "Edited" -msgstr "" - -msgctxt "#30736" -msgid "Shorts" -msgstr "" - -msgctxt "#30737" -msgid "Shorts - Max duration" -msgstr "" - -msgctxt "#30738" -msgid "" -msgstr "" - -msgctxt "#30739" -msgid "Subscribers" -msgstr "" - -msgctxt "#30740" -msgid "HLS" -msgstr "" - -msgctxt "#30741" -msgid "Multi-stream HLS" -msgstr "" - -msgctxt "#30742" -msgid "Adaptive HLS" -msgstr "" - -msgctxt "#30743" -msgid "MPEG-DASH" -msgstr "" - -msgctxt "#30744" -msgid "Original" -msgstr "" - -msgctxt "#30745" -msgid "Dubbed" -msgstr "" - -msgctxt "#30746" -msgid "Descriptive" -msgstr "" - -msgctxt "#30747" -msgid "Alternate" -msgstr "" - -msgctxt "#30748" -msgid "Stream features" -msgstr "" - -msgctxt "#30749" -msgid "Enable AV1 video" -msgstr "" - -msgctxt "#30750" -msgid "Enable Vorbis audio" -msgstr "" - -msgctxt "#30751" -msgid "Enable Opus audio" -msgstr "" - -msgctxt "#30752" -msgid "Enable AAC audio" -msgstr "" - -msgctxt "#30753" -msgid "Enable surround sound audio" -msgstr "" - -msgctxt "#30754" -msgid "Enable AC-3 audio" -msgstr "" - -msgctxt "#30755" -msgid "Enable EAC-3 audio" -msgstr "" - -msgctxt "#30756" -msgid "Enable DTS audio" -msgstr "" - -msgctxt "#30757" -msgid "Remove similar/duplicate streams" -msgstr "" - -msgctxt "#30758" -msgid "Stream selection" -msgstr "" - -msgctxt "#30759" -msgid "Quality selection" -msgstr "" - -msgctxt "#30760" -msgid "Automatic + Quality selection" -msgstr "" - -msgctxt "#30761" -msgid "Update playback history on Youtube" -msgstr "" - -msgctxt "#30762" -msgid "Multi-language" -msgstr "" - -msgctxt "#30763" -msgid "Multi-audio" -msgstr "" - -msgctxt "#30764" -msgid "Requests connect timeout" -msgstr "" - -msgctxt "#30765" -msgid "Requests read timeout" -msgstr "" - -msgctxt "#30766" -msgid "Premieres" -msgstr "" - -msgctxt "#30767" -msgid "Views" -msgstr "" - -msgctxt "#30768" -msgid "Disable high framerate video at maximum video quality" -msgstr "" - -msgctxt "#30769" -msgid "Clear Watch Later list" -msgstr "" - -msgctxt "#30770" -msgid "Are you sure you want to clear your Watch Later list?" -msgstr "" - -msgctxt "#30771" -msgid "Disable fractional framerate hinting" -msgstr "" - -msgctxt "#30772" -msgid "Disable all framerate hinting" -msgstr "" - -msgctxt "#30773" -msgid "Show video details in video lists" -msgstr "" - -msgctxt "#30774" -msgid "All available" -msgstr "" - -msgctxt "#30775" -msgid "%s (translation)" -msgstr "" - -msgctxt "#30776" -msgid "Ask + Automatic + Quality selection" -msgstr "" - -msgctxt "#30777" -msgid "Views for %s (%s)" -msgstr "" - -msgctxt "#30778" -msgid "Import old playback history?" -msgstr "" - -msgctxt "#30779" -msgid "Import old search history?" -msgstr "" - -msgctxt "#30780" -msgid "Clear local watch later list" -msgstr "" - -msgctxt "#30781" -msgid "Delete watch later database" -msgstr "" - -msgctxt "#30782" -msgid "local watch later list" -msgstr "" - -msgctxt "#30783" -msgid "settings to recommended values" -msgstr "" - -msgctxt "#30784" -msgid "listings to show minimal details" -msgstr "" - -msgctxt "#30785" -msgid "performance settings" -msgstr "" - -msgctxt "#30786" -msgid "Choose device capabilities" -msgstr "" - -msgctxt "#30787" -msgid "720p, H.264 only | Limited or older devices" -msgstr "" - -msgctxt "#30788" -msgid "1080p/30 fps | Raspberry Pi 3, or similar" -msgstr "" - -msgctxt "#30789" -msgid "4K/30 fps or 1080p/60 fps, HDR if compatible | Raspberry Pi 5, or similar" -msgstr "" - -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 "" - -msgctxt "#30791" -msgid "4K/60 fps, HDR, using AV1 | Fire TV Cube Gen 3, Fire TV Stick 4K Max, Vero V, or similar" -msgstr "" - -msgctxt "#30792" -msgid "8K/60 fps, HDR, using AV1 | Modern device or PC with full capabilities" -msgstr "" - -msgctxt "#30793" -msgid "Views count display colour" -msgstr "" - -msgctxt "#30794" -msgid "Subscriber/Likes count display colour" -msgstr "" - -msgctxt "#30795" -msgid "Videos/Comments count display colour" -msgstr "" - -msgctxt "#30796" -msgid "1080p/60 fps | Raspberry Pi 4, or similar" -msgstr "" - -msgctxt "#30797" -msgid "1080p/30 fps or 720p/30 fps, H.264 only | Raspberry Pi 1/2, or similar" -msgstr "" - -msgctxt "#30798" -msgid "Clear bookmarks list" -msgstr "" - -msgctxt "#30799" -msgid "Delete bookmarks database" -msgstr "" - -msgctxt "#30800" -msgid "bookmarks list" -msgstr "" - -msgctxt "#30801" -msgid "Clear Bookmarks list" -msgstr "" - -msgctxt "#30802" -msgid "Are you sure you want to clear your Bookmarks list?" -msgstr "" - -msgctxt "#30803" -msgid "Bookmark %s" -msgstr "" - -msgctxt "#30804" -msgid "Use YouTube website urls with external player" -msgstr "" - -msgctxt "#30805" -msgid "Use MPEG-DASH with external player" -msgstr "" - -msgctxt "#30806" -msgid "Jump to page..." -msgstr "" - -msgctxt "#30807" -msgid "Use channel name as" -msgstr "" - -msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "" - -msgctxt "#30809" -msgid "All upcoming videos" -msgstr "" - -msgctxt "#30810" -msgid "All previously streamed (completed) videos" -msgstr "" - -msgctxt "#30811" -msgid "Filter Live folders" -msgstr "" - -msgctxt "#30812" -msgid "Clear subscription feed history" -msgstr "" - -msgctxt "#30813" -msgid "Delete subscription feed history database" -msgstr "" - -msgctxt "#30814" -msgid "feed history" -msgstr "" - -msgctxt "#30815" -msgid "Go back..." -msgstr "" - -msgctxt "#30816" -msgid "List is empty.[CR][CR]Refresh from context menu or try again later." -msgstr "" - -msgctxt "#30817" -msgid "Refresh settings.xml" -msgstr "" - -msgctxt "#30818" -msgid "Are you sure you want to refresh settings.xml?" -msgstr "" - -msgctxt "#30819" -msgid "Play from start" -msgstr "" - -msgctxt "#30820" -msgid "Podcast" -msgstr "" diff --git a/plugin.video.youtube/resources/language/resource.language.ko_kr/strings.po b/plugin.video.youtube/resources/language/resource.language.ko_kr/strings.po index 06d45d1e11..404563265b 100644 --- a/plugin.video.youtube/resources/language/resource.language.ko_kr/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ko_kr/strings.po @@ -5,9 +5,9 @@ msgid "" msgstr "" "Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2025-06-02 12:13+0000\n" +"PO-Revision-Date: 2025-08-22 19:29+0000\n" "Last-Translator: Minho Park \n" "Language-Team: Korean \n" "Language: ko_kr\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.11.4\n" +"X-Generator: Weblate 5.13\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -106,8 +106,8 @@ msgid "144p" msgstr "144p" msgctxt "#30020" -msgid "Allow 3D" -msgstr "3D 허용" +msgid "Enable Stereoscopic 3D video" +msgstr "입체 3D 비디오 사용" msgctxt "#30021" msgid "Show fanart" @@ -150,8 +150,8 @@ msgid "Configure %s?" msgstr "%s을(를) 설정할까요?" msgctxt "#30031" -msgid "" -msgstr "" +msgid "Requests cache size (MB)" +msgstr "요청 캐시 크기(MB)" msgctxt "#30032" msgid "View: TV Shows" @@ -216,12 +216,12 @@ msgid "Watch Later" msgstr "나중에 보기" msgctxt "#30108" -msgid "" -msgstr "" +msgid "Enter the name of the bookmark" +msgstr "북마크 이름을 입력합니다" msgctxt "#30109" -msgid "" -msgstr "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "북마크에 유효한 유튜브나 플러그인 URL을 입력합니다" msgctxt "#30110" msgid "New Search" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "로그아웃" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "\"%s\" 제거할까요?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,12 +300,12 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" -msgstr "" +msgid "%s failed" +msgstr "%s 실패함" msgctxt "#30501" -msgid "" -msgstr "" +msgid "Edit %s" +msgstr "%s 편집" msgctxt "#30502" msgid "Go to %s" @@ -384,16 +384,16 @@ msgid "Add to..." msgstr "추가..." msgctxt "#30521" -msgid "" -msgstr "" +msgid "Delete requests cache database" +msgstr "요청 캐시 데이터베이스 삭제" msgctxt "#30522" -msgid "" -msgstr "" +msgid "Clear requests cache" +msgstr "요청 캐시 지우기" msgctxt "#30523" -msgid "" -msgstr "" +msgid "requests cache" +msgstr "요청 캐시" msgctxt "#30524" msgid "Select language" @@ -460,8 +460,8 @@ msgid "Play recently added" msgstr "최근 추가된 재생" msgctxt "#30540" -msgid "Play with..." -msgstr "다음으로 재생..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -488,8 +488,8 @@ msgid "Please complete all login prompts" msgstr "모든 로그인을 완료해 주세요" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." -msgstr "유튜브가 제대로 동작하려면 두 개 애플리케이션을 사용하라는 메시지가 표시될 수 있습니다." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +msgstr "" msgctxt "#30548" msgid "" @@ -552,12 +552,12 @@ msgid "View all" msgstr "모두 보기" msgctxt "#30563" -msgid "" -msgstr "" +msgid "Plugin execution timeout" +msgstr "플러그인 실행 시간 초과" msgctxt "#30564" -msgid "" -msgstr "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "테스트용으로만 사용하세요. 설정된 시간 제한 후 플러그인 실행을 강제로 종료합니다. 사용하지 않으려면 0초(기본값)로 설정하세요." msgctxt "#30565" msgid "" @@ -608,8 +608,8 @@ msgid "Failed" msgstr "실패" msgctxt "#30577" -msgid "" -msgstr "" +msgid "Smallest (4:3)" +msgstr "최소 (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" @@ -648,32 +648,32 @@ msgid "Blacklist" msgstr "블랙리스트" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "내 구독 필터에 추가" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "내 구독 필터에서 삭제" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "내 구독 필터에 추가함" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "내 구독 필터에서 삭제함" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "섬네일 크기" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "중간 (16:9)" +msgid "Small (16:9)" +msgstr "작게 (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "큼 (4:3)" +msgid "Medium (4:3)" +msgstr "중간 (4:3)" msgctxt "#30594" msgid "Safe search" @@ -688,20 +688,20 @@ msgid "Strict" msgstr "엄격" msgctxt "#30597" -msgid "Updated: %s" -msgstr "업데이트됨: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" -msgstr "" +msgid "Large (4:3)" +msgstr "크게 (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" msgstr "개인 API 키 사용 실패함. 없음: %s" msgctxt "#30600" -msgid "" -msgstr "" +msgid "Largest (16:9)" +msgstr "최대 (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" @@ -752,20 +752,20 @@ msgid "Retry" msgstr "다시 시도" msgctxt "#30613" -msgid "" -msgstr "" +msgid "Add to %s" +msgstr "%s에 추가" msgctxt "#30614" -msgid "" -msgstr "" +msgid "Remove from %s" +msgstr "%s에서 제거됨" msgctxt "#30615" -msgid "" -msgstr "" +msgid "Added to %s" +msgstr "%s에 추가됨" msgctxt "#30616" -msgid "" -msgstr "" +msgid "Removed from %s" +msgstr "%s에서 제거됨" msgctxt "#30617" msgid "InputStream.Adaptive" @@ -784,8 +784,8 @@ msgid "Port %s already in use. Cannot start http server." msgstr "$s 포트는 이미 사용 중입니다. http 서버를 시작할 수 없습니다." msgctxt "#30621" -msgid "" -msgstr "" +msgid "Verbose" +msgstr "상세" msgctxt "#30622" msgid "Purchases" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "InputStream Helper 설치" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "access_manager.json 지움" msgctxt "#30643" -msgid "Listen on IP" -msgstr "참조 IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -976,12 +976,12 @@ msgid "Play count minimum percent" msgstr "최소 재생 비율 계산" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "재생 안 함 표시" +msgid "%s removed" +msgstr "%s 지워짐" msgctxt "#30670" -msgid "Mark watched" -msgstr "재생 함 표시" +msgid "Added %s" +msgstr "%s 추가됨" msgctxt "#30671" msgid "Clear playback history" @@ -996,8 +996,8 @@ msgid "playback history" msgstr "재생 이력" msgctxt "#30674" -msgid "Reset resume point" -msgstr "이어 볼 지점 다시 설정" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1136,12 +1136,12 @@ msgid "Play audio only" msgstr "소리만 재생" msgctxt "#30709" -msgid "" -msgstr "" +msgid "WebVTT subtitles" +msgstr "WebVTT 자막" msgctxt "#30710" -msgid "" -msgstr "" +msgid "TTML subtitles" +msgstr "TTML 자막" msgctxt "#30711" msgid "" @@ -1152,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "재생목록의 동영상 평가" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "나중에 볼 동영상에 추가" +msgid "Prefer dubbed audio over original audio" +msgstr "원본 오디오보다 더빙을 선호합니다" msgctxt "#30714" -msgid "Added to playlist" -msgstr "재생목록에 추가" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "원본 오디오보다 자동 번역된 더빙을 선호합니다" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "재생목록에서 삭제" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "시청 기록에 유튜브내부 목록을 사용할까요?[CR][CR]애드온을 통해 로그인하고 유튜브에서 기록 추적을 사용해야 합니다." msgctxt "#30716" msgid "Liked video" @@ -1172,8 +1172,8 @@ msgid "Disliked video" msgstr "싫어하는 동영상" msgctxt "#30718" -msgid "Rating removed" -msgstr "평가 삭제" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "나중에 시청할 유튜브에 내부 목록을 사용할까요?[CR][CR]애드온을 통해 로그인해야 합니다." msgctxt "#30719" msgid "Subscribed to channel" @@ -1184,8 +1184,8 @@ msgid "Unsubscribed from channel" msgstr "채널 구독 취소" msgctxt "#30721" -msgid "" -msgstr "" +msgid "Enable Panoramic/180/360/VR video" +msgstr "파노라마/180/360/VR 비디오 사용" msgctxt "#30722" msgid "Enable HDR video" @@ -1252,8 +1252,8 @@ msgid "Shorts - Max duration" msgstr "쇼츠 - 최대 길이" msgctxt "#30738" -msgid "" -msgstr "" +msgid "Enable spatial audio" +msgstr "공간 오디오 사용" msgctxt "#30739" msgid "Subscribers" @@ -1532,8 +1532,8 @@ msgid "Use channel name as" msgstr "다음 채널 이름을 사용합니다" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "목록에서 동영상 숨기기" +msgid "Hide items from listings" +msgstr "" msgctxt "#30809" msgid "All upcoming videos" @@ -1583,6 +1583,82 @@ msgctxt "#30820" msgid "Podcast" msgstr "팟캐스트" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "참조 IP" + +#~ msgctxt "#30547" +#~ msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +#~ msgstr "유튜브가 제대로 동작하려면 두 개 애플리케이션을 사용하라는 메시지가 표시될 수 있습니다." + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "목록에서 동영상 숨기기" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "3D 허용" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "다음으로 재생..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "내 구독 필터에 추가" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "내 구독 필터에서 삭제" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "내 구독 필터에 추가함" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "내 구독 필터에서 삭제함" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "중간 (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "큼 (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "업데이트됨: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "재생 안 함 표시" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "재생 함 표시" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "이어 볼 지점 다시 설정" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "나중에 볼 동영상에 추가" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "재생목록에 추가" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "재생목록에서 삭제" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "평가 삭제" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "더 이상 링크가 없음." diff --git a/plugin.video.youtube/resources/language/resource.language.lt_lt/strings.po b/plugin.video.youtube/resources/language/resource.language.lt_lt/strings.po index 20d3b0de30..0f5f5230c7 100644 --- a/plugin.video.youtube/resources/language/resource.language.lt_lt/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.lt_lt/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.lv_lv/strings.po b/plugin.video.youtube/resources/language/resource.language.lv_lv/strings.po index 42ba09f15c..2ec76c7ad6 100644 --- a/plugin.video.youtube/resources/language/resource.language.lv_lv/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.lv_lv/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.mi/strings.po b/plugin.video.youtube/resources/language/resource.language.mi/strings.po index ca558a3e9e..45b84c3ca2 100644 --- a/plugin.video.youtube/resources/language/resource.language.mi/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.mi/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.mk_mk/strings.po b/plugin.video.youtube/resources/language/resource.language.mk_mk/strings.po index c8908a1d4e..b7d26e9b90 100644 --- a/plugin.video.youtube/resources/language/resource.language.mk_mk/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.mk_mk/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.ml_in/strings.po b/plugin.video.youtube/resources/language/resource.language.ml_in/strings.po index 99b08a14e2..d1a47b06c6 100644 --- a/plugin.video.youtube/resources/language/resource.language.ml_in/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ml_in/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.mn_mn/strings.po b/plugin.video.youtube/resources/language/resource.language.mn_mn/strings.po index 54bca1af88..f278d7c754 100644 --- a/plugin.video.youtube/resources/language/resource.language.mn_mn/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.mn_mn/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.ms_my/strings.po b/plugin.video.youtube/resources/language/resource.language.ms_my/strings.po index 65dee70f51..ff4990e01e 100644 --- a/plugin.video.youtube/resources/language/resource.language.ms_my/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ms_my/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.mt_mt/strings.po b/plugin.video.youtube/resources/language/resource.language.mt_mt/strings.po index 633e3ca847..fc80af8ab8 100644 --- a/plugin.video.youtube/resources/language/resource.language.mt_mt/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.mt_mt/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.my_mm/strings.po b/plugin.video.youtube/resources/language/resource.language.my_mm/strings.po index 9a08888670..3f1fd12ea9 100644 --- a/plugin.video.youtube/resources/language/resource.language.my_mm/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.my_mm/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.nb_no/strings.po b/plugin.video.youtube/resources/language/resource.language.nb_no/strings.po index 040408fb6b..48f70abcd4 100644 --- a/plugin.video.youtube/resources/language/resource.language.nb_no/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.nb_no/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Tillat 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "Se senere" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Logg ut" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Fjern \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "Legg til..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Spill med..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,15 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Tillat 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Spill med..." + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Ingen flere lenker funnet." diff --git a/plugin.video.youtube/resources/language/resource.language.nl_nl/strings.po b/plugin.video.youtube/resources/language/resource.language.nl_nl/strings.po index 01829dd4bb..4023951748 100644 --- a/plugin.video.youtube/resources/language/resource.language.nl_nl/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.nl_nl/strings.po @@ -106,10 +106,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Sta 3D Toe" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -152,7 +151,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -218,11 +217,11 @@ msgid "Watch Later" msgstr "Bekijk Later" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -238,7 +237,7 @@ msgid "Sign Out" msgstr "Uitloggen" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -258,7 +257,7 @@ msgid "Remove \"%s\"?" msgstr "Verwijder \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -302,11 +301,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -386,15 +385,15 @@ msgid "Add to..." msgstr "Voeg toe aan..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -462,8 +461,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Afspelen met..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -490,7 +489,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -554,11 +553,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -610,7 +609,7 @@ msgid "Failed" msgstr "Mislukt" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -650,32 +649,32 @@ msgid "Blacklist" msgstr "Zwarte lijst" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Toevoegen aan Mijn Abonnementen filter" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Verwijder van Mijn Abonnementen filter" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Toegevoegd aan Mijn Abonnementen filter" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Verwijderd van Mijn Abonnementen filter" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Miniatuur formaat" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Medium (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Hoog (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -690,11 +689,11 @@ msgid "Strict" msgstr "Streng" msgctxt "#30597" -msgid "Updated: %s" -msgstr "%s bijgewerkt" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -702,7 +701,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Inschakelen persoonlijke API sleutels mislukt. %s ontbreekt" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -754,19 +753,19 @@ msgid "Retry" msgstr "Probeer nogmaals" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -786,7 +785,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Poort %s al in gebruik. Kan de http server niet starten." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -798,7 +797,7 @@ msgid "Install InputStream Helper" msgstr "Installeer InputStream Helper" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -874,8 +873,8 @@ msgid "Delete access_manager.json" msgstr "Verwijder access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Luister op IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -978,12 +977,12 @@ msgid "Play count minimum percent" msgstr "Afspeel minimum percentage" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Markeer als onbekeken" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Markeer als bekeken" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -998,8 +997,8 @@ msgid "playback history" msgstr "Afspeelgeschiedenis" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Afspeelpunt resetten" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1138,11 +1137,11 @@ msgid "Play audio only" msgstr "Alleen geluid afspelen" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1154,16 +1153,16 @@ msgid "Rate videos in playlists" msgstr "Beoordeel video's in afspeellijsten" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Toegevoegd aan Bekijk Later" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Toegevoegd aan afspeellijst" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Verwijderd van afspeellijst" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1174,8 +1173,8 @@ msgid "Disliked video" msgstr "Niet Leuk" msgctxt "#30718" -msgid "Rating removed" -msgstr "Beoordeling verwijderd" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1186,7 +1185,7 @@ msgid "Unsubscribed from channel" msgstr "Niet meer geabonneerd" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1254,7 +1253,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1534,7 +1533,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1585,6 +1584,75 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Luister op IP" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Sta 3D Toe" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Afspelen met..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Toevoegen aan Mijn Abonnementen filter" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Verwijder van Mijn Abonnementen filter" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Toegevoegd aan Mijn Abonnementen filter" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Verwijderd van Mijn Abonnementen filter" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Medium (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Hoog (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "%s bijgewerkt" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Markeer als onbekeken" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Markeer als bekeken" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Afspeelpunt resetten" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Toegevoegd aan Bekijk Later" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Toegevoegd aan afspeellijst" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Verwijderd van afspeellijst" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Beoordeling verwijderd" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Geen verdere links gevonden" diff --git a/plugin.video.youtube/resources/language/resource.language.os_os/strings.po b/plugin.video.youtube/resources/language/resource.language.os_os/strings.po deleted file mode 100644 index 3c67c87632..0000000000 --- a/plugin.video.youtube/resources/language/resource.language.os_os/strings.po +++ /dev/null @@ -1,1585 +0,0 @@ -# Kodi Media Center language file -# Addon Name: YouTube -# Addon id: plugin.video.youtube -# Addon Provider: bromix -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2024-04-12 15:00+0000\n" -"Last-Translator: Anonymous \n" -"Language-Team: Ossetian \n" -"Language: os_os\n" -"MIME-Version: 1.0\n" -"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.4.3\n" - -msgctxt "Addon Summary" -msgid "Plugin for YouTube" -msgstr "" - -msgctxt "Addon Description" -msgid "YouTube is one of the biggest video-sharing websites of the world." -msgstr "" - -msgctxt "Addon Disclaimer" -msgid "This plugin is not endorsed by Google" -msgstr "" - -# msgctxt "Addon Summary" -# msgid "Plugin for YouTube" -# msgstr "" -# msgctxt "Addon Description" -# msgid "YouTube is a one of the biggest video-sharing websites of the world." -# msgstr "" -# Kodion Settings -msgctxt "#30000" -msgid "" -msgstr "" - -msgctxt "#30001" -msgid "" -msgstr "" - -msgctxt "#30002" -msgid "" -msgstr "" - -msgctxt "#30003" -msgid "YouTube" -msgstr "" - -# empty strings from id 30004 to 30006 -msgctxt "#30007" -msgid "Use InputStream.Adaptive" -msgstr "" - -msgctxt "#30008" -msgid "Configure InputStream.Adaptive" -msgstr "" - -msgctxt "#30009" -msgid "Always ask for the video quality" -msgstr "" - -msgctxt "#30010" -msgid "Maximum video quality" -msgstr "" - -msgctxt "#30011" -msgid "480p" -msgstr "" - -msgctxt "#30012" -msgid "720p (HD)" -msgstr "" - -msgctxt "#30013" -msgid "1080p (FHD)" -msgstr "" - -msgctxt "#30014" -msgid "2160p (4K)" -msgstr "" - -msgctxt "#30015" -msgid "4320p (8K)" -msgstr "" - -msgctxt "#30016" -msgid "240p" -msgstr "" - -msgctxt "#30017" -msgid "360p" -msgstr "" - -msgctxt "#30018" -msgid "1080p Live / 720p (HD)" -msgstr "" - -msgctxt "#30019" -msgid "144p" -msgstr "" - -# empty strings 30019 -msgctxt "#30020" -msgid "Allow 3D" -msgstr "" - -msgctxt "#30021" -msgid "Show fanart" -msgstr "" - -msgctxt "#30022" -msgid "Items per page" -msgstr "" - -msgctxt "#30023" -msgid "Search history size" -msgstr "" - -msgctxt "#30024" -msgid "Cache size (MB)" -msgstr "" - -msgctxt "#30025" -msgid "Enable Setup Wizard" -msgstr "" - -msgctxt "#30026" -msgid "Override views" -msgstr "" - -msgctxt "#30027" -msgid "View: Default" -msgstr "" - -msgctxt "#30028" -msgid "View: Episodes" -msgstr "" - -msgctxt "#30029" -msgid "View: Movies" -msgstr "" - -msgctxt "#30030" -msgid "Configure %s?" -msgstr "" - -msgctxt "#30031" -msgid "" -msgstr "" - -msgctxt "#30032" -msgid "View: TV Shows" -msgstr "" - -msgctxt "#30033" -msgid "View: Songs" -msgstr "" - -msgctxt "#30034" -msgid "View: Artists" -msgstr "" - -msgctxt "#30035" -msgid "View: Albums" -msgstr "" - -msgctxt "#30036" -msgid "Support alternative player" -msgstr "" - -msgctxt "#30037" -msgid "Custom Watch Later playlist id" -msgstr "" - -msgctxt "#30038" -msgid "Custom History playlist id" -msgstr "" - -# Kodion Common -# empty strings from id 30039 to 30099 -msgctxt "#30100" -msgid "Bookmarks" -msgstr "" - -msgctxt "#30101" -msgid "Bookmark" -msgstr "" - -msgctxt "#30102" -msgid "Search" -msgstr "" - -msgctxt "#30103" -msgid "" -msgstr "" - -msgctxt "#30104" -msgid "" -msgstr "" - -msgctxt "#30105" -msgid "filtered" -msgstr "" - -msgctxt "#30106" -msgid "Next page: %d" -msgstr "" - -msgctxt "#30107" -msgid "Watch Later" -msgstr "" - -msgctxt "#30108" -msgid "" -msgstr "" - -msgctxt "#30109" -msgid "" -msgstr "" - -msgctxt "#30110" -msgid "New Search" -msgstr "" - -msgctxt "#30111" -msgid "Sign In" -msgstr "" - -msgctxt "#30112" -msgid "Sign Out" -msgstr "" - -msgctxt "#30113" -msgid "" -msgstr "" - -msgctxt "#30114" -msgid "Confirm delete" -msgstr "" - -msgctxt "#30115" -msgid "Confirm remove" -msgstr "" - -msgctxt "#30116" -msgid "Delete \"%s\"?" -msgstr "" - -msgctxt "#30117" -msgid "Remove \"%s\"?" -msgstr "" - -msgctxt "#30118" -msgid "" -msgstr "" - -msgctxt "#30119" -msgid "Please wait..." -msgstr "" - -msgctxt "#30120" -msgid "Confirm clear" -msgstr "" - -msgctxt "#30121" -msgid "Clear %s?" -msgstr "" - -# YouTube -# empty strings from id 30121 to 30199 -msgctxt "#30200" -msgid "API" -msgstr "" - -msgctxt "#30201" -msgid "API Key" -msgstr "" - -msgctxt "#30202" -msgid "API Id" -msgstr "" - -msgctxt "#30203" -msgid "API Secret" -msgstr "" - -msgctxt "#30204" -msgid "" -msgstr "" - -msgctxt "#30205" -msgid "" -msgstr "" - -# YouTube -# empty strings from id 30206 to 30499 -msgctxt "#30500" -msgid "" -msgstr "" - -msgctxt "#30501" -msgid "" -msgstr "" - -msgctxt "#30502" -msgid "Go to %s" -msgstr "" - -msgctxt "#30503" -msgid "Channel fanart" -msgstr "" - -msgctxt "#30504" -msgid "Subscriptions" -msgstr "" - -msgctxt "#30505" -msgid "Unsubscribe" -msgstr "" - -msgctxt "#30506" -msgid "Subscribe" -msgstr "" - -msgctxt "#30507" -msgid "My Channel" -msgstr "" - -msgctxt "#30508" -msgid "Liked Videos" -msgstr "" - -msgctxt "#30509" -msgid "History" -msgstr "" - -msgctxt "#30510" -msgid "My Subscriptions" -msgstr "" - -msgctxt "#30511" -msgid "Queue video" -msgstr "" - -msgctxt "#30512" -msgid "Browse Channels" -msgstr "" - -msgctxt "#30513" -msgid "Trending" -msgstr "" - -msgctxt "#30514" -msgid "Related Videos" -msgstr "" - -msgctxt "#30515" -msgid "Auto-Remove from Watch Later" -msgstr "" - -msgctxt "#30516" -msgid "Folders" -msgstr "" - -msgctxt "#30517" -msgid "Subscribe to %s" -msgstr "" - -msgctxt "#30518" -msgid "Feeds" -msgstr "" - -msgctxt "#30519" -msgid "and enter the following code:" -msgstr "" - -msgctxt "#30520" -msgid "Add to..." -msgstr "" - -msgctxt "#30521" -msgid "" -msgstr "" - -msgctxt "#30522" -msgid "" -msgstr "" - -msgctxt "#30523" -msgid "" -msgstr "" - -msgctxt "#30524" -msgid "Select language" -msgstr "" - -msgctxt "#30525" -msgid "Select region" -msgstr "" - -msgctxt "#30526" -msgid "Setup Wizard" -msgstr "" - -msgctxt "#30527" -msgid "Language and Region" -msgstr "" - -msgctxt "#30528" -msgid "Rate..." -msgstr "" - -msgctxt "#30529" -msgid "I like this" -msgstr "" - -msgctxt "#30530" -msgid "I dislike this" -msgstr "" - -msgctxt "#30531" -msgid "" -msgstr "" - -msgctxt "#30532" -msgid "" -msgstr "" - -msgctxt "#30533" -msgid "Reverse" -msgstr "" - -msgctxt "#30534" -msgid "" -msgstr "" - -msgctxt "#30535" -msgid "Select the order of the playlist" -msgstr "" - -msgctxt "#30536" -msgid "Updating Playlist..." -msgstr "" - -msgctxt "#30537" -msgid "Play from here" -msgstr "" - -msgctxt "#30538" -msgid "Disliked Videos" -msgstr "" - -msgctxt "#30539" -msgid "Play recently added" -msgstr "" - -msgctxt "#30540" -msgid "Play with..." -msgstr "" - -msgctxt "#30541" -msgid "Show channel name and video details in description" -msgstr "" - -msgctxt "#30542" -msgid "rtmpe streams are not supported" -msgstr "" - -msgctxt "#30543" -msgid "" -msgstr "" - -msgctxt "#30544" -msgid "More Links from the description" -msgstr "" - -msgctxt "#30545" -msgid "No videos found." -msgstr "" - -msgctxt "#30546" -msgid "Please complete all login prompts" -msgstr "" - -msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." -msgstr "" - -msgctxt "#30548" -msgid "" -msgstr "" - -msgctxt "#30549" -msgid "No streams found" -msgstr "" - -msgctxt "#30550" -msgid "" -msgstr "" - -msgctxt "#30551" -msgid "Recommendations" -msgstr "" - -msgctxt "#30552" -msgid "Maintenance" -msgstr "" - -msgctxt "#30553" -msgid "Delete function cache database" -msgstr "" - -msgctxt "#30554" -msgid "Delete search history database" -msgstr "" - -msgctxt "#30555" -msgid "Clear function cache" -msgstr "" - -msgctxt "#30556" -msgid "Clear search history" -msgstr "" - -msgctxt "#30557" -msgid "function cache" -msgstr "" - -msgctxt "#30558" -msgid "search history" -msgstr "" - -msgctxt "#30559" -msgid "Delete settings.xml" -msgstr "" - -msgctxt "#30560" -msgid "" -msgstr "" - -msgctxt "#30561" -msgid "" -msgstr "" - -msgctxt "#30562" -msgid "View all" -msgstr "" - -msgctxt "#30563" -msgid "" -msgstr "" - -msgctxt "#30564" -msgid "" -msgstr "" - -msgctxt "#30565" -msgid "" -msgstr "" - -msgctxt "#30566" -msgid "" -msgstr "" - -msgctxt "#30567" -msgid "Set as Watch Later" -msgstr "" - -msgctxt "#30568" -msgid "Remove as Watch Later" -msgstr "" - -msgctxt "#30569" -msgid "Are you sure you want to remove \"%s\" as your Watch Later list?" -msgstr "" - -msgctxt "#30570" -msgid "Are you sure you want to replace your current Watch Later list with \"%s\"?" -msgstr "" - -msgctxt "#30571" -msgid "Set as History" -msgstr "" - -msgctxt "#30572" -msgid "Remove as History" -msgstr "" - -msgctxt "#30573" -msgid "Are you sure you want to remove \"%s\" as your History list?" -msgstr "" - -msgctxt "#30574" -msgid "Are you sure you want to replace your current History list with \"%s\"?" -msgstr "" - -msgctxt "#30575" -msgid "Succeeded" -msgstr "" - -msgctxt "#30576" -msgid "Failed" -msgstr "" - -msgctxt "#30577" -msgid "" -msgstr "" - -msgctxt "#30578" -msgid "Force SSL certificate verification" -msgstr "" - -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 "" - -msgctxt "#30580" -msgid "Reset access manager" -msgstr "" - -msgctxt "#30581" -msgid "Are you sure you want to reset access manager?" -msgstr "" - -msgctxt "#30582" -msgid "Autoplay suggested videos" -msgstr "" - -msgctxt "#30583" -msgid "Filters can be channel names separated by a comma eg. 'The Best Channel,The 2nd Best Channel', and/or custom video filters in the form '{ATTR}{OP}{VALUE}' eg. '{duration}{>=}{180}{artists_string}{=}{\"The Best Channel\"},{duration}{<}{180}'" -msgstr "" - -msgctxt "#30584" -msgid "My Subscriptions (Filtered)" -msgstr "" - -msgctxt "#30585" -msgid "Enable Blacklist to exclude channel names in Filters from My Subscriptions, disable to include channel names" -msgstr "" - -msgctxt "#30586" -msgid "Blacklist" -msgstr "" - -msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "" - -msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "" - -msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "" - -msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "" - -msgctxt "#30591" -msgid "Thumbnail size" -msgstr "" - -msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "" - -msgctxt "#30593" -msgid "High (4:3)" -msgstr "" - -msgctxt "#30594" -msgid "Safe search" -msgstr "" - -msgctxt "#30595" -msgid "Moderate" -msgstr "" - -msgctxt "#30596" -msgid "Strict" -msgstr "" - -msgctxt "#30597" -msgid "Updated: %s" -msgstr "" - -msgctxt "#30598" -msgid "" -msgstr "" - -msgctxt "#30599" -msgid "Failed to enable personal API keys. Missing: %s" -msgstr "" - -msgctxt "#30600" -msgid "" -msgstr "" - -msgctxt "#30601" -msgid "%s with Original/%s fallback" -msgstr "" - -msgctxt "#30602" -msgid "No auto-generated" -msgstr "" - -msgctxt "#30603" -msgid "Age gate" -msgstr "" - -msgctxt "#30604" -msgid "Allow offensive content" -msgstr "" - -msgctxt "#30605" -msgid "Quick Search" -msgstr "" - -msgctxt "#30606" -msgid "Quick Search (Incognito)" -msgstr "" - -msgctxt "#30607" -msgid "Audio only" -msgstr "" - -msgctxt "#30608" -msgid "Allow developer keys" -msgstr "" - -msgctxt "#30609" -msgid "Clear watch history" -msgstr "" - -msgctxt "#30610" -msgid "This will clear your account's watch history from all devices. You can't undo this." -msgstr "" - -msgctxt "#30611" -msgid "Saved Playlists" -msgstr "" - -msgctxt "#30612" -msgid "Retry" -msgstr "" - -msgctxt "#30613" -msgid "" -msgstr "" - -msgctxt "#30614" -msgid "" -msgstr "" - -msgctxt "#30615" -msgid "" -msgstr "" - -msgctxt "#30616" -msgid "" -msgstr "" - -msgctxt "#30617" -msgid "InputStream.Adaptive" -msgstr "" - -msgctxt "#30618" -msgid "Stream redirect" -msgstr "" - -msgctxt "#30619" -msgid "Enable to reduce resource usage on less powerful devices, but could lead to IP bans. Use at own risk." -msgstr "" - -msgctxt "#30620" -msgid "Port %s already in use. Cannot start http server." -msgstr "" - -msgctxt "#30621" -msgid "" -msgstr "" - -msgctxt "#30622" -msgid "Purchases" -msgstr "" - -msgctxt "#30623" -msgid "Install InputStream Helper" -msgstr "" - -msgctxt "#30624" -msgid "" -msgstr "" - -msgctxt "#30625" -msgid "InputStream Helper is already installed." -msgstr "" - -msgctxt "#30626" -msgid "Delete temporary files" -msgstr "" - -msgctxt "#30627" -msgid "Rate video after watching" -msgstr "" - -msgctxt "#30628" -msgid "HTTP Server" -msgstr "" - -msgctxt "#30629" -msgid "IP whitelist (comma delimited)" -msgstr "" - -msgctxt "#30630" -msgid "" -msgstr "" - -msgctxt "#30631" -msgid "Successfully updated: %s" -msgstr "" - -msgctxt "#30632" -msgid "Enable API configuration page" -msgstr "" - -msgctxt "#30633" -msgid "http://:/youtube/api (see Advanced > HTTP Server)" -msgstr "" - -msgctxt "#30634" -msgid "YouTube Add-on API Configuration" -msgstr "" - -msgctxt "#30635" -msgid "No changes detected, API keys were not updated." -msgstr "" - -msgctxt "#30636" -msgid "Personal API keys are enabled." -msgstr "" - -msgctxt "#30637" -msgid "Personal API keys are disabled." -msgstr "" - -msgctxt "#30638" -msgid "Bookmark this page to quickly add your keys in the future." -msgstr "" - -msgctxt "#30639" -msgid "Found personal API keys in api_keys.json, would you like to restore them? Choosing no will overwrite them." -msgstr "" - -msgctxt "#30640" -msgid "Restore" -msgstr "" - -msgctxt "#30641" -msgid "Delete api_keys.json" -msgstr "" - -msgctxt "#30642" -msgid "Delete access_manager.json" -msgstr "" - -msgctxt "#30643" -msgid "Listen on IP" -msgstr "" - -msgctxt "#30644" -msgid "Select listen IP" -msgstr "" - -msgctxt "#30645" -msgid "Refresh after watching" -msgstr "" - -msgctxt "#30646" -msgid "Upcoming Live" -msgstr "" - -msgctxt "#30647" -msgid "Completed Live" -msgstr "" - -msgctxt "#30648" -msgid "API Key is incorrect. Settings > API > API Key" -msgstr "" - -msgctxt "#30649" -msgid "Client Id is incorrect. Settings > API > API Id" -msgstr "" - -msgctxt "#30650" -msgid "Client Secret is incorrect. Settings > API > API Secret" -msgstr "" - -msgctxt "#30651" -msgid "Location" -msgstr "" - -msgctxt "#30652" -msgid "Location radius (km)" -msgstr "" - -msgctxt "#30653" -msgid "My Location using IP geolocation lookup" -msgstr "" - -msgctxt "#30654" -msgid "My Location" -msgstr "" - -msgctxt "#30655" -msgid "Switch User" -msgstr "" - -msgctxt "#30656" -msgid "New user" -msgstr "" - -msgctxt "#30657" -msgid "Unnamed" -msgstr "" - -msgctxt "#30658" -msgid "Enter a name for this user" -msgstr "" - -msgctxt "#30659" -msgid "User is now \"%s\"" -msgstr "" - -msgctxt "#30660" -msgid "Users" -msgstr "" - -msgctxt "#30661" -msgid "Add a user" -msgstr "" - -msgctxt "#30662" -msgid "Remove a user" -msgstr "" - -msgctxt "#30663" -msgid "Rename a user" -msgstr "" - -msgctxt "#30664" -msgid "Switch user" -msgstr "" - -msgctxt "#30665" -msgid "Switch to \"%s\" now?" -msgstr "" - -msgctxt "#30666" -msgid "\"%s\" removed" -msgstr "" - -msgctxt "#30667" -msgid "Renamed \"%s\" to \"%s\"" -msgstr "" - -msgctxt "#30668" -msgid "Play count minimum percent" -msgstr "" - -msgctxt "#30669" -msgid "Mark unwatched" -msgstr "" - -msgctxt "#30670" -msgid "Mark watched" -msgstr "" - -msgctxt "#30671" -msgid "Clear playback history" -msgstr "" - -msgctxt "#30672" -msgid "Delete playback history database" -msgstr "" - -msgctxt "#30673" -msgid "playback history" -msgstr "" - -msgctxt "#30674" -msgid "Reset resume point" -msgstr "" - -msgctxt "#30675" -msgid "Use local playback history (watched, resume tracking)" -msgstr "" - -msgctxt "#30676" -msgid "Just now" -msgstr "" - -msgctxt "#30677" -msgid "A minute ago" -msgstr "" - -msgctxt "#30678" -msgid "Recently" -msgstr "" - -msgctxt "#30679" -msgid "An hour ago" -msgstr "" - -msgctxt "#30680" -msgid "Two hours ago" -msgstr "" - -msgctxt "#30681" -msgid "Three hours ago" -msgstr "" - -msgctxt "#30682" -msgid "Yesterday at" -msgstr "" - -msgctxt "#30683" -msgid "Two days ago" -msgstr "" - -msgctxt "#30684" -msgid "Today at" -msgstr "" - -msgctxt "#30685" -msgid "Delete data cache database" -msgstr "" - -msgctxt "#30686" -msgid "Clear data cache" -msgstr "" - -msgctxt "#30687" -msgid "data cache" -msgstr "" - -msgctxt "#30688" -msgid "Use MPEG-DASH for videos" -msgstr "" - -msgctxt "#30689" -msgid "Use for live streams" -msgstr "" - -msgctxt "#30690" -msgid "InputStream.Adaptive >= 2.0.12 is required for adaptive live streams" -msgstr "" - -msgctxt "#30691" -msgid "Airing now" -msgstr "" - -msgctxt "#30692" -msgid "In a minute" -msgstr "" - -msgctxt "#30693" -msgid "Airing soon" -msgstr "" - -msgctxt "#30694" -msgid "In over an hour" -msgstr "" - -msgctxt "#30695" -msgid "In over two hours" -msgstr "" - -msgctxt "#30696" -msgid "Airing today at" -msgstr "" - -msgctxt "#30697" -msgid "Tomorrow at" -msgstr "" - -msgctxt "#30698" -msgid "Check my IP" -msgstr "" - -msgctxt "#30699" -msgid "HTTP server is not running" -msgstr "" - -msgctxt "#30700" -msgid "Client IP is %s" -msgstr "" - -msgctxt "#30701" -msgid "Failed to obtain client IP" -msgstr "" - -msgctxt "#30702" -msgid "Play with subtitles" -msgstr "" - -msgctxt "#30703" -msgid "" -msgstr "" - -msgctxt "#30704" -msgid "Use YouTube website urls with default player" -msgstr "" - -msgctxt "#30705" -msgid "Download subtitles" -msgstr "" - -msgctxt "#30706" -msgid "Download subtitles before starting playback? (Default: No)" -msgstr "" - -msgctxt "#30707" -msgid "Untitled" -msgstr "" - -msgctxt "#30708" -msgid "Play audio only" -msgstr "" - -msgctxt "#30709" -msgid "" -msgstr "" - -msgctxt "#30710" -msgid "" -msgstr "" - -msgctxt "#30711" -msgid "" -msgstr "" - -msgctxt "#30712" -msgid "Rate videos in playlists" -msgstr "" - -msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "" - -msgctxt "#30714" -msgid "Added to playlist" -msgstr "" - -msgctxt "#30715" -msgid "Removed from playlist" -msgstr "" - -msgctxt "#30716" -msgid "Liked video" -msgstr "" - -msgctxt "#30717" -msgid "Disliked video" -msgstr "" - -msgctxt "#30718" -msgid "Rating removed" -msgstr "" - -msgctxt "#30719" -msgid "Subscribed to channel" -msgstr "" - -msgctxt "#30720" -msgid "Unsubscribed from channel" -msgstr "" - -msgctxt "#30721" -msgid "" -msgstr "" - -msgctxt "#30722" -msgid "Enable HDR video" -msgstr "" - -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 "" - -msgctxt "#30724" -msgid "Enable high framerate video" -msgstr "" - -msgctxt "#30725" -msgid "1440p (QHD)" -msgstr "" - -msgctxt "#30726" -msgid "Uploads" -msgstr "" - -msgctxt "#30727" -msgid "Enable H.264 video" -msgstr "" - -msgctxt "#30728" -msgid "Enable VP9 video" -msgstr "" - -msgctxt "#30729" -msgid "Prefer lower resolution streams for unselected codecs" -msgstr "" - -msgctxt "#30730" -msgid "Play (Ask for quality)" -msgstr "" - -msgctxt "#30731" -msgid "The YouTube add-on now requires that you use your own API keys.[CR]For more information see the wiki: [B]https://ytaddon.page.link/keys[/B][CR][CR]Sorry for the inconvenience." -msgstr "" - -msgctxt "#30732" -msgid "Comments" -msgstr "" - -msgctxt "#30733" -msgid "Likes" -msgstr "" - -msgctxt "#30734" -msgid "Replies" -msgstr "" - -msgctxt "#30735" -msgid "Edited" -msgstr "" - -msgctxt "#30736" -msgid "Shorts" -msgstr "" - -msgctxt "#30737" -msgid "Shorts - Max duration" -msgstr "" - -msgctxt "#30738" -msgid "" -msgstr "" - -msgctxt "#30739" -msgid "Subscribers" -msgstr "" - -msgctxt "#30740" -msgid "HLS" -msgstr "" - -msgctxt "#30741" -msgid "Multi-stream HLS" -msgstr "" - -msgctxt "#30742" -msgid "Adaptive HLS" -msgstr "" - -msgctxt "#30743" -msgid "MPEG-DASH" -msgstr "" - -msgctxt "#30744" -msgid "Original" -msgstr "" - -msgctxt "#30745" -msgid "Dubbed" -msgstr "" - -msgctxt "#30746" -msgid "Descriptive" -msgstr "" - -msgctxt "#30747" -msgid "Alternate" -msgstr "" - -msgctxt "#30748" -msgid "Stream features" -msgstr "" - -msgctxt "#30749" -msgid "Enable AV1 video" -msgstr "" - -msgctxt "#30750" -msgid "Enable Vorbis audio" -msgstr "" - -msgctxt "#30751" -msgid "Enable Opus audio" -msgstr "" - -msgctxt "#30752" -msgid "Enable AAC audio" -msgstr "" - -msgctxt "#30753" -msgid "Enable surround sound audio" -msgstr "" - -msgctxt "#30754" -msgid "Enable AC-3 audio" -msgstr "" - -msgctxt "#30755" -msgid "Enable EAC-3 audio" -msgstr "" - -msgctxt "#30756" -msgid "Enable DTS audio" -msgstr "" - -msgctxt "#30757" -msgid "Remove similar/duplicate streams" -msgstr "" - -msgctxt "#30758" -msgid "Stream selection" -msgstr "" - -msgctxt "#30759" -msgid "Quality selection" -msgstr "" - -msgctxt "#30760" -msgid "Automatic + Quality selection" -msgstr "" - -msgctxt "#30761" -msgid "Update playback history on Youtube" -msgstr "" - -msgctxt "#30762" -msgid "Multi-language" -msgstr "" - -msgctxt "#30763" -msgid "Multi-audio" -msgstr "" - -msgctxt "#30764" -msgid "Requests connect timeout" -msgstr "" - -msgctxt "#30765" -msgid "Requests read timeout" -msgstr "" - -msgctxt "#30766" -msgid "Premieres" -msgstr "" - -msgctxt "#30767" -msgid "Views" -msgstr "" - -msgctxt "#30768" -msgid "Disable high framerate video at maximum video quality" -msgstr "" - -msgctxt "#30769" -msgid "Clear Watch Later list" -msgstr "" - -msgctxt "#30770" -msgid "Are you sure you want to clear your Watch Later list?" -msgstr "" - -msgctxt "#30771" -msgid "Disable fractional framerate hinting" -msgstr "" - -msgctxt "#30772" -msgid "Disable all framerate hinting" -msgstr "" - -msgctxt "#30773" -msgid "Show video details in video lists" -msgstr "" - -msgctxt "#30774" -msgid "All available" -msgstr "" - -msgctxt "#30775" -msgid "%s (translation)" -msgstr "" - -msgctxt "#30776" -msgid "Ask + Automatic + Quality selection" -msgstr "" - -msgctxt "#30777" -msgid "Views for %s (%s)" -msgstr "" - -msgctxt "#30778" -msgid "Import old playback history?" -msgstr "" - -msgctxt "#30779" -msgid "Import old search history?" -msgstr "" - -msgctxt "#30780" -msgid "Clear local watch later list" -msgstr "" - -msgctxt "#30781" -msgid "Delete watch later database" -msgstr "" - -msgctxt "#30782" -msgid "local watch later list" -msgstr "" - -msgctxt "#30783" -msgid "settings to recommended values" -msgstr "" - -msgctxt "#30784" -msgid "listings to show minimal details" -msgstr "" - -msgctxt "#30785" -msgid "performance settings" -msgstr "" - -msgctxt "#30786" -msgid "Choose device capabilities" -msgstr "" - -msgctxt "#30787" -msgid "720p, H.264 only | Limited or older devices" -msgstr "" - -msgctxt "#30788" -msgid "1080p/30 fps | Raspberry Pi 3, or similar" -msgstr "" - -msgctxt "#30789" -msgid "4K/30 fps or 1080p/60 fps, HDR if compatible | Raspberry Pi 5, or similar" -msgstr "" - -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 "" - -msgctxt "#30791" -msgid "4K/60 fps, HDR, using AV1 | Fire TV Cube Gen 3, Fire TV Stick 4K Max, Vero V, or similar" -msgstr "" - -msgctxt "#30792" -msgid "8K/60 fps, HDR, using AV1 | Modern device or PC with full capabilities" -msgstr "" - -msgctxt "#30793" -msgid "Views count display colour" -msgstr "" - -msgctxt "#30794" -msgid "Subscriber/Likes count display colour" -msgstr "" - -msgctxt "#30795" -msgid "Videos/Comments count display colour" -msgstr "" - -msgctxt "#30796" -msgid "1080p/60 fps | Raspberry Pi 4, or similar" -msgstr "" - -msgctxt "#30797" -msgid "1080p/30 fps or 720p/30 fps, H.264 only | Raspberry Pi 1/2, or similar" -msgstr "" - -msgctxt "#30798" -msgid "Clear bookmarks list" -msgstr "" - -msgctxt "#30799" -msgid "Delete bookmarks database" -msgstr "" - -msgctxt "#30800" -msgid "bookmarks list" -msgstr "" - -msgctxt "#30801" -msgid "Clear Bookmarks list" -msgstr "" - -msgctxt "#30802" -msgid "Are you sure you want to clear your Bookmarks list?" -msgstr "" - -msgctxt "#30803" -msgid "Bookmark %s" -msgstr "" - -msgctxt "#30804" -msgid "Use YouTube website urls with external player" -msgstr "" - -msgctxt "#30805" -msgid "Use MPEG-DASH with external player" -msgstr "" - -msgctxt "#30806" -msgid "Jump to page..." -msgstr "" - -msgctxt "#30807" -msgid "Use channel name as" -msgstr "" - -msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "" - -msgctxt "#30809" -msgid "All upcoming videos" -msgstr "" - -msgctxt "#30810" -msgid "All previously streamed (completed) videos" -msgstr "" - -msgctxt "#30811" -msgid "Filter Live folders" -msgstr "" - -msgctxt "#30812" -msgid "Clear subscription feed history" -msgstr "" - -msgctxt "#30813" -msgid "Delete subscription feed history database" -msgstr "" - -msgctxt "#30814" -msgid "feed history" -msgstr "" - -msgctxt "#30815" -msgid "Go back..." -msgstr "" - -msgctxt "#30816" -msgid "List is empty.[CR][CR]Refresh from context menu or try again later." -msgstr "" - -msgctxt "#30817" -msgid "Refresh settings.xml" -msgstr "" - -msgctxt "#30818" -msgid "Are you sure you want to refresh settings.xml?" -msgstr "" - -msgctxt "#30819" -msgid "Play from start" -msgstr "" - -msgctxt "#30820" -msgid "Podcast" -msgstr "" diff --git a/plugin.video.youtube/resources/language/resource.language.pl_pl/strings.po b/plugin.video.youtube/resources/language/resource.language.pl_pl/strings.po index 2a7465f35a..f7150b7677 100644 --- a/plugin.video.youtube/resources/language/resource.language.pl_pl/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.pl_pl/strings.po @@ -5,9 +5,9 @@ msgid "" msgstr "" "Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2025-05-10 22:41+0000\n" +"PO-Revision-Date: 2025-09-23 09:09+0000\n" "Last-Translator: Marek Adamski \n" "Language-Team: Polish \n" "Language: pl_pl\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 5.11.3\n" +"X-Generator: Weblate 5.13.3\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -106,8 +106,8 @@ msgid "144p" msgstr "144p" msgctxt "#30020" -msgid "Allow 3D" -msgstr "Dopuszczaj materiały 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "Włącz stereoskopowe wideo 3D" msgctxt "#30021" msgid "Show fanart" @@ -150,8 +150,8 @@ msgid "Configure %s?" msgstr "Skonfigurować %s?" msgctxt "#30031" -msgid "" -msgstr "" +msgid "Requests cache size (MB)" +msgstr "Rozmiar pamięci podręcznej żądań (MB)" msgctxt "#30032" msgid "View: TV Shows" @@ -216,12 +216,12 @@ msgid "Watch Later" msgstr "Do obejrzenia" msgctxt "#30108" -msgid "" -msgstr "" +msgid "Enter the name of the bookmark" +msgstr "Wprowadź nazwę zakładki" msgctxt "#30109" -msgid "" -msgstr "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "Wprowadź prawidłowy adres URL serwisu YouTube lub wtyczki dla zakładki" msgctxt "#30110" msgid "New Search" @@ -236,8 +236,8 @@ msgid "Sign Out" msgstr "Wyloguj" msgctxt "#30113" -msgid "" -msgstr "" +msgid "Related to \"%s\"" +msgstr "Powiązane z \"%s\"" msgctxt "#30114" msgid "Confirm delete" @@ -256,8 +256,8 @@ msgid "Remove \"%s\"?" msgstr "Przenieść \"%s\"?" msgctxt "#30118" -msgid "" -msgstr "" +msgid "Links from \"%s\"" +msgstr "Linki z \"%s\"" msgctxt "#30119" msgid "Please wait..." @@ -300,12 +300,12 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" -msgstr "" +msgid "%s failed" +msgstr "%s nie powiodło się" msgctxt "#30501" -msgid "" -msgstr "" +msgid "Edit %s" +msgstr "Edytuj %s" msgctxt "#30502" msgid "Go to %s" @@ -361,7 +361,7 @@ msgstr "Powiązane" msgctxt "#30515" msgid "Auto-Remove from Watch Later" -msgstr "" +msgstr "Automatyczne usuwanie z listy „Do obejrzenia”" msgctxt "#30516" msgid "Folders" @@ -373,7 +373,7 @@ msgstr "Subskrybuj %s" msgctxt "#30518" msgid "Feeds" -msgstr "" +msgstr "Kanały" msgctxt "#30519" msgid "and enter the following code:" @@ -384,16 +384,16 @@ msgid "Add to..." msgstr "Dodaj do..." msgctxt "#30521" -msgid "" -msgstr "" +msgid "Delete requests cache database" +msgstr "Usuń bazę danych pamięci podręcznej żądań" msgctxt "#30522" -msgid "" -msgstr "" +msgid "Clear requests cache" +msgstr "Wyczyść pamięć podręczną żądań" msgctxt "#30523" -msgid "" -msgstr "" +msgid "requests cache" +msgstr "pamięć podręczna żądań" msgctxt "#30524" msgid "Select language" @@ -460,8 +460,8 @@ msgid "Play recently added" msgstr "Odtwórz ostatnio dodane" msgctxt "#30540" -msgid "Play with..." -msgstr "Odtwarzaj za pomocą..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -488,8 +488,8 @@ msgid "Please complete all login prompts" msgstr "Wypełnij wszystkie monity logowania" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." -msgstr "Może zostać wyświetlony monit o włączenie dwóch aplikacji, aby YouTube działał prawidłowo." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +msgstr "Aby dodatek działał prawidłowo, może zostać wyświetlony monit o zalogowanie się i zezwolenie na dostęp do wielu aplikacji." msgctxt "#30548" msgid "" @@ -552,12 +552,12 @@ msgid "View all" msgstr "Zobacz wszystkie" msgctxt "#30563" -msgid "" -msgstr "" +msgid "Plugin execution timeout" +msgstr "Przekroczono limit czasu wykonania wtyczki" msgctxt "#30564" -msgid "" -msgstr "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "Tylko do testów – wymuś przerwanie działania wtyczki po upływie określonego czasu. Ustaw na 0 sekund (domyślnie), aby wyłączyć." msgctxt "#30565" msgid "" @@ -577,11 +577,11 @@ msgstr "Odłącz od listy Do obejrzenia" msgctxt "#30569" msgid "Are you sure you want to remove \"%s\" as your Watch Later list?" -msgstr "" +msgstr "Czy na pewno chcesz usunąć „%s” z listy „Do obejrzenia”?" msgctxt "#30570" msgid "Are you sure you want to replace your current Watch Later list with \"%s\"?" -msgstr "" +msgstr "Czy na pewno chcesz zastąpić obecną listę „Do obejrzenia” listą „%s”?" msgctxt "#30571" msgid "Set as History" @@ -593,11 +593,11 @@ msgstr "Odłącz od listy Historia" msgctxt "#30573" msgid "Are you sure you want to remove \"%s\" as your History list?" -msgstr "" +msgstr "Czy na pewno chcesz usunąć „%s” z listy Historia?" msgctxt "#30574" msgid "Are you sure you want to replace your current History list with \"%s\"?" -msgstr "" +msgstr "Czy na pewno chcesz zastąpić obecną listę Historia listą „%s”?" msgctxt "#30575" msgid "Succeeded" @@ -608,8 +608,8 @@ msgid "Failed" msgstr "Zakończone niepowodzeniem" msgctxt "#30577" -msgid "" -msgstr "" +msgid "Smallest (4:3)" +msgstr "Najmniejszy (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" @@ -648,32 +648,32 @@ msgid "Blacklist" msgstr "Czarna lista" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Dodaj do filtra Moje subskrypcje" +msgid "Hide \"Playlists\" folder" +msgstr "Ukryj folder „Playlisty”" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Dodaj z filtra Moje subskrypcje" +msgid "Hide \"Search\" folder" +msgstr "Ukryj folder „Szukaj”" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Dodano do filtra Moje subskrypcje" +msgid "Hide \"Shorts\" folder" +msgstr "Ukryj folder „Shorts”" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Usunięto z filtra Moje subskrypcje" +msgid "Hide \"Live\" folder" +msgstr "Ukryj folder „Na żywo”" msgctxt "#30591" msgid "Thumbnail size" msgstr "Rozmiar miniatur" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Średni (16:9)" +msgid "Small (16:9)" +msgstr "Mały (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Duży (4:3)" +msgid "Medium (4:3)" +msgstr "Średni (4:3)" msgctxt "#30594" msgid "Safe search" @@ -688,20 +688,20 @@ msgid "Strict" msgstr "Rygorystyczny" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Zaktualizowano: %s" +msgid "Hide \"Members only\" folder" +msgstr "Ukryj folder „Tylko dla wspierających”" msgctxt "#30598" -msgid "" -msgstr "" +msgid "Large (4:3)" +msgstr "Duży (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" msgstr "Aktywacja osobistych kluczy API zakończona niepowodzeniem. Brakuje: %s" msgctxt "#30600" -msgid "" -msgstr "" +msgid "Largest (16:9)" +msgstr "Największy (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" @@ -752,20 +752,20 @@ msgid "Retry" msgstr "Ponów" msgctxt "#30613" -msgid "" -msgstr "" +msgid "Add to %s" +msgstr "Dodaj do %s" msgctxt "#30614" -msgid "" -msgstr "" +msgid "Remove from %s" +msgstr "Usuń z %s" msgctxt "#30615" -msgid "" -msgstr "" +msgid "Added to %s" +msgstr "Dodano do %s" msgctxt "#30616" -msgid "" -msgstr "" +msgid "Removed from %s" +msgstr "Usunięto z %s" msgctxt "#30617" msgid "InputStream.Adaptive" @@ -784,8 +784,8 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Port %s jest aktualnie w użyciu. Nie można uruchomić serwera http." msgctxt "#30621" -msgid "" -msgstr "" +msgid "Verbose" +msgstr "Rozwlekły" msgctxt "#30622" msgid "Purchases" @@ -796,8 +796,8 @@ msgid "Install InputStream Helper" msgstr "Zainstaluj InputStream Helper" msgctxt "#30624" -msgid "" -msgstr "" +msgid "Members only" +msgstr "Tylko dla wspierających" msgctxt "#30625" msgid "InputStream Helper is already installed." @@ -872,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Usuń access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Nasłuchuj na IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -937,7 +937,7 @@ msgstr "Wprowadź nazwę dla użytkownika" msgctxt "#30659" msgid "User is now \"%s\"" -msgstr "" +msgstr "Użytkownik to teraz „%s”" msgctxt "#30660" msgid "Users" @@ -961,27 +961,27 @@ msgstr "Zmień użytkownika" msgctxt "#30665" msgid "Switch to \"%s\" now?" -msgstr "" +msgstr "Czy chcesz teraz przełączyć się na „%s”?" msgctxt "#30666" msgid "\"%s\" removed" -msgstr "" +msgstr "Usunięto „%s”" msgctxt "#30667" msgid "Renamed \"%s\" to \"%s\"" -msgstr "" +msgstr "Zmieniono nazwę „%s” na „%s”" msgctxt "#30668" msgid "Play count minimum percent" msgstr "Minimalny procent obejrzenia" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Oznacz nieobejrzane" +msgid "%s removed" +msgstr "Usunięto %s" msgctxt "#30670" -msgid "Mark watched" -msgstr "Oznacz obejrzane" +msgid "Added %s" +msgstr "Dodano %s" msgctxt "#30671" msgid "Clear playback history" @@ -996,8 +996,8 @@ msgid "playback history" msgstr "historia odtwarzania" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Resetuj punkt wznowienia" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1136,12 +1136,12 @@ msgid "Play audio only" msgstr "Odtwarzaj tylko dźwięk" msgctxt "#30709" -msgid "" -msgstr "" +msgid "WebVTT subtitles" +msgstr "Napisy WebVTT" msgctxt "#30710" -msgid "" -msgstr "" +msgid "TTML subtitles" +msgstr "Napisy TTML" msgctxt "#30711" msgid "" @@ -1152,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Oceń wideo na listach" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Dodano do Do obejrzenia" +msgid "Prefer dubbed audio over original audio" +msgstr "Wolę przetłumaczone audio od oryginalnego dźwięku" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Dodano do listy" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "Wolisz automatycznie przetłumaczoną ścieżkę dźwiękową zamiast oryginalnej ścieżki dźwiękowej" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Usunięto z listy" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "Czy używać wewnętrznej listy YouTube do historii oglądania?[CR][CR]Wymaga zalogowania się za pomocą dodatku i aktywowania śledzenia historii w YouTube." msgctxt "#30716" msgid "Liked video" @@ -1172,8 +1172,8 @@ msgid "Disliked video" msgstr "Nie polubiono" msgctxt "#30718" -msgid "Rating removed" -msgstr "Usunięto ocenę" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "Czy użyć wewnętrznej listy YouTube do obejrzenia później?[CR][CR]Wymagane jest zalogowanie się za pomocą dodatku." msgctxt "#30719" msgid "Subscribed to channel" @@ -1184,8 +1184,8 @@ msgid "Unsubscribed from channel" msgstr "Anulowano subskrypcję kanału" msgctxt "#30721" -msgid "" -msgstr "" +msgid "Enable Panoramic/180/360/VR video" +msgstr "Włącz wideo panoramiczne/180/360/VR" msgctxt "#30722" msgid "Enable HDR video" @@ -1252,8 +1252,8 @@ msgid "Shorts - Max duration" msgstr "Shorts - maksymalny czas trwania" msgctxt "#30738" -msgid "" -msgstr "" +msgid "Enable spatial audio" +msgstr "Włącz dźwięk przestrzenny (spatial)" msgctxt "#30739" msgid "Subscribers" @@ -1532,8 +1532,8 @@ msgid "Use channel name as" msgstr "Użyj nazwy kanału jako" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "Ukryj filmy z listy" +msgid "Hide items from listings" +msgstr "Ukryj elementy z listy" msgctxt "#30809" msgid "All upcoming videos" @@ -1583,6 +1583,82 @@ msgctxt "#30820" msgid "Podcast" msgstr "Podcast" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Nasłuchuj na IP" + +#~ msgctxt "#30547" +#~ msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +#~ msgstr "Może zostać wyświetlony monit o włączenie dwóch aplikacji, aby YouTube działał prawidłowo." + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Ukryj filmy z listy" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Dopuszczaj materiały 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Odtwarzaj za pomocą..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Dodaj do filtra Moje subskrypcje" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Dodaj z filtra Moje subskrypcje" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Dodano do filtra Moje subskrypcje" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Usunięto z filtra Moje subskrypcje" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Średni (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Duży (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Zaktualizowano: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Oznacz nieobejrzane" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Oznacz obejrzane" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Resetuj punkt wznowienia" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Dodano do Do obejrzenia" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Dodano do listy" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Usunięto z listy" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Usunięto ocenę" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Brak kolejnych łączy." diff --git a/plugin.video.youtube/resources/language/resource.language.prs/strings.po b/plugin.video.youtube/resources/language/resource.language.prs/strings.po index 681468325f..1812bc32d8 100644 --- a/plugin.video.youtube/resources/language/resource.language.prs/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.prs/strings.po @@ -105,7 +105,7 @@ msgid "144p" msgstr "" msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -149,7 +149,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -215,11 +215,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -235,7 +235,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -255,7 +255,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -299,11 +299,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -383,15 +383,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -459,7 +459,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -487,7 +487,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -551,11 +551,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -607,7 +607,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -647,19 +647,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -667,11 +667,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -687,11 +687,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -699,7 +699,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -751,19 +751,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -783,7 +783,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -795,7 +795,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -871,7 +871,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -975,11 +975,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -995,7 +995,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1135,11 +1135,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1151,15 +1151,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1171,7 +1171,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1183,7 +1183,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1251,7 +1251,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1531,7 +1531,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.pt_br/strings.po b/plugin.video.youtube/resources/language/resource.language.pt_br/strings.po index 8b056be526..98485fda6a 100644 --- a/plugin.video.youtube/resources/language/resource.language.pt_br/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.pt_br/strings.po @@ -107,8 +107,8 @@ msgid "144p" msgstr "144p" msgctxt "#30020" -msgid "Allow 3D" -msgstr "Permitir 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +151,7 @@ msgid "Configure %s?" msgstr "Configurar %s?" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +217,11 @@ msgid "Watch Later" msgstr "Assistir mais tarde" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +237,7 @@ msgid "Sign Out" msgstr "Deslogar" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +257,7 @@ msgid "Remove \"%s\"?" msgstr "Remover \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -302,11 +302,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -386,15 +386,15 @@ msgid "Add to..." msgstr "Adicionar para..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -462,8 +462,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Reproduzir com..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -490,7 +490,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -554,11 +554,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -610,7 +610,7 @@ msgid "Failed" msgstr "Falhou" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -650,32 +650,32 @@ msgid "Blacklist" msgstr "Lista negra" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Adicionara para o filtro Minhas Subscrições" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Remover do filtro Minhas Subscrições" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Adicionado ao filtro Minhas Subscrições" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Removido do filtro Minhas Subscrições" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Tamanho da miniatura" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Média (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Alta (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -690,11 +690,11 @@ msgid "Strict" msgstr "Estrita" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Atualizado: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -702,7 +702,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Falhous ao ativar a chave pessoal da API. Faltando: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -754,19 +754,19 @@ msgid "Retry" msgstr "Tentar novamente" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -786,7 +786,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Porta %s já em uso. Não pude iniciar o servidor HTTP." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -798,7 +798,7 @@ msgid "Install InputStream Helper" msgstr "Instalar InputStream Helper" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -874,8 +874,8 @@ msgid "Delete access_manager.json" msgstr "Deletar access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Ouça no IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -978,12 +978,12 @@ msgid "Play count minimum percent" msgstr "Porcentagem mínima da contagem de reprodução" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Marcar como não assistido" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Marcar como assistido" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -998,8 +998,8 @@ msgid "playback history" msgstr "histórico de reprodução" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Resetar ponto de resumo" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1138,11 +1138,11 @@ msgid "Play audio only" msgstr "Reproduzir áudio somente" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1154,16 +1154,16 @@ msgid "Rate videos in playlists" msgstr "Avaliar vídeos na lista de reprodução" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Adicionado para Assistir mais tarde" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Adicionado a lista de reprodução" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Removido da lista de reprodução" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1174,8 +1174,8 @@ msgid "Disliked video" msgstr "Vídeos Descurtidos" msgctxt "#30718" -msgid "Rating removed" -msgstr "Avaliação removida" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1186,7 +1186,7 @@ msgid "Unsubscribed from channel" msgstr "Removido subscrição para o canal" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1254,7 +1254,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1539,8 +1539,8 @@ msgid "Use channel name as" msgstr "Use o nome do canal como" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "Ocultar vídeos das listagens" +msgid "Hide items from listings" +msgstr "" msgctxt "#30809" msgid "All upcoming videos" @@ -1590,6 +1590,78 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Ouça no IP" + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Ocultar vídeos das listagens" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Permitir 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Reproduzir com..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Adicionara para o filtro Minhas Subscrições" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Remover do filtro Minhas Subscrições" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Adicionado ao filtro Minhas Subscrições" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Removido do filtro Minhas Subscrições" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Média (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Alta (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Atualizado: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Marcar como não assistido" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Marcar como assistido" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Resetar ponto de resumo" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Adicionado para Assistir mais tarde" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Adicionado a lista de reprodução" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Removido da lista de reprodução" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Avaliação removida" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Nenhum outro link encontrado." diff --git a/plugin.video.youtube/resources/language/resource.language.pt_pt/strings.po b/plugin.video.youtube/resources/language/resource.language.pt_pt/strings.po index 06d4a8acb6..3363ced1f0 100644 --- a/plugin.video.youtube/resources/language/resource.language.pt_pt/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.pt_pt/strings.po @@ -106,8 +106,8 @@ msgid "144p" msgstr "" msgctxt "#30020" -msgid "Allow 3D" -msgstr "Permitir 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -150,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -216,11 +216,11 @@ msgid "Watch Later" msgstr "Ver mais tarde" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "Terminar Sessão" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Remover \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -384,15 +384,15 @@ msgid "Add to..." msgstr "Adicionar a..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -460,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Reproduzir com..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -488,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -552,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -608,7 +608,7 @@ msgid "Failed" msgstr "Falhou" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -648,32 +648,32 @@ msgid "Blacklist" msgstr "Lista Negra" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Adicionar á minha lista de Subscrições filtradas" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Remover da minha lista de Subscrições filtradas" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Adicionado á minha lista de Subscrições filtradas" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Removido da minha lista de Subscrições filtradas" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Tamanho da miniatura" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Médio (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Grande (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -688,11 +688,11 @@ msgid "Strict" msgstr "Rigorosa" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Actualizado: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -700,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Falha ao activar chaves API pessoais. Em falta: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -752,19 +752,19 @@ msgid "Retry" msgstr "Tentar novamente" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -784,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Porta %s já está em uso. Falha ao iniciar servidor http." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Instalar InputStream Helper" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Apagar access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Escutar no IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -976,12 +976,12 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Marcar como não visto" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Marcar como visto" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -996,8 +996,8 @@ msgid "playback history" msgstr "histórico de reprodução" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Redefinir ponto de resumo" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1136,11 +1136,11 @@ msgid "Play audio only" msgstr "Reproduzir apenas audio" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1152,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Classifique vídeos em listas de reprodução" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Adicionar a Ver mais tarde" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Adicionado á Lista de Reprodução" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Removido de Lista de Reprodução" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1172,8 +1172,8 @@ msgid "Disliked video" msgstr "Vídeos que Não Gostei" msgctxt "#30718" -msgid "Rating removed" -msgstr "Classificação removida" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1184,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Não subscrito no canal" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1252,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1532,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1583,6 +1583,74 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Escutar no IP" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Permitir 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Reproduzir com..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Adicionar á minha lista de Subscrições filtradas" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Remover da minha lista de Subscrições filtradas" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Adicionado á minha lista de Subscrições filtradas" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Removido da minha lista de Subscrições filtradas" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Médio (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Grande (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Actualizado: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Marcar como não visto" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Marcar como visto" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Redefinir ponto de resumo" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Adicionar a Ver mais tarde" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Adicionado á Lista de Reprodução" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Removido de Lista de Reprodução" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Classificação removida" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Nenhum link encontrado" diff --git a/plugin.video.youtube/resources/language/resource.language.ro_ro/strings.po b/plugin.video.youtube/resources/language/resource.language.ro_ro/strings.po index 34142406f1..2e62152f40 100644 --- a/plugin.video.youtube/resources/language/resource.language.ro_ro/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ro_ro/strings.po @@ -107,8 +107,8 @@ msgid "144p" msgstr "" msgctxt "#30020" -msgid "Allow 3D" -msgstr "Permite 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +151,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +217,11 @@ msgid "Watch Later" msgstr "Privesc mai târziu" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +237,7 @@ msgid "Sign Out" msgstr "Deautentificare" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +257,7 @@ msgid "Remove \"%s\"?" msgstr "Eliminați „%s”?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +301,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +385,15 @@ msgid "Add to..." msgstr "Adaugă la..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +461,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Redă cu..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +489,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +553,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +609,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +649,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +669,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +689,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +701,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +753,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +785,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +797,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +873,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +977,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +997,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1137,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,16 +1153,16 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Șters din listă" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1173,8 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" -msgstr "Evaluare ștearsă" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,7 +1185,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1253,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1533,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1584,22 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Permite 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Redă cu..." + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Șters din listă" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Evaluare ștearsă" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Nu mai sunt alte legături." diff --git a/plugin.video.youtube/resources/language/resource.language.ru_ru/strings.po b/plugin.video.youtube/resources/language/resource.language.ru_ru/strings.po index 2e5f0ceeed..cee15dcc63 100644 --- a/plugin.video.youtube/resources/language/resource.language.ru_ru/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ru_ru/strings.po @@ -7,15 +7,15 @@ msgstr "" "Project-Id-Version: XBMC-Addons\n" "Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2024-04-14 19:47+0000\n" -"Last-Translator: Christian Gade \n" +"PO-Revision-Date: 2025-10-11 06:29+0000\n" +"Last-Translator: Dmitry Petrov \n" "Language-Team: Russian \n" "Language: ru_ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 5.4.3\n" +"X-Generator: Weblate 5.13.3\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -50,16 +50,16 @@ msgstr "" msgctxt "#30003" msgid "YouTube" -msgstr "" +msgstr "YouTube" # empty strings from id 30004 to 30006 msgctxt "#30007" msgid "Use InputStream.Adaptive" -msgstr "" +msgstr "Использовать InputStream.Adaptive" msgctxt "#30008" msgid "Configure InputStream.Adaptive" -msgstr "" +msgstr "Настроить InputStream.Adaptive" msgctxt "#30009" msgid "Always ask for the video quality" @@ -67,7 +67,7 @@ msgstr "Всегда задавать вопрос о качестве виде msgctxt "#30010" msgid "Maximum video quality" -msgstr "" +msgstr "Максимальное качество видео" msgctxt "#30011" msgid "480p" @@ -103,12 +103,11 @@ msgstr "1080p Прямой эфир / 720p (HD)" msgctxt "#30019" msgid "144p" -msgstr "" +msgstr "144p" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Разрешить 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "Включить стереоскопическое 3D-видео" msgctxt "#30021" msgid "Show fanart" @@ -128,11 +127,11 @@ msgstr "Размер кэша (Мб)" msgctxt "#30025" msgid "Enable Setup Wizard" -msgstr "" +msgstr "Включить мастер настройки" msgctxt "#30026" msgid "Override views" -msgstr "" +msgstr "Переопределить представления" msgctxt "#30027" msgid "View: Default" @@ -148,11 +147,11 @@ msgstr "Вид: Фильмы" msgctxt "#30030" msgid "Configure %s?" -msgstr "" +msgstr "Настроить %s?" msgctxt "#30031" -msgid "" -msgstr "" +msgid "Requests cache size (MB)" +msgstr "Размер кэша запросов (МБ)" msgctxt "#30032" msgid "View: TV Shows" @@ -186,11 +185,11 @@ msgstr "ID плейлиста для истории" # empty strings from id 30039 to 30099 msgctxt "#30100" msgid "Bookmarks" -msgstr "" +msgstr "Закладки" msgctxt "#30101" msgid "Bookmark" -msgstr "" +msgstr "Закладка" msgctxt "#30102" msgid "Search" @@ -206,23 +205,23 @@ msgstr "" msgctxt "#30105" msgid "filtered" -msgstr "" +msgstr "отфильтровано" msgctxt "#30106" msgid "Next page: %d" -msgstr "" +msgstr "Следующая страница: %d" msgctxt "#30107" msgid "Watch Later" msgstr "Отложенный просмотр" msgctxt "#30108" -msgid "" -msgstr "" +msgid "Enter the name of the bookmark" +msgstr "Введите название закладки" msgctxt "#30109" -msgid "" -msgstr "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "Введите действительный URL-адрес YouTube или плагина для закладки" msgctxt "#30110" msgid "New Search" @@ -237,8 +236,8 @@ msgid "Sign Out" msgstr "Выйти" msgctxt "#30113" -msgid "" -msgstr "" +msgid "Related to \"%s\"" +msgstr "Относится к \"%s\"" msgctxt "#30114" msgid "Confirm delete" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Удалить \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -266,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 @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,16 +384,16 @@ msgid "Add to..." msgstr "Добавить в..." msgctxt "#30521" -msgid "" -msgstr "" +msgid "Delete requests cache database" +msgstr "Удалить базу данных с кешом запросов" msgctxt "#30522" -msgid "" -msgstr "" +msgid "Clear requests cache" +msgstr "Удалить кеш запросов" msgctxt "#30523" -msgid "" -msgstr "" +msgid "requests cache" +msgstr "запросов в кеше" msgctxt "#30524" msgid "Select language" @@ -406,11 +405,11 @@ msgstr "Выберите регион" msgctxt "#30526" msgid "Setup Wizard" -msgstr "" +msgstr "Мастер настройки" msgctxt "#30527" msgid "Language and Region" -msgstr "" +msgstr "Язык и регион" msgctxt "#30528" msgid "Rate..." @@ -458,15 +457,15 @@ msgstr "Непонравившееся видео" msgctxt "#30539" msgid "Play recently added" -msgstr "" +msgstr "Воспроизвести недавно добавленное" msgctxt "#30540" -msgid "Play with..." -msgstr "Воспроизвести с помощью..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" -msgstr "" +msgstr "Отображать название канала и детали видео в описании" msgctxt "#30542" msgid "rtmpe streams are not supported" @@ -482,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 enable two applications so that YouTube is functioning properly." -msgstr "" +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +msgstr "Вас могут запросить войти и дать доступ нескольким приложениям, так дополнение сможет функционировать корректно." msgctxt "#30548" msgid "" @@ -498,7 +497,7 @@ msgstr "" msgctxt "#30549" msgid "No streams found" -msgstr "" +msgstr "Трансляции не найдены" msgctxt "#30550" msgid "" @@ -550,15 +549,15 @@ msgstr "" msgctxt "#30562" msgid "View all" -msgstr "" +msgstr "Посмотреть всё" msgctxt "#30563" -msgid "" -msgstr "" +msgid "Plugin execution timeout" +msgstr "Время выполнения плагина" msgctxt "#30564" -msgid "" -msgstr "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "Для целей тестирования - принудительно остановить выполнение плагина через заданное время. Установите на 0 секунд (по-умолчанию) для выключения." msgctxt "#30565" msgid "" @@ -578,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,8 +608,8 @@ msgid "Failed" msgstr "Ошибка" msgctxt "#30577" -msgid "" -msgstr "" +msgid "Smallest (4:3)" +msgstr "Наименьшее (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" @@ -618,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,32 +648,32 @@ msgid "Blacklist" msgstr "Черный список" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Добавить в фильтр моих подписок" +msgid "Hide \"Playlists\" folder" +msgstr "Скрыть папку \"Плейлисты\"" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Удалить из фильтра моих подписок" +msgid "Hide \"Search\" folder" +msgstr "Скрыть папку \"Поиск\"" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Добавлен в фильтр Мои подписки" +msgid "Hide \"Shorts\" folder" +msgstr "Скрыть папку \"Shorts\"" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Убран из фильтра Мои подписки" +msgid "Hide \"Live\" folder" +msgstr "Скрыть папку \"Прямой эфир\"" msgctxt "#30591" msgid "Thumbnail size" msgstr "Размер иконок" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Средний (16:9)" +msgid "Small (16:9)" +msgstr "Маленький (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Большой (4:3)" +msgid "Medium (4:3)" +msgstr "Средний (4:3)" msgctxt "#30594" msgid "Safe search" @@ -689,20 +688,20 @@ msgid "Strict" msgstr "Строгий" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Обновлено: %s" +msgid "Hide \"Members only\" folder" +msgstr "Скрыть папку \"Только для платных подписчиков\"" msgctxt "#30598" -msgid "" -msgstr "" +msgid "Large (4:3)" +msgstr "Большой (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" msgstr "Ошибка включения персонального API ключа. Отсутствует: %s" msgctxt "#30600" -msgid "" -msgstr "" +msgid "Largest (16:9)" +msgstr "Крупный (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" @@ -753,39 +752,39 @@ msgid "Retry" msgstr "Повтор" msgctxt "#30613" -msgid "" -msgstr "" +msgid "Add to %s" +msgstr "Добавить в %s" msgctxt "#30614" -msgid "" -msgstr "" +msgid "Remove from %s" +msgstr "Убрать из %s" msgctxt "#30615" -msgid "" -msgstr "" +msgid "Added to %s" +msgstr "Добавлено в %s" msgctxt "#30616" -msgid "" -msgstr "" +msgid "Removed from %s" +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." msgstr "Порт %s уже используется. Ошибка запуска HTTP сервера." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,8 +796,8 @@ msgid "Install InputStream Helper" msgstr "Установить InputStream Helper" msgctxt "#30624" -msgid "" -msgstr "" +msgid "Members only" +msgstr "Только для платных подписчиков" msgctxt "#30625" msgid "InputStream Helper is already installed." @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Удалить access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Слушать IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -894,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" @@ -938,7 +937,7 @@ msgstr "Введите имя пользователя" msgctxt "#30659" msgid "User is now \"%s\"" -msgstr "" +msgstr "Выбран пользователь \"%s\"" msgctxt "#30660" msgid "Users" @@ -962,27 +961,27 @@ 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" msgstr "Минимальный процент отметки просмотра" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Отметить как не просмотренное" +msgid "%s removed" +msgstr "%s удален" msgctxt "#30670" -msgid "Mark watched" -msgstr "Отметить как просмотренное" +msgid "Added %s" +msgstr "Добавлен %s" msgctxt "#30671" msgid "Clear playback history" @@ -990,19 +989,19 @@ msgstr "Очистить историю просмотра" msgctxt "#30672" msgid "Delete playback history database" -msgstr "" +msgstr "Удалить историю просмотров" msgctxt "#30673" msgid "playback history" msgstr "история просмотра" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Сброс точки возобновления" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" -msgstr "" +msgstr "Использовать локальную историю просмотров (просмотренные, трекер времени)" msgctxt "#30676" msgid "Just now" @@ -1054,7 +1053,7 @@ msgstr "кеш данных" msgctxt "#30688" msgid "Use MPEG-DASH for videos" -msgstr "" +msgstr "Использовать MPEG-DASH для видео" msgctxt "#30689" msgid "Use for live streams" @@ -1062,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" @@ -1118,7 +1117,7 @@ msgstr "" msgctxt "#30704" msgid "Use YouTube website urls with default player" -msgstr "" +msgstr "Использовать ссылки YouTube с проигрывателем по-умолчанию" msgctxt "#30705" msgid "Download subtitles" @@ -1137,12 +1136,12 @@ msgid "Play audio only" msgstr "Воспроизвести аудио" msgctxt "#30709" -msgid "" -msgstr "" +msgid "WebVTT subtitles" +msgstr "WebVTT субтитры" msgctxt "#30710" -msgid "" -msgstr "" +msgid "TTML subtitles" +msgstr "TTML субтитры" msgctxt "#30711" msgid "" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Оценивать видео в плейлистах" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Добавлено в список \"Смотреть позже\"" +msgid "Prefer dubbed audio over original audio" +msgstr "Предпочитать дубляж аудио вместо оригинала" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Добавлен в плейлист" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "Предпочитать автоматически переведенное аудио дубляж вместо оригинального аудио" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Убран из плейлиста" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "Оценка \"Не нравится\" поставлена" msgctxt "#30718" -msgid "Rating removed" -msgstr "Оценка убрана" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,20 +1184,20 @@ msgid "Unsubscribed from channel" msgstr "Отписан от канала" msgctxt "#30721" -msgid "" -msgstr "" +msgid "Enable Panoramic/180/360/VR video" +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)" @@ -1210,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)" @@ -1246,43 +1245,43 @@ msgstr "Изменен" msgctxt "#30736" msgid "Shorts" -msgstr "" +msgstr "Shorts" msgctxt "#30737" msgid "Shorts - Max duration" -msgstr "" +msgstr "Shorts - Максимальная длительность" msgctxt "#30738" -msgid "" -msgstr "" +msgid "Enable spatial audio" +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" @@ -1294,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" @@ -1374,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" @@ -1442,147 +1441,216 @@ 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 videos from listings" -msgstr "" +msgid "Hide items from listings" +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 "Подкаст" + +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Слушать IP" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Разрешить 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Воспроизвести с помощью..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Добавить в фильтр моих подписок" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Удалить из фильтра моих подписок" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Добавлен в фильтр Мои подписки" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Убран из фильтра Мои подписки" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Средний (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Большой (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Обновлено: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Отметить как не просмотренное" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Отметить как просмотренное" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Сброс точки возобновления" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Добавлено в список \"Смотреть позже\"" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Добавлен в плейлист" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Убран из плейлиста" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Оценка убрана" #~ msgctxt "#30545" #~ msgid "No further links found." diff --git a/plugin.video.youtube/resources/language/resource.language.si_lk/strings.po b/plugin.video.youtube/resources/language/resource.language.si_lk/strings.po index dff8e5e3c4..14fb370e47 100644 --- a/plugin.video.youtube/resources/language/resource.language.si_lk/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.si_lk/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.sk_sk/strings.po b/plugin.video.youtube/resources/language/resource.language.sk_sk/strings.po index 8fb86c56b1..8a461d7eec 100644 --- a/plugin.video.youtube/resources/language/resource.language.sk_sk/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.sk_sk/strings.po @@ -106,8 +106,8 @@ msgid "144p" msgstr "144p" msgctxt "#30020" -msgid "Allow 3D" -msgstr "Povoliť 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -150,7 +150,7 @@ msgid "Configure %s?" msgstr "Konfigurovať %s?" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -216,11 +216,11 @@ msgid "Watch Later" msgstr "Pozrieť neskôr" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "Odhlásiť" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "Odstrániť \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -384,15 +384,15 @@ msgid "Add to..." msgstr "Pridať do..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -460,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Prehrať pomocou..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -488,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -552,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -608,7 +608,7 @@ msgid "Failed" msgstr "Neúspech" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -648,32 +648,32 @@ msgid "Blacklist" msgstr "Zakázať" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Pridať do filtra Moje odbery" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Odobrať z filtra Moje odbery" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Pridané do filtra Moje odbery" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Odobrané z filtra Moje odbery" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Veľkosť náhľadov" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Stredná (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Vysoká (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -688,11 +688,11 @@ msgid "Strict" msgstr "Prísne" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Aktualizované: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -700,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Nepodarilo sa zapnúť osobné kľúče API. Chýba: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -752,19 +752,19 @@ msgid "Retry" msgstr "Opakovať" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -784,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Port %s už je používaný. Nepodarilo sa spustiť http server." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "Nainštalovať pomocný program pre InputStream" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Odstrániť access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Počúvať na IP adrese" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -976,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Minimálne percento prehratí" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Označiť ako nevidené" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "Označiť ako videné" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -996,8 +996,8 @@ msgid "playback history" msgstr "história prehrávania" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Resetovať pozíciu pokračovania" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1136,11 +1136,11 @@ msgid "Play audio only" msgstr "Prehrať iba zvuk" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1152,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Hodnotiť videá v zoznamoch prehrávania" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Pridané do 'Pozrieť neskôr'" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Pridané do zoznamu" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Odstránené zo zoznamu" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1172,8 +1172,8 @@ msgid "Disliked video" msgstr "Video, ktoré sa mi nepáči" msgctxt "#30718" -msgid "Rating removed" -msgstr "Hodnotenie odstránené" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1184,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Zrušenie odberu" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1252,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1532,8 +1532,8 @@ msgid "Use channel name as" msgstr "Použiť názov kanála ako" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "Skrývať filmy v zoznamoch" +msgid "Hide items from listings" +msgstr "" msgctxt "#30809" msgid "All upcoming videos" @@ -1583,6 +1583,78 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Počúvať na IP adrese" + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Skrývať filmy v zoznamoch" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Povoliť 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Prehrať pomocou..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Pridať do filtra Moje odbery" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Odobrať z filtra Moje odbery" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Pridané do filtra Moje odbery" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Odobrané z filtra Moje odbery" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Stredná (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Vysoká (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Aktualizované: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Označiť ako nevidené" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Označiť ako videné" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Resetovať pozíciu pokračovania" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Pridané do 'Pozrieť neskôr'" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Pridané do zoznamu" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Odstránené zo zoznamu" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Hodnotenie odstránené" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Ďalšie odkazy neboli nájdené." diff --git a/plugin.video.youtube/resources/language/resource.language.sl_si/strings.po b/plugin.video.youtube/resources/language/resource.language.sl_si/strings.po index 9d1a09fd16..de73e3fad9 100644 --- a/plugin.video.youtube/resources/language/resource.language.sl_si/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.sl_si/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.sq_al/strings.po b/plugin.video.youtube/resources/language/resource.language.sq_al/strings.po index f2b6ca00d1..e332459857 100644 --- a/plugin.video.youtube/resources/language/resource.language.sq_al/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.sq_al/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.sr_rs/strings.po b/plugin.video.youtube/resources/language/resource.language.sr_rs/strings.po index 1ec37f82fe..5a1150405b 100644 --- a/plugin.video.youtube/resources/language/resource.language.sr_rs/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.sr_rs/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.sr_rs@latin/strings.po b/plugin.video.youtube/resources/language/resource.language.sr_rs@latin/strings.po index 866f586427..f105e86a0a 100644 --- a/plugin.video.youtube/resources/language/resource.language.sr_rs@latin/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.sr_rs@latin/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.sv_se/strings.po b/plugin.video.youtube/resources/language/resource.language.sv_se/strings.po index 2e965b6962..58bf83baff 100644 --- a/plugin.video.youtube/resources/language/resource.language.sv_se/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.sv_se/strings.po @@ -5,9 +5,9 @@ msgid "" msgstr "" "Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2025-05-13 20:24+0000\n" +"PO-Revision-Date: 2025-11-10 00:43+0000\n" "Last-Translator: Daniel Nylander \n" "Language-Team: Swedish \n" "Language: sv_se\n" @@ -15,7 +15,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.11.3\n" +"X-Generator: Weblate 5.14.3\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "144p" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Tillåt 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "Aktivera stereoskopisk 3D-video" msgctxt "#30021" msgid "Show fanart" @@ -151,8 +150,8 @@ msgid "Configure %s?" msgstr "Konfigurera %s?" msgctxt "#30031" -msgid "" -msgstr "" +msgid "Requests cache size (MB)" +msgstr "Cachestorlek för begäranden (MB)" msgctxt "#30032" msgid "View: TV Shows" @@ -217,12 +216,12 @@ msgid "Watch Later" msgstr "Se senare" msgctxt "#30108" -msgid "" -msgstr "" +msgid "Enter the name of the bookmark" +msgstr "Ange namnet på bokmärket" msgctxt "#30109" -msgid "" -msgstr "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "Ange en giltig URL för YouTube eller tillägg för bokmärket" msgctxt "#30110" msgid "New Search" @@ -237,8 +236,8 @@ msgid "Sign Out" msgstr "Logga ut" msgctxt "#30113" -msgid "" -msgstr "" +msgid "Related to \"%s\"" +msgstr "Relaterad till \"%s\"" msgctxt "#30114" msgid "Confirm delete" @@ -257,8 +256,8 @@ msgid "Remove \"%s\"?" msgstr "Ta bort \"%s\"?" msgctxt "#30118" -msgid "" -msgstr "" +msgid "Links from \"%s\"" +msgstr "Länkar från \"%s\"" msgctxt "#30119" msgid "Please wait..." @@ -301,12 +300,12 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" -msgstr "" +msgid "%s failed" +msgstr "%s misslyckades" msgctxt "#30501" -msgid "" -msgstr "" +msgid "Edit %s" +msgstr "Redigera %s" msgctxt "#30502" msgid "Go to %s" @@ -385,16 +384,16 @@ msgid "Add to..." msgstr "Lägg till..." msgctxt "#30521" -msgid "" -msgstr "" +msgid "Delete requests cache database" +msgstr "Ta bort cache-databasen för begäran" msgctxt "#30522" -msgid "" -msgstr "" +msgid "Clear requests cache" +msgstr "Töm cache för begäran" msgctxt "#30523" -msgid "" -msgstr "" +msgid "requests cache" +msgstr "cache för begäran" msgctxt "#30524" msgid "Select language" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "Spela nyligen tillagda" msgctxt "#30540" -msgid "Play with..." -msgstr "Spela med..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,8 +488,8 @@ msgid "Please complete all login prompts" msgstr "Fyll i alla inloggningsförfrågningar" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." -msgstr "Du kan bli ombedd att aktivera två program så att YouTube fungerar korrekt." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +msgstr "Du kan bli ombedd att logga in och aktivera åtkomst till flera applikationer så att detta tillägg kan fungera korrekt." msgctxt "#30548" msgid "" @@ -553,12 +552,12 @@ msgid "View all" msgstr "Visa alla" msgctxt "#30563" -msgid "" -msgstr "" +msgid "Plugin execution timeout" +msgstr "Tidsgräns för exekvering av tillägg" msgctxt "#30564" -msgid "" -msgstr "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "Endast för testning - tvinga tillägg att avbrytas efter en viss tidsgräns. Ställ in på 0 sekunder (standard) för att inaktivera." msgctxt "#30565" msgid "" @@ -609,8 +608,8 @@ msgid "Failed" msgstr "Misslyckades" msgctxt "#30577" -msgid "" -msgstr "" +msgid "Smallest (4:3)" +msgstr "Minsta (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Svartlista" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Lägg till i filtret Mina prenumerationer" +msgid "Hide \"Playlists\" folder" +msgstr "Dölj mappen \"Spellistor\"" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Ta bort från filtret Mina prenumerationer" +msgid "Hide \"Search\" folder" +msgstr "Dölj mappen \"Sök\"" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Lade till i filtret Mina prenumerationer" +msgid "Hide \"Shorts\" folder" +msgstr "Dölj mappen \"Shorts\"" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Borttagen från filtret Mina prenumerationer" +msgid "Hide \"Live\" folder" +msgstr "Dölj mappen \"Live\"" msgctxt "#30591" msgid "Thumbnail size" msgstr "Storlek på miniatyrbild" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Medium (16:9)" +msgid "Small (16:9)" +msgstr "Liten (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Hög (4:3)" +msgid "Medium (4:3)" +msgstr "Medel (4:3)" msgctxt "#30594" msgid "Safe search" @@ -689,20 +688,20 @@ msgid "Strict" msgstr "Strikt" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Uppdaterad: %s" +msgid "Hide \"Members only\" folder" +msgstr "Dölj mappen \"Endast medlemmar\"" msgctxt "#30598" -msgid "" -msgstr "" +msgid "Large (4:3)" +msgstr "Stor (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" msgstr "Misslyckades med att aktivera personliga API-nycklar. Saknas: %s" msgctxt "#30600" -msgid "" -msgstr "" +msgid "Largest (16:9)" +msgstr "Största (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" @@ -753,20 +752,20 @@ msgid "Retry" msgstr "Försök igen" msgctxt "#30613" -msgid "" -msgstr "" +msgid "Add to %s" +msgstr "Lägg till %s" msgctxt "#30614" -msgid "" -msgstr "" +msgid "Remove from %s" +msgstr "Ta bort från %s" msgctxt "#30615" -msgid "" -msgstr "" +msgid "Added to %s" +msgstr "Tillagd till %s" msgctxt "#30616" -msgid "" -msgstr "" +msgid "Removed from %s" +msgstr "Tog bort från %s" msgctxt "#30617" msgid "InputStream.Adaptive" @@ -785,8 +784,8 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Port %s används redan. Det går inte att starta http-servern." msgctxt "#30621" -msgid "" -msgstr "" +msgid "Verbose" +msgstr "Utförlig" msgctxt "#30622" msgid "Purchases" @@ -797,8 +796,8 @@ msgid "Install InputStream Helper" msgstr "Installera InputStream-hjälpprogram" msgctxt "#30624" -msgid "" -msgstr "" +msgid "Members only" +msgstr "Endast medlemmar" msgctxt "#30625" msgid "InputStream Helper is already installed." @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "Ta bort access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Lyssna på IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "Spelantal minsta procent" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Markera osedda" +msgid "%s removed" +msgstr "%s togs bort" msgctxt "#30670" -msgid "Mark watched" -msgstr "Markera sedda" +msgid "Added %s" +msgstr "Lade till %s" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "uppspelningshistorik" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Återställ återupptagningspunkt" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,12 +1136,12 @@ msgid "Play audio only" msgstr "Spela endast upp ljud" msgctxt "#30709" -msgid "" -msgstr "" +msgid "WebVTT subtitles" +msgstr "WebVTT-undertexter" msgctxt "#30710" -msgid "" -msgstr "" +msgid "TTML subtitles" +msgstr "TTML-undertexter" msgctxt "#30711" msgid "" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Betygsätt videor i spellistor" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Tillagd till Se senare" +msgid "Prefer dubbed audio over original audio" +msgstr "Föredra dubbat ljud framför originalljud" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Tillagd till spellista" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "Föredra automatiskt översatt dubbat ljud framför originalljud" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Borttagen från spellistan" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "Använda YouTubes interna lista för att visningshistorik?[CR][CR]Kräver inloggning via tillägget och aktivering av historikspårning på YouTube." msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "Ogillade video" msgctxt "#30718" -msgid "Rating removed" -msgstr "Betyg borttaget" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "Använda YouTubes interna lista för visa senare?[CR][CR]Kräver inloggning via tillägget." msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,8 +1184,8 @@ msgid "Unsubscribed from channel" msgstr "Avregistrerad från kanal" msgctxt "#30721" -msgid "" -msgstr "" +msgid "Enable Panoramic/180/360/VR video" +msgstr "Aktivera panoramavideo/180/360/VR" msgctxt "#30722" msgid "Enable HDR video" @@ -1253,8 +1252,8 @@ msgid "Shorts - Max duration" msgstr "Shorts - Max speltid" msgctxt "#30738" -msgid "" -msgstr "" +msgid "Enable spatial audio" +msgstr "Aktivera spatialt ljud" msgctxt "#30739" msgid "Subscribers" @@ -1533,8 +1532,8 @@ msgid "Use channel name as" msgstr "Använd kanalnamn som" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "Dölj videor från listor" +msgid "Hide items from listings" +msgstr "Dölj objekt från listning" msgctxt "#30809" msgid "All upcoming videos" @@ -1584,6 +1583,83 @@ msgctxt "#30820" msgid "Podcast" msgstr "Podradio" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Lyssna på IP" + +#~ msgctxt "#30547" +#~ msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +#~ msgstr "Du kan bli ombedd att aktivera två program så att YouTube fungerar korrekt." + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Dölj videor från listor" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Tillåt 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Spela med..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Lägg till i filtret Mina prenumerationer" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Ta bort från filtret Mina prenumerationer" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Lade till i filtret Mina prenumerationer" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Borttagen från filtret Mina prenumerationer" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Medium (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Hög (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Uppdaterad: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Markera osedda" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Markera sedda" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Återställ återupptagningspunkt" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Tillagd till Se senare" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Tillagd till spellista" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Borttagen från spellistan" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Betyg borttaget" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Inga ytterligare länkar hittades." diff --git a/plugin.video.youtube/resources/language/resource.language.szl/strings.po b/plugin.video.youtube/resources/language/resource.language.szl/strings.po index 59f7a59172..2c7de533ac 100644 --- a/plugin.video.youtube/resources/language/resource.language.szl/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.szl/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.ta_in/strings.po b/plugin.video.youtube/resources/language/resource.language.ta_in/strings.po index 62b839a924..8d792192d4 100644 --- a/plugin.video.youtube/resources/language/resource.language.ta_in/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.ta_in/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.te_in/strings.po b/plugin.video.youtube/resources/language/resource.language.te_in/strings.po index 2cafabaf3b..33444b1574 100644 --- a/plugin.video.youtube/resources/language/resource.language.te_in/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.te_in/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.tg_tj/strings.po b/plugin.video.youtube/resources/language/resource.language.tg_tj/strings.po index 29895114ea..aac15eca82 100644 --- a/plugin.video.youtube/resources/language/resource.language.tg_tj/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.tg_tj/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.th_th/strings.po b/plugin.video.youtube/resources/language/resource.language.th_th/strings.po index 2f64b7baa4..25fa9d9e7f 100644 --- a/plugin.video.youtube/resources/language/resource.language.th_th/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.th_th/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.tr_tr/strings.po b/plugin.video.youtube/resources/language/resource.language.tr_tr/strings.po index e21499e4b4..a5b91df54f 100644 --- a/plugin.video.youtube/resources/language/resource.language.tr_tr/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.tr_tr/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "144p" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "3 Boyuta izin ver" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "Daha Sonra İzle" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Çıkış Yap" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "\"%s\" ögesi kaldırılsın mı?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "Ekle..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "Birlikte oynat..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "Başarısız" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "Kara liste" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "Aboneliklerim filtresine ekle" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "Aboneliklerim filtresinden çıkar" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "Aboneliklerim filtresine eklendi" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "Aboneliklerim filtresinden çıkarıldı" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "Küçük resim büyüklüğü" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Orta (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Yüksek (4.:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "Kesin" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Güncellendi: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "Kişisel API anahtarları etkinleştirilemedi. Eksik: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "Yeniden Dene" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "%s bağlantı noktası zaten kullanılıyor.Http sunucusu başlatılamıyor." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "InputStream Helper kur" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "access_manager.json dosyasını sil" msgctxt "#30643" -msgid "Listen on IP" -msgstr "IP Üzerinden Dinle" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "İzlenme sayısını en düşük yüzdesi" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "İzlenmedi olarak işaretle" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "İzlendi olarak işaretle" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "oynatma geçmişi" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Kaldığım yeri sıfırla" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "Yalnızca ses olarak oynat" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "Oynatma listesindeki videoları değerlendir" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Daha Sonra İzle'ye eklendi" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Oynatma listesine eklendi" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Oynatma listesinden kaldırıldı" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "Video beğenilmedi" msgctxt "#30718" -msgid "Rating removed" -msgstr "Değerlendirme kaldırıldı" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "Kanalın aboneliğinden ayrılıldı" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,75 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "IP Üzerinden Dinle" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "3 Boyuta izin ver" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Birlikte oynat..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Aboneliklerim filtresine ekle" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Aboneliklerim filtresinden çıkar" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Aboneliklerim filtresine eklendi" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Aboneliklerim filtresinden çıkarıldı" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Orta (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Yüksek (4.:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Güncellendi: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "İzlenmedi olarak işaretle" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "İzlendi olarak işaretle" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Kaldığım yeri sıfırla" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Daha Sonra İzle'ye eklendi" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Oynatma listesine eklendi" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Oynatma listesinden kaldırıldı" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Değerlendirme kaldırıldı" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "Daha fazla bağlantı bulunamadı." diff --git a/plugin.video.youtube/resources/language/resource.language.uk_ua/strings.po b/plugin.video.youtube/resources/language/resource.language.uk_ua/strings.po index b18352538e..e961c591d0 100644 --- a/plugin.video.youtube/resources/language/resource.language.uk_ua/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.uk_ua/strings.po @@ -7,15 +7,15 @@ msgstr "" "Project-Id-Version: XBMC-Addons\n" "Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2024-04-12 15:14+0000\n" -"Last-Translator: Christian Gade \n" +"PO-Revision-Date: 2025-09-23 09:09+0000\n" +"Last-Translator: Pavlo Marianov \n" "Language-Team: Ukrainian \n" "Language: uk_ua\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 5.4.3\n" +"X-Generator: Weblate 5.13.3\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -27,7 +27,7 @@ msgstr "YouTube є одним з найбільших відеохостингі msgctxt "Addon Disclaimer" msgid "This plugin is not endorsed by Google" -msgstr "Цей плагін не має відношення до компанії Google" +msgstr "Цей додаток не має відношення до компанії Google" # msgctxt "Addon Summary" # msgid "Plugin for YouTube" @@ -50,16 +50,16 @@ msgstr "" msgctxt "#30003" msgid "YouTube" -msgstr "" +msgstr "YouTube" # empty strings from id 30004 to 30006 msgctxt "#30007" msgid "Use InputStream.Adaptive" -msgstr "" +msgstr "Використовувати InputStream.Adaptive" msgctxt "#30008" msgid "Configure InputStream.Adaptive" -msgstr "" +msgstr "Налаштувати InputStream.Adaptive" msgctxt "#30009" msgid "Always ask for the video quality" @@ -83,11 +83,11 @@ msgstr "1080p (FHD)" msgctxt "#30014" msgid "2160p (4K)" -msgstr "" +msgstr "2160p (4K)" msgctxt "#30015" msgid "4320p (8K)" -msgstr "" +msgstr "4320p (8K)" msgctxt "#30016" msgid "240p" @@ -99,15 +99,15 @@ msgstr "360p" msgctxt "#30018" msgid "1080p Live / 720p (HD)" -msgstr "1080p для прямих ефірів / 720p (HD)" +msgstr "1080p для трансляцій / 720p (HD)" msgctxt "#30019" msgid "144p" -msgstr "" +msgstr "144p" msgctxt "#30020" -msgid "Allow 3D" -msgstr "Дозволити 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "Увімкнути стереоскопічне 3D-відео" msgctxt "#30021" msgid "Show fanart" @@ -119,55 +119,55 @@ msgstr "Кількість елементів на сторінці" msgctxt "#30023" msgid "Search history size" -msgstr "Кількість збережених пошукових запитів" +msgstr "Розмір історії пошуку" msgctxt "#30024" msgid "Cache size (MB)" -msgstr "Розмір кешу (Мб)" +msgstr "Розмір кешу (МБ)" msgctxt "#30025" msgid "Enable Setup Wizard" -msgstr "" +msgstr "Увімкнути майстер налаштування" msgctxt "#30026" msgid "Override views" -msgstr "" +msgstr "Перевизначити подання" msgctxt "#30027" msgid "View: Default" -msgstr "Вид: Звичайний" +msgstr "Вид: звичайний" msgctxt "#30028" msgid "View: Episodes" -msgstr "Вид: Серії" +msgstr "Вид: серії" msgctxt "#30029" msgid "View: Movies" -msgstr "Вид: Фільми" +msgstr "Вид: фільми" msgctxt "#30030" msgid "Configure %s?" -msgstr "" +msgstr "Налаштувати %s?" msgctxt "#30031" -msgid "" -msgstr "" +msgid "Requests cache size (MB)" +msgstr "Розмір кешу запитів (МБ)" msgctxt "#30032" msgid "View: TV Shows" -msgstr "Вид: Серіали" +msgstr "Вид: серіали" msgctxt "#30033" msgid "View: Songs" -msgstr "Вид: Композиції" +msgstr "Вид: пісні" msgctxt "#30034" msgid "View: Artists" -msgstr "Вид: Виконавці" +msgstr "Вид: виконавці" msgctxt "#30035" msgid "View: Albums" -msgstr "Вид: Альбоми" +msgstr "Вид: альбоми" msgctxt "#30036" msgid "Support alternative player" @@ -175,21 +175,21 @@ msgstr "Підтримка альтернативного програвача" msgctxt "#30037" msgid "Custom Watch Later playlist id" -msgstr "ID плейлиста для відкладеного перегляду" +msgstr "ID списку відтворення для відкладеного перегляду" msgctxt "#30038" msgid "Custom History playlist id" -msgstr "ID плейлиста для історії" +msgstr "ID списку відтворення для історії" # Kodion Common # empty strings from id 30039 to 30099 msgctxt "#30100" msgid "Bookmarks" -msgstr "" +msgstr "Закладки" msgctxt "#30101" msgid "Bookmark" -msgstr "" +msgstr "Закладка" msgctxt "#30102" msgid "Search" @@ -205,23 +205,23 @@ msgstr "" msgctxt "#30105" msgid "filtered" -msgstr "" +msgstr "відфільтровано" msgctxt "#30106" msgid "Next page: %d" -msgstr "" +msgstr "Наступна сторінка: %d" msgctxt "#30107" msgid "Watch Later" -msgstr "Відкладений перегляд" +msgstr "Переглянути пізніше" msgctxt "#30108" -msgid "" -msgstr "" +msgid "Enter the name of the bookmark" +msgstr "Введіть назву закладки" msgctxt "#30109" -msgid "" -msgstr "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "Введіть дійсну URL-адресу YouTube або плагіну для закладки" msgctxt "#30110" msgid "New Search" @@ -229,65 +229,65 @@ msgstr "Новий пошук" msgctxt "#30111" msgid "Sign In" -msgstr "Авторизація" +msgstr "Увійти" msgctxt "#30112" msgid "Sign Out" msgstr "Вийти" msgctxt "#30113" -msgid "" -msgstr "" +msgid "Related to \"%s\"" +msgstr "Пов'язано з «%s»" msgctxt "#30114" msgid "Confirm delete" -msgstr "Підтвердити знищення" +msgstr "Підтвердити видалення" msgctxt "#30115" msgid "Confirm remove" -msgstr "Підтвердити видалення" +msgstr "Підтвердити вилучення" msgctxt "#30116" msgid "Delete \"%s\"?" -msgstr "Знищіти \"%s\"?" +msgstr "Видалити \"%s\"?" msgctxt "#30117" msgid "Remove \"%s\"?" -msgstr "Видалити \"%s\"?" +msgstr "Вилучити \"%s\"?" msgctxt "#30118" -msgid "" -msgstr "" +msgid "Links from \"%s\"" +msgstr "Посилання з «%s»" msgctxt "#30119" msgid "Please wait..." -msgstr "Будь ласка, почекайте..." +msgstr "Зачекайте..." msgctxt "#30120" msgid "Confirm clear" -msgstr "" +msgstr "Підтвердити очищення" msgctxt "#30121" msgid "Clear %s?" -msgstr "" +msgstr "Очистити %s?" # YouTube # empty strings from id 30121 to 30199 msgctxt "#30200" msgid "API" -msgstr "" +msgstr "API" msgctxt "#30201" msgid "API Key" -msgstr "API ключ" +msgstr "Ключ API" msgctxt "#30202" msgid "API Id" -msgstr "" +msgstr "Ідентифікатор API" msgctxt "#30203" msgid "API Secret" -msgstr "" +msgstr "Секрет API" msgctxt "#30204" msgid "" @@ -300,12 +300,12 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" -msgstr "" +msgid "%s failed" +msgstr "Помилка %s" msgctxt "#30501" -msgid "" -msgstr "" +msgid "Edit %s" +msgstr "Редагувати %s" msgctxt "#30502" msgid "Go to %s" @@ -313,7 +313,7 @@ msgstr "Перейти до %s" msgctxt "#30503" msgid "Channel fanart" -msgstr "" +msgstr "Фанкарт каналу" msgctxt "#30504" msgid "Subscriptions" @@ -321,11 +321,11 @@ msgstr "Підписки" msgctxt "#30505" msgid "Unsubscribe" -msgstr "Відмінити підписку" +msgstr "Відписатись" msgctxt "#30506" msgid "Subscribe" -msgstr "Підписатися" +msgstr "Підписатись" msgctxt "#30507" msgid "My Channel" @@ -333,7 +333,7 @@ msgstr "Мої канали" msgctxt "#30508" msgid "Liked Videos" -msgstr "Вподобане відео" +msgstr "Вподобані відео" msgctxt "#30509" msgid "History" @@ -353,27 +353,27 @@ msgstr "Перегляд каналів" msgctxt "#30513" msgid "Trending" -msgstr "" +msgstr "В тренді" msgctxt "#30514" msgid "Related Videos" -msgstr "Схоже відео" +msgstr "Схожі відео" msgctxt "#30515" msgid "Auto-Remove from Watch Later" -msgstr "" +msgstr "Автоматично видаляти зі списку «Переглянути пізніше»" msgctxt "#30516" msgid "Folders" -msgstr "Елементи меню" +msgstr "Теки" msgctxt "#30517" msgid "Subscribe to %s" -msgstr "Підписатися на %s" +msgstr "Підписатись на %s" msgctxt "#30518" msgid "Feeds" -msgstr "" +msgstr "Стрічки" msgctxt "#30519" msgid "and enter the following code:" @@ -384,16 +384,16 @@ msgid "Add to..." msgstr "Додати в..." msgctxt "#30521" -msgid "" -msgstr "" +msgid "Delete requests cache database" +msgstr "Видалити базу даних кешу запитів" msgctxt "#30522" -msgid "" -msgstr "" +msgid "Clear requests cache" +msgstr "Очистити кеш запитів" msgctxt "#30523" -msgid "" -msgstr "" +msgid "requests cache" +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..." @@ -417,11 +417,11 @@ msgstr "Оцінити..." msgctxt "#30529" msgid "I like this" -msgstr "Мені сподобалося" +msgstr "Подобається" msgctxt "#30530" msgid "I dislike this" -msgstr "Мені не сподобалося" +msgstr "Не подобається" msgctxt "#30531" msgid "" @@ -441,35 +441,35 @@ msgstr "" msgctxt "#30535" msgid "Select the order of the playlist" -msgstr "Порядок відтворення" +msgstr "Виберіть напрямок списку відтворення" msgctxt "#30536" msgid "Updating Playlist..." -msgstr "Оновлення плейліста..." +msgstr "Оновлення списку відтворення..." msgctxt "#30537" msgid "Play from here" -msgstr "Програти з цього місця" +msgstr "Відтворити з цього місця" msgctxt "#30538" msgid "Disliked Videos" -msgstr "Відео, яке не сподобалось" +msgstr "Відео, які не сподобались" msgctxt "#30539" msgid "Play recently added" -msgstr "" +msgstr "Відтворити останні додані" msgctxt "#30540" -msgid "Play with..." -msgstr "Відтворити за допомогою..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" -msgstr "" +msgstr "Показувати в описі назву каналу та відомості про відео" msgctxt "#30542" msgid "rtmpe streams are not supported" -msgstr "потоки rtmpe не підтримуються" +msgstr "Потоки rtmpe не підтримуються" msgctxt "#30543" msgid "" @@ -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 enable two applications so that YouTube is functioning properly." -msgstr "" +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +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 "" -msgstr "" +msgid "Plugin execution timeout" +msgstr "Час очікування виконання додатка" msgctxt "#30564" -msgid "" -msgstr "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "Тільки для тестування — примусово завершити виконання додатка після встановленого обмеження часу. Щоб відключити, вкажіть 0 секунд (за замовчуванням)." msgctxt "#30565" msgid "" @@ -569,35 +569,35 @@ msgstr "" msgctxt "#30567" msgid "Set as Watch Later" -msgstr "Додати в \\\"Переглянути пізніше\\\"" +msgstr "Додати в список «Переглянути пізніше»" msgctxt "#30568" msgid "Remove as Watch Later" -msgstr "Видалити з \\\"Переглянути пізніше\\\"" +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" -msgstr "Додати до переглянутих" +msgstr "Встановити як історію" msgctxt "#30572" msgid "Remove as History" -msgstr "Видалити з історії переглянутих" +msgstr "Видалити як історію" msgctxt "#30573" msgid "Are you sure you want to remove \"%s\" as your History list?" -msgstr "" +msgstr "Дійсно видалити «%s» як список історії?" msgctxt "#30574" msgid "Are you sure you want to replace your current History list with \"%s\"?" -msgstr "" +msgstr "Дійсно замінити поточний список історії на «%s»?" msgctxt "#30575" msgid "Succeeded" @@ -608,24 +608,24 @@ msgid "Failed" msgstr "Помилка" msgctxt "#30577" -msgid "" -msgstr "" +msgid "Smallest (4:3)" +msgstr "Найменший (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" -msgstr "" +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" -msgstr "" +msgstr "Скинути диспетчер доступу" msgctxt "#30581" msgid "Are you sure you want to reset access manager?" -msgstr "" +msgstr "Дійсно скинути диспетчер доступу?" msgctxt "#30582" msgid "Autoplay suggested videos" @@ -633,47 +633,47 @@ msgstr "Автоматично відтворювати запропонован msgctxt "#30583" msgid "Filters can be channel names separated by a comma eg. 'The Best Channel,The 2nd Best Channel', and/or custom video filters in the form '{ATTR}{OP}{VALUE}' eg. '{duration}{>=}{180}{artists_string}{=}{\"The Best Channel\"},{duration}{<}{180}'" -msgstr "" +msgstr "Фільтрами можуть бути назви каналів, відокремлені комами, наприклад «Найкращий канал,Другий найкращий канал», та/або настроювані фільтри відео у вигляді '{АТРИБУТ}{ОПЕРАТОР}{ЗНАЧЕННЯ}', наприклад '{duration}{>=}{180}{artists_string}{=}{\"найкращий канал\"},{duration}{<}{180}'" msgctxt "#30584" msgid "My Subscriptions (Filtered)" -msgstr "Підписки (відсортовані)" +msgstr "Підписки (відфільтровані)" msgctxt "#30585" msgid "Enable Blacklist to exclude channel names in Filters from My Subscriptions, disable to include channel names" -msgstr "" +msgstr "Увімкніть чорний список, щоб виключити назви каналів з фільтрів в розділі «Мої підписки». Вимкніть, щоб включити назви каналів до фільтрів" msgctxt "#30586" msgid "Blacklist" -msgstr "" +msgstr "Чорний список" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "" +msgid "Hide \"Playlists\" folder" +msgstr "Приховати теку «Списки відтворення»" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "" +msgid "Hide \"Search\" folder" +msgstr "Приховати теку «Пошук»" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "" +msgid "Hide \"Shorts\" folder" +msgstr "Приховати теку «Shorts»" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "" +msgid "Hide \"Live\" folder" +msgstr "Приховати теку «Наживо»" msgctxt "#30591" msgid "Thumbnail size" -msgstr "Розмір мініатюр" +msgstr "Розмір ескізів" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "Середній (16:9)" +msgid "Small (16:9)" +msgstr "Маленький (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "Високий (4:3)" +msgid "Medium (4:3)" +msgstr "Середній (4:3)" msgctxt "#30594" msgid "Safe search" @@ -685,39 +685,39 @@ msgstr "Помірний" msgctxt "#30596" msgid "Strict" -msgstr "Суровий" +msgstr "Суворий" msgctxt "#30597" -msgid "Updated: %s" -msgstr "Оновлено: %s" +msgid "Hide \"Members only\" folder" +msgstr "Приховати теку «Лише для спонсорів»" msgctxt "#30598" -msgid "" -msgstr "" +msgid "Large (4:3)" +msgstr "Великий (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" -msgstr "" +msgstr "Не вдалось увімкнути особисті ключі API. Відсутній: %s" msgctxt "#30600" -msgid "" -msgstr "" +msgid "Largest (16:9)" +msgstr "Найбільший (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" -msgstr "" +msgstr "%s з оригіналом/%s резерв" msgctxt "#30602" msgid "No auto-generated" -msgstr "" +msgstr "Не згенеровано автоматично" msgctxt "#30603" msgid "Age gate" -msgstr "" +msgstr "Вікове обмеження" msgctxt "#30604" msgid "Allow offensive content" -msgstr "" +msgstr "Дозволити образливий вміст" msgctxt "#30605" msgid "Quick Search" @@ -741,51 +741,51 @@ msgstr "Очистити історію перегляду" msgctxt "#30610" msgid "This will clear your account's watch history from all devices. You can't undo this." -msgstr "Ваша історія перегляду буде стерта з усіх пристроїв. Цю дію неможливо відмінити." +msgstr "Ваша історія перегляду буде видалена на усіх пристроях. Цю дію неможливо відмінити." msgctxt "#30611" msgid "Saved Playlists" -msgstr "Збережені плейлисти" +msgstr "Збережені списки відтворення" msgctxt "#30612" msgid "Retry" msgstr "Повторити" msgctxt "#30613" -msgid "" -msgstr "" +msgid "Add to %s" +msgstr "Додати до %s" msgctxt "#30614" -msgid "" -msgstr "" +msgid "Remove from %s" +msgstr "Видалити з %s" msgctxt "#30615" -msgid "" -msgstr "" +msgid "Added to %s" +msgstr "Додано до %s" msgctxt "#30616" -msgid "" -msgstr "" +msgid "Removed from %s" +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." -msgstr "Порт %s зайнято. Не вдалося запустити HTTP сервер." +msgstr "Порт %s вже використовується. Не вдалося запустити HTTP-сервер." msgctxt "#30621" -msgid "" -msgstr "" +msgid "Verbose" +msgstr "Детально" msgctxt "#30622" msgid "Purchases" @@ -796,8 +796,8 @@ msgid "Install InputStream Helper" msgstr "Встановити InputStream Helper" msgctxt "#30624" -msgid "" -msgstr "" +msgid "Members only" +msgstr "Лише для спонсорів" msgctxt "#30625" msgid "InputStream Helper is already installed." @@ -813,11 +813,11 @@ msgstr "Оцінювати відео після перегляду" msgctxt "#30628" msgid "HTTP Server" -msgstr "HTTP сервер" +msgstr "HTTP-сервер" msgctxt "#30629" msgid "IP whitelist (comma delimited)" -msgstr "" +msgstr "Біли список IP-адрес (розділені комами)" msgctxt "#30630" msgid "" @@ -829,11 +829,11 @@ msgstr "Успішно оновлено: %s" msgctxt "#30632" msgid "Enable API configuration page" -msgstr "Ввімкнути сторінку налаштування API" +msgstr "Увімкнути сторінку налаштування API" msgctxt "#30633" msgid "http://:/youtube/api (see Advanced > HTTP Server)" -msgstr "" +msgstr "http://:<порт>/youtube/api (див. Розширені > HTTP-сервер)" msgctxt "#30634" msgid "YouTube Add-on API Configuration" @@ -841,23 +841,23 @@ msgstr "Налаштування API додатку YouTube" msgctxt "#30635" msgid "No changes detected, API keys were not updated." -msgstr "Змін не виявлено, ключі API не були оновлені." +msgstr "Змін не виявлено. Ключі API не були оновлені." msgctxt "#30636" msgid "Personal API keys are enabled." -msgstr "" +msgstr "Особисті ключі API увімкнені." msgctxt "#30637" msgid "Personal API keys are disabled." -msgstr "" +msgstr "Особисті ключі API вимкнені." msgctxt "#30638" msgid "Bookmark this page to quickly add your keys in the future." -msgstr "" +msgstr "Додайте цю сторінку до закладок, щоб швидко додавати ключі в майбутньому." msgctxt "#30639" msgid "Found personal API keys in api_keys.json, would you like to restore them? Choosing no will overwrite them." -msgstr "" +msgstr "В файлі api_keys.json знайдено особисті ключі API. Бажаєте відновити їх? Якщо вибрати «Ні», вони будуть перезаписані." msgctxt "#30640" msgid "Restore" @@ -872,12 +872,12 @@ msgid "Delete access_manager.json" msgstr "Видалити access_manager.json" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" msgid "Select listen IP" -msgstr "" +msgstr "Виберіть IP-адресу для прослуховування" msgctxt "#30645" msgid "Refresh after watching" @@ -885,23 +885,23 @@ msgstr "Оновити після перегляду" msgctxt "#30646" msgid "Upcoming Live" -msgstr "Анонсовані прямі ефіри" +msgstr "Майбутні трансляції" msgctxt "#30647" msgid "Completed Live" -msgstr "Завершені прямі ефіри" +msgstr "Завершені трансляції" msgctxt "#30648" msgid "API Key is incorrect. Settings > API > API Key" -msgstr "" +msgstr "Ключ API невірний. Налаштування > API > Ключ API" msgctxt "#30649" msgid "Client Id is incorrect. Settings > API > API Id" -msgstr "" +msgstr "Ідентифікатор клієнта невірний. Налаштування > API > Ідентифікатор API" msgctxt "#30650" msgid "Client Secret is incorrect. Settings > API > API Secret" -msgstr "" +msgstr "Секрет клієнта невірний. Налаштування > API > Секрет API" msgctxt "#30651" msgid "Location" @@ -913,7 +913,7 @@ msgstr "Радіус місцезнаходження (км)" msgctxt "#30653" msgid "My Location using IP geolocation lookup" -msgstr "" +msgstr "Моє розташування згідно IP-адреси" msgctxt "#30654" msgid "My Location" @@ -921,15 +921,15 @@ msgstr "Моє місцезнаходження" msgctxt "#30655" msgid "Switch User" -msgstr "Змінити користувача" +msgstr "Переключити користувача" msgctxt "#30656" msgid "New user" -msgstr "Додати користувача" +msgstr "Новий користувач" msgctxt "#30657" msgid "Unnamed" -msgstr "" +msgstr "Без імені" msgctxt "#30658" msgid "Enter a name for this user" @@ -937,7 +937,7 @@ msgstr "Введіть ім'я користувача" msgctxt "#30659" msgid "User is now \"%s\"" -msgstr "" +msgstr "Користувач тепер «%s»" msgctxt "#30660" msgid "Users" @@ -953,35 +953,35 @@ msgstr "Видалити користувача" msgctxt "#30663" msgid "Rename a user" -msgstr "Змінити ім'я користувача" +msgstr "Перейменувати користувача" msgctxt "#30664" msgid "Switch user" -msgstr "Змінити користувача" +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" -msgstr "Мінімальний відсоток для відмічення переглянутим" +msgstr "Мінімальний відсоток, щоб помітити як переглянуте" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "Відмітити як непереглянуте" +msgid "%s removed" +msgstr "%s видалено" msgctxt "#30670" -msgid "Mark watched" -msgstr "Відмітити як переглянуте" +msgid "Added %s" +msgstr "Додано %s" msgctxt "#30671" msgid "Clear playback history" @@ -989,23 +989,23 @@ msgstr "Очистити історію відтворень" msgctxt "#30672" msgid "Delete playback history database" -msgstr "" +msgstr "Видалити базу даних історії відтворення" msgctxt "#30673" msgid "playback history" msgstr "історія відтворень" msgctxt "#30674" -msgid "Reset resume point" -msgstr "Обнулити точку відновлення" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" -msgstr "Використовувати локальну історію відтворень (переглянуті, відновлення перегляду)" +msgstr "Використовувати локальну історію відтворень (переглянуті, продовження перегляду)" msgctxt "#30676" msgid "Just now" -msgstr "" +msgstr "Негайно" msgctxt "#30677" msgid "A minute ago" @@ -1041,7 +1041,7 @@ msgstr "Сьогодні о" msgctxt "#30685" msgid "Delete data cache database" -msgstr "Видалити базу даних кешу даних" +msgstr "Видалити базу даних кешу" msgctxt "#30686" msgid "Clear data cache" @@ -1053,19 +1053,19 @@ msgstr "кеш даних" msgctxt "#30688" msgid "Use MPEG-DASH for videos" -msgstr "" +msgstr "Використовувати MPEG-DASH для відео" msgctxt "#30689" msgid "Use for live streams" -msgstr "Використовувати для прямих ефірів" +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" -msgstr "Зараз в ефірі" +msgstr "Зараз наживо" msgctxt "#30692" msgid "In a minute" @@ -1073,7 +1073,7 @@ msgstr "Через хвилину" msgctxt "#30693" msgid "Airing soon" -msgstr "Незабаром в ефірі" +msgstr "Незабаром наживо" msgctxt "#30694" msgid "In over an hour" @@ -1085,7 +1085,7 @@ msgstr "Приблизно через дві години" msgctxt "#30696" msgid "Airing today at" -msgstr "Сьогодні в ефірі о" +msgstr "Сьогодні наживо о" msgctxt "#30697" msgid "Tomorrow at" @@ -1093,23 +1093,23 @@ msgstr "Завтра о" msgctxt "#30698" msgid "Check my IP" -msgstr "Перевірити мій IP" +msgstr "Перевірити мою IP-адресу" msgctxt "#30699" msgid "HTTP server is not running" -msgstr "HTTP сервер не запущений" +msgstr "HTTP-сервер не запущений" msgctxt "#30700" msgid "Client IP is %s" -msgstr "" +msgstr "IP клієнта — %s" msgctxt "#30701" msgid "Failed to obtain client IP" -msgstr "" +msgstr "Не вдалось отримати IP-адресу клієнта" msgctxt "#30702" msgid "Play with subtitles" -msgstr "" +msgstr "Відтворити з субтитрами" msgctxt "#30703" msgid "" @@ -1117,7 +1117,7 @@ msgstr "" msgctxt "#30704" msgid "Use YouTube website urls with default player" -msgstr "" +msgstr "Використовувати для URL-адрес YouTube стандартний програвач" msgctxt "#30705" msgid "Download subtitles" @@ -1125,7 +1125,7 @@ msgstr "Завантажити субтитри" msgctxt "#30706" msgid "Download subtitles before starting playback? (Default: No)" -msgstr "Завантажувати субтитри перед відтворенням? (За замовчуванням: Ні)" +msgstr "Завантажувати субтитри перед відтворенням? (за замовчуванням — ні)" msgctxt "#30707" msgid "Untitled" @@ -1133,15 +1133,15 @@ msgstr "Без назви" msgctxt "#30708" msgid "Play audio only" -msgstr "Відтворювати тільки аудіо" +msgstr "Відтворювати тільки звук" msgctxt "#30709" -msgid "" -msgstr "" +msgid "WebVTT subtitles" +msgstr "Субтитри WebVTT" msgctxt "#30710" -msgid "" -msgstr "" +msgid "TTML subtitles" +msgstr "Субтитри TTML" msgctxt "#30711" msgid "" @@ -1149,31 +1149,31 @@ msgstr "" msgctxt "#30712" msgid "Rate videos in playlists" -msgstr "" +msgstr "Оцінювати відео у списках відтворення" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "Додано до \"Переглянути пізніше\"" +msgid "Prefer dubbed audio over original audio" +msgstr "Надавати перевагу дубльованій доріжці ніж оригінальній" msgctxt "#30714" -msgid "Added to playlist" -msgstr "Додано в плейлист" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "Надавати перевагу автоматично перекладеній звуковій доріжці ніж оригінальній" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "Видалено з плейлиста" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "Використовувати внутрішній список YouTube для історії переглядів?[CR][CR]Потрібно увійти через додаток та увімкнути відстеження історії на YouTube." msgctxt "#30716" msgid "Liked video" -msgstr "Вподобані відео" +msgstr "Відео вподобане" msgctxt "#30717" msgid "Disliked video" -msgstr "" +msgstr "Відео, що не сподобалось" msgctxt "#30718" -msgid "Rating removed" -msgstr "Оцінка видалена" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "Використовувати внутрішній список YouTube для відкладеного перегляду?[CR][CR]Потребує входу через додаток." msgctxt "#30719" msgid "Subscribed to channel" @@ -1184,48 +1184,48 @@ msgid "Unsubscribed from channel" msgstr "Відписано від каналу" msgctxt "#30721" -msgid "" -msgstr "" +msgid "Enable Panoramic/180/360/VR video" +msgstr "Увімкнути панорамне/180/360/VR відео" msgctxt "#30722" msgid "Enable HDR video" -msgstr "Ввімкнути HDR" +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 потрібен проксі-сервер (див. Розширені > HTTP-сервер)[CR]Для відео HDR та >1080p потрібен InputStream.Adaptive >= 2.3.14" msgctxt "#30724" msgid "Enable high framerate video" -msgstr "" +msgstr "Увімкнути відео з високою частотою кадрів" msgctxt "#30725" msgid "1440p (QHD)" -msgstr "" +msgstr "1440p (QHD)" msgctxt "#30726" msgid "Uploads" -msgstr "" +msgstr "Завантаження" msgctxt "#30727" msgid "Enable H.264 video" -msgstr "Ввімкнути H.264" +msgstr "Увімкнути H.264" msgctxt "#30728" msgid "Enable VP9 video" -msgstr "Ввімкнути VP9" +msgstr "Увімкнути VP9" msgctxt "#30729" msgid "Prefer lower resolution streams for unselected codecs" -msgstr "" +msgstr "Для невибраних кодеків надавати перевагу потокам з меншою роздільної здатністю" msgctxt "#30730" msgid "Play (Ask for quality)" -msgstr "Відтворити (Обрати якість)" +msgstr "Відтворити (питати про якість)" msgctxt "#30731" msgid "The YouTube add-on now requires that you use your own API keys.[CR]For more information see the wiki: [B]https://ytaddon.page.link/keys[/B][CR][CR]Sorry for the inconvenience." -msgstr "" +msgstr "Для додатка YouTube тепер потрібно вказати свої особисті ключі API.[CR]Додаткові відомості див. на wiki: [B]https://ytaddon.page.link/keys[/B][CR][CR]Просимо вибачення за незручності." msgctxt "#30732" msgid "Comments" @@ -1233,7 +1233,7 @@ msgstr "Коментарі" msgctxt "#30733" msgid "Likes" -msgstr "Лайки" +msgstr "Вподобайки" msgctxt "#30734" msgid "Replies" @@ -1241,347 +1241,423 @@ msgstr "Відповіді" msgctxt "#30735" msgid "Edited" -msgstr "" +msgstr "Змінено" msgctxt "#30736" msgid "Shorts" -msgstr "" +msgstr "Шортси" msgctxt "#30737" msgid "Shorts - Max duration" -msgstr "" +msgstr "Шортси - макс. тривалість" msgctxt "#30738" -msgid "" -msgstr "" +msgid "Enable spatial audio" +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" -msgstr "" +msgstr "Описовий" msgctxt "#30747" msgid "Alternate" -msgstr "" +msgstr "Альтернативний" msgctxt "#30748" msgid "Stream features" -msgstr "" +msgstr "Можливості потоку" msgctxt "#30749" msgid "Enable AV1 video" -msgstr "Ввімкнути AV1 кодек" +msgstr "Увімкнути кодек AV1" msgctxt "#30750" msgid "Enable Vorbis audio" -msgstr "Ввімкнути Vorbis аудіо" +msgstr "Увімкнути аудіо Vorbis" msgctxt "#30751" msgid "Enable Opus audio" -msgstr "Ввімкнути Opus аудіо" +msgstr "Увімкнути аудіо Opus" msgctxt "#30752" msgid "Enable AAC audio" -msgstr "Ввімкнути AAC аудіо" +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" -msgstr "" +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" -msgstr "" +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 кадр/с | 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 кадр/с або 1080p/60 кадр/с, 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 кадр/с, 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 кадр/с, 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 кадр/с, 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 кадр/с | 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 кадр/с або 720p/30 кадр/с, тільки 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 "Додати до закладок %s" msgctxt "#30804" msgid "Use YouTube website urls with external player" -msgstr "" +msgstr "Використовувати для URL-адрес YouTube зовнішній програвач" 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 videos from listings" -msgstr "" +msgid "Hide items from listings" +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 "Подкаст" + +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Слухати на IP-адресі" + +#~ msgctxt "#30547" +#~ msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +#~ msgstr "Вам може бути запропоновано увімкнути два застосунки для нормальної роботи YouTube." + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "Приховати відео у списках" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Дозволити 3D" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "Відтворити у..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "Додати в фільтр «Мої підписки»" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "Видалити з фільтру «Мої підписки»" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "Додано в фільтр «Мої підписки»" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "Видалено з фільтру «Мої підписки»" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "Середній (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "Високий (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "Оновлено: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "Відмітити як непереглянуте" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "Відмітити як переглянуте" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "Скинути точку продовження" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "Додано до списку «Переглянути пізніше»" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "Додано до списку відтворення" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "Видалено зі списку відтворення" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "Оцінку видалено" #~ msgctxt "#30545" #~ msgid "No further links found." diff --git a/plugin.video.youtube/resources/language/resource.language.uz_uz/strings.po b/plugin.video.youtube/resources/language/resource.language.uz_uz/strings.po index 6de73a0cbd..25f9de8eb2 100644 --- a/plugin.video.youtube/resources/language/resource.language.uz_uz/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.uz_uz/strings.po @@ -105,9 +105,8 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" +msgid "Enable Stereoscopic 3D video" msgstr "" msgctxt "#30021" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" diff --git a/plugin.video.youtube/resources/language/resource.language.vi_vn/strings.po b/plugin.video.youtube/resources/language/resource.language.vi_vn/strings.po index cfbab48d30..dce62ca502 100644 --- a/plugin.video.youtube/resources/language/resource.language.vi_vn/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.vi_vn/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "Cho phép 3D" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "Đăng xuất" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "" msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,7 +460,7 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." +msgid "" msgstr "" msgctxt "#30541" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,19 +648,19 @@ msgid "Blacklist" msgstr "" msgctxt "#30587" -msgid "Add to My Subscriptions filter" +msgid "Hide \"Playlists\" folder" msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" +msgid "Hide \"Search\" folder" msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" +msgid "Hide \"Shorts\" folder" msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" +msgid "Hide \"Live\" folder" msgstr "" msgctxt "#30591" @@ -669,11 +668,11 @@ msgid "Thumbnail size" msgstr "" msgctxt "#30592" -msgid "Medium (16:9)" +msgid "Small (16:9)" msgstr "" msgctxt "#30593" -msgid "High (4:3)" +msgid "Medium (4:3)" msgstr "" msgctxt "#30594" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "" msgctxt "#30597" -msgid "Updated: %s" +msgid "Hide \"Members only\" folder" msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,7 +872,7 @@ msgid "Delete access_manager.json" msgstr "" msgctxt "#30643" -msgid "Listen on IP" +msgid "" msgstr "" msgctxt "#30644" @@ -977,11 +976,11 @@ msgid "Play count minimum percent" msgstr "" msgctxt "#30669" -msgid "Mark unwatched" +msgid "%s removed" msgstr "" msgctxt "#30670" -msgid "Mark watched" +msgid "Added %s" msgstr "" msgctxt "#30671" @@ -997,7 +996,7 @@ msgid "playback history" msgstr "" msgctxt "#30674" -msgid "Reset resume point" +msgid "" msgstr "" msgctxt "#30675" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,15 +1152,15 @@ msgid "Rate videos in playlists" msgstr "" msgctxt "#30713" -msgid "Added to Watch Later" +msgid "Prefer dubbed audio over original audio" msgstr "" msgctxt "#30714" -msgid "Added to playlist" +msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Removed from playlist" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1173,7 +1172,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Rating removed" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." msgstr "" msgctxt "#30719" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,11 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "Cho phép 3D" + # msgctxt "Addon Summary" # msgid "Plugin for YouTube" # msgstr "" diff --git a/plugin.video.youtube/resources/language/resource.language.zh_cn/strings.po b/plugin.video.youtube/resources/language/resource.language.zh_cn/strings.po index 256e8ab314..823d904b0b 100644 --- a/plugin.video.youtube/resources/language/resource.language.zh_cn/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.zh_cn/strings.po @@ -5,9 +5,9 @@ msgid "" msgstr "" "Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2025-06-25 02:29+0000\n" +"PO-Revision-Date: 2025-11-10 00:43+0000\n" "Last-Translator: wabisabi926 \n" "Language-Team: Chinese (Simplified Han script) \n" "Language: zh_cn\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.12.2\n" +"X-Generator: Weblate 5.14.3\n" msgctxt "Addon Summary" msgid "Plugin for YouTube" @@ -38,7 +38,7 @@ msgstr "此插件未被谷歌认可" # Kodion Settings msgctxt "#30000" msgid "" -msgstr "" +msgstr "Msgctxt“插件摘要”msgid“插件YouTube”msgstr“msgctxt”插件说明“msgid”YouTube是世界上最大的视频共享网站之一。“Msgstr“”Kodion设置" msgctxt "#30001" msgid "" @@ -106,8 +106,8 @@ msgid "144p" msgstr "144p" msgctxt "#30020" -msgid "Allow 3D" -msgstr "允许 3D 播放" +msgid "Enable Stereoscopic 3D video" +msgstr "启用立体 3D 视频" msgctxt "#30021" msgid "Show fanart" @@ -150,8 +150,8 @@ msgid "Configure %s?" msgstr "配置 %s?" msgctxt "#30031" -msgid "" -msgstr "" +msgid "Requests cache size (MB)" +msgstr "请求缓存大小(MB)" msgctxt "#30032" msgid "View: TV Shows" @@ -216,12 +216,12 @@ msgid "Watch Later" msgstr "稍后观看" msgctxt "#30108" -msgid "" -msgstr "" +msgid "Enter the name of the bookmark" +msgstr "输入书签的名称" msgctxt "#30109" -msgid "" -msgstr "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" +msgstr "为书签输入有效的 YouTube 或插件 URL" msgctxt "#30110" msgid "New Search" @@ -236,7 +236,7 @@ msgid "Sign Out" msgstr "登出" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -256,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "移除 \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -300,12 +300,12 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" -msgstr "" +msgid "%s failed" +msgstr "%s 失败" msgctxt "#30501" -msgid "" -msgstr "" +msgid "Edit %s" +msgstr "编辑 %s" msgctxt "#30502" msgid "Go to %s" @@ -384,16 +384,16 @@ msgid "Add to..." msgstr "添加到..." msgctxt "#30521" -msgid "" -msgstr "" +msgid "Delete requests cache database" +msgstr "删除请求缓存数据库" msgctxt "#30522" -msgid "" -msgstr "" +msgid "Clear requests cache" +msgstr "清除请求缓存" msgctxt "#30523" -msgid "" -msgstr "" +msgid "requests cache" +msgstr "请求缓存" msgctxt "#30524" msgid "Select language" @@ -460,8 +460,8 @@ msgid "Play recently added" msgstr "最近添加的播放" msgctxt "#30540" -msgid "Play with..." -msgstr "选用其他播放器..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -488,8 +488,8 @@ msgid "Please complete all login prompts" msgstr "请完成所有登录提示" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." -msgstr "系统可能会提示您启用两个应用程序,以便 YouTube 正常运行。" +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +msgstr "系统可能会提示您登录并启用对多个应用程序的访问,以便此插件可以正常运行。" msgctxt "#30548" msgid "" @@ -552,12 +552,12 @@ msgid "View all" msgstr "查看所有" msgctxt "#30563" -msgid "" -msgstr "" +msgid "Plugin execution timeout" +msgstr "插件执行超时" msgctxt "#30564" -msgid "" -msgstr "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." +msgstr "仅测试 - 在设定的时间限制后强行终止插件执行。设置为 0 秒(默认)禁用。" msgctxt "#30565" msgid "" @@ -608,8 +608,8 @@ msgid "Failed" msgstr "失败" msgctxt "#30577" -msgid "" -msgstr "" +msgid "Smallest (4:3)" +msgstr "最小 (4:3)" msgctxt "#30578" msgid "Force SSL certificate verification" @@ -648,32 +648,32 @@ msgid "Blacklist" msgstr "黑名单" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "添加到我的订阅内容过滤器" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "从我的订阅内容过滤器移除" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "已经添加到订阅内容过滤器" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "已经从订阅内容过滤器移除" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "缩略图大小" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "中 (16:9)" +msgid "Small (16:9)" +msgstr "小 (16:9)" msgctxt "#30593" -msgid "High (4:3)" -msgstr "高 (4:3)" +msgid "Medium (4:3)" +msgstr "中 (4:3)" msgctxt "#30594" msgid "Safe search" @@ -688,20 +688,20 @@ msgid "Strict" msgstr "严格" msgctxt "#30597" -msgid "Updated: %s" -msgstr "已更新:%s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" -msgstr "" +msgid "Large (4:3)" +msgstr "大 (4:3)" msgctxt "#30599" msgid "Failed to enable personal API keys. Missing: %s" msgstr "允许私人 API 密钥失败。缺失:%s" msgctxt "#30600" -msgid "" -msgstr "" +msgid "Largest (16:9)" +msgstr "最大 (16:9)" msgctxt "#30601" msgid "%s with Original/%s fallback" @@ -752,20 +752,20 @@ msgid "Retry" msgstr "重试" msgctxt "#30613" -msgid "" -msgstr "" +msgid "Add to %s" +msgstr "添加到 %s" msgctxt "#30614" -msgid "" -msgstr "" +msgid "Remove from %s" +msgstr "从 %s 删除" msgctxt "#30615" -msgid "" -msgstr "" +msgid "Added to %s" +msgstr "已添加到 %s" msgctxt "#30616" -msgid "" -msgstr "" +msgid "Removed from %s" +msgstr "已从 %s 删除" msgctxt "#30617" msgid "InputStream.Adaptive" @@ -784,8 +784,8 @@ msgid "Port %s already in use. Cannot start http server." msgstr "端口 %s 已经被占用,无法启动 HTTP 服务。" msgctxt "#30621" -msgid "" -msgstr "" +msgid "Verbose" +msgstr "Verbose" msgctxt "#30622" msgid "Purchases" @@ -796,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "安装 InputStream Helper" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -872,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "删除 access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "侦听在 IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -976,12 +976,12 @@ msgid "Play count minimum percent" msgstr "已播放计数最少百分比" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "标记为未观看" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "标记为已观看" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -996,8 +996,8 @@ msgid "playback history" msgstr "播放历史" msgctxt "#30674" -msgid "Reset resume point" -msgstr "重置恢复点" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1136,12 +1136,12 @@ msgid "Play audio only" msgstr "只播放音频" msgctxt "#30709" -msgid "" -msgstr "" +msgid "WebVTT subtitles" +msgstr "WebVTT 字幕" msgctxt "#30710" -msgid "" -msgstr "" +msgid "TTML subtitles" +msgstr "TTML 字幕" msgctxt "#30711" msgid "" @@ -1152,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "为播放列表中的视频评级" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "添加到稍后观看" +msgid "Prefer dubbed audio over original audio" +msgstr "与原始音频相比,更喜欢配音音频" msgctxt "#30714" -msgid "Added to playlist" -msgstr "添加到播放列表" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "与原始音频相比,更喜欢自动翻译的配音音频" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "从播放列表移除" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "使用 YouTube 内部列表查看观看历史记录?[CR][CR]需要通过插件登录并激活 YouTube 上的历史记录跟踪。" msgctxt "#30716" msgid "Liked video" @@ -1172,8 +1172,8 @@ msgid "Disliked video" msgstr "踩过的视频" msgctxt "#30718" -msgid "Rating removed" -msgstr "移除评级" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "将 YouTube 内部列表用于稍后观看?[CR][CR]需要通过插件登录。" msgctxt "#30719" msgid "Subscribed to channel" @@ -1184,8 +1184,8 @@ msgid "Unsubscribed from channel" msgstr "从频道取消订阅" msgctxt "#30721" -msgid "" -msgstr "" +msgid "Enable Panoramic/180/360/VR video" +msgstr "启用全景 /180/360/VR 视频" msgctxt "#30722" msgid "Enable HDR video" @@ -1252,8 +1252,8 @@ msgid "Shorts - Max duration" msgstr "短片 - 最大持续时间" msgctxt "#30738" -msgid "" -msgstr "" +msgid "Enable spatial audio" +msgstr "启用空间音频" msgctxt "#30739" msgid "Subscribers" @@ -1532,8 +1532,8 @@ msgid "Use channel name as" msgstr "使用频道名称作为" msgctxt "#30808" -msgid "Hide videos from listings" -msgstr "隐藏列表中的视频" +msgid "Hide items from listings" +msgstr "隐藏列表中的项目" msgctxt "#30809" msgid "All upcoming videos" @@ -1583,6 +1583,82 @@ msgctxt "#30820" msgid "Podcast" msgstr "播客" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "侦听在 IP" + +#~ msgctxt "#30547" +#~ msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +#~ msgstr "系统可能会提示您启用两个应用程序,以便 YouTube 正常运行。" + +#~ msgctxt "#30808" +#~ msgid "Hide videos from listings" +#~ msgstr "隐藏列表中的视频" + +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "允许 3D 播放" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "选用其他播放器..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "添加到我的订阅内容过滤器" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "从我的订阅内容过滤器移除" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "已经添加到订阅内容过滤器" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "已经从订阅内容过滤器移除" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "中 (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "高 (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "已更新:%s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "标记为未观看" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "标记为已观看" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "重置恢复点" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "添加到稍后观看" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "添加到播放列表" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "从播放列表移除" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "移除评级" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "沒有符合此描述内容的其他链接。" diff --git a/plugin.video.youtube/resources/language/resource.language.zh_tw/strings.po b/plugin.video.youtube/resources/language/resource.language.zh_tw/strings.po index ef4b6990ea..13953c5098 100644 --- a/plugin.video.youtube/resources/language/resource.language.zh_tw/strings.po +++ b/plugin.video.youtube/resources/language/resource.language.zh_tw/strings.po @@ -105,10 +105,9 @@ msgctxt "#30019" msgid "144p" msgstr "" -# empty strings 30019 msgctxt "#30020" -msgid "Allow 3D" -msgstr "容許 3D 播放" +msgid "Enable Stereoscopic 3D video" +msgstr "" msgctxt "#30021" msgid "Show fanart" @@ -151,7 +150,7 @@ msgid "Configure %s?" msgstr "" msgctxt "#30031" -msgid "" +msgid "Requests cache size (MB)" msgstr "" msgctxt "#30032" @@ -217,11 +216,11 @@ msgid "Watch Later" msgstr "稍後觀看" msgctxt "#30108" -msgid "" +msgid "Enter the name of the bookmark" msgstr "" msgctxt "#30109" -msgid "" +msgid "Enter a valid YouTube or plugin URL for the bookmark" msgstr "" msgctxt "#30110" @@ -237,7 +236,7 @@ msgid "Sign Out" msgstr "登出" msgctxt "#30113" -msgid "" +msgid "Related to \"%s\"" msgstr "" msgctxt "#30114" @@ -257,7 +256,7 @@ msgid "Remove \"%s\"?" msgstr "搬移 \"%s\"?" msgctxt "#30118" -msgid "" +msgid "Links from \"%s\"" msgstr "" msgctxt "#30119" @@ -301,11 +300,11 @@ msgstr "" # YouTube # empty strings from id 30206 to 30499 msgctxt "#30500" -msgid "" +msgid "%s failed" msgstr "" msgctxt "#30501" -msgid "" +msgid "Edit %s" msgstr "" msgctxt "#30502" @@ -385,15 +384,15 @@ msgid "Add to..." msgstr "新增至 ..." msgctxt "#30521" -msgid "" +msgid "Delete requests cache database" msgstr "" msgctxt "#30522" -msgid "" +msgid "Clear requests cache" msgstr "" msgctxt "#30523" -msgid "" +msgid "requests cache" msgstr "" msgctxt "#30524" @@ -461,8 +460,8 @@ msgid "Play recently added" msgstr "" msgctxt "#30540" -msgid "Play with..." -msgstr "選擇其他播放器 ..." +msgid "" +msgstr "" msgctxt "#30541" msgid "Show channel name and video details in description" @@ -489,7 +488,7 @@ msgid "Please complete all login prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to enable two applications so that YouTube is functioning properly." +msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" @@ -553,11 +552,11 @@ msgid "View all" msgstr "" msgctxt "#30563" -msgid "" +msgid "Plugin execution timeout" msgstr "" msgctxt "#30564" -msgid "" +msgid "For testing only - forcibly terminate plugin execution after set time limit. Set to 0 seconds (default) to disable." msgstr "" msgctxt "#30565" @@ -609,7 +608,7 @@ msgid "Failed" msgstr "失敗" msgctxt "#30577" -msgid "" +msgid "Smallest (4:3)" msgstr "" msgctxt "#30578" @@ -649,32 +648,32 @@ msgid "Blacklist" msgstr "黑名單" msgctxt "#30587" -msgid "Add to My Subscriptions filter" -msgstr "新增到我的過濾頻道列表" +msgid "Hide \"Playlists\" folder" +msgstr "" msgctxt "#30588" -msgid "Remove from My Subscriptions filter" -msgstr "從我的過濾頻道列表移除" +msgid "Hide \"Search\" folder" +msgstr "" msgctxt "#30589" -msgid "Added to My Subscriptions filter" -msgstr "已新增到我的過濾頻道列表" +msgid "Hide \"Shorts\" folder" +msgstr "" msgctxt "#30590" -msgid "Removed from My Subscriptions filter" -msgstr "已從我的過濾頻道列表移除" +msgid "Hide \"Live\" folder" +msgstr "" msgctxt "#30591" msgid "Thumbnail size" msgstr "縮圖大小" msgctxt "#30592" -msgid "Medium (16:9)" -msgstr "中 (16:9)" +msgid "Small (16:9)" +msgstr "" msgctxt "#30593" -msgid "High (4:3)" -msgstr "大 (4:3)" +msgid "Medium (4:3)" +msgstr "" msgctxt "#30594" msgid "Safe search" @@ -689,11 +688,11 @@ msgid "Strict" msgstr "嚴格" msgctxt "#30597" -msgid "Updated: %s" -msgstr "更新: %s" +msgid "Hide \"Members only\" folder" +msgstr "" msgctxt "#30598" -msgid "" +msgid "Large (4:3)" msgstr "" msgctxt "#30599" @@ -701,7 +700,7 @@ msgid "Failed to enable personal API keys. Missing: %s" msgstr "無法啟用個人 API 金鑰,缺少: %s" msgctxt "#30600" -msgid "" +msgid "Largest (16:9)" msgstr "" msgctxt "#30601" @@ -753,19 +752,19 @@ msgid "Retry" msgstr "重試" msgctxt "#30613" -msgid "" +msgid "Add to %s" msgstr "" msgctxt "#30614" -msgid "" +msgid "Remove from %s" msgstr "" msgctxt "#30615" -msgid "" +msgid "Added to %s" msgstr "" msgctxt "#30616" -msgid "" +msgid "Removed from %s" msgstr "" msgctxt "#30617" @@ -785,7 +784,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "Port %s 已經被使用. 無法啟動 HTTP 伺服器." msgctxt "#30621" -msgid "" +msgid "Verbose" msgstr "" msgctxt "#30622" @@ -797,7 +796,7 @@ msgid "Install InputStream Helper" msgstr "安裝 InputStream Helper" msgctxt "#30624" -msgid "" +msgid "Members only" msgstr "" msgctxt "#30625" @@ -873,8 +872,8 @@ msgid "Delete access_manager.json" msgstr "刪除 access_manager.json" msgctxt "#30643" -msgid "Listen on IP" -msgstr "Listen IP" +msgid "" +msgstr "" msgctxt "#30644" msgid "Select listen IP" @@ -977,12 +976,12 @@ msgid "Play count minimum percent" msgstr "超過多少百分比記錄播放次數" msgctxt "#30669" -msgid "Mark unwatched" -msgstr "標記為未觀看" +msgid "%s removed" +msgstr "" msgctxt "#30670" -msgid "Mark watched" -msgstr "標記為已觀看" +msgid "Added %s" +msgstr "" msgctxt "#30671" msgid "Clear playback history" @@ -997,8 +996,8 @@ msgid "playback history" msgstr "播放歷史" msgctxt "#30674" -msgid "Reset resume point" -msgstr "重設繼續播放點" +msgid "" +msgstr "" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" @@ -1137,11 +1136,11 @@ msgid "Play audio only" msgstr "只播放音源" msgctxt "#30709" -msgid "" +msgid "WebVTT subtitles" msgstr "" msgctxt "#30710" -msgid "" +msgid "TTML subtitles" msgstr "" msgctxt "#30711" @@ -1153,16 +1152,16 @@ msgid "Rate videos in playlists" msgstr "評分撥放清單中的影片" msgctxt "#30713" -msgid "Added to Watch Later" -msgstr "加入至稍後觀看" +msgid "Prefer dubbed audio over original audio" +msgstr "" msgctxt "#30714" -msgid "Added to playlist" -msgstr "加入至撥放清單" +msgid "Prefer automatically translated dubbed audio over original audio" +msgstr "" msgctxt "#30715" -msgid "Removed from playlist" -msgstr "從撥放清單中移除" +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgstr "" msgctxt "#30716" msgid "Liked video" @@ -1173,8 +1172,8 @@ msgid "Disliked video" msgstr "不喜歡影片" msgctxt "#30718" -msgid "Rating removed" -msgstr "移除評分" +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgstr "" msgctxt "#30719" msgid "Subscribed to channel" @@ -1185,7 +1184,7 @@ msgid "Unsubscribed from channel" msgstr "取消訂閱頻道" msgctxt "#30721" -msgid "" +msgid "Enable Panoramic/180/360/VR video" msgstr "" msgctxt "#30722" @@ -1253,7 +1252,7 @@ msgid "Shorts - Max duration" msgstr "" msgctxt "#30738" -msgid "" +msgid "Enable spatial audio" msgstr "" msgctxt "#30739" @@ -1533,7 +1532,7 @@ msgid "Use channel name as" msgstr "" msgctxt "#30808" -msgid "Hide videos from listings" +msgid "Hide items from listings" msgstr "" msgctxt "#30809" @@ -1584,6 +1583,75 @@ msgctxt "#30820" msgid "Podcast" msgstr "" +#~ msgctxt "#30643" +#~ msgid "Listen on IP" +#~ msgstr "Listen IP" + +# empty strings 30019 +#~ msgctxt "#30020" +#~ msgid "Allow 3D" +#~ msgstr "容許 3D 播放" + +#~ msgctxt "#30540" +#~ msgid "Play with..." +#~ msgstr "選擇其他播放器 ..." + +#~ msgctxt "#30587" +#~ msgid "Add to My Subscriptions filter" +#~ msgstr "新增到我的過濾頻道列表" + +#~ msgctxt "#30588" +#~ msgid "Remove from My Subscriptions filter" +#~ msgstr "從我的過濾頻道列表移除" + +#~ msgctxt "#30589" +#~ msgid "Added to My Subscriptions filter" +#~ msgstr "已新增到我的過濾頻道列表" + +#~ msgctxt "#30590" +#~ msgid "Removed from My Subscriptions filter" +#~ msgstr "已從我的過濾頻道列表移除" + +#~ msgctxt "#30592" +#~ msgid "Medium (16:9)" +#~ msgstr "中 (16:9)" + +#~ msgctxt "#30593" +#~ msgid "High (4:3)" +#~ msgstr "大 (4:3)" + +#~ msgctxt "#30597" +#~ msgid "Updated: %s" +#~ msgstr "更新: %s" + +#~ msgctxt "#30669" +#~ msgid "Mark unwatched" +#~ msgstr "標記為未觀看" + +#~ msgctxt "#30670" +#~ msgid "Mark watched" +#~ msgstr "標記為已觀看" + +#~ msgctxt "#30674" +#~ msgid "Reset resume point" +#~ msgstr "重設繼續播放點" + +#~ msgctxt "#30713" +#~ msgid "Added to Watch Later" +#~ msgstr "加入至稍後觀看" + +#~ msgctxt "#30714" +#~ msgid "Added to playlist" +#~ msgstr "加入至撥放清單" + +#~ msgctxt "#30715" +#~ msgid "Removed from playlist" +#~ msgstr "從撥放清單中移除" + +#~ msgctxt "#30718" +#~ msgid "Rating removed" +#~ msgstr "移除評分" + #~ msgctxt "#30545" #~ msgid "No further links found." #~ msgstr "沒有此範圍的其他連結。" diff --git a/plugin.video.youtube/resources/lib/__init__.py b/plugin.video.youtube/resources/lib/__init__.py index aba1ec6a3a..c20bba8299 100644 --- a/plugin.video.youtube/resources/lib/__init__.py +++ b/plugin.video.youtube/resources/lib/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/plugin.py b/plugin.video.youtube/resources/lib/plugin.py index 290bd2f303..15c304cf2b 100644 --- a/plugin.video.youtube/resources/lib/plugin.py +++ b/plugin.video.youtube/resources/lib/plugin.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/script.py b/plugin.video.youtube/resources/lib/script.py index 477c5848ae..3138e401b1 100644 --- a/plugin.video.youtube/resources/lib/script.py +++ b/plugin.video.youtube/resources/lib/script.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2024-present plugin.video.youtube + Copyright (C) 2024-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/service.py b/plugin.video.youtube/resources/lib/service.py index a141577453..66145c3d91 100644 --- a/plugin.video.youtube/resources/lib/service.py +++ b/plugin.video.youtube/resources/lib/service.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_authentication.py b/plugin.video.youtube/resources/lib/youtube_authentication.py index e2f04c9424..7b6117c892 100644 --- a/plugin.video.youtube/resources/lib/youtube_authentication.py +++ b/plugin.video.youtube/resources/lib/youtube_authentication.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -9,6 +9,7 @@ from __future__ import absolute_import, division, unicode_literals +from youtube_plugin.kodion import logging from youtube_plugin.kodion.constants import ADDON_ID from youtube_plugin.kodion.context import XbmcContext from youtube_plugin.youtube.helper import yt_login @@ -23,27 +24,8 @@ 'sign_out', ) -_SIGN_IN = 'in' -_SIGN_OUT = 'out' - -def __add_new_developer(addon_id): - """ - - :param addon_id: id of the add-on being added - :return: - """ - context = XbmcContext(params={'addon_id': addon_id}) - - access_manager = context.get_access_manager() - developers = access_manager.get_developers() - if not developers.get(addon_id, None): - developers[addon_id] = access_manager.get_new_developer() - access_manager.set_developers(developers) - context.log_debug('Creating developer user: |%s|' % addon_id) - - -def __auth(addon_id, mode=_SIGN_IN): +def _auth(addon_id, mode=yt_login.SIGN_IN): """ :param addon_id: id of the add-on being signed in @@ -51,38 +33,35 @@ def __auth(addon_id, mode=_SIGN_IN): :return: addon provider, context and client """ if not addon_id or addon_id == ADDON_ID: - context = XbmcContext() - context.log_error('Developer authentication: |%s| Invalid addon_id' % addon_id) - return - __add_new_developer(addon_id) + logging.error_trace('Invalid addon_id: %r', addon_id) + return False + provider = Provider() context = XbmcContext(params={'addon_id': addon_id}) - _ = provider.get_client(context=context) - logged_in = provider.is_logged_in() - if mode == _SIGN_IN: - if logged_in: - return True - else: - provider.reset_client() - yt_login.process(mode, provider, context, sign_out_refresh=False) - elif mode == _SIGN_OUT: - if not logged_in: - return True - else: - provider.reset_client() - try: - yt_login.process(mode, provider, context, sign_out_refresh=False) - except LoginException: - reset_access_tokens(addon_id) - else: - raise Exception('Unknown mode: |%s|' % mode) - - _ = provider.get_client(context=context) - if mode == _SIGN_IN: - return provider.is_logged_in() - else: - return not provider.is_logged_in() + access_manager = context.get_access_manager() + if access_manager.add_new_developer(addon_id): + logging.debug('Creating developer user: %r', addon_id) + + client = provider.get_client(context=context) + + if mode == yt_login.SIGN_IN: + if client.logged_in: + yt_login.process(yt_login.SIGN_OUT, + provider, + context, + client=client, + refresh=False) + client = None + elif mode != yt_login.SIGN_OUT: + raise Exception('Unknown mode: %r' % mode) + + yt_login.process(mode, provider, context, client=client, refresh=False) + + logged_in = provider.get_client(context=context).logged_in + if mode == yt_login.SIGN_IN: + return logged_in + return not logged_in def sign_in(addon_id): @@ -120,7 +99,7 @@ def sign_in(addon_id): :return: boolean, True when signed in """ - return __auth(addon_id, mode=_SIGN_IN) + return _auth(addon_id, mode=yt_login.SIGN_IN) def sign_out(addon_id): @@ -151,7 +130,7 @@ def sign_out(addon_id): :return: boolean, True when signed out """ - return __auth(addon_id, mode=_SIGN_OUT) + return _auth(addon_id, mode=yt_login.SIGN_OUT) def reset_access_tokens(addon_id): @@ -161,10 +140,9 @@ def reset_access_tokens(addon_id): :return: """ if not addon_id or addon_id == ADDON_ID: - context = XbmcContext() - context.log_error('Reset addon access tokens - invalid addon_id: |{0}|' - .format(addon_id)) + logging.error_trace('Invalid addon_id: %r', addon_id) return + context = XbmcContext(params={'addon_id': addon_id}) context.get_access_manager().update_access_token( addon_id, access_token='', expiry=-1, refresh_token='' diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/__init__.py index 4502c7696e..a34543cd33 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -17,6 +17,11 @@ 'client_id': 'ODYxNTU2NzA4NDU0LWQ2ZGxtM2xoMDVpZGQ4bnBlazE4azZiZThiYTNvYzY4', 'client_secret': 'U2JvVmhvRzlzMHJOYWZpeENTR0dLWEFU', }, + 'youtube-vr': { + 'api_key': '', + 'client_id': 'NjUyNDY5MzEyMTY5LTRsdnM5Ym5ocjlscG5zOXY0NTFqNW9pdmQ4MXZqdnUx', + 'client_secret': 'M2ZUV3JCSkk1VW9qbTFUSzdfaUpDVzVa', + }, 'provided': { '0': { 'api_key': '', diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/__init__.py index 9b3fdb3047..03ff54b971 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/abstract_provider.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/abstract_provider.py index 55edac3765..d5a43064a1 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,15 +11,16 @@ from __future__ import absolute_import, division, unicode_literals from re import ( - UNICODE as re_UNICODE, + UNICODE, compile as re_compile, ) +from . import logging from .constants import ( CHECK_SETTINGS, - CONTAINER_ID, - CONTAINER_POSITION, CONTENT, + FOLDER_URI, + ITEMS_PER_PAGE, PATHS, REROUTE_PATH, WINDOW_CACHE, @@ -27,6 +28,7 @@ WINDOW_REPLACE, WINDOW_RETURN, ) +from .debug import ExecTimeout from .exceptions import KodionException from .items import ( DirectoryItem, @@ -35,14 +37,19 @@ SearchHistoryItem, UriItem, ) -from .utils import format_stack, to_unicode +from .utils.convert_format import to_unicode class AbstractProvider(object): + log = logging.getLogger(__name__) + CACHE_TO_DISC = 'provider_cache_to_disc' # type: bool FALLBACK = 'provider_fallback' # type: bool | str FORCE_PLAY = 'provider_force_play' # type: bool + FORCE_REFRESH = 'provider_force_refresh' # type: bool FORCE_RESOLVE = 'provider_force_resolve' # type: bool + FORCE_RETURN = 'provider_force_return' # type: bool + POST_RUN = 'provider_post_run' # type: bool UPDATE_LISTING = 'provider_update_listing' # type: bool CONTENT_TYPE = 'provider_content_type' # type: tuple[str, str, str] @@ -78,13 +85,13 @@ def __init__(self): self.register_path(r''.join(( '^', PATHS.WATCH_LATER, - '/(?Padd|clear|list|play|remove)/?$' + '/(?Padd|clear|list|play|remove)?/?$' )), self.on_watch_later) self.register_path(r''.join(( '^', PATHS.BOOKMARKS, - '/(?Padd|clear|list|play|remove)/?$' + '/(?Padd|add_custom|clear|edit|list|play|remove)?/?$' )), self.on_bookmarks) self.register_path(r''.join(( @@ -96,7 +103,7 @@ def __init__(self): self.register_path(r''.join(( '^', PATHS.HISTORY, - '/(?Pclear|list|mark_unwatched|mark_watched|play|remove|reset_resume)/?$' + '/(?Pclear|list|mark_as|mark_unwatched|mark_watched|play|remove|reset_resume)?/?$' )), self.on_playback_history) self.register_path(r'(?P.*\/)extrafanart\/([\?#].+)?$', @@ -119,7 +126,7 @@ def wrapper(command): if not callable(func): return None - cls._dict_path[re_compile(re_path, re_UNICODE)] = func + cls._dict_path[re_compile(re_path, UNICODE)] = func return command if command: @@ -132,7 +139,7 @@ def run_wizard(self, context, last_run=None): ui = context.get_ui() settings_state = {'state': 'defer'} - context.wakeup(CHECK_SETTINGS, timeout=5, payload=settings_state) + context.ipc_exec(CHECK_SETTINGS, timeout=5, payload=settings_state) if last_run and last_run > 1: self.pre_run_wizard_step(provider=self, context=context) @@ -144,8 +151,8 @@ def run_wizard(self, context, last_run=None): try: if wizard_steps and ui.on_yes_no_input( ' - '.join((localize('youtube'), localize('setup_wizard'))), - (localize('setup_wizard.prompt') - % localize('setup_wizard.prompt.settings')) + localize(('setup_wizard.prompt.x', + 'setup_wizard.prompt.settings')), ): for wizard_step in wizard_steps: if callable(wizard_step): @@ -159,7 +166,7 @@ def run_wizard(self, context, last_run=None): settings = context.get_settings(refresh=True) settings.setup_wizard_enabled(False) settings_state['state'] = 'process' - context.wakeup(CHECK_SETTINGS, timeout=5, payload=settings_state) + context.ipc_exec(CHECK_SETTINGS, timeout=5, payload=settings_state) @staticmethod def get_wizard_steps(): @@ -178,6 +185,17 @@ def navigate(self, context): if not re_match: continue + exec_limit = context.get_settings().exec_limit() + if exec_limit: + handler = ExecTimeout( + seconds=exec_limit, + # log_only=True, + # trace_opcodes=True, + # trace_threads=True, + log_locals=(-15, None), + callback=None, + )(handler) + options = { self.CACHE_TO_DISC: True, self.UPDATE_LISTING: False, @@ -238,19 +256,17 @@ def on_goto_page(provider, context, re_match): params = context.get_params() if 'page_token' in params: page_token = NextPageItem.create_page_token( - page, params.get('items_per_page', 50) + page, params.get(ITEMS_PER_PAGE, 50) ) else: page_token = '' - if 'exclude' in params: - del params['exclude'] + for param in NextPageItem.JUMP_PAGE_PARAM_EXCLUSIONS: + if param in params: + del params[param] params = dict(params, page=page, page_token=page_token) if (not ui.busy_dialog_active() - and context.is_plugin_path( - context.get_infolabel('Container.FolderPath'), - partial=True, - )): + and ui.get_container_info(FOLDER_URI)): return provider.reroute(context=context, path=path, params=params) return provider.navigate(context.clone(path, params)) @@ -263,8 +279,10 @@ def on_reroute(provider, context, re_match): ) def reroute(self, context, path=None, params=None, uri=None): - container_uri = context.get_infolabel('Container.FolderPath') - current_path, current_params = context.parse_uri(container_uri) + ui = context.get_ui() + current_path, current_params = context.parse_uri( + ui.get_container_info(FOLDER_URI, container_id=None) + ) if uri is None: if path is None: @@ -278,7 +296,7 @@ def reroute(self, context, path=None, params=None, uri=None): path, params = uri if not path: - context.log_error('Rerouting - No route path') + self.log.error_trace('No route path') return False elif path.startswith(PATHS.ROUTE): path = path[len(PATHS.ROUTE):] @@ -289,101 +307,84 @@ def reroute(self, context, path=None, params=None, uri=None): window_return = params.pop(WINDOW_RETURN, True) if window_fallback: - container_uri = context.get_infolabel('Container.FolderPath') - if context.is_plugin_path(container_uri): - context.log_debug('Rerouting - Fallback route not required') + if ui.get_container_info(FOLDER_URI): + self.log.debug('Rerouting - Fallback route not required') return False, {self.FALLBACK: False} - container = None - position = None - refresh = params.get('refresh', 0) + refresh = context.refresh_requested(params=params) if (refresh or ( params == current_params and path.rstrip('/') == current_path.rstrip('/') )): - if refresh < 0: + if refresh and refresh < 0: del params['refresh'] else: - container = context.get_infolabel('System.CurrentControlId') - position = context.get_infolabel('Container.CurrentItem') params['refresh'] = context.refresh_requested( force=True, on=True, params=params, ) + else: + params['refresh'] = 0 - ui = context.get_ui() result = None - try: - if window_cache: - function_cache = context.get_function_cache() - with ui.on_busy(): - result, options = function_cache.run( - self.navigate, - _refresh=True, - _scope=function_cache.SCOPE_NONE, - context=context.clone(path, params), - ) - except Exception as exc: - context.log_error('Rerouting - Error' - '\n\tException: {exc!r}' - '\n\tStack trace (most recent call last):\n{stack}' - .format(exc=exc, - stack=format_stack())) - finally: - uri = context.create_uri(path, params) - if result or not window_cache: - context.log_debug('Rerouting - Success' - '\n\tURI: {uri}' - '\n\tCache: |{window_cache}|' - '\n\tFallback: |{window_fallback}|' - '\n\tReplace: |{window_replace}|' - '\n\tReturn: |{window_return}|' - .format(uri=uri, - window_cache=window_cache, - window_fallback=window_fallback, - window_replace=window_replace, - window_return=window_return)) - else: - context.log_debug('Rerouting - No results' - '\n\tURI: {uri}' - .format(uri=uri)) + uri = context.create_uri(path, params) + if window_cache: + function_cache = context.get_function_cache() + with ui.on_busy(): + result, options = function_cache.run( + self.navigate, + _refresh=True, + _scope=function_cache.SCOPE_NONE, + context=context.clone(path, params), + ) + if not result: + self.log.debug(('No results', 'URI: %s'), uri) return False - reroute_path = ui.get_property(REROUTE_PATH) - if reroute_path: - return True - - if window_cache: - ui.set_property(REROUTE_PATH, path) - if container and position: - ui.set_property(CONTAINER_ID, container) - ui.set_property(CONTAINER_POSITION, position) - - action = ''.join(( - 'ReplaceWindow' if window_replace else 'ActivateWindow', - '(Videos,', - uri, - ',return)' if window_return else ')', - )) - - timeout = 30 - while ui.busy_dialog_active(): - timeout -= 1 - if timeout < 0: - context.log_warning('Multiple busy dialogs active' - ' - Rerouting workaround') - return UriItem('command://{0}'.format(action)) - context.sleep(1) - else: - context.execute( - action, - # wait=True, - # wait_for=(REROUTE_PATH if window_cache else None), - # wait_for_set=False, - # block_ui=True, - ) - return True + self.log.debug(('Success', + 'URI: {uri}', + 'Cache: {window_cache!r}', + 'Fallback: {window_fallback!r}', + 'Replace: {window_replace!r}', + 'Return: {window_return!r}'), + uri=uri, + window_cache=window_cache, + window_fallback=window_fallback, + window_replace=window_replace, + window_return=window_return) + + reroute_path = ui.get_property(REROUTE_PATH) + if reroute_path: + return True + + if window_cache: + ui.set_property(REROUTE_PATH, path) + + action = ''.join(( + 'ReplaceWindow' if window_replace else 'ActivateWindow', + '(Videos,', + uri, + ',return)' if window_return else ')', + )) + + timeout = 30 + while ui.busy_dialog_active(): + timeout -= 1 + if timeout < 0: + self.log.warning('Multiple busy dialogs active' + ' - Rerouting workaround') + return UriItem('command://{0}'.format(action)) + context.sleep(0.1) + else: + context.execute( + action, + # wait=True, + # wait_for=(REROUTE_PATH if window_cache else None), + # wait_for_set=False, + # block_ui=True, + ) + return True @staticmethod def on_bookmarks(provider, context, re_match): @@ -424,47 +425,40 @@ def on_search(provider, context, re_match): query = to_unicode(params.get('q', '')) if not ui.on_yes_no_input( localize('content.remove'), - localize('content.remove.check') % query, + localize('content.remove.check.x', query), ): return False, None search_history.del_item(query) - ui.refresh_container() - - ui.show_notification( - localize('removed') % query, - time_ms=2500, - audible=False, - ) - return True, None + ui.show_notification(localize('removed.name.x', query), + time_ms=2500, + audible=False) + return True, {provider.FORCE_REFRESH: True} if command == 'rename': query = to_unicode(params.get('q', '')) result, new_query = ui.on_keyboard_input( localize('search.rename'), query ) - if result: - search_history.del_item(query) - search_history.add_item(new_query) - ui.refresh_container() - return True, None + if not result: + return False, None + + search_history.del_item(query) + search_history.add_item(new_query) + return True, {provider.FORCE_REFRESH: True} if command == 'clear': if not ui.on_yes_no_input( localize('search.clear'), - localize('content.clear.check') % localize('search.history') + localize(('content.clear.check.x', 'search.history')) ): return False, None search_history.clear() - ui.refresh_container() - - ui.show_notification( - localize('completed'), - time_ms=2500, - audible=False, - ) - return True, None + ui.show_notification(localize('completed'), + time_ms=2500, + audible=False) + return True, {provider.FORCE_REFRESH: True} if command == 'links': return provider.on_specials_x( @@ -474,77 +468,29 @@ def on_search(provider, context, re_match): ) if command.startswith('input'): - query = None - # came from page 1 of search query by '..'/back - # user doesn't want to input on this path - fallback = True - old_path, old_params = context.parse_uri( - context.get_infolabel('Container.FolderPath') + result, query = ui.on_keyboard_input( + localize('search.title') ) - old_uri = context.create_uri(old_path, old_params) - if (not context.refresh_requested() - and context.is_plugin_folder() - and context.is_plugin_path(old_uri, - PATHS.SEARCH, - partial=True)): - - query = old_params.get('q') - if not query: - fallback = ui.pop_property(provider.FALLBACK) - if fallback: - history_blacklist = ( - context.create_path(PATHS.SEARCH, 'input'), - context.create_path(PATHS.SEARCH, 'query'), - context.create_path(PATHS.SEARCH, 'list'), - ) - else: - fallback = old_uri - history_blacklist = ( - context.create_path(PATHS.SEARCH, 'input'), - context.create_path(PATHS.SEARCH, 'query'), - ) - if old_path.startswith(history_blacklist): - query = False - - if query: - query = to_unicode(query) - elif query is None: - result, input_query = ui.on_keyboard_input( - localize('search.title') - ) - if result: - query = input_query - - if query: - # Race conditions with other addons creating busy dialogs can - # prevent opening a new window - # fallback = old_uri - # ui.set_property(provider.RESULT_FALLBACK, fallback) - # return UriItem(context.create_uri( - # (PATHS.SEARCH, 'query'), - # dict(params, q=query), - # window={'replace': False, 'return': True}, - # )), {provider.RESULT_FALLBACK: False} - - # Alternate method is faster/smoother but means that history is - # not properly modified to prevent navigating back to input - # dialog - context.set_params(q=query) - context.set_path(PATHS.SEARCH, 'query') - result, options = provider.on_search_run(context, query=query) - if not options: - options = {provider.CACHE_TO_DISC: False} - fallback = options.setdefault( - provider.FALLBACK, - context.get_uri() if result else old_uri, - ) - if fallback: - ui.set_property(provider.FALLBACK, fallback) + if result and query: + result = [] + options = { + provider.FALLBACK: context.create_uri( + (PATHS.SEARCH, 'query'), + dict(params, q=query, category_label=query), + window={ + 'replace': False, + 'return': True, + }, + ), + provider.FORCE_RETURN: True, + provider.POST_RUN: True, + provider.CACHE_TO_DISC: True, + provider.UPDATE_LISTING: False, + } else: result = False options = { - provider.CACHE_TO_DISC: False, - provider.FALLBACK: fallback, + provider.FALLBACK: True, } return result, options diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/compatibility/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/compatibility/__init__.py index e2c2c66de4..2ad0a112e3 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/compatibility/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/compatibility/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -9,14 +9,17 @@ __all__ = ( 'BaseHTTPRequestHandler', + 'StringIO', 'TCPServer', 'ThreadingMixIn', 'available_cpu_count', 'byte_string_type', 'datetime_infolabel', 'entity_escape', + 'generate_hash', 'parse_qs', 'parse_qsl', + 'pickle', 'quote', 'quote_plus', 'range_type', @@ -38,8 +41,11 @@ # Kodi v19+ and Python v3.x try: + import _pickle as pickle + from hashlib import md5 from html import unescape from http.server import BaseHTTPRequestHandler + from io import StringIO from socketserver import TCPServer, ThreadingMixIn from urllib.parse import ( parse_qs, @@ -81,11 +87,44 @@ def entity_escape(text, })): return text.translate(entities) + + def generate_hash(*args, **kwargs): + return md5(''.join( + map(str, args or kwargs.get('iter')) + ).encode('utf-8')).hexdigest() + + + SAFE_CHARS = frozenset( + b'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + b'abcdefghijklmnopqrstuvwxyz' + b'0123456789' + b'_.-~' + b'/' # safe character by default + ) + reserved = { + chr(ordinal): '%%%x' % ordinal + for ordinal in range(0, 128) + if ordinal not in SAFE_CHARS + } + reserved_plus = reserved.copy() + reserved_plus.update(( + ('/', '%2f'), + (' ', '+'), + )) + reserved = str.maketrans(reserved) + reserved_plus = str.maketrans(reserved_plus) + non_ascii = str.maketrans({ + chr(ordinal): '%%%x' % ordinal + for ordinal in range(128, 256) + }) + # Compatibility shims for Kodi v18 and Python v2.7 except ImportError: + import cPickle as pickle + from hashlib import md5 from BaseHTTPServer import BaseHTTPRequestHandler - from contextlib import contextmanager from SocketServer import TCPServer, ThreadingMixIn + from StringIO import StringIO as _StringIO from urllib import ( quote as _quote, quote_plus as _quote_plus, @@ -130,6 +169,11 @@ def unquote_plus(data): def urlencode(data, *args, **kwargs): if isinstance(data, dict): data = data.items() + kwargs = { + key: value + for key, value in kwargs.viewitems() + if key in {'query', 'doseq'} + } return _urlencode({ to_str(key): ( [to_str(part) for part in value] @@ -140,21 +184,23 @@ def urlencode(data, *args, **kwargs): }, *args, **kwargs) - _File = xbmcvfs.File + class StringIO(_StringIO): + def __enter__(self): + return self + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() - @contextmanager - def _file_closer(*args, **kwargs): - file = None - try: - file = _File(*args, **kwargs) - yield file - finally: - if file: - file.close() + + class File(xbmcvfs.File): + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() - xbmcvfs.File = _file_closer + xbmcvfs.File = File xbmcvfs.translatePath = xbmc.translatePath range_type = (xrange, list) @@ -163,10 +209,12 @@ def _file_closer(*args, **kwargs): string_type = basestring - def to_str(value): + def to_str(value, _format='{0!s}'.format): + if not isinstance(value, basestring): + value = _format(value) if isinstance(value, unicode): - return value.encode('utf-8') - return str(value) + value = value.encode('utf-8') + return value def entity_escape(text, @@ -181,6 +229,19 @@ def entity_escape(text, text = text.replace(key, value) return text + + def generate_hash(*args, **kwargs): + return md5(''.join( + map(to_str, args or kwargs.get('iter')) + )).hexdigest() + + + def _loads(string, _loads=pickle.loads): + return _loads(to_str(string)) + + + pickle.loads = _loads + # Kodi v20+ if hasattr(xbmcgui.ListItem, 'setDateTime'): def datetime_infolabel(datetime_obj, *_args, **_kwargs): @@ -190,15 +251,14 @@ def datetime_infolabel(datetime_obj, *_args, **_kwargs): def datetime_infolabel(datetime_obj, str_format='%Y-%m-%d %H:%M:%S'): return datetime_obj.strftime(str_format) - -_cpu_count = _sched_get_affinity = None try: - from os import sched_getaffinity as _sched_getaffinity + from os import sched_getaffinity as _sched_get_affinity except ImportError: + _sched_get_affinity = None try: from multiprocessing import cpu_count as _cpu_count except ImportError: - pass + _cpu_count = None def available_cpu_count(): diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/__init__.py index d28c627d19..b2810694cb 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -32,7 +32,7 @@ TEMP_PATH = 'special://temp/' + ADDON_ID # Const values -VALUE_FROM_STR = { +BOOL_FROM_STR = { '0': False, '1': True, 'false': False, @@ -41,59 +41,113 @@ 'True': True, 'None': None, 'null': None, + '': None, } +VALUE_TO_STR = { + False: 'false', + True: 'true', + None: '', + -1: '', + 0: 'false', + 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' + +# Container Info +CURRENT_CONTAINER_INFO = 'Container.%s' +PLUGIN_CONTAINER_INFO = 'Container(%s).%s' +CURRENT_ITEM = 'CurrentItem' +FOLDER_NAME = 'FolderName' +FOLDER_URI = 'FolderPath' +HAS_FILES = 'HasFiles' +HAS_FOLDERS = 'HasFolders' +HAS_PARENT = 'HasParent' +SCROLLING = 'Scrolling' +UPDATING = 'IsUpdating' + +# ListItem Info +CONTAINER_LISTITEM_INFO = 'Container(%s).ListItem(0).%s' +LISTITEM_INFO = 'ListItem.%s' +ARTIST = 'Artist' +LABEL = 'Label' +PLAY_COUNT = 'PlayCount' +RESUMABLE = 'IsResumable' +TITLE = 'Title' +URI = 'FileNameAndPath' # ListItem Properties +CONTAINER_LISTITEM_PROP = 'Container(%s).ListItem(0).Property(%s)' +LISTITEM_PROP = 'ListItem.Property(%s)' +BOOKMARK_ID = 'bookmark_id' CHANNEL_ID = 'channel_id' -PLAY_COUNT = 'video_play_count' +PLAY_COUNT_PROP = 'video_play_count' PLAYLIST_ID = 'playlist_id' -PLAYLISTITEM_ID = 'playlistitem_id' +PLAYLIST_ITEM_ID = 'playlist_item_id' SUBSCRIPTION_ID = 'subscription_id' VIDEO_ID = 'video_id' # Events CHECK_SETTINGS = 'check_settings' +CONTEXT_MENU = 'cxm_action' +FILE_READ = 'file_read' +FILE_WRITE = 'file_write' +KEYMAP = 'key_action' PLAYBACK_INIT = 'playback_init' PLAYBACK_FAILED = 'playback_failed' PLAYBACK_STARTED = 'playback_started' PLAYBACK_STOPPED = 'playback_stopped' REFRESH_CONTAINER = 'refresh_container' RELOAD_ACCESS_MANAGER = 'reload_access_manager' +SERVICE_IPC = 'service_ipc' +SYNC_LISTITEM = 'sync_listitem' # Sleep/wakeup states PLUGIN_WAKEUP = 'plugin_wakeup' PLUGIN_SLEEPING = 'plugin_sleeping' SERVER_WAKEUP = 'server_wakeup' -WAKEUP = 'wakeup' # Play options +PLAY_CANCELLED = 'play_cancelled' PLAY_FORCE_AUDIO = 'audio_only' PLAY_FORCED = 'play_forced' PLAY_PROMPT_QUALITY = 'ask_for_quality' PLAY_PROMPT_SUBTITLES = 'prompt_for_subtitles' PLAY_STRM = 'strm' PLAY_TIMESHIFT = 'timeshift' -PLAY_WITH = 'play_with' +PLAY_USING = 'play_using' FORCE_PLAY_PARAMS = frozenset(( PLAY_FORCE_AUDIO, PLAY_TIMESHIFT, PLAY_PROMPT_QUALITY, PLAY_PROMPT_SUBTITLES, - PLAY_WITH, + PLAY_USING, )) # Stored data +PROPERTY = 'Window(home).Property(%s-%%s)' % ADDON_ID +PROPERTY_AS_LABEL = '$INFO[Window(home).Property(%s-%%s)]' % ADDON_ID CONTAINER_ID = 'container_id' CONTAINER_FOCUS = 'container_focus' CONTAINER_POSITION = 'container_position' DEVELOPER_CONFIGS = 'configs' LICENSE_TOKEN = 'license_token' LICENSE_URL = 'license_url' +MARK_AS_LABEL = 'mark_as_label' PLAYER_DATA = 'player_json' PLAYER_VIDEO_ID = 'player_video_id' PLAYLIST_PATH = 'playlist_path' @@ -106,6 +160,59 @@ WINDOW_REPLACE = 'window_replace' WINDOW_RETURN = 'window_return' +# Plugin url query parameters +ACTION = 'action' +ADDON_ID_PARAM = 'addon_id' +CHANNEL_IDS = 'channel_ids' +CLIP = 'clip' +END = 'end' +FANART_TYPE = 'fanart_type' +HIDE_CHANNELS = 'hide_channels' +HIDE_FOLDERS = 'hide_folders' +HIDE_LIVE = 'hide_live' +HIDE_MEMBERS = 'hide_members' +HIDE_NEXT_PAGE = 'hide_next_page' +HIDE_PLAYLISTS = 'hide_playlists' +HIDE_PROGRESS = 'hide_progress' +HIDE_SEARCH = 'hide_search' +HIDE_SHORTS = 'hide_shorts' +HIDE_VIDEOS = 'hide_videos' +INCOGNITO = 'incognito' +ITEM_FILTER = 'item_filter' +ITEMS_PER_PAGE = 'items_per_page' +LIVE = 'live' +ORDER = 'order' +PAGE = 'page' +PLAYLIST_IDS = 'playlist_ids' +SCREENSAVER = 'screensaver' +SEEK = 'seek' +START = 'start' +VIDEO_IDS = 'video_ids' + +INHERITED_PARAMS = frozenset(( + ADDON_ID_PARAM, + FANART_TYPE, + HIDE_CHANNELS, + HIDE_FOLDERS, + HIDE_LIVE, + HIDE_MEMBERS, + HIDE_NEXT_PAGE, + HIDE_PLAYLISTS, + HIDE_PROGRESS, + HIDE_SEARCH, + HIDE_SHORTS, + HIDE_VIDEOS, + INCOGNITO, + ITEM_FILTER, + ITEMS_PER_PAGE, + PLAY_FORCE_AUDIO, + PLAY_TIMESHIFT, + PLAY_PROMPT_QUALITY, + PLAY_PROMPT_SUBTITLES, + PLAY_USING, +)) + + __all__ = ( # Addon paths 'ADDON_ID', @@ -116,53 +223,91 @@ 'TEMP_PATH', # Const values - 'VALUE_FROM_STR', + 'BOOL_FROM_STR', + 'VALUE_TO_STR', + 'YOUTUBE_HOSTNAMES', # Flags 'ABORT_FLAG', 'BUSY_FLAG', + 'SERVICE_RUNNING_FLAG', + 'TRAKT_PAUSE_FLAG', 'WAIT_END_FLAG', - # ListItem properties - 'CHANNEL_ID', + # Container Info + 'CURRENT_CONTAINER_INFO', + 'PLUGIN_CONTAINER_INFO', + 'CURRENT_ITEM', + 'FOLDER_NAME', + 'FOLDER_URI', + 'HAS_FILES', + 'HAS_FOLDERS', + 'HAS_PARENT', + 'SCROLLING', + 'UPDATING', + + # ListItem Info + 'CONTAINER_LISTITEM_INFO', + 'LISTITEM_INFO', + 'ARTIST', + 'LABEL', 'PLAY_COUNT', + 'RESUMABLE', + 'TITLE', + 'URI', + + # ListItem Properties + 'CONTAINER_LISTITEM_PROP', + 'LISTITEM_PROP', + 'BOOKMARK_ID', + 'CHANNEL_ID', + 'PLAY_COUNT_PROP', 'PLAYLIST_ID', - 'PLAYLISTITEM_ID', + 'PLAYLIST_ITEM_ID', 'SUBSCRIPTION_ID', 'VIDEO_ID', # Events 'CHECK_SETTINGS', + 'CONTEXT_MENU', + 'FILE_READ', + 'FILE_WRITE', + 'KEYMAP', 'PLAYBACK_INIT', 'PLAYBACK_FAILED', 'PLAYBACK_STARTED', 'PLAYBACK_STOPPED', 'REFRESH_CONTAINER', 'RELOAD_ACCESS_MANAGER', + 'SERVICE_IPC', + 'SYNC_LISTITEM', # Sleep/wakeup states 'PLUGIN_SLEEPING', 'PLUGIN_WAKEUP', 'SERVER_WAKEUP', - 'WAKEUP', # Play options + 'PLAY_CANCELLED', 'PLAY_FORCE_AUDIO', 'PLAY_FORCED', 'PLAY_PROMPT_QUALITY', 'PLAY_PROMPT_SUBTITLES', 'PLAY_STRM', 'PLAY_TIMESHIFT', - 'PLAY_WITH', + 'PLAY_USING', 'FORCE_PLAY_PARAMS', # Stored data + 'PROPERTY', + 'PROPERTY_AS_LABEL', 'CONTAINER_ID', 'CONTAINER_FOCUS', 'CONTAINER_POSITION', 'DEVELOPER_CONFIGS', 'LICENSE_TOKEN', 'LICENSE_URL', + 'MARK_AS_LABEL', 'PLAYER_DATA', 'PLAYER_VIDEO_ID', 'PLAYLIST_PATH', @@ -175,6 +320,37 @@ 'WINDOW_REPLACE', 'WINDOW_RETURN', + # Plugin url query parameters + 'ACTION', + 'ADDON_ID_PARAM', + 'CHANNEL_IDS', + 'CLIP', + 'END', + 'FANART_TYPE', + 'HIDE_CHANNELS', + 'HIDE_FOLDERS', + 'HIDE_LIVE', + 'HIDE_MEMBERS', + 'HIDE_NEXT_PAGE', + 'HIDE_PLAYLISTS', + 'HIDE_PROGRESS', + 'HIDE_SEARCH', + 'HIDE_SHORTS', + 'HIDE_VIDEOS', + 'INCOGNITO', + 'ITEM_FILTER', + 'ITEMS_PER_PAGE', + 'LIVE', + 'ORDER', + 'PAGE', + 'PLAYLIST_IDS', + 'SCREENSAVER', + 'SEEK', + 'START', + 'VIDEO_IDS', + + 'INHERITED_PARAMS', + # Other constants 'CONTENT', 'PATHS', diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_content_types.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_content_types.py index 6235d08e7f..d0d9fc3351 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_content_types.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_content_types.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,9 +10,14 @@ from __future__ import absolute_import, division, unicode_literals + VIDEO_CONTENT = 'videos' LIST_CONTENT = 'files' +COMMENTS = 'comments' +HISTORY = 'history' +PLAYLIST = 'playlist' + AUDIO_TYPE = 'music' VIDEO_TYPE = 'video' diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_lang_region.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_lang_region.py index d045b78f61..a842eb1f31 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_lang_region.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_lang_region.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2024 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_paths.py index 3e760b7582..0164be4393 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,42 +10,51 @@ from __future__ import absolute_import, division, unicode_literals - -BOOKMARKS = '/kodion/bookmarks' -COMMAND = '/kodion/command' +INTERNAL = '/kodion' +BOOKMARKS = INTERNAL + '/bookmarks' +COMMAND = INTERNAL + '/command' EXTERNAL_SEARCH = '/search' -GOTO_PAGE = '/kodion/goto_page' -ROUTE = '/kodion/route' -SEARCH = '/kodion/search' -WATCH_LATER = '/kodion/watch_later' -HISTORY = '/kodion/playback_history' +GOTO_PAGE = INTERNAL + '/goto_page' +ROUTE = INTERNAL + '/route' +SEARCH = INTERNAL + '/search' +WATCH_LATER = INTERNAL + '/watch_later' +HISTORY = INTERNAL + '/playback_history' CHANNEL = '/channel' -DESCRIPTION_LINKS = '/special/description_links' -DISLIKED_VIDEOS = '/special/disliked_videos' +MY_CHANNEL = CHANNEL + '/mine' +LIKED_VIDEOS = CHANNEL + '/mine/playlist/LL' +MY_PLAYLIST = CHANNEL + '/mine/playlist' +MY_PLAYLISTS = CHANNEL + '/mine/playlists' + HOME = '/home' -LIKED_VIDEOS = '/channel/mine/playlist/LL' -LIVE_VIDEOS = '/special/live' -LIVE_VIDEOS_COMPLETED = '/special/completed_live' -LIVE_VIDEOS_UPCOMING = '/special/upcoming_live' -MY_PLAYLISTS = '/channel/mine/playlists' -MY_SUBSCRIPTIONS = '/special/my_subscriptions' -MY_SUBSCRIPTIONS_FILTERED = '/special/my_subscriptions_filtered' +MAINTENANCE = '/maintenance' PLAY = '/play' PLAYLIST = '/playlist' -RECOMMENDATIONS = '/special/recommendations' -RELATED_VIDEOS = '/special/related_videos' -SAVED_PLAYLISTS = '/special/saved_playlists' -SUBSCRIPTIONS = '/subscriptions/list' -TRENDING = '/special/popular_right_now' -VIDEO_COMMENTS = '/special/parent_comments' -VIDEO_COMMENTS_THREAD = '/special/child_comments' - -API = '/youtube/api' -API_SUBMIT = '/youtube/api/submit' -DRM = '/youtube/widevine' -IP = '/youtube/client_ip' -MPD = '/youtube/manifest/dash' -PING = '/youtube/ping' -REDIRECT = '/youtube/redirect' -STREAM_PROXY = '/youtube/stream' +SUBSCRIPTIONS = '/subscriptions' +VIDEO = '/video' + +SPECIAL = '/special' +DESCRIPTION_LINKS = SPECIAL + '/description_links' +DISLIKED_VIDEOS = SPECIAL + '/disliked_videos' +LIVE_VIDEOS = SPECIAL + '/live' +LIVE_VIDEOS_COMPLETED = SPECIAL + '/completed_live' +LIVE_VIDEOS_UPCOMING = SPECIAL + '/upcoming_live' +MY_SUBSCRIPTIONS = SPECIAL + '/my_subscriptions' +MY_SUBSCRIPTIONS_FILTERED = SPECIAL + '/my_subscriptions_filtered' +RECOMMENDATIONS = SPECIAL + '/recommendations' +RELATED_VIDEOS = SPECIAL + '/related_videos' +SAVED_PLAYLISTS = SPECIAL + '/saved_playlists' +TRENDING = SPECIAL + '/popular_right_now' +VIDEO_COMMENTS = SPECIAL + '/parent_comments' +VIDEO_COMMENTS_THREAD = SPECIAL + '/child_comments' +VIRTUAL_PLAYLIST = SPECIAL + '/playlist' + +HTTP_SERVER = '/youtube' +API = HTTP_SERVER + '/api' +API_SUBMIT = HTTP_SERVER + '/api/submit' +DRM = HTTP_SERVER + '/widevine' +IP = HTTP_SERVER + '/client_ip' +MPD = HTTP_SERVER + '/manifest/dash' +PING = HTTP_SERVER + '/ping' +REDIRECT = HTTP_SERVER + '/redirect' +STREAM_PROXY = HTTP_SERVER + '/stream' diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 775de5f1a7..90f1c14bb7 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -45,6 +45,34 @@ API_SECRET = 'youtube.api.secret' # (str) ALLOW_DEV_KEYS = 'youtube.allow.dev.keys' # (bool) +SHOW_SIGN_IN = 'youtube.folder.sign.in.show' # (bool) +SHOW_MY_SUBSCRIPTIONS = 'youtube.folder.my_subscriptions.show' # (bool) +SHOW_MY_SUBSCRIPTIONS_FILTERED = 'youtube.folder.my_subscriptions_filtered.show' # (bool) +SHOW_RECOMMENDATIONS = 'youtube.folder.recommendations.show' # (bool) +SHOW_RELATED = 'youtube.folder.related.show' # (bool) +SHOW_TRENDING = 'youtube.folder.popular_right_now.show' # (bool) +SHOW_SEARCH = 'youtube.folder.search.show' # (bool) +SHOW_QUICK_SEARCH = 'youtube.folder.quick_search.show' # (bool) +SHOW_INCOGNITO_SEARCH = 'youtube.folder.quick_search_incognito.show' # (bool) +SHOW_MY_LOCATION = 'youtube.folder.my_location.show' # (bool) +SHOW_MY_CHANNEL = 'youtube.folder.my_channel.show' # (bool) +SHOW_WATCH_LATER = 'youtube.folder.watch_later.show' # (bool) +SHOW_LIKED = 'youtube.folder.liked_videos.show' # (bool) +SHOW_DISLIKED = 'youtube.folder.disliked_videos.show' # (bool) +SHOW_HISTORY = 'youtube.folder.history.show' # (bool) +SHOW_PLAYLISTS = 'youtube.folder.playlists.show' # (bool) +SHOW_SAVED_PLAYLISTS = 'youtube.folder.saved.playlists.show' # (bool) +SHOW_SUBSCRIPTIONS = 'youtube.folder.subscriptions.show' # (bool) +SHOW_BOOKMARKS = 'youtube.folder.bookmarks.show' # (bool) +SHOW_BROWSE_CHANNELS = 'youtube.folder.browse_channels.show' # (bool) +SHOW_COMPlETED_LIVE = 'youtube.folder.completed.live.show' # (bool) +SHOW_UPCOMING_LIVE = 'youtube.folder.upcoming.live.show' # (bool) +SHOW_LIVE = 'youtube.folder.live.show' # (bool) +SHOW_SWITCH_USER = 'youtube.folder.switch.user.show' # (bool) +SHOW_SIGN_OUT = 'youtube.folder.sign.out.show' # (bool) +SHOW_SETUP_WIZARD = 'youtube.folder.settings.show' # (bool) +SHOW_SETTINGS = 'youtube.folder.settings.advanced.show' # (bool) + WATCH_LATER_PLAYLIST = 'youtube.folder.watch_later.playlist' # (str) HISTORY_PLAYLIST = 'youtube.folder.history.playlist' # (str) @@ -78,11 +106,20 @@ LOCATION = 'youtube.location' # (str) LOCATION_RADIUS = 'youtube.location.radius' # (int) +PLAY_SUGGESTED = 'youtube.suggested_videos' # (bool) + PLAY_COUNT_MIN_PERCENT = 'kodion.play_count.percent' # (int) +RATE_VIDEOS = 'youtube.post.play.rate' # (bool) +RATE_PLAYLISTS = 'youtube.post.play.rate.playlists' # (bool) +PLAY_REFRESH = 'youtube.post.play.refresh' # (bool) + +WATCH_LATER_REMOVE = 'youtube.playlist.watchlater.autoremove' # (bool) + VERIFY_SSL = 'requests.ssl.verify' # (bool) CONNECT_TIMEOUT = 'requests.timeout.connect' # (int) READ_TIMEOUT = 'requests.timeout.read' # (int) +REQUESTS_CACHE_SIZE = 'requests.cache.size' # (int) PROXY_SOURCE = 'requests.proxy.source' # (int) PROXY_ENABLED = 'requests.proxy.enabled' # (bool) @@ -98,4 +135,5 @@ HTTPD_IDLE_SLEEP = 'youtube.http.idle_sleep' # (bool) HTTPD_STREAM_REDIRECT = 'youtube.http.stream_redirect' # (bool) -LOGGING_ENABLED = 'kodion.logging.level' # (int) +LOG_LEVEL = 'kodion.debug.log.level' # (int) +EXEC_LIMIT = 'kodion.debug.exec.limit' # (int) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py index 8b2f6437d2..11b2a1cac8 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,62 +12,178 @@ import sys -from ..compatibility import xbmcplugin - - -xbmcplugin = xbmcplugin.__dict__ -namespace = sys.modules[__name__] -names = [ - 'NONE', # 0 - 'LABEL', # 1 - 'LABEL_IGNORE_THE', # 2 - 'DATE', # 3 - 'SIZE', # 4 - 'FILE', # 5 - 'DRIVE_TYPE', # 6 - 'TRACKNUM', # 7 - 'DURATION', # 8 - 'TITLE', # 9 - 'TITLE_IGNORE_THE', # 10 - 'ARTIST', # 11 - 'ARTIST_IGNORE_THE', # 13 - 'ALBUM', # 14 - 'ALBUM_IGNORE_THE', # 15 - 'GENRE', # 16 - 'COUNTRY', # 17 - 'VIDEO_YEAR', # 18 - 'VIDEO_RATING', # 19 - 'VIDEO_USER_RATING', # 20 - 'DATEADDED', # 21 - 'PROGRAM_COUNT', # 22 - 'PLAYLIST_ORDER', # 23 - 'EPISODE', # 24 - 'VIDEO_TITLE', # 25 - 'VIDEO_SORT_TITLE', # 26 - 'VIDEO_SORT_TITLE_IGNORE_THE', # 27 - 'PRODUCTIONCODE', # 28 - 'SONG_RATING', # 29 - 'SONG_USER_RATING', # 30 - 'MPAA_RATING', # 31 - 'VIDEO_RUNTIME', # 32 - 'STUDIO', # 33 - 'STUDIO_IGNORE_THE', # 34 - 'FULLPATH', # 35 - 'LABEL_IGNORE_FOLDERS', # 36 - 'LASTPLAYED', # 37 - 'PLAYCOUNT', # 38 - 'LISTENERS', # 39 - 'UNSORTED', # 40 - 'CHANNEL', # 41 - 'BITRATE', # 43 - 'DATE_TAKEN', # 44 - 'VIDEO_ORIGINAL_TITLE', # 49 - 'VIDEO_ORIGINAL_TITLE_IGNORE_THE', # 50 +from . import const_content_types as CONTENT +from ..compatibility import ( + xbmcplugin, +) + + +# Sort methods exposed via xbmcplugin vary by Kodi version. +# Rather than try to access them directly they are hardcoded here and checked +# against what is defined in xbmcplugin. +# IDs of sort methods exposed via xbmcplugin are defined in SortMethod +# https://github.com/xbmc/xbmc/blob/master/xbmc/SortFileItem.h +# IDs of sort methods used by the Kodi GUI and builtins are defined in SortBy +# https://github.com/xbmc/xbmc/blob/master/xbmc/utils/SortUtils.h +# The IDs don't match... +# As a workaround they are mapped here using the localised label used in the GUI +# to allow the sort methods to be set by the addon and then changed dynamically +# using infolabels and builtins +methods = [ + # Sort method name, Label ID, SortBy ID + ('UNSORTED', 571, 0), + ('NONE', 16018, 0), + ('LABEL', 551, 1), + ('LABEL_IGNORE_THE', 551, None), + ('DATE', 552, 2), + ('SIZE', 553, 3), + ('FILE', 561, 4), + ('FULLPATH', 573, 5), + ('DRIVE_TYPE', 564, 6), + ('TITLE', 556, 7), + ('TITLE_IGNORE_THE', 556, None), + ('TRACKNUM', 554, 8), + ('DURATION', 180, 9), + ('ARTIST', 557, 10), + ('ARTIST_IGNORE_THE', 557, None), + ('ALBUM', 558, 12), + ('ALBUM_IGNORE_THE', 558, None), + ('GENRE', 515, 14), + ('COUNTRY', 574, 15), + ('VIDEO_YEAR', 562, 16), + ('VIDEO_RATING', 563, 17), + ('VIDEO_USER_RATING', 38018, 18), + ('PROGRAM_COUNT', 567, 21), + ('PLAYLIST_ORDER', 559, 22), + ('EPISODE', 20359, 23), + ('DATEADDED', 570, 40), + ('VIDEO_TITLE', 556, 7), + ('VIDEO_SORT_TITLE', 171, 29), + ('VIDEO_SORT_TITLE_IGNORE_THE', 171, None), + ('VIDEO_RUNTIME', 180, 9), + ('PRODUCTIONCODE', 20368, 30), + ('SONG_RATING', 563, 17), + ('SONG_USER_RATING', 38018, 18), + ('MPAA_RATING', 20074, 31), + ('STUDIO', 572, 39), + ('STUDIO_IGNORE_THE', 572, None), + ('LABEL_IGNORE_FOLDERS', 551, None), + ('LASTPLAYED', 568, 41), + ('PLAYCOUNT', 567, 42), + ('LISTENERS', 20455, 43), + ('BITRATE', 623, 44), + ('CHANNEL', 19029, 46), + ('DATE_TAKEN', 577, 48), + ('VIDEO_ORIGINAL_TITLE', 20376, 57), + ('VIDEO_ORIGINAL_TITLE_IGNORE_THE', 20376, None), ] -for name in names: - fullname = 'SORT_METHOD_' + name - setattr(namespace, name, - xbmcplugin[fullname] if fullname in xbmcplugin else -1) +SORT = sys.modules[__name__] +name = label_id = sort_by = sort_method = None +for name, label_id, sort_by in methods: + sort_method = getattr(xbmcplugin, 'SORT_METHOD_' + name, 0) + setattr(SORT, name, sort_method) + +# Label mask token details: +# https://github.com/xbmc/xbmc/blob/master/xbmc/utils/LabelFormatter.cpp#L33-L105 + +# SORT.TRACKNUM is always included after SORT.UNSORTED to allow for +# manual/automatic setting of default sort method + +LIST_CONTENT_DETAILED = ( + (SORT.UNSORTED, '%T \u2022 %P', '%D | %J'), + (SORT.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'), + (SORT.LABEL, '%T \u2022 %P', '%D | %J'), +) +LIST_CONTENT_SIMPLE = ( + (SORT.UNSORTED, '%T', '%D'), + (SORT.TRACKNUM, '[%N. ]%T', '%D'), + (SORT.LABEL, '%T', '%D'), +) + +PLAYLIST_CONTENT_DETAILED = ( + (SORT.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'), + (SORT.LABEL, '[%N. ]%T \u2022 %P', '%D | %J'), + (SORT.CHANNEL, '[%N. ][%A - ]%T \u2022 %P', '%D | %J'), + (SORT.ARTIST, '[%N. ]%T \u2022 %P | %D | %J', '%A'), + (SORT.PROGRAM_COUNT, '[%N. ]%T \u2022 %P | %D | %J', '%C'), + (SORT.VIDEO_RATING, '[%N. ]%T \u2022 %P | %D | %J', '%R'), + (SORT.DATE, '[%N. ]%T \u2022 %P | %D', '%J'), + (SORT.DATEADDED, '[%N. ]%T \u2022 %P | %D', '%a'), + (SORT.VIDEO_RUNTIME, '[%N. ]%T \u2022 %P | %J', '%D'), +) +PLAYLIST_CONTENT_SIMPLE = ( + (SORT.TRACKNUM, '[%N. ]%T', '%D'), + (SORT.LABEL, '[%N. ]%T', '%D'), + (SORT.CHANNEL, '[%N. ][%A - ]%T', '%D'), + (SORT.ARTIST, '[%N. ]%T', '%A'), + (SORT.PROGRAM_COUNT, '[%N. ]%T', '%C'), + (SORT.VIDEO_RATING, '[%N. ]%T', '%R'), + (SORT.DATE, '[%N. ]%T', '%J'), + (SORT.DATEADDED, '[%N. ]%T', '%a'), + (SORT.VIDEO_RUNTIME, '[%N. ]%T', '%D'), +) + +VIDEO_CONTENT_DETAILED = ( + (SORT.UNSORTED, '%T \u2022 %P', '%D | %J'), + (SORT.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'), + (SORT.LABEL, '%T \u2022 %P', '%D | %J'), + (SORT.CHANNEL, '[%A - ]%T \u2022 %P', '%D | %J'), + (SORT.ARTIST, '%T \u2022 %P | %D | %J', '%A'), + (SORT.PROGRAM_COUNT, '%T \u2022 %P | %D | %J', '%C'), + (SORT.VIDEO_RATING, '%T \u2022 %P | %D | %J', '%R'), + (SORT.DATE, '%T \u2022 %P | %D', '%J'), + (SORT.DATEADDED, '%T \u2022 %P | %D', '%a'), + (SORT.VIDEO_RUNTIME, '%T \u2022 %P | %J', '%D'), +) +VIDEO_CONTENT_SIMPLE = ( + (SORT.UNSORTED, '%T', '%D'), + (SORT.TRACKNUM, '[%N. ]%T', '%D'), + (SORT.LABEL, '%T', '%D'), + (SORT.CHANNEL, '[%A - ]%T', '%D'), + (SORT.ARTIST, '%T', '%A'), + (SORT.PROGRAM_COUNT, '%T', '%C'), + (SORT.VIDEO_RATING, '%T', '%R'), + (SORT.DATE, '%T', '%J'), + (SORT.DATEADDED, '%T', '%a'), + (SORT.VIDEO_RUNTIME, '%T', '%D'), +) + +HISTORY_CONTENT_DETAILED = ( + (SORT.LASTPLAYED, '%T \u2022 %P | %J', '%D | %p'), + (SORT.PLAYCOUNT, '%T \u2022 %P | %J', '%D | %V'), +) +HISTORY_CONTENT_DETAILED += VIDEO_CONTENT_DETAILED + +HISTORY_CONTENT_SIMPLE = ( + (SORT.LASTPLAYED, '%T', '%D | %p'), + (SORT.PLAYCOUNT, '%T', '%D | %V'), +) +HISTORY_CONTENT_SIMPLE += VIDEO_CONTENT_SIMPLE + +COMMENTS_CONTENT_DETAILED = ( + (SORT.CHANNEL, '[%A - ]%P \u2022 %T', '%J'), + (SORT.TRACKNUM, '[%N. ][%A - ]%P \u2022 %T', '%J'), + (SORT.ARTIST, '[%J - ]%P \u2022 %T', '%A'), + (SORT.PROGRAM_COUNT, '[%A - ]%P | %J \u2022 %T', '%C'), + (SORT.DATE, '[%A - ]%P \u2022 %T', '%J'), +) +COMMENTS_CONTENT_SIMPLE = ( + (SORT.CHANNEL, '[%A - ]%T', '%J'), + (SORT.TRACKNUM, '[%N. ][%A - ]%T', '%J'), + (SORT.ARTIST, '[%A - ]%T', '%A'), + (SORT.PROGRAM_COUNT, '[%A - ]%T', '%C'), + (SORT.DATE, '[%A - ]%T', '%J'), +) -del sys, xbmcplugin, namespace, names, name, fullname +del ( + sys, + CONTENT, + xbmcplugin, + methods, + SORT, + name, + label_id, + sort_by, + sort_method, +) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/__init__.py index 30a9869807..2174fd1420 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 9c9124fd8e..f23f2292c8 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,7 +12,7 @@ import os -from ..logger import Logger +from .. import logging from ..compatibility import ( parse_qsl, quote, @@ -23,96 +23,136 @@ urlsplit, ) from ..constants import ( + ACTION, + ADDON_ID_PARAM, + BOOL_FROM_STR, + CHANNEL_ID, + CHANNEL_IDS, + CLIP, + CONTEXT_MENU, + END, + FANART_TYPE, + HIDE_CHANNELS, + HIDE_FOLDERS, + HIDE_LIVE, + HIDE_MEMBERS, + HIDE_NEXT_PAGE, + HIDE_PLAYLISTS, + HIDE_PROGRESS, + HIDE_SEARCH, + HIDE_SHORTS, + HIDE_VIDEOS, + INCOGNITO, + ITEMS_PER_PAGE, + ITEM_FILTER, + KEYMAP, + LIVE, + ORDER, + PAGE, PATHS, + PLAYLIST_ID, + PLAYLIST_IDS, + PLAYLIST_ITEM_ID, PLAY_FORCE_AUDIO, PLAY_PROMPT_QUALITY, PLAY_PROMPT_SUBTITLES, PLAY_STRM, PLAY_TIMESHIFT, - PLAY_WITH, - VALUE_FROM_STR, + PLAY_USING, + SCREENSAVER, + SEEK, + START, + SUBSCRIPTION_ID, + VIDEO_ID, + VIDEO_IDS, WINDOW_CACHE, WINDOW_FALLBACK, WINDOW_REPLACE, WINDOW_RETURN, ) -from ..json_store import AccessManager from ..sql_store import ( BookmarksList, DataCache, FeedHistory, FunctionCache, PlaybackHistory, + RequestCache, SearchHistory, WatchLaterList, ) -from ..utils import current_system_version +from ..utils.system_version import current_system_version -class AbstractContext(Logger): +class AbstractContext(object): + log = logging.getLogger(__name__) + _initialized = False _addon = None _settings = None - _BOOL_PARAMS = { + _BOOL_PARAMS = frozenset(( + CONTEXT_MENU, + KEYMAP, PLAY_FORCE_AUDIO, PLAY_PROMPT_SUBTITLES, PLAY_PROMPT_QUALITY, PLAY_STRM, PLAY_TIMESHIFT, - PLAY_WITH, + PLAY_USING, 'confirmed', - 'clip', + CLIP, 'enable', - 'hide_folders', - 'hide_live', - 'hide_next_page', - 'hide_playlists', - 'hide_progress', - 'hide_search', - 'hide_shorts', - 'hide_videos', - 'incognito', + HIDE_CHANNELS, + HIDE_FOLDERS, + HIDE_LIVE, + HIDE_MEMBERS, + HIDE_NEXT_PAGE, + HIDE_PLAYLISTS, + HIDE_PROGRESS, + HIDE_SEARCH, + HIDE_SHORTS, + HIDE_VIDEOS, + INCOGNITO, 'location', 'logged_in', 'resume', - 'screensaver', + SCREENSAVER, WINDOW_CACHE, WINDOW_FALLBACK, WINDOW_REPLACE, WINDOW_RETURN, - } - _INT_PARAMS = { - 'fanart_type', + )) + _INT_PARAMS = frozenset(( + FANART_TYPE, 'filtered', - 'items_per_page', - 'live', + ITEMS_PER_PAGE, + LIVE, 'next_page_token', - 'page', + PAGE, 'refresh', - } - _INT_BOOL_PARAMS = { + )) + _INT_BOOL_PARAMS = frozenset(( 'refresh', - } - _FLOAT_PARAMS = { - 'end', + )) + _FLOAT_PARAMS = frozenset(( + END, 'recent_days', - 'seek', - 'start', - } - _LIST_PARAMS = { - 'channel_ids', + SEEK, + START, + )) + _LIST_PARAMS = frozenset(( + CHANNEL_IDS, 'exclude', - 'item_filter', - 'playlist_ids', - 'video_ids', - } - _STRING_PARAMS = { + ITEM_FILTER, + PLAYLIST_IDS, + VIDEO_IDS, + )) + _STRING_PARAMS = frozenset(( 'api_key', - 'action', - 'addon_id', + ACTION, + ADDON_ID_PARAM, 'category_label', - 'channel_id', + CHANNEL_ID, 'client_id', 'client_secret', 'click_tracking', @@ -120,35 +160,41 @@ class AbstractContext(Logger): 'item', 'item_id', 'item_name', - 'order', + ORDER, 'page_token', 'parent_id', 'playlist', # deprecated - 'playlist_id', + PLAYLIST_ITEM_ID, + PLAYLIST_ID, 'q', 'rating', 'reload_path', 'search_type', - 'subscription_id', + SUBSCRIPTION_ID, 'uri', 'videoid', # deprecated - 'video_id', + VIDEO_ID, 'visitor', - } - _STRING_BOOL_PARAMS = { + )) + _STRING_BOOL_PARAMS = frozenset(( + 'logged_in', 'reload_path', - } + )) + _STRING_INT_PARAMS = frozenset(( + )) _NON_EMPTY_STRING_PARAMS = set() def __init__(self, path='/', params=None, plugin_id=''): self._access_manager = None self._uuid = None + self._api_store = None self._bookmarks_list = None self._data_cache = None self._feed_history = None self._function_cache = None self._playback_history = None + self._requests_cache = None self._search_history = None self._watch_later_list = None @@ -158,11 +204,13 @@ def __init__(self, path='/', params=None, plugin_id=''): self._plugin_icon = None self._version = 'UNKNOWN' + self._param_string = '' self._params = params or {} - self.parse_params(self._params) + if params: + self.parse_params(params) self._uri = None - self._path = path + self._path = None self._path_parts = [] self.set_path(path, force=True) @@ -192,67 +240,94 @@ def get_region(self): def get_playback_history(self): uuid = self.get_uuid() - if not self._playback_history or self._playback_history.uuid != uuid: + playback_history = self._playback_history + if not playback_history or playback_history.uuid != uuid: filepath = (self.get_data_path(), uuid, 'history.sqlite') - self._playback_history = PlaybackHistory(filepath) - return self._playback_history + playback_history = PlaybackHistory(filepath) + self._playback_history = playback_history + return playback_history def get_feed_history(self): uuid = self.get_uuid() - if not self._feed_history or self._feed_history.uuid != uuid: + feed_history = self._feed_history + if not feed_history or feed_history.uuid != uuid: filepath = (self.get_data_path(), uuid, 'feeds.sqlite') - self._feed_history = FeedHistory(filepath) - return self._feed_history + feed_history = FeedHistory(filepath) + self._feed_history = feed_history + return feed_history def get_data_cache(self): uuid = self.get_uuid() - if not self._data_cache or self._data_cache.uuid != uuid: + data_cache = self._data_cache + if not data_cache or data_cache.uuid != uuid: filepath = (self.get_data_path(), uuid, 'data_cache.sqlite') - self._data_cache = DataCache( + data_cache = DataCache( filepath, max_file_size_mb=self.get_settings().cache_size() / 2, ) - return self._data_cache + self._data_cache = data_cache + return data_cache def get_function_cache(self): uuid = self.get_uuid() - if not self._function_cache or self._function_cache.uuid != uuid: + function_cache = self._function_cache + if not function_cache or function_cache.uuid != uuid: filepath = (self.get_data_path(), uuid, 'cache.sqlite') - self._function_cache = FunctionCache( + function_cache = FunctionCache( filepath, max_file_size_mb=self.get_settings().cache_size() / 2, ) - return self._function_cache + self._function_cache = function_cache + return function_cache + + def get_requests_cache(self): + uuid = self.get_uuid() + requests_cache = self._requests_cache + if not requests_cache or requests_cache.uuid != uuid: + filepath = (self.get_data_path(), uuid, 'requests_cache.sqlite') + requests_cache = RequestCache( + filepath, + max_file_size_mb=self.get_settings().requests_cache_size(), + ) + self._requests_cache = requests_cache + return requests_cache def get_search_history(self): uuid = self.get_uuid() - if not self._search_history or self._search_history.uuid != uuid: + search_history = self._search_history + if not search_history or search_history.uuid != uuid: filepath = (self.get_data_path(), uuid, 'search.sqlite') - self._search_history = SearchHistory( + search_history = SearchHistory( filepath, max_item_count=self.get_settings().get_search_history_size(), ) - return self._search_history + self._search_history = search_history + return search_history def get_bookmarks_list(self): uuid = self.get_uuid() - if not self._bookmarks_list or self._bookmarks_list.uuid != uuid: + bookmarks_list = self._bookmarks_list + if not bookmarks_list or bookmarks_list.uuid != uuid: filepath = (self.get_data_path(), uuid, 'bookmarks.sqlite') - self._bookmarks_list = BookmarksList(filepath) - return self._bookmarks_list + bookmarks_list = BookmarksList(filepath) + self._bookmarks_list = bookmarks_list + return bookmarks_list def get_watch_later_list(self): uuid = self.get_uuid() - if not self._watch_later_list or self._watch_later_list.uuid != uuid: + watch_later_list = self._watch_later_list + if not watch_later_list or watch_later_list.uuid != uuid: filepath = (self.get_data_path(), uuid, 'watch_later.sqlite') - self._watch_later_list = WatchLaterList(filepath) - return self._watch_later_list + watch_later_list = WatchLaterList(filepath) + self._watch_later_list = watch_later_list + return watch_later_list def get_uuid(self): uuid = self._uuid - if uuid: - return uuid - return self.reload_access_manager(get_uuid=True) + if not uuid: + uuid = self.get_access_manager().get_current_user_id() + self._uuid = uuid + return uuid def get_access_manager(self): access_manager = self._access_manager @@ -260,16 +335,19 @@ def get_access_manager(self): return access_manager return self.reload_access_manager() - def reload_access_manager(self, get_uuid=False): - access_manager = AccessManager(self) - self._access_manager = access_manager - uuid = access_manager.get_current_user_id() - self._uuid = uuid - if get_uuid: - return uuid - return access_manager + def reload_access_manager(self): + raise NotImplementedError() + + def get_api_store(self): + api_store = self._api_store + if api_store: + return api_store + return self.reload_api_store() + + def reload_api_store(self): + raise NotImplementedError() - def get_playlist_player(self): + def get_playlist_player(self, playlist_type=None): raise NotImplementedError() def get_ui(self): @@ -307,9 +385,12 @@ def create_uri(self, if isinstance(params, dict): params = params.items() params = urlencode([ - (('%' + param, ','.join([quote(item) for item in value])) - if len(value) > 1 else - (param, value[0])) + ( + ('%' + param, + ','.join([quote(item) for item in value])) + if len(value) > 1 else + (param, value[0]) + ) if value and isinstance(value, (list, tuple)) else (param, value) for param, value in params @@ -318,7 +399,14 @@ def create_uri(self, command = 'command://' if command else '' if run: - return ''.join((command, 'RunPlugin(', uri, ')')) + return ''.join((command, + 'RunAddon(' + if run == 'addon' else + 'RunScript(' + if run == 'script' else + 'RunPlugin(', + uri, + ')')) if play is not None: return ''.join(( command, @@ -329,15 +417,32 @@ def create_uri(self, if window: if not isinstance(window, dict): window = {} - window_replace = window.setdefault('replace', False) - window_return = window.setdefault('return', True) + if window.setdefault('refresh', False): + method = 'Container.Refresh(' + if not window.setdefault('replace', False): + uri = '' + history_replace = False + window_return = False + elif window.setdefault('update', False): + method = 'Container.Update(' + history_replace = window.setdefault('replace', False) + window_return = False + else: + history_replace = False + window_name = window.setdefault('name', 'Videos') + if window.setdefault('replace', False): + method = 'ReplaceWindow(%s,' % window_name + window_return = window.setdefault('return', False) + else: + method = 'ActivateWindow(%s,' % window_name + window_return = window.setdefault('return', True) return ''.join(( command, - 'ReplaceWindow(Videos,' - if window_replace else - 'ActivateWindow(Videos,', + method, uri, - ',return)' if window_return else ')', + ',return' if window_return else '', + ',replace' if history_replace else '', + ')' )) return uri @@ -347,11 +452,16 @@ def get_parent_uri(self, **kwargs): @staticmethod def create_path(*args, **kwargs): include_parts = kwargs.get('parts') + parser = kwargs.get('parser') parts = [ - part for part in [ + parser(part[6:-1]) + if parser and part.startswith('$INFO[') else + part + for part in [ to_str(arg).strip('/').replace('\\', '/').replace('//', '/') for arg in args - ] if part + ] + if part ] if parts: path = '/'.join(parts).join(('/', '/')) @@ -361,8 +471,13 @@ def create_path(*args, **kwargs): parts = [] elif path.startswith(PATHS.GOTO_PAGE): parts = parts[2:] - if parts and parts[0].isnumeric(): - parts = parts[1:] + if parts: + try: + int(parts[0]) + except (TypeError, ValueError): + pass + else: + parts = parts[1:] else: return ('/', parts) if include_parts else '/' @@ -379,7 +494,11 @@ def set_path(self, *path, **kwargs): path = unquote(path[0]) if parts is None: path = path.split('/') - path, parts = self.create_path(*path, parts=True) + path, parts = self.create_path( + *path, + parts=True, + parser=kwargs.get('parser') + ) else: path, parts = self.create_path(*path, parts=True) @@ -388,25 +507,34 @@ def set_path(self, *path, **kwargs): if kwargs.get('update_uri', True): self.update_uri() + def get_original_params(self): + return self._param_string + def get_params(self): return self._params def get_param(self, name, default=None): return self._params.get(name, default) - def parse_uri(self, uri, update=False): + def pop_param(self, name, default=None): + return self._params.pop(name, default) + + def parse_uri(self, uri, parse_params=True, update=False): uri = urlsplit(uri) path = uri.path - params = self.parse_params( - dict(parse_qsl(uri.query, keep_blank_values=True)), - update=False, - ) - if update: - self._params = params - self.set_path(path) + if parse_params: + params = self.parse_params( + dict(parse_qsl(uri.query, keep_blank_values=True)), + update=False, + ) + if update: + self._params = params + self.set_path(path) + else: + params = uri.query return path, params - def parse_params(self, params, update=True): + def parse_params(self, params, update=True, parser=None): to_delete = [] output = self._params if update else {} @@ -416,10 +544,15 @@ def parse_params(self, params, update=True): value = unquote(value) try: if param in self._BOOL_PARAMS: - parsed_value = VALUE_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( - (VALUE_FROM_STR.get(str(value), value) or 0) + (BOOL_FROM_STR.get(str(value), value) or 0) if param in self._INT_BOOL_PARAMS else value ) @@ -432,11 +565,19 @@ def parse_params(self, params, update=True): [unquote(val) for val in value.split(',') if val] ) elif param in self._STRING_PARAMS: - parsed_value = to_str(value) + if parser and value.startswith('$INFO['): + parsed_value = parser(value[6:-1]) + else: + parsed_value = value if param in self._STRING_BOOL_PARAMS: - parsed_value = VALUE_FROM_STR.get( + parsed_value = BOOL_FROM_STR.get( parsed_value, parsed_value ) + elif param in self._STRING_INT_PARAMS: + try: + parsed_value = int(parsed_value) + except (TypeError, ValueError): + pass # process and translate deprecated parameters elif param == 'action': if parsed_value in {'play_all', 'play_video'}: @@ -445,27 +586,24 @@ def parse_params(self, params, update=True): continue elif param == 'videoid': to_delete.append(param) - param = 'video_id' + param = VIDEO_ID elif params == 'playlist': to_delete.append(param) - param = 'playlist_id' + param = PLAYLIST_ID elif param in self._NON_EMPTY_STRING_PARAMS: - parsed_value = to_str(value) - parsed_value = VALUE_FROM_STR.get( - parsed_value, parsed_value - ) + parsed_value = BOOL_FROM_STR.get(value, value) if not parsed_value: raise ValueError else: - self.log_debug('Unknown parameter - |{0}: {1!r}|'.format( - param, value - )) + self.log.debug('Unknown parameter {param!r}: {value!r}', + param=param, + value=value) to_delete.append(param) continue except (TypeError, ValueError): - self.log_error('Invalid parameter value - |{0}: {1}|'.format( - param, value - )) + self.log.exception('Invalid value for {param!r}: {value!r}', + param=param, + value=value) to_delete.append(param) continue @@ -523,7 +661,7 @@ def get_handle(self): def get_settings(self, refresh=False): raise NotImplementedError() - def localize(self, text_id, default_text=None): + def localize(self, text_id, args=None, default_text=None): raise NotImplementedError() def apply_content(self, @@ -543,35 +681,27 @@ def execute(self, wait=False, wait_for=None, wait_for_set=True, - block_ui=False): + block_ui=None): raise NotImplementedError() @staticmethod def sleep(timeout=None): raise NotImplementedError() - @staticmethod - def get_infobool(name): - raise NotImplementedError() - - @staticmethod - def get_infolabel(name): - raise NotImplementedError() - - @staticmethod - def get_listitem_property(detail_name): - raise NotImplementedError() - - @staticmethod - def get_listitem_info(detail_name): - raise NotImplementedError() - def tear_down(self): pass - def wakeup(self, target, timeout=None): + def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False): raise NotImplementedError() @staticmethod def is_plugin_folder(folder_name=None): raise NotImplementedError() + + def refresh_requested(self, force=False, on=False, off=False, params=None): + raise NotImplementedError + + def parse_item_ids(self, + uri=None, + from_listitem=True): + raise NotImplementedError() diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 99b9816987..05412ae9f0 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,12 +10,14 @@ from __future__ import absolute_import, division, unicode_literals -import atexit import json import sys +from atexit import register as atexit_register +from timeit import default_timer from weakref import proxy from ..abstract_context import AbstractContext +from ... import logging from ...compatibility import ( parse_qsl, urlsplit, @@ -26,27 +28,76 @@ 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, - WAKEUP, + URI, + VIDEO_ID, ) +from ...json_store import APIKeyStore, AccessManager from ...player import XbmcPlaylistPlayer from ...settings import XbmcPluginSettings from ...ui import XbmcContextUI -from ...utils import ( - current_system_version, +from ...utils.convert_format import to_unicode +from ...utils.file_system import make_dirs +from ...utils.methods import ( get_kodi_setting_bool, get_kodi_setting_value, jsonrpc, loose_version, - make_dirs, - to_unicode, wait, ) +from ...utils.system_version import current_system_version + + +class IPCMonitor(xbmc.Monitor): + EXPECTED_SENDER = '.'.join((ADDON_ID, 'service')) + + def __init__(self, target, timeout): + super(IPCMonitor, self).__init__() + self.target = target + self.value = None + self.latency = None + self.received = False + + wait_period = 0.01 + elapsed = 0 + self._start = default_timer() + while not self.received and not self.waitForAbort(wait_period): + if timeout: + elapsed += wait_period + if elapsed >= timeout: + break + + def onNotification(self, sender, method, data): + if sender != self.EXPECTED_SENDER: + return + + group, separator, event = method.partition('.') + + if event == SERVICE_IPC: + if not isinstance(data, dict): + data = json.loads(data) + if not data: + return + + if self.target != data.get('target'): + return + + self.value = data.get('response') + self.latency = 1000 * (default_timer() - self._start) + self.received = True class XbmcContext(AbstractContext): + log = logging.getLogger(__name__) + # https://github.com/xbmc/xbmc/blob/master/xbmc/LangInfo.cpp#L1230 _KODI_UI_PLAYER_LANGUAGE_OPTIONS = { None, # No setting value @@ -65,6 +116,9 @@ class XbmcContext(AbstractContext): } LOCAL_MAP = { + 'add.to.x': 30613, + 'added.x': 30670, + 'added.to.x': 30615, 'after_watch.play_suggested': 30582, 'api.config': 30634, 'api.config.bookmark': 30638, @@ -81,27 +135,30 @@ class XbmcContext(AbstractContext): 'are_you_sure': 750, 'author': 21863, 'bookmark': 30101, - 'bookmark.channel': 30803, + 'bookmark.x': 30803, 'bookmark.created': 21362, 'bookmark.remove': 20404, 'bookmarks': 30100, + 'bookmarks.add': 294, 'bookmarks.clear': 30801, 'bookmarks.clear.check': 30802, + 'bookmarks.edit.name': 30108, + 'bookmarks.edit.uri': 30109, 'browse_channels': 30512, 'cancel': 222, 'channel': 19029, 'channels': 19019, 'client.id.incorrect': 30649, - 'client.ip': 30700, + 'client.ip.is.x': 30700, 'client.ip.failed': 30701, 'client.secret.incorrect': 30650, 'completed': 19256, 'content.clear': 30120, - 'content.clear.check': 30121, + 'content.clear.check.x': 30121, 'content.delete': 30114, - 'content.delete.check': 30116, + 'content.delete.check.x': 30116, 'content.remove': 30115, - 'content.remove.check': 30117, + 'content.remove.check.x': 30117, 'datetime.a_minute_ago': 30677, 'datetime.airing_now': 30691, 'datetime.airing_soon': 30693, @@ -120,30 +177,33 @@ class XbmcContext(AbstractContext): 'datetime.yesterday_at': 30682, 'delete': 117, 'disliked.video': 30717, + 'edit.x': 30501, 'error.no_streams_found': 30549, 'error.no_videos_found': 30545, 'error.rtmpe_not_supported': 30542, 'failed': 30576, + 'failed.x': 30500, 'feeds': 30518, 'filtered': 30105, - 'go_to_channel': 30502, + 'go_to.x': 30502, 'history': 30509, 'history.clear': 30609, 'history.clear.check': 30610, - 'history.list.remove': 30572, - 'history.list.remove.check': 30573, - 'history.list.set': 30571, - 'history.list.set.check': 30574, - 'history.mark.unwatched': 30669, - 'history.mark.watched': 30670, + 'history.list.unassign': 30572, + 'history.list.unassign.check': 30573, + 'history.list.assign': 30571, + 'history.list.assign.check': 30574, + 'history.mark.unwatched': 16104, + 'history.mark.watched': 16103, 'history.remove': 15015, - 'history.reset.resume_point': 30674, + 'history.reset.resume_point': 38209, 'home': 10000, 'httpd': 30628, 'httpd.not.running': 30699, 'httpd.connect.wait': 13028, 'httpd.connect.failed': 1001, 'inputstreamhelper.is_installed': 30625, + 'internet.connection.required': 21451, 'isa.enable.check': 30579, 'key.requirement': 30731, 'liked.video': 30716, @@ -158,23 +218,21 @@ class XbmcContext(AbstractContext): 'maintenance.feed_history': 30814, 'maintenance.function_cache': 30557, 'maintenance.playback_history': 30673, + 'maintenance.requests_cache': 30523, 'maintenance.search_history': 30558, 'maintenance.watch_later': 30782, + 'members_only': 30624, 'my_channel': 30507, 'my_location': 30654, 'my_subscriptions': 30510, 'my_subscriptions.loading': 30510, - 'my_subscriptions.filter.add': 30587, - 'my_subscriptions.filter.added': 30589, - 'my_subscriptions.filter.remove': 30588, - 'my_subscriptions.filter.removed': 30590, 'my_subscriptions.filtered': 30584, 'none': 231, 'page.back': 30815, 'page.choose': 30806, 'page.empty': 30816, 'page.next': 30106, - 'playlist.added_to': 30714, + 'playlist': 559, 'playlist.create': 525, 'playlist.play.all': 22083, 'playlist.play.default': 571, @@ -185,22 +243,26 @@ class XbmcContext(AbstractContext): 'playlist.play.shuffle': 191, 'playlist.podcast': 30820, 'playlist.progress.updating': 30536, - 'playlist.removed_from': 30715, 'playlist.select': 524, 'playlist.view.all': 30562, 'playlists': 136, 'please_wait': 30119, 'purchases': 30622, + 'rating': 563, 'recommendations': 30551, 'refresh': 184, 'refresh.settings.check': 30818, - 'related_videos': 30514, 'remove': 15015, - 'removed': 30666, + 'remove.from.x': 30614, + 'removed.from.x': 30616, + 'removed.name.x': 30666, + 'removed.x': 30669, 'rename': 118, - 'renamed': 30667, + 'renamed.x.y': 30667, 'reset.access_manager.check': 30581, 'retry': 30612, + 'save': 190, + 'saved': 35259, 'saved.playlists': 30611, 'search': 137, 'search.clear': 30556, @@ -233,10 +295,12 @@ class XbmcContext(AbstractContext): 'setup_wizard.capabilities.max': 30792, 'setup_wizard.locale.language': 30524, 'setup_wizard.locale.region': 30525, - 'setup_wizard.prompt': 30030, + 'setup_wizard.prompt.x': 30030, 'setup_wizard.prompt.import_playback_history': 30778, 'setup_wizard.prompt.import_search_history': 30779, 'setup_wizard.prompt.locale': 30527, + 'setup_wizard.prompt.migrate_watch_history': 30715, + 'setup_wizard.prompt.migrate_watch_later': 30718, 'setup_wizard.prompt.my_location': 30653, 'setup_wizard.prompt.settings': 10004, 'setup_wizard.prompt.settings.defaults': 30783, @@ -269,7 +333,7 @@ class XbmcContext(AbstractContext): 'stream.original': 30744, 'stream.secondary': 30747, 'subscribe': 30506, - 'subscribe_to': 30517, + 'subscribe_to.x': 30517, 'subscribed.to.channel': 30719, 'subscriptions': 30504, 'subtitles.download': 30705, @@ -277,25 +341,24 @@ class XbmcContext(AbstractContext): 'subtitles.all': 30774, 'subtitles.language': 21448, 'subtitles.no_asr': 30602, - 'subtitles.translation': 30775, + 'subtitles.translation.x': 30775, 'subtitles.with_fallback': 30601, 'succeeded': 30575, 'trending': 30513, - 'unrated.video': 30718, 'unsubscribe': 30505, 'unsubscribed.from.channel': 30720, 'untitled': 30707, 'upcoming': 30766, - 'updated_': 30597, + 'updated.x': 30631, 'uploads': 30726, - 'user.changed': 30659, + 'user.changed_to.x': 30659, 'user.default': 571, 'user.enter_name': 30658, 'user.new': 30656, 'user.remove': 30662, 'user.rename': 30663, 'user.switch': 30655, - 'user.switch.now': 30665, + 'user.switch_to.x': 30665, 'user.unnamed': 30657, 'video.add_to_playlist': 30520, 'video.comments': 30732, @@ -303,6 +366,7 @@ class XbmcContext(AbstractContext): 'video.comments.likes': 30733, 'video.comments.replies': 30734, 'video.description_links': 30544, + 'video.description_links.from.x': 30118, 'video.description_links.not_found': 30545, 'video.disliked': 30538, 'video.liked': 30508, @@ -311,23 +375,24 @@ class XbmcContext(AbstractContext): 'video.play.ask_for_quality': 30730, 'video.play.audio_only': 30708, 'video.play.timeshift': 30819, - 'video.play.with': 30540, + 'video.play.using': 15213, 'video.play.with_subtitles': 30702, 'video.queue': 30511, 'video.rate': 30528, 'video.rate.dislike': 30530, 'video.rate.like': 30529, 'video.rate.none': 15015, + 'video.related': 30514, + 'video.related.to.x': 30113, 'videos': 3, 'watch_later': 30107, 'watch_later.add': 30107, - 'watch_later.added_to': 30713, 'watch_later.clear': 30769, 'watch_later.clear.check': 30770, - 'watch_later.list.remove': 30568, - 'watch_later.list.remove.check': 30569, - 'watch_later.list.set': 30567, - 'watch_later.list.set.check': 30570, + 'watch_later.list.unassign': 30568, + 'watch_later.list.unassign.check': 30569, + 'watch_later.list.assign': 30567, + 'watch_later.list.assign.check': 30570, 'watch_later.remove': 15015, 'youtube': 30003, } @@ -341,7 +406,7 @@ class XbmcContext(AbstractContext): 'locationRadius', 'maxResults', 'order', - 'pageToken' + 'pageToken', 'publishedAfter', 'publishedBefore', 'q', @@ -396,12 +461,12 @@ def __init__(self, self._ui = None self._playlist = None - atexit.register(self.tear_down) + atexit_register(self.tear_down) def init(self): num_args = len(sys.argv) if num_args: - uri = sys.argv[0] + uri = to_unicode(sys.argv[0]) if uri.startswith('plugin://'): self._plugin_handle = int(sys.argv[1]) else: @@ -412,15 +477,22 @@ def init(self): return # first the path of the uri - self.set_path(urlsplit(uri).path, force=True, update_uri=False) + self.set_path( + urlsplit(uri).path, + force=True, + parser=XbmcContextUI.get_infolabel, + update_uri=False, + ) # after that try to get the params - self._params = {} if num_args > 2: - params = sys.argv[2][1:] + params = to_unicode(sys.argv[2][1:]) + self._param_string = params + self._params = {} if params: self.parse_params( - dict(parse_qsl(params, keep_blank_values=True)) + dict(parse_qsl(params, keep_blank_values=True)), + parser=XbmcContextUI.get_infolabel, ) # then Kodi resume status @@ -509,17 +581,31 @@ def get_subtitle_language(self): language = xbmc.convertLanguage(language, xbmc.ISO_639_1) return language + def reload_access_manager(self): + access_manager = AccessManager(proxy(self)) + self._access_manager = access_manager + return access_manager + + def reload_api_store(self): + api_store = APIKeyStore(proxy(self)) + self._api_store = api_store + return api_store + def get_playlist_player(self, playlist_type=None): if self.get_param(PLAY_FORCE_AUDIO) or self.get_settings().audio_only(): playlist_type = 'audio' - if not self._playlist or playlist_type: - self._playlist = XbmcPlaylistPlayer(proxy(self), playlist_type) - return self._playlist + playlist_player = self._playlist + if not playlist_player or playlist_type: + playlist_player = XbmcPlaylistPlayer(proxy(self), playlist_type) + self._playlist = playlist_player + return playlist_player def get_ui(self): - if not self._ui: - self._ui = XbmcContextUI(proxy(self)) - return self._ui + ui = self._ui + if not ui: + ui = XbmcContextUI(proxy(self)) + self._ui = ui + return ui def get_data_path(self): return self._data_path @@ -545,30 +631,64 @@ def get_settings(self, refresh=False): self.__class__._settings = XbmcPluginSettings(addon) return self._settings - def localize(self, text_id, default_text=None): - if default_text is None: - default_text = 'Undefined string ID: |{0}|'.format(text_id) + def localize(self, text_id, args=None, default_text=None): + if isinstance(text_id, tuple): + _args = text_id[1:] + _text_id = text_id[0] + localize_args = True + else: + _args = args + _text_id = text_id + localize_args = False - if not isinstance(text_id, int): + if not isinstance(_text_id, int): try: - text_id = self.LOCAL_MAP[text_id] + _text_id = self.LOCAL_MAP[_text_id] except KeyError: try: - text_id = int(text_id) - except ValueError: - return default_text - if text_id <= 0: + _text_id = int(_text_id) + except (TypeError, ValueError): + _text_id = -1 + if _text_id <= 0: + msg = 'Undefined string ID: {text_id!r}' + if default_text is None: + default_text = msg.format(text_id=text_id) + self.log.warning(default_text) + else: + self.log.warning(msg, text_id=text_id) return default_text """ We want to use all localization strings! - Addons should only use the range 30000 thru 30999 - (see: http://kodi.wiki/view/Language_support) but we do it anyway. + Addons should only use the range 30000 through 30999 + (see: http://kodi.wiki/view/Language_support), but we do it anyway. I want some of the localized strings for the views of a skin. """ - source = self._addon if 30000 <= text_id < 31000 else xbmc - result = source.getLocalizedString(text_id) - result = to_unicode(result) if result else default_text + source = self._addon if 30000 <= _text_id < 31000 else xbmc + result = source.getLocalizedString(_text_id) + if not result: + msg = 'Untranslated string ID: {text_id!r}' + if default_text is None: + default_text = msg.format(text_id=text_id) + self.log.warning(default_text) + else: + self.log.warning(msg, text_id=text_id) + return default_text + result = to_unicode(result) + + if _args: + if localize_args: + _args = tuple(self.localize(arg, default_text=arg) + for arg in _args) + try: + return result % _args + except TypeError: + self.log.exception(('Localization error', + 'String: {result!r} ({text_id!r})', + 'args: {original_args!r}'), + result=result, + text_id=text_id, + original_args=args) return result def apply_content(self, @@ -576,13 +696,15 @@ def apply_content(self, sub_type=None, category_label=None): # ui local variable used for ui.get_view_manager() in unofficial version + # noinspection PyUnusedLocal ui = self.get_ui() if content_type: - self.log_debug('Applying content-type: |{type}| for |{path}|' - .format(type=(sub_type or content_type), - path=self.get_path())) - xbmcplugin.setContent(self._plugin_handle, content_type) + self.log.debug('Applying content-type: {type!r} for {path!r}', + type=(sub_type or content_type), + path=self.get_path()) + if content_type != 'default': + xbmcplugin.setContent(self._plugin_handle, content_type) if category_label is None: category_label = self.get_param('category_label') @@ -590,66 +712,51 @@ def apply_content(self, xbmcplugin.setPluginCategory(self._plugin_handle, category_label) detailed_labels = self.get_settings().show_detailed_labels() - if sub_type == 'history': + if sub_type == CONTENT.HISTORY: self.add_sort_method( - (SORT.LASTPLAYED, '%T \u2022 %P', '%D | %J'), - (SORT.PLAYCOUNT, '%T \u2022 %P', '%D | %J'), - (SORT.UNSORTED, '%T \u2022 %P', '%D | %J'), - (SORT.LABEL, '%T \u2022 %P', '%D | %J'), - ) if detailed_labels else self.add_sort_method( - (SORT.LASTPLAYED,), - (SORT.PLAYCOUNT,), - (SORT.UNSORTED,), - (SORT.LABEL,), + SORT.HISTORY_CONTENT_DETAILED + if detailed_labels else + SORT.HISTORY_CONTENT_SIMPLE ) - elif sub_type == 'comments': + elif sub_type == CONTENT.COMMENTS: self.add_sort_method( - (SORT.CHANNEL, '[%A - ]%P \u2022 %T', '%J'), - (SORT.ARTIST, '[%J - ]%P \u2022 %T', '%A'), - (SORT.PROGRAM_COUNT, '[%A - ]%P | %J \u2022 %T', '%C'), - (SORT.DATE, '[%A - ]%P \u2022 %T', '%J'), - (SORT.TRACKNUM, '[%N. ][%A - ]%P \u2022 %T', '%J'), - ) if detailed_labels else self.add_sort_method( - (SORT.CHANNEL, '[%A - ]%T'), - (SORT.ARTIST, '[%A - ]%T'), - (SORT.PROGRAM_COUNT, '[%A - ]%T'), - (SORT.DATE, '[%A - ]%T'), - (SORT.TRACKNUM, '[%N. ][%A - ]%T '), + SORT.COMMENTS_CONTENT_DETAILED + if detailed_labels else + SORT.COMMENTS_CONTENT_SIMPLE ) - else: + elif sub_type == CONTENT.PLAYLIST: self.add_sort_method( - (SORT.UNSORTED, '%T \u2022 %P', '%D | %J'), - (SORT.LABEL, '%T \u2022 %P', '%D | %J'), - ) if detailed_labels else self.add_sort_method( - (SORT.UNSORTED,), - (SORT.LABEL,), + SORT.PLAYLIST_CONTENT_DETAILED + if detailed_labels else + SORT.PLAYLIST_CONTENT_SIMPLE ) - - if content_type == CONTENT.VIDEO_CONTENT: + elif content_type == CONTENT.VIDEO_CONTENT: + self.add_sort_method( + SORT.VIDEO_CONTENT_DETAILED + if detailed_labels else + SORT.VIDEO_CONTENT_SIMPLE + ) + else: self.add_sort_method( - (SORT.CHANNEL, '[%A - ]%T \u2022 %P', '%D | %J'), - (SORT.ARTIST, '%T \u2022 %P | %D | %J', '%A'), - (SORT.PROGRAM_COUNT, '%T \u2022 %P | %D | %J', '%C'), - (SORT.VIDEO_RATING, '%T \u2022 %P | %D | %J', '%R'), - (SORT.DATE, '%T \u2022 %P | %D', '%J'), - (SORT.DATEADDED, '%T \u2022 %P | %D', '%a'), - (SORT.VIDEO_RUNTIME, '%T \u2022 %P | %J', '%D'), - (SORT.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'), - ) if detailed_labels else self.add_sort_method( - (SORT.CHANNEL, '[%A - ]%T'), - (SORT.ARTIST,), - (SORT.PROGRAM_COUNT,), - (SORT.VIDEO_RATING,), - (SORT.DATE,), - (SORT.DATEADDED,), - (SORT.VIDEO_RUNTIME,), - (SORT.TRACKNUM, '[%N. ]%T '), + SORT.LIST_CONTENT_DETAILED + if detailed_labels else + SORT.LIST_CONTENT_SIMPLE ) - def add_sort_method(self, *sort_methods): - args = slice(None if current_system_version.compatible(19) else 2) - for sort_method in sort_methods: - xbmcplugin.addSortMethod(self._plugin_handle, *sort_method[args]) + if current_system_version.compatible(19): + def add_sort_method(self, + sort_methods, + _add_sort_method=xbmcplugin.addSortMethod): + handle = self._plugin_handle + for sort_method in sort_methods: + _add_sort_method(handle, *sort_method) + else: + def add_sort_method(self, + sort_methods, + _add_sort_method=xbmcplugin.addSortMethod): + handle = self._plugin_handle + for sort_method in sort_methods: + _add_sort_method(handle, *sort_method[:3:2]) def clone(self, new_path=None, new_params=None): if not new_path: @@ -664,12 +771,14 @@ def clone(self, new_path=None, new_params=None): new_context._access_manager = self._access_manager new_context._uuid = self._uuid + new_context._api_store = self._api_store new_context._bookmarks_list = self._bookmarks_list new_context._data_cache = self._data_cache new_context._feed_history = self._feed_history new_context._function_cache = self._function_cache new_context._playback_history = self._playback_history + new_context._requests_cache = self._requests_cache new_context._search_history = self._search_history new_context._watch_later_list = self._watch_later_list @@ -683,31 +792,40 @@ def execute(self, wait=False, wait_for=None, wait_for_set=True, - block_ui=False): + block_ui=None, + _execute=xbmc.executebuiltin): if not wait_for: - xbmc.executebuiltin(command, wait) + if block_ui is False: + _execute('Dialog.Close(all,true)') + _execute(command, wait) return ui = self.get_ui() - waitForAbort = xbmc.Monitor().waitForAbort + wait_for_abort = xbmc.Monitor().waitForAbort - xbmc.executebuiltin(command, wait) + if block_ui is False: + _execute('Dialog.Close(all,true)') + _execute(command, wait) if block_ui: - xbmc.executebuiltin('ActivateWindow(busydialognocancel)') + _execute('ActivateWindow(busydialognocancel)') - if wait_for_set: + if isinstance(wait_for, tuple): + wait_for, wait_for_kwargs, delay = wait_for + while not wait_for(**wait_for_kwargs) and not wait_for_abort(delay): + pass + elif wait_for_set: ui.clear_property(wait_for) pop_property = ui.pop_property - while not pop_property(wait_for) and not waitForAbort(1): + while not pop_property(wait_for) and not wait_for_abort(1): pass else: get_property = ui.get_property - while get_property(wait_for) and not waitForAbort(1): + while get_property(wait_for) and not wait_for_abort(1): pass if block_ui: - xbmc.executebuiltin('Dialog.Close(busydialognocancel)') + _execute('Dialog.Close(busydialognocancel)') @staticmethod def sleep(timeout=None): @@ -719,15 +837,13 @@ def addon_enabled(self, addon_id): 'properties': ['enabled']}) try: return response['result']['addon']['enabled'] is True - except (KeyError, TypeError) as exc: + except (KeyError, TypeError): error = response.get('error', {}) - self.log_error('XbmcContext.addon_enabled - Error' - '\n\tException: {exc!r}' - '\n\tCode: {code}' - '\n\tMessage: {msg}' - .format(exc=exc, - code=error.get('code', 'Unknown'), - msg=error.get('message', 'Unknown'))) + self.log.exception(('Error', + 'Code: {code}', + 'Message: {message}'), + code=error.get('code', 'Unknown'), + message=error.get('message', 'Unknown')) return False def set_addon_enabled(self, addon_id, enabled=True): @@ -736,21 +852,19 @@ def set_addon_enabled(self, addon_id, enabled=True): 'enabled': enabled}) try: return response['result'] == 'OK' - except (KeyError, TypeError) as exc: + except (KeyError, TypeError): error = response.get('error', {}) - self.log_error('XbmcContext.set_addon_enabled - Error' - '\n\tException: {exc!r}' - '\n\tCode: {code}' - '\n\tMessage: {msg}' - .format(exc=exc, - code=error.get('code', 'Unknown'), - msg=error.get('message', 'Unknown'))) + self.log.exception(('Error', + 'Code: {code}', + 'Message: {message}'), + code=error.get('code', 'Unknown'), + message=error.get('message', 'Unknown')) return False @staticmethod - def send_notification(method, data=True): + def send_notification(method, data=True, sender=ADDON_ID): jsonrpc(method='JSONRPC.NotifyAll', - params={'sender': ADDON_ID, + params={'sender': sender, 'message': method, 'data': data}) @@ -783,6 +897,8 @@ def use_inputstream_adaptive(self, prompt=False): 'drm': loose_version('2.2.12'), 'live': loose_version('2.0.12'), 'timeshift': loose_version('2.5.2'), + # subtitles + 'vtt': loose_version('2.3.8'), 'ttml': loose_version('20.0.0'), # properties 'config_prop': loose_version('21.4.11'), @@ -833,24 +949,9 @@ def inputstream_adaptive_auto_stream_selection(): return False def abort_requested(self): - return self.get_ui().get_property(ABORT_FLAG).lower() == 'true' - - @staticmethod - def get_infobool(name): - return xbmc.getCondVisibility(name) - - @staticmethod - def get_infolabel(name): - return xbmc.getInfoLabel(name) - - @staticmethod - def get_listitem_property(detail_name): - return xbmc.getInfoLabel('Container.ListItem(0).Property({0})' - .format(detail_name)) - - @staticmethod - def get_listitem_info(detail_name): - return xbmc.getInfoLabel('Container.ListItem(0).' + detail_name) + return self.get_ui().get_property( + ABORT_FLAG, stacklevel=3, as_bool=True + ) def tear_down(self): self.clear_settings() @@ -870,6 +971,8 @@ def tear_down(self): attrs = ( '_ui', '_playlist', + '_api_store', + '_access_manager', ) for attr in attrs: try: @@ -878,65 +981,64 @@ def tear_down(self): except AttributeError: pass - def wakeup(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) - self.send_notification(WAKEUP, data) + self.send_notification(SERVICE_IPC, data) + if not timeout: return None - - pop_property = self.get_ui().pop_property - no_timeout = timeout < 0 - remaining = timeout = timeout * 1000 - wait_period_ms = 100 - wait_period = wait_period_ms / 1000 - - while no_timeout or remaining > 0: - data = pop_property(WAKEUP) - if data: - data = json.loads(data) - - if data: - response = data.get('response') - response_target = data.get('target') or 'Unknown' - - if target == response_target: - if response: - self.log_debug('Wakeup |{0}| in {1}ms' - .format(response_target, - timeout - remaining)) - else: - self.log_error('Wakeup |{0}| in {1}ms - failed' - .format(response_target, - timeout - remaining)) - return response - - self.log_error('Wakeup |{0}| in {1}ms - expected |{2}|' - .format(response_target, - timeout - remaining, - target)) - break - - wait(wait_period) - remaining -= wait_period_ms + if timeout < 0: + timeout = None + + response = IPCMonitor(target, timeout) + if response.received: + value = response.value + if value: + self.log.debug(('Service IPC - Responded', + 'Procedure: {target!r}', + 'Latency: {latency:.2f}ms'), + target=target, + latency=response.latency) + elif value is False: + self.log.error_trace(('Service IPC - Failed', + 'Procedure: {target!r}', + 'Latency: {latency:.2f}ms'), + target=target, + latency=response.latency) else: - self.log_error('Wakeup |{0}| timed out in {1}ms' - .format(target, timeout)) - return False + value = None + self.log.error_trace(('Service IPC - Timed out', + 'Procedure: {target!r}', + 'Timeout: {timeout:.2f}s'), + target=target, + timeout=timeout) + return value def is_plugin_folder(self, folder_name=None): if folder_name is None: - folder_name = xbmc.getInfoLabel('Container.FolderName') + folder_name = XbmcContextUI.get_container_info(FOLDER_NAME, + container_id=False) return folder_name == self._plugin_name def refresh_requested(self, force=False, on=False, off=False, params=None): if params is None: params = self.get_params() - refresh = params.get('refresh', 0) + refresh = params.get('refresh') if not force: - return refresh > 0 + return refresh and refresh > 0 + if refresh is None: + refresh = 0 if off: if refresh > 0: refresh = -refresh @@ -946,3 +1048,39 @@ def refresh_requested(self, force=False, on=False, off=False, params=None): refresh += 1 return refresh + + def parse_item_ids(self, + uri='', + from_listitem=True, + _ids={'video': VIDEO_ID, + 'channel': CHANNEL_ID, + 'playlist': PLAYLIST_ID}): + item_ids = {} + if not uri and from_listitem: + uri = XbmcContextUI.get_listitem_info(URI) + if not uri or not self.is_plugin_path(uri): + return item_ids + uri = urlsplit(uri) + + path = uri.path.rstrip('/') + while path: + id_type, _, next_part = path.partition('/') + if not next_part: + break + + if id_type in _ids: + id_value = next_part.partition('/')[0] + if id_value: + item_ids[_ids[id_type]] = id_value + + path = next_part + + params = dict(parse_qsl(uri.query)) + for name in _ids.values(): + id_value = params.get(name) + if not id_value and from_listitem: + id_value = XbmcContextUI.get_listitem_property(name) + if id_value: + item_ids[name] = id_value + + return item_ids diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/debug.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/debug.py index 8022c0ede4..ef39afe641 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/debug.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/debug.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-present plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,13 +10,24 @@ from __future__ import absolute_import, division, unicode_literals -import atexit -import os +import sys +import threading +import time +from atexit import register as atexit_register +from cProfile import Profile +from functools import wraps +from inspect import getargvalues +from os.path import normpath +import pstats +from traceback import extract_stack, format_list +from weakref import ref -from .logger import Logger +from . import logging +from .compatibility import StringIO def debug_here(host='localhost'): + import os import sys for comp in sys.path: @@ -36,6 +47,48 @@ def debug_here(host='localhost'): pydevd.settrace(host, stdoutToServer=True, stderrToServer=True) +class ProfilerProxy(ref): + def __call__(self, *args, **kwargs): + return super(ProfilerProxy, self).__call__().__call__( + *args, **kwargs + ) + + def __enter__(self, *args, **kwargs): + return super(ProfilerProxy, self).__call__().__enter__( + *args, **kwargs + ) + + def __exit__(self, *args, **kwargs): + return super(ProfilerProxy, self).__call__().__exit__( + *args, **kwargs + ) + + def disable(self, *args, **kwargs): + return super(ProfilerProxy, self).__call__().disable( + *args, **kwargs + ) + + def enable(self, *args, **kwargs): + return super(ProfilerProxy, self).__call__().enable( + *args, **kwargs + ) + + def get_stats(self, *args, **kwargs): + return super(ProfilerProxy, self).__call__().get_stats( + *args, **kwargs + ) + + def print_stats(self, *args, **kwargs): + return super(ProfilerProxy, self).__call__().print_stats( + *args, **kwargs + ) + + def tear_down(self, *args, **kwargs): + return super(ProfilerProxy, self).__call__().tear_down( + *args, **kwargs + ) + + class Profiler(object): """Class used to profile a block of code""" @@ -46,63 +99,10 @@ class Profiler(object): '_print_callees', '_profiler', '_reuse', - '_sort_by', '_timer', - 'name', ) - from cProfile import Profile as _Profile - from pstats import Stats as _Stats - - try: - from StringIO import StringIO as _StringIO - except ImportError: - from io import StringIO as _StringIO - from functools import wraps as _wraps - - _wraps = staticmethod(_wraps) - from weakref import ref as _ref - - class Proxy(_ref): - def __call__(self, *args, **kwargs): - return super(Profiler.Proxy, self).__call__().__call__( - *args, **kwargs - ) - - def __enter__(self, *args, **kwargs): - return super(Profiler.Proxy, self).__call__().__enter__( - *args, **kwargs - ) - - def __exit__(self, *args, **kwargs): - return super(Profiler.Proxy, self).__call__().__exit__( - *args, **kwargs - ) - - def disable(self, *args, **kwargs): - return super(Profiler.Proxy, self).__call__().disable( - *args, **kwargs - ) - - def enable(self, *args, **kwargs): - return super(Profiler.Proxy, self).__call__().enable( - *args, **kwargs - ) - - def get_stats(self, *args, **kwargs): - return super(Profiler.Proxy, self).__call__().get_stats( - *args, **kwargs - ) - - def print_stats(self, *args, **kwargs): - return super(Profiler.Proxy, self).__call__().print_stats( - *args, **kwargs - ) - - def tear_down(self, *args, **kwargs): - return super(Profiler.Proxy, self).__call__().tear_down( - *args, **kwargs - ) + log = logging.getLogger(__name__) _instances = set() @@ -111,31 +111,27 @@ def __new__(cls, *args, **kwargs): cls._instances.add(self) if not kwargs.get('enabled') or kwargs.get('lazy'): self.__init__(*args, **kwargs) - return cls.Proxy(self) + return ProfilerProxy(self) return self def __init__(self, enabled=True, lazy=True, - name=__name__, num_lines=20, print_callees=False, reuse=False, - sort_by=('cumulative', 'time'), timer=None): self._enabled = enabled self._num_lines = num_lines self._print_callees = print_callees self._profiler = None self._reuse = reuse - self._sort_by = sort_by self._timer = timer - self.name = name if enabled and not lazy: self._create_profiler() - atexit.register(self.tear_down) + atexit_register(self.tear_down) def __enter__(self): if not self._enabled: @@ -157,53 +153,19 @@ def __call__(self, func=None, name=__name__, reuse=False): if not func: self._reuse = reuse - self.name = name return self - @self.__class__._wraps(func) # pylint: disable=protected-access + @wraps(func) def wrapper(*args, **kwargs): - """Wrapper to: + """ + Wrapper to: 1) create a new Profiler instance; 2) run the function being profiled; 3) print out profiler result to the log; and - 4) return result of function call""" - - name = getattr(func, '__qualname__', None) - if name: - # If __qualname__ is available (Python 3.3+) then use it - pass - - elif args and getattr(args[0], func.__name__, None): - if isinstance(args[0], type): - class_name = args[0].__name__ - else: - class_name = args[0].__class__.__name__ - name = '.'.join(( - class_name, - func.__name__, - )) - - elif (func.__class__ - and not isinstance(func.__class__, type) - and func.__class__.__name__ != 'function'): - name = '.'.join(( - func.__class__.__name__, - func.__name__, - )) - - elif func.__module__: - name = '.'.join(( - func.__module__, - func.__name__, - )) - - else: - name = func.__name__ - - self.name = name + 4) return result of function call + """ with self: result = func(*args, **kwargs) - return result if not self._enabled: @@ -213,15 +175,17 @@ def wrapper(*args, **kwargs): def _create_profiler(self): if self._timer: - self._profiler = self._Profile(timer=self._timer) + self._profiler = Profile(timer=self._timer) else: - self._profiler = self._Profile() + self._profiler = Profile() self._profiler.enable() - @classmethod - def wait_timer(cls): - times = os.times() - return times.elapsed - (times.system + times.user) + try: + elapsed_timer = time.perf_counter + process_timer = time.process_time + except AttributeError: + elapsed_timer = time.clock + process_timer = time.clock def disable(self): if self._profiler: @@ -238,23 +202,26 @@ def get_stats(self, flush=True, num_lines=20, print_callees=False, - reuse=False, - sort_by=('cumulative', 'time')): + reuse=False): if not (self._enabled and self._profiler): return None self.disable() - output_stream = self._StringIO() + output_stream = StringIO() try: - stats = self._Stats( + stats = Stats( self._profiler, stream=output_stream ) - stats.strip_dirs().sort_stats(*sort_by) + stats.strip_dirs() if print_callees: + stats.sort_stats('cumulative') stats.print_callees(num_lines) else: + stats.sort_stats('cumpercall') + stats.print_stats(num_lines) + stats.sort_stats('totalpercall') stats.print_stats(num_lines) output = output_stream.getvalue() # Occurs when no stats were able to be generated from profiler @@ -270,12 +237,197 @@ def get_stats(self, return output def print_stats(self): - Logger.log_debug('Profiling stats: {0}'.format(self.get_stats( - num_lines=self._num_lines, - print_callees=self._print_callees, - reuse=self._reuse, - sort_by=self._sort_by, - ))) + self.log.info('Profiling stats: %s', + self.get_stats( + num_lines=self._num_lines, + print_callees=self._print_callees, + reuse=self._reuse, + ), + stacklevel=3) def tear_down(self): self.__class__._instances.discard(self) + + +class Stats(pstats.Stats): + """ + Custom Stats class that adds functionality to sort by + - Cumulative time per call ("cumpercall") + - Total time per call ("totalpercall") + Code by alexnvdias from https://bugs.python.org/issue18795 + """ + + sort_arg_dict_default = { + "calls" : (((1,-1), ), "call count"), + "ncalls" : (((1,-1), ), "call count"), + "cumtime" : (((4,-1), ), "cumulative time"), + "cumulative" : (((4,-1), ), "cumulative time"), + "filename" : (((6, 1), ), "file name"), + "line" : (((7, 1), ), "line number"), + "module" : (((6, 1), ), "file name"), + "name" : (((8, 1), ), "function name"), + "nfl" : (((8, 1),(6, 1),(7, 1),), "name/file/line"), + "pcalls" : (((0,-1), ), "primitive call count"), + "stdname" : (((9, 1), ), "standard name"), + "time" : (((2,-1), ), "internal time"), + "tottime" : (((2,-1), ), "internal time"), + "cumpercall" : (((5,-1), ), "cumulative time per call"), + "totalpercall": (((3,-1), ), "total time per call"), + } + + def sort_stats(self, *field): + if not field: + self.fcn_list = 0 + return self + if len(field) == 1 and isinstance(field[0], int): + # Be compatible with old profiler + field = [{-1: "stdname", + 0: "calls", + 1: "time", + 2: "cumulative"}[field[0]]] + elif len(field) >= 2: + for arg in field[1:]: + if type(arg) != type(field[0]): + raise TypeError("Can't have mixed argument type") + + sort_arg_defs = self.get_sort_arg_defs() + + sort_tuple = () + self.sort_type = "" + connector = "" + for word in field: + if isinstance(word, pstats.SortKey): + word = word.value + sort_tuple = sort_tuple + sort_arg_defs[word][0] + self.sort_type += connector + sort_arg_defs[word][1] + connector = ", " + + stats_list = [] + for func, (cc, nc, tt, ct, callers) in self.stats.items(): + if nc == 0: + npc = 0 + else: + npc = float(tt) / nc + + if cc == 0: + cpc = 0 + else: + cpc = float(ct) / cc + + stats_list.append((cc, nc, tt, npc, ct, cpc) + func + + (pstats.func_std_string(func), func)) + + stats_list.sort(key=pstats.cmp_to_key(pstats.TupleComp(sort_tuple).compare)) + + self.fcn_list = fcn_list = [] + for tuple in stats_list: + fcn_list.append(tuple[-1]) + return self + + +class ExecTimeout(object): + log = logging.getLogger('__name__') + src_file = None + + def __init__(self, + seconds, + log_only=False, + trace_opcodes=False, + trace_threads=False, + log_locals=False, + callback=None, + skip_paths=('\\python\\lib\\', + '\\logging.py', + '\\addons\\script.')): + self._interval = seconds + self._log_only = log_only + self._last_event = (None, None, None) + self._timed_out = False + + self._trace_opcodes = trace_opcodes + self._trace_threads = trace_threads + self._log_locals = log_locals + self._callback = callback if callable(callback) else None + + self._skip_paths = skip_paths + + def __call__(self, function): + @wraps(function) + def wrapper(*args, **kwargs): + timer = threading.Timer(self._interval, self.set_timed_out) + timer.daemon = True + + if self._trace_threads: + threading.settrace(self.timeout_trace) + sys.settrace(self.timeout_trace) + timer.start() + try: + return function(*args, **kwargs) + finally: + timer.cancel() + if self._trace_threads: + threading.settrace(None) + sys.settrace(None) + if self._callback: + self._callback() + self._last_event = (None, None, None) + + return wrapper + + def timeout_trace(self, frame, event, arg): + if self._trace_opcodes and hasattr(frame, 'f_trace_opcodes'): + frame.f_trace_opcodes = True + if self._timed_out: + if not self._log_only: + raise RuntimeError('Python execution timed out') + else: + filename = normpath(frame.f_code.co_filename).lower() + skip_event = ( + filename == self.src_file + or (self._skip_paths + and any(skip_path in filename + for skip_path in self._skip_paths)) + ) + if not skip_event: + self._last_event = (event, frame, arg) + return self.timeout_trace + + def set_timed_out(self): + msg, kwargs = self._get_msg(to_log=True) + self.log.error(msg, **kwargs) + self._timed_out = True + + def _get_msg(self, to_log=False): + event, frame, arg = self._last_event + out = ( + 'Python execution timed out', + 'Event: {event!r}', + 'Frame: {frame!r}', + 'Arg: {arg!r}', + 'Locals: {locals!r}', + '', + 'Stack (most recent call last):', + '{stack_trace}', + ) + log_locals = self._log_locals + if log_locals: + _locals = getargvalues(frame).locals + if log_locals is not True: + _locals = dict(tuple(_locals.items())[slice(*log_locals)]) + else: + _locals = None + kwargs = { + 'event': event, + 'frame': frame, + 'arg': arg, + 'locals': _locals, + 'stack_trace': ''.join(format_list(extract_stack(frame))), + } + if to_log: + return out, kwargs + return '\n'.join(out).format(**kwargs) + + +ExecTimeout.src_file = normpath( + ExecTimeout.__init__.__code__.co_filename +).lower() diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/exceptions.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/exceptions.py index c52fcb71db..2a480b8216 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/exceptions.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/exceptions.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/__init__.py index 16e888f58d..fbd2c40b8f 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,6 +12,7 @@ from . import menu_items from .base_item import BaseItem +from .bookmark_item import BookmarkItem from .command_item import CommandItem from .directory_item import DirectoryItem from .image_item import ImageItem @@ -33,6 +34,7 @@ __all__ = ( 'AudioItem', 'BaseItem', + 'BookmarkItem', 'CommandItem', 'DirectoryItem', 'ImageItem', diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/base_item.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/base_item.py index d3e8aadcc8..0713653fdf 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,31 +12,32 @@ import json from datetime import date, datetime -from hashlib import md5 from .menu_items import separator from ..compatibility import ( datetime_infolabel, - parse_qsl, string_type, to_str, unescape, - urlsplit, ) from ..constants import MEDIA_PATH +from ..utils.methods import generate_hash class BaseItem(object): _version = 3 _playable = False - def __init__(self, name, uri, image=None, fanart=None): + def __init__(self, name, uri, image=None, fanart=None, **_kwargs): + super(BaseItem, self).__init__() self._name = None self.set_name(name) self._uri = uri self._available = True self._callback = None + self._filter_reason = None + self._special_sort = None self._image = '' if image: @@ -60,67 +61,54 @@ def __init__(self, name, uri, image=None, fanart=None): self._artists = None self._studios = None + def __str_parts__(self, as_dict=False): + kwargs = { + 'type': self.__class__.__name__, + 'name': self._name, + 'uri': self._uri, + 'available': self._available, + 'added': self._added_utc, + 'filtered': self._filter_reason, + } + if as_dict: + return kwargs + out = ( + '{type}(', + 'name={name!r}, ', + 'uri={uri!r}, ', + 'available={available!r}, ', + 'added=\'{added!s}\', ', + 'filtered={filtered!r})', + ) + return out, kwargs + def __str__(self): - return ('{type}(name="{name}", uri="{uri}", image="{image}")' - .format(type=self.__class__.__name__, - name=self._name, - uri=self._uri, - image=self._image)) + out, kwargs = self.__str_parts__() + return ''.join(out).format(**kwargs) + + def __repr_data__(self): + return {'type': self.__class__.__name__, 'data': self.__dict__} def __repr__(self): return json.dumps( - {'type': self.__class__.__name__, 'data': self.__dict__}, + self.__repr_data__(), ensure_ascii=False, cls=_Encoder ) + @staticmethod + def generate_id(*args, **kwargs): + prefix = kwargs.get('prefix') + if prefix: + return '%s.%s' % (prefix, generate_hash(*args)) + return generate_hash(*args) + def get_id(self): """ Returns a unique id of the item. :return: unique id of the item. """ - return md5(''.join((self._name, self._uri)).encode('utf-8')).hexdigest() - - def parse_item_ids_from_uri(self): - if not self._uri: - return None - - item_ids = {} - - uri = urlsplit(self._uri) - path = uri.path.rstrip('/') - params = dict(parse_qsl(uri.query)) - - video_id = params.get('video_id') - if video_id: - item_ids['video_id'] = video_id - - channel_id = None - playlist_id = None - - while path: - part, _, next_part = path.partition('/') - if not next_part: - break - - if part == 'channel': - channel_id = next_part.partition('/')[0] - elif part == 'playlist': - playlist_id = next_part.partition('/')[0] - path = next_part - - if channel_id: - item_ids['channel_id'] = channel_id - if playlist_id: - item_ids['playlist_id'] = playlist_id - - for item_id, value in item_ids.items(): - try: - setattr(self, item_id, value) - except AttributeError: - pass - - return item_ids + return self.generate_id(self._name, self._uri) def set_name(self, name): try: @@ -161,7 +149,7 @@ def callback(self): @callback.setter def callback(self, value): - self._callback = value + self._callback = value.__get__(self) if callable(value) else None def set_image(self, image): if not image: @@ -343,17 +331,32 @@ def set_track_number(self, track_number): def get_track_number(self): return self._track_number + def set_filter_reason(self, reason): + self._filter_reason = reason + + def get_filter_reason(self): + return self._filter_reason + + def set_special_sort(self, position): + self._special_sort = position + + def get_special_sort(self): + return self._special_sort + class _Encoder(json.JSONEncoder): def encode(self, obj, nested=False): if isinstance(obj, (date, datetime)): class_name = obj.__class__.__name__ - if 'fromisoformat' in dir(obj): - obj = { - '__class__': class_name, - '__isoformat__': obj.isoformat(), - } - else: + try: + if obj.fromisoformat: + obj = { + '__class__': class_name, + '__isoformat__': obj.isoformat(), + } + else: + raise AttributeError + except AttributeError: if class_name == 'datetime': if obj.tzinfo: format_string = '%Y-%m-%dT%H:%M:%S%z' diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/bookmark_item.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/bookmark_item.py new file mode 100644 index 0000000000..eb59c13f63 --- /dev/null +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/bookmark_item.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2014-2016 bromix (plugin.video.youtube) + Copyright (C) 2016-2025 plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from __future__ import absolute_import, division, unicode_literals + +from .directory_item import DirectoryItem +from .media_item import VideoItem + + +class BookmarkItem(VideoItem, DirectoryItem): + def __init__(self, + name, + uri, + image='{media}/bookmarks.png', + fanart=None, + plot=None, + action=False, + playable=None, + special_sort=None, + date_time=None, + category_label=None, + bookmark_id=None, + video_id=None, + channel_id=None, + playlist_id=None, + playlist_item_id=None, + subscription_id=None, + **_kwargs): + super(BookmarkItem, self).__init__( + name=name, + uri=uri, + image=image, + fanart=fanart, + plot=plot, + action=action, + special_sort=special_sort, + date_time=date_time, + category_label=category_label, + bookmark_id=bookmark_id, + video_id=video_id, + channel_id=channel_id, + playlist_id=playlist_id, + playlist_item_id=playlist_item_id, + subscription_id=subscription_id, + ) + self._bookmark_id = bookmark_id + if playable is not None: + self._playable = playable diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/command_item.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/command_item.py index 501a28f28a..db4cf26444 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/command_item.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/command_item.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -22,7 +22,8 @@ def __init__(self, context, image=None, fanart=None, - plot=None): + plot=None, + **_kwargs): super(CommandItem, self).__init__( name, context.create_uri((PATHS.COMMAND, command)), @@ -34,7 +35,7 @@ def __init__(self, ) context_menu = [ - menu_items.refresh(context), + menu_items.refresh_listing(context), menu_items.goto_home(context), menu_items.goto_quick_search(context), ] diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/directory_item.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/directory_item.py index e34c61e764..44d78270d2 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/directory_item.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/directory_item.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,7 +11,7 @@ from __future__ import absolute_import, division, unicode_literals from .base_item import BaseItem -from ..compatibility import unescape, urlencode +from ..compatibility import parse_qsl, unescape, urlencode, urlsplit class DirectoryItem(BaseItem): @@ -22,20 +22,35 @@ def __init__(self, fanart=None, plot=None, action=False, + special_sort=None, + date_time=None, category_label=None, + bookmark_id=None, channel_id=None, playlist_id=None, - subscription_id=None): - super(DirectoryItem, self).__init__(name, uri, image, fanart) + subscription_id=None, + **kwargs): + super(DirectoryItem, self).__init__( + name=name, + uri=uri, + image=image, + fanart=fanart, + **kwargs + ) name = self.get_name() self._category_label = None self.set_category_label(category_label or name) self._plot = plot or name self._is_action = action + self._bookmark_id = bookmark_id self._channel_id = channel_id self._playlist_id = playlist_id self._subscription_id = subscription_id self._next_page = False + if special_sort is not None: + self.set_special_sort(special_sort) + if date_time is not None: + self.set_date_from_datetime(date_time=date_time) def set_name(self, name, category_label=None): name = super(DirectoryItem, self).set_name(name) @@ -50,19 +65,15 @@ def set_category_label(self, label): return current_label = self._category_label - if current_label: - if current_label != label: - uri = self.get_uri() - self.set_uri(uri.replace( - urlencode({'category_label': current_label}), - urlencode({'category_label': label}) if label else '', - )) - elif label: - uri = self.get_uri() - self.set_uri(('&' if '?' in uri else '?').join(( - uri, - urlencode({'category_label': label}), - ))) + if current_label or label and current_label != label: + uri = urlsplit(self.get_uri()) + params = dict(parse_qsl(uri.query)) + if label: + params['category_label'] = label + else: + del params['category_label'] + self.set_uri(uri._replace(query=urlencode(params)).geturl()) + self._category_label = label def get_category_label(self): diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/image_item.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/image_item.py index dd04364105..2a9e468ecc 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/image_item.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/image_item.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/media_item.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/media_item.py index 2bb2c7c496..0ce7611a33 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/media_item.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/media_item.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -15,8 +15,8 @@ from . import BaseItem from ..compatibility import datetime_infolabel, to_str, unescape, urlencode -from ..constants import CONTENT -from ..utils import duration_to_seconds, seconds_to_duration +from ..constants import CHANNEL_ID, CONTENT, PLAYLIST_ID, VIDEO_ID +from ..utils.convert_format import duration_to_seconds, seconds_to_duration class MediaItem(BaseItem): @@ -35,8 +35,15 @@ def __init__(self, channel_id=None, playlist_id=None, playlist_item_id=None, - subscription_id=None): - super(MediaItem, self).__init__(name, uri, image, fanart) + subscription_id=None, + **kwargs): + super(MediaItem, self).__init__( + name=name, + uri=uri, + image=image, + fanart=fanart, + **kwargs + ) self._aired = None self._premiered = None self._scheduled_start_utc = None @@ -73,6 +80,53 @@ def __init__(self, self._playlist_id = playlist_id self._playlist_item_id = playlist_item_id + def __str_parts__(self, as_dict=False): + kwargs = { + 'type': self.__class__.__name__, + 'name': self._name, + 'uri': self._uri, + VIDEO_ID: self._video_id, + CHANNEL_ID: self._channel_id, + PLAYLIST_ID: self._playlist_id, + # PLAYLIST_ITEM_ID: self._playlist_item_id, + # SUBSCRIPTION_ID: self._subscription_id, + 'available': self._available, + 'vod': self._vod, + 'live': self._live, + 'completed': self._completed, + 'upcoming': self._upcoming, + 'short': self._short, + 'duration': self._duration, + 'play_count': self._play_count, + 'added': self._added_utc, + 'track_number': self._track_number, + 'filtered': self._filter_reason, + } + if as_dict: + return kwargs + out = ( + '{type}(', + 'name={name!r}, ', + 'uri={uri!r}, ', + 'video_id={video_id!r}, ', + 'channel_id={channel_id!r}, ', + 'playlist_id={playlist_id!r}, ', + # 'playlist_item_id={playlist_item_id!r}, ', + # 'subscription_id={subscription_id!r}, ', + 'available={available!r}, ', + 'vod={vod!r}, ', + 'live={live!r}, ', + 'completed={completed!r}, ', + 'upcoming={upcoming!r}, ', + 'short={short!r}, ', + 'duration={duration!r}, ', + 'play_count={play_count!r}, ', + 'added=\'{added!s}\', ', + 'track_number={track_number!r}, ', + 'filtered={filtered!r})', + ) + return out, kwargs + def set_aired(self, year, month, day): self._aired = date(year, month, day) @@ -347,16 +401,18 @@ def __init__(self, playlist_id=None, playlist_item_id=None, subscription_id=None): - super(AudioItem, self).__init__(name, - uri, - image, - fanart, - plot, - video_id, - channel_id, - playlist_id, - playlist_item_id, - subscription_id) + super(AudioItem, self).__init__( + name=name, + uri=uri, + image=image, + fanart=fanart, + plot=plot, + video_id=video_id, + channel_id=channel_id, + playlist_id=playlist_id, + playlist_item_id=playlist_item_id, + subscription_id=subscription_id, + ) self._album = None def set_album_name(self, album_name): @@ -386,17 +442,21 @@ def __init__(self, channel_id=None, playlist_id=None, playlist_item_id=None, - subscription_id=None): - super(VideoItem, self).__init__(name, - uri, - image, - fanart, - plot, - video_id, - channel_id, - playlist_id, - playlist_item_id, - subscription_id) + subscription_id=None, + **kwargs): + super(VideoItem, self).__init__( + name=name, + uri=uri, + image=image, + fanart=fanart, + plot=plot, + video_id=video_id, + channel_id=channel_id, + playlist_id=playlist_id, + playlist_item_id=playlist_item_id, + subscription_id=subscription_id, + **kwargs + ) self._directors = None self._imdb_id = None diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/menu_items.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/menu_items.py index 308f599b12..4b8db4365e 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,23 +11,56 @@ from __future__ import absolute_import, division, unicode_literals from ..constants import ( + ARTIST, + BOOKMARK_ID, + CHANNEL_ID, + CONTEXT_MENU, + INCOGNITO, + MARK_AS_LABEL, + ORDER, PATHS, + PLAYLIST_ITEM_ID, + PLAYLIST_ID, PLAY_FORCE_AUDIO, PLAY_PROMPT_QUALITY, PLAY_PROMPT_SUBTITLES, PLAY_TIMESHIFT, - PLAY_WITH, + PLAY_USING, + PROPERTY_AS_LABEL, + SUBSCRIPTION_ID, + TITLE, + URI, + VIDEO_ID, WINDOW_RETURN, ) -def more_for_video(context, - video_id, - video_name=None, +ARTIST_INFOLABEL = PROPERTY_AS_LABEL % ARTIST +BOOKMARK_ID_INFOLABEL = PROPERTY_AS_LABEL % BOOKMARK_ID +CHANNEL_ID_INFOLABEL = PROPERTY_AS_LABEL % CHANNEL_ID +PLAYLIST_ID_INFOLABEL = PROPERTY_AS_LABEL % PLAYLIST_ID +PLAYLIST_ITEM_ID_INFOLABEL = PROPERTY_AS_LABEL % PLAYLIST_ITEM_ID +SUBSCRIPTION_ID_INFOLABEL = PROPERTY_AS_LABEL % SUBSCRIPTION_ID +TITLE_INFOLABEL = PROPERTY_AS_LABEL % TITLE +URI_INFOLABEL = PROPERTY_AS_LABEL % URI +VIDEO_ID_INFOLABEL = PROPERTY_AS_LABEL % VIDEO_ID + + +def context_menu_uri(context, path, params=None): + if params is None: + params = {CONTEXT_MENU: True} + else: + params[CONTEXT_MENU] = True + return context.create_uri(path, params, run=True) + + +def video_more_for(context, + video_id=VIDEO_ID_INFOLABEL, + video_name=TITLE_INFOLABEL, logged_in=False, refresh=False): params = { - 'video_id': video_id, + VIDEO_ID: video_id, 'item_name': video_name, 'logged_in': logged_in, } @@ -36,348 +69,422 @@ def more_for_video(context, params['refresh'] = _refresh return ( context.localize('video.more'), - context.create_uri( + context_menu_uri( + context, ('video', 'more',), params, - run=True, ), ) -def related_videos(context, video_id): +def video_related(context, + video_id=VIDEO_ID_INFOLABEL, + video_name=TITLE_INFOLABEL): return ( - context.localize('related_videos'), - context.create_uri( + context.localize('video.related'), + context_menu_uri( + context, (PATHS.ROUTE, PATHS.RELATED_VIDEOS,), { - 'video_id': video_id, + VIDEO_ID: video_id, + 'item_name': video_name, }, - run=True, ), ) -def video_comments(context, video_id, video_name=None): +def video_comments(context, + video_id=VIDEO_ID_INFOLABEL, + video_name=TITLE_INFOLABEL): return ( context.localize('video.comments'), - context.create_uri( + context_menu_uri( + context, (PATHS.ROUTE, PATHS.VIDEO_COMMENTS), { - 'video_id': video_id, + VIDEO_ID: video_id, 'item_name': video_name, }, - run=True, ) ) -def content_from_description(context, video_id): +def video_description_links(context, + video_id=VIDEO_ID_INFOLABEL, + video_name=TITLE_INFOLABEL): return ( context.localize('video.description_links'), - context.create_uri( + context_menu_uri( + context, (PATHS.ROUTE, PATHS.DESCRIPTION_LINKS), { - 'video_id': video_id, + VIDEO_ID: video_id, + 'item_name': video_name, }, - run=True, ) ) -def play_with(context, video_id): +def media_play_using(context, video_id=VIDEO_ID_INFOLABEL): return ( - context.localize('video.play.with'), - context.create_uri( + context.localize('video.play.using'), + context_menu_uri( + context, (PATHS.PLAY,), { - 'video_id': video_id, - PLAY_WITH: True, + VIDEO_ID: video_id, + PLAY_USING: True, }, - run=True, ), ) -def refresh(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.create_uri( - (PATHS.ROUTE, context.get_path(),), - dict(context.get_params(), - refresh=context.refresh_requested(force=True, on=True)), - run=True, + context_menu_uri( + context, + path, + dict(params, + refresh=context.refresh_requested( + force=True, + on=True, + params=params, + )), ), ) -def play_all_from(context, path, order='normal'): +def folder_play(context, path, order='normal'): return ( context.localize('playlist.play.shuffle') if order == 'shuffle' else context.localize('playlist.play.all'), - context.create_uri( + context_menu_uri( + context, (path, 'play',), { 'order': order, }, - run=True, ), ) -def play_video(context): +def media_play(context): return ( context.localize('video.play'), 'Action(Play)' ) -def queue_video(context): +def media_queue(context): return ( context.localize('video.queue'), 'Action(Queue)' ) -def play_playlist(context, playlist_id): +def playlist_play(context, playlist_id=PLAYLIST_ID_INFOLABEL): return ( context.localize('playlist.play.all'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAY,), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, 'order': 'ask', }, - run=True, ), ) -def play_playlist_from(context, playlist_id, video_id): +def playlist_play_from(context, + playlist_id=PLAYLIST_ID_INFOLABEL, + video_id=VIDEO_ID_INFOLABEL): return ( context.localize('playlist.play.from_here'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAY,), { - 'playlist_id': playlist_id, - 'video_id': video_id, + PLAYLIST_ID: playlist_id, + VIDEO_ID: video_id, }, - run=True, ), ) -def play_playlist_recently_added(context, playlist_id): +def playlist_play_recently_added(context, playlist_id=PLAYLIST_ID_INFOLABEL): return ( context.localize('playlist.play.recently_added'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAY,), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, 'recent_days': 1, }, - run=True, ), ) -def view_playlist(context, playlist_id): +def playlist_view(context, playlist_id=PLAYLIST_ID_INFOLABEL): return ( context.localize('playlist.view.all'), - context.create_uri( + context_menu_uri( + context, (PATHS.ROUTE, PATHS.PLAY,), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, 'order': 'normal', 'action': 'list', }, - run=True, ), ) -def shuffle_playlist(context, playlist_id): +def playlist_shuffle(context, playlist_id=PLAYLIST_ID_INFOLABEL): return ( context.localize('playlist.play.shuffle'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAY,), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, 'order': 'shuffle', 'action': 'play', }, - run=True, ), ) -def add_video_to_playlist(context, video_id): +def playlist_add_to(context, + playlist_id, + name_id='playlist', + video_id=VIDEO_ID_INFOLABEL): + return ( + context.localize(('add.to.x', name_id)), + context_menu_uri( + context, + (PATHS.PLAYLIST, 'add', 'video',), + { + PLAYLIST_ID: playlist_id, + VIDEO_ID: video_id, + }, + ), + ) + + +def playlist_add_to_selected(context, video_id=VIDEO_ID_INFOLABEL): return ( context.localize('video.add_to_playlist'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAYLIST, 'select', 'playlist',), { - 'video_id': video_id, + VIDEO_ID: video_id, }, - run=True, ), ) -def remove_video_from_playlist(context, playlist_id, video_id, video_name): +def playlist_remove_from(context, + playlist_id=PLAYLIST_ID_INFOLABEL, + playlist_item_id=PLAYLIST_ITEM_ID_INFOLABEL, + video_id=VIDEO_ID_INFOLABEL, + video_name=TITLE_INFOLABEL): return ( context.localize('remove'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAYLIST, 'remove', 'video',), dict( context.get_params(), - playlist_id=playlist_id, - video_id=video_id, - item_name=video_name, - reload_path=context.get_path(), + **{ + PLAYLIST_ID: playlist_id, + PLAYLIST_ITEM_ID: playlist_item_id, + VIDEO_ID: video_id, + 'item_name': video_name, + 'reload_path': context.get_path(), + } ), - run=True, ), ) -def rename_playlist(context, playlist_id, playlist_name): +def playlist_rename(context, + playlist_id=PLAYLIST_ID_INFOLABEL, + playlist_name=TITLE_INFOLABEL): return ( context.localize('rename'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAYLIST, 'rename', 'playlist',), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, 'item_name': playlist_name }, - run=True, ), ) -def delete_playlist(context, playlist_id, playlist_name): +def playlist_delete(context, + playlist_id=PLAYLIST_ID_INFOLABEL, + playlist_name=TITLE_INFOLABEL): return ( context.localize('delete'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAYLIST, 'remove', 'playlist',), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, 'item_name': playlist_name }, - run=True, ), ) -def remove_as_watch_later(context, playlist_id, playlist_name): +def playlist_save_to_library(context, playlist_id=PLAYLIST_ID_INFOLABEL): return ( - context.localize('watch_later.list.remove'), - context.create_uri( - (PATHS.PLAYLIST, 'remove', 'watch_later',), + context.localize('save'), + context_menu_uri( + context, + (PATHS.PLAYLIST, 'like', 'playlist',), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, + }, + ), + ) + + +def playlist_remove_from_library(context, + playlist_id=PLAYLIST_ID_INFOLABEL, + playlist_name=TITLE_INFOLABEL): + return ( + context.localize('remove'), + context_menu_uri( + context, + (PATHS.PLAYLIST, 'unlike', 'playlist',), + { + PLAYLIST_ID: playlist_id, + 'item_name': playlist_name, + 'reload_path': context.get_path(), + }, + ), + ) + + +def watch_later_list_unassign(context, + playlist_id=PLAYLIST_ID_INFOLABEL, + playlist_name=TITLE_INFOLABEL): + return ( + context.localize('watch_later.list.unassign'), + context_menu_uri( + context, + (PATHS.PLAYLIST, 'unassign', 'watch_later',), + { + PLAYLIST_ID: playlist_id, 'item_name': playlist_name }, - run=True, ), ) -def set_as_watch_later(context, playlist_id, playlist_name): +def watch_later_list_assign(context, + playlist_id=PLAYLIST_ID_INFOLABEL, + playlist_name=TITLE_INFOLABEL): return ( - context.localize('watch_later.list.set'), - context.create_uri( - (PATHS.PLAYLIST, 'set', 'watch_later',), + context.localize('watch_later.list.assign'), + context_menu_uri( + context, + (PATHS.PLAYLIST, 'assign', 'watch_later',), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, 'item_name': playlist_name }, - run=True, ), ) -def remove_as_history(context, playlist_id, playlist_name): +def history_list_unassign(context, + playlist_id=PLAYLIST_ID_INFOLABEL, + playlist_name=TITLE_INFOLABEL): return ( - context.localize('history.list.remove'), - context.create_uri( - (PATHS.PLAYLIST, 'remove', 'history',), + context.localize('history.list.unassign'), + context_menu_uri( + context, + (PATHS.PLAYLIST, 'unassign', 'history',), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, 'item_name': playlist_name }, - run=True, ), ) -def set_as_history(context, playlist_id, playlist_name): +def history_list_assign(context, + playlist_id=PLAYLIST_ID_INFOLABEL, + playlist_name=TITLE_INFOLABEL): return ( - context.localize('history.list.set'), - context.create_uri( - (PATHS.PLAYLIST, 'set', 'history',), + context.localize('history.list.assign'), + context_menu_uri( + context, + (PATHS.PLAYLIST, 'assign', 'history',), { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, 'item_name': playlist_name }, - run=True, ), ) -def remove_my_subscriptions_filter(context, channel_name): +def my_subscriptions_filter_remove(context, channel_name=ARTIST_INFOLABEL): return ( - context.localize('my_subscriptions.filter.remove'), - context.create_uri( + context.localize(('remove.from.x', 'my_subscriptions.filtered')), + context_menu_uri( + context, ('my_subscriptions', 'filter', 'remove'), { 'item_name': channel_name, }, - run=True, ), ) -def add_my_subscriptions_filter(context, channel_name): +def my_subscriptions_filter_add(context, channel_name=ARTIST_INFOLABEL): return ( - context.localize('my_subscriptions.filter.add'), - context.create_uri( + context.localize(('add.to.x', 'my_subscriptions.filtered')), + context_menu_uri( + context, ('my_subscriptions', 'filter', 'add',), { 'item_name': channel_name, }, - run=True, ), ) -def rate_video(context, video_id, refresh=False): +def video_rate(context, video_id=VIDEO_ID_INFOLABEL, refresh=False): params = { - 'video_id': video_id, + VIDEO_ID: video_id, } _refresh = context.refresh_requested(force=True, on=refresh) if _refresh: params['refresh'] = _refresh return ( context.localize('video.rate'), - context.create_uri( + context_menu_uri( + context, ('video', 'rate',), params, - run=True, - ), - ) - - -def watch_later_add(context, playlist_id, video_id): - return ( - context.localize('watch_later.add'), - context.create_uri( - (PATHS.PLAYLIST, 'add', 'video',), - { - 'playlist_id': playlist_id, - 'video_id': video_id, - }, - run=True, ), ) @@ -385,27 +492,29 @@ def watch_later_add(context, playlist_id, video_id): def watch_later_local_add(context, item): return ( context.localize('watch_later.add'), - context.create_uri( + context_menu_uri( + context, (PATHS.WATCH_LATER, 'add',), { - 'video_id': item.video_id, + VIDEO_ID: item.video_id, 'item': repr(item), }, - run=True, ), ) -def watch_later_local_remove(context, video_id, video_name=''): +def watch_later_local_remove(context, + video_id=VIDEO_ID_INFOLABEL, + video_name=TITLE_INFOLABEL): return ( context.localize('watch_later.remove'), - context.create_uri( + context_menu_uri( + context, (PATHS.WATCH_LATER, 'remove',), { - 'video_id': video_id, + VIDEO_ID: video_id, 'item_name': video_name, }, - run=True, ), ) @@ -413,173 +522,199 @@ def watch_later_local_remove(context, video_id, video_name=''): def watch_later_local_clear(context): return ( context.localize('watch_later.clear'), - context.create_uri( + context_menu_uri( + context, (PATHS.WATCH_LATER, 'clear',), - run=True, ), ) -def go_to_channel(context, channel_id, channel_name): +def channel_go_to(context, + channel_id=CHANNEL_ID_INFOLABEL, + channel_name=ARTIST_INFOLABEL): return ( - context.localize('go_to_channel') % context.get_ui().bold(channel_name), - context.create_uri( + context.localize('go_to.x', context.get_ui().bold(channel_name)), + context_menu_uri( + context, (PATHS.ROUTE, PATHS.CHANNEL, channel_id,), - run=True, + { + 'category_label': channel_name, + } ), ) -def subscribe_to_channel(context, channel_id, channel_name=''): +def channel_subscribe_to(context, + channel_id=CHANNEL_ID_INFOLABEL, + channel_name=ARTIST_INFOLABEL): return ( - context.localize('subscribe_to') % context.get_ui().bold(channel_name) + context.localize('subscribe_to.x', context.get_ui().bold(channel_name)) if channel_name else context.localize('subscribe'), - context.create_uri( + context_menu_uri( + context, ('subscriptions', 'add',), { - 'subscription_id': channel_id, + SUBSCRIPTION_ID: channel_id, }, - run=True, ), ) -def unsubscribe_from_channel(context, channel_id=None, subscription_id=None): +def channel_unsubscribe_from(context, channel_id=None, subscription_id=None): return ( context.localize('unsubscribe'), - context.create_uri( + context_menu_uri( + context, ('subscriptions', 'remove',), { - 'subscription_id': subscription_id, + SUBSCRIPTION_ID: subscription_id, }, - run=True, ) if subscription_id else - context.create_uri( + context_menu_uri( + context, ('subscriptions', 'remove',), { - 'channel_id': channel_id, + CHANNEL_ID: channel_id, }, - run=True, ), ) -def play_with_subtitles(context, video_id): +def media_play_with_subtitles(context, + video_id=VIDEO_ID_INFOLABEL): return ( context.localize('video.play.with_subtitles'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAY,), { - 'video_id': video_id, + VIDEO_ID: video_id, PLAY_PROMPT_SUBTITLES: True, }, - run=True, ), ) -def play_audio_only(context, video_id): +def media_play_audio_only(context, + video_id=VIDEO_ID_INFOLABEL): return ( context.localize('video.play.audio_only'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAY,), { - 'video_id': video_id, + VIDEO_ID: video_id, PLAY_FORCE_AUDIO: True, }, - run=True, ), ) -def play_ask_for_quality(context, video_id): +def media_play_ask_for_quality(context, + video_id=VIDEO_ID_INFOLABEL): return ( context.localize('video.play.ask_for_quality'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAY,), { - 'video_id': video_id, + VIDEO_ID: video_id, PLAY_PROMPT_QUALITY: True, }, - run=True, ), ) -def play_timeshift(context, video_id): +def media_play_timeshift(context, + video_id=VIDEO_ID_INFOLABEL): return ( context.localize('video.play.timeshift'), - context.create_uri( + context_menu_uri( + context, (PATHS.PLAY,), { - 'video_id': video_id, + VIDEO_ID: video_id, PLAY_TIMESHIFT: True, }, - run=True, ), ) -def history_remove(context, video_id, video_name=''): +def history_local_remove(context, + video_id=VIDEO_ID_INFOLABEL, + video_name=TITLE_INFOLABEL): return ( context.localize('history.remove'), - context.create_uri( + context_menu_uri( + context, (PATHS.HISTORY, 'remove',), { - 'video_id': video_id, + VIDEO_ID: video_id, 'item_name': video_name, }, - run=True, ), ) -def history_clear(context): +def history_local_clear(context): return ( context.localize('history.clear'), - context.create_uri( + context_menu_uri( + context, (PATHS.HISTORY, 'clear',), - run=True, ), ) -def history_mark_watched(context, video_id): +def history_local_mark_as(context, video_id=VIDEO_ID_INFOLABEL): + return ( + PROPERTY_AS_LABEL % MARK_AS_LABEL, + context_menu_uri( + context, + (PATHS.HISTORY, 'mark_as',), + { + VIDEO_ID: video_id, + }, + ), + ) + + +def history_local_mark_watched(context, video_id=VIDEO_ID_INFOLABEL): return ( context.localize('history.mark.watched'), - context.create_uri( + context_menu_uri( + context, (PATHS.HISTORY, 'mark_watched',), { - 'video_id': video_id, + VIDEO_ID: video_id, }, - run=True, ), ) -def history_mark_unwatched(context, video_id): +def history_local_mark_unwatched(context, video_id=VIDEO_ID_INFOLABEL): return ( context.localize('history.mark.unwatched'), - context.create_uri( + context_menu_uri( + context, (PATHS.HISTORY, 'mark_unwatched',), { - 'video_id': video_id, + VIDEO_ID: video_id, }, - run=True, ), ) -def history_reset_resume(context, video_id): +def history_local_reset_resume(context, video_id=VIDEO_ID_INFOLABEL): return ( context.localize('history.reset.resume_point'), - context.create_uri( + context_menu_uri( + context, (PATHS.HISTORY, 'reset_resume',), { - 'video_id': video_id, + VIDEO_ID: video_id, }, - run=True, ), ) @@ -587,44 +722,66 @@ def history_reset_resume(context, video_id): def bookmark_add(context, item): return ( context.localize('bookmark'), - context.create_uri( + context_menu_uri( + context, (PATHS.BOOKMARKS, 'add',), { 'item_id': item.get_id(), 'item': repr(item), }, - run=True, ), ) -def bookmark_add_channel(context, channel_id, channel_name=''): +def bookmark_add_channel(context, + channel_id=CHANNEL_ID_INFOLABEL, + channel_name=ARTIST_INFOLABEL): return ( - (context.localize('bookmark.channel') % ( - context.get_ui().bold(channel_name) if channel_name else - context.localize('channel') - )), - context.create_uri( + context.localize('bookmark.x', + context.get_ui().bold(channel_name) + if channel_name else + context.localize('channel')), + context_menu_uri( + context, (PATHS.BOOKMARKS, 'add',), { 'item_id': channel_id, 'item': None, }, - run=True, ), ) -def bookmark_remove(context, item_id, item_name=''): +def bookmark_edit(context, + item_id=BOOKMARK_ID_INFOLABEL, + item_name=TITLE_INFOLABEL, + item_uri=URI_INFOLABEL): + return ( + context.localize(('edit.x', 'bookmark')), + context_menu_uri( + context, + (PATHS.BOOKMARKS, 'edit',), + { + 'item_id': item_id, + 'item_name': item_name, + 'uri': item_uri, + }, + ), + ) + + +def bookmark_remove(context, + item_id=BOOKMARK_ID_INFOLABEL, + item_name=TITLE_INFOLABEL): return ( context.localize('bookmark.remove'), - context.create_uri( + context_menu_uri( + context, (PATHS.BOOKMARKS, 'remove',), { 'item_id': item_id, 'item_name': item_name, }, - run=True, ), ) @@ -632,9 +789,9 @@ def bookmark_remove(context, item_id, item_name=''): def bookmarks_clear(context): return ( context.localize('bookmarks.clear'), - context.create_uri( + context_menu_uri( + context, (PATHS.BOOKMARKS, 'clear',), - run=True, ), ) @@ -642,12 +799,12 @@ def bookmarks_clear(context): def search_remove(context, query): return ( context.localize('search.remove'), - context.create_uri( + context_menu_uri( + context, (PATHS.SEARCH, 'remove',), { 'q': query, }, - run=True, ), ) @@ -655,12 +812,12 @@ def search_remove(context, query): def search_rename(context, query): return ( context.localize('search.rename'), - context.create_uri( + context_menu_uri( + context, (PATHS.SEARCH, 'rename',), { 'q': query, }, - run=True, ), ) @@ -668,22 +825,23 @@ def search_rename(context, query): def search_clear(context): return ( context.localize('search.clear'), - context.create_uri( + context_menu_uri( + context, (PATHS.SEARCH, 'clear',), - run=True, ), ) def search_sort_by(context, params, order): - selected = params.get('order', 'relevance') == order + selected = params.get(ORDER, 'relevance') == order order_label = context.localize('search.sort.' + order) return ( context.localize('search.sort').format( context.get_ui().bold(order_label) if selected else order_label ), - context.create_uri( - (PATHS.ROUTE, PATHS.SEARCH, 'query',), + context_menu_uri( + context, + (PATHS.ROUTE, context.get_path(),), params=dict(params, order=order, page=1, @@ -691,7 +849,6 @@ def search_sort_by(context, params, order): pageToken='', window_replace=True, window_return=False), - run=True, ), ) @@ -706,12 +863,12 @@ def separator(): def goto_home(context): return ( context.localize('home'), - context.create_uri( + context_menu_uri( + context, (PATHS.ROUTE, PATHS.HOME,), { WINDOW_RETURN: False, }, - run=True, ), ) @@ -720,17 +877,17 @@ def goto_quick_search(context, params=None, incognito=None): if params is None: params = {} if incognito is None: - incognito = params.get('incognito') + incognito = params.get(INCOGNITO) else: - params['incognito'] = incognito + params[INCOGNITO] = incognito return ( context.localize('search.quick.incognito' if incognito else 'search.quick'), - context.create_uri( + context_menu_uri( + context, (PATHS.ROUTE, PATHS.SEARCH, 'input',), params, - run=True, ), ) @@ -738,9 +895,9 @@ def goto_quick_search(context, params=None, incognito=None): def goto_page(context, params=None): return ( context.localize('page.choose'), - context.create_uri( + context_menu_uri( + context, (PATHS.GOTO_PAGE, context.get_path(),), params or context.get_params(), - run=True, ), ) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/next_page_item.py index bfebc33eb5..c3a9aaad74 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,26 +12,48 @@ from . import menu_items from .directory_item import DirectoryItem -from ..constants import PATHS +from ..constants import ITEMS_PER_PAGE, PAGE, PATHS class NextPageItem(DirectoryItem): - def __init__(self, context, params, image=None, fanart=None): - if 'refresh' in params: - del params['refresh'] + NEXT_PAGE_PARAM_EXCLUSIONS = ( + 'refresh', + ) + JUMP_PAGE_PARAM_EXCLUSIONS = ( + 'click_tracking', + 'exclude', + 'filtered', + 'page', + 'refresh', + 'visitor', + ) + def __init__(self, context, params, image=None, fanart=None): path = context.get_path() - page = params.get('page') or 2 - items_per_page = params.get('items_per_page') or 50 + + page = params.get(PAGE) or 2 + is_first_page_link = page < 2 + + items_per_page = params.get(ITEMS_PER_PAGE) or 50 can_jump = ('next_page_token' not in params and not path.startswith(('/channel', PATHS.RECOMMENDATIONS, - PATHS.RELATED_VIDEOS))) - can_search = not path.startswith(PATHS.SEARCH) - if 'page_token' not in params and can_jump: + PATHS.RELATED_VIDEOS, + PATHS.VIRTUAL_PLAYLIST))) + if can_jump and not is_first_page_link and 'page_token' not in params: params['page_token'] = self.create_page_token(page, items_per_page) - name = context.localize('page.next') % page + can_search = not path.startswith(PATHS.SEARCH) + + for param in ( + self.JUMP_PAGE_PARAM_EXCLUSIONS + if is_first_page_link else + self.NEXT_PAGE_PARAM_EXCLUSIONS + ): + if param in params: + del params[param] + + name = context.localize('page.next', page) filtered = params.get('filtered') if filtered: name = ''.join(( @@ -49,13 +71,14 @@ def __init__(self, context, params, image=None, fanart=None): image=image, fanart=fanart, category_label='__inherit__', + special_sort='bottom', ) self.next_page = page self.items_per_page = items_per_page context_menu = [ - menu_items.refresh(context), + menu_items.refresh_listing(context), menu_items.goto_page(context, params) if can_jump else None, menu_items.goto_home(context), menu_items.goto_quick_search(context) if can_search else None, diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/search_items.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/search_items.py index c54fc5a1e8..6a9072a8aa 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/search_items.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/search_items.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,7 +12,7 @@ from . import menu_items from .directory_item import DirectoryItem -from ..constants import PATHS +from ..constants import CHANNEL_ID, INCOGNITO, PATHS class SearchItem(DirectoryItem): @@ -88,14 +88,18 @@ class NewSearchItem(DirectoryItem): def __init__(self, context, name=None, + title=None, image=None, fanart=None, incognito=False, channel_id='', addon_id='', - location=False): + location=False, + **kwargs): if not name: - name = context.get_ui().bold(context.localize('search.new')) + name = context.get_ui().bold( + title or context.localize('search.new') + ) if image is None: image = '{media}/new_search.png' @@ -104,9 +108,9 @@ def __init__(self, if addon_id: params['addon_id'] = addon_id if incognito: - params['incognito'] = incognito + params[INCOGNITO] = incognito if channel_id: - params['channel_id'] = channel_id + params[CHANNEL_ID] = channel_id if location: params['location'] = location @@ -116,7 +120,8 @@ def __init__(self, params=params, ), image=image, - fanart=fanart) + fanart=fanart, + **kwargs) if context.is_plugin_path(context.get_uri(), ((PATHS.SEARCH, 'list'),)): context_menu = [ diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/uri_item.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/uri_item.py index c5926afb2c..9f5487f82b 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/uri_item.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/uri_item.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/utils.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/utils.py index 7b595da428..efa03ad9af 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/utils.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -13,15 +13,18 @@ import json from datetime import date, datetime +from .bookmark_item import BookmarkItem from .directory_item import DirectoryItem from .image_item import ImageItem from .media_item import AudioItem, VideoItem +from .. import logging from ..compatibility import string_type, to_str -from ..utils.datetime_parser import strptime +from ..utils.datetime import strptime _ITEM_TYPES = { 'AudioItem': AudioItem, + 'BookmarkItem': BookmarkItem, 'DirectoryItem': DirectoryItem, 'ImageItem': ImageItem, 'VideoItem': VideoItem, @@ -67,13 +70,16 @@ def from_json(json_data, *args): item_type = json_data.get('type') if not item_type or item_type not in _ITEM_TYPES: + logging.warning_trace(('Unsupported item type', 'Data: {data!r}'), + data=json_data) return None - item = _ITEM_TYPES[item_type](name='', uri='') + item_data = json_data.get('data') + if not item_data: + return None - for key, value in json_data.get('data', {}).items(): - if hasattr(item, key): - setattr(item, key, value) + item = _ITEM_TYPES[item_type](name='', uri='') + item.__dict__.update(item_data) if bookmark_id: item.bookmark_id = bookmark_id diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/watch_later_item.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/watch_later_item.py index 9006720b66..f44187f1ed 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/watch_later_item.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/watch_later_item.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index 8d7a5b339e..960c01d589 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -19,19 +19,26 @@ MediaItem, VideoItem, ) +from ... import logging from ...compatibility import to_str, xbmc, xbmcgui from ...constants import ( + ACTION, + BOOKMARK_ID, CHANNEL_ID, - PLAYLISTITEM_ID, + PATHS, + PLAYLIST_ITEM_ID, PLAYLIST_ID, - PLAY_COUNT, + PLAY_COUNT_PROP, PLAY_STRM, PLAY_TIMESHIFT, - PLAY_WITH, + PLAY_USING, SUBSCRIPTION_ID, + VALUE_TO_STR, VIDEO_ID, ) -from ...utils import current_system_version, datetime_parser, redact_ip_in_uri +from ...utils.datetime import datetime_to_since, utc_to_local +from ...utils.redact import redact_ip_in_uri +from ...utils.system_version import current_system_version def set_info(list_item, item, properties, set_play_count=True, resume=True): @@ -85,7 +92,7 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): if value is not None: if set_play_count: info_labels['playcount'] = value - properties[PLAY_COUNT] = value + properties[PLAY_COUNT_PROP] = value value = item.get_rating() if value is not None: @@ -104,15 +111,13 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): info_labels['year'] = value resume_time = resume and item.get_start_time() - if resume_time: + if resume_time is not None: properties['ResumeTime'] = str(resume_time) duration = item.get_duration() - if duration: + if duration > 0: properties['TotalTime'] = str(duration) if info_type == 'video': list_item.addStreamInfo(info_type, {'duration': duration}) - - if duration is not None: info_labels['duration'] = duration elif isinstance(item, DirectoryItem): @@ -276,7 +281,7 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): info_tag.setPlaycount(value) elif info_type == 'music': info_tag.setPlayCount(value) - properties[PLAY_COUNT] = value + properties[PLAY_COUNT_PROP] = value # rating: float value = item.get_rating() @@ -305,25 +310,26 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): resume_time = resume and item.get_start_time() duration = item.get_duration() if info_type == 'video': - if resume_time and duration: - info_tag.setResumePoint(resume_time, float(duration)) - elif resume_time: - info_tag.setResumePoint(resume_time) - if duration: + if resume_time is not None: + if duration > 0: + info_tag.setResumePoint(resume_time, float(duration)) + else: + info_tag.setResumePoint(resume_time) + if duration > 0: info_tag.addVideoStream(xbmc.VideoStreamDetail( duration=duration, )) elif info_type == 'music': # These properties are deprecated but there is no other way to set # these details for a ListItem with a MusicInfoTag - if resume_time: + if resume_time is not None: properties['ResumeTime'] = str(resume_time) - if duration: + if duration > 0: properties['TotalTime'] = str(duration) # duration: int # As seconds - if duration is not None: + if duration > 0: info_tag.setDuration(duration) elif isinstance(item, DirectoryItem): @@ -406,14 +412,15 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): def playback_item(context, media_item, show_fanart=None, **_kwargs): uri = media_item.get_uri() - context.log_debug('Converting %s |%s|' % (media_item.__class__.__name__, - redact_ip_in_uri(uri))) + logging.debug('Converting %s for playback: %r', + media_item.__class__.__name__, + redact_ip_in_uri(uri)) params = context.get_params() settings = context.get_settings() ui = context.get_ui() - is_external = ui.get_property(PLAY_WITH) + is_external = ui.get_property(PLAY_USING) is_strm = params.get(PLAY_STRM) mime_type = None @@ -431,9 +438,10 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): 'offscreen': True, } props = { - 'isPlayable': str(media_item.playable).lower(), + 'isPlayable': VALUE_TO_STR[media_item.playable], 'playlist_type_hint': ( - xbmc.PLAYLIST_MUSIC if isinstance(media_item, AudioItem) else + xbmc.PLAYLIST_MUSIC + if isinstance(media_item, AudioItem) else xbmc.PLAYLIST_VIDEO ), } @@ -531,7 +539,15 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): def directory_listitem(context, directory_item, show_fanart=None, **_kwargs): uri = directory_item.get_uri() - context.log_debug('Converting DirectoryItem |%s|' % uri) + is_action = directory_item.is_action() + if not is_action: + path, params = context.parse_uri(uri) + if path.rstrip('/') == PATHS.PLAY and params.get(ACTION) != 'list': + is_action = True + if is_action: + logging.debug('Converting DirectoryItem action: %r', uri) + else: + logging.debug('Converting DirectoryItem: %r', uri) kwargs = { 'label': directory_item.get_name(), @@ -546,23 +562,39 @@ def directory_listitem(context, directory_item, show_fanart=None, **_kwargs): if directory_item.next_page: props['specialSort'] = 'bottom' else: - special_sort = 'top' + _special_sort = directory_item.get_special_sort() + if _special_sort is None: + special_sort = 'top' + elif _special_sort is False: + special_sort = None + else: + special_sort = _special_sort prop_value = directory_item.subscription_id if prop_value: - special_sort = None + special_sort = _special_sort props[SUBSCRIPTION_ID] = prop_value prop_value = directory_item.channel_id if prop_value: - special_sort = None + special_sort = _special_sort props[CHANNEL_ID] = prop_value prop_value = directory_item.playlist_id if prop_value: - special_sort = None + special_sort = _special_sort props[PLAYLIST_ID] = prop_value + prop_value = directory_item.bookmark_id + if prop_value: + special_sort = _special_sort + props[BOOKMARK_ID] = prop_value + + prop_value = is_action and getattr(directory_item, VIDEO_ID, None) + if prop_value: + special_sort = _special_sort + props[VIDEO_ID] = prop_value + if special_sort: props['specialSort'] = special_sort @@ -581,29 +613,16 @@ def directory_listitem(context, directory_item, show_fanart=None, **_kwargs): set_info(list_item, directory_item, props) - """ - ListItems that do not open a lower level list should have the isFolder - parameter of the xbmcplugin.addDirectoryItem set to False, however this - now appears to mark the ListItem as playable, even if the IsPlayable - property is not set or set to "false". - Set isFolder to True as a workaround, regardless of whether the ListItem - is actually a folder. - """ - # Workaround: - # is_folder = True - # Test correctly setting isFolder: - is_folder = not directory_item.is_action() - context_menu = directory_item.get_context_menu() if context_menu is not None: list_item.addContextMenuItems(context_menu) - return uri, list_item, is_folder + return uri, list_item, not is_action def image_listitem(context, image_item, show_fanart=None, **_kwargs): uri = image_item.get_uri() - context.log_debug('Converting ImageItem |%s|' % uri) + logging.debug('Converting ImageItem: %r', uri) kwargs = { 'label': image_item.get_name(), @@ -611,7 +630,7 @@ def image_listitem(context, image_item, show_fanart=None, **_kwargs): 'offscreen': True, } props = { - 'isPlayable': str(image_item.playable).lower(), + 'isPlayable': VALUE_TO_STR[image_item.playable], 'ForceResolvePlugin': 'true', } @@ -636,9 +655,9 @@ def image_listitem(context, image_item, show_fanart=None, **_kwargs): return uri, list_item, False -def uri_listitem(context, uri_item, **_kwargs): +def uri_listitem(_context, uri_item, **_kwargs): uri = uri_item.get_uri() - context.log_debug('Converting UriItem |%s|' % uri) + logging.debug('Converting UriItem: %r', uri) kwargs = { 'label': uri_item.get_name(), @@ -646,7 +665,7 @@ def uri_listitem(context, uri_item, **_kwargs): 'offscreen': True, } props = { - 'isPlayable': str(uri_item.playable).lower(), + 'isPlayable': VALUE_TO_STR[uri_item.playable], 'ForceResolvePlugin': 'true', } @@ -658,12 +677,10 @@ def uri_listitem(context, uri_item, **_kwargs): def media_listitem(context, media_item, show_fanart=None, - focused=None, - played=None, + to_sync=None, **_kwargs): uri = media_item.get_uri() - context.log_debug('Converting %s |%s|' % (media_item.__class__.__name__, - uri)) + logging.debug('Converting %s: %r', media_item.__class__.__name__, uri) kwargs = { 'label': media_item.get_name(), @@ -672,10 +689,11 @@ def media_listitem(context, 'offscreen': True, } props = { - 'isPlayable': str(media_item.playable).lower(), + 'isPlayable': VALUE_TO_STR[media_item.playable], 'ForceResolvePlugin': 'true', 'playlist_type_hint': ( - xbmc.PLAYLIST_MUSIC if isinstance(media_item, AudioItem) else + xbmc.PLAYLIST_MUSIC + if isinstance(media_item, AudioItem) else xbmc.PLAYLIST_VIDEO ), } @@ -685,12 +703,12 @@ def media_listitem(context, datetime = scheduled_start or published_at local_datetime = None if datetime: - local_datetime = datetime_parser.utc_to_local(datetime) + local_datetime = utc_to_local(datetime) props['PublishedLocal'] = to_str(local_datetime) if media_item.live: props['PublishedSince'] = context.localize('live') elif local_datetime: - props['PublishedSince'] = to_str(datetime_parser.datetime_to_since( + props['PublishedSince'] = to_str(datetime_to_since( context, local_datetime )) @@ -698,14 +716,11 @@ def media_listitem(context, resume = True prop_value = media_item.video_id if prop_value: - if focused and prop_value == focused: - set_play_count = False - resume = False - if played and prop_value == played: - set_play_count = 'forced' - resume = False props[VIDEO_ID] = prop_value + if to_sync and prop_value in to_sync: + set_play_count = False + # make channel_id property available for keymapping prop_value = media_item.channel_id if prop_value: @@ -724,7 +739,12 @@ def media_listitem(context, # make playlist_item_id property available for keymapping prop_value = media_item.playlist_item_id if prop_value: - props[PLAYLISTITEM_ID] = prop_value + props[PLAYLIST_ITEM_ID] = prop_value + + # make bookmark_id property available for keymapping + prop_value = media_item.bookmark_id + if prop_value: + props[BOOKMARK_ID] = prop_value list_item = xbmcgui.ListItem(**kwargs) @@ -744,19 +764,9 @@ def media_listitem(context, set_info(list_item, media_item, props, - set_play_count=set_play_count and set_play_count != 'forced', + set_play_count=set_play_count, resume=resume) - if not set_play_count: - video_id = media_item.video_id - playback_history = context.get_playback_history() - playback_history.set_item(video_id, dict( - playback_history.get_item(video_id) or {}, - play_count=int(not media_item.get_play_count()), - played_time=0.0, - played_percent=0, - )) - context_menu = media_item.get_context_menu() if context_menu: list_item.addContextMenuItems(context_menu) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/__init__.py index 0e05ebe418..a0e9551816 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index 0695eda7b4..6bc8267e69 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,13 +10,11 @@ import time import uuid -from hashlib import md5 from .json_store import JSONStore +from ..compatibility import string_type from ..constants import ADDON_ID - - -__author__ = 'bromix' +from ..utils.methods import generate_hash class AccessManager(JSONStore): @@ -26,112 +24,161 @@ class AccessManager(JSONStore): 'token_expires': -1, 'last_key_hash': '', 'name': 'Default', + 'id': None, 'watch_later': 'WL', 'watch_history': 'HL' } + DEFAULT_NEW_DEVELOPER = { + 'access_token': '', + 'refresh_token': '', + 'token_expires': -1, + 'last_key_hash': '' + } def __init__(self, context): - super(AccessManager, self).__init__('access_manager.json') - self._context = 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) def set_defaults(self, reset=False): data = {} if reset else self.get_data() - if 'access_manager' not in data: - data = { - 'access_manager': { - 'users': { - 0: self.DEFAULT_NEW_USER.copy() - } - } + + access_manager = data.get('access_manager') + if not access_manager or not isinstance(access_manager, dict): + users = { + 0: self.DEFAULT_NEW_USER.copy(), } - if 'users' not in data['access_manager']: - data['access_manager']['users'] = { - 0: self.DEFAULT_NEW_USER.copy() + access_manager = { + 'users': users, + 'current_user': 0, + 'last_origin': ADDON_ID, + 'developers': {}, } - if 0 not in data['access_manager']['users']: - data['access_manager']['users'][0] = self.DEFAULT_NEW_USER.copy() - if 'current_user' not in data['access_manager']: - data['access_manager']['current_user'] = 0 - if 'last_origin' not in data['access_manager']: - data['access_manager']['last_origin'] = ADDON_ID - if 'developers' not in data['access_manager']: - data['access_manager']['developers'] = {} - - # clean up - if data['access_manager']['current_user'] == 'default': - data['access_manager']['current_user'] = 0 - if 'access_token' in data['access_manager']: - del data['access_manager']['access_token'] - if 'refresh_token' in data['access_manager']: - del data['access_manager']['refresh_token'] - if 'token_expires' in data['access_manager']: - del data['access_manager']['token_expires'] - if 'default' in data['access_manager']: - if ((data['access_manager']['default'].get('access_token') - or data['access_manager']['default'].get('refresh_token')) - and not data['access_manager']['users'][0].get( - 'access_token') - and not data['access_manager']['users'][0].get( - 'refresh_token')): - if 'name' not in data['access_manager']['default']: - data['access_manager']['default']['name'] = 'Default' - data['access_manager']['users'][0] = data['access_manager'][ - 'default'] - del data['access_manager']['default'] - # end clean up - - current_user = data['access_manager']['current_user'] - if 'watch_later' not in data['access_manager']['users'][current_user]: - data['access_manager']['users'][current_user]['watch_later'] = 'WL' - if 'watch_history' not in data['access_manager']['users'][current_user]: - data['access_manager']['users'][current_user][ - 'watch_history'] = 'HL' + else: + users = access_manager.get('users') + if not users or not isinstance(users, dict): + users = { + 0: self.DEFAULT_NEW_USER.copy(), + } + elif any(not isinstance(user_id, int) for user_id in users): + new_users = {} + old_users = {} + for user_id, user in users.items(): + if isinstance(user_id, int): + new_users[user_id] = user + else: + try: + user_id = int(user_id) + if user_id in users: + raise ValueError + new_users[user_id] = user + except (TypeError, ValueError): + old_users[user_id] = user + if new_users: + users = new_users + if old_users: + new_user_id = max(users) + 1 if users else 0 + for user in old_users.values(): + users[new_user_id] = user + new_user_id += 1 + access_manager['users'] = users + + current_id = access_manager.get('current_user') + if (not current_id + or current_id == 'default' + or current_id not in users): + current_id = min(users) + else: + if not isinstance(current_id, int): + try: + current_id = int(current_id) + if current_id not in users: + raise ValueError + except (TypeError, ValueError): + current_id = min(users) + access_manager['current_user'] = current_id + current_user = users[current_id] + current_user.setdefault('watch_later', 'WL') + current_user.setdefault('watch_history', 'HL') + + if 'default' in access_manager: + default_user = access_manager['default'] + if (isinstance(default_user, dict) + and (default_user.get('access_token') + or default_user.get('refresh_token')) + and not current_user.get('access_token') + and not current_user.get('refresh_token')): + default_user.setdefault('name', 'Default') + users[current_id] = default_user + del access_manager['default'] + + if 'access_token' in access_manager: + del access_manager['access_token'] + + if 'refresh_token' in access_manager: + del access_manager['refresh_token'] + + if 'token_expires' in access_manager: + del access_manager['token_expires'] + + last_origin = access_manager.get('last_origin') + if not last_origin or not isinstance(last_origin, string_type): + access_manager['last_origin'] = ADDON_ID + + developers = access_manager.get('developers') + if not developers or not isinstance(developers, dict): + access_manager['developers'] = {} + data['access_manager'] = access_manager # ensure all users have uuid uuids = set() - for user in data['access_manager']['users'].values(): - c_uuid = user.get('id') - while not c_uuid or c_uuid in uuids: - c_uuid = uuid.uuid4().hex - uuids.add(c_uuid) - user['id'] = c_uuid + for user in users.values(): + user_uuid = user.get('id') + if user_uuid: + if user_uuid in uuids: + user['old_id'] = user_uuid + user_uuid = None + else: + uuids.add(user_uuid) + continue + while not user_uuid or user_uuid in uuids: + user_uuid = uuid.uuid4().hex + uuids.add(user_uuid) + user['id'] = user_uuid # end uuid check - self.save(data) + return self.save(data) @staticmethod def _process_data(data): - # process users, change str keys (old format) to int (current format) - users = data['access_manager']['users'] - if '0' in users: - data['access_manager']['users'] = { - int(key): value - for key, value in users.items() - } - current_user = data['access_manager']['current_user'] - try: - data['access_manager']['current_user'] = int(current_user) - except (TypeError, ValueError): - pass - return data - - def get_data(self, process=_process_data.__func__): - return super(AccessManager, self).get_data(process) - - def load(self, process=_process_data.__func__): - return super(AccessManager, self).load(process) - - def save(self, data, update=False, process=_process_data.__func__): - return super(AccessManager, self).save(data, update, process) + output = {} + for key, value in data: + if key in output: + continue + if key == 'current_user': + try: + value = int(value) + except (TypeError, ValueError): + value = 0 + else: + try: + key = int(key) + except (TypeError, ValueError): + pass + output[key] = value + return output def get_current_user_details(self, addon_id=None): """ :return: current user """ - if addon_id: + if addon_id and addon_id != ADDON_ID: return self.get_developers().get(addon_id, {}) return self.get_users()[self._user] @@ -153,23 +200,17 @@ def get_new_user(self, username=''): new_uuid = None while not new_uuid or new_uuid in uuids: new_uuid = uuid.uuid4().hex - return { - 'access_token': '', - 'refresh_token': '', - 'token_expires': -1, - 'last_key_hash': '', - 'name': username, - 'id': new_uuid, - 'watch_later': 'WL', - 'watch_history': 'HL' - } + return dict(self.DEFAULT_NEW_USER, + name=username, + id=new_uuid) def get_users(self): """ 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): """ @@ -283,21 +324,23 @@ def get_watch_later_id(self): Returns the current users watch later playlist id :return: the current users watch later playlist id """ - current_id = (self.get_current_user_details().get('watch_later') - or 'WL').strip() + current_id = self.get_current_user_details().get('watch_later', '') + current_id = current_id.strip() + current_id_lower = current_id.lower() settings = self._context.get_settings() settings_id = settings.get_watch_later_playlist() + settings_id_lower = settings_id.lower() - if settings_id.lower() == 'wl': + if settings_id_lower == 'local': current_id = self.set_watch_later_id(None) - elif settings_id and settings_id != current_id: + elif settings_id and settings_id_lower != current_id_lower: current_id = self.set_watch_later_id(settings_id) - elif current_id: - if current_id.lower() == 'wl': - current_id = '' - elif settings_id: - settings.set_watch_later_playlist('') + elif current_id_lower == 'local': + current_id = '' + + if settings_id: + settings.set_watch_later_playlist('') return current_id @@ -334,21 +377,24 @@ def get_watch_history_id(self): Returns the current users watch history playlist id :return: the current users watch history playlist id """ - current_id = (self.get_current_user_details().get('watch_history') - or 'HL').strip() + + current_id = self.get_current_user_details().get('watch_history', '') + current_id = current_id.strip() + current_id_lower = current_id.lower() settings = self._context.get_settings() settings_id = settings.get_history_playlist() + settings_id_lower = settings_id.lower() - if settings_id.lower() == 'hl': + if settings_id_lower == 'local': current_id = self.set_watch_history_id(None) - elif settings_id and settings_id != current_id: + elif settings_id and settings_id_lower != current_id_lower: current_id = self.set_watch_history_id(settings_id) - elif current_id: - if current_id.lower() == 'hl': - current_id = '' - elif settings_id: - settings.set_history_playlist('') + elif current_id_lower == 'local': + current_id = '' + + if settings_id: + settings.set_history_playlist('') return current_id @@ -401,36 +447,32 @@ def get_last_origin(self): """ return self._last_origin - def get_access_token(self, addon_id=None): - """ - Returns the access token for some API - :return: access_token - """ - details = self.get_current_user_details(addon_id) - return details.get('access_token', '').split('|') - - def get_refresh_token(self, addon_id=None): + def get_refresh_tokens(self, addon_id=None): """ - Returns the refresh token - :return: refresh token + Returns a tuple containing a list of refresh tokens and the number of + valid refresh tokens + :return: """ details = self.get_current_user_details(addon_id) - return details.get('refresh_token', '').split('|') + refresh_tokens = details.get('refresh_token', '').split('|') + num_refresh_tokens = len([1 for token in refresh_tokens if token]) + return refresh_tokens, num_refresh_tokens - def is_access_token_expired(self, addon_id=None): + def get_access_tokens(self, addon_id=None): """ - Returns True if the access_token is expired otherwise False. - If no expiration date was provided and an access_token exists - this method will always return True + Returns a tuple containing a list of access tokens, the number of valid + access tokens, and the token expiry timestamp. :return: """ details = self.get_current_user_details(addon_id) - access_token = details.get('access_token') - expires = int(details.get('token_expires', -1)) - - if access_token and expires <= int(time.time()): - return True - return False + access_tokens = details.get('access_token').split('|') + expiry_timestamp = int(details.get('token_expires', -1)) + if expiry_timestamp > int(time.time()): + num_access_tokens = len([1 for token in access_tokens if token]) + else: + access_tokens = [None, None, None, None] + num_access_tokens = 0 + return access_tokens, num_access_tokens, expiry_timestamp def update_access_token(self, addon_id, @@ -475,7 +517,7 @@ def update_access_token(self, 'developers': { addon_id: details, }, - } if addon_id else { + } if addon_id and addon_id != ADDON_ID else { 'users': { self._user: details, }, @@ -495,7 +537,7 @@ def set_last_key_hash(self, key_hash, addon_id=None): 'last_key_hash': key_hash, }, }, - } if addon_id else { + } if addon_id and addon_id != ADDON_ID else { 'users': { self._user: { 'last_key_hash': key_hash, @@ -505,49 +547,44 @@ def set_last_key_hash(self, key_hash, addon_id=None): } self.save(data, update=True) - @staticmethod - def get_new_developer(): - """ - :return: a new developer dict - """ - return { - 'access_token': '', - 'refresh_token': '', - 'token_expires': -1, - 'last_key_hash': '' - } - def get_developers(self): """ 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 set_developers(self, developers): + def add_new_developer(self, addon_id): """ - Updates the users - :param developers: dict, developers + Updates the developer users + :param addon_id: str :return: """ data = self.get_data() - data['access_manager']['developers'] = developers - self.save(data) + developers = data['access_manager'].get('developers', {}) + if addon_id not in developers: + developers[addon_id] = self.DEFAULT_NEW_DEVELOPER.copy() + data['access_manager']['developers'] = developers + return self.save(data) + return False - def dev_keys_changed(self, addon_id, api_key, client_id, client_secret): + def keys_changed(self, + addon_id, + api_key, + client_id, + client_secret, + update_hash=True): last_hash = self.get_last_key_hash(addon_id) - current_hash = self.calc_key_hash(api_key, client_id, client_secret) + current_hash = generate_hash(api_key, client_id, client_secret) + keys_changed = False if not last_hash and current_hash: - self.set_last_key_hash(current_hash, addon_id) - return False - - if last_hash != current_hash: - self.set_last_key_hash(current_hash, addon_id) - return True - - return False - - @staticmethod - def calc_key_hash(key, id, secret, **_kwargs): - return md5(''.join((key, id, secret)).encode('utf-8')).hexdigest() + if update_hash: + self.set_last_key_hash(current_hash, addon_id) + elif (not current_hash and last_hash + or last_hash != current_hash): + if update_hash: + self.set_last_key_hash(current_hash, addon_id) + keys_changed = True + return keys_changed diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/api_keys.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/api_keys.py index 488ea8ab42..2e99fc3893 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/api_keys.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/api_keys.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -9,19 +9,371 @@ from __future__ import absolute_import, division, unicode_literals +from base64 import b64decode, b64encode + from .json_store import JSONStore +from .. import logging +from ..constants import DEVELOPER_CONFIGS +from ... import key_sets class APIKeyStore(JSONStore): - def __init__(self): - super(APIKeyStore, self).__init__('api_keys.json') + log = logging.getLogger(__name__) + + DOMAIN_SUFFIX = '.apps.googleusercontent.com' + + def __init__(self, context): + super(APIKeyStore, self).__init__('api_keys.json', context) def set_defaults(self, reset=False): data = {} if reset else self.get_data() + if 'keys' not in data: - data = {'keys': {'personal': {'api_key': '', 'client_id': '', 'client_secret': ''}, 'developer': {}}} - if 'personal' not in data['keys']: - data['keys']['personal'] = {'api_key': '', 'client_id': '', 'client_secret': ''} - if 'developer' not in data['keys']: - data['keys']['developer'] = {} + data = { + 'keys': { + 'user': { + 'api_key': '', + 'client_id': '', + 'client_secret': '', + }, + 'developer': {}, + }, + } + else: + keys = data['keys'] or {} + if 'user' not in keys: + keys['user'] = keys.pop('personal', { + 'api_key': '', + 'client_id': '', + 'client_secret': '', + }) + if 'developer' not in keys: + keys['developer'] = {} + data['keys'] = keys + self.save(data) + + @staticmethod + def get_current_switch(): + return 'user' + + def has_user_api_keys(self): + api_data = self.get_data() + try: + return (api_data['keys']['user']['api_key'] + and api_data['keys']['user']['client_id'] + and api_data['keys']['user']['client_secret']) + except KeyError: + return False + + def get_api_keys(self, switch): + api_data = self.get_data() + if switch == 'developer': + return api_data['keys'][switch] + + decode = True + if switch == 'youtube-tv': + system = 'YouTube TV' + key_set_details = key_sets[switch] + + elif switch == 'youtube-vr': + system = 'YouTube VR' + key_set_details = key_sets[switch] + + elif switch.startswith('user'): + decode = False + system = 'All' + key_set_details = api_data['keys']['user'] + + else: + system = 'All' + if switch not in key_sets['provided']: + switch = 0 + key_set_details = key_sets['provided'][switch] + + key_set = { + 'system': system, + 'id': '', + 'key': '', + 'secret': '' + } + for key, value in key_set_details.items(): + if decode: + value = b64decode(value).decode('utf-8') + key = key.partition('_')[-1] + if key and key in key_set: + key_set[key] = value + if (key_set['id'] + and not key_set['id'].endswith(self.DOMAIN_SUFFIX)): + key_set['id'] += self.DOMAIN_SUFFIX + return key_set + + def get_key_set(self, switch): + key_set = self.get_api_keys(switch) + if switch.startswith('user'): + client_id = key_set['id'].replace(self.DOMAIN_SUFFIX, '') + if switch == 'user_old': + client_id += self.DOMAIN_SUFFIX + key_set['id'] = client_id + return key_set + + def strip_details(self, api_key, client_id, client_secret): + stripped_key = ''.join(api_key.split()) + stripped_id = ''.join(client_id.replace(self.DOMAIN_SUFFIX, '').split()) + stripped_secret = ''.join(client_secret.split()) + + if api_key != stripped_key: + if stripped_key not in api_key: + self.log.debug('Personal API key' + ' - skipped (mangled by stripping)') + return_key = api_key + else: + self.log.debug('Personal API key' + ' - whitespace removed') + return_key = stripped_key + else: + return_key = api_key + + if client_id != stripped_id: + if stripped_id not in client_id: + self.log.debug('Personal API client ID' + ' - skipped (mangled by stripping)') + return_id = client_id + elif self.DOMAIN_SUFFIX in client_id: + self.log.debug('Personal API client ID' + ' - whitespace and domain removed') + return_id = stripped_id + else: + self.log.debug('Personal API client ID' + ' - whitespace removed') + return_id = stripped_id + else: + return_id = client_id + + if client_secret != stripped_secret: + if stripped_secret not in client_secret: + self.log.debug('Personal API client secret' + ' - skipped (mangled by stripping)') + return_secret = client_secret + else: + self.log.debug('Personal API client secret' + ' - whitespace removed') + return_secret = stripped_secret + else: + return_secret = client_secret + + return return_key, return_id, return_secret + + def get_configs(self): + return { + 'tv': self.get_api_keys('youtube-tv'), + 'user': self.get_api_keys(self.get_current_switch()), + 'vr': self.get_api_keys('youtube-vr'), + 'dev': self.get_api_keys('developer'), + } + + def get_developer_config(self, developer_id): + context = self._context + + developer_configs = self.get_api_keys('developer') + if developer_id and developer_configs: + config = developer_configs.get(developer_id) + else: + config = context.get_ui().pop_property(DEVELOPER_CONFIGS) + if config: + self.log.warning('Storing developer keys in window property' + ' has been deprecated. Please use the' + ' youtube_registration module instead') + config = self.load_data(config) + + if not config: + return {} + + if not context.get_settings().allow_dev_keys(): + self.log.debug('Developer config ignored') + return {} + + origin = config.get('origin', developer_id) + key_details = config.get(origin) + required_details = {'key', 'id', 'secret'} + if (not origin + or not key_details + or not required_details.issubset(key_details)): + self.log.error_trace(('Invalid developer config: {config!r}', + 'Expected: {{', + ' "origin": ADDON_ID,', + ' ADDON_ID: {{', + ' "system": SYSTEM_NAME,', + ' "key": API_KEY,', + ' "id": CLIENT_ID,', + ' "secret": CLIENT_SECRET', + ' }},', + '}}'), + config=config) + return {} + + key_system = key_details.get('system') + if key_system == 'JSONStore': + for key in required_details: + key_details[key] = b64decode(key_details[key]).decode('utf-8') + + self.log.debug(('Using developer config', + 'Origin: {origin!r}', + 'System: {system!r}'), + origin=origin, + system=key_system) + + return { + 'origin': origin, + origin: { + 'system': key_system, + 'key': key_details['key'], + 'id': key_details['id'], + 'secret': key_details['secret'], + } + } + + def update_developer_config(self, + developer_id, + api_key, + client_id, + client_secret): + data = self.get_data() + existing_config = data['keys']['developer'].get(developer_id, {}) + + new_config = { + 'origin': developer_id, + developer_id: { + 'system': 'JSONStore', + 'key': b64encode( + bytes(api_key, 'utf-8') + ).decode('ascii'), + 'id': b64encode( + bytes(client_id, 'utf-8') + ).decode('ascii'), + 'secret': b64encode( + bytes(client_secret, 'utf-8') + ).decode('ascii'), + } + } + + if existing_config and existing_config == new_config: + return False + data['keys']['developer'][developer_id] = new_config + return self.save(data) + + def sync(self): + api_data = self.get_data() + settings = self._context.get_settings() + + update_saved_values = False + update_settings_values = False + + saved_details = ( + api_data['keys']['user'].get('api_key', ''), + api_data['keys']['user'].get('client_id', ''), + api_data['keys']['user'].get('client_secret', ''), + ) + if all(saved_details): + update_settings_values = True + # users are now pasting keys into api_keys.json + # try stripping whitespace and domain suffix from API details + # and save the results if they differ + stripped_details = self.strip_details(*saved_details) + if all(stripped_details) and saved_details != stripped_details: + saved_details = stripped_details + api_data['keys']['user'] = { + 'api_key': saved_details[0], + 'client_id': saved_details[1], + 'client_secret': saved_details[2], + } + update_saved_values = True + + setting_details = ( + settings.api_key(), + settings.api_id(), + settings.api_secret(), + ) + if all(setting_details): + update_settings_values = False + stripped_details = self.strip_details(*setting_details) + if all(stripped_details) and setting_details != stripped_details: + setting_details = ( + settings.api_key(stripped_details[0]), + settings.api_id(stripped_details[1]), + settings.api_secret(stripped_details[2]), + ) + + if saved_details != setting_details: + api_data['keys']['user'] = { + 'api_key': setting_details[0], + 'client_id': setting_details[1], + 'client_secret': setting_details[2], + } + update_saved_values = True + + if update_settings_values: + settings.api_key(saved_details[0]) + settings.api_id(saved_details[1]) + settings.api_secret(saved_details[2]) + + if update_saved_values: + self.save(api_data) + return True + return False + + def update(self): + context = self._context + localize = context.localize + settings = context.get_settings() + ui = context.get_ui() + + params = context.get_params() + api_key = params.get('api_key') + client_id = params.get('client_id') + client_secret = params.get('client_secret') + enable = params.get('enable') + + updated_list = [] + log_list = [] + + if api_key: + settings.api_key(api_key) + updated_list.append(localize('api.key')) + log_list.append('api_key') + if client_id: + settings.api_id(client_id) + updated_list.append(localize('api.id')) + log_list.append('client_id') + if client_secret: + settings.api_secret(client_secret) + updated_list.append(localize('api.secret')) + log_list.append('client_secret') + if updated_list: + ui.show_notification(localize('updated.x', ', '.join(updated_list))) + self.log.debug('Updated API details: %s', log_list) + + client_id = settings.api_id() + client_secret = settings.api_secret() + api_key = settings.api_key + missing_list = [] + log_list = [] + + if enable and client_id and client_secret and api_key: + ui.show_notification(localize('api.personal.enabled')) + self.log.debug('Personal API keys enabled') + elif enable: + if not api_key: + missing_list.append(localize('api.key')) + log_list.append('api_key') + if not client_id: + missing_list.append(localize('api.id')) + log_list.append('client_id') + if not client_secret: + missing_list.append(localize('api.secret')) + log_list.append('client_secret') + ui.show_notification(localize('api.personal.failed', + ', '.join(missing_list))) + self.log.error_trace(('Failed to enable personal API keys', + 'Missing: %s'), + log_list) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 12beef2c77..2e87dd75f3 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -13,101 +13,181 @@ import os from io import open -from ..constants import DATA_PATH -from ..logger import Logger -from ..utils import make_dirs, merge_dicts, to_unicode +from .. import logging +from ..constants import DATA_PATH, FILE_READ, FILE_WRITE +from ..utils.convert_format import to_unicode +from ..utils.file_system import make_dirs +from ..utils.methods import merge_dicts -class JSONStore(Logger): +class JSONStore(object): + log = logging.getLogger(__name__) + BASE_PATH = make_dirs(DATA_PATH) - def __init__(self, filename): + _process_data = None + + def __init__(self, filename, context): if self.BASE_PATH: self.filepath = os.path.join(self.BASE_PATH, filename) else: - self.log_error('JSONStore.__init__ - temp directory not available') + self.log.error_trace(('Addon data directory not available', + 'Path: %s'), + DATA_PATH, + stacklevel=2) self.filepath = None + self._context = context + self._loaded = False self._data = {} - self.load() - 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 - def save(self, data, update=False, process=None): - if not self.filepath: - return + def save(self, data, update=False, process=True, ipc=True, stacklevel=2): + filepath = self.filepath + if not filepath: + return False if update: data = merge_dicts(self._data, data) if data == self._data: - self.log_debug('JSONStore.save - data unchanged' - '\n\tFile: {filepath}' - .format(filepath=self.filepath)) - return - self.log_debug('JSONStore.save - saving' - '\n\tFile: {filepath}' - .format(filepath=self.filepath)) + self.log.debug(('Data unchanged', 'File: %s'), + filepath, + stacklevel=stacklevel) + return None + self.log.debug(('Saving', 'File: %s'), + filepath, + stacklevel=stacklevel) try: if not data: raise ValueError - _data = json.loads(json.dumps(data, ensure_ascii=False)) - with open(self.filepath, mode='w', encoding='utf-8') as jsonfile: - jsonfile.write(to_unicode(json.dumps(_data, - ensure_ascii=False, - indent=4, - sort_keys=True))) - self._data = process(_data) if process is not None else _data - except (IOError, OSError) as exc: - self.log_error('JSONStore.save - Access error' - '\n\tException: {exc!r}' - '\n\tFile: {filepath}' - .format(exc=exc, filepath=self.filepath)) - return - except (TypeError, ValueError) as exc: - self.log_error('JSONStore.save - Invalid data' - '\n\tException: {exc!r}' - '\n\tData: {data}' - .format(exc=exc, data=data)) + _data = json.dumps( + data, ensure_ascii=False, indent=4, sort_keys=True + ) + self._data = json.loads( + _data, + object_pairs_hook=(self._process_data if process else None), + ) + + if ipc: + self._context.get_ui().set_property( + '-'.join((FILE_WRITE, filepath)), + to_unicode(_data), + log_value='', + ) + response = self._context.ipc_exec( + FILE_WRITE, + timeout=5, + payload={'filepath': filepath}, + raise_exc=True, + ) + if response is False: + raise IOError + if response is None: + self.log.debug(('Data unchanged', 'File: %s'), + filepath, + stacklevel=stacklevel) + return None + else: + with open(filepath, mode='w', encoding='utf-8') as file: + file.write(to_unicode(_data)) + 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) self.set_defaults(reset=True) + return False + return True - def load(self, process=None): - if not self.filepath: - return + def load(self, process=True, ipc=True, stacklevel=2): + filepath = self.filepath + if not filepath: + return False - self.log_debug('JSONStore.load - loading' - '\n\tFile: {filepath}' - .format(filepath=self.filepath)) + self.log.debug(('Loading', 'File: %s'), + filepath, + stacklevel=stacklevel) try: - with open(self.filepath, mode='r', encoding='utf-8') as jsonfile: - data = jsonfile.read() + if ipc: + if self._context.ipc_exec( + FILE_READ, + timeout=5, + payload={'filepath': filepath}, + raise_exc=True, + ) is not False: + data = self._context.get_ui().get_property( + '-'.join((FILE_READ, filepath)), + log_value='', + ) + else: + raise IOError + else: + with open(filepath, mode='r', encoding='utf-8') as file: + data = file.read() if not data: raise ValueError - _data = json.loads(data) - self._data = process(_data) if process is not None else _data - except (IOError, OSError) as exc: - self.log_error('JSONStore.load - Access error' - '\n\tException: {exc!r}' - '\n\tFile: {filepath}' - .format(exc=exc, filepath=self.filepath)) - except (TypeError, ValueError) as exc: - self.log_error('JSONStore.load - Invalid data' - '\n\tException: {exc!r}' - '\n\tData: {data}' - .format(exc=exc, data=data)) + self._data = json.loads( + data, + object_pairs_hook=(self._process_data if process else None), + ) + 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 - def get_data(self, process=None): try: - if not self._data: + if not data: raise ValueError - _data = json.loads(json.dumps(self._data, ensure_ascii=False)) - return process(_data) if process is not None else _data + return json.loads( + json.dumps(data, ensure_ascii=False), + object_pairs_hook=(self._process_data if process else None), + ) except (TypeError, ValueError) as exc: - self.log_error('JSONStore.get_data - Invalid data' - '\n\tException: {exc!r}' - '\n\tData: {data}' - .format(exc=exc, data=self._data)) - self.set_defaults(reset=True) - _data = json.loads(json.dumps(self._data, ensure_ascii=False)) - return process(_data) if process is not None else _data + self.log.exception(('Invalid data', 'Data: {data!r}'), + data=data, + stacklevel=stacklevel) + if fallback: + self.set_defaults(reset=True) + return self.get_data(process=process, fallback=False) + if self._loaded: + raise exc + return data + + def load_data(self, data, process=True, stacklevel=2): + try: + return json.loads( + data, + object_pairs_hook=(self._process_data if process else None), + ) + except (TypeError, ValueError): + self.log.exception(('Invalid data', 'Data: {data!r}'), + data=data, + stacklevel=stacklevel) + return {} diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/logger.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/logger.py deleted file mode 100644 index 7742608770..0000000000 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/logger.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from __future__ import absolute_import, division, unicode_literals - -from .compatibility import xbmc -from .constants import ADDON_ID - - -class Logger(object): - LOGDEBUG = xbmc.LOGDEBUG - LOGINFO = xbmc.LOGINFO - LOGNOTICE = xbmc.LOGNOTICE - LOGWARNING = xbmc.LOGWARNING - LOGERROR = xbmc.LOGERROR - LOGFATAL = xbmc.LOGFATAL - LOGSEVERE = xbmc.LOGSEVERE - LOGNONE = xbmc.LOGNONE - - @staticmethod - def log(text, log_level=LOGDEBUG, addon_id=ADDON_ID): - log_line = '[%s] %s' % (addon_id, text) - xbmc.log(msg=log_line, level=log_level) - - @staticmethod - def log_debug(text, addon_id=ADDON_ID): - log_line = '[%s] %s' % (addon_id, text) - xbmc.log(msg=log_line, level=Logger.LOGDEBUG) - - @staticmethod - def log_info(text, addon_id=ADDON_ID): - log_line = '[%s] %s' % (addon_id, text) - xbmc.log(msg=log_line, level=Logger.LOGINFO) - - @staticmethod - def log_notice(text, addon_id=ADDON_ID): - log_line = '[%s] %s' % (addon_id, text) - xbmc.log(msg=log_line, level=Logger.LOGNOTICE) - - @staticmethod - def log_warning(text, addon_id=ADDON_ID): - log_line = '[%s] %s' % (addon_id, text) - xbmc.log(msg=log_line, level=Logger.LOGWARNING) - - @staticmethod - def log_error(text, addon_id=ADDON_ID): - log_line = '[%s] %s' % (addon_id, text) - xbmc.log(msg=log_line, level=Logger.LOGERROR) - - @staticmethod - def debug_log(on=False, off=True): - if on: - Logger.LOGDEBUG = Logger.LOGNOTICE - elif off: - Logger.LOGDEBUG = xbmc.LOGDEBUG - return Logger.LOGDEBUG diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/logging.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/logging.py new file mode 100644 index 0000000000..7e58a3b0f1 --- /dev/null +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/logging.py @@ -0,0 +1,625 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2014-2016 bromix (plugin.video.youtube) + Copyright (C) 2016-2025 plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from __future__ import absolute_import, division, unicode_literals + +import logging +import sys +from os.path import normpath +from pprint import PrettyPrinter +from string import Formatter +from sys import exc_info as sys_exc_info +from traceback import extract_stack, format_list + +from .compatibility import StringIO, string_type, to_str, xbmc +from .constants import ADDON_ID +from .utils.convert_format import to_unicode +from .utils.system_version import current_system_version + + +# noinspection PyUnresolvedReferences +__all__ = ( + 'check_frame', + 'critical', + 'debug', + 'debugging', + 'error', + 'exception', + 'info', + 'log', + 'warning', + 'CRITICAL', + 'DEBUG', + 'ERROR', + 'INFO', + 'WARNING', +) + + +class RecordFormatter(logging.Formatter): + def formatMessage(self, record): + record.__dict__['__sep__'] = '\n' if '\n' in record.message else ' - ' + try: + return self._style.format(record) + except AttributeError: + try: + return self._fmt % record.__dict__ + except UnicodeDecodeError as e: + record.__dict__ = { + key: to_unicode(value) + for key, value in record.__dict__.items() + } + try: + return self._fmt % record.__dict__ + except UnicodeDecodeError: + raise e + + def formatStack(self, stack_info): + return stack_info + + def format(self, record): + record.message = to_unicode(record.getMessage()) + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + s = self.formatMessage(record) + if record.exc_info: + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + if record.exc_text: + if record.stack_info: + if s[-1:] != '\n': + s += '\n\n' + s += self.formatStack(record.stack_info) + if s[-1:] != '\n': + s += '\n\n' + s += record.exc_text + elif record.stack_info: + if s[-1:] != '\n': + s += '\n\n' + s += self.formatStack(record.stack_info) + return s + + +class StreamWrapper(object): + OPEN = frozenset(('(', '[', '{')) + CLOSE = frozenset((')', ']', '}')) + + def __init__(self, stream, indent_per_level, level, indent): + self.stream = stream + self.indent_per_level = indent_per_level + self.level = level + self.indent = indent + self.previous_indent = 0 + self.previous_out = '' + + def update_level(self, level, indent): + self.level = level + self.indent = indent + + def write(self, out): + write = self.stream.write + indent = self.indent + out = to_unicode(out) + if '\n' in out: + write(to_str(out)) + elif out in self.OPEN: + write(to_str(out)) + write('\n' + (1 + indent) * ' ') + elif out in self.CLOSE: + if self.previous_out not in self.CLOSE: + if indent == self.previous_indent: + indent = (self.level - 1) * self.indent_per_level + write('\n' + indent * ' ') + write(to_str(out)) + else: + write(to_str(out)) + self.previous_indent = indent + self.previous_out = out + + +class VariableWidthPrettyPrinter(PrettyPrinter, object): + def _format(self, object, stream, indent, allowance, context, level): + if not isinstance(object, string_type): + indent = level * self._indent_per_level + + if level: + stream.update_level(level, indent) + else: + stream = StreamWrapper( + stream, + self._indent_per_level, + level, + indent, + ) + + super(VariableWidthPrettyPrinter, self)._format( + object=object, + stream=stream, + indent=indent, + allowance=allowance, + context=context, + level=level, + ) + + +class PrettyPrintFormatter(Formatter): + _pretty_printer = VariableWidthPrettyPrinter(indent=4, width=160) + + def convert_field(self, value, conversion): + if conversion == 'r': + return self._pretty_printer.pformat(value) + if conversion in {'d', 'e', 't', 'w'}: + _sort_dicts = sort_dicts = getattr(self._pretty_printer, + '_sort_dicts', + None) + width = self._pretty_printer._width + # __dict__ + if conversion == 'd': + if sort_dicts: + _sort_dicts = False + try: + value = getattr(value, '__repr_data__')() + except AttributeError: + if not isinstance(value, dict): + value = { + attr: getattr(value, attr, None) + for attr in dir(value) + } + # eval iterators + elif conversion == 'e': + if (getattr(value, '__iter__', None) + and not getattr(value, '__len__', None)): + value = tuple(value) + if sort_dicts: + _sort_dicts = False + # text representation + elif conversion == 't': + try: + value = getattr(value, '__str_parts__')(as_dict=True) + if sort_dicts: + _sort_dicts = False + except AttributeError: + pass + # wide output + elif conversion == 'w': + self._pretty_printer._width = 2 * width + if _sort_dicts != sort_dicts: + self._pretty_printer._sort_dicts = _sort_dicts + out = self._pretty_printer.pformat(value) + if sort_dicts: + self._pretty_printer._sort_dicts = sort_dicts + self._pretty_printer._width = width + return out + return super(PrettyPrintFormatter, self).convert_field( + value, + conversion, + ) + + if not current_system_version.compatible(19): + def parse(self, *args, **kwargs): + output = super(PrettyPrintFormatter, self).parse(*args, **kwargs) + return ( + (to_str(literal_text), field_name, format_spec, conversion) + for literal_text, field_name, format_spec, conversion in output + ) + + def format_field(self, *args, **kwargs): + return to_str( + super(PrettyPrintFormatter, self).format_field(*args, **kwargs) + ) + + +class MessageFormatter(object): + _formatter = PrettyPrintFormatter() + + __slots__ = ( + 'args', + 'kwargs', + 'msg', + ) + + def __init__(self, msg, *args, **kwargs): + self.msg = msg + self.args = args + self.kwargs = kwargs + + def __str__(self): + return self._formatter.vformat(self.msg, self.args, self.kwargs) + + +class Handler(logging.Handler): + LEVELS = { + logging.NOTSET: xbmc.LOGNONE, + logging.DEBUG: xbmc.LOGDEBUG, + # logging.INFO: xbmc.LOGINFO, + logging.INFO: xbmc.LOGNOTICE, + logging.WARN: xbmc.LOGWARNING, + logging.WARNING: xbmc.LOGWARNING, + logging.ERROR: xbmc.LOGERROR, + logging.CRITICAL: xbmc.LOGFATAL, + } + STANDARD_FORMATTER = RecordFormatter( + fmt='[%(addon_id)s] %(module)s:%(lineno)d(%(funcName)s)' + '%(__sep__)s%(message)s', + ) + DEBUG_FORMATTER = RecordFormatter( + fmt='[%(addon_id)s] %(module)s, line %(lineno)d, in %(funcName)s' + '\n%(message)s', + ) + + _stack_info = False + + def __init__(self, level): + super(Handler, self).__init__(level=level) + self.setFormatter(self.STANDARD_FORMATTER) + + def emit(self, record): + record.addon_id = ADDON_ID + xbmc.log( + msg=self.format(record), + level=self.LEVELS.get(record.levelno, xbmc.LOGDEBUG), + ) + + def format(self, record): + if self.stack_info: + fmt = self.DEBUG_FORMATTER + else: + fmt = self.STANDARD_FORMATTER + return fmt.format(record) + + @property + def stack_info(self): + return self._stack_info + + @stack_info.setter + def stack_info(self, value): + type(self)._stack_info = value + + +class LogRecord(logging.LogRecord): + def __init__(self, name, level, pathname, lineno, msg, args, exc_info, + func=None, **kwargs): + stack_info = kwargs.pop('sinfo', None) + super(LogRecord, self).__init__(name, + level, + pathname, + lineno, + msg, + args, + exc_info, + func=func, + **kwargs) + self.stack_info = stack_info + + if not current_system_version.compatible(19): + def getMessage(self): + msg = self.msg + if isinstance(msg, MessageFormatter): + msg = msg.__str__() + else: + msg = to_str(msg) + if self.args: + msg = msg % self.args + return msg + + +class KodiLogger(logging.Logger): + _verbose_logging = False + _stack_info = False + + def __init__(self, name, level=logging.DEBUG): + super(KodiLogger, self).__init__(name=name, level=level) + self.propagate = False + self.addHandler(Handler(level=logging.DEBUG)) + + def _log(self, + level, + msg, + args, + exc_info=None, + extra=None, + stack_info=False, + stacklevel=1, + **kwargs): + if isinstance(msg, (list, tuple)): + msg = '\n'.join(map(to_str, msg)) + if kwargs: + msg = MessageFormatter(msg, *args, **kwargs) + args = () + elif args and args[0] == '*(' and args[-1] == ')': + msg = MessageFormatter(msg, *args[1:-1], **kwargs) + args = () + + if stack_info: + if exc_info or self.stack_info: + pass + elif stack_info == 'forced': + stack_info = True + else: + stack_info = False + sinfo = None + if _srcfiles: + try: + fn, lno, func, sinfo = self.findCaller(stack_info, stacklevel) + except ValueError: + fn, lno, func = '(unknown file)', 0, '(unknown function)' + else: + fn, lno, func = '(unknown file)', 0, '(unknown function)' + + if exc_info: + if isinstance(exc_info, BaseException): + exc_info = (type(exc_info), exc_info, exc_info.__traceback__) + elif not isinstance(exc_info, tuple): + exc_info = sys_exc_info() + + record = self.makeRecord(self.name, level, fn, lno, msg, args, + exc_info, func, extra, sinfo) + self.handle(record) + + def findCaller(self, stack_info=False, stacklevel=1): + target_frame = logging.currentframe() + if target_frame is None: + return '(unknown file)', 0, '(unknown function)', None + + last_frame = None + while stacklevel > 0: + next_frame = target_frame.f_back + if next_frame is None: + break + target_frame = next_frame + stacklevel, is_internal = check_frame(target_frame, stacklevel) + if is_internal: + continue + if last_frame is None: + last_frame = target_frame + stacklevel -= 1 + + if stack_info: + with StringIO() as output: + output.write('Stack (most recent call last):\n') + for item in format_list(extract_stack(last_frame)): + output.write(item) + stack_info = output.getvalue() + if stack_info[-1] == '\n': + stack_info = stack_info[:-1] + else: + stack_info = None + + target_frame_code = target_frame.f_code + return (target_frame_code.co_filename, + target_frame.f_lineno, + target_frame_code.co_name, + stack_info) + + def makeRecord(self, name, level, fn, lno, msg, args, exc_info, + func=None, extra=None, sinfo=None): + rv = LogRecord(name, + level, + fn, + lno, + msg, + args, + exc_info, + func=func, + sinfo=sinfo) + if extra is not None: + for key in extra: + if (key in ["message", "asctime"]) or (key in rv.__dict__): + raise KeyError("Attempt to overwrite %r in LogRecord" % key) + rv.__dict__[key] = extra[key] + return rv + + def exception(self, msg, *args, **kwargs): + if self.isEnabledFor(ERROR): + self._log( + ERROR, + msg, + args, + exc_info=kwargs.pop('exc_info', True), + stack_info=kwargs.pop('stack_info', True), + stacklevel=kwargs.pop('stacklevel', 1), + **kwargs + ) + + def error_trace(self, msg, *args, **kwargs): + if self.isEnabledFor(ERROR): + self._log( + ERROR, + msg, + args, + stack_info=kwargs.pop('stack_info', True), + stacklevel=kwargs.pop('stacklevel', 1), + **kwargs + ) + + def warning_trace(self, msg, *args, **kwargs): + if self.isEnabledFor(WARNING): + self._log( + WARNING, + msg, + args, + stack_info=kwargs.pop('stack_info', True), + stacklevel=kwargs.pop('stacklevel', 1), + **kwargs + ) + + def debug_trace(self, msg, *args, **kwargs): + if self.isEnabledFor(DEBUG): + self._log( + DEBUG, + msg, + args, + stack_info=kwargs.pop('stack_info', True), + stacklevel=kwargs.pop('stacklevel', 1), + **kwargs + ) + + @property + def debugging(self): + return self.isEnabledFor(logging.DEBUG) + + @debugging.setter + def debugging(self, value): + if value: + Handler.LEVELS[logging.DEBUG] = xbmc.LOGNOTICE + self.setLevel(logging.DEBUG) + root.setLevel(logging.DEBUG) + else: + Handler.LEVELS[logging.DEBUG] = xbmc.LOGDEBUG + self.setLevel(logging.INFO) + root.setLevel(logging.INFO) + + @property + def stack_info(self): + return self._stack_info + + @stack_info.setter + def stack_info(self, value): + if value: + type(self)._stack_info = True + Handler.stack_info = True + else: + type(self)._stack_info = False + Handler.stack_info = False + + @property + def verbose_logging(self): + return self._verbose_logging + + @verbose_logging.setter + def verbose_logging(self, value): + cls = type(self) + if value: + cls._verbose_logging = True + logging.root = root + logging.Logger.root = root + logging.Logger.manager = manager + logging.Logger.manager.setLoggerClass(KodiLogger) + logging.setLoggerClass(KodiLogger) + else: + if cls._verbose_logging: + logging.root = logging.RootLogger(logging.WARNING) + logging.Logger.root = logging.root + logging.Logger.manager = logging.Manager(logging.root) + logging.Logger.manager.setLoggerClass(logging.Logger) + logging.setLoggerClass(logging.Logger) + cls._verbose_logging = False + + +class RootLogger(KodiLogger): + def __init__(self, level): + super(RootLogger, self).__init__('root', level) + + def __reduce__(self): + return getLogger, () + + +root = RootLogger(logging.INFO) +KodiLogger.root = root +manager = logging.Manager(root) +KodiLogger.manager = manager +KodiLogger.manager.setLoggerClass(KodiLogger) + +critical = root.critical +error = root.error +warning = root.warning +info = root.info +debug = root.debug +log = root.log + +CRITICAL = logging.CRITICAL +ERROR = logging.ERROR +WARNING = logging.WARNING +INFO = logging.INFO +DEBUG = logging.DEBUG + + +def exception(msg, *args, **kwargs): + root.error(msg, + *args, + exc_info=kwargs.pop('exc_info', True), + stack_info=kwargs.pop('stack_info', True), + stacklevel=kwargs.pop('stacklevel', 1), + **kwargs) + + +def error_trace(msg, *args, **kwargs): + root.error(msg, + *args, + stack_info=kwargs.pop('stack_info', True), + stacklevel=kwargs.pop('stacklevel', 1), + **kwargs) + + +def warning_trace(msg, *args, **kwargs): + root.warning(msg, + *args, + stack_info=kwargs.pop('stack_info', True), + stacklevel=kwargs.pop('stacklevel', 1), + **kwargs) + + +def debug_trace(msg, *args, **kwargs): + root.debug(msg, + *args, + stack_info=kwargs.pop('stack_info', True), + stacklevel=kwargs.pop('stacklevel', 1), + **kwargs) + + +def getLogger(name=None): + if not name or isinstance(name, string_type) and name == root.name: + return root + return KodiLogger.manager.getLogger(name) + + +_srcfiles = { + normpath(getLogger.__code__.co_filename).lower(), + normpath(logging.getLogger.__code__.co_filename).lower(), +} + + +def check_frame(frame, stacklevel=None, skip_paths=None): + filename = normpath(frame.f_code.co_filename).lower() + is_internal = ( + filename in _srcfiles + or ('importlib' in filename and '_bootstrap' in filename) + or (skip_paths + and any(skip_path in filename for skip_path in skip_paths)) + ) + if stacklevel is None: + return is_internal + + if (ADDON_ID in filename and filename.endswith(( + 'function_cache.py', + 'abstract_settings.py', + 'xbmc_items.py', + ))): + stacklevel += 1 + return stacklevel, is_internal + + +__original_module__ = sys.modules[__name__] + + +class ModuleProperties(__original_module__.__class__, object): + __name__ = __original_module__.__name__ + __file__ = __original_module__.__file__ + __getattribute__ = __original_module__.__getattribute__ + + def __getattr__(self, item): + if item == 'debugging': + return root.isEnabledFor(logging.DEBUG) + raise AttributeError( + 'module \'{}\' has no attribute \'{}\''.format(__name__, item) + ) + + +sys.modules[__name__] = ModuleProperties(__name__, __doc__) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/__init__.py index 4719edf420..4c744a9912 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -13,6 +13,7 @@ from .player_monitor import PlayerMonitor from .service_monitor import ServiceMonitor + __all__ = ( 'PlayerMonitor', 'ServiceMonitor', diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py index bec87dbd27..53b6bcc072 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,21 +12,30 @@ import json import threading +from .. import logging from ..compatibility import xbmc from ..constants import ( BUSY_FLAG, + CHANNEL_ID, PATHS, PLAYBACK_STARTED, PLAYBACK_STOPPED, PLAYER_DATA, - PLAY_WITH, + PLAY_USING, REFRESH_CONTAINER, + TRAKT_PAUSE_FLAG, + VIDEO_ID, ) +from ..utils.redact import redact_params class PlayerMonitorThread(threading.Thread): def __init__(self, player, provider, context, monitor, player_data): - super(PlayerMonitorThread, self).__init__() + self.player_data = player_data + video_id = player_data.get(VIDEO_ID) + self.video_id = video_id + self.channel_id = player_data.get(CHANNEL_ID) + self.video_status = player_data.get('video_status') self._stopped = threading.Event() self._ended = threading.Event() @@ -36,15 +45,17 @@ def __init__(self, player, provider, context, monitor, player_data): self._context = context self._monitor = monitor - self.player_data = player_data - self.video_id = player_data.get('video_id') - self.channel_id = player_data.get('channel_id') - self.video_status = player_data.get('video_status') - self.current_time = 0.0 self.total_time = 0.0 self.progress = 0 + name = '{class_name}[{video_id}]'.format( + class_name=self.__class__.__name__, + video_id=video_id, + ) + self.log = logging.getLogger(name) + + super(PlayerMonitorThread, self).__init__(name=name) self.daemon = True self.start() @@ -54,6 +65,7 @@ def abort_now(self): or self.stopped()) def run(self): + video_id = self.video_id playing_file = self.player_data.get('playing_file') play_count = self.player_data.get('play_count', 0) use_remote_history = self.player_data.get('use_remote_history', False) @@ -62,48 +74,50 @@ def run(self): refresh_only = self.player_data.get('refresh_only', False) clip = self.player_data.get('clip', False) - self._context.log_debug('PlayerMonitorThread[{0}]: Starting' - .format(self.video_id)) - + context = self._context + log = self.log + monitor = self._monitor player = self._player + provider = self._provider + + log.debug('Starting') timeout_period = 5 waited = 0 wait_interval = 0.5 while not player.isPlaying(): - if self._context.abort_requested(): + if context.abort_requested(): break if waited >= timeout_period: self.end() return - self._context.log_debug('Waiting for playback to start') - self._monitor.waitForAbort(wait_interval) + log.debug('Waiting for playback to start') + monitor.waitForAbort(wait_interval) waited += wait_interval else: - self._context.send_notification(PLAYBACK_STARTED, { - 'video_id': self.video_id, - 'channel_id': self.channel_id, + context.send_notification(PLAYBACK_STARTED, { + VIDEO_ID: video_id, + CHANNEL_ID: self.channel_id, 'status': self.video_status, }) - client = self._provider.get_client(self._context) - logged_in = self._provider.is_logged_in() + client = provider.get_client(context) + logged_in = client.logged_in report_url = use_remote_history and playback_stats.get('playback_url') state = 'playing' if report_url: client.update_watch_history( - self._context, - self.video_id, + video_id, report_url, ) - access_manager = self._context.get_access_manager() - settings = self._context.get_settings() - playlist_player = self._context.get_playlist_player() + access_manager = context.get_access_manager() + settings = context.get_settings() + playlist_player = context.get_playlist_player() - video_id_param = 'video_id=%s' % self.video_id + video_id_param = 'video_id=%s' % video_id report_url = use_remote_history and playback_stats.get('watchtime_url') segment_start = 0.0 @@ -123,7 +137,7 @@ def run(self): break if (not current_file.startswith(playing_file) and not ( - self._context.is_plugin_path(current_file, PATHS.PLAY) + context.is_plugin_path(current_file, PATHS.PLAY) and video_id_param in current_file )) or total_time <= 0: self.stop() @@ -169,23 +183,24 @@ def run(self): # only report state='paused' once if state == 'playing' or last_state == 'playing': - client = self._provider.get_client(self._context) - logged_in = self._provider.is_logged_in() + client = provider.get_client(context) + logged_in = client.logged_in if logged_in: client.update_watch_history( - self._context, - self.video_id, + video_id, report_url, - status=(played_time, - segment_start, - segment_end, - state), + status=( + played_time, + segment_start, + segment_end, + state, + ), ) segment_start = segment_end - self._monitor.waitForAbort(wait_interval) + monitor.waitForAbort(wait_interval) waited += wait_interval self.current_time = player.current_time @@ -194,55 +209,82 @@ def run(self): self.progress = int(100 * self.current_time / self.total_time) if logged_in: - client = self._provider.get_client(self._context) - logged_in = self._provider.is_logged_in() + client = provider.get_client(context) + logged_in = client.logged_in - if self.progress >= settings.get_play_count_min_percent(): + if self.video_status.get('live'): play_count += 1 - self.current_time = 0 - segment_end = self.total_time - else: segment_end = self.current_time - refresh_only = True - - play_data = { - 'play_count': play_count, - 'total_time': self.total_time, - 'played_time': self.current_time, - 'played_percent': self.progress, - } + play_data = { + 'play_count': play_count, + 'total_time': 0, + 'played_time': 0, + 'played_percent': 0, + } + else: + if self.progress >= settings.get_play_count_min_percent(): + play_count += 1 + self.current_time = 0 + segment_end = self.total_time + else: + segment_end = self.current_time + refresh_only = True + play_data = { + 'play_count': play_count, + 'total_time': self.total_time, + 'played_time': self.current_time, + 'played_percent': self.progress, + } self.player_data['play_data'] = play_data if logged_in and report_url: client.update_watch_history( - self._context, - self.video_id, + video_id, report_url, - status=(segment_end, segment_end, segment_end, 'stopped'), + status=( + segment_end, + segment_end, + segment_end, + 'stopped', + ), ) if use_local_history: - self._context.get_playback_history().set_item(self.video_id, - play_data) + context.get_playback_history().set_item(video_id, play_data) - self._context.send_notification(PLAYBACK_STOPPED, self.player_data) - self._context.log_debug('Playback stopped [{video_id}]:' - ' {played_time:.3f} secs of {total_time:.3f}' - ' @ {played_percent}%,' - ' played {play_count} time(s)' - .format(video_id=self.video_id, **play_data)) + context.send_notification(PLAYBACK_STOPPED, self.player_data) + log.debug('Playback stopped:' + ' {played_time:.3f} secs of {total_time:.3f}' + ' @ {played_percent}%,' + ' played {play_count} time(s)', + **play_data) if refresh_only: pass - elif settings.get_bool('youtube.playlist.watchlater.autoremove', True): + elif settings.get_bool(settings.WATCH_LATER_REMOVE, True): watch_later_id = logged_in and access_manager.get_watch_later_id() - if watch_later_id: + if not watch_later_id: + context.get_watch_later_list().del_item(video_id) + elif watch_later_id.lower() == 'wl': + provider.on_playlist_x( + provider, + context, + command='remove', + category='video', + playlist_id=watch_later_id, + video_id=video_id, + video_name='', + confirmed=True, + ) + else: playlist_item_id = client.get_playlist_item_id_of_video_id( - playlist_id=watch_later_id, video_id=self.video_id + playlist_id=watch_later_id, + video_id=video_id, + do_auth=True, ) if playlist_item_id: - self._provider.on_playlist_x( - self._provider, - self._context, + provider.on_playlist_x( + provider, + context, command='remove', category='video', playlist_id=watch_later_id, @@ -250,47 +292,43 @@ def run(self): video_name='', confirmed=True, ) - else: - self._context.get_watch_later_list().del_item(self.video_id) if logged_in and not refresh_only: history_id = access_manager.get_watch_history_id() - if history_id: - client.add_video_to_playlist(history_id, self.video_id) + if history_id and history_id.lower() != 'hl': + client.add_video_to_playlist(history_id, video_id) # rate video - if (settings.get_bool('youtube.post.play.rate') and - (settings.get_bool('youtube.post.play.rate.playlists') + if (settings.get_bool(settings.RATE_VIDEOS) and + (settings.get_bool(settings.RATE_PLAYLISTS) or xbmc.PlayList(xbmc.PLAYLIST_VIDEO).size() < 2)): - json_data = client.get_video_rating(self.video_id) + json_data = client.get_video_rating(video_id) if json_data: items = json_data.get('items', [{'rating': 'none'}]) rating = items[0].get('rating', 'none') if rating == 'none': - self._provider.on_video_x( - self._provider, - self._context, + provider.on_video_x( + provider, + context, command='rate', - video_id=self.video_id, + video_id=video_id, current_rating=rating, ) - if settings.get_bool('youtube.post.play.refresh', False): - self._context.send_notification(REFRESH_CONTAINER) + if settings.get_bool(settings.PLAY_REFRESH): + context.send_notification(REFRESH_CONTAINER) self.end() def stop(self): - self._context.log_debug('PlayerMonitorThread[{0}]: Stop event set' - .format(self.video_id)) + self.log.debug('Stop event set') self._stopped.set() def stopped(self): return self._stopped.is_set() def end(self): - self._context.log_debug('PlayerMonitorThread[{0}]: End event set' - .format(self.video_id)) + self.log.debug('End event set') self._ended.set() def ended(self): @@ -298,6 +336,8 @@ def ended(self): class PlayerMonitor(xbmc.Player): + log = logging.getLogger(__name__) + def __init__(self, provider, context, monitor): super(PlayerMonitor, self).__init__() self._provider = provider @@ -318,8 +358,7 @@ def stop_threads(self): continue if not thread.stopped(): - self._context.log_debug('PlayerMonitorThread[{0}]: stopping' - .format(thread.video_id)) + self.log.debug('Stopping: %s', thread.name) thread.stop() for thread in self.threads: @@ -331,17 +370,17 @@ def stop_threads(self): def cleanup_threads(self, only_ended=True): active_threads = [] + active_thread_names = [] for thread in self.threads: if only_ended and not thread.ended(): active_threads.append(thread) + active_thread_names.append(thread.name) continue if thread.ended(): - self._context.log_debug('PlayerMonitorThread[{0}]: clean up' - .format(thread.video_id)) + self.log.debug('Clean up: %s', thread.name) else: - self._context.log_debug('PlayerMonitorThread[{0}]: stopping' - .format(thread.video_id)) + self.log.debug('Stopping: %s', thread.name) if not thread.stopped(): thread.stop() try: @@ -349,33 +388,33 @@ def cleanup_threads(self, only_ended=True): except RuntimeError: pass - self._context.log_debug('PlayerMonitor active threads: |{0}|'.format( - ', '.join([thread.video_id for thread in active_threads]) - )) + self.log.debug('Active threads: %s', active_thread_names) self.threads = active_threads def onPlayBackStarted(self): if not self._ui.busy_dialog_active(): self._ui.clear_property(BUSY_FLAG) - if self._ui.get_property(PLAY_WITH): + if self._ui.get_property(PLAY_USING): self._context.execute('Action(SwitchPlayer)') self._context.execute('Action(Stop)') return def onAVStarted(self): - if self._ui.get_property(PLAY_WITH): + ui = self._ui + if ui.get_property(PLAY_USING): return - if not self._ui.busy_dialog_active(): - self._ui.clear_property(BUSY_FLAG) + if not ui.busy_dialog_active(): + ui.clear_property(BUSY_FLAG) - player_data = self._ui.pop_property(PLAYER_DATA) + player_data = ui.pop_property(PLAYER_DATA, + process=json.loads, + log_process=redact_params) if not player_data: return self.cleanup_threads() - player_data = json.loads(player_data) try: self.seek_time = float(player_data.get('seek_time')) self.start_time = float(player_data.get('start_time')) @@ -396,10 +435,12 @@ def onAVStarted(self): player_data)) def onPlayBackEnded(self): - if not self._ui.busy_dialog_active(): - self._ui.clear_property(BUSY_FLAG) + ui = self._ui + if not ui.busy_dialog_active(): + ui.clear_property(BUSY_FLAG) - self._ui.pop_property(PLAY_WITH) + ui.pop_property(PLAY_USING) + ui.clear_property(TRAKT_PAUSE_FLAG, raw=True) self.stop_threads() self.cleanup_threads() diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index 7772834c39..29cad74f36 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,27 +10,45 @@ from __future__ import absolute_import, division, unicode_literals import json -import threading +from io import open +from threading import Event, Lock, Thread +from .. import logging from ..compatibility import urlsplit, xbmc, xbmcgui from ..constants import ( + ACTION, ADDON_ID, CHECK_SETTINGS, CONTAINER_FOCUS, + CONTAINER_ID, + CONTAINER_POSITION, + CURRENT_ITEM, + FILE_READ, + FILE_WRITE, + HAS_PARENT, + MARK_AS_LABEL, PATHS, PLAYBACK_STOPPED, PLAYER_VIDEO_ID, + PLAY_CANCELLED, + PLAY_COUNT, PLAY_FORCED, PLUGIN_WAKEUP, REFRESH_CONTAINER, RELOAD_ACCESS_MANAGER, + RESUMABLE, SERVER_WAKEUP, - WAKEUP, + SERVICE_IPC, + SYNC_LISTITEM, + VIDEO_ID, ) from ..network import get_connect_address, get_http_server, httpd_status +from ..utils.methods import jsonrpc class ServiceMonitor(xbmc.Monitor): + log = logging.getLogger(__name__) + _settings_changes = 0 _settings_collect = False get_idle_time = xbmc.getGlobalIdleTime @@ -55,64 +73,48 @@ def __init__(self, context): self.refresh = False self.interrupt = False + self.file_access = {} + self.onSettingsChanged(force=True) super(ServiceMonitor, self).__init__() @staticmethod - def busy_dialog_active(all_modals=False, dialog_ids=frozenset(( - 10100, # WINDOW_DIALOG_YES_NO - 10101, # WINDOW_DIALOG_PROGRESS - 10103, # WINDOW_DIALOG_KEYBOARD - 10109, # WINDOW_DIALOG_NUMERIC - 10138, # WINDOW_DIALOG_BUSY - 10151, # WINDOW_DIALOG_EXT_PROGRESS - 10160, # WINDOW_DIALOG_BUSY_NOCANCEL - 12000, # WINDOW_DIALOG_SELECT - 12002, # WINDOW_DIALOG_OK - ))): - if all_modals and xbmc.getCondVisibility('System.HasActiveModalDialog'): - return True - dialog_id = xbmcgui.getCurrentWindowDialogId() - if dialog_id in dialog_ids: - return dialog_id - return False - - @staticmethod - def is_plugin_container(url='plugin://{0}/'.format(ADDON_ID), - check_all=False, - _bool=xbmc.getCondVisibility, - _busy=busy_dialog_active.__func__, - _label=xbmc.getInfoLabel): - if check_all: - return (not _bool('Container.IsUpdating') - and not _busy() - and _label('Container.FolderPath').startswith(url)) - is_plugin = _label('Container.FolderPath').startswith(url) - return { - 'is_plugin': is_plugin, - 'is_loaded': is_plugin and not _bool('Container.IsUpdating'), - 'is_active': is_plugin and not _busy(), - } - - def clear_property(self, property_id): - self._context.log_debug('Clear property |{id}|'.format(id=property_id)) - _property_id = '-'.join((ADDON_ID, property_id)) - xbmcgui.Window(10000).clearProperty(_property_id) - return None - - def set_property(self, property_id, value='true'): - self._context.log_debug('Set property |{id}|: {value!r}' - .format(id=property_id, value=value)) - _property_id = '-'.join((ADDON_ID, property_id)) + def send_notification(method, + data=True, + sender='.'.join((ADDON_ID, 'service'))): + jsonrpc(method='JSONRPC.NotifyAll', + params={'sender': sender, + 'message': method, + 'data': data}) + + def set_property(self, + property_id, + value='true', + stacklevel=2, + process=None, + log_value=None, + log_process=None, + raw=False): + if log_value is None: + log_value = value + if log_process: + log_value = log_process(log_value) + self.log.debug_trace('Set property {property_id!r}: {value!r}', + property_id=property_id, + value=log_value, + stacklevel=stacklevel) + _property_id = property_id if raw else '-'.join((ADDON_ID, property_id)) + if process: + value = process(value) xbmcgui.Window(10000).setProperty(_property_id, value) return value def refresh_container(self, force=False): - self.set_property(REFRESH_CONTAINER) - if force or self.is_plugin_container(check_all=True): - xbmc.executebuiltin('Container.Refresh') - else: + if force: + self.refresh = False + refreshed = self._context.get_ui().refresh_container(force=force) + if refreshed is None: self.refresh = True def onNotification(self, sender, method, data): @@ -155,15 +157,27 @@ def onNotification(self, sender, method, data): data = json.loads(data) position = data.get('position', 0) - item_path = context.get_infolabel( - 'Player.position({0}).FilenameAndPath'.format(position) - ) - - if context.is_plugin_path(item_path): - if not context.is_plugin_path(item_path, PATHS.PLAY): - context.log_warning('Playlist.OnAdd - non-playable path' - '\n\tPath: {0}'.format(item_path)) + playlist_player = context.get_playlist_player() + item_uri = playlist_player.get_item_path(position) + + if context.is_plugin_path(item_uri): + path, params = context.parse_uri(item_uri) + if path.rstrip('/') != PATHS.PLAY: + self.log.warning(('Playlist.OnAdd item is not playable', + 'Path: {path}', + 'Params: {params}'), + path=path, + params=params) self.set_property(PLAY_FORCED) + elif params.get(ACTION) == 'list': + playlist_player.stop() + playlist_player.clear() + self.log.warning(('Playlist.OnAdd item is a listing', + 'Path: {path}', + 'Params: {params}'), + path=path, + params=params) + self.set_property(PLAY_CANCELLED) return @@ -172,7 +186,7 @@ def onNotification(self, sender, method, data): group, separator, event = method.partition('.') - if event == WAKEUP: + if event == SERVICE_IPC: if not isinstance(data, dict): data = json.loads(data) if not data: @@ -200,14 +214,60 @@ def onNotification(self, sender, method, data): self._settings_collect = True elif state == 'process': self.onSettingsChanged(force=True) + elif state == 'ignore': + self._settings_collect = -1 response = True + elif target in {FILE_READ, FILE_WRITE}: + response = None + filepath = data.get('filepath') + if filepath: + if filepath not in self.file_access: + read_access = Event() + read_access.set() + write_access = Lock() + self.file_access[filepath] = (read_access, write_access) + else: + read_access, write_access = self.file_access[filepath] + + if target == FILE_READ: + try: + with open(filepath, mode='r', + encoding='utf-8') as file: + read_access.wait() + self.set_property( + '-'.join((FILE_READ, filepath)), + file.read(), + log_value='', + ) + response = True + except (IOError, OSError): + response = False + else: + with write_access: + content = self._context.get_ui().pop_property( + '-'.join((FILE_WRITE, filepath)), + log_value='', + ) + response = None + if content: + read_access.clear() + try: + with open(filepath, mode='w', + encoding='utf-8') as file: + file.write(content) + response = True + except (IOError, OSError): + response = False + finally: + read_access.set() + else: return if data.get('response_required'): data['response'] = response - self.set_property(WAKEUP, json.dumps(data, ensure_ascii=False)) + self.send_notification(SERVICE_IPC, data) elif event == REFRESH_CONTAINER: self.refresh_container() @@ -215,9 +275,11 @@ def onNotification(self, sender, method, data): elif event == CONTAINER_FOCUS: if data: data = json.loads(data) - if not data or not self.is_plugin_container(check_all=True): - return - xbmc.executebuiltin('SetFocus({0},{1},absolute)'.format(*data)) + 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() @@ -230,7 +292,46 @@ def onNotification(self, sender, method, data): return if data.get('play_data', {}).get('play_count'): - self.set_property(PLAYER_VIDEO_ID, data.get('video_id')) + self.set_property(PLAYER_VIDEO_ID, data.get(VIDEO_ID)) + + elif event == SYNC_LISTITEM: + video_ids = json.loads(data) if data else None + if not video_ids: + return + + context = self._context + ui = context.get_ui() + focused_video_id = ui.get_listitem_property(VIDEO_ID) + if not focused_video_id: + return + + playback_history = context.get_playback_history() + for video_id in video_ids: + if not video_id or video_id != focused_video_id: + continue + + play_count = ui.get_listitem_info(PLAY_COUNT) + resumable = ui.get_listitem_bool(RESUMABLE) + + self.set_property(MARK_AS_LABEL, + context.localize('history.mark.unwatched') + if play_count else + context.localize('history.mark.watched')) + + item_history = playback_history.get_item(video_id) + if item_history: + item_history = dict( + item_history, + play_count=int(play_count) if play_count else 0, + ) + if not resumable: + item_history['played_time'] = 0 + item_history['played_percent'] = 0 + playback_history.update_item(video_id, item_history) + else: + playback_history.set_item(video_id, { + 'play_count': int(play_count) if play_count else 0, + }) def onSettingsChanged(self, force=False): context = self._context @@ -241,6 +342,8 @@ def onSettingsChanged(self, force=False): else: self._settings_changes += 1 if self._settings_collect: + if self._settings_collect == -1: + self._settings_collect = False return total = self._settings_changes @@ -248,14 +351,23 @@ def onSettingsChanged(self, force=False): if total != self._settings_changes: return - context.log_debug('onSettingsChanged: {0} change(s)'.format(total)) + self.log.debug('onSettingsChanged: %d change(s)', total) self._settings_changes = 0 settings = context.get_settings(refresh=True) - if settings.logging_enabled(): - context.debug_log(on=True) + log_level = settings.log_level() + if log_level: + self.log.debugging = True + if log_level & 2: + self.log.stack_info = True + self.log.verbose_logging = True + else: + self.log.stack_info = False + self.log.verbose_logging = False else: - context.debug_log(off=True) + self.log.debugging = False + self.log.stack_info = False + self.log.verbose_logging = False self.set_property(CHECK_SETTINGS) self.refresh_container() @@ -300,9 +412,9 @@ def start_httpd(self): return True context = self._context - context.log_debug('HTTPServer: Starting |{ip}:{port}|' - .format(ip=self._httpd_address, - port=self._httpd_port)) + self.log.debug('HTTPServer: Starting {ip}:{port}', + ip=self._httpd_address, + port=self._httpd_port) self.httpd_address_sync() self.httpd = get_http_server(address=self._httpd_address, port=self._httpd_port, @@ -311,14 +423,13 @@ def start_httpd(self): self._httpd_error = True return False - self.httpd_thread = threading.Thread(target=self.httpd.serve_forever) + self.httpd_thread = Thread(target=self.httpd.serve_forever) self.httpd_thread.daemon = True self.httpd_thread.start() address = self.httpd.socket.getsockname() - context.log_debug('HTTPServer: Listening on |{ip}:{port}|' - .format(ip=address[0], - port=address[1])) + self.log.debug('HTTPServer: Listening on {address[0]}:{address[1]}', + address=address) self._httpd_error = False return True @@ -328,12 +439,12 @@ def shutdown_httpd(self, on_idle=False, terminate=False, player=None): and (on_idle or self.system_idle) and self.httpd_required(on_idle=True, player=player))): return - self._context.log_debug('HTTPServer: Shutting down |{ip}:{port}|' - .format(ip=self._old_httpd_address, - port=self._old_httpd_port)) + self.log.debug('HTTPServer: Shutting down {ip}:{port}', + ip=self._old_httpd_address, + port=self._old_httpd_port) self.httpd_address_sync() - shutdown_thread = threading.Thread(target=self.httpd.shutdown) + shutdown_thread = Thread(target=self.httpd.shutdown) shutdown_thread.daemon = True shutdown_thread.start() @@ -351,12 +462,12 @@ def shutdown_httpd(self, on_idle=False, terminate=False, player=None): self.httpd = None def restart_httpd(self): - self._context.log_debug('HTTPServer: Restarting' - ' |{old_ip}:{old_port}| > |{ip}:{port}|' - .format(old_ip=self._old_httpd_address, - old_port=self._old_httpd_port, - ip=self._httpd_address, - port=self._httpd_port)) + self.log.debug('HTTPServer: Restarting' + ' {old_ip}:{old_port} > {ip}:{port}', + old_ip=self._old_httpd_address, + old_port=self._old_httpd_port, + ip=self._httpd_address, + port=self._httpd_port) self.shutdown_httpd(terminate=True) self.start_httpd() diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/__init__.py index 5c241deb95..ef666c2b75 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/http_server.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/http_server.py index b8d44f7700..dfdd3736e0 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -13,14 +13,24 @@ import re import socket from collections import deque -from errno import ECONNABORTED, ECONNREFUSED, ECONNRESET +from errno import ( + ECONNABORTED, + ECONNREFUSED, + ECONNRESET, + EPIPE, + EPROTOTYPE, + ESHUTDOWN, +) from functools import partial from io import open from json import dumps as json_dumps, loads as json_loads from select import select from textwrap import dedent +from urllib3.exceptions import HTTPError + from .requests import BaseRequestsClass +from .. import logging from ..compatibility import ( BaseHTTPRequestHandler, TCPServer, @@ -39,7 +49,9 @@ PATHS, TEMP_PATH, ) -from ..utils import parse_and_redact_uri, wait +from ..utils.convert_format import fix_subtitle_stream +from ..utils.methods import wait +from ..utils.redact import parse_and_redact_uri class HTTPServer(ThreadingMixIn, TCPServer): @@ -101,6 +113,8 @@ def server_close(self): class RequestHandler(BaseHTTPRequestHandler, object): + log = logging.getLogger(__name__) + protocol_version = 'HTTP/1.1' server_version = 'plugin.video.youtube/1.0' @@ -116,6 +130,15 @@ class RequestHandler(BaseHTTPRequestHandler, object): 'server_lists': {}, } + SWALLOWED_ERRORS = { + ECONNABORTED, + ECONNREFUSED, + ECONNRESET, + EPIPE, + EPROTOTYPE, + ESHUTDOWN, + } + def __init__(self, request, client_address, server): if not RequestHandler.requests: RequestHandler.requests = BaseRequestsClass(context=self._context) @@ -156,10 +179,13 @@ def handle_one_request(self): try: super(RequestHandler, self).handle_one_request() return - except OSError as exc: + except (HTTPError, OSError) as exc: self.close_connection = True - if exc.errno not in {ECONNABORTED, ECONNREFUSED, ECONNRESET}: - raise exc + self.log.exception('Request failed') + if (isinstance(exc, HTTPError) + or getattr(exc, 'errno', None) in self.SWALLOWED_ERRORS): + return + raise exc def ip_address_status(self, ip_address): is_whitelisted = ip_address in self.whitelist_ips @@ -205,23 +231,21 @@ def connection_allowed(self, method): } if not path['path'].startswith(PATHS.PING): - msg = ('HTTPServer - {method}' - '\n\tPath: |{path}|' - '\n\tParams: |{params}|' - '\n\tAddress: |{client_ip}|' - '\n\tWhitelisted: {is_whitelisted}' - '\n\tLocal range: {is_local}' - '\n\tStatus: {status}' - .format(method=method, - path=path['path'], - params=path['log_params'], + self.log.debug(('{status}', + 'Method: {method!r}', + 'Path: {path[path]!r}', + 'Params: {path[log_params]!r}', + 'Address: {client_ip!r}', + 'Whitelisted: {is_whitelisted}', + 'Local range: {is_local}'), + status=('Allowed' if ip_allowed else 'Blocked'), + method=method, + path=path, client_ip=client_ip, is_whitelisted=is_whitelisted, is_local=('Undetermined' if is_local is None else - is_local), - status='Allowed' if ip_allowed else 'Blocked')) - self._context.log_debug(msg) + is_local)) return ip_allowed, path # noinspection PyPep8Naming @@ -262,14 +286,14 @@ def do_GET(self): self.send_header('Content-Length', str(file_size)) self.end_headers() - with open(file_path, 'rb', buffering=self.chunk_size) as f: + with open(file_path, mode='rb', buffering=self.chunk_size) as f: while 1: file_chunk = f.read() if not file_chunk: break self.wfile.write(file_chunk) except IOError: - response = ('File Not Found: |{uri}| -> |{file_path}|' + response = ('File Not Found: {uri!r} -> {file_path!r}' .format(uri=path['log_uri'], file_path=file_path)) self.send_error(404, response) @@ -327,7 +351,7 @@ def do_GET(self): if updated: # Successfully updated - updated = localize('api.config.updated') % ', '.join(updated) + updated = localize('api.config.updated', ', '.join(updated)) else: # No changes, not updated updated = localize('api.config.not_updated') @@ -361,29 +385,44 @@ def do_GET(self): params = path['params'] original_path = params.pop('__path', empty)[0] or '/videoplayback' request_servers = params.pop('__host', empty) - stream_id = (params.pop('__id', empty)[0], - params.get('itag', empty)[0]) - ids = self.server_priority_list['stream_ids'] - server_lists = self.server_priority_list['server_lists'] - if stream_id in ids: - priority_list = server_lists[stream_id]['list'] - if priority_list: - request_servers.sort( - key=partial(self._sort_servers, - _len=len(priority_list), - _index=priority_list.index), - reverse=True, - ) + stream_id = params.pop('__id', empty)[0] + method = params.pop('__method', empty)[0] or 'POST' + if original_path == '/videoplayback': + stream_id = (stream_id, params.get('itag', empty)[0]) + stream_type = params.get('mime', empty)[0] + if stream_type: + stream_type = stream_type.split('/') + else: + stream_type = (None, None) + ids = self.server_priority_list['stream_ids'] + server_lists = self.server_priority_list['server_lists'] + if stream_id in ids: + priority_list = server_lists[stream_id]['list'] + if priority_list: + request_servers.sort( + key=partial(self._sort_servers, + _len=len(priority_list), + _index=priority_list.index), + reverse=True, + ) + else: + ids.append(stream_id) + if len(ids) > 5: + old_id = ids.popleft() + del server_lists[old_id] + priority_list = [] + server_lists[stream_id] = { + 'started': False, + 'list': priority_list, + } + elif original_path == '/api/timedtext': + stream_type = (params.get('type', ['track'])[0], + params.get('fmt', empty)[0], + params.get('kind', empty)[0]) + priority_list = [] else: - ids.append(stream_id) - if len(ids) > 5: - old_id = ids.popleft() - del server_lists[old_id] + stream_type = (None, None) priority_list = [] - server_lists[stream_id] = { - 'started': False, - 'list': priority_list, - } headers = params.pop('__headers', empty)[0] if headers: @@ -397,11 +436,11 @@ def do_GET(self): stream_redirect = settings.httpd_stream_redirect() - log_msg = ('HTTPServer - Stream proxy response {success}' - '\n\tServer: |{server}|' - '\n\tTarget: |{target}|' - '\n\tStatus: |{status}|' - '\n\tReason: |{reason}|') + log_msg = ('Stream proxy response {success}', + 'Method: {method!r}', + 'Server: {server!r}', + 'Target: {target!r}', + 'Status: {status} {reason}') response = None server = None @@ -438,71 +477,96 @@ def do_GET(self): break headers['Host'] = server - with self.requests.request( - stream_url, - method='GET', - headers=headers, - allow_redirects=False, - stream=True - ) as response: - if response is None: - status = -1 - reason = 'Failed' - else: - while response.is_redirect: - request = response.next - if not request: - break - - target = urlsplit(request.url).hostname - if (target.endswith('googlevideo.com') - and 'Authorization' in headers): - _headers = ( - ('Authorization', headers['Authorization']), - ('Host', target), - ) - else: - _headers = ( - ('Host', target), - ) - request.headers.update(_headers) - - response = self.requests.request( - prepared_request=request, - allow_redirects=False, - stream=True, + response = self.requests.request( + stream_url, + method=method, + headers=headers, + allow_redirects=False, + stream=True, + cache=False, + ) + if response is None: + self.log.log( + level=logging.WARNING, + msg=log_msg, + success='not OK', + method=method, + server=server, + target=target, + status=-1, + reason='Failed', + ) + break + with response: + while response.is_redirect: + request = response.next + if not request: + break + + target = urlsplit(request.url).hostname + if (target.endswith('googlevideo.com') + and 'Authorization' in headers): + _headers = ( + ('Authorization', headers['Authorization']), + ('Host', target), + ('Referer', response.url) ) - if response is None: - break - status = response.status_code - reason = response.reason + else: + _headers = ( + ('Host', target), + ('Referer', response.url) + ) + request.headers.update(_headers) + + response = self.requests.request( + prepared_request=request, + allow_redirects=False, + stream=True, + cache=False, + ) + if response is None: + break + status = response.status_code + reason = response.reason if 100 <= status < 400: success = True - log_level = context.LOGDEBUG + log_level = logging.DEBUG if server not in priority_list: priority_list.append(server) else: success = False - log_level = context.LOGWARNING + log_level = logging.WARNING if server in priority_list: priority_list.remove(server) - context.log( - log_msg.format( - success='OK' if success else 'not OK', - server=server, - target=target, - status=status, - reason=reason, - ), - log_level=log_level + self.log.log( + level=log_level, + msg=log_msg, + success=('OK' if success else 'not OK'), + method=method, + server=server, + target=target, + status=status, + reason=reason, ) if not success: continue self.send_response(status) + if status == 200: + content = response.content + if stream_type[0] == 'track': + content = fix_subtitle_stream(stream_type, content) + response.headers['Content-Length'] = len(content) + if 'Content-Encoding' in response.headers: + del response.headers['Content-Encoding'] + if 'Transfer-Encoding' in response.headers: + del response.headers['Transfer-Encoding'] + self.send_header('Connection', 'close') + else: + content = None for header, value in response.headers.items(): self.send_header(header, value) self.end_headers() @@ -514,13 +578,16 @@ def do_GET(self): pass if self._close_all or wfile.closed: break - wfile.write( - response.raw.read( - amt=None, - decode_content=False, - cache_content=False, + if content: + wfile.write(content) + else: + wfile.write( + response.raw.read( + amt=None, + decode_content=False, + cache_content=False, + ) ) - ) break else: @@ -550,7 +617,7 @@ def do_HEAD(self): self.send_header('Content-Length', str(file_size)) self.end_headers() except IOError: - response = ('File Not Found: |{uri}| -> |{file_path}|' + response = ('File Not Found: {uri!r} -> {file_path!r}' .format(uri=path['log_uri'], file_path=file_path)) self.send_error(404, response) @@ -596,7 +663,8 @@ def do_POST(self): method='POST', headers=li_headers, data=post_data, - stream=True) + stream=True, + cache=False) if response is None: self.send_error(500) return @@ -618,9 +686,9 @@ def do_POST(self): re.MULTILINE) if match: authorized_types = match.group('authorized_types').split(',') - self._context.log_debug('HTTPServer - Found authorized formats' - '\n\tFormats: {auth_fmts}' - .format(auth_fmts=authorized_types)) + self.log.debug(('Found authorized formats', + 'Formats: %s'), + authorized_types) fmt_to_px = { 'SD': (1280 * 528) - 1, @@ -905,10 +973,10 @@ def get_http_server(address, port, context): server = HTTPServer((address, port), RequestHandler) return server except socket.error as exc: - context.log_error('HTTPServer - Failed to start' - '\n\tAddress: |{address}:{port}|' - '\n\tResponse: {response}' - .format(address=address, port=port, response=exc)) + logging.exception(('Failed to start', + 'Address: {address}:{port}'), + address=address, + port=port) xbmcgui.Dialog().notification(context.get_name(), str(exc), context.get_icon(), @@ -928,24 +996,24 @@ def httpd_status(context, address=None): )) if not RequestHandler.requests: RequestHandler.requests = BaseRequestsClass(context=context) - response = RequestHandler.requests.request(url) + response = RequestHandler.requests.request(url, cache=False) if response is None: result = None else: - result = response.status_code - if result == 204: - return True - - context.log_debug('HTTPServer - Ping' - '\n\tAddress: |{netloc}|' - '\n\tResponse: {response}' - .format(netloc=netloc, - response=result or 'failed')) + with response: + result = response.status_code + if result == 204: + return True + + logging.debug(('Ping', + 'Address: {netloc!r}', + 'Response: {response}'), + netloc=netloc, + response=(result or 'failed')) return False def get_client_ip_address(context): - ip_address = None url = urlunsplit(( 'http', get_connect_address(context, as_netloc=True), @@ -955,12 +1023,16 @@ def get_client_ip_address(context): )) if not RequestHandler.requests: RequestHandler.requests = BaseRequestsClass(context=context) - response = RequestHandler.requests.request(url) - if response is not None and response.status_code == 200: + response = RequestHandler.requests.request(url, cache=False) + if response is None: + return None + with response: + if response.status_code != 200: + return None response_json = response.json() - if response_json: - ip_address = response_json.get('ip') - return ip_address + if response_json: + return response_json.get('ip') + return None def get_connect_address(context, as_netloc=False, address=None): @@ -988,30 +1060,21 @@ def get_connect_address(context, as_netloc=False, address=None): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if hasattr(socket, 'SO_REUSEPORT'): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - except socket.error as exc: - context.log_error('HTTPServer' - ' - get_connect_address failed to create socket' - '\n\tException: {exc!r}' - .format(exc=exc)) + except socket.error: + logging.exception('Failed to create socket') connect_address = xbmc.getIPAddress() else: sock.settimeout(0) try: sock.connect((broadcast_address, 0)) - except socket.error as exc: - context.log_error('HTTPServer' - ' - get_connect_address failed connect' - '\n\tException: {exc!r}' - .format(exc=exc)) + except socket.error: + logging.exception('Failed to connect') connect_address = xbmc.getIPAddress() else: try: connect_address = sock.getsockname()[0] - except socket.error as exc: - context.log_error('HTTPServer' - ' - get_connect_address failed to get address' - '\n\tException: {exc!r}' - .format(exc=exc)) + except socket.error: + logging.exception('No address') connect_address = xbmc.getIPAddress() finally: sock.close() diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/ip_api.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/ip_api.py index 9c8e053f4f..294c947ee0 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/ip_api.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/ip_api.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,9 +10,11 @@ from __future__ import absolute_import, division, unicode_literals from .requests import BaseRequestsClass +from .. import logging class Locator(BaseRequestsClass): + log = logging.getLogger(__name__) def __init__(self, context): self._base_url = 'http://ip-api.com' @@ -26,17 +28,20 @@ def response(self): def locate_requester(self): request_url = '/'.join((self._base_url, 'json')) response = self.request(request_url) - self._response = response.json() if response is not None else {} + if response is None: + self._response = {} + return + with response: + self._response = response.json() def success(self): response = self.response() successful = response.get('status', 'fail') == 'success' if successful: - self.log_debug('Locator - Request successful') + self.log.debug('Request successful') else: - self.log_error('Locator - Request failed' - '\n\tMessage: {msg}' - .format(msg=response.get('message', 'Unknown'))) + self.log.error(('Request failed', 'Message: %s'), + response.get('message', 'Unknown')) return successful def coordinates(self): @@ -46,7 +51,7 @@ def coordinates(self): lat = self._response.get('lat') lon = self._response.get('lon') if lat is None or lon is None: - self.log_error('Locator - No coordinates returned') + self.log.error('No coordinates returned') return None - self.log_debug('Locator - Coordinates found') + self.log.debug('Coordinates found') return {'lat': lat, 'lon': lon} diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/requests.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/requests.py index 6171e5715b..73a694cee6 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/network/requests.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -9,17 +9,26 @@ from __future__ import absolute_import, division, unicode_literals -import atexit import socket +from atexit import register as atexit_register +from collections import OrderedDict -from requests import Session from requests.adapters import HTTPAdapter, Retry from requests.exceptions import InvalidJSONError, RequestException, URLRequired -from requests.utils import DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths +from requests.hooks import default_hooks +from requests.models import DEFAULT_REDIRECT_LIMIT, Request +from requests.sessions import Session +from requests.utils import ( + DEFAULT_CA_BUNDLE_PATH, + cookiejar_from_dict, + default_headers, + extract_zipped_paths, +) from urllib3.util.ssl_ import create_urllib3_context -from ..logger import Logger -from ..utils.methods import format_stack +from .. import logging +from ..utils.datetime import imf_fixdate +from ..utils.methods import generate_hash __all__ = ( @@ -63,32 +72,139 @@ def cert_verify(self, conn, url, verify, cert): return super(SSLHTTPAdapter, self).cert_verify(conn, url, verify, cert) -class BaseRequestsClass(Logger): - _session = Session() - _session.mount('https://', SSLHTTPAdapter( - pool_maxsize=10, - pool_block=True, - max_retries=Retry( - total=3, - backoff_factor=0.1, - status_forcelist={500, 502, 503, 504}, - allowed_methods=None, +class CustomSession(Session): + def __init__(self): + #: A case-insensitive dictionary of headers to be sent on each + #: :class:`Request ` sent from this + #: :class:`Session `. + self.headers = default_headers() + + #: Default Authentication tuple or object to attach to + #: :class:`Request `. + self.auth = None + + #: Dictionary mapping protocol or protocol and host to the URL of the proxy + #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to + #: be used on each :class:`Request `. + self.proxies = {} + + #: Event-handling hooks. + self.hooks = default_hooks() + + #: Dictionary of querystring data to attach to each + #: :class:`Request `. The dictionary values may be lists for + #: representing multivalued query parameters. + self.params = {} + + #: Stream response content default. + self.stream = False + + #: SSL Verification default. + #: Defaults to `True`, requiring requests to verify the TLS certificate at the + #: remote end. + #: If verify is set to `False`, requests will accept any TLS certificate + #: presented by the server, and will ignore hostname mismatches and/or + #: expired certificates, which will make your application vulnerable to + #: man-in-the-middle (MitM) attacks. + #: Only set this to `False` for testing. + self.verify = True + + #: SSL client certificate default, if String, path to ssl client + #: cert file (.pem). If Tuple, ('cert', 'key') pair. + self.cert = None + + #: Maximum number of redirects allowed. If the request exceeds this + #: limit, a :class:`TooManyRedirects` exception is raised. + #: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is + #: 30. + self.max_redirects = DEFAULT_REDIRECT_LIMIT + + #: Trust environment settings for proxy configuration, default + #: authentication and similar. + #: CustomSession.trust_env is False + self.trust_env = False + + #: A CookieJar containing all currently outstanding cookies set on this + #: session. By default it is a + #: :class:`RequestsCookieJar `, but + #: may be any other ``cookielib.CookieJar`` compatible object. + self.cookies = cookiejar_from_dict({}) + + # Default connection adapters. + self.adapters = OrderedDict() + self.mount('https://', SSLHTTPAdapter( + pool_maxsize=20, + pool_block=True, + max_retries=Retry( + total=3, + backoff_factor=0.1, + status_forcelist={500, 502, 503, 504}, + allowed_methods=None, + ) + )) + self.mount('http://', HTTPAdapter()) + + +class BaseRequestsClass(object): + log = logging.getLogger(__name__) + + _session = CustomSession() + atexit_register(_session.close) + + _context = None + _verify = True + _timeout = (9.5, 27) + _proxy = None + _default_exc = (RequestException,) + + METHODS_TO_CACHE = {'GET', 'HEAD'} + + def __init__(self, + context=None, + verify_ssl=None, + timeout=None, + proxy_settings=None, + exc_type=None, + **_kwargs): + super(BaseRequestsClass, self).__init__() + BaseRequestsClass.init( + context=context, + verify_ssl=verify_ssl, + timeout=timeout, + proxy_settings=proxy_settings, ) - )) - atexit.register(_session.close) - - def __init__(self, context, exc_type=None, **kwargs): - settings = context.get_settings() - self._verify = kwargs.get('verify_ssl') or settings.verify_ssl() - self._timeout = kwargs.get('timeout') or settings.requests_timeout() - self._proxy = kwargs.get('proxy_settings') or settings.proxy_settings() - - if isinstance(exc_type, tuple): - self._default_exc = (RequestException,) + exc_type - elif exc_type: - self._default_exc = (RequestException, exc_type) - else: - self._default_exc = (RequestException,) + self._default_exc = ( + (RequestException,) + exc_type + if isinstance(exc_type, tuple) else + (RequestException, exc_type) + if exc_type else + (RequestException,) + ) + + @classmethod + def init(cls, + context=None, + verify_ssl=None, + timeout=None, + proxy_settings=None, + **_kwargs): + cls._context = (cls._context + if context is None else + context) + if cls._context: + settings = cls._context.get_settings() + cls._verify = (settings.verify_ssl() + if verify_ssl is None else + verify_ssl) + cls._timeout = (settings.requests_timeout() + if timeout is None else + timeout) + cls._proxy = (settings.proxy_settings() + if proxy_settings is None else + proxy_settings) + + def reinit(self, **kwargs): + self.__init__(**kwargs) def __enter__(self): return self @@ -96,6 +212,65 @@ def __enter__(self): def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): self._session.close() + @staticmethod + def _raise_exception(new_exception, *args, **kwargs): + if not new_exception: + return + if issubclass(new_exception, RequestException): + new_exception = new_exception(*args) + attrs = new_exception.__dict__ + for attr, value in kwargs.items(): + if attr not in attrs: + setattr(new_exception, attr, value) + raise new_exception + else: + raise new_exception(*args, **kwargs) + + def _response_hook_json(self, **kwargs): + response = kwargs['response'] + if response is None: + return None, None + with response: + try: + json_data = response.json() + if 'error' in json_data: + kwargs.setdefault('pass_data', True) + kwargs.setdefault('json_data', json_data) + json_data.setdefault('code', response.status_code) + self._raise_exception( + kwargs.get('exception', RequestException), + '"error" in response JSON data', + **kwargs + ) + except ValueError as exc: + if kwargs.get('raise_exc') is None: + kwargs['raise_exc'] = True + self._raise_exception( + InvalidJSONError, + exc, + **kwargs + ) + + response.raise_for_status() + + return json_data.get('etag'), json_data + + def _response_hook_text(self, **kwargs): + response = kwargs['response'] + if response is None: + return None, None + with response: + response.raise_for_status() + result = response and response.text + if not result: + self._raise_exception( + kwargs.get('exception', RequestException), + 'Empty response text', + **kwargs + ) + + return None, result + def request(self, url=None, method='GET', params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, @@ -105,10 +280,13 @@ def request(self, url=None, method='GET', # See _response_hook and _error_hook in login_client.py # for example usage response_hook=None, - response_hook_kwargs=None, error_hook=None, - error_hook_kwargs=None, - error_title=None, error_info=None, raise_exc=False, **_): + event_hook_kwargs=None, + error_title=None, + error_info=None, + raise_exc=None, + cache=None, + **kwargs): if timeout is None: timeout = self._timeout if verify is None: @@ -117,8 +295,72 @@ def request(self, url=None, method='GET', proxies = self._proxy if allow_redirects is None: allow_redirects = True + stacklevel = kwargs.pop('stacklevel', 2) response = None + request_id = None + cached_response = None + etag = None + timestamp = None + + if url: + prepared_request = self._session.prepare_request(Request( + method=method, + url=url, + headers=headers, + files=files, + data=data, + json=json, + params=params, + auth=auth, + cookies=cookies, + hooks=hooks, + )) + + if cache is not False: + if prepared_request: + method = prepared_request.method + if cache is True or method in self.METHODS_TO_CACHE: + headers = prepared_request.headers + request_id = generate_hash( + method, + prepared_request.url, + headers, + prepared_request.body, + ) + + if request_id: + if cache == 'refresh': + cache = self._context.get_requests_cache() + cached_request = None + else: + cache = self._context.get_requests_cache() + cached_request = cache.get(request_id) + else: + cache = False + cached_request = None + + if cached_request: + etag, cached_response = cached_request['value'] + if cached_response is not None: + if etag: + # Etag is meant to be enclosed in double quotes, but the + # Google servers don't seem to support this + headers['If-None-Match'] = '"{0}", {0}'.format(etag) + timestamp = imf_fixdate(cached_request['timestamp']) + headers['If-Modified-Since'] = timestamp + self.log.debug(('Cached response', + 'Request ID: {request_id}', + 'Etag: {etag}', + 'Modified: {timestamp}'), + request_id=request_id, + etag=etag, + timestamp=timestamp, + stacklevel=stacklevel) + + if event_hook_kwargs is None: + event_hook_kwargs = {} + try: if prepared_request: response = self._session.send( @@ -130,99 +372,82 @@ def request(self, url=None, method='GET', timeout=timeout, allow_redirects=allow_redirects, ) - elif url: - response = self._session.request( - method=method, - url=url, - params=params, - data=data, - headers=headers, - cookies=cookies, - files=files, - auth=auth, - timeout=timeout, - allow_redirects=allow_redirects, - proxies=proxies, - hooks=hooks, - stream=stream, - verify=verify, - cert=cert, - json=json, - ) else: raise URLRequired() - if not getattr(response, 'status_code', None): + status_code = getattr(response, 'status_code', None) + if not status_code: raise self._default_exc[0](response=response) - if response_hook: - if response_hook_kwargs is None: - response_hook_kwargs = {} - response_hook_kwargs['response'] = response - response = response_hook(**response_hook_kwargs) - else: - response.raise_for_status() + if cached_response is None or status_code != 304: + timestamp = response.headers.get('Date') + if response_hook: + event_hook_kwargs['exception'] = self._default_exc[-1] + event_hook_kwargs['raise_exc'] = raise_exc + event_hook_kwargs['response'] = response + etag, response = response_hook(**event_hook_kwargs) + else: + etag = None + response.raise_for_status() + # Only clear cached response if there was no error response + cached_response = None except self._default_exc as exc: exc_response = exc.response or response - response_text = exc_response and exc_response.text - stack = format_stack() - error_details = {'exc': exc} + if exc_response: + response_text = exc_response.text + response_status = exc_response.status_code + response_reason = exc_response.reason + else: + response_text = None + response_status = 'Error' + response_reason = 'No response' + + log_msg = [ + '{title}', + 'URL: {method} {url}', + 'Status: {response_status} - {response_reason}', + 'Response: {response_text}', + ] + + kwargs.update(event_hook_kwargs) + kwargs['exc'] = exc + kwargs['response'] = exc_response if error_hook: - if error_hook_kwargs is None: - error_hook_kwargs = {} - error_hook_kwargs['exc'] = exc - error_hook_kwargs['response'] = exc_response - error_response = error_hook(**error_hook_kwargs) - _title, _info, _detail, _response, _trace, _exc = error_response + error_response = error_hook(**kwargs) + _title, _info, _detail, _response, _exc = error_response if _title is not None: error_title = _title - if _info is not None: - error_info = _info + if _info: + if isinstance(_info, (list, tuple)): + log_msg.extend(_info) + else: + log_msg.append(_info) if _detail is not None: - error_details.update(_detail) + kwargs.update(_detail) if _response is not None: response = _response - response_text = repr(_response) - if _trace is not None: - stack = _trace + if response and not response_text: + response_text = repr(_response) if _exc is not None: raise_exc = _exc - if error_title is None: - error_title = 'Request - Failed' - - if error_info is None: - try: - error_info = '\n\t'.join(( - 'URL: {method} {url}', - 'Status: {response.status_code} - {response.reason}', - )).format(method=method, - url=url, - response=exc.response) - except AttributeError: - error_info = ('Exception: {exc!r}' - .format(exc=exc)) - elif '{' in error_info: - try: - error_info = error_info.format(**error_details) - except (AttributeError, IndexError, KeyError): - error_info = ('Exception: {exc!r}' - .format(exc=exc)) - - if response_text: - response_text = ('Response: {0}' - .format(response_text)) - - if stack: - stack = 'Stack trace (most recent call last):\n{stack}'.format( - stack=''.join(stack) - ) - - self.log_error('\n\t'.join([part for part in [ - error_title, error_info, response_text, stack - ] if part])) + if error_info: + if isinstance(error_info, (list, tuple)): + log_msg.extend(error_info) + else: + log_msg.append(error_info) + + self.log.exception(log_msg, + title=(error_title or 'Failed'), + method=method, + url=url, + response_status=response_status, + response_reason=response_reason, + response_text=response_text, + stacklevel=stacklevel, + **kwargs) if raise_exc: if not isinstance(raise_exc, BaseException): @@ -235,4 +460,28 @@ def request(self, url=None, method='GET', raise raise_exc raise exc + if not cache: + pass + elif cached_response is not None: + self.log.debug(('Using cached response', + 'Request ID: {request_id}', + 'Etag: {etag}', + 'Modified: {timestamp}'), + request_id=request_id, + etag=etag, + timestamp=timestamp, + stacklevel=stacklevel) + cache.set(request_id) + response = cached_response + elif response is not None: + self.log.debug(('Saving response to cache', + 'Request ID: {request_id}', + 'Etag: {etag}', + 'Modified: {timestamp}'), + request_id=request_id, + etag=etag, + timestamp=timestamp, + stacklevel=stacklevel) + cache.set(request_id, response, etag) + return response diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/__init__.py index 83ed44b372..63d7ce263f 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/abstract_playlist_player.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/abstract_playlist_player.py index 34c97b833e..1dc994d053 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/abstract_playlist_player.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/abstract_playlist_player.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist_player.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist_player.py index 19a99cf0cb..bf0530dece 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist_player.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist_player.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -13,22 +13,31 @@ import json from ..abstract_playlist_player import AbstractPlaylistPlayer +from ... import logging from ...compatibility import xbmc from ...items import VideoItem, media_listitem from ...utils.methods import jsonrpc, wait +from ...utils.system_version import current_system_version class XbmcPlaylistPlayer(AbstractPlaylistPlayer): + log = logging.getLogger(__name__) + _CACHE = { 'player_id': None, 'playlist_id': None } + # xbmc.PlayList only supports music and video playlist IDs PLAYLIST_MAP = { + # -1: 'none', 0: 'music', 1: 'video', - 'video': xbmc.PLAYLIST_VIDEO, # 1 + # 2: 'picture', + # 'none': -1, 'audio': xbmc.PLAYLIST_MUSIC, # 0 + 'video': xbmc.PLAYLIST_VIDEO, # 1 + # 'picture': 2, } def __init__(self, context, playlist_type=None, retry=None): @@ -153,27 +162,38 @@ def get_playlist_id(cls, retry=0): cls.set_playlist_id(playlist_id) return playlist_id - def get_items(self, properties=None, dumps=False): + if current_system_version.compatible(19): + @staticmethod + def get_item_path(position, _label=xbmc.getInfoLabel): + return _label('Player.position(%d).FilenameAndPath' % position) + else: + def get_item_path(self, position): + item = self.get_items(start=position, end=position + 1) + return item[0]['file'] if item else '' + + 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': limits, }) try: result = response['result']['items'] return json.dumps(result, ensure_ascii=False) if dumps else result - except (KeyError, TypeError, ValueError) as exc: + except (KeyError, TypeError, ValueError): error = response.get('error', {}) - self._context.log_error('XbmcPlaylist.get_items - Error' - '\n\tException: {exc!r}' - '\n\tCode: {code}' - '\n\tMessage: {msg}' - .format(exc=exc, - code=error.get('code', 'Unknown'), - msg=error.get('message', 'Unknown'))) + self.log.exception(('Error', + 'Code: {code}', + 'Message: {message}'), + code=error.get('code', 'Unknown'), + message=error.get('message', 'Unknown')) return '' if dumps else [] def add_items(self, items, loads=False): @@ -202,18 +222,17 @@ def play_playlist_item(self, position, resume=False, defer=False): first item in the playlist is position 1 """ - context = self._context playlist_id = self._playlist.getPlayListId() if position == 'next': position, _ = self.get_position(offset=1) if not position: - context.log_warning('Unable to play from playlist position: {0}' - .format(position)) - return - context.log_debug('Playing from playlist: {id}, position: {position}' - .format(id=playlist_id, - position=position)) + self.log.warning('Unable to play from playlist position: %s', + position) + return None + self.log.debug('Playing from playlist: %d, position: %d', + playlist_id, + position) if not resume: command = 'Playlist.PlayOffset({type},{position})'.format( @@ -225,12 +244,14 @@ def play_playlist_item(self, position, resume=False, defer=False): return self._context.execute(command) # JSON Player.Open can be too slow but is needed if resuming is enabled - jsonrpc(method='Player.Open', - params={'item': {'playlistid': playlist_id, - # Convert 1 indexed to 0 indexed position - 'position': position - 1}}, - options={'resume': True}, - no_response=True) + return jsonrpc( + method='Player.Open', + params={'item': {'playlistid': playlist_id, + # Convert 1 indexed to 0 indexed position + 'position': position - 1}}, + options={'resume': True}, + no_response=True, + ) def play(self, playlist_index=-1, defer=False): """ @@ -279,9 +300,9 @@ def get_position(self, offset=0): # A playlist with only one element has no next item if playlist_size >= 1 and position <= playlist_size: - self._context.log_debug('playlist_id: {0}, position - {1}/{2}' - .format(self.get_playlist_id(), - position, - playlist_size)) + self.log.debug('playlist_id: %d, position - %d/%d', + self.get_playlist_id(), + position, + playlist_size) return position, (playlist_size - position) return None, None diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/__init__.py index 7c0a50d999..e52883e289 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/abstract_plugin.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/abstract_plugin.py index 5be271e46a..940754c73e 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/abstract_plugin.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/abstract_plugin.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 304ca177f8..5ff6b44574 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,18 +11,22 @@ from __future__ import absolute_import, division, unicode_literals from ..abstract_plugin import AbstractPlugin +from ... import logging from ...compatibility import string_type, xbmc, xbmcgui, xbmcplugin from ...constants import ( + ACTION, BUSY_FLAG, CONTAINER_FOCUS, CONTAINER_ID, CONTAINER_POSITION, + FOLDER_URI, FORCE_PLAY_PARAMS, PATHS, PLAYBACK_FAILED, PLAYER_VIDEO_ID, PLAYLIST_PATH, PLAYLIST_POSITION, + PLAY_CANCELLED, PLAY_FORCED, PLAY_FORCE_AUDIO, PLUGIN_SLEEPING, @@ -30,6 +34,8 @@ REFRESH_CONTAINER, RELOAD_ACCESS_MANAGER, REROUTE_PATH, + SYNC_LISTITEM, + TRAKT_PAUSE_FLAG, VIDEO_ID, WINDOW_FALLBACK, WINDOW_REPLACE, @@ -44,12 +50,13 @@ playback_item, uri_listitem, ) -from ...utils import format_stack, parse_and_redact_uri +from ...utils.redact import parse_and_redact_uri class XbmcPlugin(AbstractPlugin): _LIST_ITEM_MAP = { 'AudioItem': media_listitem, + 'BookmarkItem': directory_listitem, 'CommandItem': directory_listitem, 'DirectoryItem': directory_listitem, 'ImageItem': image_listitem, @@ -63,6 +70,7 @@ class XbmcPlugin(AbstractPlugin): _PLAY_ITEM_MAP = { 'AudioItem': playback_item, + 'BookmarkItem': playback_item, 'UriItem': uri_listitem, 'VideoItem': playback_item, } @@ -70,12 +78,21 @@ class XbmcPlugin(AbstractPlugin): def __init__(self): super(XbmcPlugin, self).__init__() - def run(self, provider, context, forced=None): + def run(self, + provider, + context, + forced=False, + is_same_path=False, + **kwargs): handle = context.get_handle() ui = context.get_ui() + uri = context.get_uri() + path = context.get_path().rstrip('/') + route = ui.pop_property(REROUTE_PATH) - post_run_action = None + _post_run_action = None + post_run_actions = [] succeeded = False for was_busy in (ui.pop_property(BUSY_FLAG),): if was_busy: @@ -86,17 +103,15 @@ def run(self, provider, context, forced=None): else: break - uri = context.get_uri() playlist_player = context.get_playlist_player() position, remaining = playlist_player.get_position() - playing = (playlist_player.is_playing() - and context.is_plugin_path(uri, PATHS.PLAY)) + playing = path == PATHS.PLAY and playlist_player.is_playing() if playing: items = playlist_player.get_items() playlist_player.clear() - context.log_warning('Multiple busy dialogs active' - ' - Playlist cleared to avoid Kodi crash') + logging.warning('Multiple busy dialogs active' + ' - Playlist cleared to avoid Kodi crash') xbmcplugin.endOfDirectory( handle, @@ -106,10 +121,13 @@ def run(self, provider, context, forced=None): ) if not playing: - context.log_warning('Multiple busy dialogs active' - ' - Plugin call ended to avoid Kodi crash') - result, post_run_action = self.uri_action(context, uri) + logging.warning('Multiple busy dialogs active' + ' - Plugin call ended to avoid Kodi crash') + result, _post_run_action = self.uri_action(context, uri) succeeded = result + if _post_run_action: + post_run_actions.append(_post_run_action) + _post_run_action = None continue if position: @@ -127,13 +145,13 @@ def run(self, provider, context, forced=None): while ui.busy_dialog_active(): timeout -= 1 if timeout < 0: - context.log_error('Multiple busy dialogs active' - ' - Extended busy period') + logging.error('Multiple busy dialogs active' + ' - Extended busy period') break context.sleep(1) - context.log_warning('Multiple busy dialogs active' - ' - Reloading playlist...') + logging.warning('Multiple busy dialogs active' + ' - Reloading playlist...') num_items = playlist_player.add_items(items) if position: @@ -145,26 +163,29 @@ def run(self, provider, context, forced=None): while ui.busy_dialog_active() or playlist_player.size() < position: timeout -= 1 if timeout < 0: - context.log_error('Multiple busy dialogs active' - ' - Playback restart failed, retrying...') + logging.error('Multiple busy dialogs active' + ' - Playback restart failed, retrying...') command = playlist_player.play_playlist_item(position, defer=True) - result, post_run_action = self.uri_action( + result, _post_run_action = self.uri_action( context, command, ) succeeded = False + if _post_run_action: + post_run_actions.append(_post_run_action) + _post_run_action = None break context.sleep(1) else: playlist_player.play_playlist_item(position) else: - if post_run_action: - self.post_run(context, ui, post_run_action) + if post_run_actions: + self.post_run(context, ui, *post_run_actions) return succeeded if ui.get_property(PLUGIN_SLEEPING): - context.wakeup(PLUGIN_WAKEUP) + context.ipc_exec(PLUGIN_WAKEUP) if ui.pop_property(RELOAD_ACCESS_MANAGER): context.reload_access_manager() @@ -187,34 +208,42 @@ def run(self, provider, context, forced=None): else: result, options = provider.navigate(context) if ui.get_property(REROUTE_PATH): - return + xbmcplugin.endOfDirectory( + handle, + succeeded=False, + updateListing=True, + cacheToDisc=False, + ) + return False except KodionException as exc: result = None options = {} if not provider.handle_exception(context, exc): - msg = ('XbmcRunner.run - Error' - '\n\tException: {exc!r}' - '\n\tStack trace (most recent call last):\n{stack}' - .format(exc=exc, - stack=format_stack())) - context.log_error(msg) + logging.exception('Error') ui.on_ok('Error in ContentProvider', exc.__str__()) - if not ui.pop_property(REFRESH_CONTAINER) and forced: - player_video_id = ui.pop_property(PLAYER_VIDEO_ID) - if player_video_id: + if not ui.pop_property(REFRESH_CONTAINER, as_bool=True) and forced: + played_video_id = ui.pop_property(PLAYER_VIDEO_ID) + if played_video_id: focused_video_id = None - played_video_id = player_video_id else: - focused_video_id = ui.get_property(VIDEO_ID) + focused_video_id = None if route else ui.get_property(VIDEO_ID) played_video_id = None else: focused_video_id = None played_video_id = None + sync_items = (focused_video_id, played_video_id) - items = isinstance(result, (list, tuple)) - item_count = 0 - if items: + play_cancelled = ui.pop_property(PLAY_CANCELLED) + if play_cancelled: + result = None + + force_resolve = options.get(provider.FORCE_RESOLVE) + force_return = options.get(provider.FORCE_RETURN) + result_item = None + result_item_type = None + items = None + if isinstance(result, (list, tuple)): if not result: result = [ CommandItem( @@ -225,51 +254,50 @@ def run(self, provider, context, forced=None): plot=context.localize('page.empty'), ) ] - - force_resolve = provider.FORCE_RESOLVE - if not options.pop(force_resolve, False): - force_resolve = False - - items = [ - self._LIST_ITEM_MAP[item.__class__.__name__]( + if force_return: + _, _post_run_action = self.uri_action( + context, + 'command://Action(Back)' + ) + if _post_run_action: + post_run_actions.append(_post_run_action) + _post_run_action = None + + items = [] + for item in result: + item_type = item.__class__.__name__ + + if (force_resolve + and not result_item + and item_type in self._PLAY_ITEM_MAP + and item.playable): + result_item = item + result_item_type = item_type + + listitem_type = self._LIST_ITEM_MAP.get(item_type) + if (not listitem_type + or (listitem_type == directory_listitem + and not item.available)): + continue + + items.append(listitem_type( context, item, show_fanart=show_fanart, - focused=focused_video_id, - played=played_video_id, - ) - for item in result - if self.classify_list_item(item, options, force_resolve) - ] - item_count = len(items) - - if force_resolve: - result = options.get(force_resolve) - - if result and result.__class__.__name__ in self._PLAY_ITEM_MAP: - if options.get(provider.FORCE_PLAY) or not result.playable: - result, post_run_action = self.uri_action( - context, - result.get_uri() - ) - else: - item = self._PLAY_ITEM_MAP[result.__class__.__name__]( - context, - result, - show_fanart=show_fanart, - ) - xbmcplugin.setResolvedUrl( - handle, succeeded=True, listitem=item - ) + to_sync=sync_items, + )) + else: + result_item = result + result_item_type = result.__class__.__name__ - if item_count: + if items: content_type = options.get(provider.CONTENT_TYPE) if content_type: context.apply_content(**content_type) else: context.apply_content() succeeded = xbmcplugin.addDirectoryItems( - handle, items, item_count + handle, items, len(items) ) cache_to_disc = options.get(provider.CACHE_TO_DISC, True) update_listing = options.get(provider.UPDATE_LISTING, False) @@ -279,64 +307,97 @@ def run(self, provider, context, forced=None): ui.clear_property(provider.FALLBACK) else: succeeded = bool(result) - if not succeeded: - ui.clear_property(BUSY_FLAG) - for param in FORCE_PLAY_PARAMS: - ui.clear_property(param) - - uri = context.get_uri() - fallback = options.get(provider.FALLBACK, True) - if isinstance(fallback, string_type) and fallback != uri: + cache_to_disc = options.get(provider.CACHE_TO_DISC, False) + update_listing = options.get(provider.UPDATE_LISTING, True) + + if result_item and result_item_type in self._PLAY_ITEM_MAP: + if not forced and not force_resolve and path != PATHS.PLAY: + force_play = True + else: + force_play = options.get(provider.FORCE_PLAY) + + if force_play or not result_item.playable: + _, _post_run_action = self.uri_action( + context, + result_item.get_uri() + ) + if _post_run_action: + post_run_actions.append(_post_run_action) + _post_run_action = None + else: + item = self._PLAY_ITEM_MAP[result_item_type]( + context, + result_item, + show_fanart=show_fanart, + ) + xbmcplugin.setResolvedUrl( + handle, succeeded=True, listitem=item + ) + elif not items or force_return: + ui.clear_property(BUSY_FLAG) + ui.clear_property(TRAKT_PAUSE_FLAG, raw=True) + for param in FORCE_PLAY_PARAMS: + ui.clear_property(param) + + fallback = options.get(provider.FALLBACK, not force_return) + if isinstance(fallback, string_type) and fallback != uri: + if options.get(provider.POST_RUN): + _, _post_run_action = self.uri_action( + context, + fallback, + ) + else: context.parse_uri(fallback, update=True) return self.run(provider, context, forced=forced) - if fallback: - _post_run_action = None - - if context.is_plugin_folder(): - if context.is_plugin_path( - uri, PATHS.PLAY - ): - context.send_notification( - PLAYBACK_FAILED, - {'video_id': context.get_param('video_id')}, - ) - # None of the following will actually prevent the - # playback attempt from occurring - item = xbmcgui.ListItem(path=uri, offscreen=True) - item.setContentLookup(False) - props = { - 'isPlayable': 'false', - 'ForceResolvePlugin': 'true', - } - item.setProperties(props) - xbmcplugin.setResolvedUrl( - handle, - succeeded=False, - listitem=item, - ) - elif context.is_plugin_path( - context.get_infolabel('Container.FolderPath') - ): - _, _post_run_action = self.uri_action( - context, - context.get_parent_uri(params={ - WINDOW_FALLBACK: True, - WINDOW_REPLACE: True, - WINDOW_RETURN: False, - }), - ) - else: - _, _post_run_action = self.uri_action( - context, - 'command://Action(Back)', - ) - if post_run_action and _post_run_action: - post_run_action = (post_run_action, _post_run_action) + elif fallback: + if play_cancelled: + _, _post_run_action = self.uri_action(context, uri) + elif path == PATHS.PLAY: + context.send_notification( + PLAYBACK_FAILED, + {VIDEO_ID: context.get_param(VIDEO_ID)}, + ) + # None of the following will actually prevent the + # playback attempt from occurring + item = xbmcgui.ListItem(path=uri, offscreen=True) + item.setContentLookup(False) + props = { + 'isPlayable': 'false', + 'ForceResolvePlugin': 'true', + } + item.setProperties(props) + xbmcplugin.setResolvedUrl( + handle, + succeeded=False, + listitem=item, + ) + elif options.get(provider.FORCE_REFRESH): + _post_run_action = ( + context.send_notification, + { + 'method': REFRESH_CONTAINER, + }, + ) + else: + if context.is_plugin_path( + ui.get_container_info(FOLDER_URI, container_id=None) + ): + _, _post_run_action = self.uri_action( + context, + context.get_parent_uri(params={ + WINDOW_FALLBACK: True, + WINDOW_REPLACE: True, + WINDOW_RETURN: False, + }), + ) else: - post_run_action = _post_run_action - - cache_to_disc = options.get(provider.CACHE_TO_DISC, False) - update_listing = options.get(provider.UPDATE_LISTING, True) + _, _post_run_action = self.uri_action( + context, + 'command://Action(Back)', + ) + if _post_run_action: + post_run_actions.append(_post_run_action) + _post_run_action = None if ui.pop_property(PLAY_FORCED): context.set_path(PATHS.PLAY) @@ -349,49 +410,79 @@ def run(self, provider, context, forced=None): cacheToDisc=cache_to_disc, ) - container = ui.pop_property(CONTAINER_ID) - position = ui.pop_property(CONTAINER_POSITION) - if container and position: - context.send_notification(CONTAINER_FOCUS, [container, position]) - - if isinstance(post_run_action, tuple): - self.post_run(context, ui, *post_run_action) - elif post_run_action: - self.post_run(context, ui, post_run_action) + if not force_return: + if any(sync_items): + context.send_notification(SYNC_LISTITEM, sync_items) + + container = ui.get_property(CONTAINER_ID) + position = ui.get_property(CONTAINER_POSITION) + + if is_same_path: + if (container and position + and (forced or position == 'current') + and (not played_video_id or route)): + post_run_actions.append(( + context.send_notification, + { + 'method': CONTAINER_FOCUS, + 'data': { + CONTAINER_ID: container, + CONTAINER_POSITION: position, + }, + }, + )) + + if post_run_actions: + self.post_run(context, ui, *post_run_actions) return succeeded @staticmethod def post_run(context, ui, *actions, **kwargs): timeout = kwargs.get('timeout', 30) + interval = kwargs.get('interval', 0.1) for action in actions: - while ui.busy_dialog_active(): - timeout -= 1 + while not ui.get_container(container_type=None, check_ready=True): + timeout -= interval if timeout < 0: - context.log_error('Multiple busy dialogs active' - ' - Post run action unable to execute') + logging.error('Container not ready' + ' - Post run action unable to execute') break - context.sleep(1) + context.sleep(interval) else: - context.execute(action) + if isinstance(action, tuple): + action, action_kwargs = action + else: + action_kwargs = None + logging.debug('Executing queued post-run action: {action}', + action=action, + stacklevel=2) + if callable(action): + if action_kwargs: + action(**action_kwargs) + else: + action() + else: + context.execute(action) + context.sleep(interval) @staticmethod def uri_action(context, uri): if uri.startswith('script://'): _uri = uri[len('script://'):] - log_action = 'Running script' + log_action = 'RunScript queued' log_uri = _uri action = 'RunScript({0})'.format(_uri) result = True elif uri.startswith('command://'): _uri = uri[len('command://'):] - log_action = 'Running command' + log_action = 'Builtin command queued' log_uri = _uri action = _uri result = True elif uri.startswith('PlayMedia('): - log_action = 'Redirecting for playback' + log_action = 'Redirect for playback queued' log_uri = uri[len('PlayMedia('):-1].split(',') log_uri[0] = parse_and_redact_uri( log_uri[0], @@ -402,7 +493,7 @@ def uri_action(context, uri): result = True elif uri.startswith('RunPlugin('): - log_action = 'Running plugin' + log_action = 'RunPlugin queued' log_uri = parse_and_redact_uri( uri[len('RunPlugin('):-1], redact_only=True, @@ -411,7 +502,7 @@ def uri_action(context, uri): result = True elif uri.startswith('ActivateWindow('): - log_action = 'Activating window' + log_action = 'ActivateWindow queued' log_uri = uri[len('ActivateWindow('):-1].split(',') if len(log_uri) >= 2: log_uri[1] = parse_and_redact_uri( @@ -423,7 +514,7 @@ def uri_action(context, uri): result = False elif uri.startswith('ReplaceWindow('): - log_action = 'Replacing window' + log_action = 'ReplaceWindow queued' log_uri = uri[len('ReplaceWindow('):-1].split(',') if len(log_uri) >= 2: log_uri[1] = parse_and_redact_uri( @@ -434,10 +525,34 @@ def uri_action(context, uri): action = uri result = False + elif uri.startswith('Container.Update('): + log_action = 'Container.Update queued' + log_uri = uri[len('Container.Update('):-1].split(',') + if log_uri[0]: + log_uri[0] = parse_and_redact_uri( + log_uri[0], + redact_only=True, + ) + log_uri = ','.join(log_uri) + action = uri + result = False + + elif uri.startswith('Container.Refresh('): + log_action = 'Container.Refresh queued' + log_uri = uri[len('Container.Refresh('):-1].split(',') + if log_uri[0]: + log_uri[0] = parse_and_redact_uri( + log_uri[0], + redact_only=True, + ) + log_uri = ','.join(log_uri) + action = uri + result = False + elif context.is_plugin_path(uri, PATHS.PLAY): parts, params, log_uri, _ = parse_and_redact_uri(uri) - if params.get('action') == 'list': - log_action = 'Redirecting to' + if params.get(ACTION, [None])[0] == 'list': + log_action = 'Redirect queued' action = context.create_uri( (PATHS.ROUTE, parts.path.rstrip('/')), params, @@ -445,7 +560,7 @@ def uri_action(context, uri): ) result = False else: - log_action = 'Redirecting for playback' + log_action = 'Redirect for playback queued' action = context.create_uri( (parts.path.rstrip('/'),), params, @@ -457,7 +572,7 @@ def uri_action(context, uri): result = True elif context.is_plugin_path(uri): - log_action = 'Redirecting to' + log_action = 'Redirect queued' parts, params, log_uri, _ = parse_and_redact_uri(uri) action = context.create_uri( (PATHS.ROUTE, parts.path.rstrip('/') or PATHS.HOME), @@ -471,21 +586,8 @@ def uri_action(context, uri): result = False return result, action - context.log_debug(''.join(( - log_action, - ': |', - log_uri, - '|', - ))) + logging.debug('{action}: {uri!r}', + action=log_action, + uri=log_uri, + stacklevel=2) return result, action - - def classify_list_item(self, item, options, force_resolve): - item_type = item.__class__.__name__ - listitem_type = self._LIST_ITEM_MAP.get(item_type) - if force_resolve and item_type in self._PLAY_ITEM_MAP: - options.setdefault(force_resolve, item) - if listitem_type: - if listitem_type == directory_listitem: - return item.available - return True - return False diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin_runner.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin_runner.py index 33599ab2de..3824b72374 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,7 +10,14 @@ from __future__ import absolute_import, division, unicode_literals -from .constants import CHECK_SETTINGS +import gc + +from . import logging +from .constants import ( + CHECK_SETTINGS, + FOLDER_URI, + PATHS, +) from .context import XbmcContext from .debug import Profiler from .plugin import XbmcPlugin @@ -20,70 +27,115 @@ __all__ = ('run',) _context = XbmcContext() +_log = logging.getLogger(__name__) _plugin = XbmcPlugin() _provider = Provider() -_profiler = Profiler(enabled=False, print_callees=False, num_lines=20) +_profiler = Profiler(enabled=False, + timer=Profiler.elapsed_timer, + print_callees=False, + num_lines=20) def run(context=_context, + log=_log, plugin=_plugin, provider=_provider, profiler=_profiler): + ui = context.get_ui() - if context.get_ui().pop_property(CHECK_SETTINGS): - provider.reset_client() + if ui.pop_property(CHECK_SETTINGS): + provider.reset_client(context=context) settings = context.get_settings(refresh=True) else: settings = context.get_settings() - debug = settings.logging_enabled() - if debug: - context.debug_log(on=True) + log_level = settings.log_level() + if log_level: + log.debugging = True + if log_level & 2: + log.stack_info = True + log.verbose_logging = True + else: + log.stack_info = False + log.verbose_logging = False profiler.enable(flush=True) else: - context.debug_log(off=True) - - current_uri = context.get_uri() - current_path = context.get_path() - current_params = context.get_params() + log.debugging = False + log.stack_info = False + log.verbose_logging = False + profiler.disable() + + old_path = context.get_path().rstrip('/') + old_uri = ui.get_container_info(FOLDER_URI, container_id=None) + old_handle = context.get_handle() context.init() - new_uri = context.get_uri() - new_params = context.get_params() - new_handle = context.get_handle() - - forced = (new_handle != -1 - and ((current_uri == new_uri - and current_path != '/' - and current_params == new_params) - or (current_uri != new_uri - and current_path == '/' - and not current_params) - or (current_path == '/play/'))) + current_path = context.get_path().rstrip('/') + current_params = context.get_original_params() + current_handle = context.get_handle() + + new_params = {} + new_kwargs = {} + + params = context.get_params() + refresh = context.refresh_requested(params=params) + was_playing = old_path == PATHS.PLAY + is_same_path = current_path == old_path and old_handle != -1 + + 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) - if refresh: - new_params['refresh'] = refresh + refresh = context.refresh_requested(force=True, off=True, params=params) + new_params['refresh'] = refresh if refresh else 0 + + if new_params: + context.set_params(**new_params) - log_params = new_params.copy() + log_params = params.copy() for key in ('api_key', 'client_id', 'client_secret'): if key in log_params: log_params[key] = '' system_version = context.get_system_version() - context.log_notice('Plugin: Running v{version}' - '\n\tKodi: v{kodi}' - '\n\tPython: v{python}' - '\n\tHandle: {handle}' - '\n\tPath: |{path}|' - '\n\tParams: |{params}|' - .format(version=context.get_version(), - kodi=str(system_version), - python=system_version.get_python_version(), - handle=new_handle, - path=context.get_path(), - params=log_params)) - - plugin.run(provider, context, forced=forced) - - if debug: - profiler.print_stats() + log.info(('Running v{version}', + 'Kodi: v{kodi}', + 'Python: v{python}', + 'Handle: {handle}', + 'Path: {path!r} ({path_link})', + 'Params: {params!r}', + 'Forced: {forced!r}'), + version=context.get_version(), + kodi=str(system_version), + python=system_version.get_python_version(), + handle=current_handle, + path=current_path, + path_link='linked' if is_same_path else 'new', + params=log_params, + forced=forced) + + gc_threshold = gc.get_threshold() + gc.set_threshold(0) + try: + plugin.run(provider, + context, + forced=forced, + is_same_path=is_same_path, + **new_kwargs) + finally: + if log_level: + profiler.print_stats() + gc.collect() + gc.set_threshold(*gc_threshold) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/script_actions.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/script_actions.py index 1222393651..eca49fcdff 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/script_actions.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2024-present plugin.video.youtube + Copyright (C) 2024-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,6 +11,7 @@ import os +from . import logging from .compatibility import parse_qsl, urlsplit, xbmc, xbmcaddon, xbmcvfs from .constants import ( DATA_PATH, @@ -28,10 +29,13 @@ get_listen_addresses, httpd_status, ) -from .utils import rm_dir +from .utils.file_system import rm_dir from ..youtube import Provider +log = logging.getLogger(__name__) + + def _config_actions(context, action, *_args): localize = context.localize settings = context.get_settings() @@ -73,8 +77,8 @@ def _config_actions(context, action, *_args): sub_opts = [ localize('none'), localize('select'), - localize('subtitles.with_fallback') % (preferred, fallback), - localize('subtitles.with_fallback') % (preferred_no_asr, fallback), + localize('subtitles.with_fallback', (preferred, fallback)), + localize('subtitles.with_fallback', (preferred_no_asr, fallback)), preferred_no_asr, ] @@ -110,12 +114,12 @@ def _config_actions(context, action, *_args): settings.httpd_listen(addresses[selected_address]) elif action == 'show_client_ip': - context.wakeup(SERVER_WAKEUP, timeout=5) + context.ipc_exec(SERVER_WAKEUP, timeout=5) if httpd_status(context): client_ip = get_client_ip_address(context) if client_ip: ui.on_ok(context.get_name(), - context.localize('client.ip') % client_ip) + context.localize('client.ip.is.x', client_ip)) else: ui.show_notification(context.localize('client.ip.failed')) else: @@ -141,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] @@ -180,7 +187,10 @@ def _get_selected_language(item): 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] @@ -227,6 +237,7 @@ def _maintenance_actions(context, action, params): 'feed_history': context.get_feed_history, 'function_cache': context.get_function_cache, 'playback_history': context.get_playback_history, + 'requests_cache': context.get_requests_cache, 'search_history': context.get_search_history, 'watch_later': context.get_watch_later_list, } @@ -287,6 +298,7 @@ def _maintenance_actions(context, action, params): 'feed_history': 'feeds.sqlite', 'function_cache': 'cache.sqlite', 'playback_history': 'history.sqlite', + 'requests_cache': 'requests_cache.sqlite', 'search_history': 'search.sqlite', 'watch_later': 'watch_later.sqlite', 'api_keys': 'api_keys.json', @@ -370,10 +382,9 @@ def add_user(): def switch_to_user(user): access_manager.set_user(user, switch_to=True) - ui.show_notification( - localize('user.changed') % access_manager.get_username(user), - localize('user.switch') - ) + ui.show_notification(localize('user.changed_to.x', + access_manager.get_username(user)), + localize('user.switch')) if action == 'switch': result, user_index_map = select_user(localize('user.switch'), @@ -394,7 +405,7 @@ def switch_to_user(user): if user is not None: result = ui.on_yes_no_input( localize('user.switch'), - localize('user.switch.now') % details.get('name') + localize('user.switch_to.x', details.get('name')) ) if result: switch_to_user(user) @@ -409,7 +420,7 @@ def switch_to_user(user): username = access_manager.get_username(user) if ui.on_remove_content(username): access_manager.remove_user(user) - ui.show_notification(localize('removed') % username, + ui.show_notification(localize('removed.name.x', username), localize('remove')) if user == 0: access_manager.add_user(username=localize('user.default'), @@ -436,10 +447,9 @@ def switch_to_user(user): return False if access_manager.set_username(user, new_username): - ui.show_notification( - localize('renamed') % (old_username, new_username), - localize('rename') - ) + ui.show_notification(localize('renamed.x.y', + (old_username, new_username)), + localize('rename')) reload = True if reload: @@ -468,19 +478,33 @@ def run(argv): if params: params = dict(parse_qsl(args.query)) + log_level = context.get_settings().log_level() + if log_level: + log.debugging = True + if log_level & 2: + log.stack_info = True + log.verbose_logging = True + else: + log.stack_info = False + log.verbose_logging = False + else: + log.debugging = False + log.stack_info = False + log.verbose_logging = False + system_version = context.get_system_version() - context.log_notice('Script: Running v{version}' - '\n\tKodi: v{kodi}' - '\n\tPython: v{python}' - '\n\tCategory: |{category}|' - '\n\tAction: |{action}|' - '\n\tParams: |{params}|' - .format(version=context.get_version(), - kodi=str(system_version), - python=system_version.get_python_version(), - category=category, - action=action, - params=params)) + log.info(('Running v{version}', + 'Kodi: v{kodi}', + 'Python: v{python}', + 'Category: {category!r}', + 'Action: {action!r}', + 'Params: {params!r}'), + version=context.get_version(), + kodi=str(system_version), + python=system_version.get_python_version(), + category=category, + action=action, + params=params) if not category: xbmcaddon.Addon().openSettings() diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/service_runner.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/service_runner.py index ee8869e388..c4871e4b03 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/service_runner.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,15 +10,31 @@ from __future__ import absolute_import, division, unicode_literals +from . import logging from .constants import ( ABORT_FLAG, + ARTIST, + BOOKMARK_ID, + BUSY_FLAG, + CHANNEL_ID, + CONTAINER_ID, + CONTAINER_POSITION, + CURRENT_ITEM, + MARK_AS_LABEL, + PLAYLIST_ID, + PLAYLIST_ITEM_ID, + PLAY_COUNT, PLUGIN_SLEEPING, + SERVICE_RUNNING_FLAG, + SUBSCRIPTION_ID, TEMP_PATH, + TITLE, + URI, VIDEO_ID, ) from .context import XbmcContext from .monitors import PlayerMonitor, ServiceMonitor -from .utils import rm_dir +from .utils.file_system import rm_dir from ..youtube.provider import Provider @@ -29,27 +45,33 @@ def run(): context = XbmcContext() provider = Provider() - system_version = context.get_system_version() - context.log_notice('Service: Starting v{version}' - '\n\tKodi: v{kodi}' - '\n\tPython: v{python}' - .format(version=context.get_version(), - kodi=str(system_version), - python=system_version.get_python_version())) + monitor = ServiceMonitor(context=context) + player = PlayerMonitor(provider=provider, + context=context, + monitor=monitor) - get_listitem_info = context.get_listitem_info - get_listitem_property = context.get_listitem_property + system_version = context.get_system_version() + logging.info(('Starting v{version}', + 'Kodi: v{kodi}', + 'Python: v{python}'), + version=context.get_version(), + kodi=str(system_version), + python=system_version.get_python_version()) ui = context.get_ui() + get_container = ui.get_container + get_container_info = ui.get_container_info + get_listitem_info = ui.get_listitem_info + get_listitem_property = ui.get_listitem_property clear_property = ui.clear_property set_property = ui.set_property - clear_property(ABORT_FLAG) + localize = context.localize - monitor = ServiceMonitor(context=context) - player = PlayerMonitor(provider=provider, - context=context, - monitor=monitor) + 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) @@ -70,8 +92,30 @@ def run(): active_interval_ms = 100 idle_interval_ms = 1000 - video_id = None - container = monitor.is_plugin_container() + def _get_mark_as_label(_name, + container_id, + unwatched_label=localize('history.mark.unwatched'), + watched_label=localize('history.mark.watched')): + if get_listitem_info(PLAY_COUNT, container_id): + return unwatched_label + return watched_label + + container_id = None + container_position = None + item_has_id = None + plugin_item_details = { + VIDEO_ID: {'getter': get_listitem_property, 'value': None}, + BOOKMARK_ID: {'getter': get_listitem_property, 'value': None}, + CHANNEL_ID: {'getter': get_listitem_property, 'value': None}, + PLAYLIST_ID: {'getter': get_listitem_property, 'value': None}, + PLAYLIST_ITEM_ID: {'getter': get_listitem_property, 'value': None}, + SUBSCRIPTION_ID: {'getter': get_listitem_property, 'value': None}, + '__has_id__': {'getter': None, 'value': TypeError}, + URI: {'getter': get_listitem_info, 'value': None}, + TITLE: {'getter': get_listitem_info, 'value': None}, + ARTIST: {'getter': get_listitem_info, 'value': None}, + MARK_AS_LABEL: {'getter': _get_mark_as_label, 'value': None}, + } while not monitor.abortRequested(): is_idle = monitor.system_idle or monitor.get_idle_time() >= loop_period @@ -115,7 +159,8 @@ def run(): else: monitor.shutdown_httpd(terminate=True) - check_item = not plugin_is_idle and container['is_plugin'] + container = get_container(container_type=False) + check_item = not plugin_is_idle and all(container.values()) if check_item: wait_interval_ms = active_interval_ms else: @@ -134,13 +179,13 @@ def run(): if monitor.refresh and all(container.values()): monitor.refresh_container(force=True) - monitor.refresh = False break - if monitor.interrupt: + if (monitor.interrupt + or (not check_item and wait_time_ms >= idle_interval_ms)): monitor.interrupt = False - container = monitor.is_plugin_container() - if check_item != container['is_plugin']: + container = get_container(container_type=False) + if check_item != all(container.values()): check_item = not check_item if check_item: wait_interval_ms = active_interval_ms @@ -149,14 +194,43 @@ def run(): wait_interval = wait_interval_ms / 1000 if check_item: - new_video_id = get_listitem_property(VIDEO_ID) - if new_video_id: - if video_id != new_video_id: - video_id = new_video_id - set_property(VIDEO_ID, video_id) - elif video_id and get_listitem_info('Label'): - video_id = None - clear_property(VIDEO_ID) + if container_id != container['id']: + container_id = container['id'] + set_property(CONTAINER_ID, container_id) + + _position = get_container_info(CURRENT_ITEM, container_id) + if _position and _position != container_position: + _item_has_id = None + for name, detail in plugin_item_details.items(): + value = detail['value'] + if value is TypeError: + if _item_has_id is None: + container = get_container(container_type=False) + if check_item != all(container.values()): + check_item = not check_item + break + _item_has_id = False + continue + if _item_has_id is not False: + new_value = detail['getter'](name, container_id) + else: + new_value = None + if new_value: + if new_value != value: + detail['value'] = new_value + set_property(name, new_value) + _item_has_id = True + elif value: + detail['value'] = None + clear_property(name) + else: + container_position = _position + if _item_has_id: + set_property(CONTAINER_POSITION, container_position) + elif item_has_id: + clear_property(CONTAINER_POSITION) + item_has_id = _item_has_id + elif not plugin_is_idle and not container['is_plugin']: plugin_is_idle = set_property(PLUGIN_SLEEPING) @@ -171,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) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/__init__.py index ed3f6d9a81..56830b6d09 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 690285db76..96b4b6d5d9 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,13 +12,17 @@ import sys -from ..constants import SETTINGS -from ..utils import ( - current_system_version, - get_kodi_setting_bool, - get_kodi_setting_value, +from ..constants import ( + HIDE_LIVE, + HIDE_MEMBERS, + HIDE_PLAYLISTS, + HIDE_SEARCH, + HIDE_SHORTS, + SETTINGS, ) from ..network.http_server import validate_ip_address +from ..utils.methods import get_kodi_setting_bool, get_kodi_setting_value +from ..utils.system_version import current_system_version class AbstractSettings(object): @@ -27,7 +31,7 @@ class AbstractSettings(object): _vars[name] = value del _vars - _echo = False + _echo_level = 0 _cache = {} _check_set = True @@ -35,28 +39,28 @@ class AbstractSettings(object): def flush(cls, xbmc_addon): raise NotImplementedError() - def get_bool(self, setting, default=None, echo=None): + def get_bool(self, setting, default=None, echo_level=2): raise NotImplementedError() - def set_bool(self, setting, value, echo=None): + def set_bool(self, setting, value, echo_level=2): raise NotImplementedError() - def get_int(self, setting, default=-1, converter=None, echo=None): + def get_int(self, setting, default=-1, converter=None, echo_level=2): raise NotImplementedError() - def set_int(self, setting, value, echo=None): + def set_int(self, setting, value, echo_level=2): raise NotImplementedError() - def get_string(self, setting, default='', echo=None): + def get_string(self, setting, default='', echo_level=2): raise NotImplementedError() - def set_string(self, setting, value, echo=None): + def set_string(self, setting, value, echo_level=2): raise NotImplementedError() - def get_string_list(self, setting, default=None, echo=None): + def get_string_list(self, setting, default=None, echo_level=2): raise NotImplementedError() - def set_string_list(self, setting, value, echo=None): + def set_string_list(self, setting, value, echo_level=2): raise NotImplementedError() def open_settings(self): @@ -171,14 +175,26 @@ def set_subtitle_download(self, value): return self.set_bool(SETTINGS.SUBTITLE_DOWNLOAD, value) _THUMB_SIZES = { - 0: { # Medium (16:9) + 3: { # default (4:3) + 'size': 120 * 90, + 'ratio': 120 / 90, + }, + 0: { # mqdefault (16:9) 'size': 320 * 180, 'ratio': 320 / 180, }, - 1: { # High (4:3) + 1: { # hqdefault (4:3) 'size': 480 * 360, 'ratio': 480 / 360, }, + 4: { # sddefault (4:3) + 'size': 640 * 480, + 'ratio': 640 / 480, + }, + 5: { # hq720 (16:9) + 'size': 1280 * 720, + 'ratio': 1280 / 720, + }, 2: { # Best available 'size': 0, 'ratio': 0, @@ -226,6 +242,12 @@ def requests_timeout(self, value=None): read_timout = self.get_int(SETTINGS.READ_TIMEOUT, 27) return connect_timeout, read_timout + def requests_cache_size(self, value=None): + if value is not None: + self.set_int(SETTINGS.REQUESTS_CACHE_SIZE, value) + return value + return self.get_int(SETTINGS.REQUESTS_CACHE_SIZE, 20) + _PROXY_TYPE_SCHEME = { 0: 'http', 1: 'socks4', @@ -303,12 +325,13 @@ def proxy_settings(self, value=None, as_mapping=True): setting.get('kodi_name'), process=setting_type, ) or setting_default - elif setting_type is int: - setting_value = self.get_int(setting_name, setting_default) - elif setting_type is str: - setting_value = self.get_string(setting_name, setting_default) else: - setting_value = self.get_bool(setting_name, setting_default) + setting_method = ( + self.get_int if setting_type is int else + self.get_string if setting_type is str else + self.get_bool + ) + setting_value = setting_method(setting_name, setting_default) settings[setting_name] = { 'value': setting_value, @@ -390,7 +413,7 @@ def use_mpd_videos(self, value=None): def live_stream_type(self, value=None): if self.use_isa(): - default = 3 + default = 2 setting = SETTINGS.LIVE_STREAMS + '.1' else: default = 1 @@ -441,7 +464,8 @@ def httpd_listen(self, value=None): ip_address = '.'.join(map(str, octets)) if value is not None: - return self.set_string(SETTINGS.HTTPD_LISTEN, ip_address) + if not self.set_string(SETTINGS.HTTPD_LISTEN, ip_address): + return False return ip_address def httpd_whitelist(self): @@ -487,7 +511,7 @@ def api_secret(self, new_secret=None): return self.get_string(SETTINGS.API_SECRET) def get_location(self): - location = self.get_string(SETTINGS.LOCATION, '').replace(' ', '').strip() + location = self.get_string(SETTINGS.LOCATION).replace(' ', '').strip() coords = location.split(',') latitude = longitude = None if len(coords) == 2: @@ -508,7 +532,10 @@ def set_location(self, value): self.set_string(SETTINGS.LOCATION, value) def get_location_radius(self): - return ''.join((self.get_int(SETTINGS.LOCATION_RADIUS, 500, str), 'km')) + return ''.join(( + self.get_int(SETTINGS.LOCATION_RADIUS, 500, str), + 'km' + )) def get_play_count_min_percent(self): return self.get_int(SETTINGS.PLAY_COUNT_MIN_PERCENT, 0) @@ -516,22 +543,24 @@ def get_play_count_min_percent(self): def use_local_history(self): return self.get_bool(SETTINGS.USE_LOCAL_HISTORY, False) - def use_remote_history(self): + def use_remote_history(self, value=None): + if value is not None: + return self.set_bool(SETTINGS.USE_REMOTE_HISTORY, value) return self.get_bool(SETTINGS.USE_REMOTE_HISTORY, False) - # Selections based on max width and min height at common (utra-)wide aspect ratios - _QUALITY_SELECTIONS = { # Setting | Resolution - 7: {'width': 7680, 'min_height': 3148, 'nom_height': 4320, 'label': '{0}p{1} (8K){2}'}, # 7 | 4320p 8K - 6: {'width': 3840, 'min_height': 1080, 'nom_height': 2160, 'label': '{0}p{1} (4K){2}'}, # 6 | 2160p 4K - 5: {'width': 2560, 'min_height': 984, 'nom_height': 1440, 'label': '{0}p{1} (QHD){2}'}, # 5 | 1440p 2.5K / QHD - 4.1: {'width': 2048, 'min_height': 858, 'nom_height': 1152, 'label': '{0}p{1} (2K){2}'}, # N/A | 1152p 2K / QWXGA - 4: {'width': 1920, 'min_height': 787, 'nom_height': 1080, 'label': '{0}p{1} (FHD){2}'}, # 4 | 1080p FHD - 3: {'width': 1280, 'min_height': 525, 'nom_height': 720, 'label': '{0}p{1} (HD){2}'}, # 3 | 720p HD - 2: {'width': 854, 'min_height': 350, 'nom_height': 480, 'label': '{0}p{1}{2}'}, # 2 | 480p - 1: {'width': 640, 'min_height': 263, 'nom_height': 360, 'label': '{0}p{1}{2}'}, # 1 | 360p - 0: {'width': 426, 'min_height': 175, 'nom_height': 240, 'label': '{0}p{1}{2}'}, # 0 | 240p - -1: {'width': 256, 'min_height': 105, 'nom_height': 144, 'label': '{0}p{1}{2}'}, # N/A | 144p - -2: {'width': 0, 'min_height': 0, 'nom_height': 0, 'label': '{0}p{1}{2}'}, # N/A | Custom + # Selections based on width and min height at common aspect ratios + _QUALITY_SELECTIONS = { # Setting | Resolution + 7: {'width_16:9': 7680, 'width_4:3': 5760, 'min_height': 3148, 'nom_height': 4320, 'label': '{0}p{1} (8K){2}{3}{4}'}, # 7 | 4320p 8K + 6: {'width_16:9': 3840, 'width_4:3': 2880, 'min_height': 1080, 'nom_height': 2160, 'label': '{0}p{1} (4K){2}{3}{4}'}, # 6 | 2160p 4K + 5: {'width_16:9': 2560, 'width_4:3': 1920, 'min_height': 984, 'nom_height': 1440, 'label': '{0}p{1} (QHD){2}{3}{4}'}, # 5 | 1440p 2.5K / QHD + 4.1: {'width_16:9': 2048, 'width_4:3': 1536, 'min_height': 858, 'nom_height': 1152, 'label': '{0}p{1} (2K){2}{3}{4}'}, # N/A | 1152p 2K / QWXGA + 4: {'width_16:9': 1920, 'width_4:3': 1440, 'min_height': 787, 'nom_height': 1080, 'label': '{0}p{1} (FHD){2}{3}{4}'}, # 4 | 1080p FHD + 3: {'width_16:9': 1280, 'width_4:3': 960, 'min_height': 525, 'nom_height': 720, 'label': '{0}p{1} (HD){2}{3}{4}'}, # 3 | 720p HD + 2: {'width_16:9': 854, 'width_4:3': 640, 'min_height': 350, 'nom_height': 480, 'label': '{0}p{1}{2}{3}{4}'}, # 2 | 480p + 1: {'width_16:9': 640, 'width_4:3': 480, 'min_height': 263, 'nom_height': 360, 'label': '{0}p{1}{2}{3}{4}'}, # 1 | 360p + 0: {'width_16:9': 426, 'width_4:3': 320, 'min_height': 175, 'nom_height': 240, 'label': '{0}p{1}{2}{3}{4}'}, # 0 | 240p + -1: {'width_16:9': 256, 'width_4:3': 192, 'min_height': 105, 'nom_height': 144, 'label': '{0}p{1}{2}{3}{4}'}, # N/A | 144p + -2: {'width_16:9': 0, 'width_4:3': 0, 'min_height': 0, 'nom_height': 0, 'label': '{0}p{1}{2}{3}{4}'}, # N/A | Custom } def mpd_video_qualities(self, value=None): @@ -572,7 +601,7 @@ def stream_select(self, value=None): return self._STREAM_SELECT[value] return self._STREAM_SELECT[default] - _DEFAULT_FILTER = { + _DEFAULT_ITEM_FILTER = { 'shorts': True, 'upcoming': True, 'upcoming_live': True, @@ -582,40 +611,68 @@ def stream_select(self, value=None): 'vod': True, 'custom': None, } + _DEFAULT_FOLDER_FILTER = { + HIDE_PLAYLISTS: False, + HIDE_SEARCH: False, + HIDE_SHORTS: False, + HIDE_LIVE: False, + HIDE_MEMBERS: False + } - def item_filter(self, update=None, override=None, exclude=None): + def item_filter(self, + update=None, + override=None, + params=None, + exclude=None): if override is None: override = self.get_string_list(SETTINGS.HIDE_VIDEOS) - override = dict.fromkeys(override, False) - override['custom'] = (self.get_string(SETTINGS.FILTER_LIST) - .split(',')) + override = { + filter_type: filter_type.startswith('hide_') + for filter_type in override + } + override['custom'] = self.get_string(SETTINGS.FILTER_LIST).split(',') elif isinstance(override, (list, tuple)): _override = {'custom': []} - for value in override: - if value in self._DEFAULT_FILTER: - _override[value] = False + for filter_type in override: + if filter_type in self._DEFAULT_ITEM_FILTER: + _override[filter_type] = False + elif filter_type in self._DEFAULT_FOLDER_FILTER: + _override[filter_type] = True else: - _override['custom'].append(value) + _override['custom'].append(filter_type) override = _override - types = dict(self._DEFAULT_FILTER, **override) + + if params: + _override = { + filter_type: value + for filter_type, value in params.items() + if filter_type in self._DEFAULT_FOLDER_FILTER + } + if override is None: + override = _override + else: + override.update(_override) + + filter_types = dict(self._DEFAULT_ITEM_FILTER, **override) if update: if 'live_folder' in update: - if 'live_folder' not in types: + if 'live_folder' not in filter_types: update.update(( - ('vod', False), ('upcoming', True), ('upcoming_live', True), ('live', True), ('premieres', True), ('completed', True), )) - types.update(update) + if 'vod' not in update: + update['vod'] = False + filter_types.update(update) if exclude: - types['exclude'] = exclude + filter_types['exclude'] = exclude - return types + return filter_types def subscriptions_filter_enabled(self, value=None): if value is not None: @@ -632,9 +689,9 @@ def subscriptions_filter(self, value=None): if isinstance(value, (list, tuple, set)): value = ','.join(value).lstrip(',') return self.set_string(SETTINGS.SUBSCRIPTIONS_FILTER_LIST, value) - return self.get_string( - SETTINGS.SUBSCRIPTIONS_FILTER_LIST, '' - ).replace(', ', ',') + return self.get_string(SETTINGS.SUBSCRIPTIONS_FILTER_LIST).replace( + ', ', ',' + ) def shorts_duration(self, value=None): if value is not None: @@ -664,13 +721,13 @@ def set_region(self, region_id): return self.set_string(SETTINGS.REGION, region_id) def get_watch_later_playlist(self): - return self.get_string(SETTINGS.WATCH_LATER_PLAYLIST, '').strip() + return self.get_string(SETTINGS.WATCH_LATER_PLAYLIST).strip() def set_watch_later_playlist(self, value): return self.set_string(SETTINGS.WATCH_LATER_PLAYLIST, value) def get_history_playlist(self): - return self.get_string(SETTINGS.HISTORY_PLAYLIST, '').strip() + return self.get_string(SETTINGS.HISTORY_PLAYLIST).strip() def set_history_playlist(self, value): return self.set_string(SETTINGS.HISTORY_PLAYLIST, value) @@ -701,6 +758,13 @@ def get_label_color(self, label_part): def get_channel_name_aliases(self): return frozenset(self.get_string_list(SETTINGS.CHANNEL_NAME_ALIASES)) - def logging_enabled(self): - return (self.get_int(SETTINGS.LOGGING_ENABLED, 0) + def log_level(self, value=None): + if value is not None: + return self.set_int(SETTINGS.LOG_LEVEL, value) + return (self.get_int(SETTINGS.LOG_LEVEL, 0) or get_kodi_setting_bool('debug.showloginfo')) + + def exec_limit(self, value=None): + if value is not None: + return self.set_int(SETTINGS.EXEC_LIMIT, value) + return self.get_int(SETTINGS.EXEC_LIMIT, 0) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py index 1e8051cb85..b6ffe0b9ba 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -13,9 +13,9 @@ from weakref import ref from ..abstract_settings import AbstractSettings +from ... import logging from ...compatibility import xbmcaddon -from ...constants import ADDON_ID, VALUE_FROM_STR -from ...logger import Logger +from ...constants import ADDON_ID, BOOL_FROM_STR from ...utils.system_version import current_system_version @@ -93,7 +93,9 @@ def ref(self): del self._ref -class XbmcPluginSettings(AbstractSettings, Logger): +class XbmcPluginSettings(AbstractSettings): + log = logging.getLogger(__name__) + _instances = set() _proxy = None @@ -132,9 +134,9 @@ def flush(self, xbmc_addon=None, fill=False, flush_all=True): self.__class__._instances.add(xbmc_addon) self._proxy = SettingsProxy(xbmc_addon) - self._echo = self.logging_enabled() + self._echo_level = self.log_level() - def get_bool(self, setting, default=None, echo=None): + def get_bool(self, setting, default=None, echo_level=2): if setting in self._cache: return self._cache[setting] @@ -144,8 +146,8 @@ def get_bool(self, setting, default=None, echo=None): except (TypeError, ValueError) as exc: error = exc try: - value = self.get_string(setting, echo=False) - value = VALUE_FROM_STR.get(value, default) + value = self.get_string(setting, echo_level=0) + value = BOOL_FROM_STR.get(value, default) except TypeError as exc: error = exc value = default @@ -153,15 +155,17 @@ def get_bool(self, setting, default=None, echo=None): error = exc value = default - if self._echo and echo is not False: - self.log_debug('Get |{setting}|: {value} (bool, {status})' - .format(setting=setting, - value=value, - status=error if error else 'success')) + if echo_level and self._echo_level: + self.log.debug_trace('Get setting {name!r}:' + ' {value!r} (bool, {state})', + name=setting, + value=value, + state=(error if error else 'success'), + stacklevel=echo_level) self._cache[setting] = value return value - def set_bool(self, setting, value, echo=None): + def set_bool(self, setting, value, echo_level=2): try: error = not self._proxy.set_bool(setting, value) if error and self._check_set: @@ -172,14 +176,16 @@ def set_bool(self, setting, value, echo=None): except (RuntimeError, TypeError) as exc: error = exc - if self._echo and echo is not False: - self.log_debug('Set |{setting}|: {value} (bool, {status})' - .format(setting=setting, - value=value, - status=error if error else 'success')) + if echo_level and self._echo_level: + self.log.debug_trace('Set setting {name!r}:' + ' {value!r} (bool, {state})', + name=setting, + value=value, + state=(error if error else 'success'), + stacklevel=echo_level) return not error - def get_int(self, setting, default=-1, process=None, echo=None): + def get_int(self, setting, default=-1, process=None, echo_level=2): if setting in self._cache: return self._cache[setting] @@ -191,7 +197,7 @@ def get_int(self, setting, default=-1, process=None, echo=None): except (TypeError, ValueError) as exc: error = exc try: - value = self.get_string(setting, echo=False) + value = self.get_string(setting, echo_level=0) value = int(value) except (TypeError, ValueError) as exc: error = exc @@ -200,15 +206,17 @@ def get_int(self, setting, default=-1, process=None, echo=None): error = exc value = default - if self._echo and echo is not False: - self.log_debug('Get |{setting}|: {value} (int, {status})' - .format(setting=setting, - value=value, - status=error if error else 'success')) + if echo_level and self._echo_level: + self.log.debug_trace('Get setting {name!r}:' + ' {value!r} (int, {state})', + name=setting, + value=value, + state=(error if error else 'success'), + stacklevel=echo_level) self._cache[setting] = value return value - def set_int(self, setting, value, echo=None): + def set_int(self, setting, value, echo_level=2): try: error = not self._proxy.set_int(setting, value) if error and self._check_set: @@ -219,14 +227,16 @@ def set_int(self, setting, value, echo=None): except (RuntimeError, TypeError) as exc: error = exc - if self._echo and echo is not False: - self.log_debug('Set |{setting}|: {value} (int, {status})' - .format(setting=setting, - value=value, - status=error if error else 'success')) + if echo_level and self._echo_level: + self.log.debug_trace('Set setting {name!r}:' + ' {value!r} (int, {state})', + name=setting, + value=value, + state=(error if error else 'success'), + stacklevel=echo_level) return not error - def get_string(self, setting, default='', echo=None): + def get_string(self, setting, default='', echo_level=2): if setting in self._cache: return self._cache[setting] @@ -237,23 +247,29 @@ def get_string(self, setting, default='', echo=None): error = exc value = default - if self._echo and echo is not False: - if setting == 'youtube.location': - echo = 'xx.xxxx,xx.xxxx' - elif setting == 'youtube.api.id': - echo = '...'.join((value[:3], value[-5:])) - elif setting in {'youtube.api.key', 'youtube.api.secret'}: - echo = '...'.join((value[:3], value[-3:])) + if echo_level and self._echo_level: + if setting == self.LOCATION: + log_value = 'xx.xxxx,xx.xxxx' + elif setting == self.API_ID: + log_value = ('...'.join((value[:3], value[-5:])) + if len(value) > 11 else + '...') + elif setting in {self.API_KEY, self.API_SECRET}: + log_value = ('...'.join((value[:3], value[-3:])) + if len(value) > 9 else + '...') else: - echo = value - self.log_debug('Get |{setting}|: "{echo}" (str, {status})' - .format(setting=setting, - echo=echo, - status=error if error else 'success')) + log_value = value + self.log.debug_trace('Get setting {name!r}:' + ' {value!r} (str, {state})', + name=setting, + value=log_value, + state=(error if error else 'success'), + stacklevel=echo_level) self._cache[setting] = value return value - def set_string(self, setting, value, echo=None): + def set_string(self, setting, value, echo_level=2): try: error = not self._proxy.set_str(setting, value) if error and self._check_set: @@ -264,22 +280,28 @@ def set_string(self, setting, value, echo=None): except (RuntimeError, TypeError) as exc: error = exc - if self._echo and echo is not False: - if setting == 'youtube.location': - echo = 'xx.xxxx,xx.xxxx' - elif setting == 'youtube.api.id': - echo = '...'.join((value[:3], value[-5:])) - elif setting in {'youtube.api.key', 'youtube.api.secret'}: - echo = '...'.join((value[:3], value[-3:])) + if echo_level and self._echo_level: + if setting == self.LOCATION: + log_value = 'xx.xxxx,xx.xxxx' + elif setting == self.API_ID: + log_value = ('...'.join((value[:3], value[-5:])) + if len(value) > 11 else + '...') + elif setting in {self.API_KEY, self.API_SECRET}: + log_value = ('...'.join((value[:3], value[-3:])) + if len(value) > 9 else + '...') else: - echo = value - self.log_debug('Set |{setting}|: "{echo}" (str, {status})' - .format(setting=setting, - echo=echo, - status=error if error else 'success')) + log_value = value + self.log.debug_trace('Set setting {name!r}:' + ' {value!r} (str, {state})', + name=setting, + value=log_value, + state=(error if error else 'success'), + stacklevel=echo_level) return not error - def get_string_list(self, setting, default=None, echo=None): + def get_string_list(self, setting, default=None, echo_level=2): if setting in self._cache: return self._cache[setting] @@ -290,17 +312,19 @@ def get_string_list(self, setting, default=None, echo=None): value = [] if default is None else default except (RuntimeError, TypeError) as exc: error = exc - value = default - - if self._echo and echo is not False: - self.log_debug('Get |{setting}|: "{value}" (str list, {status})' - .format(setting=setting, - value=value, - status=error if error else 'success')) + value = [] if default is None else default + + if echo_level and self._echo_level: + self.log.debug_trace('Get setting {name!r}:' + ' {value} (list[str], {state})', + name=setting, + value=value, + state=(error if error else 'success'), + stacklevel=echo_level) self._cache[setting] = value return value - def set_string_list(self, setting, value, echo=None): + def set_string_list(self, setting, value, echo_level=2): try: error = not self._proxy.set_str_list(setting, value) if error and self._check_set: @@ -311,9 +335,11 @@ def set_string_list(self, setting, value, echo=None): except (RuntimeError, TypeError) as exc: error = exc - if self._echo and echo is not False: - self.log_debug('Set |{setting}|: "{value}" (str list, {status})' - .format(setting=setting, - value=value, - status=error if error else 'success')) + if echo_level and self._echo_level: + self.log.debug_trace('Set setting {name!r}:' + ' {value} (list[str], {state})', + name=setting, + value=value, + state=(error if error else 'success'), + stacklevel=echo_level) return not error diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/__init__.py index d626623796..94cb4565bf 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,6 +12,7 @@ from .feed_history import FeedHistory from .function_cache import FunctionCache from .playback_history import PlaybackHistory +from .request_cache import RequestCache from .search_history import SearchHistory from .watch_later_list import WatchLaterList @@ -22,6 +23,7 @@ 'FeedHistory', 'FunctionCache', 'PlaybackHistory', + 'RequestCache', 'SearchHistory', 'WatchLaterList', ) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/bookmarks_list.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/bookmarks_list.py index c7ca563aac..d4b92c4479 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/bookmarks_list.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/bookmarks_list.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -38,5 +38,5 @@ def update_item(self, item_id, item, timestamp=None): def _optimize_item_count(self, limit=-1, defer=False): return False - def _optimize_file_size(self, limit=-1, defer=False): + def _optimize_file_size(self, defer=False, db=None): return False diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index 3c6f6a08fc..90eea0eb3a 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2019 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -18,6 +18,8 @@ class DataCache(Storage): _table_updated = False _sql = {} + _memory_store = {} + def __init__(self, filepath, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 super(DataCache, self).__init__(filepath, @@ -56,11 +58,11 @@ def get_item(self, content_id, seconds=None, as_dict=False): result = self._get(content_id, seconds=seconds, as_dict=as_dict) return result - def set_item(self, content_id, item): - self._set(content_id, item) + def set_item(self, content_id, item, defer=False, flush=False): + self._set(content_id, item, defer=defer, flush=flush) - def set_items(self, items): - self._set_many(items) + def set_items(self, items, defer=False, flush=False): + self._set_many(items, defer=defer, flush=flush) def del_item(self, content_id): self._remove(content_id) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/feed_history.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/feed_history.py index 0efbdd9ed5..0cddb7ddc1 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/feed_history.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/feed_history.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -17,6 +17,8 @@ class FeedHistory(Storage): _table_updated = False _sql = {} + _memory_store = {} + def __init__(self, filepath): super(FeedHistory, self).__init__(filepath) @@ -32,10 +34,10 @@ def get_item(self, content_id, seconds=None): return result def set_items(self, items): - self._set_many(items) + self._set_many(items, defer=True) def _optimize_item_count(self, limit=-1, defer=False): return False - def _optimize_file_size(self, limit=-1, defer=False): + def _optimize_file_size(self, defer=False, db=None): return False diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py index 0f1d23330d..bde57cdfc0 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2019 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,10 +11,10 @@ from __future__ import absolute_import, division, unicode_literals from functools import partial -from hashlib import md5 from itertools import chain from .storage import Storage +from ..utils.methods import generate_hash class FunctionCache(Storage): @@ -78,7 +78,7 @@ def _create_id_from_func(cls, partial_func, scope=SCOPE_ALL): partial_func.args, partial_func.keywords.items(), ) - return md5(','.join(map(str, signature)).encode('utf-8')).hexdigest() + return generate_hash(iter=signature) def get_result(self, func, *args, **kwargs): partial_func = partial(func, *args, **kwargs) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py index 48ed96542c..9020ff2884 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -25,10 +25,11 @@ def _add_last_played(value, item): value['last_played'] = fromtimestamp(item[1]) return value - def get_items(self, keys=None, limit=-1, process=None): + def get_items(self, keys=None, limit=-1, process=None, excluding=None): if process is None: process = self._add_last_played result = self._get_by_ids(keys, + excluding=excluding, oldest_first=False, process=process, as_dict=True, @@ -39,8 +40,8 @@ def get_item(self, key): result = self._get(key, process=self._add_last_played) return result - def set_item(self, video_id, play_data, timestamp=None): - self._set(video_id, play_data, timestamp) + def set_item(self, video_id, play_data): + self._set(video_id, play_data) def del_item(self, video_id): self._remove(video_id) @@ -51,5 +52,5 @@ def update_item(self, video_id, play_data, timestamp=None): def _optimize_item_count(self, limit=-1, defer=False): return False - def _optimize_file_size(self, limit=-1, defer=False): + def _optimize_file_size(self, defer=False, db=None): return False diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/request_cache.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/request_cache.py new file mode 100644 index 0000000000..2aa8cd201c --- /dev/null +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/request_cache.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2014-2016 bromix (plugin.video.youtube) + Copyright (C) 2016-2025 plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from __future__ import absolute_import, division, unicode_literals + +from .storage import Storage + + +class RequestCache(Storage): + _table_name = 'storage_v2' + _table_updated = False + _sql = {} + + _memory_store = {} + + def __init__(self, filepath, max_file_size_mb=20): + max_file_size_kb = max_file_size_mb * 1024 + super(RequestCache, self).__init__(filepath, + max_file_size_kb=max_file_size_kb) + + def get(self, request_id, seconds=None, as_dict=True, with_timestamp=True): + result = self._get(request_id, + seconds=seconds, + as_dict=as_dict, + with_timestamp=with_timestamp) + return result + + def set(self, request_id, response=None, etag=None, timestamp=None): + if response: + item = (etag, response) + if timestamp: + self._update(request_id, item, timestamp, defer=True) + else: + self._set(request_id, item, defer=True) + else: + self._refresh(request_id, timestamp, defer=True) + + def _optimize_item_count(self, limit=-1, defer=False): + return False diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/search_history.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/search_history.py index 46c0587433..b7ce4d7c31 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/search_history.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/search_history.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2019 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,9 +10,8 @@ from __future__ import absolute_import, division, unicode_literals -from hashlib import md5 - from .storage import Storage +from ..utils.methods import generate_hash class SearchHistory(Storage): @@ -31,22 +30,18 @@ def get_items(self, process=None): process=process) return result - @staticmethod - def _make_id(query): - return md5(query.encode('utf-8')).hexdigest() - def add_item(self, query): if isinstance(query, dict): params = query query = params['q'] else: params = {'q': query} - self._set(self._make_id(query), params) + self._set(generate_hash(query), params) def del_item(self, query): if isinstance(query, dict): query = query['q'] - self._remove(self._make_id(query)) + self._remove(generate_hash(query)) def update_item(self, query, timestamp=None): if isinstance(query, dict): @@ -54,4 +49,4 @@ def update_item(self, query, timestamp=None): query = params['q'] else: params = {'q': query} - self._update(self._make_id(query), params, timestamp) + self._update(generate_hash(query), params, timestamp) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 4428fe4d13..9485c98764 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2019 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,18 +11,71 @@ from __future__ import absolute_import, division, unicode_literals import os -import pickle import sqlite3 import time -from threading import Lock +from atexit import register as atexit_register +from threading import RLock, Timer + +from .. import logging +from ..compatibility import pickle, to_str +from ..utils.datetime import fromtimestamp, since_epoch +from ..utils.file_system import make_dirs +from ..utils.system_version import current_system_version + + +class StorageLock(object): + def __init__(self): + self._lock = RLock() + self._num_accessing = 0 + self._num_waiting = 0 + + if current_system_version.compatible(19): + def __enter__(self): + self._num_waiting += 1 + locked = not self._lock.acquire(timeout=3) + self._num_waiting -= 1 + return locked + else: + def __enter__(self): + self._num_waiting += 1 + locked = not self._lock.acquire(blocking=False) + self._num_waiting -= 1 + return locked + + def __exit__(self, exc_type, exc_val, exc_tb): + try: + self._lock.release() + except RuntimeError: + pass + + def accessing(self, start=False, done=False): + num = self._num_accessing + if start: + num += 1 + elif done and num > 0: + num -= 1 + self._num_accessing = num + return num > 0 + + def waiting(self): + return self._num_waiting > 0 + + +class ExistingDBConnection(object): + def __init__(self, db): + self._db = db + + def __enter__(self): + db = self._db + return db, db.cursor() if db else None -from ..compatibility import to_str -from ..logger import Logger -from ..utils.datetime_parser import fromtimestamp, since_epoch -from ..utils.methods import format_stack, make_dirs + def __exit__(self, *excinfo): + pass class Storage(object): + log = logging.getLogger(__name__) + ONE_MINUTE = 60 ONE_HOUR = 60 * ONE_MINUTE ONE_DAY = 24 * ONE_HOUR @@ -77,6 +130,12 @@ class Storage(object): ' ORDER BY {order_col} DESC' ' LIMIT {{0}};' ), + 'get_by_key_excluding': ( + 'SELECT *' + ' FROM {table}' + ' WHERE key in ({{0}})' + ' AND key not in ({{1}});' + ), 'get_many': ( 'SELECT *' ' FROM {table}' @@ -89,6 +148,17 @@ class Storage(object): ' ORDER BY {order_col} DESC' ' LIMIT {{0}};' ), + 'get_total_data_size': ( + 'SELECT SUM(size)' + 'FROM {table}' + ), + 'get_database_size': ( + 'SELECT page_size * page_count' + ' FROM (' + ' pragma_page_count(),' + ' pragma_page_size()' + ');' + ), 'has_old_table': ( 'SELECT EXISTS (' ' SELECT 1' @@ -127,6 +197,17 @@ class Storage(object): ' ) <= {{0}}' ' );' ), + 'prune_invalid': ( + 'DELETE' + ' FROM {table}' + ' WHERE key IS NULL;' + ), + 'refresh': ( + 'UPDATE' + ' {table}' + ' SET timestamp = ?' + ' WHERE key = ?;' + ), 'remove': ( 'DELETE' ' FROM {table}' @@ -165,10 +246,13 @@ def __init__(self, self.uuid = filepath[1] self._filepath = os.path.join(*filepath) self._db = None - self._cursor = None - self._lock = Lock() + self._lock = StorageLock() + self._memory_store = getattr(self.__class__, '_memory_store', None) + self._close_timer = None + self._close_actions = False self._max_item_count = -1 if migrate else max_item_count self._max_file_size_kb = -1 if migrate else max_file_size_kb + atexit_register(self._close, event='shutdown') if migrate: self._base = self @@ -206,148 +290,263 @@ def set_max_item_count(self, max_item_count): def set_max_file_size_kb(self, max_file_size_kb): self._max_file_size_kb = max_file_size_kb + def __del__(self): + self._close(event='deleted') + def __enter__(self): - self._lock.acquire() - if not self._db or not self._cursor: - return self._open() - return self._db, self._cursor + self._lock.accessing(start=True) + + close_timer = self._close_timer + if close_timer: + close_timer.cancel() + + db = self._db or self._open() + try: + cursor = db.cursor() + except (AttributeError, sqlite3.ProgrammingError): + db = self._open() + cursor = db.cursor() + cursor.arraysize = 100 + return db, cursor def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): - self._close() - self._lock.release() + close_timer = self._close_timer + if close_timer: + close_timer.cancel() + + if self._lock.accessing(done=True) or self._lock.waiting(): + return + + with self._lock as locked: + if locked or self._close_timer: + return + close_timer = Timer(5, self._close) + close_timer.start() + self._close_timer = close_timer def _open(self): - statements = [] + table_queries = [] if not os.path.exists(self._filepath): make_dirs(os.path.dirname(self._filepath)) - statements.append( - self._sql['create_table'] - ) + table_queries.extend(( + self._sql['create_table'], + )) self._base._table_updated = True - for _ in range(3): + for attempt in range(1, 4): try: db = sqlite3.connect(self._filepath, + cached_statements=0, check_same_thread=False, isolation_level=None) break except (sqlite3.Error, sqlite3.OperationalError) as exc: - msg = ('SQLStorage._open - Error' - '\n\tException: {exc!r}' - '\n\tStack trace (most recent call last):\n{stack}' - .format(exc=exc, - stack=format_stack())) - if isinstance(exc, sqlite3.OperationalError): - Logger.log_warning(msg) + if attempt < 3 and isinstance(exc, sqlite3.OperationalError): + self.log.warning('Attempt %d of 3', + attempt, + exc_info=True) time.sleep(0.1) else: - Logger.log_error(msg) - return None, None - + self.log.exception('Failed') + return None else: - return None, None + return None cursor = db.cursor() - cursor.arraysize = 100 - sql_script = [ + queries = [ 'PRAGMA busy_timeout = 1000;', 'PRAGMA read_uncommitted = TRUE;', 'PRAGMA secure_delete = FALSE;', - 'PRAGMA synchronous = OFF;', - 'PRAGMA locking_mode = EXCLUSIVE;' + # 'PRAGMA synchronous = OFF;', + 'PRAGMA synchronous = NORMAL;', + # 'PRAGMA locking_mode = EXCLUSIVE;' 'PRAGMA temp_store = MEMORY;', - 'PRAGMA mmap_size = 4096000;', + 'PRAGMA mmap_size = -1;', 'PRAGMA page_size = 4096;', - 'PRAGMA cache_size = 1000;', - 'PRAGMA journal_mode = PERSIST;', + 'PRAGMA cache_size = -2000;', + # 'PRAGMA journal_mode = TRUNCATE;', + # 'PRAGMA journal_mode = PERSIST;', + # 'PRAGMA journal_mode = MEMORY;', + 'PRAGMA journal_mode = WAL;', ] if not self._table_updated: for result in self._execute(cursor, self._sql['has_old_table']): if result[0] == 1: - statements.extend(( + table_queries.extend(( 'PRAGMA writable_schema = 1;', self._sql['drop_old_table'], 'PRAGMA writable_schema = 0;', )) break - if statements: - transaction_begin = len(sql_script) + 1 - sql_script.extend(('BEGIN;', 'COMMIT;', 'VACUUM;')) - sql_script[transaction_begin:transaction_begin] = statements - self._execute(cursor, '\n'.join(sql_script), script=True) + if table_queries: + transaction_begin = len(queries) + 1 + queries.extend(('BEGIN IMMEDIATE;', 'COMMIT;', 'VACUUM;')) + queries[transaction_begin:transaction_begin] = table_queries + self._execute(cursor, queries) self._base._table_updated = True self._db = db - self._cursor = cursor - return db, cursor + return db - def _close(self): - if self._cursor: - self._execute(self._cursor, 'PRAGMA optimize') - self._cursor.close() - self._cursor = None - if self._db: - # Not needed if using self._db as a context manager - # self._db.commit() - self._db.close() + def _close(self, commit=False, event=None): + close_timer = self._close_timer + if close_timer: + close_timer.cancel() + + if self._lock.accessing() or self._lock.waiting(): + return False + + db = self._db + if not db: + if self._close_actions: + db = self._open() + else: + return None + + if event or self._close_actions: + if not event: + queries = ( + 'BEGIN IMMEDIATE;', + self._set_many(items=None, defer=True, flush=True), + 'COMMIT;', + 'BEGIN IMMEDIATE;', + self._optimize_item_count(defer=True), + self._optimize_file_size(defer=True, db=db), + 'COMMIT;', + 'VACUUM;', + ) + elif self._close_actions: + queries = ( + 'BEGIN IMMEDIATE;', + self._set_many(items=None, defer=True, flush=True), + 'COMMIT;', + 'BEGIN IMMEDIATE;', + self._sql['prune_invalid'], + self._optimize_item_count(defer=True), + self._optimize_file_size(defer=True, db=db), + 'COMMIT;', + 'VACUUM;', + 'PRAGMA optimize;', + ) + else: + queries = ( + 'BEGIN IMMEDIATE;', + self._sql['prune_invalid'], + 'COMMIT;', + 'VACUUM;', + 'PRAGMA optimize;', + ) + self._execute(db.cursor(), queries) + + # Not needed if using db as a context manager + if commit: + db.commit() + + if event: + db.close() self._db = None + self._close_actions = False + self._close_timer = None + return True - @staticmethod - def _execute(cursor, query, values=None, many=False, script=False): + def _execute(self, cursor, queries, values=(), many=False, script=False): + result = [] if not cursor: - Logger.log_error('SQLStorage._execute - Database not available') - return [] - if values is None: - values = () - """ - Tests revealed that sqlite has problems to release the database in time - This happens no so often, but just to be sure, we try at least 3 times - to execute our statement. - """ - for _ in range(3): - try: - if many: - return cursor.executemany(query, values) - if script: - return cursor.executescript(query) - return cursor.execute(query, values) - except (sqlite3.Error, sqlite3.OperationalError) as exc: - msg = ('SQLStorage._execute - Error' - '\n\tException: {exc!r}' - '\n\tStack trace (most recent call last):\n{stack}' - .format(exc=exc, - stack=format_stack())) - if isinstance(exc, sqlite3.OperationalError): - Logger.log_warning(msg) - time.sleep(0.1) - else: - Logger.log_error(msg) - return [] - return [] + self.log.error_trace('Database not available') + return result + + if isinstance(queries, (list, tuple)): + if script: + queries = ('\n'.join(queries),) + else: + queries = (queries,) + + for query in queries: + if not query: + continue + if isinstance(query, tuple): + query, _values, _many = query + else: + _many = many + _values = values + + # Retry DB operation 3 times in case DB is locked or busy + abort = False + for attempt in range(1, 4): + try: + if _many: + result = cursor.executemany(query, _values) + elif script: + result = cursor.executescript(query) + else: + result = cursor.execute(query, _values) + break + except (sqlite3.Error, sqlite3.OperationalError) as exc: + if attempt >= 3: + abort = True + elif isinstance(exc, sqlite3.OperationalError): + time.sleep(0.1) + elif isinstance(exc, sqlite3.InterfaceError): + cursor = self._db.cursor() + else: + abort = True + if abort: + self.log.exception(('Failed', + 'Query: {query!r}', + 'Values: {values!r}'), + attempt=attempt, + query=query, + values=values) + break + self.log.warning_trace(('Attempt {attempt} of 3', + 'Query: {query!r}', + 'Values: {values!r}'), + attempt=attempt, + query=query, + values=values, + exc_info=True) + if abort: + break + return result - def _optimize_file_size(self, defer=False): + def _optimize_file_size(self, defer=False, db=None): # do nothing - optimize only if max size limit has been set if self._max_file_size_kb <= 0: return False - try: - file_size_kb = (os.path.getsize(self._filepath) // 1024) - if file_size_kb <= self._max_file_size_kb: + with ExistingDBConnection(db) if db else self as (db, cursor): + result = self._execute(cursor, self._sql['get_total_data_size']) + result = result.fetchone() if result else None + result = result[0] if result else None + if result is not None: + size_kb = result // 1024 + else: + try: + size_kb = (os.path.getsize(self._filepath) // 1024) + except OSError: return False - except OSError: + + if size_kb <= self._max_file_size_kb: return False - prune_size = 1024 * int(file_size_kb - self._max_file_size_kb / 2) + prune_size = 1024 * int(size_kb - self._max_file_size_kb / 2) query = self._sql['prune_by_size'].format(prune_size) if defer: return query - with self as (db, cursor), db: - self._execute(cursor, query) - self._execute(cursor, 'VACUUM') - return True + with self as (db, cursor): + self._execute( + cursor, + ( + 'BEGIN IMMEDIATE;', + query, + 'COMMIT;', + 'VACUUM;', + ), + ) + return None def _optimize_item_count(self, limit=-1, defer=False): # do nothing - optimize only if max item limit has been set @@ -365,60 +564,204 @@ def _optimize_item_count(self, limit=-1, defer=False): ) if defer: return query + with self as (db, cursor): + self._execute( + cursor, + ( + 'BEGIN IMMEDIATE;', + query, + 'COMMIT;', + 'VACUUM;', + ), + ) + return None + + def _set(self, item_id, item, defer=False, flush=False): + memory_store = self._memory_store + if memory_store is not None: + key = to_str(item_id) + if defer: + memory_store[key] = ( + item_id, + since_epoch(), + item, + ) + self._close_actions = True + return None + if flush: + memory_store.clear() + return False + if memory_store: + memory_store[key] = ( + item_id, + since_epoch(), + item, + ) + return self._set_many(items=None) + with self as (db, cursor), db: - self._execute(cursor, query) - self._execute(cursor, 'VACUUM') + self._execute( + cursor, + self._sql['set'], + self._encode(item_id, item), + ) return True - def _set(self, item_id, item, timestamp=None): - values = self._encode(item_id, item, timestamp) - optimize_query = self._optimize_item_count(1, defer=True) - with self as (db, cursor), db: - if optimize_query: - self._execute(cursor, 'BEGIN') - self._execute(cursor, optimize_query) - self._execute(cursor, self._sql['set'], values=values) + def _set_many(self, items, flatten=False, defer=False, flush=False): + memory_store = self._memory_store + if memory_store is not None: + if defer and not flush: + now = since_epoch() + memory_store.update({ + to_str(item_id): ( + item_id, + now, + item, + ) + for item_id, item in items.items() + }) + self._close_actions = True + return None + if flush and not defer: + memory_store.clear() + return False + if memory_store: + flush = True - def _set_many(self, items, flatten=False): now = since_epoch() - num_items = len(items) + values = [] if flatten: - values = [enc_part - for item in items.items() - for enc_part in self._encode(*item, timestamp=now)] + num_item = 0 + if items: + values.extend([ + part + for item_id, item in items.items() + for part in self._encode(item_id, item, now) + ]) + num_item += len(items) + if memory_store: + values.extend([ + part + for item_id, timestamp, item in memory_store.values() + for part in self._encode(item_id, item, timestamp) + ]) + num_item += len(memory_store) query = self._sql['set_flat'].format( - '(?,?,?,?),' * (num_items - 1) + '(?,?,?,?)' + '(?,?,?,?),' * (num_item - 1) + '(?,?,?,?)' ) + many = False else: - values = [self._encode(*item, timestamp=now) - for item in items.items()] + if items: + values.extend([ + self._encode(item_id, item, now) + for item_id, item in items.items() + ]) + if memory_store: + values.extend([ + self._encode(item_id, item, timestamp) + for item_id, timestamp, item in memory_store.values() + ]) query = self._sql['set'] + many = True + + if flush and memory_store: + memory_store.clear() + + if values: + if defer: + return query, values, many + + with self as (db, cursor): + self._execute( + cursor, + ( + 'BEGIN IMMEDIATE;', + (query, values, many), + 'COMMIT;', + ), + ) + self._close_actions = True + return None - optimize_query = self._optimize_item_count(num_items, defer=True) - with self as (db, cursor), db: - self._execute(cursor, 'BEGIN') - if optimize_query: - self._execute(cursor, optimize_query) - self._execute(cursor, query, many=(not flatten), values=values) - self._optimize_file_size() + def _refresh(self, item_id, timestamp=None, defer=False): + key = to_str(item_id) + if not timestamp: + timestamp = since_epoch() + + memory_store = self._memory_store + if memory_store and key in memory_store: + if defer: + item = memory_store[key] + memory_store[key] = ( + item_id, + timestamp, + item[2], + ) + self._close_actions = True + return None + del memory_store[key] + + values = (timestamp, key) + with self as (db, cursor): + self._execute( + cursor, + ( + 'BEGIN IMMEDIATE;', + (self._sql['refresh'], values, False), + 'COMMIT;', + ), + ) + return True + + def _update(self, item_id, item, timestamp=None, defer=False): + key = to_str(item_id) + if not timestamp: + timestamp = since_epoch() + + memory_store = self._memory_store + if memory_store and key in memory_store: + if defer: + memory_store[key] = ( + item_id, + timestamp, + item, + ) + self._close_actions = True + return None + del memory_store[key] - def _update(self, item_id, item, timestamp=None): values = self._encode(item_id, item, timestamp, for_update=True) - with self as (db, cursor), db: - self._execute(cursor, self._sql['update'], values=values) + with self as (db, cursor): + self._execute( + cursor, + ( + 'BEGIN IMMEDIATE;', + (self._sql['update'], values, False), + 'COMMIT;', + ), + ) + return True def clear(self, defer=False): + memory_store = self._memory_store + if memory_store: + memory_store.clear() + query = self._sql['clear'] if defer: return query + with self as (db, cursor), db: - self._execute(cursor, query) - self._execute(cursor, 'VACUUM') - return True + self._execute( + cursor, + query, + ) + self._close_actions = True + return None def is_empty(self): - with self as (db, cursor), db: + with self as (db, cursor): result = self._execute(cursor, self._sql['is_empty']) for item in result: is_empty = item[0] == 0 @@ -429,7 +772,10 @@ def is_empty(self): @staticmethod def _decode(obj, process=None, item=None): - decoded_obj = pickle.loads(obj) + if item and item[3] is None: + decoded_obj = obj + else: + decoded_obj = pickle.loads(obj) if process: return process(decoded_obj, item) return decoded_obj @@ -438,7 +784,7 @@ def _decode(obj, process=None, item=None): def _encode(key, obj, timestamp=None, for_update=False): timestamp = timestamp or since_epoch() blob = sqlite3.Binary(pickle.dumps( - obj, protocol=pickle.HIGHEST_PROTOCOL + obj, protocol=-1 )) size = getattr(blob, 'nbytes', None) if not size: @@ -449,26 +795,53 @@ def _encode(key, obj, timestamp=None, for_update=False): return to_str(key), timestamp, blob, size return timestamp, blob, size - def _get(self, item_id, process=None, seconds=None, as_dict=False): - with self as (db, cursor), db: - result = self._execute(cursor, self._sql['get'], [to_str(item_id)]) - item = result.fetchone() if result else None - if not item: - return None + def _get(self, + item_id, + process=None, + seconds=None, + as_dict=False, + with_timestamp=False): + key = to_str(item_id) + memory_store = self._memory_store + if memory_store and key in memory_store: + item = memory_store[key] + item = ( + item_id, + item[1], # timestamp from memory store item + item[2], # object from memory store item + None, + ) + else: + with self as (db, cursor): + result = self._execute( + cursor, + self._sql['get'], + (key,), + ) + item = result.fetchone() if result else None + if not item or not all(item): + return None + cut_off = since_epoch() - seconds if seconds else 0 if not cut_off or item[1] >= cut_off: if as_dict: - return { + output = { 'item_id': item_id, 'age': since_epoch() - item[1], 'value': self._decode(item[2], process, item), } + if with_timestamp: + output['timestamp'] = item[1] + return output return self._decode(item[2], process, item) return None - def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, + def _get_by_ids(self, item_ids=(), oldest_first=True, limit=-1, wildcard=False, seconds=None, process=None, - as_dict=False, values_only=True): + as_dict=False, values_only=True, excluding=None): + in_memory_result = None + result = None + if not item_ids: if oldest_first: query = self._sql['get_many'] @@ -482,49 +855,121 @@ def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, query = self._sql['get_by_key_like_desc'] query = query.format(limit) else: - num_ids = len(item_ids) - query = self._sql['get_by_key'].format('?,' * (num_ids - 1) + '?') - item_ids = tuple(item_ids) - - epoch = since_epoch() - cut_off = epoch - seconds if seconds else 0 - with self as (db, cursor), db: - result = self._execute(cursor, query, item_ids) - if as_dict: - if values_only: - result = { - item[0]: self._decode(item[2], process, item) - for item in result if not cut_off or item[1] >= cut_off - } + if excluding: + query = self._sql['get_by_key_excluding'].format( + '?,' * (len(item_ids) - 1) + '?', + '?,' * (len(excluding) - 1) + '?', + ) + item_ids = tuple(item_ids) + tuple(excluding) + else: + memory_store = self._memory_store + if memory_store: + in_memory_result = [] + _item_ids = [] + for item_id in item_ids: + key = to_str(item_id) + if key in memory_store: + item = memory_store[key] + in_memory_result.append(( + item_id, + item[1], # timestamp from memory store item + item[2], # object from memory store item + None, + )) + else: + _item_ids.append(item_id) + item_ids = _item_ids + + if item_ids: + query = self._sql['get_by_key'].format( + '?,' * (len(item_ids) - 1) + '?' + ) + item_ids = tuple(map(to_str, item_ids)) else: - result = { - item[0]: { - 'age': epoch - item[1], - 'value': self._decode(item[2], process, item), - } - for item in result if not cut_off or item[1] >= cut_off - } - elif values_only: - result = [ - self._decode(item[2], process, item) + query = None + + if query: + with self as (db, cursor): + result = self._execute(cursor, query, item_ids) + if result: + result = result.fetchall() + + if in_memory_result: + if result: + in_memory_result.extend(result) + result = in_memory_result + + now = since_epoch() + cut_off = now - seconds if seconds else 0 + + if as_dict: + if values_only: + result = { + item[0]: self._decode(item[2], process, item) for item in result if not cut_off or item[1] >= cut_off - ] + } else: - result = [ - (item[0], - fromtimestamp(item[1]), - self._decode(item[2], process, item)) + result = { + item[0]: { + 'age': now - item[1], + 'value': self._decode(item[2], process, item), + } for item in result if not cut_off or item[1] >= cut_off - ] + } + elif values_only: + result = [ + self._decode(item[2], process, item) + for item in result if not cut_off or item[1] >= cut_off + ] + else: + result = [ + (item[0], + fromtimestamp(item[1]), + self._decode(item[2], process, item)) + for item in result if not cut_off or item[1] >= cut_off + ] return result def _remove(self, item_id): - with self as (db, cursor), db: - self._execute(cursor, self._sql['remove'], [item_id]) + key = to_str(item_id) + memory_store = self._memory_store + if memory_store and key in memory_store: + del memory_store[key] + + with self as (db, cursor): + self._execute( + cursor, + ( + 'BEGIN IMMEDIATE;', + (self._sql['remove'], (key,), False), + 'COMMIT;', + ), + ) + self._close_actions = True + return True def _remove_many(self, item_ids): + memory_store = self._memory_store + if memory_store: + _item_ids = [] + for item_id in item_ids: + key = to_str(item_id) + if key in memory_store: + del memory_store[key] + else: + _item_ids.append(item_id) + item_ids = _item_ids + num_ids = len(item_ids) query = self._sql['remove_by_key'].format('?,' * (num_ids - 1) + '?') - with self as (db, cursor), db: - self._execute(cursor, query, tuple(item_ids)) - self._execute(cursor, 'VACUUM') + with self as (db, cursor): + self._execute( + cursor, + ( + 'BEGIN IMMEDIATE;', + (query, tuple(map(to_str, item_ids)), False), + 'COMMIT;', + ), + ) + self._close_actions = True + return True diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py index b876ce2f1a..7c9de95c8a 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/__init__.py index 003c27de85..475e51221c 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py index 2b5019a731..5ef5e3e2da 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -22,16 +22,20 @@ def create_progress_dialog(self, message_template=None): raise NotImplementedError() - def on_keyboard_input(self, title, default='', hidden=False): + @staticmethod + def on_keyboard_input(title, default='', hidden=False): raise NotImplementedError() - def on_numeric_input(self, title, default=''): + @staticmethod + def on_numeric_input(title, default=''): raise NotImplementedError() - def on_yes_no_input(self, title, text, nolabel='', yeslabel=''): + @staticmethod + def on_yes_no_input(title, text, nolabel='', yeslabel=''): raise NotImplementedError() - def on_ok(self, title, text): + @staticmethod + def on_ok(title, text): raise NotImplementedError() def on_remove_content(self, name): @@ -43,21 +47,187 @@ def on_delete_content(self, name): def on_clear_content(self, name): raise NotImplementedError() - def on_select(self, title, items=None, preselect=-1, use_details=False): + @staticmethod + def on_select(title, items=None, preselect=-1, use_details=False): raise NotImplementedError() def show_notification(self, message, header='', image_uri='', time_ms=5000, audible=True): raise NotImplementedError() - def on_busy(self): + @staticmethod + 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. :return: """ raise NotImplementedError() + + def focus_container(self, container_id=None, position=None): + raise NotImplementedError() + + @staticmethod + def get_infobool(name): + raise NotImplementedError() + + @staticmethod + def get_infolabel(name): + raise NotImplementedError() + + def get_container(self, + container_type=True, + check_ready=False, + stacklevel=None): + raise NotImplementedError() + + @classmethod + def get_container_id(cls, container_type=True): + raise NotImplementedError() + + @classmethod + def get_container_bool(cls, + name, + container_id=True, + strict=True, + stacklevel=None): + raise NotImplementedError() + + @classmethod + def get_container_info(cls, + name, + container_id=True, + strict=True, + stacklevel=None): + raise NotImplementedError() + + @classmethod + def get_listitem_bool(cls, + name, + container_id=True, + strict=True, + stacklevel=None): + raise NotImplementedError() + + @classmethod + def get_listitem_info(cls, + name, + container_id=True, + strict=True, + stacklevel=None): + raise NotImplementedError() + + @classmethod + def get_listitem_property(cls, + name, + container_id=True, + strict=True, + stacklevel=None): + raise NotImplementedError() + + @classmethod + def set_property(cls, + property_id, + value='true', + stacklevel=2, + process=None, + log_value=None, + log_process=None, + raw=False): + raise NotImplementedError() + + @classmethod + def get_property(cls, + property_id, + stacklevel=2, + process=None, + log_value=None, + log_process=None, + raw=False, + as_bool=False, + default=False): + raise NotImplementedError() + + @classmethod + def pop_property(cls, + property_id, + stacklevel=2, + process=None, + log_value=None, + log_process=None, + raw=False, + as_bool=False, + default=False): + raise NotImplementedError() + + @classmethod + def clear_property(cls, property_id, stacklevel=2, raw=False): + raise NotImplementedError() + + @staticmethod + def bold(value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[B]', value, '[/B]', + '[CR]' * cr_after, + )) + + @staticmethod + def uppercase(value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[UPPERCASE]', value, '[/UPPERCASE]', + '[CR]' * cr_after, + )) + + @staticmethod + def color(color, value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[COLOR=', color.lower(), ']', value, '[/COLOR]', + '[CR]' * cr_after, + )) + + @staticmethod + def light(value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[LIGHT]', value, '[/LIGHT]', + '[CR]' * cr_after, + )) + + @staticmethod + def italic(value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[I]', value, '[/I]', + '[CR]' * cr_after, + )) + + @staticmethod + def indent(number=1, value='', cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[TABS]', str(number), '[/TABS]', value, + '[CR]' * cr_after, + )) + + @staticmethod + def new_line(value=1, cr_before=0, cr_after=0): + if isinstance(value, int): + return '[CR]' * value + return ''.join(( + '[CR]' * cr_before, + value, + '[CR]' * cr_after, + )) + + def set_focus_next_item(self): + raise NotImplementedError() + + @staticmethod + def busy_dialog_active(): + raise NotImplementedError() diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index 39ab344521..fd279c210b 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -13,12 +13,37 @@ from weakref import proxy from ..abstract_context_ui import AbstractContextUI +from ... import logging from ...compatibility import string_type, xbmc, xbmcgui -from ...constants import ADDON_ID, REFRESH_CONTAINER -from ...utils import to_unicode +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, + HIDE_PROGRESS, + LISTITEM_INFO, + LISTITEM_PROP, + PLUGIN_CONTAINER_INFO, + PROPERTY, + REFRESH_CONTAINER, + UPDATING, + URI, +) +from ...utils.convert_format import to_unicode class XbmcContextUI(AbstractContextUI): + log = logging.getLogger(__name__) + def __init__(self, context): super(XbmcContextUI, self).__init__() self._context = context @@ -29,7 +54,8 @@ def create_progress_dialog(self, total=None, background=False, message_template=None, - template_params=None): + template_params=None, + hide_progress=None): if not message_template and background: message_template = '{_message} {_current}/{_total}' @@ -44,10 +70,15 @@ def create_progress_dialog(self, total=int(total) if total is not None else 0, message_template=message_template, template_params=template_params, - hide=self._context.get_param('hide_progress'), + hide=( + self._context.get_param(HIDE_PROGRESS) + if hide_progress is None else + hide_progress + ), ) - def on_keyboard_input(self, title, default='', hidden=False): + @staticmethod + def on_keyboard_input(title, default='', hidden=False): # Starting with Gotham (13.X > ...) dialog = xbmcgui.Dialog() result = dialog.input(title, @@ -59,7 +90,8 @@ def on_keyboard_input(self, title, default='', hidden=False): return False, '' - def on_numeric_input(self, title, default=''): + @staticmethod + def on_numeric_input(title, default=''): dialog = xbmcgui.Dialog() result = dialog.input(title, str(default), type=xbmcgui.INPUT_NUMERIC) if result: @@ -67,33 +99,36 @@ def on_numeric_input(self, title, default=''): return False, None - def on_yes_no_input(self, title, text, nolabel='', yeslabel=''): + @staticmethod + def on_yes_no_input(title, text, nolabel='', yeslabel=''): dialog = xbmcgui.Dialog() return dialog.yesno(title, text, nolabel=nolabel, yeslabel=yeslabel) - def on_ok(self, title, text): + @staticmethod + def on_ok(title, text): dialog = xbmcgui.Dialog() return dialog.ok(title, text) def on_remove_content(self, name): return self.on_yes_no_input( self._context.localize('content.remove'), - self._context.localize('content.remove.check') % to_unicode(name), + self._context.localize('content.remove.check.x', to_unicode(name)), ) def on_delete_content(self, name): return self.on_yes_no_input( self._context.localize('content.delete'), - self._context.localize('content.delete.check') % to_unicode(name), + self._context.localize('content.delete.check.x', to_unicode(name)), ) def on_clear_content(self, name): return self.on_yes_no_input( self._context.localize('content.clear'), - self._context.localize('content.clear.check') % to_unicode(name), + self._context.localize('content.clear.check.x', to_unicode(name)), ) - def on_select(self, title, items=None, preselect=-1, use_details=False): + @staticmethod + def on_select(title, items=None, preselect=-1, use_details=False): if isinstance(items, (list, tuple)): items = enumerate(items) elif isinstance(items, dict): @@ -155,114 +190,423 @@ def show_notification(self, time_ms, audible) - def on_busy(self): + @staticmethod + 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 - def set_property(self, property_id, value='true'): - self._context.log_debug('Set property |{id}|: {value!r}' - .format(id=property_id, value=value)) - _property_id = '-'.join((ADDON_ID, property_id)) - xbmcgui.Window(10000).setProperty(_property_id, value) - return value + stacklevel = 2 if stacklevel is None else stacklevel + 1 - def get_property(self, property_id): - _property_id = '-'.join((ADDON_ID, property_id)) - value = xbmcgui.Window(10000).getProperty(_property_id) - self._context.log_debug('Get property |{id}|: {value!r}' - .format(id=property_id, value=value)) - return value + 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 - def pop_property(self, property_id): - _property_id = '-'.join((ADDON_ID, property_id)) - window = xbmcgui.Window(10000) - value = window.getProperty(_property_id) - if value: - window.clearProperty(_property_id) - self._context.log_debug('Pop property |{id}|: {value!r}' - .format(id=property_id, value=value)) - return value + if container['is_active']: + self.set_property(REFRESH_CONTAINER) + xbmc.executebuiltin('Container.Refresh') + return True - def clear_property(self, property_id): - self._context.log_debug('Clear property |{id}|'.format(id=property_id)) - _property_id = '-'.join((ADDON_ID, property_id)) - xbmcgui.Window(10000).clearProperty(_property_id) + self.set_property(REFRESH_CONTAINER, BUSY_FLAG) + self.log.debug('Plugin container not active - deferring refresh', + stacklevel=stacklevel) return None - @staticmethod - def bold(value, cr_before=0, cr_after=0): - return ''.join(( - '[CR]' * cr_before, - '[B]', value, '[/B]', - '[CR]' * cr_after, - )) + def focus_container(self, container_id=None, position=None): + if position is None: + return - @staticmethod - def uppercase(value, cr_before=0, cr_after=0): - return ''.join(( - '[CR]' * cr_before, - '[UPPERCASE]', value, '[/UPPERCASE]', - '[CR]' * cr_after, - )) + container = self.get_container() + if not all(container.values()): + return - @staticmethod - def color(color, value, cr_before=0, cr_after=0): - return ''.join(( - '[CR]' * cr_before, - '[COLOR=', color.lower(), ']', value, '[/COLOR]', - '[CR]' * cr_after, - )) + if container_id is None: + container_id = container['id'] + elif not container_id: + return - @staticmethod - def light(value, cr_before=0, cr_after=0): - return ''.join(( - '[CR]' * cr_before, - '[LIGHT]', value, '[/LIGHT]', - '[CR]' * cr_after, - )) + if not isinstance(container_id, int): + try: + container_id = int(container_id) + except (TypeError, ValueError): + return - @staticmethod - def italic(value, cr_before=0, cr_after=0): - return ''.join(( - '[CR]' * cr_before, - '[I]', value, '[/I]', - '[CR]' * cr_after, - )) + 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 - @staticmethod - def indent(number=1, value='', cr_before=0, cr_after=0): - return ''.join(( - '[CR]' * cr_before, - '[TABS]', str(number), '[/TABS]', value, - '[CR]' * cr_after, + xbmc.executebuiltin('SetFocus({0},{1},absolute)'.format( + container_id, + position + offset, )) @staticmethod - def new_line(value=1, cr_before=0, cr_after=0): - if isinstance(value, int): - return '[CR]' * value - return ''.join(( - '[CR]' * cr_before, - value, - '[CR]' * cr_after, - )) + def get_infobool(name, _bool=xbmc.getCondVisibility): + return _bool(name) @staticmethod - def set_focus_next_item(): - container = xbmc.getInfoLabel('System.CurrentControlId') - position = xbmc.getInfoLabel('Container.CurrentItem') - try: - position = int(position) + 1 - except ValueError: - return - xbmc.executebuiltin( - 'SetFocus({container},{position},absolute)'.format( - container=container, - position=position + def get_infolabel(name, _label=xbmc.getInfoLabel): + return _label(name) + + def get_container(self, + container_type=True, + check_ready=False, + stacklevel=None, + _url='plugin://{0}/'.format(ADDON_ID)): + stacklevel = 2 if stacklevel is None else stacklevel + 1 + container_id = self.get_container_id(container_type) + _container_id = container_id if container_type else container_type + is_plugin = self.get_listitem_info( + URI, + _container_id, + stacklevel=stacklevel, + ).startswith(_url) + if check_ready and container_type is True and not is_plugin: + is_active = False + is_loaded = False + else: + is_active = not self.busy_dialog_active(all_modals=True) + is_loaded = ( + not self.get_container_bool( + UPDATING, + _container_id, + stacklevel=stacklevel, + ) + and ( + self.get_container_bool( + HAS_FOLDERS, + _container_id, + stacklevel=stacklevel + ) + or + self.get_container_bool( + HAS_FILES, + _container_id, + stacklevel=stacklevel + ) + or + self.get_container_bool( + HAS_PARENT, + _container_id, + stacklevel=stacklevel, + ) + ) ) - ) + + if check_ready: + return is_active and is_loaded + return { + 'is_plugin': is_plugin, + 'id': container_id, + 'is_loaded': is_active, + 'is_active': is_loaded, + } + + @classmethod + def get_container_id(cls, + container_type=True, + _label=xbmc.getInfoLabel): + if container_type is True: + return _label(PROPERTY % CONTAINER_ID) + if container_type is None: + return None + return _label('System.CurrentControlID') + + @classmethod + def get_container_bool(cls, + name, + container_id=True, + strict=True, + stacklevel=None, + _bool=xbmc.getCondVisibility, + _label=xbmc.getInfoLabel): + if container_id is True: + container_id = _label(PROPERTY % CONTAINER_ID) + elif container_id is None: + strict = False + elif container_id is False: + container_id = _label('System.CurrentControlID') + strict = False + + stacklevel = 2 if stacklevel is None else stacklevel + 1 + + if container_id: + out = _bool(PLUGIN_CONTAINER_INFO % (container_id, name)) + log_msg = 'Container {container_id} used for {name!r}: {out!r}' + elif strict: + out = False + log_msg = None + cls.log.warning('Plugin container not found for %r', name, + stacklevel=stacklevel) + else: + out = _bool(CURRENT_CONTAINER_INFO % name) + log_msg = 'Current container used for {name!r}: {out!r}' + if log_msg and cls.log.verbose_logging: + cls.log.debug(log_msg, + container_id=container_id, + name=name, + out=out, + stacklevel=stacklevel) + return out + + @classmethod + def get_container_info(cls, + name, + container_id=True, + strict=True, + stacklevel=None, + _label=xbmc.getInfoLabel): + if container_id is True: + container_id = _label(PROPERTY % CONTAINER_ID) + elif container_id is None: + strict = False + elif container_id is False: + container_id = _label('System.CurrentControlID') + strict = False + + stacklevel = 2 if stacklevel is None else stacklevel + 1 + + if container_id: + out = _label(PLUGIN_CONTAINER_INFO % (container_id, name)) + log_msg = 'Container {container_id} used for {name!r}: {out!r}' + elif strict: + out = False + log_msg = None + cls.log.warning('Plugin container not found for %r', name, + stacklevel=stacklevel) + else: + out = _label(CURRENT_CONTAINER_INFO % name) + log_msg = 'Current container used for {name!r}: {out!r}' + if log_msg and cls.log.verbose_logging: + cls.log.debug(log_msg, + container_id=container_id, + name=name, + out=out, + stacklevel=stacklevel) + return out + + @classmethod + def get_listitem_bool(cls, + name, + container_id=True, + strict=True, + stacklevel=None, + _bool=xbmc.getCondVisibility, + _label=xbmc.getInfoLabel): + if container_id is True: + container_id = _label(PROPERTY % CONTAINER_ID) + elif container_id is None: + strict = False + elif container_id is False: + container_id = _label('System.CurrentControlID') + strict = False + + stacklevel = 2 if stacklevel is None else stacklevel + 1 + + if container_id: + out = _bool(CONTAINER_LISTITEM_INFO % (container_id, name)) + log_msg = 'Container {container_id} used for {name!r}: {out!r}' + elif strict: + out = False + log_msg = None + cls.log.warning('Plugin container not found for %r', name, + stacklevel=stacklevel) + else: + out = _bool(LISTITEM_INFO % name) + log_msg = 'Current container used for {name!r}: {out!r}' + if log_msg and cls.log.verbose_logging: + cls.log.debug(log_msg, + container_id=container_id, + name=name, + out=out, + stacklevel=stacklevel) + return out + + @classmethod + def get_listitem_info(cls, + name, + container_id=True, + strict=True, + stacklevel=None, + _label=xbmc.getInfoLabel): + if container_id is True: + container_id = _label(PROPERTY % CONTAINER_ID) + elif container_id is None: + strict = False + elif container_id is False: + container_id = _label('System.CurrentControlID') + strict = False + + stacklevel = 2 if stacklevel is None else stacklevel + 1 + + if container_id: + out = _label(CONTAINER_LISTITEM_INFO % (container_id, name)) + log_msg = 'Container {container_id} used for {name!r}: {out!r}' + elif strict: + out = '' + log_msg = None + cls.log.warning('Plugin container not found for %r', name, + stacklevel=stacklevel) + else: + out = _label(LISTITEM_INFO % name) + log_msg = 'Current container used for {name!r}: {out!r}' + if log_msg and cls.log.verbose_logging: + cls.log.debug(log_msg, + container_id=container_id, + name=name, + out=out, + stacklevel=stacklevel) + return out + + @classmethod + def get_listitem_property(cls, + name, + container_id=True, + strict=True, + stacklevel=None, + _label=xbmc.getInfoLabel): + if container_id is True: + container_id = _label(PROPERTY % CONTAINER_ID) + elif container_id is None: + strict = False + elif container_id is False: + container_id = _label('System.CurrentControlID') + strict = False + + stacklevel = 2 if stacklevel is None else stacklevel + 1 + + if container_id: + out = _label(CONTAINER_LISTITEM_PROP % (container_id, name)) + log_msg = 'Container {container_id} used for {name!r}: {out!r}' + elif strict: + out = '' + log_msg = None + cls.log.warning('Plugin container not found for %r', name, + stacklevel=stacklevel) + else: + out = _label(LISTITEM_PROP % name) + log_msg = 'Current container used for {name!r}: {out!r}' + if log_msg and cls.log.verbose_logging: + cls.log.debug(log_msg, + container_id=container_id, + name=name, + out=out, + stacklevel=stacklevel) + return out + + @classmethod + def set_property(cls, + property_id, + value='true', + stacklevel=2, + process=None, + log_value=None, + log_process=None, + raw=False): + if log_value is None: + log_value = value + if log_process: + log_value = log_process(log_value) + cls.log.debug_trace('Set property {property_id!r}: {value!r}', + property_id=property_id, + value=log_value, + stacklevel=stacklevel) + _property_id = property_id if raw else '-'.join((ADDON_ID, property_id)) + if process: + value = process(value) + xbmcgui.Window(10000).setProperty(_property_id, value) + return value + + @classmethod + def get_property(cls, + 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) + cls.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 + + @classmethod + def pop_property(cls, + 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) + cls.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 + + @classmethod + def clear_property(cls, property_id, stacklevel=2, raw=False): + cls.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 set_focus_next_item(self): + self._context.send_notification(method=CONTAINER_FOCUS, + data={ + CONTAINER_POSITION: 'next', + }) @staticmethod def busy_dialog_active(all_modals=False, dialog_ids=frozenset(( @@ -442,6 +786,10 @@ def __enter__(self): def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): self.close() + if exc_val: + logging.exception('Error', + exc_info=(exc_type, exc_val, exc_tb), + stacklevel=2) @staticmethod def close(): diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/__init__.py index 0754adc1fe..e69de29bb2 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from __future__ import absolute_import, division, unicode_literals - -from . import datetime_parser -from .methods import ( - duration_to_seconds, - find_video_id, - format_stack, - friendly_number, - get_kodi_setting_bool, - get_kodi_setting_value, - jsonrpc, - loose_version, - make_dirs, - merge_dicts, - parse_and_redact_uri, - redact_auth_header, - redact_ip_in_uri, - redact_params, - rm_dir, - seconds_to_duration, - select_stream, - strip_html_from_text, - to_unicode, - wait, -) -from .system_version import current_system_version - - -__all__ = ( - 'current_system_version', - 'datetime_parser', - 'duration_to_seconds', - 'find_video_id', - 'format_stack', - 'friendly_number', - 'get_kodi_setting_bool', - 'get_kodi_setting_value', - 'jsonrpc', - 'loose_version', - 'make_dirs', - 'merge_dicts', - 'parse_and_redact_uri', - 'redact_auth_header', - 'redact_ip_in_uri', - 'redact_params', - 'rm_dir', - 'seconds_to_duration', - 'select_stream', - 'strip_html_from_text', - 'to_unicode', - 'wait', -) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/convert_format.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/convert_format.py new file mode 100644 index 0000000000..189e3b29cd --- /dev/null +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/convert_format.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2014-2016 bromix (plugin.video.youtube) + Copyright (C) 2016-2025 plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from __future__ import absolute_import, division, unicode_literals + +from datetime import timedelta +from math import floor, log +from re import DOTALL, compile as re_compile + +from ..compatibility import byte_string_type + + +def to_unicode(text): + if isinstance(text, byte_string_type): + try: + return text.decode('utf-8', 'ignore') + except UnicodeError: + pass + return text + + +def strip_html_from_text(text, tag_re=re_compile('<[^<]+?>')): + """ + Removes html tags + :param text: html text + :param tag_re: RE pattern object used to match html tags + :return: + """ + return tag_re.sub('', text) + + +def friendly_number(value, precision=3, scale=('', 'K', 'M', 'B'), as_str=True): + value = float('{value:.{precision}g}'.format( + value=float(value), + precision=precision, + )) + abs_value = abs(value) + magnitude = 0 if abs_value < 1000 else int(log(floor(abs_value), 1000)) + output = '{output:f}'.format( + output=value / 1000 ** magnitude + ).rstrip('0').rstrip('.') + scale[magnitude] + return output if as_str else (output, value) + + +def duration_to_seconds(duration, + periods_seconds_map={ + '': 1, # 1 second for unitless period + 's': 1, # 1 second + 'm': 60, # 1 minute + 'h': 3600, # 1 hour + 'd': 86400, # 1 day + }, + periods_re=re_compile(r'([\d.]+)(d|h|m|s|$)')): + if ':' in duration: + seconds = 0 + for part in duration.split(':'): + seconds = seconds * 60 + (float(part) if '.' in part else int(part)) + return seconds + return sum( + (float(number) if '.' in number else int(number)) + * periods_seconds_map.get(period, 1) + for number, period in periods_re.findall(duration.lower()) + ) + + +def seconds_to_duration(seconds): + return str(timedelta(seconds=seconds)) + + +def timedelta_to_timestamp(delta, offset=None, multiplier=1.0): + if isinstance(delta, timedelta): + pass + elif isinstance(delta, (list, tuple)) and len(delta) == 3: + delta = timedelta(hours=int(delta[0]), + minutes=int(delta[1]), + seconds=float(delta[2])) + else: + return None + + if offset is not None: + if isinstance(offset, timedelta): + delta += offset + elif isinstance(offset, (list, tuple)) and len(offset) == 3: + delta += timedelta(hours=int(offset[0]), + minutes=int(offset[1]), + seconds=float(offset[2])) + elif isinstance(offset, dict): + delta += timedelta(**offset) + + total_seconds = delta.total_seconds() * multiplier + hrs, rem = divmod(total_seconds, 3600) + mins, secs = divmod(rem, 60) + return '{0:02.0f}:{1:02.0f}:{2:06.3f}'.format(hrs, mins, secs) + + +def _srt_to_vtt(content, + srt_re=re_compile( + br'\d+[\r\n]' + br'(?P[\d:,]+) --> ' + br'(?P[\d:,]+)[\r\n]' + br'(?P.+?)[\r\n][\r\n]', + flags=DOTALL, + )): + subtitle_iter = srt_re.finditer(content) + try: + subtitle = next(subtitle_iter).groupdict() + start = subtitle['start'].replace(b',', b'.') + end = subtitle['end'].replace(b',', b'.') + text = subtitle['text'] + except StopIteration: + return content + next_subtitle = next_start = next_end = next_text = None + output = [b'WEBVTT\n\n'] + while 1: + if next_start and next_end: + start = next_start + end = next_end + if next_subtitle: + subtitle = next_subtitle + text = next_text + elif not subtitle: + break + + try: + next_subtitle = next(subtitle_iter).groupdict() + next_start = next_subtitle['start'].replace(b',', b'.') + next_end = next_subtitle['end'].replace(b',', b'.') + next_text = next_subtitle['text'] + except StopIteration: + if subtitle == next_subtitle: + break + subtitle = None + next_subtitle = None + + if next_subtitle and end > next_start: + if end > next_end: + fill_start, fill_end = next_start, next_end + end, next_start, next_end = fill_start, fill_end, end + next_subtitle = None + else: + fill_start, fill_end = next_start, end + end, next_start = fill_start, fill_end + subtitle = None + output.append(b'%s --> %s\n%s\n\n' + b'%s --> %s\n%s\n%s\n\n' + % ( + start, end, text, + fill_start, fill_end, text, next_text, + )) + elif end > start: + output.append(b'%s --> %s\n%s\n\n' % (start, end, text)) + return b''.join(output) + + +def fix_subtitle_stream(stream_type, + content, + vtt_re=re_compile( + br'(\d+:\d+:\d+\.\d+ --> \d+:\d+:\d+\.\d+)' + br'(?: (?:' + br'align:start' + br'|position:0%' + br'|position:63%' + br'|line:0%' + br'))+' + ), + vtt_repl=br'\1'): + content_type, sub_format, sub_type = stream_type + if content_type != 'track': + pass + elif sub_format == 'vtt': + content = vtt_re.sub(vtt_repl, content) + elif sub_format == 'srt': + content = _srt_to_vtt(content) + return content diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/datetime.py similarity index 85% rename from plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py rename to plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/datetime.py index d33111aa3b..48aac1b04d 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/datetime.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals +from time import strftime, gmtime from datetime import date, datetime, time as dt_time, timedelta from importlib import import_module from re import compile as re_compile @@ -44,9 +45,11 @@ ) __INTERNAL_CONSTANTS__ = { - 'epoch_dt': ( - datetime.fromtimestamp(0, tz=timezone.utc) if timezone - else datetime.fromtimestamp(0) + 'epoch_dt': datetime.fromtimestamp(0), + 'epoch_dt_utc': ( + datetime.fromtimestamp(0, tz=timezone.utc) + if timezone else + None ), 'local_offset': None, 'Jan': 1, @@ -70,7 +73,7 @@ fromtimestamp = datetime.fromtimestamp -def parse(datetime_string): +def parse_to_dt(datetime_string): if not datetime_string: return None @@ -139,7 +142,7 @@ def parse(datetime_string): match['tzinfo'] = timezone.utc return datetime(**match) - raise KodionException('Could not parse |{datetime}| as ISO 8601' + raise KodionException('Could not parse {datetime!r} as ISO 8601' .format(datetime=datetime_string)) @@ -292,7 +295,15 @@ def strptime(datetime_str, fmt=None, _strptime=modules['_strptime']): def since_epoch(dt_object=None): if dt_object is None: dt_object = now(tz=timezone.utc) if timezone else datetime.utcnow() - return (dt_object - __INTERNAL_CONSTANTS__['epoch_dt']).total_seconds() + if dt_object.tzinfo: + if timezone: + epoch = __INTERNAL_CONSTANTS__['epoch_dt_utc'] + else: + dt_object = dt_object.replace(tzinfo=None) + epoch = __INTERNAL_CONSTANTS__['epoch_dt'] + else: + epoch = __INTERNAL_CONSTANTS__['epoch_dt'] + return (dt_object - epoch).total_seconds() def yt_datetime_offset(**kwargs): @@ -302,3 +313,33 @@ def yt_datetime_offset(**kwargs): _now = datetime.utcnow() return (_now - timedelta(**kwargs)).strftime('%Y-%m-%dT%H:%M:%SZ') + + +def imf_fixdate(seconds, + _days=( + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', + 'Sun', + ), + _months=( + None, + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + )): + _time = gmtime(seconds) + out = strftime('{weekday}, %d {month} %Y %H:%M:%S GMT', _time) + return out.format(weekday=_days[_time.tm_wday], month=_months[_time.tm_mon]) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/file_system.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/file_system.py new file mode 100644 index 0000000000..36598116d3 --- /dev/null +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/file_system.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2014-2016 bromix (plugin.video.youtube) + Copyright (C) 2016-2025 plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from __future__ import absolute_import, division, unicode_literals + +import os +import shutil + +from .. import logging +from ..compatibility import xbmcvfs + + +def make_dirs(path): + if not path.endswith('/'): + path = ''.join((path, '/')) + path = xbmcvfs.translatePath(path) + + if xbmcvfs.exists(path) or xbmcvfs.mkdirs(path): + return path + + try: + os.makedirs(path) + except OSError: + if not xbmcvfs.exists(path): + logging.exception(('Failed', 'Path: %r'), path) + return False + return path + + +def rm_dir(path): + if not path.endswith('/'): + path = ''.join((path, '/')) + path = xbmcvfs.translatePath(path) + + if not xbmcvfs.exists(path) or xbmcvfs.rmdir(path, force=True): + return True + + try: + shutil.rmtree(path) + except OSError: + logging.exception(('Failed', 'Path: %r'), path) + return not xbmcvfs.exists(path) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/methods.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/methods.py index affe16ad1f..69a82e2ea8 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,48 +11,17 @@ from __future__ import absolute_import, division, unicode_literals import json -import os -import shutil -from base64 import urlsafe_b64decode -from datetime import timedelta -from math import floor, log -from re import compile as re_compile -from sys import exc_info -from traceback import format_stack as _format_stack -from ..compatibility import ( - byte_string_type, - parse_qs, - string_type, - urlencode, - urlsplit, - urlunsplit, - xbmc, - xbmcvfs, -) -from ..logger import Logger +from ..compatibility import generate_hash, string_type, xbmc __all__ = ( - 'duration_to_seconds', - 'find_video_id', - 'format_stack', - 'friendly_number', + 'generate_hash', 'get_kodi_setting_bool', 'get_kodi_setting_value', 'jsonrpc', 'loose_version', - 'make_dirs', 'merge_dicts', - 'parse_and_redact_uri', - 'redact_auth_header', - 'redact_ip_in_uri', - 'redact_params', - 'rm_dir', - 'seconds_to_duration', - 'select_stream', - 'strip_html_from_text', - 'to_unicode', 'wait', ) @@ -61,194 +30,6 @@ def loose_version(v): return [point.zfill(8) for point in v.split('.')] -def to_unicode(text): - if isinstance(text, byte_string_type): - try: - return text.decode('utf-8', 'ignore') - except UnicodeError: - pass - return text - - -def select_stream(context, - stream_data_list, - ask_for_quality, - audio_only, - use_mpd=True): - settings = context.get_settings() - if settings.use_isa(): - isa_capabilities = context.inputstream_adaptive_capabilities() - use_adaptive = bool(isa_capabilities) - use_live_adaptive = use_adaptive and 'live' in isa_capabilities - use_live_mpd = use_live_adaptive and settings.use_mpd_live_streams() - else: - use_adaptive = False - use_live_adaptive = False - use_live_mpd = False - - if audio_only: - context.log_debug('Select stream - Audio only') - stream_list = [item for item in stream_data_list - if 'video' not in item] - else: - stream_list = [ - item for item in stream_data_list - if (not item.get('adaptive') - or (not item.get('live') - and ((use_mpd and item.get('dash/video')) - or (use_adaptive and item.get('hls/video')))) - or (item.get('live') - and ((use_live_mpd and item.get('dash/video')) - or (use_live_adaptive and item.get('hls/video'))))) - ] - - if not stream_list: - context.log_debug('Select stream - No streams found') - return None - - def _stream_sort(_stream): - return _stream.get('sort', [0, 0, 0]) - - stream_list.sort(key=_stream_sort, reverse=True) - num_streams = len(stream_list) - ask_for_quality = ask_for_quality and num_streams >= 1 - context.log_debug('Available streams: {0}'.format(num_streams)) - - for idx, stream in enumerate(stream_list): - log_data = stream.copy() - - if 'license_info' in log_data: - license_info = log_data['license_info'].copy() - for detail in ('url', 'token'): - original_value = license_info.get(detail) - if original_value: - license_info[detail] = '' - log_data['license_info'] = license_info - - original_value = log_data.get('url') - if original_value: - log_data['url'] = redact_ip_in_uri(original_value) - - context.log_debug('Stream {idx}:' - '\n\t{stream_details}' - .format(idx=idx, stream_details=log_data)) - - if ask_for_quality: - selected_stream = context.get_ui().on_select( - context.localize('select_video_quality'), - [stream['title'] for stream in stream_list], - ) - if selected_stream == -1: - context.log_debug('Select stream - No stream selected') - return None - else: - selected_stream = 0 - - context.log_debug('Selected stream: Stream {0}'.format(selected_stream)) - return stream_list[selected_stream] - - -def strip_html_from_text(text, tag_re=re_compile('<[^<]+?>')): - """ - Removes html tags - :param text: html text - :param tag_re: RE pattern object used to match html tags - :return: - """ - return tag_re.sub('', text) - - -def make_dirs(path): - if not path.endswith('/'): - path = ''.join((path, '/')) - path = xbmcvfs.translatePath(path) - - succeeded = xbmcvfs.exists(path) or xbmcvfs.mkdirs(path) - if succeeded: - return path - - try: - os.makedirs(path) - succeeded = True - except OSError: - succeeded = xbmcvfs.exists(path) - - if succeeded: - return path - Logger.log_error('utils.make_dirs - Failed to create directory' - '\n\tPath: {path}'.format(path=path)) - return False - - -def rm_dir(path): - if not path.endswith('/'): - path = ''.join((path, '/')) - path = xbmcvfs.translatePath(path) - - succeeded = (not xbmcvfs.exists(path) - or xbmcvfs.rmdir(path, force=True)) - if not succeeded: - try: - shutil.rmtree(path) - except OSError: - pass - succeeded = not xbmcvfs.exists(path) - - if succeeded: - return True - Logger.log_error('utils.rm_dir - Failed to remove directory' - '\n\tPath: {path}'.format(path=path)) - return False - - -def find_video_id(plugin_path, - video_id_re=re_compile( - r'.*video_id=(?P[a-zA-Z0-9_\-]{11}).*' - )): - match = video_id_re.search(plugin_path) - if match: - return match.group('video_id') - return '' - - -def friendly_number(value, precision=3, scale=('', 'K', 'M', 'B'), as_str=True): - value = float('{value:.{precision}g}'.format( - value=float(value), - precision=precision, - )) - abs_value = abs(value) - magnitude = 0 if abs_value < 1000 else int(log(floor(abs_value), 1000)) - output = '{output:f}'.format( - output=value / 1000 ** magnitude - ).rstrip('0').rstrip('.') + scale[magnitude] - return output if as_str else (output, value) - - -def duration_to_seconds(duration, - periods_seconds_map={ - '': 1, # 1 second for unitless period - 's': 1, # 1 second - 'm': 60, # 1 minute - 'h': 3600, # 1 hour - 'd': 86400, # 1 day - }, - periods_re=re_compile(r'([\d.]+)(d|h|m|s|$)')): - if ':' in duration: - seconds = 0 - for part in duration.split(':'): - seconds = seconds * 60 + (float(part) if '.' in part else int(part)) - return seconds - return sum( - (float(number) if '.' in number else int(number)) - * periods_seconds_map.get(period, 1) - for number, period in periods_re.findall(duration.lower()) - ) - - -def seconds_to_duration(seconds): - return str(timedelta(seconds=seconds)) - - def merge_dicts(item1, item2, templates=None, compare_str=False, _=Ellipsis): if not isinstance(item1, dict) or not isinstance(item2, dict): if (compare_str @@ -316,97 +97,3 @@ def wait(timeout=None): elif timeout < 0: timeout = 0.1 return xbmc.Monitor().waitForAbort(timeout) - - -def redact_ip_in_uri( - url, - _re=re_compile(r'([?&/]|%3F|%26|%2F)ip([=/]|%3D|%2F)[^?&/%]+'), -): - return _re.sub(r'\g<1>ip\g<2>', url) - - -def redact_auth_header(header_string, - _re=re_compile(r'"Authorization": "[^"]+"')): - return _re.sub(r'"Authorization": ""', header_string) - - -def redact_params(params): - log_params = params.copy() - for param, value in params.items(): - if param in {'key', 'api_key', 'api_secret', 'client_secret'}: - log_value = ( - ['...'.join((val[:3], val[-3:])) for val in value] - if isinstance(value, (list, tuple)) else - '...'.join((value[:3], value[-3:])) - ) - elif param in {'api_id', 'client_id'}: - log_value = ( - ['...'.join((val[:3], val[-5:])) for val in value] - if isinstance(value, (list, tuple)) else - '...'.join((value[:3], value[-5:])) - ) - elif param in {'access_token', 'refresh_token', 'token'}: - log_value = ( - ['' for _ in value] - if isinstance(value, (list, tuple)) else - '' - ) - elif param == 'url': - log_value = ( - [redact_ip_in_uri(val) for val in value] - if isinstance(value, (list, tuple)) else - redact_ip_in_uri(value) - ) - elif param == 'ip': - log_value = ( - ['' for _ in value] - if isinstance(value, (list, tuple)) else - '' - ) - elif param == 'location': - log_value = ( - ['|xx.xxxx,xx.xxxx|' for _ in value] - if isinstance(value, (list, tuple)) else - '|xx.xxxx,xx.xxxx|' - ) - elif param == '__headers': - log_value = ( - [redact_auth_header(val) for val in value] - if isinstance(value, (list, tuple)) else - redact_auth_header(value) - ) - else: - continue - log_params[param] = log_value - return log_params - - -def parse_and_redact_uri(uri, redact_only=False): - parts = urlsplit(uri) - if parts.query: - params = parse_qs(parts.query, keep_blank_values=True) - headers = params.get('__headers', [None])[0] - if headers: - params['__headers'] = [urlsafe_b64decode(headers).decode('utf-8')] - log_params = redact_params(params) - log_uri = urlunsplit(( - parts.scheme, parts.netloc, parts.path, urlencode(log_params), '', - )) - else: - params = log_params = None - log_uri = uri - if redact_only: - return log_uri - return parts, params, log_uri, log_params - - -def format_stack(): - tb_obj = exc_info()[2] - while tb_obj: - next_tb_obj = tb_obj.tb_next - if next_tb_obj: - tb_obj = next_tb_obj - else: - return ''.join(_format_stack(f=tb_obj.tb_frame)) - else: - return None diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/redact.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/redact.py new file mode 100644 index 0000000000..a612a10f51 --- /dev/null +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/redact.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2014-2016 bromix (plugin.video.youtube) + Copyright (C) 2016-2025 plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from __future__ import absolute_import, division, unicode_literals + +from base64 import urlsafe_b64decode +from re import compile as re_compile +from ..compatibility import parse_qs, urlencode, urlsplit, urlunsplit + + +def redact_ip_in_uri( + url, + _re=re_compile(r'([?&/]|%3F|%26|%2F)ip([=/]|%3D|%2F)[^?&/%]+'), +): + return _re.sub(r'\g<1>ip\g<2>', url) + + +def redact_auth_header(headers, + _re=re_compile(r'"Authorization": "[^"]+"')): + if isinstance(headers, dict): + log_headers = headers.copy() + if 'Authorization' in log_headers: + log_headers['Authorization'] = '' + return log_headers + return _re.sub(r'"Authorization": ""', headers) + + +def redact_license_info(license_info): + license_info = license_info.copy() + for detail in ('url', 'token'): + if detail in license_info: + license_info[detail] = '' + return license_info + + +def redact_params(params): + log_params = params.copy() + for param, value in params.items(): + if param in {'key', 'api_key', 'api_secret', 'client_secret'}: + log_value = ( + ['...'.join((val[:3], val[-3:])) + if len(val) > 9 else + '...' + for val in value] + if isinstance(value, (list, tuple)) else + '...'.join((value[:3], value[-3:])) + if len(value) > 9 else + '...' + ) + elif param in {'api_id', 'client_id'}: + log_value = ( + ['...'.join((val[:3], val[-5:])) + if len(val) > 11 else + '...' + for val in value] + if isinstance(value, (list, tuple)) else + '...'.join((value[:3], value[-5:])) + if len(value) > 11 else + '...' + ) + elif param in {'access_token', + 'ip', + 'playback_stats', + 'refresh_token', + 'token'}: + log_value = ( + ['' for _ in value] + if isinstance(value, (list, tuple)) else + '' + ) + elif param in {'url', + 'playing_file'}: + log_value = ( + [redact_ip_in_uri(val) for val in value] + if isinstance(value, (list, tuple)) else + redact_ip_in_uri(value) + ) + elif param == 'location': + log_value = ( + ['xx.xxxx,xx.xxxx' for _ in value] + if isinstance(value, (list, tuple)) else + 'xx.xxxx,xx.xxxx' + ) + elif param in {'headers', '__headers'}: + log_value = ( + [redact_auth_header(val) for val in value] + if isinstance(value, (list, tuple)) else + redact_auth_header(value) + ) + elif param == 'license_info': + log_value = ( + [redact_license_info(val) for val in value] + if isinstance(value, (list, tuple)) else + redact_license_info(value) + ) + else: + continue + log_params[param] = log_value + return log_params + + +def parse_and_redact_uri(uri, redact_only=False): + parts = urlsplit(uri) + if parts.query: + params = parse_qs(parts.query, keep_blank_values=True) + headers = params.get('__headers', [None])[0] + if headers: + params['__headers'] = [urlsafe_b64decode(headers).decode('utf-8')] + log_params = redact_params(params) + log_uri = urlunsplit(( + parts.scheme, + parts.netloc, + parts.path, + urlencode(log_params, doseq=True), + '', + )) + else: + params = log_params = None + log_uri = uri + if redact_only: + return log_uri + return parts, params, log_uri, log_params diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/system_version.py b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/system_version.py index a6e8b15b74..a0cbd3ec84 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/system_version.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/system_version.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/__init__.py index f1edbe0f98..b13327b143 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/__config__.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/__config__.py deleted file mode 100644 index 65488ce3b3..0000000000 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/__config__.py +++ /dev/null @@ -1,202 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2017-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from __future__ import absolute_import, division, unicode_literals - -from base64 import b64decode - -from ... import key_sets -from ...kodion.json_store import APIKeyStore, AccessManager - - -DEFAULT_SWITCH = 1 - - -class APICheck(object): - def __init__(self, context): - self._context = context - self._api_jstore = APIKeyStore() - self._json_api = self._api_jstore.get_data() - self._access_manager = AccessManager(context) - - j_key = self._json_api['keys']['personal'].get('api_key', '') - j_id = self._json_api['keys']['personal'].get('client_id', '') - j_secret = self._json_api['keys']['personal'].get('client_secret', '') - - if j_key and j_id and j_secret: - # users are now pasting keys into api_keys.json - # try stripping whitespace and .apps.googleusercontent.com from keys and saving the result if they differ - stripped_key, stripped_id, stripped_secret = self._strip_api_keys(j_key, j_id, j_secret) - if (stripped_key and stripped_id and stripped_secret - and (j_key != stripped_key or j_id != stripped_id or j_secret != stripped_secret)): - self._json_api['keys']['personal'] = {'api_key': stripped_key, 'client_id': stripped_id, 'client_secret': stripped_secret} - self._api_jstore.save(self._json_api) - - settings = self._context.get_settings() - original_key = settings.api_key() - original_id = settings.api_id() - original_secret = settings.api_secret() - if original_key and original_id and original_secret: - own_key, own_id, own_secret = self._strip_api_keys(original_key, original_id, original_secret) - if own_key and own_id and own_secret: - if (original_key != own_key) or (original_id != own_id) or (original_secret != own_secret): - settings.api_key(own_key) - settings.api_id(own_id) - settings.api_secret(own_secret) - - if (j_key != own_key) or (j_id != own_id) or (j_secret != own_secret): - self._json_api['keys']['personal'] = {'api_key': own_key, 'client_id': own_id, 'client_secret': own_secret} - self._api_jstore.save(self._json_api) - - self._json_api = self._api_jstore.get_data() - j_key = self._json_api['keys']['personal'].get('api_key', '') - j_id = self._json_api['keys']['personal'].get('client_id', '') - j_secret = self._json_api['keys']['personal'].get('client_secret', '') - - if ((not original_key or not original_id or not original_secret) - and j_key and j_secret and j_id): - settings.api_key(j_key) - settings.api_id(j_id) - settings.api_secret(j_secret) - - switch = self.get_current_switch() - last_hash = self._access_manager.get_last_key_hash() - current_hash = self._get_key_set_hash(switch) - - changed = current_hash != last_hash - if changed and switch == 'own': - changed = self._get_key_set_hash('own_old') != last_hash - if not changed: - self._access_manager.set_last_key_hash(current_hash) - self.changed = changed - - self._context.log_debug('User: |{user}|, ' - 'Using API key set: |{switch}|' - .format(user=self.get_current_user(), - switch=switch)) - if changed: - self._context.log_debug('API key set changed: Signing out') - self._access_manager.set_last_key_hash(current_hash) - self._context.execute(self._context.create_uri( - ('sign', 'out'), - { - 'confirmed': True, - }, - run=True, - )) - - @staticmethod - def get_current_switch(): - return 'own' - - def get_current_user(self): - return self._access_manager.get_current_user() - - def has_own_api_keys(self): - json_data = self._api_jstore.get_data() - try: - return (json_data['keys']['personal']['api_key'] - and json_data['keys']['personal']['client_id'] - and json_data['keys']['personal']['client_secret']) - except KeyError: - return False - - def get_api_keys(self, switch): - self._json_api = self._api_jstore.get_data() - if switch == 'developer': - return self._json_api['keys'][switch] - - decode = True - if switch == 'youtube-tv': - system = 'YouTube TV' - key_set_details = key_sets[switch] - - elif switch.startswith('own'): - decode = False - system = 'All' - key_set_details = self._json_api['keys']['personal'] - - else: - system = 'All' - if switch not in key_sets['provided']: - switch = 0 - key_set_details = key_sets['provided'][switch] - - key_set = { - 'system': system, - 'id': '', - 'key': '', - 'secret': '' - } - for key, value in key_set_details.items(): - if decode: - value = b64decode(value).decode('utf-8') - key = key.partition('_')[-1] - if key and key in key_set: - key_set[key] = value - if (key_set['id'] - and not key_set['id'].endswith('.apps.googleusercontent.com')): - key_set['id'] += '.apps.googleusercontent.com' - return key_set - - def _get_key_set_hash(self, switch): - key_set = self.get_api_keys(switch) - if switch.startswith('own'): - client_id = key_set['id'].replace('.apps.googleusercontent.com', '') - if switch == 'own_old': - client_id += '.apps.googleusercontent.com' - key_set['id'] = client_id - return self._access_manager.calc_key_hash(**key_set) - - def _strip_api_keys(self, api_key, client_id, client_secret): - stripped_key = ''.join(api_key.split()) - stripped_id = ''.join(client_id.replace('.apps.googleusercontent.com', '').split()) - stripped_secret = ''.join(client_secret.split()) - - if api_key != stripped_key: - if stripped_key not in api_key: - self._context.log_debug('Personal API setting: |Key| Skipped: potentially mangled by stripping') - return_key = api_key - else: - self._context.log_debug('Personal API setting: |Key| had whitespace removed') - return_key = stripped_key - else: - return_key = api_key - - if client_id != stripped_id: - if stripped_id not in client_id: - self._context.log_debug('Personal API setting: |Id| Skipped: potentially mangled by stripping') - return_id = client_id - else: - googleusercontent = '' - if '.apps.googleusercontent.com' in client_id: - googleusercontent = ' and .apps.googleusercontent.com' - self._context.log_debug('Personal API setting: |Id| had whitespace%s removed' % googleusercontent) - return_id = stripped_id - else: - return_id = client_id - - if client_secret != stripped_secret: - if stripped_secret not in client_secret: - self._context.log_debug('Personal API setting: |Secret| Skipped: potentially mangled by stripping') - return_secret = client_secret - else: - self._context.log_debug('Personal API setting: |Secret| had whitespace removed') - return_secret = stripped_secret - else: - return_secret = client_secret - - return return_key, return_id, return_secret - - def get_configs(self): - return { - 'youtube-tv': self.get_api_keys('youtube-tv'), - 'main': self.get_api_keys(self.get_current_switch()), - 'developer': self.get_api_keys('developer') - } diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/__init__.py index bd3a6a3974..af4770966a 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,11 +10,9 @@ from __future__ import absolute_import, division, unicode_literals -from .__config__ import APICheck -from .youtube import YouTube +from .player_client import YouTubePlayerClient __all__ = ( - 'APICheck', - 'YouTube', + 'YouTubePlayerClient', ) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/youtube.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/data_client.py similarity index 69% rename from plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/youtube.py rename to plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/data_client.py index 28dcbfb36b..9ec8cc5117 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/data_client.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,60 +12,403 @@ import json import threading -import xml.etree.ElementTree as ET from functools import partial from itertools import chain, islice from random import randint from re import compile as re_compile +from xml.etree.ElementTree import Element as ET_Element, XML as ET_XML -from .login_client import LoginClient -from ..helper.stream_info import StreamInfo +from .login_client import YouTubeLoginClient from ..helper.utils import channel_filter_split from ..helper.v3 import pre_fill from ..youtube_exceptions import InvalidJSON, YouTubeException -from ...kodion.compatibility import available_cpu_count, string_type, to_str +from ...kodion import logging +from ...kodion.compatibility import available_cpu_count, string_type +from ...kodion.constants import CHANNEL_ID, PLAYLIST_ID from ...kodion.items import DirectoryItem -from ...kodion.utils import ( - datetime_parser as dt, - format_stack, - strip_html_from_text, - to_unicode, +from ...kodion.utils.convert_format import strip_html_from_text +from ...kodion.utils.datetime import ( + since_epoch, + strptime, + yt_datetime_offset, ) -class YouTube(LoginClient): - def __init__(self, context, **kwargs): - self._context = context - if 'items_per_page' in kwargs: - self._max_results = kwargs.pop('items_per_page') +class YouTubeDataClient(YouTubeLoginClient): + log = logging.getLogger(__name__) - super(YouTube, self).__init__(context=context, **kwargs) + _max_results = 50 + _VIRTUAL_LISTS = frozenset(('WL', 'LL', 'HL')) + JSON_PATHS = { + 'tv_grid': { + 'items': ( + 'contents', + 'tvBrowseRenderer', + 'content', + 'tvSurfaceContentRenderer', + 'content', + 'gridRenderer', + 'items', + ), + 'item_id': ( + 'tileRenderer', + 'contentId', + ), + 'title': ( + 'tileRenderer', + 'metadata', + 'tileMetadataRenderer', + 'title', + 'simpleText', + ), + 'thumbnails': ( + 'tileRenderer', + 'header', + 'tileHeaderRenderer', + 'thumbnail', + 'thumbnails', + ), + 'channel_id': ( + 'tileRenderer', + 'onLongPressCommand', + 'showMenuCommand', + 'menu', + 'menuRenderer', + 'items', + slice(None), + None, + 'menuNavigationItemRenderer', + 'navigationEndpoint', + 'browseEndpoint', + 'browseId', + ), + 'continuation': ( + 'contents', + 'tvBrowseRenderer', + 'content', + 'tvSurfaceContentRenderer', + 'content', + 'sectionListRenderer', + ( + ( + 'contents', + slice(None), + None, + 'shelfRenderer', + 'content', + ('horizontalListRenderer', 'verticalListRenderer'), + 'continuations', + 0, + 'nextContinuationData', + ), + ( + 'continuations', + 0, + 'nextContinuationData' + ) + ), + ), + 'continuation_items': ( + 'continuationContents', + ('horizontalListContinuation', 'sectionListContinuation'), + 'items', + ), + 'continuation_continuation': ( + 'continuationContents', + ('horizontalListContinuation', 'sectionListContinuation'), + 'continuations', + 0, + 'nextContinuationData', + ), + }, + 'tv_playlist': { + 'items': ( + 'contents', + 'tvBrowseRenderer', + 'content', + 'tvSurfaceContentRenderer', + 'content', + 'twoColumnRenderer', + 'rightColumn', + 'playlistVideoListRenderer', + 'contents', + ), + 'item_id': ( + 'tileRenderer', + 'onSelectCommand', + 'watchEndpoint', + 'videoId', + ), + 'title': ( + 'tileRenderer', + 'metadata', + 'tileMetadataRenderer', + 'title', + 'simpleText', + ), + 'thumbnails': ( + 'tileRenderer', + 'header', + 'tileHeaderRenderer', + 'thumbnail', + 'thumbnails', + ), + 'channel_id': ( + 'tileRenderer', + 'onLongPressCommand', + 'showMenuCommand', + 'menu', + 'menuRenderer', + 'items', + -1, + 'menuNavigationItemRenderer', + 'navigationEndpoint', + 'browseEndpoint', + 'browseId', + ), + 'playlist_id': ( + 'tileRenderer', + 'onSelectCommand', + 'watchEndpoint', + 'playlistId', + ), + 'continuation': ( + 'contents', + 'tvBrowseRenderer', + 'content', + 'tvSurfaceContentRenderer', + 'content', + 'twoColumnRenderer', + 'rightColumn', + 'playlistVideoListRenderer', + 'continuations', + 0, + 'nextContinuationData', + ), + 'continuation_items': ( + 'continuationContents', + 'playlistVideoListContinuation', + 'contents', + ), + 'continuation_continuation': ( + 'continuationContents', + 'playlistVideoListContinuation', + 'continuations', + 0, + 'nextContinuationData', + ), + }, + 'tv_shelf_horizontal': { + 'items': ( + 'contents', + 'tvBrowseRenderer', + 'content', + 'tvSurfaceContentRenderer', + 'content', + 'sectionListRenderer', + 'contents', + slice(None), + 'shelfRenderer', + 'content', + ('horizontalListRenderer', 'verticalListRenderer'), + 'items', + ), + 'item_id': ( + 'tileRenderer', + 'onSelectCommand', + 'watchEndpoint', + 'videoId', + ), + 'title': ( + 'tileRenderer', + 'metadata', + 'tileMetadataRenderer', + 'title', + 'simpleText', + ), + 'thumbnails': ( + 'tileRenderer', + 'header', + 'tileHeaderRenderer', + 'thumbnail', + 'thumbnails', + ), + 'channel_id': ( + 'tileRenderer', + 'onLongPressCommand', + 'showMenuCommand', + 'menu', + 'menuRenderer', + 'items', + slice(None), + None, + 'menuNavigationItemRenderer', + 'navigationEndpoint', + 'browseEndpoint', + 'browseId', + ), + 'continuation': ( + 'contents', + 'tvBrowseRenderer', + 'content', + 'tvSurfaceContentRenderer', + 'content', + 'sectionListRenderer', + ( + ( + 'contents', + slice(None), + None, + 'shelfRenderer', + 'content', + ('horizontalListRenderer', 'verticalListRenderer'), + 'continuations', + 0, + 'nextContinuationData', + ), + ( + 'continuations', + 0, + 'nextContinuationData' + ) + ), + ), + 'continuation_items': ( + 'continuationContents', + ('horizontalListContinuation', 'sectionListContinuation'), + ( + ('items',), + ( + 'contents', + slice(None), + 'shelfRenderer', + 'content', + ('horizontalListRenderer', 'verticalListRenderer'), + 'items', + ), + ), + ), + 'continuation_continuation': ( + 'continuationContents', + ('horizontalListContinuation', 'sectionListContinuation'), + 'continuations', + 0, + 'nextContinuationData', + ), + }, + 'vr_shelf': { + 'items': ( + 'contents', + 'singleColumnBrowseResultsRenderer', + 'tabs', + 0, + 'tabRenderer', + 'content', + 'sectionListRenderer', + 'contents', + slice(None), + 'shelfRenderer', + 'content', + ('horizontalListRenderer', 'verticalListRenderer'), + 'items', + slice(None), + ( + 'gridVideoRenderer', + 'compactVideoRenderer', + 'tileRenderer', + ), + # 'videoId', + ), + 'continuation': ( + 'contents', + 'singleColumnBrowseResultsRenderer', + 'tabs', + 0, + 'tabRenderer', + 'content', + 'sectionListRenderer', + 'continuations', + 0, + 'nextContinuationData', + ), + 'continuation_items': ( + 'continuationContents', + 'sectionListContinuation', + 'contents', + slice(None), + 'shelfRenderer', + 'content', + ('horizontalListRenderer', 'verticalListRenderer'), + 'items', + slice(None), + ( + 'gridVideoRenderer', + 'compactVideoRenderer', + 'tileRenderer', + ), + # 'videoId', + ), + 'continuation_continuation': ( + 'continuationContents', + 'sectionListContinuation', + 'continuations', + 0, + 'nextContinuationData', + ), + }, + } - def max_results(self): - return self._context.get_param('items_per_page') or self._max_results + def __init__(self, context, items_per_page=None, **kwargs): + self.channel_id = None + + if items_per_page is None: + items_per_page = context.get_settings().items_per_page() + + super(YouTubeDataClient, self).__init__(context=context, **kwargs) + YouTubeDataClient.init(items_per_page=items_per_page) + + @classmethod + def init(cls, items_per_page=50, **_kwargs): + cls._max_results = items_per_page + + def reinit(self, **kwargs): + super(YouTubeDataClient, self).reinit(**kwargs) - def get_language(self): - return self._language + def set_access_token(self, access_tokens=None): + super(YouTubeDataClient, self).set_access_token(access_tokens) + if self.logged_in: + context = self._context + function_cache = context.get_function_cache() + self.channel_id = function_cache.run( + self.get_channel_by_identifier, + function_cache.ONE_MONTH, + _refresh=context.refresh_requested(), + identifier='mine', + do_search=False, + notify=False, + ) + else: + self.channel_id = None - def get_region(self): - return self._region + def max_results(self): + return self._context.get_param('items_per_page') or self._max_results - def update_watch_history(self, context, video_id, url, status=None): + def update_watch_history(self, video_id, url, status=None): if status is None: cmt = st = et = state = None else: cmt, st, et, state = status - context.log_debug('Playback reported [{video_id}]:' - ' current time={cmt},' - ' segment start={st},' - ' segment end={et},' - ' state={state}' - .format(video_id=video_id, - cmt=cmt, - st=st, - et=et, - state=state)) + self.log.debug('Playback reported [{video_id}]:' + ' current time={cmt},' + ' segment start={st},' + ' segment end={et},' + ' state={state}', + video_id=video_id, + cmt=cmt, + st=st, + et=et, + state=state) client_data = { '_video_id': video_id, @@ -83,32 +426,19 @@ def update_watch_history(self, context, video_id, url, status=None): if state is not None: params['state'] = state - self.api_request(client='watch_history', + self.api_request('watch_history', 'GET', client_data=client_data, params=params, - no_content=True) - - def get_streams(self, - context, - video_id, - ask_for_quality=False, - audio_only=False, - use_mpd=True): - return StreamInfo( - context, - access_token=self._access_token, - access_token_tv=self._access_token_tv, - ask_for_quality=ask_for_quality, - audio_only=audio_only, - use_mpd=use_mpd, - ).load_stream_info(video_id) + no_content=True, + do_auth=True, + cache=False) def remove_playlist(self, playlist_id, **kwargs): params = {'id': playlist_id, 'mine': True} - return self.api_request(method='DELETE', - path='playlists', + return self.api_request(method='DELETE', path='playlists', params=params, + do_auth=True, no_content=True, **kwargs) @@ -121,10 +451,8 @@ def get_supported_languages(self, language=None, **kwargs): self._language ), } - return self.api_request(method='GET', - path='i18nLanguages', + return self.api_request(method='GET', path='i18nLanguages', params=params, - no_login=True, **kwargs) def get_supported_regions(self, language=None, **kwargs): @@ -136,10 +464,8 @@ def get_supported_regions(self, language=None, **kwargs): self._language ), } - return self.api_request(method='GET', - path='i18nRegions', + return self.api_request(method='GET', path='i18nRegions', params=params, - no_login=True, **kwargs) def rename_playlist(self, @@ -152,8 +478,7 @@ def rename_playlist(self, 'id': playlist_id, 'snippet': {'title': new_title}, 'status': {'privacyStatus': privacy_status}} - return self.api_request(method='PUT', - path='playlists', + return self.api_request(method='PUT', path='playlists', params=params, post_data=post_data, **kwargs) @@ -163,8 +488,7 @@ def create_playlist(self, title, privacy_status='private', **kwargs): post_data = {'kind': 'youtube#playlist', 'snippet': {'title': title}, 'status': {'privacyStatus': privacy_status}} - return self.api_request(method='POST', - path='playlists', + return self.api_request(method='POST', path='playlists', params=params, post_data=post_data, **kwargs) @@ -177,9 +501,9 @@ def get_video_rating(self, video_id, **kwargs): ','.join(video_id) ), } - return self.api_request(method='GET', - path='videos/getRating', + return self.api_request(method='GET', path='videos/getRating', params=params, + do_auth=True, **kwargs) def rate_video(self, video_id, rating='like', **kwargs): @@ -191,50 +515,120 @@ def rate_video(self, video_id, rating='like', **kwargs): """ params = {'id': video_id, 'rating': rating} - return self.api_request(method='POST', - path='videos/rate', + return self.api_request(method='POST', path='videos/rate', params=params, + do_auth=True, no_content=True, **kwargs) + def rate_playlist(self, playlist_id, rating='like', **kwargs): + if rating == 'like': + post_data = { + 'status': 'LIKE', + 'target': { + 'playlistId': playlist_id, + }, + } + path = 'like/like' + else: + post_data = { + 'status': 'INDIFFERENT', + 'target': { + 'playlistId': playlist_id, + }, + } + path = 'like/removelike' + + return self.api_request('tv', 'POST', path=path, + post_data=post_data, + do_auth=True, + **kwargs) + def add_video_to_playlist(self, playlist_id, video_id, **kwargs): - params = {'part': 'snippet', - 'mine': True} - post_data = {'kind': 'youtube#playlistItem', - 'snippet': {'playlistId': playlist_id, - 'resourceId': {'kind': 'youtube#video', - 'videoId': video_id}}} - return self.api_request(method='POST', - path='playlistItems', - params=params, + playlist_id_upper = playlist_id.upper() + if playlist_id_upper not in self._VIRTUAL_LISTS: + params = {'part': 'snippet', + 'mine': True} + post_data = {'kind': 'youtube#playlistItem', + 'snippet': {'playlistId': playlist_id, + 'resourceId': {'kind': 'youtube#video', + 'videoId': video_id}}} + return self.api_request(method='POST', path='playlistItems', + params=params, + post_data=post_data, + **kwargs) + + if playlist_id_upper == 'WL': + post_data = { + 'playlistId': playlist_id_upper, + 'actions': [{ + 'addedVideoId': video_id, + # 'setVideoId': '', + 'action': 'ACTION_ADD_VIDEO', + }], + } + path = 'browse/edit_playlist' + + else: + return False + + return self.api_request('tv', 'POST', path=path, post_data=post_data, + do_auth=True, **kwargs) # noinspection PyUnusedLocal def remove_video_from_playlist(self, playlist_id, playlist_item_id, + video_id, **kwargs): - params = {'id': playlist_item_id} - return self.api_request(method='DELETE', - path='playlistItems', - params=params, - no_content=True, + playlist_id_upper = playlist_id.upper() if playlist_id else '' + if playlist_id_upper not in self._VIRTUAL_LISTS: + params = {'id': playlist_item_id} + return self.api_request(method='DELETE', path='playlistItems', + params=params, + do_auth=True, + no_content=True, + **kwargs) + + if playlist_id_upper == 'WL': + post_data = { + 'playlistId': playlist_id_upper, + 'actions': [{ + 'removedVideoId': video_id, + 'action': 'ACTION_REMOVE_VIDEO_BY_VIDEO_ID', + }], + } + path = 'browse/edit_playlist' + + elif playlist_id_upper == 'LL': + post_data = { + 'target': { + 'videoId': video_id, + }, + } + path = 'like/removelike' + + else: + return False + + return self.api_request('tv', 'POST', path=path, + post_data=post_data, + do_auth=True, **kwargs) def unsubscribe(self, subscription_id, **kwargs): params = {'id': subscription_id} - return self.api_request(method='DELETE', - path='subscriptions', + return self.api_request(method='DELETE', path='subscriptions', params=params, + do_auth=True, no_content=True, **kwargs) def unsubscribe_channel(self, channel_id, **kwargs): post_data = {'channelIds': [channel_id]} - return self.api_request(client='tv', - method='POST', - path='subscription/unsubscribe', + return self.api_request('tv', 'POST', path='subscription/unsubscribe', post_data=post_data, **kwargs) @@ -243,8 +637,7 @@ def subscribe(self, channel_id, **kwargs): post_data = {'kind': 'youtube#subscription', 'snippet': {'resourceId': {'kind': 'youtube#channel', 'channelId': channel_id}}} - return self.api_request(method='POST', - path='subscriptions', + return self.api_request(method='POST', path='subscriptions', params=params, post_data=post_data, **kwargs) @@ -270,8 +663,7 @@ def get_subscription(self, if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='subscriptions', + return self.api_request(method='GET', path='subscriptions', params=params, **kwargs) @@ -283,8 +675,7 @@ def get_guide_category(self, guide_category_id, page_token='', **kwargs): 'hl': self._language} if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='channels', + return self.api_request(method='GET', path='channels', params=params, **kwargs) @@ -296,8 +687,7 @@ def get_guide_categories(self, page_token='', **kwargs): if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='guideCategories', + return self.api_request(method='GET', path='guideCategories', params=params, **kwargs) @@ -309,10 +699,8 @@ def get_trending_videos(self, page_token='', **kwargs): 'chart': 'mostPopular'} if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='videos', + return self.api_request(method='GET', path='videos', params=params, - no_login=True, **kwargs) def get_video_category(self, video_category_id, page_token='', **kwargs): @@ -324,10 +712,8 @@ def get_video_category(self, video_category_id, page_token='', **kwargs): 'hl': self._language} if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='videos', + return self.api_request(method='GET', path='videos', params=params, - no_login=True, **kwargs) def get_video_categories(self, page_token='', **kwargs): @@ -338,168 +724,10 @@ def get_video_categories(self, page_token='', **kwargs): if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='videoCategories', + return self.api_request(method='GET', path='videoCategories', params=params, - no_login=True, **kwargs) - def get_recommended_for_home_tv(self, - visitor='', - page_token='', - click_tracking=''): - return self.get_browse_videos( - browse_id='FEwhat_to_watch', - client='tv', - no_login=False, - json_path={ - 'items': ( - 'contents', - 'tvBrowseRenderer', - 'content', - 'tvSurfaceContentRenderer', - 'content', - 'sectionListRenderer', - 'contents', - slice(None), - 'shelfRenderer', - 'content', - 'horizontalListRenderer', - 'items', - ), - 'video_id': ( - 'tileRenderer', - 'onSelectCommand', - 'watchEndpoint', - 'videoId', - ), - 'title': ( - 'tileRenderer', - 'metadata', - 'tileMetadataRenderer', - 'title', - 'simpleText', - ), - 'thumbnails': ( - 'tileRenderer', - 'header', - 'tileHeaderRenderer', - 'thumbnail', - 'thumbnails', - ), - 'channel_id': ( - 'tileRenderer', - 'onLongPressCommand', - 'showMenuCommand', - 'menu', - 'menuRenderer', - 'items', - slice(None), - None, - 'menuNavigationItemRenderer', - 'navigationEndpoint', - 'browseEndpoint', - 'browseId', - ), - 'continuation': ( - 'contents', - 'tvBrowseRenderer', - 'content', - 'tvSurfaceContentRenderer', - 'content', - 'sectionListRenderer', - 'contents', - 0, - 'shelfRenderer', - 'content', - 'horizontalListRenderer', - 'continuations', - 0, - 'nextContinuationData', - ), - 'continuation_items': ( - 'continuationContents', - 'horizontalListContinuation', - 'items', - ), - 'continuation_continuation': ( - 'continuationContents', - 'horizontalListContinuation', - 'continuations', - 0, - 'nextContinuationData', - ), - }, - visitor=visitor, - page_token=page_token, - click_tracking=click_tracking, - ) - - def get_recommended_for_home_vr(self, - visitor='', - page_token='', - click_tracking=''): - return self.get_browse_videos( - browse_id='FEwhat_to_watch', - client='android_vr', - no_login=False, - json_path={ - 'items': ( - 'contents', - 'singleColumnBrowseResultsRenderer', - 'tabs', - 0, - 'tabRenderer', - 'content', - 'sectionListRenderer', - 'contents', - slice(None), - 'shelfRenderer', - 'content', - ('horizontalListRenderer', 'verticalListRenderer'), - 'items', - slice(None), - ('gridVideoRenderer', 'compactVideoRenderer'), - # 'videoId', - ), - 'continuation': ( - 'contents', - 'singleColumnBrowseResultsRenderer', - 'tabs', - 0, - 'tabRenderer', - 'content', - 'sectionListRenderer', - 'continuations', - 0, - 'nextContinuationData', - ), - 'continuation_items': ( - 'continuationContents', - 'sectionListContinuation', - 'contents', - slice(None), - 'shelfRenderer', - 'content', - ('horizontalListRenderer', 'verticalListRenderer'), - 'items', - slice(None), - ('gridVideoRenderer', 'compactVideoRenderer'), - # 'videoId', - ), - 'continuation_continuation': ( - 'continuationContents', - 'sectionListContinuation', - 'continuations', - 0, - 'nextContinuationData', - ), - }, - visitor=visitor, - page_token=page_token, - click_tracking=click_tracking, - ) - def get_related_for_home(self, page_token='', refresh=False): """ YouTube has deprecated this API, so we use history and related items to @@ -515,27 +743,33 @@ def get_related_for_home(self, page_token='', refresh=False): # Related videos are retrieved for the following num_items from history num_items = 10 - local_history = self._context.get_settings().use_local_history() + video_ids = [] + history_id = self._context.get_access_manager().get_watch_history_id() - if not history_id: - if local_history: - history = self._context.get_playback_history() - video_ids = history.get_items(limit=num_items) - else: - return payload - else: - history = self.get_playlist_items(history_id, max_results=num_items) - if history and 'items' in history: - history_items = history['items'] or [] - video_ids = [] - else: - return payload + if history_id: + history = self.get_playlist_items(history_id, + max_results=num_items, + do_auth=True) + history_items = history and history.get('items') + if history_items: + for item in history_items: + try: + video_id = item['snippet']['resourceId']['videoId'] + except KeyError: + continue + video_ids.append(video_id) - for item in history_items: - try: - video_ids.append(item['snippet']['resourceId']['videoId']) - except KeyError: - continue + remaining_items = num_items - len(video_ids) + local_history = self._context.get_settings().use_local_history() + if local_history and remaining_items: + history = self._context.get_playback_history() + history_items = history.get_items(limit=remaining_items, + excluding=video_ids) + if history_items: + video_ids.extend(history_items) + + if not video_ids: + return payload # Fetch existing list of items, if any data_cache = self._context.get_data_cache() @@ -781,8 +1015,7 @@ def get_activities(self, channel_id, page_token='', **kwargs): if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='activities', + return self.api_request(method='GET', path='activities', params=params, **kwargs) @@ -801,10 +1034,8 @@ def get_channel_sections(self, channel_id, **kwargs): identifier=channel_id, ) params['channelId'] = channel_id - return self.api_request(method='GET', - path='channelSections', + return self.api_request(method='GET', path='channelSections', params=params, - no_login=True, **kwargs) def get_playlists_of_channel(self, channel_id, page_token='', **kwargs): @@ -824,19 +1055,19 @@ def get_playlists_of_channel(self, channel_id, page_token='', **kwargs): if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='playlists', + return self.api_request(method='GET', path='playlists', params=params, - no_login=True, **kwargs) def get_playlist_item_id_of_video_id(self, playlist_id, video_id, + do_auth=None, page_token=''): json_data = self.get_playlist_items( playlist_id=playlist_id, page_token=page_token, + do_auth=do_auth, max_results=self.max_results(), ) if not json_data: @@ -852,6 +1083,7 @@ def get_playlist_item_id_of_video_id(self, return self.get_playlist_item_id_of_video_id( playlist_id=playlist_id, video_id=video_id, + do_auth=do_auth, page_token=next_page_token, ) return None @@ -859,31 +1091,47 @@ def get_playlist_item_id_of_video_id(self, def get_playlist_items(self, playlist_id, page_token='', + do_auth=None, max_results=None, **kwargs): - # prepare params - params = { - 'part': 'snippet', - 'maxResults': ( - self.max_results() - if max_results is None else - max_results - ), - 'playlistId': playlist_id, - } - if page_token: - params['pageToken'] = page_token + playlist_id_upper = playlist_id.upper() + if playlist_id_upper not in self._VIRTUAL_LISTS: + params = { + 'part': 'snippet', + 'maxResults': ( + self.max_results() + if max_results is None else + max_results + ), + 'playlistId': playlist_id, + } + if page_token: + params['pageToken'] = page_token + + return self.api_request(method='GET', path='playlistItems', + params=params, + do_auth=do_auth, + **kwargs) - if self._context.get_param('channel_id', 'mine') == 'mine': - no_login = False + if playlist_id_upper == 'HL': + browse_id = 'FEhistory' + json_path = self.JSON_PATHS['tv_grid'] + response_type = 'videos' else: - no_login = True + browse_id = 'VL' + playlist_id_upper + json_path = self.JSON_PATHS['tv_playlist'] + response_type = 'playlistItems' - return self.api_request(method='GET', - path='playlistItems', - params=params, - no_login=no_login, - **kwargs) + 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, + json_path=json_path, + **kwargs + ) def get_channel_by_identifier(self, identifier, @@ -906,6 +1154,7 @@ def get_channel_by_identifier(self, params = {'part': 'id'} if mine or identifier == 'mine': params['mine'] = True + mine = True elif id_re.match(identifier): if not verify_id: return identifier @@ -918,33 +1167,30 @@ def get_channel_by_identifier(self, handle = True params['forHandle'] = identifier - json_data = self.api_request( - method='GET', - path='channels', - params=params, - no_login=True, - **kwargs - ) + json_data = self.api_request(method='GET', path='channels', + params=params, + do_auth=True if mine else False, + **kwargs) if as_json: return json_data try: return json_data['items'][0]['id'] - except (IndexError, KeyError, TypeError) as exc: - self._context.log_warning('YouTube.get_channel_by_identifier' - ' - Channel ID not found' - '\n\tException: {exc!r}' - '\n\tData: {data}' - '\n\tIdentifier: |{identifier}|' - '\n\tmine: |{mine}|' - '\n\tforHandle: |{handle}|' - '\n\tforUsername: |{username}|' - .format(exc=exc, - data=json_data, - identifier=identifier, - mine=mine, - handle=handle, - username=username)) + except (IndexError, KeyError, TypeError): + self.log.warning(('Channel ID not found', + 'Data: {data}', + 'Identifier: {identifier!r}', + 'mine: {mine!r}', + 'forHandle: {handle!r}', + 'forUsername: {username!r}'), + data=json_data, + identifier=identifier, + mine=mine, + handle=handle, + username=username, + exc_info=True, + stack_info=True, + stacklevel=2) if not do_search: return None @@ -963,13 +1209,15 @@ def get_channel_by_identifier(self, def get_channels_by_identifiers(self, identifiers, **kwargs): function_cache = self._context.get_function_cache() - refresh = self._context.refresh_requested() + if self._context.refresh_requested(): + max_age = function_cache.ONE_DAY + else: + max_age = function_cache.ONE_MONTH return { function_cache.run( self.get_channel_by_identifier, - function_cache.ONE_MONTH, - _refresh=refresh, + max_age, identifier=identifier, **kwargs ) @@ -981,13 +1229,17 @@ def channel_match(self, identifier, identifiers, exclude=False): return False function_cache = self._context.get_function_cache() - refresh = self._context.refresh_requested() + if self._context.refresh_requested(): + max_age = function_cache.ONE_DAY + refresh = True + else: + max_age = function_cache.ONE_MONTH + refresh = False result = False channel_id = function_cache.run( self.get_channel_by_identifier, - function_cache.ONE_MONTH, - _refresh=refresh, + max_age, identifier=identifier, ) if channel_id: @@ -1025,7 +1277,7 @@ def get_channels(self, channel_id, max_results=None, **kwargs): max_results = self.max_results() params = { 'part': 'snippet,contentDetails,brandingSettings,statistics', - 'maxResults': str(max_results), + 'maxResults': max_results, } if channel_id == 'mine': @@ -1035,10 +1287,8 @@ def get_channels(self, channel_id, max_results=None, **kwargs): else: params['id'] = ','.join(channel_id) - return self.api_request(method='GET', - path='channels', + return self.api_request(method='GET', path='channels', params=params, - no_login=True, **kwargs) def get_disliked_videos(self, page_token='', **kwargs): @@ -1053,9 +1303,9 @@ def get_disliked_videos(self, page_token='', **kwargs): if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='videos', + return self.api_request(method='GET', path='videos', params=params, + do_auth=True, **kwargs) def get_videos(self, @@ -1088,10 +1338,8 @@ def get_videos(self, max_results ), } - return self.api_request(method='GET', - path='videos', + return self.api_request(method='GET', path='videos', params=params, - no_login=True, **kwargs) def get_playlists(self, playlist_id, max_results=None, **kwargs): @@ -1108,35 +1356,58 @@ def get_playlists(self, playlist_id, max_results=None, **kwargs): max_results ), } - return self.api_request(method='GET', - path='playlists', + return self.api_request(method='GET', path='playlists', params=params, - no_login=True, **kwargs) - def get_browse_videos(self, - browse_id=None, - channel_id=None, - params=None, - route=None, - _route={ - 'featured': 'EghmZWF0dXJlZPIGBAoCMgA%3D', - 'videos': 'EgZ2aWRlb3PyBgQKAjoA', - 'shorts': 'EgZzaG9ydHPyBgUKA5oBAA%3D%3D', - 'streams': 'EgdzdHJlYW1z8gYECgJ6AA%3D%3D', - 'podcasts': 'Eghwb2RjYXN0c_IGBQoDugEA', - 'courses': 'Egdjb3Vyc2Vz8gYFCgPCAQA%3D', - 'playlists': 'EglwbGF5bGlzdHPyBgQKAkIA', - 'community': 'Egljb21tdW5pdHnyBgQKAkoA', - 'search': 'EgZzZWFyY2jyBgQKAloA', - }, - data=None, - client=None, - no_login=True, - visitor='', - page_token='', - click_tracking='', - json_path=None): + def get_browse_items(self, + browse_id=None, + channel_id=None, + playlist_id=None, + skip_ids=None, + params=None, + route=None, + _route={ + 'featured': 'EghmZWF0dXJlZPIGBAoCMgA%3D', + 'videos': 'EgZ2aWRlb3PyBgQKAjoA', + 'shorts': 'EgZzaG9ydHPyBgUKA5oBAA%3D%3D', + 'streams': 'EgdzdHJlYW1z8gYECgJ6AA%3D%3D', + 'podcasts': 'Eghwb2RjYXN0c_IGBQoDugEA', + 'courses': 'Egdjb3Vyc2Vz8gYFCgPCAQA%3D', + 'playlists': 'EglwbGF5bGlzdHPyBgQKAkIA', + 'community': 'Egljb21tdW5pdHnyBgQKAkoA', + 'search': 'EgZzZWFyY2jyBgQKAloA', + }, + response_type='videos', + _response_types={ + 'videos': ( + 'youtube#videoListResponse', + 'youtube#video', + 'videoId', + ), + 'playlists': ( + 'youtube#playlistListResponse', + 'youtube#playlist', + 'contentId', + ), + 'playlistItems': ( + 'youtube#playlistItemListResponse', + 'youtube#playlistItem', + 'contentId', + ), + }, + data=None, + client=None, + do_auth=False, + page_token=None, + click_tracking=None, + visitor=None, + items_per_page=None, + json_path=None): + response_type = _response_types.get(response_type) + if not response_type: + return None + if channel_id: function_cache = self._context.get_function_cache() channel_id = function_cache.run( @@ -1176,14 +1447,11 @@ def get_browse_videos(self, } post_data['context'] = context - result = self.api_request( - client=client or 'web', - url='https://www.youtube.com/youtubei/v1/{_endpoint}', - path='browse', - method='POST', - post_data=post_data, - no_login=no_login, - ) + result = self.api_request(client or 'web', 'POST', path='browse', + url=self.V1_API_URL, + post_data=post_data, + do_auth=do_auth, + cache=True) if not result: return {} @@ -1197,32 +1465,57 @@ def get_browse_videos(self, if not item_path: return result + response_kind, item_kind, item_id_kind = response_type + v3_response = { - 'kind': 'youtube#videoListResponse', + 'kind': response_kind, 'items': None, } nodes = self.json_traverse(result, path=item_path, default=()) items = [] - for videos in nodes: - if not isinstance(videos, (list, tuple)): - videos = (videos,) - for video in videos: - if not video: + for content in nodes: + if not isinstance(content, (list, tuple)): + content = (content,) + for item in content: + if not item: continue - video_id = self.json_traverse( - video, - json_path.get('video_id') or ('videoId',), + item_id = self.json_traverse( + item, + json_path.get('item_id') or (item_id_kind,), ) - if not video_id: + if not item_id or skip_ids and item_id in skip_ids: continue + if channel_id: + _channel_id = channel_id + else: + _channel_id = self.json_traverse( + item, + json_path.get('channel_id') or ( + ('longBylineText', 'shortBylineText'), + 'runs', + 0, + 'navigationEndpoint', + 'browseEndpoint', + 'browseId', + ), + ) + 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': 'youtube#video', - 'id': video_id, + 'kind': item_kind, + 'id': item_id, '_partial': True, 'snippet': { 'title': self.json_traverse( - video, + item, json_path.get('title') or ( ( ('title', 'runs', 0, 'text'), @@ -1231,23 +1524,14 @@ def get_browse_videos(self, ), ), 'thumbnails': self.json_traverse( - video, + item, json_path.get('thumbnails') or ( 'thumbnail', 'thumbnails' ), ), - 'channelId': channel_id or self.json_traverse( - video, - json_path.get('channel_id') or ( - ('longBylineText', 'shortBylineText'), - 'runs', - 0, - 'navigationEndpoint', - 'browseEndpoint', - 'browseId', - ), - ), + 'channelId': _channel_id, + 'playlistId': _playlist_id, } }) if not items: @@ -1263,10 +1547,8 @@ def get_browse_videos(self, ) if continuation: click_tracking = continuation.get('clickTrackingParams') - if click_tracking: - v3_response['clickTracking'] = click_tracking - page_token = self.json_traverse( + next_page_token = self.json_traverse( continuation, json_path.get('page_token') or ( ( @@ -1280,8 +1562,8 @@ def get_browse_videos(self, ), ), ) - if page_token: - v3_response['nextPageToken'] = page_token + if next_page_token == page_token: + next_page_token = None visitor = self.json_traverse( result, @@ -1290,14 +1572,44 @@ def get_browse_videos(self, 'visitorData', ), ) or visitor - if visitor: - v3_response['visitorData'] = visitor else: - v3_response['visitorData'] = visitor - v3_response['nextPageToken'] = page_token - v3_response['clickTracking'] = click_tracking - + next_page_token = None + + if items_per_page: + if items_per_page is True: + items_per_page = len(items) + max_results = self.max_results() - items_per_page + while next_page_token and len(items) <= max_results: + next_response = self.get_browse_items( + browse_id=browse_id, + channel_id=channel_id, + skip_ids=skip_ids, + params=params, + route=route, + data=data, + client=client, + do_auth=do_auth, + page_token=next_page_token, + click_tracking=click_tracking, + visitor=visitor, + items_per_page=None, + json_path=json_path, + ) + if not next_response: + break + next_items = next_response.get('items') + if next_items: + items.extend(next_items) + next_page_token = next_response.get('nextPageToken') + click_tracking = next_response.get('clickTracking') + visitor = next_response.get('visitorData') + + v3_response['nextPageToken'] = next_page_token v3_response['items'] = items + if click_tracking: + v3_response['clickTracking'] = click_tracking + if visitor: + v3_response['visitorData'] = visitor return v3_response def get_live_events(self, @@ -1318,6 +1630,7 @@ def get_live_events(self, # prepare params params = {'part': 'snippet', 'type': 'video', + 'q': '-|', 'order': order, 'eventType': event_type, 'regionCode': self._region, @@ -1339,23 +1652,21 @@ def get_live_events(self, if isinstance(after, string_type) and after.startswith('{'): after = json.loads(after) params['publishedAfter'] = ( - dt.yt_datetime_offset(**after) + yt_datetime_offset(**after) if isinstance(after, dict) else after ) - return self.api_request(method='GET', - path='search', + return self.api_request(method='GET', path='search', params=params, - no_login=True, **kwargs) def get_related_videos(self, video_id, - page_token='', - retry=0, - visitor=None, + page_token=None, click_tracking=None, + visitor=None, + retry=0, **kwargs): post_data = {'videoId': video_id} @@ -1374,13 +1685,16 @@ def get_related_videos(self, } post_data['context'] = context - result = self.api_request(client=('tv' if retry == 1 else - 'tv_embed' if retry == 2 else - 'v1'), - method='POST', - path='next', + related_client = ( + 'tv' + if retry == 1 else + 'tv_embed' + if retry == 2 else + 'v1' + ) + result = self.api_request(related_client, 'POST', path='next', post_data=post_data, - no_login=True) + do_auth=False) if not result: return None @@ -1410,7 +1724,7 @@ def get_related_videos(self, 2, 'shelfRenderer', 'content', - 'horizontalListRenderer', + ('horizontalListRenderer', 'verticalListRenderer'), 'items', ) if retry == 2 else ( 'contents', @@ -1561,7 +1875,8 @@ def get_related_videos(self, )), }, }) - elif content_type == 'LOCKUP_CONTENT_TYPE_PLAYLIST': + elif content_type in {'LOCKUP_CONTENT_TYPE_PLAYLIST', + 'LOCKUP_CONTENT_TYPE_PODCAST'}: items.append({ 'kind': 'youtube#playlist', 'id': new_content_id, @@ -1609,23 +1924,24 @@ def get_related_videos(self, }) if retry: - page_token = '' - click_tracking = '' + next_page_token = None + click_tracking = None else: continuation = related_videos[-1] - page_token = self.json_traverse(continuation, path=( + next_page_token = self.json_traverse(continuation, path=( 'continuationCommand', 'token', )) - if page_token: - click_tracking = continuation.get('clickTrackingParams', '') + if next_page_token and next_page_token != page_token: + click_tracking = continuation.get('clickTrackingParams') else: - click_tracking = '' + next_page_token = None + click_tracking = None v3_response = { 'kind': 'youtube#videoListResponse', 'items': items or [], - 'nextPageToken': page_token, + 'nextPageToken': next_page_token, 'visitorData': self.json_traverse(result, path=( 'responseContext', 'visitorData', @@ -1654,10 +1970,8 @@ def get_parent_comments(self, if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='commentThreads', + return self.api_request(method='GET', path='commentThreads', params=params, - no_login=True, **kwargs) def get_child_comments(self, @@ -1680,10 +1994,8 @@ def get_child_comments(self, if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='comments', + return self.api_request(method='GET', path='comments', params=params, - no_login=True, **kwargs) def get_channel_videos(self, channel_id, page_token='', **kwargs): @@ -1713,10 +2025,8 @@ def get_channel_videos(self, channel_id, page_token='', **kwargs): if page_token: params['pageToken'] = page_token - return self.api_request(method='GET', - path='search', + return self.api_request(method='GET', path='search', params=params, - no_login=True, **kwargs) def search(self, @@ -1832,10 +2142,8 @@ def search(self, params['locationRadius'] = settings.get_location_radius() params['type'] = 'video' - return self.api_request(method='GET', - path='search', + return self.api_request(method='GET', path='search', params=params, - no_login=True, **kwargs) def search_with_params(self, @@ -1909,7 +2217,7 @@ def search_with_params(self, if isinstance(published, string_type) and published.startswith('{'): published = json.loads(published) search_params['publishedBefore'] = ( - dt.yt_datetime_offset(**published) + yt_datetime_offset(**published) if isinstance(published, dict) else published ) @@ -1919,7 +2227,7 @@ def search_with_params(self, if isinstance(published, string_type) and published.startswith('{'): published = json.loads(published) search_params['publishedAfter'] = ( - dt.yt_datetime_offset(**published) + yt_datetime_offset(**published) if isinstance(published, dict) else published ) @@ -1939,19 +2247,16 @@ def search_with_params(self, search_params['type'] = 'video' return (params, - self.api_request(method='GET', - path='search', + self.api_request(method='GET', path='search', params=search_params, - no_login=True, **kwargs)) def get_my_subscriptions(self, page_token=1, - logged_in=False, do_filter=False, feed_type='videos', refresh=False, - use_cache=True, + force_cache=False, progress_dialog=None, **kwargs): """ @@ -2006,7 +2311,7 @@ def _sort_by_date_time(item, limits): if '_timestamp' in item: timestamp = item['_timestamp'] else: - timestamp = dt.since_epoch(item['snippet'].get('publishedAt')) + timestamp = since_epoch(item['snippet'].get('publishedAt')) item['_timestamp'] = timestamp return timestamp @@ -2023,11 +2328,11 @@ def _sort_by_date_time(item, limits): playlist_ids = threaded_output['playlist_ids'] for item_id, item in bookmarks.items(): if isinstance(item, DirectoryItem): - item_id = getattr(item, 'playlist_id', None) + item_id = getattr(item, PLAYLIST_ID, None) if item_id: playlist_ids.append(item_id) continue - item_id = getattr(item, 'channel_id', None) + item_id = getattr(item, CHANNEL_ID, None) elif not isinstance(item, float): continue if item_id: @@ -2113,22 +2418,25 @@ def _get_feed(output, return True, False response = self.request( - ''.join(( - 'https://www.youtube.com/feeds/videos.xml?playlist_id=', - item_id, - )), + ''.join((self.BASE_URL, + '/feeds/videos.xml?playlist_id=', + item_id)), headers=headers, ) if response is None: return False, True - elif response.status_code == 404: - response = None - elif response.status_code == 429: - return False, True + with response: + if response.status_code == 404: + content = None + elif response.status_code == 429: + return False, True + else: + response.encoding = 'utf-8' + content = response.content _output = { 'channel_id': channel_id, - 'content': response, + 'content': content, 'refresh': True, } @@ -2149,7 +2457,6 @@ def _parse_feeds(feeds, sort_method, sort_limits, progress_dialog=None, - utf8=context.get_system_version().compatible(19), filters=channel_filters, ns=namespaces, feed_history=feed_history, @@ -2162,8 +2469,8 @@ def _parse_feeds(feeds, ) dict_get = {}.get - find = ET.Element.find - findtext = ET.Element.findtext + find = ET_Element.find + findtext = ET_Element.findtext all_items = {} new_cache = {} @@ -2175,10 +2482,7 @@ def _parse_feeds(feeds, content = feed.get('content') if refresh_feed and content: - content.encoding = 'utf-8' - content = to_unicode(content.content).replace('\n', '') - - root = ET.fromstring(content if utf8 else to_str(content)) + root = ET_XML(content) channel_name = findtext( root, 'atom:author/atom:name', @@ -2211,7 +2515,7 @@ def _parse_feeds(feeds, '', ns, ), - 'publishedAt': dt.strptime( + 'publishedAt': strptime( findtext(item, 'atom:published', '', ns) ), }, @@ -2291,18 +2595,20 @@ def _threaded_fetch(kwargs, pool_id, check_inputs, **_kwargs): + active_thread_ids = threads['active_thread_ids'] + thread_id = threading.current_thread().ident + active_thread_ids.add(thread_id) counts = threads['counts'] complete = False while not threads['balance'].is_set(): - threads['loop'].set() + threads['loop_enable'].set() if kwargs is True: _kwargs = {} elif kwargs: _kwargs = {'inputs': kwargs} if do_batch else kwargs.pop() elif check_inputs: - if check_inputs.wait(0.1): - if kwargs: - continue + if check_inputs.wait(0.1) and kwargs: + continue break else: complete = True @@ -2310,13 +2616,8 @@ def _threaded_fetch(kwargs, try: success, complete = worker(output, **_kwargs) - except Exception as exc: - msg = ('get_my_subscriptions._threaded_fetch - Error' - '\n\tException: {exc!r}' - '\n\tStack trace (most recent call last):\n{stack}' - .format(exc=exc, - stack=format_stack())) - context.log_error(msg) + except Exception: + self.log.exception('Error') continue if complete or not success or not counts[pool_id]: @@ -2330,27 +2631,27 @@ def _threaded_fetch(kwargs, elif counts[pool_id]: counts[pool_id] -= 1 counts['all'] -= 1 - threads['current'].discard(threading.current_thread()) - threads['loop'].set() + threads['active_thread_ids'].discard(thread_id) + threads['loop_enable'].set() max_threads = min(32, 2 * (available_cpu_count() + 4)) counts = { 'all': 0, } - current_threads = set() + active_thread_ids = set() counter = threading.Semaphore(max_threads) balance_enable = threading.Event() loop_enable = threading.Event() threads = { 'balance': balance_enable, - 'loop': loop_enable, + 'loop_enable': loop_enable, 'counter': counter, 'counts': counts, - 'current': current_threads, + 'active_thread_ids': active_thread_ids, } payloads = {} - if logged_in: + if self.logged_in: function_cache = context.get_function_cache() channel_params = { @@ -2410,17 +2711,16 @@ def _get_updated_subscriptions(new_data, old_data): def _get_channels(output, _params=channel_params, - _refresh=(refresh or not use_cache), + _refresh=refresh, + _force_cache=force_cache, function_cache=function_cache): json_data = function_cache.run( - self.api_request, - function_cache.ONE_HOUR - if 'pageToken' in _params else - 5 * function_cache.ONE_MINUTE, + self.api_request, method='GET', path='subscriptions', + seconds=(function_cache.ONE_HOUR + if _force_cache or 'pageToken' in _params else + 5 * function_cache.ONE_MINUTE), _refresh=_refresh, _process=_get_updated_subscriptions, - method='GET', - path='subscriptions', params=_params, **kwargs ) @@ -2452,11 +2752,14 @@ def _get_channels(output, # # def _get_playlists(output, # _params=playlist_params, - # _refresh=(refresh or not use_cache), + # _refresh=refresh, + # _force_cache=force_cache, # function_cache=function_cache): # json_data = function_cache.run( # self.get_saved_playlists, - # function_cache.ONE_HOUR, + # function_cache.ONE_HOUR + # if _force_cache or 'pageToken' in _params else + # 5 * function_cache.ONE_MINUTE, # _refresh=_refresh, # **kwargs # ) @@ -2529,17 +2832,18 @@ def _get_channels(output, remaining = payloads.keys() iterator = iter(payloads) loop_enable.set() - while loop_enable.wait(): + while loop_enable.wait(1) or active_thread_ids: try: pool_id = next(iterator) except StopIteration: - loop_enable.clear() - if not current_threads: - break + if active_thread_ids: + loop_enable.clear() for pool_id in completed: del payloads[pool_id] - completed = [] remaining = payloads.keys() + if not remaining and not active_thread_ids: + break + completed = [] iterator = iter(payloads) if progress_dialog: progress_dialog.grow_total( @@ -2584,7 +2888,6 @@ def _get_channels(output, kwargs=payload, ) new_thread.daemon = True - current_threads.add(new_thread) counts[pool_id] += 1 counts['all'] += 1 counter.acquire(True) @@ -2611,189 +2914,67 @@ def _get_channels(output, v3_response['_item_filter'] = item_filter return v3_response - def get_saved_playlists(self, page_token, offset): - if not page_token: - page_token = '' - - result = {'items': [], - 'next_page_token': page_token, - 'offset': offset} - - def _perform(_playlist_idx, _page_token, _offset, _result): - _post_data = { - 'context': { - 'client': { - 'clientName': 'TVHTML5', - 'clientVersion': '5.20150304', - 'theme': 'CLASSIC', - 'acceptRegion': '%s' % self._region, - 'acceptLanguage': '%s' % self._language.replace('_', '-') - }, - 'user': { - 'enableSafetyMode': False - } - } - } - if _page_token: - _post_data['continuation'] = _page_token - else: - _post_data['browseId'] = 'FEmy_youtube' - - _json_data = self.api_request(client='v1', - method='POST', - path='browse', - post_data=_post_data) - _data = {} - if 'continuationContents' in _json_data: - _data = (_json_data.get('continuationContents', {}) - .get('horizontalListContinuation', {})) - elif 'contents' in _json_data: - _data = (_json_data.get('contents', {}) - .get('sectionListRenderer', {}) - .get('contents', [{}])[_playlist_idx] - .get('shelfRenderer', {}) - .get('content', {}) - .get('horizontalListRenderer', {})) - - _items = _data.get('items', []) - if not _result: - _result = {'items': []} - - _new_offset = self.max_results() - len(_result['items']) + _offset - if _offset > 0: - _items = _items[_offset:] - _result['offset'] = _new_offset - - for _item in _items: - _item = _item.get('gridPlaylistRenderer', {}) - if _item: - _video_item = { - 'id': _item['playlistId'], - 'title': (_item.get('title', {}) - .get('runs', [{}])[0] - .get('text', '')), - 'channel': (_item.get('shortBylineText', {}) - .get('runs', [{}])[0] - .get('text', '')), - 'channel_id': (_item.get('shortBylineText', {}) - .get('runs', [{}])[0] - .get('navigationEndpoint', {}) - .get('browseEndpoint', {}) - .get('browseId', '')), - 'thumbnails': (_item.get('thumbnail', {}) - .get('thumbnails', [{}])), - } - - _result['items'].append(_video_item) - - _continuations = (_data.get('continuations', [{}])[0] - .get('nextContinuationData', {}) - .get('continuation', '')) - if _continuations and len(_result['items']) <= self.max_results(): - _result['next_page_token'] = _continuations - - if len(_result['items']) < self.max_results(): - _result = _perform(_playlist_idx=playlist_index, - _page_token=_continuations, - _offset=0, - _result=_result) - - # trim result - if len(_result['items']) > self.max_results(): - _items = _result['items'] - _items = _items[:self.max_results()] - _result['items'] = _items - _result['continue'] = True - - if len(_result['items']) < self.max_results(): - if 'continue' in _result: - del _result['continue'] - - if 'next_page_token' in _result: - del _result['next_page_token'] - - if 'offset' in _result: - del _result['offset'] - - return _result - - _en_post_data = { - 'context': { - 'client': { - 'clientName': 'TVHTML5', - 'clientVersion': '5.20150304', - 'theme': 'CLASSIC', - 'acceptRegion': 'US', - 'acceptLanguage': 'en-US' - }, - 'user': { - 'enableSafetyMode': False - } - }, - 'browseId': 'FEmy_youtube' - } - - playlist_index = None - json_data = self.api_request(client='v1', - method='POST', - path='browse', - post_data=_en_post_data) - contents = (json_data.get('contents', {}) - .get('sectionListRenderer', {}) - .get('contents', [{}])) - - for idx, shelf in enumerate(contents): - title = (shelf.get('shelfRenderer', {}) - .get('title', {}) - .get('runs', [{}])[0] - .get('text', '')) - if title.lower() == 'saved playlists': - playlist_index = idx - break - - if playlist_index is not None: - contents = (json_data.get('contents', {}) - .get('sectionListRenderer', {}) - .get('contents', [{}])) - if 0 <= playlist_index < len(contents): - result = _perform(_playlist_idx=playlist_index, - _page_token=page_token, - _offset=offset, - _result=result) - - return result - - def _response_hook(self, **kwargs): - response = kwargs['response'] - if kwargs.get('extended_debug'): - self._context.log_debug('API response: |{0.status_code}|' - '\n\tHeaders: |{0.headers}|' - '\n\tContent: |{0.text}|' - .format(response)) + def _auth_required(self, params): + if params: + if params.get('mine') or params.get('forMine'): + return True + request_channel_id = params.get('channelId') + if request_channel_id == 'mine': + return True else: - self._context.log_debug('API response: |{0.status_code}|' - '\n\tHeaders: |{0.headers}|' - .format(response)) + request_channel_id = None - if response.status_code == 204 and 'no_content' in kwargs: + uri_channel_id = self._context.get_param(CHANNEL_ID) + if uri_channel_id == 'mine': return True - try: - json_data = response.json() - except ValueError as exc: - kwargs.setdefault('raise_exc', True) - raise InvalidJSON(exc, **kwargs) + channel_id = self.channel_id + if channel_id and channel_id in (uri_channel_id, request_channel_id): + return True + return False - if 'error' in json_data: - kwargs.setdefault('pass_data', True) - raise YouTubeException('"error" in response JSON data', - json_data=json_data, - **kwargs) + def _request_response_hook(self, **kwargs): + response = kwargs['response'] + if response is None: + return None, None + with response: + headers = response.headers + if kwargs.get('extended_debug'): + self.log.debug(('Request response', + 'Status: {response.status_code!r}', + 'Headers: {headers!r}', + 'Content: {response.text}'), + response=response, + headers=headers._store if headers else None, + stacklevel=4) + else: + self.log.debug(('Request response', + 'Status: {response.status_code!r}', + 'Headers: {headers!r}'), + response=response, + headers=headers._store if headers else None, + stacklevel=4) - response.raise_for_status() - return json_data + if response.status_code == 204 and 'no_content' in kwargs: + return None, True - def _error_hook(self, **kwargs): + try: + json_data = response.json() + except ValueError as exc: + if kwargs.get('raise_exc') is None: + kwargs['raise_exc'] = True + raise InvalidJSON(exc, **kwargs) + + if 'error' in json_data: + kwargs.setdefault('pass_data', True) + raise YouTubeException('"error" in response JSON data', + json_data=json_data, + **kwargs) + + response.raise_for_status() + return json_data.get('etag'), json_data + + def _request_error_hook(self, **kwargs): exc = kwargs['exc'] json_data = getattr(exc, 'json_data', None) if getattr(exc, 'pass_data', False): @@ -2806,43 +2987,51 @@ def _error_hook(self, **kwargs): exception = None if not json_data or 'error' not in json_data: - info = ('Request - Failed' - '\n\tException: {exc!r}') - details = kwargs - return None, info, details, data, None, exception + return 'API request error', None, None, data, exception details = json_data['error'] reason = details.get('errors', [{}])[0].get('reason', 'Unknown') message = strip_html_from_text(details.get('message', 'Unknown error')) if getattr(exc, 'notify', True): + context = self._context ok_dialog = False - timeout = 5000 if reason in {'accessNotConfigured', 'forbidden'}: - notification = self._context.localize('key.requirement') + notification = context.localize('key.requirement') ok_dialog = True elif reason == 'keyInvalid' and message == 'Bad Request': - notification = self._context.localize('api.key.incorrect') - timeout = 7000 + notification = context.localize('api.key.incorrect') elif reason in {'quotaExceeded', 'dailyLimitExceeded'}: notification = message - timeout = 7000 + elif reason == 'authError': + auth_type = kwargs.get('_auth_type') + if auth_type: + if auth_type in self._access_tokens: + self._access_tokens[auth_type] = None + self.set_access_token(self._access_tokens) + context.get_access_manager().update_access_token( + context.get_param('addon_id'), + access_token=self.convert_access_tokens(to_list=True), + ) + notification = message else: notification = message - title = ': '.join((self._context.get_name(), reason)) + title = ': '.join((context.get_name(), reason)) if ok_dialog: - self._context.get_ui().on_ok(title, notification) + context.get_ui().on_ok(title, notification) else: - self._context.get_ui().show_notification(notification, - title, - time_ms=timeout) + context.get_ui().show_notification(notification, title) - info = ('API error - {reason}' - '\n\tException: {exc!r}' - '\n\tMessage: {message}') - details = {'reason': reason, 'message': message} - return '', info, details, data, False, exception + info = ( + 'Reason: {error_reason}', + 'Message: {error_message}', + ) + details = { + 'error_reason': reason, + 'error_message': message, + } + return 'API request error', info, details, data, exception def api_request(self, client='v3', @@ -2853,7 +3042,8 @@ def api_request(self, params=None, post_data=None, headers=None, - no_login=False, + do_auth=None, + cache=None, **kwargs): if not client_data: client_data = {} @@ -2868,29 +3058,38 @@ def api_request(self, if post_data: client_data['json'] = post_data clear_data = False + if do_auth is None: + do_auth = True else: + if do_auth is None and method == 'DELETE': + do_auth = True clear_data = True if params: - if params.get('mine') or params.get('forMine'): - no_login = False client_data['params'] = params - abort = False - if not no_login: - client_data.setdefault('_auth_requested', True) - # a config can decide if a token is allowed - if self._access_token and self._config.get('token-allowed', True): - client_data['_access_token'] = self._access_token - if self._access_token_tv: - client_data['_access_token_tv'] = self._access_token_tv + if do_auth is None: + do_auth = self._auth_required(params) + if do_auth: + abort = not self.logged_in + client_data.setdefault('_auth_required', do_auth) + else: + abort = False + + client_data['_access_tokens'] = access_tokens = {} + client_data['_api_keys'] = api_keys = {} + for config_type, config in self._configs.items(): + if not config: + continue - key = self._config.get('key') - if key: - client_data['_api_key'] = key + key = config.get('key') + if key: + api_keys[config_type] = key - key = self._config_tv.get('key') - if key: - client_data['_api_key_tv'] = key + if not config.get('token-allowed', True): + continue + access_token = self._access_tokens.get(config_type) + if access_token: + access_tokens[config_type] = access_token client = self.build_client(client, client_data) if not client: @@ -2908,12 +3107,14 @@ def api_request(self, key = params['key'] if key: abort = False - log_params['key'] = '...'.join((key[:3], key[-3:])) + log_params['key'] = ('...'.join((key[:3], key[-3:])) + if len(key) > 9 else + '...') elif not client['_has_auth']: abort = True if 'location' in params: - log_params['location'] = '|xx.xxxx,xx.xxxx|' + log_params['location'] = 'xx.xxxx,xx.xxxx' else: log_params = None @@ -2921,35 +3122,41 @@ def api_request(self, if headers: log_headers = headers.copy() if 'Authorization' in log_headers: - log_headers['Authorization'] = '|logged in|' + log_headers['Authorization'] = '' else: log_headers = None context = self._context - context.log_debug('API request:' - '\n\ttype: |{type}|' - '\n\tmethod: |{method}|' - '\n\tpath: |{path}|' - '\n\tparams: |{params}|' - '\n\tpost_data: |{data}|' - '\n\theaders: |{headers}|' - .format(type=client.get('_name'), - method=method, - path=path, - params=log_params, - data=client.get('json'), - headers=log_headers)) + self.log.debug(('{request_name} API request', + 'method: {method!r}', + 'path: {path!r}', + 'params: {params!r}', + 'post_data: {data!r}', + 'headers: {headers!r}'), + request_name=client.get('_name'), + method=method, + path=path, + params=log_params, + data=client.get('json'), + headers=log_headers, + stacklevel=2) if abort: if kwargs.get('notify', True): context.get_ui().on_ok( context.get_name(), context.localize('key.requirement'), ) - context.log_warning('API request: aborted') + self.log.warning('Aborted', stacklevel=2) return {} - if context.get_settings().logging_enabled() & 2: + if context.get_settings().log_level() & 2: kwargs.setdefault('extended_debug', True) - return self.request(response_hook=self._response_hook, - response_hook_kwargs=kwargs, - error_hook=self._error_hook, + if cache is None and 'no_content' in kwargs: + cache = False + elif cache is not False and self._context.refresh_requested(): + cache = 'refresh' + return self.request(response_hook=self._request_response_hook, + event_hook_kwargs=kwargs, + error_hook=self._request_error_hook, + stacklevel=3, + cache=cache, **client) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/login_client.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/login_client.py index f1e9a8ca78..79be5bccd1 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,82 +11,149 @@ from __future__ import absolute_import, division, unicode_literals from .request_client import YouTubeRequestClient -from ..youtube_exceptions import ( - InvalidGrant, - InvalidJSON, - LoginException, -) +from ..youtube_exceptions import InvalidGrant, LoginException +from ...kodion import logging -class LoginClient(YouTubeRequestClient): - ANDROID_CLIENT_AUTH_URL = 'https://android.clients.google.com/auth' +class YouTubeLoginClient(YouTubeRequestClient): + log = logging.getLogger(__name__) + + 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', - 'tv': 'tv', - 1: 'personal', - 'personal': 'personal', + 'tv': 0, + 1: 'user', + 'user': 1, + 2: 'vr', + 'vr': 2, + 3: 'dev', + 'dev': 3, + } + + _configs = { + 'dev': {}, + 'user': {}, + 'tv': {}, + 'vr': {}, + } + _access_tokens = { + 'dev': None, + 'user': None, + 'tv': None, + 'vr': None, } + _initialised = False + _logged_in = False def __init__(self, configs=None, - access_token='', - access_token_tv='', + access_tokens=None, **kwargs): + super(YouTubeLoginClient, self).__init__( + exc_type=LoginException, + **kwargs + ) + YouTubeLoginClient.init(configs) + self.set_access_token(access_tokens) + self.initialised = any(self._configs.values()) + + @classmethod + def init(cls, configs=None, **_kwargs): + _configs = cls._configs if not configs: - configs = {} - self._config = configs.get('main') or {} - self._config_tv = configs.get('youtube-tv') or {} + return + for config_type, config in configs.items(): + if config_type in _configs: + _configs[config_type] = config + + def reinit(self, **kwargs): + super(YouTubeLoginClient, self).reinit(**kwargs) + + @classmethod + def convert_access_tokens(cls, + access_tokens=None, + to_dict=False, + to_list=False): + if access_tokens is None: + access_tokens = cls._access_tokens + if to_dict or isinstance(access_tokens, (list, tuple)): + access_tokens = { + cls.TOKEN_TYPES[token_idx]: token + for token_idx, token in enumerate(access_tokens) + if token and token_idx in cls.TOKEN_TYPES + } + elif to_list or isinstance(access_tokens, dict): + _access_tokens = [None, None, None, None] + for token_type, token in access_tokens.items(): + token_idx = cls.TOKEN_TYPES.get(token_type) + if token_idx is None: + continue + _access_tokens[token_idx] = token + access_tokens = _access_tokens + return access_tokens + + def set_access_token(self, access_tokens=None): + existing_access_tokens = type(self)._access_tokens + if access_tokens: + if isinstance(access_tokens, (list, tuple)): + access_tokens = self.convert_access_tokens( + access_tokens, + to_dict=True, + ) + token_status = 0 + for token_type, token in existing_access_tokens.items(): + if token_type in access_tokens: + token = access_tokens[token_type] + existing_access_tokens[token_type] = token + if token or token_type == 'dev': + token_status |= 1 + else: + token_status |= 2 + + self.logged_in = ( + 'partially' + if token_status & 2 else + 'fully' + if token_status & 1 else + False + ) + self.log.info('User is %s logged in', self.logged_in or 'not') + else: + for token_type in existing_access_tokens: + existing_access_tokens[token_type] = None + self.logged_in = False + self.log.info('User is not logged in') - self._access_token = access_token - self._access_token_tv = access_token_tv + @property + def initialised(self): + return self._initialised - super(LoginClient, self).__init__(exc_type=LoginException, **kwargs) + @initialised.setter + def initialised(self, value): + type(self)._initialised = value - @staticmethod - def _response_hook(**kwargs): - response = kwargs['response'] - try: - json_data = response.json() - if 'error' in json_data: - json_data.setdefault('code', response.status_code) - raise LoginException('"error" in response JSON data', - json_data=json_data, - response=response) - except ValueError as exc: - raise InvalidJSON(exc, response=response) - response.raise_for_status() - return json_data + @property + def logged_in(self): + return self._logged_in + + @logged_in.setter + def logged_in(self, value): + type(self)._logged_in = value @staticmethod - def _error_hook(**kwargs): + def _login_error_hook(**kwargs): json_data = getattr(kwargs['exc'], 'json_data', None) if not json_data or 'error' not in json_data: - return None, None, None, None, None, LoginException + return None, None, None, None, LoginException if json_data['error'] == 'authorization_pending': - return None, None, None, json_data, False, False + return None, None, None, json_data, False if (json_data['error'] == 'invalid_grant' and json_data.get('code') == 400): - return None, None, None, json_data, False, InvalidGrant(json_data) - return None, None, None, json_data, False, LoginException(json_data) - - def set_access_token(self, personal=None, tv=None): - if personal is not None: - self._access_token = personal - if tv is not None: - self._access_token_tv = tv + return None, None, None, json_data, InvalidGrant(json_data) + return None, None, None, json_data, LoginException(json_data) def revoke(self, refresh_token): # https://developers.google.com/youtube/v3/guides/auth/devices @@ -98,25 +165,23 @@ def revoke(self, refresh_token): post_data = {'token': refresh_token} - self.request(self.REVOKE_URL, - method='POST', - data=post_data, - headers=headers, - response_hook=LoginClient._response_hook, - error_hook=LoginClient._error_hook, - error_title='Logout Failed', - error_info=('Revoke - Failed' - '\n\tException: {exc!r}'), - raise_exc=True) + self.request( + self.REVOKE_URL, + method='POST', + data=post_data, + headers=headers, + response_hook=self._response_hook_json, + error_hook=self._login_error_hook, + error_title='Logout failed - Refresh token revocation error', + raise_exc=True, + ) def refresh_token(self, token_type, refresh_token=None): login_type = self.TOKEN_TYPES.get(token_type) - if login_type == 'tv': - client_id = self._config_tv.get('id') - client_secret = self._config_tv.get('secret') - elif login_type == 'personal': - client_id = self._config.get('id') - client_secret = self._config.get('secret') + config = self._configs.get(login_type) + if config: + client_id = config.get('id') + client_secret = config.get('secret') else: return None if not client_id or not client_secret or not refresh_token: @@ -134,40 +199,47 @@ def refresh_token(self, token_type, refresh_token=None): 'refresh_token': refresh_token, 'grant_type': 'refresh_token'} - config_type = self._get_config_type(client_id, client_secret) - client_id.replace('.apps.googleusercontent.com', '') - client = (('\n\tconfig_type: |{config_type}|' - '\n\tclient_id: |{id_start}...{id_end}|' - '\n\tclient_secret: |{secret_start}...{secret_end}|') - .format(config_type=config_type, - id_start=client_id[:3], - id_end=client_id[-3:], - secret_start=client_secret[:3], - secret_end=client_secret[-3:])) - self.log_debug('Refresh token:{0}'.format(client)) - - json_data = self.request(self.TOKEN_URL, - method='POST', - data=post_data, - headers=headers, - response_hook=LoginClient._response_hook, - error_hook=LoginClient._error_hook, - error_title='Login Failed', - error_info=('Refresh token - Failed' - '\n\tException: {{exc!r}}' - '{client}' - .format(client=client)), - raise_exc=True) + client_id.replace(self.DOMAIN_SUFFIX, '') + log_info = ('Login type: {login_type!r}', + 'client_id: {client_id!r}', + 'client_secret: {client_secret!r}') + log_params = { + 'login_type': login_type, + 'client_id': '...', + 'client_secret': '...', + } + if len(client_id) > 11: + log_params['client_id'] = '...'.join(( + client_id[:3], + client_id[-5:], + )) + if len(client_secret) > 9: + log_params['client_secret'] = '...'.join(( + client_secret[:3], + client_secret[-3:], + )) + self.log.debug(('Refresh token:',) + log_info, **log_params) + + json_data = self.request( + self.TOKEN_URL, + method='POST', + data=post_data, + headers=headers, + response_hook=self._response_hook_json, + error_hook=self._login_error_hook, + error_title='Login failed - Refresh token grant error', + error_info=log_info, + raise_exc=True, + **log_params + ) return json_data def request_access_token(self, token_type, code=None): login_type = self.TOKEN_TYPES.get(token_type) - if login_type == 'tv': - client_id = self._config_tv.get('id') - client_secret = self._config_tv.get('secret') - elif login_type == 'personal': - client_id = self._config.get('id') - client_secret = self._config.get('secret') + config = self._configs.get(login_type) + if config: + client_id = config.get('id') + client_secret = config.get('secret') else: return None if not client_id or not client_secret or not code: @@ -185,38 +257,46 @@ def request_access_token(self, token_type, code=None): 'code': code, 'grant_type': 'http://oauth.net/grant_type/device/1.0'} - config_type = self._get_config_type(client_id, client_secret) - client_id.replace('.apps.googleusercontent.com', '') - client = (('\n\tconfig_type: |{config_type}|' - '\n\tclient_id: |{id_start}...{id_end}|' - '\n\tclient_secret: |{secret_start}...{secret_end}|') - .format(config_type=config_type, - id_start=client_id[:3], - id_end=client_id[-3:], - secret_start=client_secret[:3], - secret_end=client_secret[-3:])) - self.log_debug('Requesting access token:{0}'.format(client)) - - json_data = self.request(self.TOKEN_URL, - method='POST', - data=post_data, - headers=headers, - response_hook=LoginClient._response_hook, - error_hook=LoginClient._error_hook, - error_title='Login Failed: Unknown response', - error_info=('Access token request - Failed' - '\n\tException: {{exc!r}}' - '{client}' - .format(client=client)), - raise_exc=True) + client_id.replace(self.DOMAIN_SUFFIX, '') + log_info = ('Login type: {login_type!r}', + 'client_id: {client_id!r}', + 'client_secret: {client_secret!r}') + log_params = { + 'login_type': login_type, + 'client_id': '...', + 'client_secret': '...', + } + if len(client_id) > 11: + log_params['client_id'] = '...'.join(( + client_id[:3], + client_id[-5:], + )) + if len(client_secret) > 9: + log_params['client_secret'] = '...'.join(( + client_secret[:3], + client_secret[-3:], + )) + self.log.debug(('Access token request:',) + log_info, **log_params) + + json_data = self.request( + self.TOKEN_URL, + method='POST', + data=post_data, + headers=headers, + response_hook=self._response_hook_json, + error_hook=self._login_error_hook, + error_title='Login failed - Access token request error', + error_info=log_info, + raise_exc=True, + **log_params + ) return json_data def request_device_and_user_code(self, token_type): login_type = self.TOKEN_TYPES.get(token_type) - if login_type == 'tv': - client_id = self._config_tv.get('id') - elif login_type == 'personal': - client_id = self._config.get('id') + config = self._configs.get(login_type) + if config: + client_id = config.get('id') else: return None if not client_id: @@ -232,54 +312,30 @@ def request_device_and_user_code(self, token_type): post_data = {'client_id': client_id, 'scope': 'https://www.googleapis.com/auth/youtube'} - config_type = self._get_config_type(client_id) - client_id.replace('.apps.googleusercontent.com', '') - client = (('\n\tconfig_type: |{config_type}|' - '\n\tclient_id: |{id_start}...{id_end}|') - .format(config_type=config_type, - id_start=client_id[:3], - id_end=client_id[-3:])) - self.log_debug('Requesting device and user code:{0}'.format(client)) - - json_data = self.request(self.DEVICE_CODE_URL, - method='POST', - data=post_data, - headers=headers, - response_hook=LoginClient._response_hook, - error_hook=LoginClient._error_hook, - error_title='Login Failed: Unknown response', - error_info=('Device/user code request - Failed' - '\n\tException: {{exc!r}}' - '{client}' - .format(client=client)), - raise_exc=True) + client_id.replace(self.DOMAIN_SUFFIX, '') + log_info = ('Login type: {login_type!r}', + 'client_id: {client_id!r}') + log_params = { + 'login_type': login_type, + 'client_id': '...', + } + if len(client_id) > 11: + log_params['client_id'] = '...'.join(( + client_id[:3], + client_id[-5:], + )) + self.log.debug(('Device/user code request:',) + log_info, **log_params) + + json_data = self.request( + self.DEVICE_CODE_URL, + method='POST', + data=post_data, + headers=headers, + response_hook=self._response_hook_json, + error_hook=self._login_error_hook, + error_title='Login failed - Device/user code request error', + error_info=log_info, + raise_exc=True, + **log_params + ) return json_data - - def _get_config_type(self, client_id, client_secret=None): - """used for logging""" - if client_secret is None: - config_id = self._config_tv.get('id') - using_conf_tv = config_id and client_id == config_id - config_id = self._config.get('id') - using_conf_main = config_id and client_id == config_id - else: - config_secret = self._config_tv.get('secret') - config_id = self._config_tv.get('id') - using_conf_tv = ( - config_secret and client_secret == config_secret - and config_id and client_id == config_id - ) - config_secret = self._config.get('secret') - config_id = self._config.get('id') - using_conf_main = ( - config_secret and client_secret == config_secret - and config_id and client_id == config_id - ) - - if not using_conf_main and not using_conf_tv: - return 'None' - if using_conf_tv: - return 'YouTube-TV' - if using_conf_main: - return 'YouTube-Kodi' - return 'Unknown' diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/player_client.py similarity index 72% rename from plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/stream_info.py rename to plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/player_client.py index cd2725f10c..317329f822 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/player_client.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-present plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -14,14 +14,15 @@ from json import dumps as json_dumps, loads as json_loads from os import path as os_path from random import choice as random_choice -from re import compile as re_compile +from re import compile as re_compile, sub as re_sub -from .ratebypass import ratebypass -from .signature.cipher import Cipher +from .data_client import YouTubeDataClient from .subtitles import SUBTITLE_SELECTIONS, Subtitles -from .utils import THUMB_TYPES -from ..client.request_client import YouTubeRequestClient -from ..youtube_exceptions import InvalidJSON, YouTubeException +from ..helper.ratebypass import ratebypass +from ..helper.signature.cipher import Cipher +from ..helper.utils import THUMB_TYPES, THUMB_URL +from ..youtube_exceptions import YouTubeException +from ...kodion import logging from ...kodion.compatibility import ( entity_escape, parse_qs, @@ -29,23 +30,21 @@ unescape, unquote, urlencode, - urljoin, urlsplit, urlunsplit, xbmcvfs, ) -from ...kodion.constants import PATHS, TEMP_PATH +from ...kodion.constants import INCOGNITO, PATHS, TEMP_PATH, VALUE_TO_STR from ...kodion.network import get_connect_address -from ...kodion.utils import ( - format_stack, - make_dirs, - merge_dicts, - redact_ip_in_uri, -) -from ...kodion.utils.datetime_parser import fromtimestamp +from ...kodion.utils.datetime import fromtimestamp +from ...kodion.utils.file_system import make_dirs +from ...kodion.utils.methods import merge_dicts +from ...kodion.utils.redact import redact_ip_in_uri + +class YouTubePlayerClient(YouTubeDataClient): + log = logging.getLogger(__name__) -class StreamInfo(YouTubeRequestClient): BASE_PATH = make_dirs(TEMP_PATH) FORMAT = { @@ -228,9 +227,15 @@ class StreamInfo(YouTubeRequestClient): '136': {'container': 'mp4', 'dash/video': True, 'video': {'height': 720, 'codec': 'h.264'}}, + '214': {'container': 'mp4', + 'dash/video': True, + 'video': {'height': 720, 'codec': 'h.264'}}, '137': {'container': 'mp4', 'dash/video': True, 'video': {'height': 1080, 'codec': 'h.264'}}, + '216': {'container': 'mp4', + 'dash/video': True, + 'video': {'height': 1080, 'codec': 'h.264'}}, '138': {'container': 'mp4', # Discontinued 'discontinued': True, 'dash/video': True, @@ -271,6 +276,10 @@ class StreamInfo(YouTubeRequestClient): '248': {'container': 'webm', 'dash/video': True, 'video': {'height': 1080, 'codec': 'vp9'}}, + '356': {'container': 'webm', + 'title': 'Premium 1080p', + 'dash/video': True, + 'video': {'height': 1080, 'codec': 'vp9'}}, '779': {'container': 'webm', 'title': '1080p vertical', 'dash/video': True, @@ -281,6 +290,11 @@ class StreamInfo(YouTubeRequestClient): 'dash/video': True, 'fps': 30, 'video': {'height': 480, 'width': 1080, 'codec': 'vp9'}}, + '788': {'container': 'mp4', + 'title': '608p', + 'dash/video': True, + 'fps': 30, + 'video': {'height': 608, 'width': 1080, 'codec': 'av1'}}, '264': {'container': 'mp4', 'dash/video': True, 'video': {'height': 1440, 'codec': 'h.264'}}, @@ -405,6 +419,7 @@ class StreamInfo(YouTubeRequestClient): 'fps': 30, 'video': {'height': 4320, 'codec': 'av1'}}, '571': {'container': 'mp4', + 'title': 'Premium 4k', 'dash/video': True, 'fps': 30, 'video': {'height': 4320, 'codec': 'av1'}}, @@ -539,6 +554,10 @@ class StreamInfo(YouTubeRequestClient): 'title': '720p', 'hls/video': True, 'video': {'height': 720, 'codec': 'h.264'}}, + '379': {'container': 'hls', + 'title': 'Premium 720p', + 'hls/video': True, + 'video': {'height': 720, 'codec': 'h.264'}}, '269': {'container': 'hls', 'title': '144p', 'hls/video': True, @@ -762,37 +781,55 @@ class StreamInfo(YouTubeRequestClient): } LANG_ROLE_DETAILS = { - 4: ('original', 'main', -1), - 3: ('dub', 'dub', -2), - 6: ('secondary', 'alternate', -3), - 10: ('dub.auto', 'dub', -4), - 2: ('descriptive', 'description', -5), - 0: ('alt', 'alternate', -6), - -1: ('original', 'main', -6), + '4': ('original', 'main', -1), + '3': ('dub', 'dub', -2), + '6': ('secondary', 'alternate', -3), + '10': ('dub.auto', 'dub', -4), + '2': ('descriptive', 'description', -5), + '0': ('alt', 'alternate', -6), + '-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, - access_token='', - access_token_tv='', clients=None, - ask_for_quality=False, - audio_only=False, - use_mpd=True, **kwargs): self.video_id = None self.yt_item = None - self._context = context - self._access_token = access_token - self._access_token_tv = access_token_tv - self._ask_for_quality = ask_for_quality - self._audio_only = audio_only - self._use_mpd = use_mpd + settings = context.get_settings() + self._ask_for_quality = settings.ask_for_video_quality() + self._audio_only = settings.audio_only() + self._use_mpd = settings.use_mpd_videos() audio_language, prefer_default = context.get_player_language() if audio_language == 'mediadefault': - self._language_base = context.get_settings().get_language()[0:2] + self._language_base = settings.get_language()[0:2] elif audio_language == 'original': self._language_base = '' else: @@ -800,61 +837,43 @@ def __init__(self, self._language_prefer_default = prefer_default self._player_js = None - self._calculate_n = True - self._cipher = None - + # signatureCipher and nsig handling currently broken and disabled + # self._calculate_n = True + # self._cipher = None + self._calculate_n = False + self._cipher = False + + self._visitor_data = { + 'current': None, + INCOGNITO: None, + } + self._visitor_data_key = 'current' self._auth_client = {} - self._client_groups = { - 'custom': clients if clients else (), - # Access "premium" streams, HLS and DASH - # Limited video stream availability - 'default': ( - 'ios_youtube_tv', - 'ios', - ), - # Will play most videos with subtitles at full resolution with HDR - # Some restricted videos require additional requests for subtitles - # Limited audio stream availability with some clients - 'mpd': ( + self._client_groups = ( + ('custom', clients if clients else ()), + ('auth_enabled|initial_request|no_playable_streams', ( + 'tv_unplugged', + 'tv', + )), + ('auth_disabled|kids|av1|vp9|vp9.2|avc1|stereo_sound|multi_audio', ( + 'ios_testsuite_params', + )), + ('auth_disabled|kids|av1|vp9.2|avc1|surround_sound|multi_audio', ( + 'android_testsuite_params', + )), + ('auth_enabled|no_kids|av1|vp9.2|avc1|surround_sound', ( 'android_vr', - 'android_youtube_tv', - ), - # Progressive streams - # Limited video and audio stream availability - 'ask': ( - # 'media_connect_frontend', - ), - } - - super(StreamInfo, self).__init__(context=context, **kwargs) - - @staticmethod - def _response_hook_json(**kwargs): - response = kwargs['response'] - try: - json_data = response.json() - if 'error' in json_data: - kwargs.setdefault('pass_data', True) - raise YouTubeException('"error" in response JSON data', - json_data=json_data, - **kwargs) - except ValueError as exc: - kwargs.setdefault('raise_exc', True) - raise InvalidJSON(exc, **kwargs) - response.raise_for_status() - return json_data + )), + ('mpd', ( + )), + ('ask', ( + )), + ) - @staticmethod - def _response_hook_text(**kwargs): - response = kwargs['response'] - response.raise_for_status() - result = response and response.text - if not result: - raise YouTubeException('Empty response text', **kwargs) - return result + super(YouTubePlayerClient, self).__init__(context=context, **kwargs) @staticmethod - def _error_hook(**kwargs): + def _player_error_hook(**kwargs): exc = kwargs.pop('exc') json_data = getattr(exc, 'json_data', None) if getattr(exc, 'pass_data', False): @@ -867,38 +886,39 @@ def _error_hook(**kwargs): exception = None if not json_data or 'error' not in json_data: - info = ('Request - Failed' - '\n\tException: {exc!r}' - '\n\tvideo_id: |{video_id}|' - '\n\tClient: |{client}|' - '\n\tAuth: |{auth}|') - return None, info, kwargs, data, None, exception - + info = ( + 'video_id: {video_id!r}', + 'Client: {client_name!r}', + 'Auth: {has_auth!r}', + ) + return None, info, None, data, exception + + info = ( + 'Reason: {error_reason}', + 'Message: {error_message}', + 'video_id: {video_id!r}', + 'Client: {client_name!r}', + 'Auth: {has_auth!r}', + ) details = json_data['error'] - reason = details.get('errors', [{}])[0].get('reason', 'Unknown') - message = details.get('message', 'Unknown error') - - info = ('Request - Failed' - '\n\tException: {exc!r}' - '\n\tReason: {reason}' - '\n\tMessage: {message}' - '\n\tvideo_id: |{video_id}|' - '\n\tClient: |{client}|' - '\n\tAuth: |{auth}|') - kwargs['message'] = message - kwargs['reason'] = reason - return None, info, kwargs, data, None, exception + details = { + 'error_reason': ( + (details.get('errors') or [{}])[0].get('reason') + or 'Unknown' + ), + 'error_message': details.get('message') or 'Unknown error', + } + return None, info, details, data, exception @staticmethod - def _generate_cpn(): + def _generate_cpn(_alphabet=('abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789-_')): # https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L1381 # LICENSE: The Unlicense # cpn generation algorithm is reverse engineered from base.js. # In fact it works even with dummy cpn. - cpn_alphabet = ('abcdefghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - '0123456789-_') - return ''.join(random_choice(cpn_alphabet) for _ in range(16)) + return ''.join([random_choice(_alphabet) for _ in range(16)]) def _get_stream_format(self, itag, info=None, max_height=None, **kwargs): yt_format = self.FORMAT.get(itag) @@ -970,10 +990,11 @@ def _get_stream_format(self, itag, info=None, max_height=None, **kwargs): return yt_format def _get_player_config(self, client_name='web', embed=False): + video_id = self.video_id if embed: - url = ''.join(('https://www.youtube.com/embed/', self.video_id)) + url = self.BASE_URL + '/embed/%s' % video_id else: - url = ''.join(('https://www.youtube.com/watch?v=', self.video_id)) + url = self.WATCH_URL.format(_video_id=video_id) # Manually configured cookies to avoid cookie consent redirect cookies = {'SOCS': 'CAISAiAD'} @@ -986,12 +1007,11 @@ def _get_player_config(self, client_name='web', embed=False): headers=client['headers'], response_hook=self._response_hook_text, error_title='Failed to get player html', - error_hook=self._error_hook, - error_hook_kwargs={ - 'video_id': self.video_id, - 'client': client_name, - 'auth': False, - }, + video_id=self.video_id, + error_hook=self._player_error_hook, + client_name=client_name, + has_auth=False, + cache=False, ) if not result: return None @@ -1018,7 +1038,7 @@ def _get_player_key(self, html): start_index += len(pattern) end_index = html.find('"', start_index) player_key = html[start_index:end_index] - self._context.log_debug('Player key found: {0}'.format(player_key)) + self.log.debug('Player key found: %r', player_key) return player_key return None @@ -1062,12 +1082,11 @@ def _get_player_js(self): headers=client['headers'], response_hook=self._response_hook_text, error_title='Failed to get player JavaScript', - error_hook=self._error_hook, - error_hook_kwargs={ - 'video_id': self.video_id, - 'client': client_name, - 'auth': False, - }, + error_hook=self._player_error_hook, + video_id=self.video_id, + client_name=client_name, + has_auth=False, + cache=False, ) if not result: return '' @@ -1087,18 +1106,6 @@ def _prepare_headers(headers, cookies=None, new_headers=None): headers.update(new_headers) return headers - @staticmethod - def _normalize_url(url): - if not url: - url = '' - elif url.startswith(('http://', 'https://')): - pass - elif url.startswith('//'): - url = urljoin('https:', url) - elif url.startswith('/'): - url = urljoin('https://www.youtube.com', url) - return url - def _process_mpd(self, stream_list, responses, @@ -1128,12 +1135,22 @@ def _process_mpd(self, headers = response['client']['headers'] - if '?' in url: - url += '&mpd_version=5' - elif url.endswith('/'): - url += 'mpd_version/5' + url_components = urlsplit(url) + if url_components.query: + params = dict(parse_qs(url_components.query)) + params['mpd_version'] = ['7'] + url = url_components._replace( + query=urlencode(params, doseq=True), + ).geturl() else: - url += '/mpd_version/5' + path = re_sub( + r'/mpd_version/\d+|/?$', + '/mpd_version/7', + url_components.path, + ) + url = url_components._replace( + path=path, + ).geturl() stream_list[itag] = self._get_stream_format( itag=itag, @@ -1169,7 +1186,16 @@ def _process_hls(self, selected_height = qualities[0]['nom_height'] else: selected_height = settings.fixed_video_quality() - log_debug = context.log_debug + + # Regular expression used to capture the URL of a HLS m3u8 playlist and + # the itag from that URL. + # The playlist might include a #EXT-X-MEDIA entry, but it's usually + # for a default stream with itag 133 (240p) and can be ignored. + re_playlist_data = re_compile( + r'#EXT-X-STREAM-INF[^#]+' + r'(?Phttp\S+/itag/(?P\d+)\S+)' + ) + itags = ('9995', '9996') if is_live else ('9993', '9994') for client_name, response in responses.items(): url = response['hls_manifest'] @@ -1183,17 +1209,16 @@ def _process_hls(self, headers=headers, response_hook=self._response_hook_text, error_title='Failed to get HLS manifest', - error_hook=self._error_hook, - error_hook_kwargs={ - 'video_id': self.video_id, - 'client': client_name, - 'auth': False, - }, + error_hook=self._player_error_hook, + video_id=self.video_id, + client_name=client_name, + has_auth=False, + cache=False, ) if not result: continue - for itag in ('9995', '9996') if is_live else ('9993', '9994'): + for itag in itags: if itag in stream_list: continue @@ -1206,13 +1231,6 @@ def _process_hls(self, playback_stats=playback_stats, ) - # The playlist might include a #EXT-X-MEDIA entry, but it's usually - # for a default stream with itag 133 (240p) and can be ignored. - # Capture the URL of a .m3u8 playlist and the itag from that URL. - re_playlist_data = re_compile( - r'#EXT-X-STREAM-INF[^#]+' - r'(?Phttp\S+/itag/(?P\d+)\S+)' - ) for match in re_playlist_data.finditer(result): itag = match.group('itag') if itag in stream_list: @@ -1229,9 +1247,10 @@ def _process_hls(self, ) if yt_format is None: stream_info = redact_ip_in_uri(match.group(1)) - log_debug('Unknown itag - {itag}' - '\n\t{stream}' - .format(itag=itag, stream=stream_info)) + self.log.debug(('Unknown itag - {itag}', + '{stream}'), + itag=itag, + stream=stream_info) if (not yt_format or (yt_format.get('hls/video') and not yt_format.get('hls/audio'))): @@ -1269,7 +1288,6 @@ def _process_progressive_streams(self, selected_height = qualities[0]['nom_height'] else: selected_height = settings.fixed_video_quality() - log_debug = context.log_debug for client_name, response in responses.items(): streams = response['progressive_fmts'] @@ -1301,9 +1319,13 @@ def _process_progressive_streams(self, else: new_url = url + new_url = self._process_url_params(new_url, + mpd=False, + headers=headers, + referrer=None, + visitor_data=None) if not new_url: continue - new_url = self._process_url_params(new_url, mpd=False) stream_map['itag'] = itag yt_format = self._get_stream_format( @@ -1322,9 +1344,10 @@ def _process_progressive_streams(self, stream_map['conn'] = redact_ip_in_uri(conn) if stream: stream_map['stream'] = redact_ip_in_uri(stream) - log_debug('Unknown itag - {itag}' - '\n\t{stream}' - .format(itag=itag, stream=stream_map)) + self.log.debug(('Unknown itag - {itag}', + '{stream}'), + itag=itag, + stream=stream_map) if (not yt_format or (yt_format.get('dash/video') and not yt_format.get('dash/audio'))): @@ -1357,11 +1380,12 @@ def _process_progressive_streams(self, def _process_signature_cipher(self, stream_map): if self._cipher is None: - self._context.log_debug('signatureCipher detected') + self.log.debug('signatureCipher detected') if self._player_js is None: self._player_js = self._get_player_js() self._cipher = Cipher(self._context, javascript=self._player_js) if not self._cipher: + self.log.warning('signatureCipher handling disabled') return None signature_cipher = parse_qs(stream_map['signatureCipher']) @@ -1379,16 +1403,9 @@ def _process_signature_cipher(self, stream_map): if not signature: try: signature = self._cipher.get_signature(encrypted_signature) - except Exception as exc: - msg = ('StreamInfo._process_signature_cipher' - ' - Failed to extract URL' - '\n\tException: {exc!r}' - '\n\tSignature: |{sig}|' - '\n\tStack trace (most recent call last):\n{stack}' - .format(exc=exc, - sig=encrypted_signature, - stack=format_stack())) - self._context.log_error(msg) + except Exception: + self.log.exception(('Failed to extract URL', 'Signature: %r'), + encrypted_signature) self._cipher = False return None data_cache.set_item(encrypted_signature, {'sig': signature}) @@ -1402,6 +1419,10 @@ def _process_url_params(self, url, mpd=True, headers=None, + cpn=False, + referrer=False, + visitor_data=False, + method='POST', digits_re=re_compile(r'\d+')): if not url: return url @@ -1410,20 +1431,25 @@ def _process_url_params(self, params = parse_qs(parts.query) new_params = {} - if self._calculate_n and 'n' in params: + if 'n' not in params: + pass + elif not self._calculate_n: + self.log.debug('Decoding of nsig value disabled') + return None + else: if self._player_js is None: self._player_js = self._get_player_js() if self._calculate_n is True: - self._context.log_debug('nsig detected') + self.log.debug('Detected nsig in stream url') self._calculate_n = ratebypass.CalculateN(self._player_js) # Cipher n to get the updated value - new_n = self._calculate_n.calculate_n(params['n']) + new_n = self._calculate_n.calculate_n(params['n'][0]) if new_n: new_params['n'] = new_n - new_params['ratebypass'] = 'yes' + new_params['ratebypass'] = ['yes'] else: - self._context.log_error('nsig handling failed') + self.log.error('nsig handling failed') self._calculate_n = False if 'lmt' in params: @@ -1435,8 +1461,22 @@ def _process_url_params(self, modified = None snippet['publishedAt'] = modified + if headers: + if visitor_data is not False: + headers.setdefault( + 'X-Goog-Visitor-Id', + visitor_data or self._visitor_data[self._visitor_data_key], + ) + if referrer is not False: + headers.setdefault( + 'Referer', + referrer + or 'https://www.youtube.com/watch?v=%s' % self.video_id, + ) + if mpd: new_params['__id'] = self.video_id + new_params['__method'] = method new_params['__host'] = [parts.hostname] new_params['__path'] = parts.path new_params['__headers'] = urlsafe_b64encode( @@ -1453,6 +1493,9 @@ def _process_url_params(self, server.replace(primary, secondary), ))) + if cpn is not False: + new_params['cpn'] = cpn or self._generate_cpn() + params.update(new_params) query_str = urlencode(params, doseq=True) @@ -1463,7 +1506,7 @@ def _process_url_params(self, query=query_str, ).geturl() - elif 'range' not in params: + elif 'ratebypass' not in params and 'range' not in params: content_length = params.get('clen', [''])[0] new_params['range'] = '0-{0}'.format(content_length) @@ -1482,11 +1525,12 @@ def _process_captions(self, subtitles, responses): for client_name, response in responses.items(): captions = response['captions'] client = response['client'] - do_query = client.get('_query_subtitles') + use_subtitles = client.get('_use_subtitles') if (not captions - or do_query is True - or (do_query and subtitles.sub_selection == all_subs)): + or not use_subtitles + or (use_subtitles is not True + and subtitles.sub_selection == all_subs)): continue subtitles.load(captions, client['headers'].copy()) @@ -1500,23 +1544,24 @@ def _process_captions(self, subtitles, responses): 'json': { 'videoId': video_id, }, - 'url': 'https://www.youtube.com/youtubei/v1/player', + 'url': self.V1_API_URL, 'method': 'POST', + '_endpoint': 'player', + '_visitor_data': self._visitor_data[self._visitor_data_key], } - for client_name in ('smart_tv_embedded', 'web'): + for client_name in ('tv_unplugged', 'web'): client = self.build_client(client_name, client_data) if not client: continue result = self.request( response_hook=self._response_hook_json, error_title='Caption player request failed', - error_hook=self._error_hook, - error_hook_kwargs={ - 'video_id': video_id, - 'client': client_name, - 'auth': client.get('_has_auth', False), - }, + error_hook=self._player_error_hook, + video_id=video_id, + client_name=client_name, + has_auth=client.get('_has_auth'), + cache=False, **client ) @@ -1532,27 +1577,23 @@ def _process_captions(self, subtitles, responses): return default_lang, subs_data - def _get_error_details(self, playability_status, details=None): + def _get_error_details(self, + playability_status, + details=('errorScreen', ( + ('playerErrorMessageRenderer', + 'reason'), + ('confirmDialogRenderer', + 'title'), + ('playerCaptchaViewModel', + 'accessibility', + 'accessibilityData', + 'label'), + ))): if not playability_status: return None - if not details: - details = ( - 'errorScreen', - ( - ( - 'playerErrorMessageRenderer', - 'reason', - ), - ( - 'confirmDialogRenderer', - 'title', - ), - ) - ) result = self.json_traverse(playability_status, details) - - if not result or 'runs' not in result: + if not result or not isinstance(result, dict) or 'runs' not in result: return result detail_texts = [ @@ -1566,22 +1607,46 @@ def _get_error_details(self, playability_status, details=None): return result['simpleText'] return None - def load_stream_info(self, video_id): + def load_stream_info(self, + video_id, + ask_for_quality=None, + audio_only=None, + incognito=None, + use_mpd=None): self.video_id = video_id + if ask_for_quality is None: + ask_for_quality = self._ask_for_quality + else: + self._ask_for_quality = ask_for_quality + + if audio_only is None: + audio_only = self._audio_only + else: + self._audio_only = audio_only + + if incognito is None: + incognito = self._context.get_param(INCOGNITO, False) + if incognito: + visitor_data_key = self._visitor_data_key = INCOGNITO + self._visitor_data[visitor_data_key] = None + else: + visitor_data_key = self._visitor_data_key = 'current' + + if use_mpd is None: + use_mpd = self._use_mpd + else: + self._use_mpd = use_mpd + context = self._context settings = context.get_settings() age_gate_enabled = settings.age_gate() - audio_only = self._audio_only - ask_for_quality = self._ask_for_quality - use_mpd = self._use_mpd use_remote_history = settings.use_remote_history() _client_name = None _client = None _has_auth = None _result = None - _visitor_data = None _video_details = None _microformat = None _streaming_data = None @@ -1589,63 +1654,65 @@ def load_stream_info(self, video_id): _status = None _reason = None + visitor_data = self._visitor_data[visitor_data_key] video_details = {} microformat = {} responses = {} stream_list = {} - log_debug = context.log_debug - log_warning = context.log_warning - - abort_reasons = { - 'country', - 'not available', - } - reauth_reasons = { - 'age', - 'inappropriate', - 'sign in', - } - skip_reasons = { - 'latest version', - } - retry_reasons = { - 'try again later', - 'unavailable', - 'unknown', - } + fail = self.FAILURE_REASONS abort = False - has_access_token = bool(self._access_token or self._access_token_tv) + logged_in = self.logged_in client_data = { 'json': { 'videoId': video_id, }, - 'url': 'https://www.youtube.com/youtubei/v1/player', + 'url': self.V1_API_URL, 'method': 'POST', - '_auth_required': False, - '_auth_requested': 'personal' if use_remote_history else False, - '_access_token': self._access_token, - '_access_token_tv': self._access_token_tv, - '_visitor_data': None, + '_access_tokens': { + 'user': (self._access_tokens.get('user') + if (self._configs.get('user', {}) + .get('token-allowed', True)) else + None), + 'tv': self._access_tokens.get('tv'), + 'vr': self._access_tokens.get('vr'), + }, + '_endpoint': 'player', + '_cpn': None, + '_visitor_data': visitor_data, } + if use_remote_history: + client_data['_auth_type'] = 'user' + client_data['_auth_requested'] = True - for name, clients in self._client_groups.items(): + for name, clients in self._client_groups: if not clients: continue - if name == 'mpd' and not (use_mpd or use_remote_history): + 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 + exclude_retry = set() restart = None while 1: for _client_name in clients: + if _client_name in exclude_retry: + continue + client_data['_cpn'] = self._generate_cpn() _client = self.build_client(_client_name, client_data) if _client: - _has_auth = _client.get('_has_auth', False) - if _has_auth: - restart = False + _has_auth = _client.get('_has_auth') + if _has_auth or _has_auth is False: + exclude_retry.add(_client_name) else: _has_auth = None _result = None @@ -1660,29 +1727,55 @@ def load_stream_info(self, video_id): _result = self.request( response_hook=self._response_hook_json, error_title='Player request failed', - error_hook=self._error_hook, - error_hook_kwargs={ - 'video_id': video_id, - 'client': _client_name, - 'auth': _has_auth, - }, + error_hook=self._player_error_hook, + video_id=video_id, + client_name=_client_name, + has_auth=_has_auth, + cache=False, + pass_data=True, + raise_exc=False, **_client ) or {} - if not _visitor_data: - _visitor_data = (_result - .get('responseContext', {}) - .get('visitorData')) - if _visitor_data: - client_data['_visitor_data'] = _visitor_data + if not visitor_data: + visitor_data = self.json_traverse( + _result, + ( + 'responseContext', + ( + ( + 'visitorData', + ), + ( + 'serviceTrackingParams', + 0, + 'params', + { + 'name': 'key', + 'match': ('visitor_data', + 'visitorData'), + 'out': 'value', + }, + ), + ), + ) + ) + if visitor_data: + client_data['_visitor_data'] = visitor_data + self._visitor_data[visitor_data_key] = visitor_data _video_details = _result.get('videoDetails', {}) _microformat = (_result .get('microformat', {}) .get('playerMicroformatRenderer')) _streaming_data = _result.get('streamingData', {}) _playability = _result.get('playabilityStatus', {}) - _status = _playability.get('status', 'ERROR').upper() - _reason = _playability.get('reason', 'UNKNOWN') + if _playability: + _status = _playability.get('status', 'ERROR').upper() + _reason = _playability.get('reason', 'UNKNOWN') + else: + _error = _result.get('error', {}) + _status = _error.get('status', 'ERROR').upper() + _reason = _error.get('message', 'UNKNOWN') if (_video_details and video_id != _video_details.get('videoId')): @@ -1698,7 +1791,7 @@ def load_stream_info(self, video_id): break elif _status == 'OK': break - elif _status in { + elif not _playability or _status in { 'AGE_CHECK_REQUIRED', 'AGE_VERIFICATION_REQUIRED', 'CONTENT_CHECK_REQUIRED', @@ -1707,43 +1800,48 @@ def load_stream_info(self, video_id): 'ERROR', 'UNPLAYABLE', }: - log_warning( - 'Failed to retrieve video info' - '\n\tStatus: {status}' - '\n\tReason: {reason}' - '\n\tvideo_id: |{video_id}|' - '\n\tClient: |{client}|' - '\n\tAuth: |{auth}|' - .format( - status=_status, - reason=_reason or 'UNKNOWN', - video_id=video_id, - client=_client_name, - auth=_has_auth, - ) - ) - compare_reason = _reason.lower() - if any(why in compare_reason for why in reauth_reasons): - if client_data.get('_auth_required'): + self.log.warning(('Failed to retrieve video info', + 'Status: {status}', + 'Reason: {reason}', + 'video_id: {video_id!r}', + 'Client: {client!r}', + 'Auth: {has_auth!r}'), + status=_status, + reason=_reason or 'UNKNOWN', + video_id=video_id, + client=_client_name, + has_auth=_has_auth) + 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'): restart = False abort = True - elif restart is None and has_access_token: + elif restart is None and logged_in: client_data['_auth_required'] = True restart = True break - if any(why in compare_reason for why in retry_reasons): - continue - if any(why in compare_reason for why in skip_reasons): - 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 + elif any(why in fail_reason for why in fail['skip']): + if allow_skip: + break + elif any(why in fail_reason for why in fail['retry']): + continue else: - log_debug( - 'Unknown playabilityStatus in player response' - '\n\tplayabilityStatus: {0}' - .format(_playability) - ) + self.log.warning('Unknown playabilityStatus: {status!r}', + status=_playability) else: break if not restart: @@ -1754,17 +1852,13 @@ def load_stream_info(self, video_id): break if _status == 'OK': - log_debug( - 'Retrieved video info:' - '\n\tvideo_id: |{video_id}|' - '\n\tClient: |{client}|' - '\n\tAuth: |{auth}|' - .format( - video_id=video_id, - client=_client_name, - auth=_has_auth, - ) - ) + self.log.debug(('Retrieved video info:', + 'video_id: {video_id!r}', + 'Client: {client!r}', + 'Auth: {has_auth!r}'), + video_id=video_id, + client=_client_name, + has_auth=_has_auth) video_details = merge_dicts( _video_details, @@ -1783,6 +1877,7 @@ def load_stream_info(self, video_id): 'client': _client.copy(), 'result': _result, } + client_data['_auth_requested'] = False responses[_client_name] = { 'client': _client, @@ -1793,6 +1888,10 @@ def load_stream_info(self, video_id): 'captions': _result.get('captions'), } + if (not client_data.get('_auth_required') + and video_details.get('isPrivate')): + client_data['_auth_required'] = True + if not responses: if _status == 'LIVE_STREAM_OFFLINE': if not _reason: @@ -1810,7 +1909,7 @@ def load_stream_info(self, video_id): _reason = self._get_error_details(_playability) raise YouTubeException(_reason or 'UNKNOWN') - self.yt_item = { + self.yt_item = yt_item = { 'id': video_id, 'snippet': { 'title': video_details.get('title'), @@ -1829,7 +1928,7 @@ def load_stream_info(self, video_id): }, '_partial': True, } - is_live = video_details.get('isLiveContent', False) + is_live = video_details.get('isLiveContent') or video_details.get('hasLiveStreamingData') if is_live: is_live = video_details.get('isLive', False) live_dvr = video_details.get('isLiveDvrEnabled', False) @@ -1858,9 +1957,12 @@ def load_stream_info(self, video_id): }, 'thumbnails': { thumb_type: { - 'url': thumb['url'].format(video_id, thumb_suffix), + 'url': THUMB_URL.format( + video_id, thumb['name'], thumb_suffix + ), 'size': thumb['size'], 'ratio': thumb['ratio'], + 'unverified': True, } for thumb_type, thumb in THUMB_TYPES.items() }, @@ -1875,7 +1977,7 @@ def load_stream_info(self, video_id): playback_tracking = (self._auth_client .get('result', {}) .get('playbackTracking', {})) - cpn = self._generate_cpn() + cpn = self._auth_client.get('_cpn') or self._generate_cpn() for key, url_key in playback_stats.items(): url = playback_tracking.get(url_key, {}).get('baseUrl') @@ -1899,14 +2001,15 @@ def load_stream_info(self, video_id): ) if not is_live or live_dvr: - subtitles = Subtitles(context, video_id) + subtitles = Subtitles(context, video_id, use_mpd=use_mpd) default_lang, subs_data = self._process_captions( subtitles=subtitles, responses=responses, ) - if subs_data and (not use_mpd or subtitles.pre_download): + if subs_data and not subtitles.use_isa: meta_info['subtitles'] = [ subtitle['url'] for subtitle in subs_data.values() + if 'url' in subtitle ] subs_data = None else: @@ -1985,16 +2088,23 @@ def load_stream_info(self, video_id): playback_stats=playback_stats, ) - if not stream_list: + if stream_list: + self.log.debug(('Media details:', + 'Status: {status!r}', + 'Item: {item!r}'), + status=meta_info['status'], + item=yt_item) + else: raise YouTubeException('No streams found') - return stream_list.values(), self.yt_item + return stream_list.values(), yt_item def _process_adaptive_streams(self, responses, default_lang_code='und', codec_re=re_compile( - r'codecs="([a-z0-9]+([.\-][0-9](?="))?)' + r'codecs=' + r'"((?P.+?)\.(?P.+))"' )): context = self._context settings = context.get_settings() @@ -2002,10 +2112,15 @@ def _process_adaptive_streams(self, qualities = settings.mpd_video_qualities() isa_capabilities = context.inputstream_adaptive_capabilities() stream_features = settings.stream_features() + allow_3d = '3d' in stream_features allow_hdr = 'hdr' in stream_features allow_hfr = 'hfr' in stream_features disable_hfr_max = 'no_hfr_max' in stream_features + allow_spa = 'spa' in stream_features allow_ssa = 'ssa' in stream_features + allow_vr = 'vr' in stream_features + prefer_dub = 'prefer_dub' in stream_features + prefer_auto_dub = 'prefer_auto_dub' in stream_features fps_map = (self.INTEGER_FPS_SCALE if 'no_frac_fr_hint' in stream_features else self.FRACTIONAL_FPS_SCALE) @@ -2013,11 +2128,13 @@ def _process_adaptive_streams(self, stream_select = settings.stream_select() localize = context.localize + debugging = self.log.debugging + audio_data = {} video_data = {} preferred_audio = { - 'id': '', 'language_code': None, + 'role_id': None, 'role_order': None, 'fallback': True, } @@ -2034,12 +2151,16 @@ def _process_adaptive_streams(self, if not stream_data: continue + log_client = debugging + log_audio_header = None + log_video_header = None + for stream in stream_data: mime_type = stream.get('mimeType') if not mime_type: continue - itag = stream.get('itag') + itag_id = itag = str(stream.get('itag')) if not itag: continue @@ -2058,18 +2179,24 @@ def _process_adaptive_streams(self, continue mime_type, codecs = unquote(mime_type).split('; ') - codec = codec_re.match(codecs) - if codec: - codec = codec.group(1) - if codec.startswith('vp9'): + codecs = codec_re.match(codecs) + if codecs: + codec = codecs.group('codec') + codec_properties = codecs.group('props') + codecs = codecs.group(1) + if codec.startswith(('vp9', 'vp09')): codec = 'vp9' - elif codec.startswith('vp09'): - codec = 'vp9.2' - elif codec.startswith('dts'): - codec = 'dts' - if codec not in isa_capabilities: + preferred_codec = codec in stream_features + if codec_properties.startswith(('2', '02.')): + codec = 'vp9.2' + else: + if codec.startswith('dts'): + codec = 'dts' + preferred_codec = codec in stream_features + if codec not in isa_capabilities: + continue + else: continue - preferred_codec = codec.split('.')[0] in stream_features media_type, container = mime_type.split('/') bitrate = stream.get('bitrate', 0) @@ -2079,88 +2206,103 @@ def _process_adaptive_streams(self, if channels > 2 and not allow_ssa: continue + is_spa = stream.get('spatialAudioType', '') + if is_spa and not allow_spa: + continue + if 'audioTrack' in stream: audio_track = stream['audioTrack'] language = audio_track.get('id', default_lang_code) if '.' in language: - language_code, role_str = language.split('.') - role_id = int(role_str) + language_code, role_id = language.split('.') else: language_code = language - role_id = 4 - role_str = '4' - - role_details = lang_role_details.get(role_id) - # Unsure of what other audio types are available - # Role set to "alternate" as default fallback - if not role_details: - role_details = lang_role_details[0] - - role_type, role, role_order = role_details - label = localize('stream.{0}'.format(role_type)) - - preferred_order = preferred_audio['role_order'] - language_fallback = preferred_audio['fallback'] - - if (default_lang - and language_code.startswith(default_lang)): - is_fallback = False - if prefer_default_lang: + role_id = '4' + else: + language_code = default_lang_code + role_id = '-1' + + role_details = lang_role_details.get(role_id) + # Unsure of what other audio types are available + # Role set to "alternate" as default fallback + if not role_details: + role_details = lang_role_details[0] + role_type, role, role_order = role_details + + preferred_order = preferred_audio['role_order'] + language_fallback = preferred_audio['fallback'] + + if (default_lang + and language_code.startswith(default_lang)): + is_fallback = role != 'main' + if role_type == 'dub.auto': + if prefer_auto_dub: role = 'main' role_order = 0 - elif role_type.startswith('dub'): - is_fallback = True - lang_match = ( - (language_fallback and not is_fallback) - or preferred_order is None - or role_order > preferred_order - ) - language_fallback = is_fallback - else: - lang_match = ( - language_fallback - and (preferred_order is None - or role_order > preferred_order) - ) - language_fallback = True - - if lang_match: - preferred_audio = { - 'id': ''.join(( - '_', - language_code, - '.', - role_str, - )), - 'language_code': language_code, - 'role_order': role_order, - 'fallback': language_fallback, - } - - mime_group = ''.join(( - mime_type, '_', language_code, '.', role_str, - )) + elif role_type == 'dub': + if prefer_dub: + role = 'main' + role_order = 0 + elif prefer_default_lang: + role = 'main' + role_order = 0 + lang_match = ( + (language_fallback and not is_fallback) + or preferred_order is None + or role_order > preferred_order + ) + language_fallback = is_fallback else: - language_code = default_lang_code - role_id = -1 - role_str = str(role_id) - role_details = lang_role_details[role_id] - role_type, role, role_order = role_details - label = localize('stream.{0}'.format(role_type)) - mime_group = mime_type + lang_match = ( + language_fallback + and (preferred_order is None + or role_order > preferred_order) + ) + language_fallback = True + + if lang_match: + preferred_audio = { + 'language_code': language_code, + 'role_id': role_id, + 'role_order': role_order, + 'fallback': language_fallback, + } + language = context.get_language_name(language_code) sample_rate = int(stream.get('audioSampleRate', '0'), 10) - height = width = fps = frame_rate = hdr = None - language = context.get_language_name(language_code) - label = '{0} ({1} kbps)'.format(label, bitrate // 1000) + + is_drc = stream.get('isDrc', False) + if is_drc: + itag += '.drc' + + mime_group = ( + mime_type, + language_code, + role_id, + ) + + label = '{0} ({1} kbps)'.format( + localize('stream.{0}'.format(role_type)), + bitrate // 1000, + ) if channels > 2 or 'auto' not in stream_select: - quality_group = ''.join(( - container, '_', codec, '_', language_code, - '.', role_str, - )) + quality_group = ( + container, + codec, + language_code, + role_id, + ) else: quality_group = mime_group + + height = width = fps = frame_rate = None + is_hdr = is_vr = is_3d = None + + log_audio = debugging + log_video = False + if log_audio_header is None: + log_audio_header = debugging elif audio_only: continue else: @@ -2174,13 +2316,26 @@ def _process_adaptive_streams(self, continue if 'colorInfo' in stream: - hdr = not any(value.endswith('BT709') - for value in stream['colorInfo'].values()) + is_hdr = not any( + value.endswith('BT709') + for value in stream['colorInfo'].values() + ) else: - hdr = 'HDR' in stream.get('qualityLabel', '') - if hdr and not allow_hdr: + is_hdr = 'HDR' in stream.get('qualityLabel', '') + if is_hdr and not allow_hdr: continue + is_3d = stream.get('stereoLayout', '') + if is_3d and not allow_3d: + continue + + is_vr = stream.get('projectionType', '') + if is_vr: + if is_vr == 'RECTANGULAR': + is_vr = '' + elif not allow_vr: + continue + height = stream.get('height') width = stream.get('width') if height > width: @@ -2189,19 +2344,27 @@ def _process_adaptive_streams(self, else: compare_width = width compare_height = height + # Compare video stream width against pre-computed quality + # selection width based on approximate aspect ratio. + # 1.69 ~= 0.95 * 16 / 9 + if width / height > 1.69: + nom_width = 'width_16:9' + else: + nom_width = 'width_4:3' bound = None + _disable_hfr_max = disable_hfr_max for quality in qualities: - if compare_width > quality['width']: + if compare_width > quality[nom_width]: if bound: if compare_height >= bound['min_height']: quality = bound elif compare_height < quality['min_height']: quality = qualities[-1] - if fps > 30 and disable_hfr_max: - bound = None + if fps > 30 and _disable_hfr_max: + bound = None break - disable_hfr_max = disable_hfr_max and not bound + _disable_hfr_max = _disable_hfr_max and not bound bound = quality if not bound: continue @@ -2213,31 +2376,38 @@ def _process_adaptive_streams(self, else: frame_rate = None - mime_group = '_'.join(( + mime_group = ( mime_type, codec, - 'hdr', - ) if hdr else ( - mime_type, - codec, - )) - channels = sample_rate = None - language = role = role_order = None + is_hdr, + is_vr, + ) + label = quality['label'].format( quality['nom_height'] or compare_height, fps if fps > 30 else '', - ' HDR' if hdr else '', + ' HDR' if is_hdr else '', + ' 3D' if is_3d else '', + ' VR' if is_vr else '', ) - quality_group = '_'.join((container, codec, label)) + quality_group = ( + container, + codec, + label, + ) + + channels = sample_rate = is_drc = is_spa = None + language = role = role_order = role_type = None - if mime_group not in data: - data[mime_group] = {} - if quality_group not in data: - data[quality_group] = {} + log_audio = False + log_video = debugging + if log_video_header is None: + log_video_header = debugging urls = self._process_url_params( unquote(url), headers=client['headers'], + cpn=client.get('_cpn'), ) if not urls: continue @@ -2261,7 +2431,9 @@ def _process_adaptive_streams(self, // 1000), 'fps': fps, 'frameRate': frame_rate, - 'hdr': hdr, + 'hdr': is_hdr, + 'projection': is_vr, + 'stereoLayout': is_3d, 'indexRange': '{start}-{end}'.format(**index_range), 'initRange': '{start}-{end}'.format(**init_range), 'langCode': language_code, @@ -2270,11 +2442,102 @@ def _process_adaptive_streams(self, 'roleOrder': role_order, 'sampleRate': sample_rate, 'channels': channels, + 'drc': is_drc, + 'spatial': is_spa, } - data[mime_group][itag] = data[quality_group][itag] = details + mime_group = data.setdefault(mime_group, {}) + quality_group = data.setdefault(quality_group, {}) + mime_group[itag] = quality_group[itag] = details + + if log_client: + self.log.debug('{_:{_}^100}', _='=') + self.log.debug('Streams found for %r client:', client_name) + log_client = False + if log_audio: + if log_audio_header: + self.log.debug('{_:{_}^100}', _='-') + self.log.debug('{itag:^3}' + ' | {container:^4}' + ' | {channels:^5}' + ' | {bitrate:^8}' + ' | {sample_rate:^9}' + ' | {drc:^3}' + ' | {codecs:^19}' + ' | {info}', + itag='ID', + container='TYPE', + channels='CH', + bitrate='ABR', + sample_rate='ASR', + drc='DRC', + codecs='CODECS', + info='INFO') + self.log.debug('{_:{_}^100}', _='-') + log_audio_header = False + self.log.debug('{itag:3}' + ' | {container:4}' + ' | {channels:2} ch' + ' | {bitrate:3} kbps' + ' | {sample_rate:<5.2f} kHz' + ' | {drc:^3}' + ' | {codecs:19}' + ' | {language}' + ' {role_type}', + itag=itag_id, + container=container, + channels=channels, + bitrate=bitrate // 1000, + sample_rate=sample_rate / 1000, + drc='Y' if is_drc else '-', + codecs='%s (%s)' % (codec, codecs), + language=language, + role_type=role_type) + elif log_video: + if log_video_header: + self.log.debug('{_:{_}^100}', _='-') + self.log.debug('{itag:^3}' + ' | {container:^4}' + ' | {width:>4} x {height:<4}' + ' | {fps:^6}' + ' | {hdr:^3}' + ' | {s3d:^3}' + ' | {vr:^3}' + ' | {bitrate:^11}' + ' | {codecs}', + itag='ID', + container='TYPE', + width='W', + height='H', + fps='FPS', + hdr='HDR', + s3d='3D', + vr='VR', + bitrate='VBR', + codecs='CODECS') + self.log.debug('{_:{_}^100}', _='-') + log_video_header = False + self.log.debug('{itag:3}' + ' | {container:4}' + ' | {width:>4} x {height:<4}' + ' | {fps:2} fps' + ' | {hdr:^3}' + ' | {s3d:^3}' + ' | {vr:^3}' + ' | {bitrate:6,} kbps' + ' | {codecs}', + itag=itag_id, + container=container, + width=width, + height=height, + fps=fps, + hdr='Y' if is_hdr else '-', + s3d='Y' if is_3d else '-', + vr='Y' if is_vr else '-', + bitrate=bitrate // 1000, + codecs='%s (%s)' % (codec, codecs)) if not video_data and not audio_only: - context.log_debug('Generate MPD: No video mime-types found') + self.log.debug('No video mime-types found') return None, None def _stream_sort(stream, alt_sort=('alt_sort' in stream_features)): @@ -2287,13 +2550,17 @@ def _stream_sort(stream, alt_sort=('alt_sort' in stream_features)): - stream['height'] if preferred or not alt_sort else stream['height'], + not stream['projection'], + not stream['stereoLayout'], - stream['fps'], - stream['hdr'], - stream['biasedBitrate'], ) if stream['mediaType'] == 'video' else ( - preferred, + not stream['spatial'], - stream['channels'], - stream['biasedBitrate'], + stream['drc'], ) def _group_sort(item): @@ -2301,10 +2568,11 @@ def _group_sort(item): main_stream = streams[0] key = ( - not group.startswith(main_stream['mimeType']), + group[0] != main_stream['mimeType'], ) if main_stream['mediaType'] == 'video' else ( - not group.startswith(main_stream['mimeType']), - preferred_audio['id'] not in group, + group[0] != main_stream['mimeType'], + group[-2] != preferred_audio['language_code'], + group[-1] != preferred_audio['role_id'], main_stream['langName'], - main_stream['roleOrder'], ) @@ -2331,12 +2599,8 @@ def _generate_mpd_manifest(self, if not video_data or not audio_data: return None, None - context = self._context - log_error = context.log_error - if not self.BASE_PATH: - log_error('StreamInfo._generate_mpd_manifest' - ' - Unable to access temp directory') + self.log.error_trace('Unable to access temp directory') return None, None def _filter_group(previous_group, previous_stream, item): @@ -2353,8 +2617,8 @@ def _filter_group(previous_group, previous_stream, item): if media_type != previous_stream['mediaType']: return not skip_group - if previous_group.startswith(previous_stream['mimeType']): - if new_group.startswith(new_stream['container']): + if previous_group[0] == previous_stream['mimeType']: + if new_group[0] == new_stream['container']: return not skip_group skip_group = ( @@ -2363,7 +2627,7 @@ def _filter_group(previous_group, previous_stream, item): new_stream['channels'] <= previous_stream['channels'] ) else: - if new_group.startswith(new_stream['mimeType']): + if new_group[0] == new_stream['mimeType']: return not skip_group skip_group = ( @@ -2383,6 +2647,7 @@ def _filter_group(previous_group, previous_stream, item): ) return skip_group + context = self._context settings = context.get_settings() stream_features = settings.stream_features() do_filter = 'filter' in stream_features @@ -2432,7 +2697,7 @@ def _filter_group(previous_group, previous_stream, item): language = stream['langCode'] role = stream['role'] or '' - if group.startswith(mime_type) and 'auto' in stream_select: + if group[0] == mime_type and 'auto' in stream_select: label = '{0} [{1}]'.format( stream['langName'] or localize('stream.automatic'), @@ -2441,7 +2706,7 @@ def _filter_group(previous_group, previous_stream, item): if stream == main_stream[media_type]: default = True role = 'main' - elif group.startswith(container) and 'list' in stream_select: + elif group[0] == container and 'list' in stream_select: if 'auto' in stream_select or media_type == 'video': label = stream['label'] else: @@ -2481,9 +2746,9 @@ def _filter_group(previous_group, previous_stream, item): # MPD spec. Should be a child Label element instead ' name="[B]', label, '[/B]"' # original / default / impaired are ISA specific attributes - ' original="', str(original).lower(), '"' - ' default="', str(default).lower(), '"' - ' impaired="', str(impaired).lower(), '"' + ' original="', VALUE_TO_STR[original], '"' + ' default="', VALUE_TO_STR[default], '"' + ' impaired="', VALUE_TO_STR[impaired], '"' '>\n' # AdaptationSet Label element not currently used by ISA '\t\t\t\n' @@ -2498,7 +2763,7 @@ def _filter_group(previous_group, previous_stream, item): output.extend([( '\t\t\t\n' # AdaptationSet Label element not currently used by ISA '\t\t\t\n' @@ -2606,8 +2879,10 @@ def _filter_group(previous_group, previous_stream, item): '/>\n' '\t\t\t\n' '\t\t\t\t', url, '\n' '\t\t\t\n' @@ -2629,12 +2904,8 @@ def _filter_group(previous_group, previous_stream, item): try: with xbmcvfs.File(filepath, 'w') as mpd_file: success = mpd_file.write(output) - except (IOError, OSError) as exc: - log_error('StreamInfo._generate_mpd_manifest' - ' - File write failed' - '\n\tException: {exc!r}' - '\n\tFile: {filepath}' - .format(exc=exc, filepath=filepath)) + except (IOError, OSError): + self.log.exception(('File write failed', 'File: %s'), filepath) success = False if success: return urlunsplit(( diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/request_client.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/request_client.py index 7b1f82da1d..70069e7aae 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2023-present plugin.video.youtube + Copyright (C) 2023-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,9 +10,9 @@ from __future__ import absolute_import, division, unicode_literals from ..youtube_exceptions import YouTubeException -from ...kodion.compatibility import range_type +from ...kodion.compatibility import range_type, unescape, urljoin from ...kodion.network import BaseRequestsClass -from ...kodion.utils import merge_dicts +from ...kodion.utils.methods import merge_dicts class YouTubeRequestClient(BaseRequestsClass): @@ -21,14 +21,20 @@ class YouTubeRequestClient(BaseRequestsClass): 'android_embedded': 'AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw', 'ios': 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc', 'ios_youtube_tv': 'AIzaSyAA2X8Iz20HQACliPKA2J9URIdPmS3xFUA', - 'android_youtube_tv': 'AIzaSyDCU8hByM-4DrUqRUYnGn-3llEO78bcxq8', + 'youtube_tv': 'AIzaSyDCU8hByM-4DrUqRUYnGn-3llEO78bcxq8', 'web': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', } _PLAYER_PARAMS = { - 'android': 'CgIIAdgDAQ==', - 'android_testsuite': '2AMB', + 'default': '8AEB', + 'testsuite': '2AMB', } + BASE_URL = 'https://www.youtube.com' + BASE_URL_MOBILE = 'https://m.youtube.com' + V1_API_URL = BASE_URL + '/youtubei/v1/{_endpoint}' + V3_API_URL = 'https://www.googleapis.com/youtube/v3/{_endpoint}' + WATCH_URL = BASE_URL + '/watch?v={_video_id}' + CLIENTS = { # Disabled - requires PO token # Requests for stream urls result in HTTP 403 errors @@ -45,7 +51,7 @@ class YouTubeRequestClient(BaseRequestsClass): 'platform': 'MOBILE', }, '_auth_type': False, - '_query_subtitles': 'optional', + '_use_subtitles': 'optional', 'json': { 'context': { 'client': { @@ -57,8 +63,11 @@ class YouTubeRequestClient(BaseRequestsClass): 'platform': '{_id[platform]}', }, }, + 'cpn': None, + 'params': _PLAYER_PARAMS['default'], }, 'headers': { + 'Origin': BASE_URL_MOBILE, 'User-Agent': ( '{_id[package_id]}/{_id[client_version]}' ' (Linux; U;' @@ -73,7 +82,7 @@ class YouTubeRequestClient(BaseRequestsClass): '_id': { 'client_id': 28, 'client_name': 'ANDROID_VR', - 'client_version': '1.64.34', + 'client_version': '1.65.10', 'android_sdk_version': '34', 'device_codename': 'eureka', 'device_make': 'Oculus', @@ -83,7 +92,8 @@ class YouTubeRequestClient(BaseRequestsClass): 'os_build': 'UP1A.231005.007.A1', 'package_id': 'com.google.android.apps.youtube.vr.oculus', }, - '_query_subtitles': False, + '_auth_type': 'vr', + '_use_subtitles': False, 'json': { 'context': { 'client': { @@ -98,6 +108,7 @@ class YouTubeRequestClient(BaseRequestsClass): }, }, 'headers': { + 'Origin': BASE_URL, 'User-Agent': ( '{_id[package_id]}/{_id[client_version]}' ' (Linux; U;' @@ -110,11 +121,13 @@ class YouTubeRequestClient(BaseRequestsClass): 'X-YouTube-Client-Version': '{_id[client_version]}', }, }, + # Disabled - requires login but fails using OAuth2 authorisation # 4k with HDR # Some videos block this client, may also require embedding enabled # Limited subtitle availability # Limited audio streams 'android_youtube_tv': { + '_disabled': True, '_id': { 'client_id': 29, 'client_name': 'ANDROID_UNPLUGGED', @@ -126,8 +139,8 @@ class YouTubeRequestClient(BaseRequestsClass): 'platform': 'TV', }, '_auth_required': True, - '_auth_type': 'personal', - '_query_subtitles': True, + '_auth_type': 'user', + '_use_subtitles': False, 'json': { 'context': { 'client': { @@ -141,6 +154,46 @@ class YouTubeRequestClient(BaseRequestsClass): }, }, 'headers': { + 'Origin': BASE_URL_MOBILE, + 'User-Agent': ( + '{_id[package_id]}/{_id[client_version]}' + ' (Linux; U;' + ' {_id[os_name]} {_id[os_version]}' + ') gzip' + ), + 'X-YouTube-Client-Name': '{_id[client_id]}', + 'X-YouTube-Client-Version': '{_id[client_version]}', + }, + }, + 'android_testsuite_params': { + '_id': { + 'client_id': 3, + 'client_name': 'ANDROID', + 'client_version': '20.10.38', + 'android_sdk_version': '32', + 'os_name': 'Android', + 'os_version': '15', + 'package_id': 'com.google.android.youtube', + 'platform': 'MOBILE', + }, + '_auth_type': False, + '_use_subtitles': 'optional', + 'json': { + 'context': { + 'client': { + 'clientName': '{_id[client_name]}', + 'clientVersion': '{_id[client_version]}', + 'androidSdkVersion': '{_id[android_sdk_version]}', + 'osName': '{_id[os_name]}', + 'osVersion': '{_id[os_version]}', + 'platform': '{_id[platform]}', + }, + }, + 'cpn': None, + 'params': _PLAYER_PARAMS['testsuite'], + }, + 'headers': { + 'Origin': BASE_URL_MOBILE, 'User-Agent': ( '{_id[package_id]}/{_id[client_version]}' ' (Linux; U;' @@ -156,31 +209,43 @@ class YouTubeRequestClient(BaseRequestsClass): # 4k no VP9 HDR # Limited subtitle availability 'android_testsuite': { - '_id': 30, '_disabled': True, - '_query_subtitles': True, + '_id': { + 'client_id': 30, + 'client_name': 'ANDROID_TESTSUITE', + 'client_version': '1.9', + 'android_sdk_version': '32', + 'os_name': 'Android', + 'os_version': '15', + 'package_id': 'com.google.android.youtube', + 'platform': 'MOBILE', + }, + '_auth_type': False, + '_use_subtitles': False, 'json': { - # 'params': _PLAYER_PARAMS['android_testsuite'], 'context': { 'client': { - 'clientName': 'ANDROID_TESTSUITE', - 'clientVersion': '1.9', - 'androidSdkVersion': '30', - 'osName': 'Android', - 'osVersion': '11', - 'platform': 'MOBILE', + 'clientName': '{_id[client_name]}', + 'clientVersion': '{_id[client_version]}', + 'androidSdkVersion': '{_id[android_sdk_version]}', + 'osName': '{_id[os_name]}', + 'osVersion': '{_id[os_version]}', + 'platform': '{_id[platform]}', }, }, + 'cpn': None, + 'params': _PLAYER_PARAMS['testsuite'], }, 'headers': { + 'Origin': BASE_URL_MOBILE, 'User-Agent': ( - 'com.google.android.youtube/' - '{json[context][client][clientVersion]}' - ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]}) gzip' + '{_id[package_id]}/{_id[client_version]}' + ' (Linux; U;' + ' {_id[os_name]} {_id[os_version]}' + ') gzip' ), - 'X-YouTube-Client-Name': '{_id}', - 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', + 'X-YouTube-Client-Name': '{_id[client_id]}', + 'X-YouTube-Client-Version': '{_id[client_version]}', }, }, # Disabled - requires PO token @@ -188,34 +253,44 @@ class YouTubeRequestClient(BaseRequestsClass): # Only for videos that allow embedding # Limited to 720p on some videos 'android_embedded': { - '_id': 55, '_disabled': True, - '_query_subtitles': 'optional', + '_id': { + 'client_id': 55, + 'client_name': 'ANDROID_EMBEDDED_PLAYER', + 'client_version': '20.10.38', + 'android_sdk_version': '32', + 'os_name': 'Android', + 'os_version': '15', + 'package_id': 'com.google.android.youtube', + 'platform': 'MOBILE', + }, + '_auth_type': False, + '_use_subtitles': 'optional', 'json': { 'context': { 'client': { - 'clientName': 'ANDROID_EMBEDDED_PLAYER', + 'clientName': '{_id[client_name]}', 'clientScreen': 'EMBED', - 'clientVersion': '19.29.37', - 'androidSdkVersion': '30', - 'osName': 'Android', - 'osVersion': '11', - 'platform': 'MOBILE', + 'clientVersion': '{_id[client_version]}', + 'androidSdkVersion': '{_id[android_sdk_version]}', + 'osName': '{_id[os_name]}', + 'osVersion': '{_id[os_version]}', + 'platform': '{_id[platform]}', }, }, 'thirdParty': { - 'embedUrl': 'https://www.youtube.com/', + 'embedUrl': BASE_URL, }, }, 'headers': { 'User-Agent': ( - 'com.google.android.youtube/' - '{json[context][client][clientVersion]}' - ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]}) gzip' + '{_id[package_id]}/{_id[client_version]}' + ' (Linux; U;' + ' {_id[os_name]} {_id[os_version]}' + ') gzip' ), - 'X-YouTube-Client-Name': '{_id}', - 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', + 'X-YouTube-Client-Name': '{_id[client_id]}', + 'X-YouTube-Client-Version': '{_id[client_version]}', }, }, 'ios': { @@ -251,8 +326,10 @@ class YouTubeRequestClient(BaseRequestsClass): 'platform': '{_id[platform]}', }, }, + 'cpn': None, }, 'headers': { + 'Origin': BASE_URL_MOBILE, 'User-Agent': ( '{_id[package_id]}/{_id[client_version]}' ' ({_id[device_model]}; U; CPU' @@ -264,11 +341,11 @@ class YouTubeRequestClient(BaseRequestsClass): 'X-YouTube-Client-Version': '{_id[client_version]}', }, }, - 'ios_youtube_tv': { + 'ios_testsuite_params': { '_id': { - 'client_id': 33, - 'client_name': 'IOS_UNPLUGGED', - 'client_version': '9.21', + 'client_id': 5, + 'client_name': 'IOS', + 'client_version': '20.20.7', 'device_make': 'Apple', 'device_model': 'iPhone16,2', 'os_name': 'iOS', @@ -276,11 +353,11 @@ class YouTubeRequestClient(BaseRequestsClass): 'os_minor': '5', 'os_patch': '0', 'os_build': '22F76', - 'package_id': 'com.google.ios.youtubeunplugged', + 'package_id': 'com.google.ios.youtube', 'platform': 'MOBILE', }, - '_auth_required': True, - '_auth_type': 'personal', + '_auth_type': False, + '_use_subtitles': 'optional', 'json': { 'context': { 'client': { @@ -298,8 +375,11 @@ class YouTubeRequestClient(BaseRequestsClass): 'platform': '{_id[platform]}', }, }, + 'cpn': None, + 'params': _PLAYER_PARAMS['testsuite'], }, 'headers': { + 'Origin': BASE_URL_MOBILE, 'User-Agent': ( '{_id[package_id]}/{_id[client_version]}' ' ({_id[device_model]}; U; CPU' @@ -311,53 +391,65 @@ class YouTubeRequestClient(BaseRequestsClass): 'X-YouTube-Client-Version': '{_id[client_version]}', }, }, - # Disabled - request are now blocked with following response - # 403 Forbidden - The caller does not have permission - # Provides progressive streams - 'media_connect_frontend': { - '_id': 95, + # Disabled - requires login but fails using OAuth2 authorisation + 'ios_youtube_tv': { '_disabled': True, - '_query_subtitles': True, - 'json': { - 'context': { - 'client': { - 'clientName': 'MEDIA_CONNECT_FRONTEND', - 'clientVersion': '0.1', - }, - }, + '_id': { + 'client_id': 33, + 'client_name': 'IOS_UNPLUGGED', + 'client_version': '9.21', + 'device_make': 'Apple', + 'device_model': 'iPhone16,2', + 'os_name': 'iOS', + 'os_major': '18', + 'os_minor': '5', + 'os_patch': '0', + 'os_build': '22F76', + 'package_id': 'com.google.ios.youtubeunplugged', + 'platform': 'MOBILE', }, - 'headers': {}, - }, - # Used to requests captions for clients that don't provide them - # Requires handling of nsig to overcome throttling (TODO) - 'smart_tv_embedded': { - '_id': 85, + '_auth_required': True, + '_auth_type': 'user', + '_use_subtitles': False, 'json': { 'context': { 'client': { - 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', - 'clientScreen': 'WATCH', - 'clientVersion': '2.0', + 'clientName': '{_id[client_name]}', + 'clientVersion': '{_id[client_version]}', + 'deviceMake': '{_id[device_make]}', + 'deviceModel': '{_id[device_model]}', + 'osName': '{_id[os_name]}', + 'osVersion': ( + '{_id[os_major]}' + '.{_id[os_minor]}' + '.{_id[os_patch]}' + '.{_id[os_build]}' + ), + 'platform': '{_id[platform]}', }, }, - 'thirdParty': { - 'embedUrl': 'https://www.google.com/', - }, }, - # Headers from a 2022 Samsung Tizen 6.5 based Smart TV 'headers': { - 'User-Agent': ('Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.5)' - ' AppleWebKit/537.36 (KHTML, like Gecko)' - ' 85.0.4183.93/6.5 TV Safari/537.36'), + 'Origin': BASE_URL_MOBILE, + 'User-Agent': ( + '{_id[package_id]}/{_id[client_version]}' + ' ({_id[device_model]}; U; CPU' + ' {_id[os_name]}' + ' {_id[os_major]}_{_id[os_minor]}_{_id[os_patch]}' + ' like Mac OS X)' + ), + 'X-YouTube-Client-Name': '{_id[client_id]}', + 'X-YouTube-Client-Version': '{_id[client_version]}', }, }, 'v1': { '_id': { 'client_id': 1, 'client_name': 'WEB', - 'client_version': '2.20250312.04.00', + 'client_version': '2.20250925.01.00', }, - 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', + '_auth_type': False, + 'url': V1_API_URL, 'method': None, 'json': { 'context': { @@ -373,29 +465,54 @@ class YouTubeRequestClient(BaseRequestsClass): }, }, 'v3': { - '_auth_requested': 'personal', - 'url': 'https://www.googleapis.com/youtube/v3/{_endpoint}', + '_auth_type': 'user', + 'url': V3_API_URL, 'method': None, - 'headers': { - 'Host': 'www.googleapis.com', - }, 'params': { 'key': None, }, }, 'tv': { '_id': { + 'browser_name': 'SamsungBrowser', + 'browser_version': '9.2', 'client_id': 7, 'client_name': 'TVHTML5', - 'client_version': '7.20250312.16.00', + 'client_version': '7.20250923.13.00', + 'device_make': 'Samsung', + 'device_model': 'SmartTV', + 'os_name': 'Tizen', + 'os_major': '4', + 'os_minor': '0', + 'os_patch': '0', + 'os_build': '2', + 'platform': 'TV', }, - 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', + '_auth_type': 'tv', + '_auth_user_agent': ( + 'Mozilla/5.0' + ' (ChromiumStylePlatform)' + ' Cobalt/25.lts.30.1034943-gold (unlike Gecko)' + ' Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)' + ), + '_use_subtitles': 'optional', + 'url': V1_API_URL, 'method': None, 'json': { 'context': { 'client': { 'clientName': '{_id[client_name]}', 'clientVersion': '{_id[client_version]}', + 'deviceMake': '{_id[device_make]}', + 'deviceModel': '{_id[device_model]}', + 'osName': '{_id[os_name]}', + 'osVersion': ( + '{_id[os_major]}' + '.{_id[os_minor]}' + '.{_id[os_patch]}' + '.{_id[os_build]}' + ), + 'platform': '{_id[platform]}', }, }, }, @@ -409,14 +526,59 @@ class YouTubeRequestClient(BaseRequestsClass): 'X-YouTube-Client-Version': '{_id[client_version]}', }, }, + # Used to requests captions for clients that don't provide them + # Requires handling of nsig to overcome throttling (TODO) 'tv_embed': { '_id': { 'client_id': 85, 'client_name': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', 'client_version': '2.0', }, - 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', + '_auth_type': 'tv', + '_auth_user_agent': ( + 'Mozilla/5.0' + ' (ChromiumStylePlatform)' + ' Cobalt/25.lts.30.1034943-gold (unlike Gecko)' + ' Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)' + ), + '_use_subtitles': True, + 'url': V1_API_URL, 'method': None, + 'json': { + 'context': { + 'client': { + 'clientName': '{_id[client_name]}', + 'clientVersion': '{_id[client_version]}', + }, + }, + 'thirdParty': { + 'embedUrl': 'https://www.google.com/', + }, + }, + 'headers': { + 'User-Agent': ( + 'Mozilla/5.0' + ' (ChromiumStylePlatform)' + ' Cobalt/Version' + ), + 'X-YouTube-Client-Name': '{_id[client_id]}', + 'X-YouTube-Client-Version': '{_id[client_version]}', + }, + }, + 'tv_unplugged': { + '_id': { + 'client_id': 65, + 'client_name': 'TVHTML5_UNPLUGGED', + 'client_version': '6.36', + }, + '_auth_type': 'user', + '_auth_user_agent': ( + 'Mozilla/5.0' + ' (ChromiumStylePlatform)' + ' Cobalt/25.lts.30.1034943-gold (unlike Gecko)' + ' Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)' + ), + '_use_subtitles': True, 'json': { 'context': { 'client': { @@ -426,6 +588,11 @@ class YouTubeRequestClient(BaseRequestsClass): }, }, 'headers': { + 'User-Agent': ( + 'Mozilla/5.0' + ' (ChromiumStylePlatform)' + ' Cobalt/Version' + ), 'X-YouTube-Client-Name': '{_id[client_id]}', 'X-YouTube-Client-Version': '{_id[client_version]}', }, @@ -444,7 +611,8 @@ class YouTubeRequestClient(BaseRequestsClass): 'os_build': '15E148', 'platform': 'MOBILE', }, - 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', + '_auth_type': False, + 'url': V1_API_URL, 'method': None, 'json': { 'context': { @@ -474,30 +642,40 @@ class YouTubeRequestClient(BaseRequestsClass): # Used for misc api requests by default # Requires handling of nsig to overcome throttling (TODO) 'web': { - '_id': 1, + '_id': { + 'client_id': 1, + 'client_name': 'WEB', + 'client_version': '2.20250925.01.00', + }, + '_auth_type': False, 'json': { 'context': { 'client': { - 'clientName': 'WEB', - 'clientVersion': '2.20250312.04.00', + 'clientName': '{_id[client_name]}', + 'clientVersion': '{_id[client_version]}', }, }, }, - # Headers for a "Galaxy S20 Ultra" from Chrome dev tools device - # emulation 'headers': { - 'User-Agent': ('Mozilla/5.0 (Linux; Android 10; SM-G981B)' - ' AppleWebKit/537.36 (KHTML, like Gecko)' - ' Chrome/80.0.3987.162 Mobile Safari/537.36'), + # UA for a "Galaxy S20 Ultra" from Chrome dev tools device + # emulation + 'User-Agent': ( + 'Mozilla/5.0 (Linux; Android 10; SM-G981B)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/140.0.0.0' + ' Mobile Safari/537.36' + ), + 'X-YouTube-Client-Name': '{_id[client_id]}', + 'X-YouTube-Client-Version': '{_id[client_version]}', }, }, 'watch_history': { '_auth_required': True, - '_auth_type': 'personal', + '_auth_type': 'user', '_video_id': None, 'headers': { 'Host': 's.youtube.com', - 'Referer': 'https://www.youtube.com/watch?v={_video_id}', + 'Referer': WATCH_URL, }, 'params': { 'referrer': 'https://accounts.google.com/', @@ -509,9 +687,36 @@ class YouTubeRequestClient(BaseRequestsClass): 'muted': '0', }, }, + 'generate_204': { + 'url': BASE_URL + '/generate_204', + 'method': 'HEAD', + 'headers': { + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'Accept': '*/*', + 'Accept-Language': 'en-US,en;q=0.5', + 'User-Agent': ( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/140.0.0.0' + ' Safari/537.36' + ), + }, + 'cache': False, + }, '_common': { - '_access_token': None, - '_access_token_tv': None, + '_access_tokens': { + 'dev': None, + 'tv': None, + 'user': None, + 'vr': None, + }, + '_api_keys': { + 'dev': None, + 'tv': None, + 'user': None, + 'vr': None, + }, 'json': { 'contentCheckOk': True, 'context': { @@ -536,12 +741,12 @@ class YouTubeRequestClient(BaseRequestsClass): 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.5', - 'Authorization': 'Bearer {{0}}', + 'Authorization': None, 'User-Agent': ( - 'Mozilla/5.0 (Linux; Android 10; SM-G981B)' + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' ' AppleWebKit/537.36 (KHTML, like Gecko)' - ' Chrome/80.0.3987.162' - ' Mobile Safari/537.36' + ' Chrome/140.0.0.0' + ' Safari/537.36' ), }, 'params': { @@ -551,29 +756,40 @@ class YouTubeRequestClient(BaseRequestsClass): }, } - def __init__(self, - context, - language=None, - region=None, - exc_type=None, - **_kwargs): - common_client = self.CLIENTS['_common']['json']['context']['client'] - # the default language is always en_US (like YouTube on the WEB) - language = language.replace('-', '_') if language else 'en_US' - self._language = common_client['hl'] = language - self._region = common_client['gl'] = region if region else 'US' - - if isinstance(exc_type, tuple): - exc_type = (YouTubeException,) + exc_type - elif exc_type: - exc_type = (YouTubeException, exc_type) - else: - exc_type = (YouTubeException,) + _language = 'en_US' + _region = 'US' + def __init__(self, language='en_US', region='US', exc_type=None, **kwargs): super(YouTubeRequestClient, self).__init__( - context=context, - exc_type=exc_type, - ) + exc_type=( + (YouTubeException,) + exc_type + if isinstance(exc_type, tuple) else + (YouTubeException, exc_type) + if exc_type else + (YouTubeException,) + ), + **kwargs) + YouTubeRequestClient.init(language=language, region=region) + + @classmethod + def init(cls, + language='en_US', + region='US', + **_kwargs): + common_client = cls.CLIENTS['_common']['json']['context']['client'] + # the default language is always en_US (like YouTube on the WEB) + common_client['hl'] = 'en_US' + cls._language = language.replace('-', '_') + cls._region = common_client['gl'] = region + + def reinit(self, **kwargs): + super(YouTubeRequestClient, self).reinit(**kwargs) + + def get_language(self): + return self._language + + def get_region(self): + return self._region @classmethod def json_traverse(cls, json_data, path, default=None): @@ -587,26 +803,21 @@ def json_traverse(cls, json_data, path, default=None): if isinstance(keys, slice): next_key = path[idx + 1] + parts = result[keys] if next_key is None: - for part in result[keys]: - new_result = cls.json_traverse( - part, - path[idx + 2:], - default=default, - ) + new_path = path[idx + 2:] + for part in parts: + new_result = cls.json_traverse(part, new_path, default) if not new_result or new_result == default: continue return new_result if isinstance(next_key, range_type): results_limit = len(next_key) + new_path = path[idx + 2:] new_results = [] - for part in result[keys]: - new_result = cls.json_traverse( - part, - path[idx + 2:], - default=default, - ) + for part in parts: + new_result = cls.json_traverse(part, new_path, default) if not new_result or new_result == default: continue new_results.append(new_result) @@ -615,9 +826,10 @@ def json_traverse(cls, json_data, path, default=None): break results_limit -= 1 else: + new_path = path[idx + 1:] new_results = [ - cls.json_traverse(part, path[idx + 1:], default=default) - for part in result[keys] + cls.json_traverse(part, new_path, default) + for part in parts if part ] return new_results @@ -627,7 +839,7 @@ def json_traverse(cls, json_data, path, default=None): for key in keys: if isinstance(key, tuple): - new_result = cls.json_traverse(result, key, default=default) + new_result = cls.json_traverse(result, key, default) if new_result: result = new_result break @@ -636,9 +848,14 @@ def json_traverse(cls, json_data, path, default=None): try: if callable(key): result = key(result) + elif isinstance(key, dict): + result = next( + param for param in result + if param.get(key['name']) in key['match'] + )[key['out']] else: result = result[key] - except (KeyError, IndexError, TypeError): + except (KeyError, IndexError, StopIteration, TypeError): continue break else: @@ -655,66 +872,96 @@ def build_client(cls, client_name=None, data=None): base_client = None if client_name: base_client = cls.CLIENTS.get(client_name) - if base_client and base_client.get('_disabled'): + if not base_client or base_client.get('_disabled'): return None if not base_client: base_client = YouTubeRequestClient.CLIENTS['web'] auth_required = base_client.get('_auth_required') auth_requested = base_client.get('_auth_requested') + auth_type = base_client.get('_auth_type') if data: base_client = merge_dicts(base_client, data) client = merge_dicts(cls.CLIENTS['_common'], base_client, templates) client['_name'] = client_name - if auth_required: + if auth_required is not None: client['_auth_required'] = auth_required - if auth_requested: + if auth_requested is not None: client['_auth_requested'] = auth_requested + if auth_type is not None: + client['_auth_type'] = auth_type + + headers = client.get('headers') + client_json = client.get('json') + if client_json: + if 'cpn' in client_json: + cpn = client.get('_cpn') + if cpn: + client_json['cpn'] = cpn + else: + client_json = client_json.copy() + del client_json['cpn'] + client['json'] = client_json + + client_config = cls.json_traverse( + client_json, + ('context', 'client'), + ) + playback_context = cls.json_traverse( + client_json, + ('playbackContext', 'contentPlaybackContext'), + ) + else: + client_config = None + playback_context = None visitor_data = client.get('_visitor_data') if visitor_data: - client['json']['context']['client']['visitorData'] = visitor_data + if client_config is not None: + client_config['visitorData'] = visitor_data + if headers is not None: + headers['X-Goog-Visitor-Id'] = visitor_data for values, template_id, template in templates.values(): if template_id in values: values[template_id] = template.format(**client) - has_auth = False + has_auth = None try: params = client['params'] auth_required = client.get('_auth_required') auth_requested = client.get('_auth_requested') auth_type = client.get('_auth_type') - if auth_type == 'tv' and auth_requested != 'personal': - auth_token = client.get('_access_token_tv') - api_key = client.get('_api_key_tv') - elif auth_type is not False: - auth_token = client.get('_access_token') - api_key = client.get('_api_key') + if auth_type: + auth_token = client.get('_access_tokens', {}).get(auth_type) + api_key = client.get('_api_keys', {}).get(auth_type) else: auth_token = None api_key = None if auth_token and (auth_required or auth_requested): - headers = client['headers'] - if 'Authorization' in headers: + if headers is not None and 'Authorization' in headers: headers = headers.copy() auth_header = headers.get('Authorization') or 'Bearer {0}' headers['Authorization'] = auth_header.format(auth_token) + + auth_user_agent = client.get('_auth_user_agent') + if auth_user_agent: + headers['User-Agent'] = auth_user_agent + client['headers'] = headers - has_auth = True + has_auth = auth_type if 'key' in params: params = params.copy() del params['key'] client['params'] = params - elif auth_required: + elif auth_required and auth_required != 'ignore_fail': return None else: - headers = client['headers'] - if 'Authorization' in headers: + if headers is not None and 'Authorization' in headers: headers = headers.copy() del headers['Authorization'] client['headers'] = headers @@ -732,3 +979,35 @@ def build_client(cls, client_name=None, data=None): client['_has_auth'] = has_auth return client + + def internet_available(self, notify=True): + response = self.request(**self.CLIENTS['generate_204']) + if response is not None: + with response: + if response.status_code == 204: + return True + if notify: + self._context.get_ui().show_notification( + self._context.localize('internet.connection.required') + ) + return False + + @classmethod + def _normalize_url(cls, url): + if not url: + url = '' + elif url.startswith(('http://', 'https://')): + pass + elif url.startswith('//'): + url = urljoin('https:', url) + elif url.startswith('/'): + url = urljoin(cls.BASE_URL, url) + return url + + @classmethod + def _unescape(cls, text): + try: + text = unescape(text) + except Exception: + cls.log.error(('Failed', 'Text: %r'), text) + return text diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/subtitles.py similarity index 69% rename from plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/subtitles.py rename to plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/subtitles.py index aa3f3ab63a..847882c7d5 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/subtitles.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2017-2021 plugin.video.youtube + Copyright (C) 2017-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,21 +10,15 @@ import os -from ...kodion.compatibility import ( - parse_qs, - unescape, - urlencode, - urljoin, - urlsplit, - xbmcvfs, -) +from .request_client import YouTubeRequestClient +from ...kodion import logging +from ...kodion.compatibility import parse_qs, urlencode, urlsplit, xbmcvfs from ...kodion.constants import ( PLAY_PROMPT_SUBTITLES, TEMP_PATH, TRANSLATION_LANGUAGES, ) -from ...kodion.network import BaseRequestsClass -from ...kodion.utils import make_dirs +from ...kodion.utils.file_system import make_dirs SUBTITLE_OPTIONS = { @@ -57,24 +51,43 @@ } -class Subtitles(object): +class Subtitles(YouTubeRequestClient): + log = logging.getLogger(__name__) + BASE_PATH = make_dirs(TEMP_PATH) FORMATS = { - '_default': None, + # '_default': None, + # '_fallback': None, + 'srt': { + # 'mime_type': 'application/x-subrip', + # Fake mimetype to allow ISA to decode as WebVTT + 'mime_type': 'text/vtt', + 'extension': 'srt', + # Fake @codecs to allow ISA to decode as WebVTT + 'codec': 'wvtt', + }, 'vtt': { 'mime_type': 'text/vtt', 'extension': 'vtt', + 'codec': 'wvtt', }, 'ttml': { 'mime_type': 'application/ttml+xml', 'extension': 'ttml', + 'codec': 'ttml', }, } - def __init__(self, context, video_id): + def __init__(self, context, video_id, use_mpd=None): + settings = context.get_settings() + super(Subtitles, self).__init__( + context=context, + language=settings.get_language(), + region=settings.get_region(), + ) + self.video_id = video_id - self._context = context self.defaults = None self.headers = None @@ -82,16 +95,41 @@ def __init__(self, context, video_id): self.caption_tracks = None self.translation_langs = None - settings = context.get_settings() self.pre_download = settings.subtitle_download() self.sub_selection = settings.get_subtitle_selection() + stream_features = settings.stream_features() + if use_mpd is None: + use_mpd = settings.use_mpd_videos() + + use_isa = not self.pre_download and use_mpd + self.use_isa = use_isa + default_format = None + fallback_format = None + if use_isa: + if ('ttml' in stream_features + and context.inputstream_adaptive_capabilities('ttml')): + default_format = 'ttml' + fallback_format = 'ttml' + + if context.inputstream_adaptive_capabilities('vtt'): + if 'vtt' in stream_features: + default_format = default_format or 'vtt' + fallback_format = 'vtt' + else: + default_format = default_format or 'srt' + fallback_format = 'srt' + + if not default_format or not use_isa: + if ('vtt' in stream_features + and context.get_system_version().compatible(20)): + default_format = 'vtt' + fallback_format = 'vtt' + else: + default_format = 'srt' + fallback_format = 'srt' - if (not self.pre_download - and settings.use_mpd_videos() - and context.inputstream_adaptive_capabilities('ttml')): - self.FORMATS['_default'] = 'ttml' - else: - self.FORMATS['_default'] = 'vtt' + self.FORMATS['_default'] = default_format + self.FORMATS['_fallback'] = fallback_format kodi_sub_lang = context.get_subtitle_language() plugin_lang = settings.get_language() @@ -108,8 +146,9 @@ def __init__(self, context, video_id): else: self.preferred_lang = ('en',) - ui = context.get_ui() - self.prompt_override = bool(ui.pop_property(PLAY_PROMPT_SUBTITLES)) + self.prompt_override = bool( + context.get_ui().pop_property(PLAY_PROMPT_SUBTITLES) + ) def load(self, captions, headers=None): if headers: @@ -186,14 +225,6 @@ def load(self, captions, headers=None): self.defaults['base_lang'] = base_lang break - def _unescape(self, text): - try: - text = unescape(text) - except Exception: - self._context.log_error('Subtitles._unescape - failed: |{text}|' - .format(text=text)) - return text - def get_lang_details(self): return { 'default': self.defaults['default_lang'], @@ -257,7 +288,7 @@ def get_subtitles(self): track_key = '_'.join((track_lang, track_kind)) else: track_key = track_lang - url, mime_type = self._get_url(track=track, lang=track_lang) + url, sub_format = self._get_url(track=track, lang=track_lang) if url: subtitles[track_key] = { 'default': track_lang in preferred_lang, @@ -265,9 +296,12 @@ def get_subtitles(self): 'kind': track_kind, 'lang': track_lang, 'language': track_language, - 'mime_type': mime_type, + 'mime_type': sub_format['mime_type'], + 'codec': sub_format['codec'], 'url': url, } + if subtitles and self.use_isa: + subtitles['_headers'] = self.headers return subtitles def get_all(self): @@ -280,7 +314,7 @@ def get_all(self): track_lang = track.get('languageCode') track_kind = track.get('kind') track_language = self._get_language_name(track) - url, mime_type = self._get_url(track=track) + url, sub_format = self._get_url(track=track) if url: if track_kind: track_key = '_'.join((track_lang, track_kind)) @@ -292,33 +326,35 @@ def get_all(self): 'kind': track_kind, 'lang': track_lang, 'language': track_language, - 'mime_type': mime_type, + 'mime_type': sub_format['mime_type'], + 'codec': sub_format['codec'], 'url': url, } - base_track = self.defaults['base'] + base = self.defaults['base'] base_lang = self.defaults['base_lang'] - if not base_track: - return subtitles - - for track in self.translation_langs: - track_lang = track.get('languageCode') - if not track_lang or track_lang in subtitles: - continue - track_language = self._get_language_name(track) - url, mime_type = self._get_url(track=base_track, lang=track_lang) - if url: - track_key = '_'.join((base_lang, track_lang)) - subtitles[track_key] = { - 'default': track_lang in preferred_lang, - 'original': track_lang == original_lang, - 'kind': 'translation', - 'lang': track_lang, - 'language': track_language, - 'mime_type': mime_type, - 'url': url, - } + if base: + for track in self.translation_langs: + track_lang = track.get('languageCode') + if not track_lang or track_lang in subtitles: + continue + track_language = self._get_language_name(track) + url, sub_format = self._get_url(track=base, lang=track_lang) + if url: + track_key = '_'.join((base_lang, track_lang)) + subtitles[track_key] = { + 'default': track_lang in preferred_lang, + 'original': track_lang == original_lang, + 'kind': 'translation', + 'lang': track_lang, + 'language': track_language, + 'mime_type': sub_format['mime_type'], + 'codec': sub_format['codec'], + 'url': url, + } + if subtitles and self.use_isa: + subtitles['_headers'] = self.headers return subtitles def _prompt(self): @@ -335,14 +371,14 @@ def _prompt(self): num_total = num_captions + num_translations if not num_total: - self._context.log_debug('Subtitles._prompt' - ' - No subtitles found for prompt') + self.log.debug('No subtitles found for prompt') else: - translation_lang = self._context.localize('subtitles.translation') + localize = self._context.localize choice = self._context.get_ui().on_select( - self._context.localize('subtitles.language'), + localize('subtitles.language'), [name for _, name in captions] + - [translation_lang % name for _, name in translations] + [localize('subtitles.translation.x', name) + for _, name in translations] ) if 0 <= choice < num_captions: @@ -354,30 +390,36 @@ def _prompt(self): track_kind = 'translation' choice = translations[choice - num_captions] else: - self._context.log_debug('Subtitles._prompt' - ' - Subtitle selection cancelled') + self.log.debug('Subtitle selection cancelled') return False lang, language = choice - self._context.log_debug('Subtitles._prompt - selected: |{lang}|' - .format(lang=lang)) - url, mime_type = self._get_url(track=track, lang=lang) + self.log.debug('Selected: %r', lang) + url, sub_format = self._get_url(track=track, lang=lang) if url: - return { + subtitle = { lang: { 'default': True, 'original': lang == self.defaults['original_lang'], 'kind': track_kind, 'lang': lang, 'language': language, - 'mime_type': mime_type, + 'mime_type': sub_format['mime_type'], + 'codec': sub_format['codec'], 'url': url, }, } + if self.use_isa: + subtitle['_headers'] = self.headers + return subtitle return None def _get_url(self, track, lang=None): - sub_format = self.FORMATS['_default'] + sub_format = self.FORMATS.get('_default') + if not sub_format: + self.log.error_trace('Invalid subtitle options selected') + return None, None + tlang = None base_lang = track.get('languageCode') kind = track.get('kind') @@ -386,7 +428,7 @@ def _get_url(self, track, lang=None): lang = '-'.join((base_lang, tlang)) elif kind == 'asr': lang = '-'.join((base_lang, kind)) - sub_format = 'vtt' + sub_format = self.FORMATS['_fallback'] else: lang = base_lang @@ -398,62 +440,62 @@ def _get_url(self, track, lang=None): self.FORMATS[sub_format]['extension'] )) if not self.BASE_PATH: - self._context.log_error('Subtitles._get_url' - ' - Unable to access temp directory') + self.log.error_trace('Unable to access temp directory') return None, None file_path = os.path.join(self.BASE_PATH, filename) if xbmcvfs.exists(file_path): - self._context.log_debug('Subtitles._get_url' - ' - Use existing subtitle for: |{lang}|' - '\n\tFile: {file}' - .format(lang=lang, file=file_path)) - return file_path, self.FORMATS[sub_format]['mime_type'] + self.log.debug(('Use existing subtitle for: {lang!r}', + 'File: {file}'), + lang=lang, + file=file_path) + return file_path, self.FORMATS[sub_format] base_url = self._normalize_url(track.get('baseUrl')) if not base_url: - self._context.log_error('Subtitles._get_url - no URL for: |{lang}|' - .format(lang=lang)) + self.log.error_trace('No URL for: %r', lang) return None, None subtitle_url = self._set_query_param( base_url, - ('type', 'track'), ('fmt', sub_format), - ('tlang', tlang) if tlang else (None, None), + ('tlang', tlang), + ('xosf', None), ) - self._context.log_debug('Subtitles._get_url' - ' - found new subtitle for: |{lang}|' - '\n\tURL: {url}' - .format(lang=lang, url=subtitle_url)) + self.log.debug(('Found new subtitle for: {lang!r}', + 'URL: {url}'), + lang=lang, + url=subtitle_url) if not download: - return subtitle_url, self.FORMATS[sub_format]['mime_type'] + return subtitle_url, self.FORMATS[sub_format] - response = BaseRequestsClass(context=self._context).request( + response = self.request( subtitle_url, headers=self.headers, - error_info=('Subtitles._get_url - GET failed for: |{lang}|' - '\n\tException: {{exc!r}}' - .format(lang=lang)) + error_title='Failed to download subtitle for: {sub_lang!r}', + sub_lang=lang, ) - response = response and response.text - if not response: + if response is None: return None, None + with response: + response_text = response.text + if not response_text: + return None, None - output = bytearray(self._unescape(response), + output = bytearray(self._unescape(response_text), encoding='utf8', errors='ignore') try: with xbmcvfs.File(file_path, 'w') as sub_file: success = sub_file.write(output) except (IOError, OSError): - self._context.log_error('Subtitles._get_url' - ' - write failed for: |{lang}|' - '\n\tFile: {file}' - .format(lang=lang, file=file_path)) + self.log.exception(('Write failed for: {lang!r}', + 'File: {file}'), + lang=lang, + file=file_path) if success: - return file_path, self.FORMATS[sub_format]['mime_type'] + return file_path, self.FORMATS[sub_format] return None, None def _get_track(self, @@ -505,9 +547,7 @@ def _get_track(self, if sel_track: return sel_track, sel_lang, sel_language, sel_kind - self._context.log_debug('Subtitles._get_track' - ' - no subtitle for: |{lang}|' - .format(lang=lang)) + self.log.debug('No subtitle for: %r', lang) return None, None, None, None @staticmethod @@ -549,21 +589,15 @@ def _set_query_param(url, *pairs): query_params = parse_qs(components.query) for name, value in pairs: - if name: + if not name: + continue + if isinstance(value, (list, tuple)): + query_params[name] = value + elif value is not None: query_params[name] = [value] + elif name in query_params: + del query_params[name] return components._replace( query=urlencode(query_params, doseq=True) ).geturl() - - @staticmethod - def _normalize_url(url): - if not url: - url = '' - elif url.startswith(('http://', 'https://')): - pass - elif url.startswith('//'): - url = urljoin('https:', url) - elif url.startswith('/'): - url = urljoin('https://www.youtube.com', url) - return url diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/__init__.py index 3fff8d2153..4e391607b4 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py index 876d6b43a8..127aa50ae6 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py @@ -13,13 +13,7 @@ import re -try: - from ....kodion.logger import Logger -except: - class Logger(object): - @staticmethod - def log_debug(txt): - print(txt) +from ....kodion import logging def throttling_reverse(arr): @@ -259,17 +253,17 @@ def get_throttling_function_code(js): # This pattern is only present in the throttling function code. fiduciary_index = js.find('enhanced_except_') if fiduciary_index == -1: - Logger.log_debug('ratebypass: fiduciary_index not found') + logging.debug('fiduciary_index not found') return None start_index = js.rfind('=function(', 0, fiduciary_index) if start_index == -1: - Logger.log_debug('ratebypass: function code start not found') + logging.debug('function code start not found') return None end_index = js.find('};', fiduciary_index) if end_index == -1: - Logger.log_debug('ratebypass: function code end not found') + logging.debug('function code end not found') return None return js[start_index:end_index].replace('\n', '') @@ -294,7 +288,7 @@ def get_throttling_plan_gen(raw_code): plan_start_pattern = 'try{' plan_start_index = raw_code.find(plan_start_pattern) if plan_start_index == -1: - Logger.log_debug('ratebypass: command block start not found') + logging.debug('command block start not found') raise Exception() else: # Skip the whole start pattern, it's not needed. @@ -302,7 +296,7 @@ def get_throttling_plan_gen(raw_code): plan_end_index = raw_code.find('}', plan_start_index) if plan_end_index == -1: - Logger.log_debug('ratebypass: command block end not found') + logging.debug('command block end not found') raise Exception() plan_code = raw_code[plan_start_index:plan_end_index] @@ -365,14 +359,14 @@ def get_throttling_function_array(cls, mutable_n_list, raw_code): array_start_pattern = ",c=[" array_start_index = raw_code.find(array_start_pattern) if array_start_index == -1: - Logger.log_debug('ratebypass: "c" array pattern not found') + logging.debug('"c" array pattern not found') raise Exception() else: array_start_index += len(array_start_pattern) array_end_index = raw_code.rfind('];') if array_end_index == -1: - Logger.log_debug('ratebypass: "c" array end not found') + logging.debug('"c" array end not found') raise Exception() array_code = raw_code[array_start_index:array_end_index] @@ -404,8 +398,7 @@ def get_throttling_function_array(cls, mutable_n_list, raw_code): found = True break else: - Logger.log_debug('ratebypass: mapping function not yet ' - 'listed: {unknown}'.format(unknown=el)) + logging.debug('unknown mapping function: %s', el) if found: continue @@ -428,16 +421,14 @@ def calculate_n(self, mutable_n_list): video stream URL. """ if self.calculated_n: - Logger.log_debug('`n` already calculated: {calculated_n}. returning early...' - .format(calculated_n=self.calculated_n)) + logging.debug('Reusing calculated "n": %s', self.calculated_n) return self.calculated_n if not self.throttling_function_code: return None - initial_n_string = ''.join(mutable_n_list) - Logger.log_debug('Attempting to calculate `n` from initial: {initial_n}' - .format(initial_n=initial_n_string)) + logging.debug('Attempting to calculate "n" from initial: %s', + ''.join(mutable_n_list)) # For each step in the plan, get the first item of the step as the # index of the function to call, and then call that function using @@ -445,13 +436,13 @@ def calculate_n(self, mutable_n_list): try: throttling_array = self.get_throttling_function_array( mutable_n_list, - self.throttling_function_code) + self.throttling_function_code + ) for step in self.get_throttling_plan_gen(self.throttling_function_code): curr_func = throttling_array[int(step[0])] if not callable(curr_func): - Logger.log_debug('{curr_func} is not callable.'.format(curr_func=curr_func)) - Logger.log_debug('Throttling array:\n{throttling_array}\n' - .format(throttling_array=throttling_array)) + logging.debug('%s is not callable', curr_func) + logging.debug(('Throttling array:', '%r'), throttling_array) return None first_arg = throttling_array[int(step[1])] @@ -461,11 +452,10 @@ def calculate_n(self, mutable_n_list): elif len(step) == 3: second_arg = throttling_array[int(step[2])] curr_func(first_arg, second_arg) - except: - Logger.log_debug('Error calculating new `n`') + except Exception: + logging.exception('Error calculating "n"') return None self.calculated_n = ''.join(mutable_n_list) - Logger.log_debug('Calculated `n`: {calculated_n}' - .format(calculated_n=self.calculated_n)) + logging.debug('Calculated "n": %s', self.calculated_n) return self.calculated_n diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 7aab5c9c93..35c97d250f 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,29 +10,36 @@ from __future__ import absolute_import, division, unicode_literals +from itertools import chain + from .utils import get_thumbnail +from ...kodion import logging +from ...kodion.constants import CHANNEL_ID, FANART_TYPE, INCOGNITO class ResourceManager(object): - def __init__(self, provider, context, progress_dialog=None): + log = logging.getLogger(__name__) + + def __init__(self, provider, context, client, progress_dialog=None): self._provider = provider self._context = context + self._client = client self._progress_dialog = progress_dialog self.new_data = {} params = context.get_params() - self._incognito = params.get('incognito') + self._incognito = params.get(INCOGNITO) - fanart_type = params.get('fanart_type') + fanart_type = params.get(FANART_TYPE) settings = context.get_settings() if fanart_type is None: fanart_type = settings.fanart_selection() self._channel_fanart = fanart_type == settings.FANART_CHANNEL self._thumb_size = settings.get_thumbnail_size() - def context_changed(self, context): - return self._context != context + def context_changed(self, context, client): + return self._context != context or self._client != client def update_progress_dialog(self, progress_dialog): old_progress_dialog = self._progress_dialog @@ -53,10 +60,18 @@ def _list_batch(self, input_list, n=50): def get_channels(self, ids, suppress_errors=False, defer_cache=False): context = self._context - client = self._provider.get_client(context) + client = self._client data_cache = context.get_data_cache() function_cache = context.get_function_cache() + refresh = context.refresh_requested() + forced_cache = not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) + refresh = not forced_cache and refresh + updated = [] handles = {} for identifier in ids: @@ -82,18 +97,26 @@ def get_channels(self, ids, suppress_errors=False, defer_cache=False): if refresh or not ids: result = {} else: - result = data_cache.get_items(ids, data_cache.ONE_DAY) - to_update = [id_ for id_ in ids - if id_ not in result - or not result[id_] - or result[id_].get('_partial')] + result = data_cache.get_items( + ids, + None if forced_cache else data_cache.ONE_DAY, + ) + to_update = ( + [] + if forced_cache else + [id_ for id_ in ids + if id_ + and (id_ not in result + or not result[id_] + or result[id_].get('_partial'))] + ) if result: - context.debug_log and context.log_debug( - 'ResourceManager.get_channels' - ' - Using cached data for {num} channel(s)' - '\n\tChannel IDs: {ids}' - .format(num=len(result), ids=list(result)) + self.log.debugging and self.log.debug( + ('Using cached data for {num} channel(s)', + 'Channel IDs: {ids}'), + num=len(result), + ids=list(result), ) if self._progress_dialog: self._progress_dialog.update(steps=len(result) - len(to_update)) @@ -118,11 +141,11 @@ def get_channels(self, ids, suppress_errors=False, defer_cache=False): new_data = None if new_data: - context.debug_log and context.log_debug( - 'ResourceManager.get_channels' - ' - Retrieved new data for {num} channel(s)' - '\n\tChannel IDs: {ids}' - .format(num=len(to_update), ids=to_update) + self.log.debugging and self.log.debug( + ('Retrieved new data for {num} channel(s)', + 'Channel IDs: {ids}'), + num=len(to_update), + ids=to_update, ) result.update(new_data) self.cache_data(new_data, defer=defer_cache) @@ -144,37 +167,55 @@ def get_channel_info(self, suppress_errors=False, defer_cache=False): context = self._context + client = self._client + function_cache = context.get_function_cache() + refresh = context.refresh_requested() + forced_cache = not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) + refresh = not forced_cache and refresh + if not refresh and channel_data: result = channel_data else: result = {} to_check = [id_ for id_ in ids - if id_ not in result - or not result[id_] - or result[id_].get('_partial')] + if id_ + and (id_ not in result + or not result[id_] + or result[id_].get('_partial'))] if to_check: data_cache = context.get_data_cache() - result.update(data_cache.get_items(to_check, data_cache.ONE_MONTH)) - to_update = [id_ for id_ in ids - if id_ not in result - or not result[id_] - or result[id_].get('_partial')] + result.update(data_cache.get_items( + to_check, + None if forced_cache else data_cache.ONE_MONTH, + )) + to_update = ( + [] + if forced_cache else + [id_ for id_ in ids + if id_ + and (id_ not in result + or not result[id_] + or result[id_].get('_partial'))] + ) if result: - context.debug_log and context.log_debug( - 'ResourceManager.get_channel_info' - ' - Using cached data for {num} channel(s)' - '\n\tChannel IDs: {ids}' - .format(num=len(result), ids=list(result)) + self.log.debugging and self.log.debug( + ('Using cached data for {num} channel(s)', + 'Channel IDs: {ids}'), + num=len(result), + ids=list(result), ) if self._progress_dialog: self._progress_dialog.update(steps=len(result) - len(to_update)) if to_update: notify_and_raise = not suppress_errors - client = self._provider.get_client(context) new_data = [client.get_channels(list_of_50, max_results=50, notify=notify_and_raise, @@ -193,11 +234,11 @@ def get_channel_info(self, new_data = None if new_data: - context.debug_log and context.log_debug( - 'ResourceManager.get_channel_info' - ' - Retrieved new data for {num} channel(s)' - '\n\tChannel IDs: {ids}' - .format(num=len(to_update), ids=to_update) + self.log.debugging and self.log.debug( + ('Retrieved new data for {num} channel(s)', + 'Channel IDs: {ids}'), + num=len(to_update), + ids=to_update, ) result.update(new_data) self.cache_data(new_data, defer=defer_cache) @@ -241,32 +282,50 @@ def get_channel_info(self, return result def get_playlists(self, ids, suppress_errors=False, defer_cache=False): - context = self._context ids = tuple(ids) + + context = self._context + client = self._client + function_cache = context.get_function_cache() + refresh = context.refresh_requested() + forced_cache = not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) + refresh = not forced_cache and refresh + if refresh or not ids: result = {} else: data_cache = context.get_data_cache() - result = data_cache.get_items(ids, data_cache.ONE_DAY) - to_update = [id_ for id_ in ids - if id_ not in result - or not result[id_] - or result[id_].get('_partial')] + result = data_cache.get_items( + ids, + None if forced_cache else data_cache.ONE_DAY, + ) + to_update = ( + [] + if forced_cache else + [id_ for id_ in ids + if id_ + and (id_ not in result + or not result[id_] + or result[id_].get('_partial'))] + ) if result: - context.debug_log and context.log_debug( - 'ResourceManager.get_playlists' - ' - Using cached data for {num} playlist(s)' - '\n\tPlaylist IDs: {ids}' - .format(num=len(result), ids=list(result)) + self.log.debugging and self.log.debug( + ('Using cached data for {num} playlist(s)', + 'Playlist IDs: {ids}'), + num=len(result), + ids=list(result), ) if self._progress_dialog: self._progress_dialog.update(steps=len(result) - len(to_update)) if to_update: notify_and_raise = not suppress_errors - client = self._provider.get_client(context) new_data = [client.get_playlists(list_of_50, max_results=50, notify=notify_and_raise, @@ -285,11 +344,11 @@ def get_playlists(self, ids, suppress_errors=False, defer_cache=False): new_data = None if new_data: - context.debug_log and context.log_debug( - 'ResourceManager.get_playlists' - ' - Retrieved new data for {num} playlist(s)' - '\n\tPlaylist IDs: {ids}' - .format(num=len(to_update), ids=to_update) + self.log.debugging and self.log.debug( + ('Retrieved new data for {num} playlist(s)', + 'Playlist IDs: {ids}'), + num=len(to_update), + ids=to_update, ) result.update(new_data) self.cache_data(new_data, defer=defer_cache) @@ -309,16 +368,31 @@ def get_playlist_items(self, ids=None, batch_id=None, page_token=None, - defer_cache=False): + defer_cache=False, + flatten=False, + **kwargs): if not ids and not batch_id: return None context = self._context + client = self._client + function_cache = context.get_function_cache() + refresh = context.refresh_requested() + forced_cache = ( + not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) + or (context.get_param(CHANNEL_ID) == 'mine' + and not client.logged_in) + ) + refresh = not forced_cache and refresh if batch_id: ids = [batch_id[0]] - page_token = batch_id[1] + page_token = batch_id[1] or page_token fetch_next = False elif page_token is None: fetch_next = True @@ -342,28 +416,44 @@ def get_playlist_items(self, else: batch = data_cache.get_item( '{0},{1}'.format(*batch_id), - data_cache.ONE_HOUR if page_token - else data_cache.ONE_MINUTE * 5 + as_dict=True, ) + if not batch: + if not forced_cache: + to_update.append(batch_id) + break + age = batch.get('age') + batch = batch.get('value') if not batch: to_update.append(batch_id) break - result[batch_id] = batch + elif forced_cache: + result[batch_id] = batch + elif page_token: + if age <= data_cache.ONE_DAY: + result[batch_id] = batch + else: + to_update.append(batch_id) + break + else: + if age <= data_cache.ONE_MINUTE * 5: + result[batch_id] = batch + else: + to_update.append(batch_id) page_token = batch.get('nextPageToken') if fetch_next else None - if page_token is None: + if not page_token: break if result: - context.debug_log and context.log_debug( - 'ResourceManager.get_playlist_items' - ' - Using cached data for {num} playlist part(s)' - '\n\tBatch IDs: {ids}' - .format(num=len(result), ids=list(result)) + self.log.debugging and self.log.debug( + ('Using cached data for {num} playlist part(s)', + 'Batch IDs: {ids}'), + num=len(result), + ids=list(result), ) if self._progress_dialog: self._progress_dialog.update(steps=len(result) - len(to_update)) - client = self._provider.get_client(context) new_data = {} insert_point = 0 for playlist_id, page_token in to_update: @@ -372,13 +462,15 @@ def get_playlist_items(self, insert_point = batch_ids.index(batch_id, insert_point) while 1: batch_id = (playlist_id, page_token) - batch = client.get_playlist_items(*batch_id) + if batch_id in result: + break + batch = client.get_playlist_items(*batch_id, **kwargs) if not batch: break new_batch_ids.append(batch_id) new_data[batch_id] = batch page_token = batch.get('nextPageToken') if fetch_next else None - if page_token is None: + if not page_token: break if new_batch_ids: @@ -386,11 +478,11 @@ def get_playlist_items(self, insert_point += len(new_batch_ids) if new_data: - context.debug_log and context.log_debug( - 'ResourceManager.get_playlist_items' - ' - Retrieved new data for {num} playlist part(s)' - '\n\tBatch IDs: {ids}' - .format(num=len(new_data), ids=list(new_data)) + self.log.debugging and self.log.debug( + ('Retrieved new data for {num} playlist part(s)', + 'Batch IDs: {ids}'), + num=len(new_data), + ids=list(new_data), ) result.update(new_data) self.cache_data({ @@ -409,6 +501,14 @@ def get_playlist_items(self, if not fetch_next: return result[batch_ids[0]] + if flatten: + items = chain.from_iterable( + batch.get('items', []) + for batch in result.values() + ) + result = result[batch_ids[-1]] + result['items'] = list(items) + return result return result def get_related_playlists(self, channel_id, defer_cache=False): @@ -428,10 +528,7 @@ def get_related_playlists(self, channel_id, defer_cache=False): return item.get('contentDetails', {}).get('relatedPlaylists') def get_my_playlists(self, channel_id, page_token, defer_cache=False): - context = self._context - client = self._provider.get_client(context) - - result = client.get_playlists_of_channel(channel_id, page_token) + result = self._client.get_playlists_of_channel(channel_id, page_token) if not result: return None @@ -441,11 +538,11 @@ def get_my_playlists(self, channel_id, page_token, defer_cache=False): if yt_item } if new_data: - context.debug_log and context.log_debug( - 'ResourceManager.get_my_playlists' - ' - Retrieved new data for {num} playlist(s)' - '\n\tPlaylist IDs: {ids}' - .format(num=len(new_data), ids=list(new_data)) + self.log.debugging and self.log.debug( + ('Retrieved new data for {num} playlist(s)', + 'Playlist IDs: {ids}'), + num=len(new_data), + ids=list(new_data), ) self.cache_data(new_data, defer=defer_cache) @@ -456,34 +553,54 @@ def get_videos(self, live_details=False, suppress_errors=False, defer_cache=False, - yt_items=None): - context = self._context + yt_items_dict=None): ids = tuple(ids) + + context = self._context + client = self._client + function_cache = context.get_function_cache() + refresh = context.refresh_requested() + forced_cache = not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) + refresh = not forced_cache and refresh + if refresh or not ids: result = {} else: data_cache = context.get_data_cache() - result = data_cache.get_items(ids, data_cache.ONE_MONTH) - to_update = [id_ for id_ in ids - if id_ - and (id_ not in result - or not result[id_] - or result[id_].get('_partial'))] + result = data_cache.get_items( + ids, + None if forced_cache else data_cache.ONE_MONTH, + ) + to_update = ( + [] + if forced_cache else + [id_ for id_ in ids + if id_ + and (id_ not in result + or not result[id_] + or result[id_].get('_partial') + or (yt_items_dict + and yt_items_dict.get(id_) + and result[id_].get('_unavailable')))] + ) if result: - context.debug_log and context.log_debug( - 'ResourceManager.get_videos' - ' - Using cached data for {num} video(s)' - '\n\tVideo IDs: {ids}' - .format(num=len(result), ids=list(result)) + self.log.debugging and self.log.debug( + ('Using cached data for {num} video(s)', + 'Video IDs: {ids}'), + num=len(result), + ids=list(result), ) if self._progress_dialog: self._progress_dialog.update(steps=len(result) - len(to_update)) if to_update: notify_and_raise = not suppress_errors - client = self._provider.get_client(context) new_data = [client.get_videos(list_of_50, live_details, max_results=50, @@ -503,22 +620,19 @@ def get_videos(self, new_data = None if new_data: - context.debug_log and context.log_debug( - 'ResourceManager.get_videos' - ' - Retrieved new data for {num} video(s)' - '\n\tVideo IDs: {ids}' - .format(num=len(to_update), ids=to_update) + self.log.debugging and self.log.debug( + ('Retrieved new data for {num} video(s)', + 'Video IDs: {ids}'), + num=len(to_update), + ids=to_update, ) new_data = dict(dict.fromkeys(to_update, {'_unavailable': True}), **new_data) result.update(new_data) self.cache_data(new_data, defer=defer_cache) - if not result and not new_data and yt_items: - result = { - yt_item.get('id'): yt_item - for yt_item in yt_items - } + if not result and not new_data and yt_items_dict: + result = yt_items_dict self.cache_data(result, defer=defer_cache) # Re-sort result to match order of requested IDs @@ -540,26 +654,25 @@ def get_videos(self, return result def cache_data(self, data=None, defer=False): - if self._incognito: - return - - if defer: - if data: - self.new_data.update(data) - return - - flush = False if not data: - data = self.new_data - flush = True - if data: - context = self._context - context.get_data_cache().set_items(data) - context.debug_log and context.log_debug( - 'ResourceManager.cache_data' - ' - Storing new data to cache for {num} item(s)' - '\n\tIDs: {ids}' - .format(num=len(data), ids=list(data)) + return None + + incognito = self._incognito + if not defer and self.log.debugging: + self.log.debug( + ( + 'Incognito mode active - discarded data for {num} item(s)', + 'IDs: {ids}' + ) if incognito else ( + 'Storing new data to cache for {num} item(s)', + 'IDs: {ids}' + ), + num=len(data), + ids=list(data) ) - if flush: - self.new_data = {} + + return self._context.get_data_cache().set_items( + data, + defer=defer, + flush=incognito, + ) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py index c3cf86311c..f1da29c038 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py index b5aec8f0cc..c68d6c17aa 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/json_script_engine.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/json_script_engine.py index 975d20e5d3..b5d537ab79 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/json_script_engine.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/signature/json_script_engine.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/tv.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/tv.py deleted file mode 100644 index dc41f183b1..0000000000 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/tv.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from __future__ import absolute_import, division, unicode_literals - -from collections import deque - -from ..helper import utils -from ...kodion.constants import PATHS -from ...kodion.items import DirectoryItem, NextPageItem, VideoItem - - -def tv_videos_to_items(provider, context, json_data): - item_params = { - 'video_id': None, - } - if context.get_param('incognito'): - item_params['incognito'] = True - - items = [] - video_id_dict = {} - channel_items_dict = {} - - for item in json_data.get('items', []): - video_id = item['id'] - item_params['video_id'] = video_id - item = VideoItem( - name=item['title'], - uri=context.create_uri((PATHS.PLAY,), item_params), - video_id=video_id, - ) - items.append(item) - if video_id in video_id_dict: - fifo_queue = video_id_dict[video_id] - else: - fifo_queue = deque() - video_id_dict[video_id] = fifo_queue - fifo_queue.appendleft(item) - - item_filter = context.get_settings().item_filter() - - utils.update_video_items( - provider, - context, - video_id_dict, - channel_items_dict=channel_items_dict, - item_filter=item_filter, - ) - utils.update_channel_info(provider, context, channel_items_dict) - - if item_filter: - result, _ = utils.filter_videos(items, **item_filter) - else: - result = items - - # next page - next_page_token = json_data.get('next_page_token') - if next_page_token or json_data.get('continue'): - params = context.get_params() - new_params = dict(params, - next_page_token=next_page_token, - offset=json_data.get('offset', 0), - page=params.get('page', 1) + 1) - next_page_item = NextPageItem(context, new_params) - result.append(next_page_item) - - return result - - -def saved_playlists_to_items(provider, context, json_data): - result = [] - playlist_id_dict = {} - - thumb_size = context.get_settings().get_thumbnail_size() - incognito = context.get_param('incognito', False) - item_params = {} - if incognito: - item_params['incognito'] = incognito - - items = json_data.get('items', []) - for item in items: - title = item['title'] - channel_id = item['channel_id'] - playlist_id = item['id'] - image = utils.get_thumbnail(thumb_size, item.get('thumbnails')) - - if channel_id: - item_uri = context.create_uri( - (PATHS.CHANNEL, channel_id, 'playlist', playlist_id,), - item_params, - ) - else: - item_uri = context.create_uri( - (PATHS.PLAYLIST, playlist_id,), - item_params, - ) - - playlist_item = DirectoryItem( - name=title, - uri=item_uri, - image=image, - playlist_id=playlist_id, - ) - result.append(playlist_item) - playlist_id_dict[playlist_id] = playlist_item - - channel_items_dict = {} - utils.update_playlist_items(provider, - context, - playlist_id_dict, - channel_items_dict=channel_items_dict) - utils.update_channel_info(provider, context, channel_items_dict) - - # next page - next_page_token = json_data.get('next_page_token') - if next_page_token or json_data.get('continue'): - params = context.get_params() - new_params = dict(params, - next_page_token=next_page_token, - offset=json_data.get('offset', 0), - page=params.get('page', 1) + 1) - next_page_item = NextPageItem(context, new_params) - result.append(next_page_item) - - return result diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index cffca53b6f..e778d61de4 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,7 +12,9 @@ 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 @@ -59,18 +61,19 @@ class YouTubeResolver(AbstractResolver): _RE_CLIP_DETAILS = re_compile(r'()' + r'|(?P"clipConfig":\{)' r'|("startTimeMs":"(?P\d+)")' r'|("endTimeMs":"(?P\d+)")') + _RE_MUSIC_VIDEO_ID = re_compile(r'"INITIAL_ENDPOINT":.+?videoId\\":\\"' + r'(?P[^\\"]+)' + 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() @@ -89,10 +92,14 @@ def supports_url(self, url, url_components): '/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 @@ -130,36 +137,46 @@ def resolve(self, url, url_components, method='HEAD'): # consent redirect cookies={'SOCS': 'CAISAiAD'}, allow_redirects=True) - if response is None or response.status_code >= 400: + if response is None: return url + with response: + if response.status_code >= 400: + return url + url = response.url + response_text = response.text if method == 'GET' else None if path.startswith('/clip'): - all_matches = self._RE_CLIP_DETAILS.finditer(response.text) - num_matched = 0 + all_matches = self._RE_CLIP_DETAILS.finditer(response_text) + matched_state = 0 url_components = params = start_time = end_time = None for matches in all_matches: matches = matches.groupdict() - if not num_matched & 1: - url = matches['video_url'] - if url: - num_matched += 1 - url_components = urlsplit(unescape(url)) + if not matched_state & 1: + new_url = matches['video_url'] + if new_url: + matched_state += 1 + url_components = urlsplit(unescape(new_url)) params = dict(parse_qsl(url_components.query)) - if not num_matched & 2: - start_time = matches['start_time'] - if start_time: - start_time = int(start_time) / 1000 - num_matched += 2 - - if not num_matched & 4: - end_time = matches['end_time'] - if end_time: - end_time = int(end_time) / 1000 - num_matched += 4 - - if num_matched != 7: + if not matched_state & 2: + is_clip = matches['is_clip'] + if is_clip: + matched_state += 2 + else: + if not matched_state & 4: + start_time = matches['start_time'] + if start_time: + start_time = int(start_time) / 1000 + matched_state += 4 + + if not matched_state & 8: + end_time = matches['end_time'] + if end_time: + end_time = int(end_time) / 1000 + matched_state += 8 + + if matched_state != 15: continue params.update(( @@ -171,7 +188,7 @@ def resolve(self, url, url_components, method='HEAD'): elif path == '/watch_videos': params = dict(parse_qsl(url_components.query)) - new_components = urlsplit(response.url) + new_components = urlsplit(url) new_params = dict(parse_qsl(new_components.query)) # add/overwrite all other params from original query string new_params.update(params) @@ -180,24 +197,34 @@ def resolve(self, url, url_components, method='HEAD'): 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/ elif method == 'GET': - match = self._RE_CHANNEL_URL.search(response.text) + match = self._RE_CHANNEL_URL.search(response_text) if match: - url = match.group('channel_url') + new_url = match.group('channel_url') if path.endswith(('/live', '/streams')): - url_components = urlsplit(unescape(url)) + url_components = urlsplit(unescape(new_url)) params = dict(parse_qsl(url_components.query)) params['live'] = 1 return url_components._replace( query=urlencode(params) ).geturl() - if url != 'undefined': - return url + if new_url != 'undefined': + return new_url - return response.url + return url class CommonResolver(AbstractResolver): @@ -205,11 +232,7 @@ def __init__(self, *args, **kwargs): 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' @@ -218,12 +241,17 @@ def resolve(self, url, url_components, method='HEAD'): method=method, headers=self._HEADERS, allow_redirects=True) - if response is None or response.status_code >= 400: + if response is None: return url - return response.url + with response: + if response.status_code >= 400: + return url + return response.url class UrlResolver(object): + log = logging.getLogger(__name__) + def __init__(self, context): self._context = context self._resolvers = ( @@ -240,14 +268,14 @@ def _resolve(self, url): if not method: continue - self._context.log_debug('Resolving |{uri}| using |{name} {method}|' - .format(uri=resolved_url, - name=resolver_name, - method=method)) + self.log.debug('Resolving {uri!r} using {name} {method}', + uri=resolved_url, + name=resolver_name, + method=method) resolved_url = resolver.resolve(resolved_url, url_components, method) - self._context.log_debug('Resolved to |{0}|'.format(resolved_url)) + self.log.debug('Resolved to %r', resolved_url) return resolved_url def resolve(self, url): diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index 5ce9287de4..79e52fde4f 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -10,26 +10,38 @@ from __future__ import absolute_import, division, unicode_literals -from collections import deque from re import ( - IGNORECASE as re_IGNORECASE, + IGNORECASE, compile as re_compile, ) from . import utils +from ...kodion import logging from ...kodion.compatibility import parse_qsl, urlsplit -from ...kodion.constants import PATHS +from ...kodion.constants import ( + CHANNEL_ID, + CHANNEL_IDS, + CLIP, + END, + LIVE, + ORDER, + PATHS, + PLAYLIST_ID, + PLAYLIST_IDS, + SEEK, + START, + VIDEO_ID, + VIDEO_IDS, + YOUTUBE_HOSTNAMES, +) from ...kodion.items import DirectoryItem, UriItem, VideoItem -from ...kodion.utils import duration_to_seconds +from ...kodion.utils.convert_format import duration_to_seconds class UrlToItemConverter(object): - RE_PATH_ID = re_compile(r'/[^/]*?[/@](?P[^/?#]+)', re_IGNORECASE) - VALID_HOSTNAMES = { - 'youtube.com', - 'www.youtube.com', - 'm.youtube.com', - } + log = logging.getLogger(__name__) + + RE_PATH_ID = re_compile(r'/[^/]*?[/@](?P[^/?#]+)', IGNORECASE) def __init__(self, flatten=True): self._flatten = flatten @@ -44,28 +56,31 @@ def __init__(self, flatten=True): self._channel_id_dict = {} self._channel_items = [] self._channel_ids = [] + self._channel_items_dict = {} - def add_url(self, url, context): + self._new_params = None + + def add_url(self, url): parsed_url = urlsplit(url) if (not parsed_url.hostname - or parsed_url.hostname.lower() not in self.VALID_HOSTNAMES): - context.log_debug('Unknown hostname "{0}" in url "{1}"'.format( - parsed_url.hostname, url - )) - return + or parsed_url.hostname.lower() not in YOUTUBE_HOSTNAMES): + self.log.debug('Unknown hostname "{hostname}" in url "{url}"', + hostname=parsed_url.hostname, + url=url) + return False url_params = dict(parse_qsl(parsed_url.query)) new_params = { new: process(url_params[old]) if process else url_params[old] for old, new, process in ( - ('end', 'end', duration_to_seconds), - ('start', 'start', duration_to_seconds), - ('t', 'seek', duration_to_seconds), - ('list', 'playlist_id', False), - ('v', 'video_id', False), - ('live', 'live', False), - ('clip', 'clip', False), - ('video_ids', 'video_ids', False), + ('end', END, duration_to_seconds), + ('start', START, duration_to_seconds), + ('t', SEEK, duration_to_seconds), + ('list', PLAYLIST_ID, False), + ('v', VIDEO_ID, False), + ('live', LIVE, False), + ('clip', CLIP, False), + ('video_ids', VIDEO_IDS, False), ) if old in url_params } @@ -75,114 +90,139 @@ def add_url(self, url, context): pass elif path.startswith(('/c/', '/channel/', '/u/', '/user/', '/@')): re_match = self.RE_PATH_ID.match(parsed_url.path) - new_params['channel_id'] = re_match.group('id') + new_params[CHANNEL_ID] = re_match.group('id') if ('live' not in new_params and path.endswith(('/live', '/streams'))): new_params['live'] = 1 elif path.startswith(('/clip/', '/embed/', '/live/', '/shorts/')): re_match = self.RE_PATH_ID.match(parsed_url.path) - new_params['video_id'] = re_match.group('id') + new_params[VIDEO_ID] = re_match.group('id') else: - context.log_debug('Unknown path "{0}" in url "{1}"'.format( - parsed_url.path, url - )) - return - + self.log.debug('Unknown path "{path}" in url "{url}"', + path=parsed_url.path, + url=url) + self._new_params = None + return False + self._new_params = new_params + return True + + def create_item(self, context, as_uri=False): + new_params = self._new_params item = None - if 'video_ids' in new_params: - for video_id in new_params['video_ids'].split(','): + if VIDEO_IDS in new_params: + item_uri = context.create_uri(PATHS.PLAY, new_params) + if as_uri: + return item_uri + + for video_id in new_params[VIDEO_IDS].split(','): item = VideoItem( name='', uri=context.create_uri( - (PATHS.PLAY,), + PATHS.PLAY, dict(new_params, video_id=video_id), ), video_id=video_id, ) - if video_id in self._video_id_dict: - fifo_queue = self._video_id_dict[video_id] - else: - fifo_queue = deque() - self._video_id_dict[video_id] = fifo_queue - fifo_queue.appendleft(item) + items = self._video_id_dict.setdefault(video_id, []) + items.append(item) + + elif VIDEO_ID in new_params: + item_uri = context.create_uri(PATHS.PLAY, new_params) + if as_uri: + return item_uri - elif 'video_id' in new_params: - video_id = new_params['video_id'] + video_id = new_params[VIDEO_ID] item = VideoItem( name='', - uri=context.create_uri((PATHS.PLAY,), new_params), + uri=item_uri, video_id=video_id, ) - if video_id in self._video_id_dict: - fifo_queue = self._video_id_dict[video_id] - else: - fifo_queue = deque() - self._video_id_dict[video_id] = fifo_queue - fifo_queue.appendleft(item) + items = self._video_id_dict.setdefault(video_id, []) + items.append(item) + + if PLAYLIST_ID in new_params: + playlist_id = new_params[PLAYLIST_ID] - if 'playlist_id' in new_params: - playlist_id = new_params['playlist_id'] + item_uri = context.create_uri( + (PATHS.PLAYLIST, playlist_id), + new_params, + ) + if as_uri: + return item_uri if self._flatten: self._playlist_ids.append(playlist_id) - return + return playlist_id item = DirectoryItem( name='', - uri=context.create_uri(('playlist', playlist_id,), new_params), + uri=item_uri, playlist_id=playlist_id, ) - self._playlist_id_dict[playlist_id] = item + items = self._playlist_id_dict.setdefault(playlist_id, []) + items.append(item) - if 'channel_id' in new_params: - channel_id = new_params['channel_id'] + if CHANNEL_ID in new_params: + channel_id = new_params[CHANNEL_ID] live = new_params.get('live') + item_uri = context.create_uri( + PATHS.PLAY if live else (PATHS.CHANNEL, channel_id), + new_params + ) + if as_uri: + return item_uri + if not live and self._flatten: self._channel_ids.append(channel_id) - return + return channel_id item = VideoItem( name='', - uri=context.create_uri((PATHS.PLAY,), new_params), + uri=item_uri, channel_id=channel_id, ) if live else DirectoryItem( name='', - uri=context.create_uri(('channel', channel_id,), new_params), + uri=item_uri, channel_id=channel_id, ) - self._channel_id_dict[channel_id] = item + items = self._channel_id_dict.setdefault(channel_id, []) + items.append(item) + + return item + def process_url(self, url, context, as_uri=False): + if not self.add_url(url): + return False + item = self.create_item(context, as_uri=as_uri) if not item: - context.log_debug('No items found in url "{0}"'.format(url)) + self.log.debug('No items found in url "%s"', url) + return item - def add_urls(self, urls, context): + def process_urls(self, urls, context): for url in urls: - self.add_url(url, context) + self.process_url(url, context) def get_items(self, provider, context, skip_title=False): result = [] query = context.get_param('q') if self._channel_ids: - # remove duplicates - self._channel_ids = list(frozenset(self._channel_ids)) - item_label = context.localize('channels') channels_item = DirectoryItem( context.get_ui().bold(item_label), context.create_uri( (PATHS.SEARCH, 'links',), { - 'channel_ids': ','.join(self._channel_ids), + CHANNEL_IDS: ','.join(self._channel_ids), 'q': query, }, ) if query else context.create_uri( (PATHS.DESCRIPTION_LINKS,), { - 'channel_ids': ','.join(self._channel_ids), + CHANNEL_IDS: ','.join(self._channel_ids), }, ), image='{media}/channels.png', @@ -191,16 +231,13 @@ def get_items(self, provider, context, skip_title=False): result.append(channels_item) if self._playlist_ids: - # remove duplicates - self._playlist_ids = list(frozenset(self._playlist_ids)) - if context.get_param('uri'): playlists_item = UriItem( context.create_uri( (PATHS.PLAY,), { - 'playlist_ids': ','.join(self._playlist_ids), - 'order': 'normal', + PLAYLIST_IDS: ','.join(self._playlist_ids), + ORDER: 'normal', }, ), playable=True, @@ -212,13 +249,13 @@ def get_items(self, provider, context, skip_title=False): context.create_uri( (PATHS.SEARCH, 'links',), { - 'playlist_ids': ','.join(self._playlist_ids), + PLAYLIST_IDS: ','.join(self._playlist_ids), 'q': query, }, ) if query else context.create_uri( (PATHS.DESCRIPTION_LINKS,), { - 'playlist_ids': ','.join(self._playlist_ids), + PLAYLIST_IDS: ','.join(self._playlist_ids), }, ), image='{media}/playlist.png', @@ -241,23 +278,17 @@ def get_video_items(self, provider, context, skip_title=False): if self._video_items: return self._video_items - video_items = [ - video_item - for video_items in self._video_id_dict.values() - for video_item in video_items - ] - - channel_items_dict = {} utils.update_video_items( provider, context, self._video_id_dict, - channel_items_dict=channel_items_dict, + channel_items_dict=self._channel_items_dict, ) - utils.update_channel_info(provider, context, channel_items_dict) + utils.update_channel_info(provider, context, self._channel_items_dict) self._video_items = [ video_item + for video_items in self._video_id_dict.values() for video_item in video_items if skip_title or video_item.get_name() ] @@ -267,26 +298,38 @@ def get_playlist_items(self, provider, context, skip_title=False): if self._playlist_items: return self._playlist_items - channel_items_dict = {} - utils.update_playlist_items(provider, context, - self._playlist_id_dict, - channel_items_dict=channel_items_dict) - utils.update_channel_info(provider, context, channel_items_dict) + utils.update_playlist_items( + provider, + context, + self._playlist_id_dict, + channel_items_dict=self._channel_items_dict, + ) + utils.update_channel_info(provider, context, self._channel_items_dict) self._playlist_items = [ playlist_item - for playlist_item in self._playlist_id_dict.values() + for playlist_items in self._playlist_id_dict.values() + for playlist_item in playlist_items if skip_title or playlist_item.get_name() ] return self._playlist_items - def get_channel_items(self, _provider, _context, skip_title=False): + def get_channel_items(self, provider, context, skip_title=False): if self._channel_items: return self._channel_items + utils.update_channel_items( + provider, + context, + self._channel_id_dict, + channel_items_dict=self._channel_items_dict, + ) + utils.update_channel_info(provider, context, self._channel_items_dict) + self._channel_items = [ channel_item - for channel_item in self._channel_id_dict.values() + for channel_items in self._channel_id_dict.values() + for channel_item in channel_items if skip_title or channel_item.get_name() ] return self._channel_items diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/utils.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/utils.py index e204dc91c5..69fd5daf92 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,7 +11,7 @@ from __future__ import absolute_import, division, unicode_literals import time -from datetime import date, datetime +from datetime import date as dt_date, datetime as dt_datetime from math import log10 from operator import ( contains as op_contains, @@ -27,17 +27,33 @@ search as re_search, ) +from ...kodion import logging from ...kodion.compatibility import string_type, unquote, urlsplit -from ...kodion.constants import CONTENT, PATHS -from ...kodion.items import AudioItem, CommandItem, DirectoryItem, menu_items -from ...kodion.logger import Logger -from ...kodion.utils import ( - datetime_parser, - friendly_number, - strip_html_from_text, +from ...kodion.constants import ( + CHANNEL_ID, + CONTENT, + FANART_TYPE, + PATHS, + PLAYLIST_ID, +) +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, + parse_to_dt, + utc_to_local, ) +# RegExp used to match plugin playlist paths of the form: +# /channel/[CHANNEL_ID]/playlist/[PLAYLIST_ID]/ +# /playlist/[PLAYLIST_ID]/ __RE_PLAYLIST = re_compile( r'^(/channel/(?P[^/]+))/playlist/(?P[^/]+)/?$' ) @@ -121,8 +137,8 @@ def make_comment_item(context, snippet, uri, reply_count=0): ui.new_line(body, cr_after=1) if body else '' )) - datetime = datetime_parser.parse(published_at) - local_datetime = datetime_parser.utc_to_local(datetime) + datetime = parse_to_dt(published_at) + local_datetime = utc_to_local(datetime) if uri: comment_item = DirectoryItem( @@ -133,6 +149,7 @@ def make_comment_item(context, snippet, uri, reply_count=0): category_label=' - '.join( (author, context.format_date_short(local_datetime)) ), + special_sort=False, ) else: comment_item = CommandItem( @@ -158,8 +175,8 @@ def make_comment_item(context, snippet, uri, reply_count=0): comment_item.set_dateadded_from_datetime(local_datetime) if edited: - datetime = datetime_parser.parse(updated_at) - local_datetime = datetime_parser.utc_to_local(datetime) + datetime = parse_to_dt(updated_at) + local_datetime = utc_to_local(datetime) comment_item.set_date_from_datetime(local_datetime) return comment_item @@ -188,7 +205,7 @@ def update_channel_items(provider, context, channel_id_dict, subscription_id_dict = {} client = provider.get_client(context) - logged_in = provider.is_logged_in() + logged_in = client.logged_in settings = context.get_settings() show_details = settings.show_detailed_description() @@ -216,17 +233,45 @@ def update_channel_items(provider, context, channel_id_dict, settings.subscriptions_filter() ) + fanart_type = context.get_param(FANART_TYPE) + if fanart_type is None: + fanart_type = settings.fanart_selection() thumb_size = settings.get_thumbnail_size() + thumb_fanart = ( + settings.get_thumbnail_size(settings.THUMB_SIZE_BEST) + if fanart_type == settings.FANART_THUMBNAIL else + False + ) + + cxm_unsubscribe_from_channel = menu_items.channel_unsubscribe_from( + context, + subscription_id=menu_items.SUBSCRIPTION_ID_INFOLABEL, + ) + cxm_subscribe_to_channel = ( + menu_items.channel_subscribe_to(context) + if logged_in and not in_subscription_list else + None + ) + cxm_filter_remove = menu_items.my_subscriptions_filter_remove(context) + cxm_filter_add = menu_items.my_subscriptions_filter_add(context) + cxm_bookmark_channel = ( + None + if in_bookmarks_list else + menu_items.bookmark_add_channel(context) + ) for channel_id, yt_item in data.items(): if not yt_item or 'snippet' not in yt_item: continue - snippet = yt_item['snippet'] - channel_item = channel_id_dict.get(channel_id) - if not channel_item: + channel_items = channel_id_dict.get(channel_id) + if channel_items: + channel_item = channel_items[-1] + else: continue + snippet = yt_item['snippet'] + label_stats = [] stats = [] if 'statistics' in yt_item: @@ -277,73 +322,57 @@ def update_channel_items(provider, context, channel_id_dict, ui.bold(channel_name, cr_after=1), ui.new_line(stats, cr_after=1) if stats else '', ui.new_line(description, cr_after=1) if description else '', - ui.new_line('--------', cr_before=1, cr_after=1), + ui.new_line('--------', cr_after=1), 'https://www.youtube.com/', channel_handle if channel_handle else channel_id if channel_id.startswith('@') else - '/channel/' + channel_id, + 'channel/' + channel_id, )) channel_item.set_plot(description) # date time published_at = snippet.get('publishedAt') if published_at: - datetime = datetime_parser.parse(published_at) + datetime = parse_to_dt(published_at) channel_item.set_added_utc(datetime) - local_datetime = datetime_parser.utc_to_local(datetime) + local_datetime = utc_to_local(datetime) channel_item.set_date_from_datetime(local_datetime) - # image + # try to find a better resolution for the image image = get_thumbnail(thumb_size, snippet.get('thumbnails')) channel_item.set_image(image) - # - update context menu - context_menu = [] + # try to find a better resolution for the fanart + if thumb_fanart: + fanart = get_thumbnail(thumb_fanart, snippet.get('thumbnails')) + channel_item.set_fanart(fanart) - # -- unsubscribe from channel subscription_id = subscription_id_dict.get(channel_id, '') if subscription_id: channel_item.subscription_id = subscription_id - context_menu.append( - menu_items.unsubscribe_from_channel( - context, subscription_id=subscription_id - ) - ) - - # -- subscribe to the channel - if logged_in and not in_subscription_list: - context_menu.append( - menu_items.subscribe_to_channel( - context, channel_id - ) - ) + context_menu = [ + cxm_unsubscribe_from_channel, + cxm_bookmark_channel, + ] + else: + context_menu = [ + cxm_subscribe_to_channel, + cxm_bookmark_channel, + ] # add/remove from filter list if filters_set is not None: context_menu.append( - menu_items.remove_my_subscriptions_filter( - context, channel_handle or channel_name - ) if client.channel_match(channel_id, filters_set) else - menu_items.add_my_subscriptions_filter( - context, channel_handle or channel_name - ) + cxm_filter_remove + if client.channel_match(channel_id, filters_set) else + cxm_filter_add ) - if not in_bookmarks_list: - context_menu.append( - menu_items.bookmark_add_channel( - context, channel_id - ) - ) - - if context_menu: - channel_item.add_context_menu(context_menu) - - # update channel mapping - if channel_items_dict is not None: - if channel_id not in channel_items_dict: - channel_items_dict[channel_id] = [] - channel_items_dict[channel_id].append(channel_item) + update_duplicate_items(channel_item, + channel_items, + channel_id, + channel_items_dict, + context_menu) if channel_items_dict: update_channel_info(provider, @@ -367,44 +396,93 @@ def update_playlist_items(provider, context, playlist_id_dict, return access_manager = context.get_access_manager() - custom_watch_later_id = access_manager.get_watch_later_id() - custom_history_id = access_manager.get_watch_history_id() - logged_in = provider.is_logged_in() + logged_in = provider.get_client(context).logged_in + if logged_in: + history_id = access_manager.get_watch_history_id() + watch_later_id = access_manager.get_watch_later_id() + else: + history_id = '' + watch_later_id = '' settings = context.get_settings() - thumb_size = settings.get_thumbnail_size() show_details = settings.show_detailed_description() item_count_color = settings.get_label_color('itemCount') + 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() + thumb_fanart = ( + settings.get_thumbnail_size(settings.THUMB_SIZE_BEST) + if fanart_type == settings.FANART_THUMBNAIL else + False + ) + localize = context.localize episode_count_label = localize('stats.itemCount') video_count_label = localize('stats.videoCount') podcast_label = context.localize('playlist.podcast') untitled = localize('untitled') - separator = menu_items.separator() path = context.get_path() ui = context.get_ui() + in_bookmarks_list = False + in_my_playlists = False + in_saved_playlists = False + # if the path directs to a playlist of our own, set channel id to 'mine' if path.startswith(PATHS.MY_PLAYLISTS): - in_bookmarks_list = False in_my_playlists = True elif path.startswith(PATHS.BOOKMARKS): in_bookmarks_list = True - in_my_playlists = False - else: - in_bookmarks_list = False - in_my_playlists = False + elif path.startswith(PATHS.SAVED_PLAYLISTS): + in_saved_playlists = True + + cxm_playlist_delete = menu_items.playlist_delete(context) + cxm_playlist_rename = menu_items.playlist_rename(context) + cxm_watch_later_unassign = menu_items.watch_later_list_unassign(context) + cxm_watch_later_assign = menu_items.watch_later_list_assign(context) + cxm_history_list_unassign = menu_items.history_list_unassign(context) + cxm_history_list_assign = menu_items.history_list_assign(context) + cxm_separator = menu_items.separator() + cxm_play_playlist = menu_items.playlist_play(context) + 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) + if logged_in and not (in_my_playlists or in_saved_playlists) else + None + ) + cxm_go_to_channel = ( + menu_items.channel_go_to(context) + if not in_my_playlists else + None + ) + cxm_subscribe_to_channel = ( + menu_items.channel_subscribe_to(context) + if logged_in and not in_my_playlists else + None + ) + cxm_bookmark_channel = ( + menu_items.bookmark_add_channel(context) + if not in_my_playlists else + None + ) for playlist_id, yt_item in data.items(): - playlist_item = playlist_id_dict.get(playlist_id) - if not playlist_item: + if not yt_item or 'snippet' not in yt_item: continue - if not yt_item or 'snippet' not in yt_item: + playlist_items = playlist_id_dict.get(playlist_id) + if playlist_items: + playlist_item = playlist_items[-1] + else: continue - snippet = yt_item['snippet'] item_count_str, item_count = friendly_number( yt_item.get('contentDetails', {}).get('itemCount', 0), @@ -413,6 +491,8 @@ def update_playlist_items(provider, context, playlist_id_dict, if not item_count and playlist_id.startswith('UU'): continue + snippet = yt_item['snippet'] + playlist_item.available = True is_podcast = yt_item.get('status', {}).get('podcastStatus') == 'enabled' @@ -456,7 +536,7 @@ def update_playlist_items(provider, context, playlist_id_dict, cr_after=1, ), ui.new_line(description, cr_after=1) if description else '', - ui.new_line('--------', cr_before=1, cr_after=1), + ui.new_line('--------', cr_after=1), 'https://youtube.com/playlist?list=' + playlist_id, )) playlist_item.set_plot(description) @@ -464,88 +544,76 @@ def update_playlist_items(provider, context, playlist_id_dict, # date time published_at = snippet.get('publishedAt') if published_at: - datetime = datetime_parser.parse(published_at) + datetime = parse_to_dt(published_at) playlist_item.set_added_utc(datetime) - local_datetime = datetime_parser.utc_to_local(datetime) + local_datetime = utc_to_local(datetime) playlist_item.set_date_from_datetime(local_datetime) + # try to find a better resolution for the image image = get_thumbnail(thumb_size, snippet.get('thumbnails')) playlist_item.set_image(image) + # try to find a better resolution for the fanart + if thumb_fanart: + fanart = get_thumbnail(thumb_fanart, snippet.get('thumbnails')) + playlist_item.set_fanart(fanart) + # update channel mapping channel_id = snippet.get('channelId', '') playlist_item.channel_id = channel_id - if channel_id and channel_items_dict is not None: - if channel_id not in channel_items_dict: - channel_items_dict[channel_id] = [] - channel_items_dict[channel_id].append(playlist_item) - - # play all videos of the playlist - context_menu = [ - menu_items.play_playlist( - context, playlist_id - ), - menu_items.play_playlist_recently_added( - context, playlist_id - ), - menu_items.view_playlist( - context, playlist_id - ), - menu_items.shuffle_playlist( - context, playlist_id - ), - separator, + + if in_my_playlists: + context_menu = [ + # remove my playlist + cxm_playlist_delete, + # rename playlist + cxm_playlist_rename, + # remove as my custom watch later playlist + cxm_watch_later_unassign + if playlist_id == watch_later_id else + # set as my custom watch later playlist + cxm_watch_later_assign, + # remove as custom history playlist + cxm_history_list_unassign + if playlist_id == history_id else + # set as custom history playlist + cxm_history_list_assign, + cxm_separator, + ] + elif in_saved_playlists: + context_menu = [ + cxm_remove_saved_playlist, + cxm_separator, + ] + else: + context_menu = [] + + context_menu.extend(( + # play all videos of the playlist + cxm_play_playlist, + cxm_play_recently_added, + cxm_view_playlist, + cxm_play_shuffled_playlist, + cxm_refresh_listing, + cxm_separator, + cxm_save_playlist, menu_items.bookmark_add( context, playlist_item - ) if not in_bookmarks_list and not in_my_playlists else None, - ] - - if logged_in: - if in_my_playlists: - context_menu.extend(( - # remove my playlist - menu_items.delete_playlist( - context, playlist_id, title - ), - # rename playlist - menu_items.rename_playlist( - context, playlist_id, title - ), - # remove as my custom watch later playlist - menu_items.remove_as_watch_later( - context, playlist_id, title - ) if playlist_id == custom_watch_later_id else - # set as my custom watch later playlist - menu_items.set_as_watch_later( - context, playlist_id, title - ), - # remove as custom history playlist - menu_items.remove_as_history( - context, playlist_id, title - ) if playlist_id == custom_history_id else - # set as custom history playlist - menu_items.set_as_history( - context, playlist_id, title - ), - )) - else: - # subscribe to the channel via the playlist item - context_menu.append( - menu_items.subscribe_to_channel( - context, channel_id, channel_name - ) - ) - - if not in_bookmarks_list and not in_my_playlists: - context_menu.append( - # bookmark channel of the playlist - menu_items.bookmark_add_channel( - context, channel_id, channel_name - ) ) + if not (in_my_playlists or in_bookmarks_list) else + None, + cxm_go_to_channel, + # subscribe to the channel via the playlist item + cxm_subscribe_to_channel, + # bookmark channel of the playlist + cxm_bookmark_channel, + )) - if context_menu: - playlist_item.add_context_menu(context_menu) + update_duplicate_items(playlist_item, + playlist_items, + channel_id, + channel_items_dict, + context_menu) def update_video_items(provider, context, video_id_dict, @@ -553,7 +621,7 @@ def update_video_items(provider, context, video_id_dict, live_details=True, item_filter=None, data=None, - yt_items=None): + yt_items_dict=None): if not video_id_dict and not data: return @@ -563,16 +631,16 @@ def update_video_items(provider, context, video_id_dict, data = resource_manager.get_videos(video_ids, live_details=live_details, suppress_errors=True, - yt_items=yt_items) + yt_items_dict=yt_items_dict) if not data: return - logged_in = provider.is_logged_in() + logged_in = provider.get_client(context).logged_in if logged_in: watch_later_id = context.get_access_manager().get_watch_later_id() else: - watch_later_id = None + watch_later_id = '' settings = context.get_settings() alternate_player = settings.support_alternative_player() @@ -582,9 +650,21 @@ def update_video_items(provider, context, video_id_dict, show_details = settings.show_detailed_description() shorts_duration = settings.shorts_duration() subtitles_prompt = settings.get_subtitle_selection() == 1 + use_play_data = settings.use_local_history() + + 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() + get_better_thumbs = (settings.get_int(settings.THUMB_SIZE) + == settings.THUMB_SIZE_BEST) + thumb_fanart = ( + settings.get_thumbnail_size(settings.THUMB_SIZE_BEST) + if fanart_type == settings.FANART_THUMBNAIL else + False + ) thumb_stamp = get_thumb_timestamp() - use_play_data = settings.use_local_history() localize = context.localize untitled = localize('untitled') @@ -592,40 +672,97 @@ def update_video_items(provider, context, video_id_dict, path = context.get_path() ui = context.get_ui() + playlist_id = None + playlist_channel_id = None + + in_bookmarks_list = False + in_my_subscriptions_list = False + in_watch_history_list = False + in_watch_later_list = False + if path.startswith(PATHS.MY_SUBSCRIPTIONS): - in_bookmarks_list = False in_my_subscriptions_list = True - in_watched_later_list = False - playlist_match = False elif path.startswith(PATHS.WATCH_LATER): - in_bookmarks_list = False - in_my_subscriptions_list = False - in_watched_later_list = True - playlist_match = False + in_watch_later_list = True elif path.startswith(PATHS.BOOKMARKS): in_bookmarks_list = True - in_my_subscriptions_list = False - in_watched_later_list = False - playlist_match = False + elif path.startswith(PATHS.VIRTUAL_PLAYLIST): + playlist_id = params.get(PLAYLIST_ID) + playlist_channel_id = 'mine' + if playlist_id: + playlist_id_upper = playlist_id.upper() + if playlist_id_upper == 'WL': + in_watch_later_list = True + elif playlist_id_upper == 'HL': + in_watch_history_list = True else: - in_bookmarks_list = False - in_my_subscriptions_list = False - in_watched_later_list = False playlist_match = __RE_PLAYLIST.match(path) + if playlist_match: + playlist_id = playlist_match.group(PLAYLIST_ID) + playlist_channel_id = playlist_match.group(CHANNEL_ID) - media_items = None - media_item = None + cxm_remove_from_playlist = menu_items.playlist_remove_from( + context, + playlist_id=playlist_id, + ) + cxm_separator = menu_items.separator() + cxm_play = menu_items.media_play(context) + cxm_play_with_subtitles = ( + None + if subtitles_prompt else + menu_items.media_play_with_subtitles(context) + ) + cxm_play_audio_only = ( + None + if audio_only else + menu_items.media_play_audio_only(context) + ) + cxm_play_ask_for_quality = ( + None + if ask_quality else + menu_items.media_play_ask_for_quality(context) + ) + cxm_play_timeshift = menu_items.media_play_timeshift(context) + cxm_play_using = ( + menu_items.media_play_using(context) + if alternate_player else + None + ) + cxm_play_from = menu_items.playlist_play_from(context, playlist_id) + cxm_queue = menu_items.media_queue(context) + cxm_watch_later = menu_items.playlist_add_to( + context, + watch_later_id, + 'watch_later', + ) + cxm_go_to_channel = menu_items.channel_go_to(context) + cxm_unsubscribe_from_channel = menu_items.channel_unsubscribe_from( + context, + channel_id=menu_items.CHANNEL_ID_INFOLABEL, + ) + cxm_subscribe_to_channel = menu_items.channel_subscribe_to(context) + cxm_remove_bookmarked_channel = menu_items.bookmark_remove( + context, + menu_items.CHANNEL_ID_INFOLABEL, + menu_items.ARTIST_INFOLABEL, + ) + cxm_bookmark_channel = menu_items.bookmark_add_channel(context) + cxm_mark_as = menu_items.history_local_mark_as(context) + cxm_reset_resume = menu_items.history_local_reset_resume(context) + cxm_refresh_listing = menu_items.refresh_listing(context) + cxm_more = menu_items.video_more_for( + context, + logged_in=logged_in, + refresh=path.startswith((PATHS.LIKED_VIDEOS, PATHS.DISLIKED_VIDEOS)), + ) for video_id, yt_item in data.items(): - if media_items and media_item: - update_duplicate_items(media_item, media_items) - if not yt_item: continue media_items = video_id_dict.get(video_id) if media_items: - media_item = media_items.pop() + media_item = media_items[-1] else: continue @@ -651,7 +788,7 @@ def update_video_items(provider, context, video_id_dict, else: duration = yt_item.get('contentDetails', {}).get('duration') if duration: - duration = datetime_parser.parse(duration) + duration = parse_to_dt(duration) if duration.seconds: # subtract 1s because YouTube duration is +1s too long duration = duration.seconds - 1 @@ -707,27 +844,33 @@ def update_video_items(provider, context, video_id_dict, ): continue - if media_item.live: - media_item.set_play_count(0) - use_play_data = False - play_data = None - elif play_data: - if 'play_count' in play_data: - media_item.set_play_count(play_data['play_count']) + if play_data: + if media_item.live: + if 'play_count' in play_data: + media_item.set_play_count(play_data['play_count']) + + if 'last_played' in play_data: + media_item.set_last_played(play_data['last_played']) + + media_item.set_start_percent(0) + media_item.set_start_time(0) + else: + if 'play_count' in play_data: + media_item.set_play_count(play_data['play_count']) - if 'played_percent' in play_data: - media_item.set_start_percent(play_data['played_percent']) + if 'last_played' in play_data: + media_item.set_last_played(play_data['last_played']) - if 'played_time' in play_data: - media_item.set_start_time(play_data['played_time']) + if 'played_percent' in play_data: + media_item.set_start_percent(play_data['played_percent']) - if 'last_played' in play_data: - media_item.set_last_played(play_data['last_played']) + if 'played_time' in play_data: + media_item.set_start_time(play_data['played_time']) if start_at: - datetime = datetime_parser.parse(start_at) + datetime = parse_to_dt(start_at) media_item.set_scheduled_start_utc(datetime) - local_datetime = datetime_parser.utc_to_local(datetime) + local_datetime = utc_to_local(datetime) media_item.set_year_from_datetime(local_datetime) media_item.set_aired_from_datetime(local_datetime) media_item.set_premiered_from_datetime(local_datetime) @@ -743,12 +886,14 @@ def update_video_items(provider, context, video_id_dict, type_label = localize('start') start_at = ' '.join(( type_label, - datetime_parser.get_scheduled_start(context, local_datetime), + get_scheduled_start(context, local_datetime), )) label_stats = [] stats = [] - rating = [0, 0] + rating = 0 + likes = 0 + views = 0 if 'statistics' in yt_item: for stat, value in yt_item['statistics'].items(): label = context.LOCAL_MAP.get('stats.' + stat) @@ -770,21 +915,23 @@ def update_video_items(provider, context, video_id_dict, ))))) if stat == 'likeCount': - rating[0] = value + likes = value elif stat == 'viewCount': - rating[1] = value - media_item.set_count(value) + views = value + media_item.set_count(views) label_stats = ' | '.join(label_stats) stats = ' | '.join(stats) - if 0 < rating[0] <= rating[1]: - if rating[0] == rating[1]: + if 0 < likes <= views: + if likes == views: rating = 10 else: # This is a completely made up, arbitrary ranking score - rating = (10 * (log10(rating[1]) * log10(rating[0])) - / (log10(rating[0] + rating[1]) ** 2)) + rating = ( + 10 * (log10(views) * log10(likes)) + / (log10(likes + views) ** 2) + ) media_item.set_rating(rating) # Used for label2, but is poorly supported in skins @@ -797,7 +944,7 @@ def update_video_items(provider, context, video_id_dict, # update and set the title localised_info = snippet.get('localized') or {} title = media_item.get_name() - if not title or title == untitled: + if not title or title == untitled or media_item.bookmark_id: title = (localised_info.get('title') or snippet.get('title') or untitled) @@ -844,7 +991,7 @@ def update_video_items(provider, context, video_id_dict, (ui.italic(start_at, cr_after=1) if media_item.upcoming else ui.new_line(start_at, cr_after=1)) if start_at else '', ui.new_line(description, cr_after=1) if description else '', - ui.new_line('--------', cr_before=1, cr_after=1), + ui.new_line('--------', cr_after=1), 'https://youtu.be/' + video_id, )) media_item.set_plot(description) @@ -854,13 +1001,13 @@ def update_video_items(provider, context, video_id_dict, if not published_at: datetime = None elif isinstance(published_at, string_type): - datetime = datetime_parser.parse(published_at) + datetime = parse_to_dt(published_at) else: datetime = published_at if datetime: media_item.set_added_utc(datetime) - local_datetime = datetime_parser.utc_to_local(datetime) - # If item is in a playlist, then use data added to playlist rather + local_datetime = utc_to_local(datetime) + # If item is in a playlist, then use date added to playlist rather # than date that item was published to YouTube if not media_item.get_dateadded(): media_item.set_dateadded_from_datetime(local_datetime) @@ -872,86 +1019,64 @@ def update_video_items(provider, context, video_id_dict, # try to find a better resolution for the image image = media_item.get_image() - if not image or image.startswith('Default'): + if (not image + or get_better_thumbs + or image.startswith(('Default', 'special://'))): image = get_thumbnail(thumb_size, snippet.get('thumbnails')) - if image and image.endswith('_live.jpg'): - image = ''.join((image, '?ct=', thumb_stamp)) + if image and media_item.live: + if '?' in image: + image = ''.join((image, '&ct=', thumb_stamp)) + elif image.endswith(('_live.jpg', '_live.webp')): + image = ''.join((image, '?ct=', thumb_stamp)) media_item.set_image(image) + # try to find a better resolution for the fanart + if thumb_fanart: + fanart = get_thumbnail(thumb_fanart, snippet.get('thumbnails')) + if fanart and media_item.live: + if '?' in fanart: + fanart = ''.join((fanart, '&ct=', thumb_stamp)) + elif image.endswith(('_live.jpg', '_live.webp')): + fanart = ''.join((fanart, '?ct=', thumb_stamp)) + media_item.set_fanart(fanart) + # update channel mapping - channel_id = snippet.get('channelId', '') + channel_id = snippet.get('channelId') or playlist_channel_id media_item.channel_id = channel_id - if channel_id and channel_items_dict is not None: - if channel_id not in channel_items_dict: - channel_items_dict[channel_id] = [] - channel_items_dict[channel_id].append(media_item) - """ - Play all videos of the playlist. + item_from_playlist = playlist_id or media_item.playlist_id - /channel/[CHANNEL_ID]/playlist/[PLAYLIST_ID]/ - /playlist/[PLAYLIST_ID]/ - """ - playlist_channel_id = '' - if playlist_match: - playlist_id = playlist_match.group('playlist_id') - playlist_channel_id = playlist_match.group('channel_id') - else: - playlist_id = media_item.playlist_id - - # provide 'remove' in my playlists that have a real playlist_id - if (playlist_id + # Provide 'remove' in own playlists or virtual lists, except the + # YouTube Watch History list as that does not support direct edits + if (not in_watch_history_list + and item_from_playlist and logged_in - and playlist_channel_id == 'mine' - and playlist_id.strip().lower() not in {'wl', 'hl'}): + and playlist_channel_id == 'mine'): context_menu = [ - menu_items.remove_video_from_playlist( - context, - playlist_id=playlist_id, - video_id=media_item.playlist_item_id, - video_name=title, - ), - menu_items.separator(), + cxm_remove_from_playlist, + cxm_separator, ] else: context_menu = [] if available: context_menu.extend(( - menu_items.play_video(context), - menu_items.play_with_subtitles( - context, video_id - ) if not subtitles_prompt else None, - menu_items.play_audio_only( - context, video_id - ) if not audio_only else None, - menu_items.play_ask_for_quality( - context, video_id - ) if not ask_quality else None, - menu_items.play_timeshift( - context, video_id - ) if media_item.live else None, - # 'play with...' (external player) - menu_items.play_with( - context, video_id - ) if alternate_player else None, - menu_items.play_playlist_from( - context, playlist_id, video_id - ) if playlist_id else None, - menu_items.queue_video(context), + cxm_play, + cxm_play_with_subtitles, + cxm_play_audio_only, + cxm_play_ask_for_quality, + cxm_play_timeshift if media_item.live else None, + cxm_play_using, + cxm_play_from if item_from_playlist else None, + cxm_queue, )) # add 'Watch Later' only if we are not in my 'Watch Later' list - if not available: + if not available or in_watch_later_list: pass elif watch_later_id: - if not playlist_id or watch_later_id != playlist_id: - context_menu.append( - menu_items.watch_later_add( - context, watch_later_id, video_id - ) - ) - elif not in_watched_later_list: + context_menu.append(cxm_watch_later) + else: context_menu.append( menu_items.watch_later_local_add( context, media_item @@ -969,68 +1094,42 @@ def update_video_items(provider, context, video_id_dict, # got to [CHANNEL] only if we are not directly in the channel if context.create_path(PATHS.CHANNEL, channel_id) != path: media_item.channel_id = channel_id - context_menu.append( - menu_items.go_to_channel( - context, channel_id, channel_name - ) - ) + context_menu.append(cxm_go_to_channel) if logged_in: context_menu.append( # unsubscribe from the channel of the video - menu_items.unsubscribe_from_channel( - context, channel_id=channel_id - ) if in_my_subscriptions_list else + cxm_unsubscribe_from_channel + if in_my_subscriptions_list else # subscribe to the channel of the video - menu_items.subscribe_to_channel( - context, channel_id, channel_name - ) - ) - - if not in_bookmarks_list: - context_menu.append( - # remove bookmarked channel of the video - menu_items.bookmark_remove( - context, channel_id, channel_name - ) if in_my_subscriptions_list else - # bookmark channel of the video - menu_items.bookmark_add_channel( - context, channel_id, channel_name - ) + cxm_subscribe_to_channel ) - if use_play_data: context_menu.append( - menu_items.history_mark_unwatched( - context, video_id - ) if play_data and play_data.get('play_count') else - menu_items.history_mark_watched( - context, video_id - ) + # remove bookmarked channel of the video + cxm_remove_bookmarked_channel + if in_my_subscriptions_list else + # bookmark channel of the video + cxm_bookmark_channel ) + + if use_play_data: + context_menu.append(cxm_mark_as) if play_data and (play_data.get('played_percent', 0) > 0 or play_data.get('played_time', 0) > 0): - context_menu.append( - menu_items.history_reset_resume( - context, video_id - ) - ) + context_menu.append(cxm_reset_resume) # more... - refresh = path.startswith((PATHS.LIKED_VIDEOS, PATHS.DISLIKED_VIDEOS)) context_menu.extend(( - menu_items.refresh(context), - menu_items.more_for_video( - context, - video_id, - video_name=title, - logged_in=logged_in, - refresh=refresh, - ), + cxm_refresh_listing, + cxm_more, )) - if context_menu: - media_item.add_context_menu(context_menu) + update_duplicate_items(media_item, + media_items, + channel_id, + channel_items_dict, + context_menu) def update_play_info(provider, @@ -1040,7 +1139,10 @@ def update_play_info(provider, video_stream, yt_item=None): update_video_items( - provider, context, {video_id: [media_item]}, yt_items=[yt_item] + provider, + context, + {video_id: [media_item]}, + yt_items_dict={video_id: yt_item}, ) settings = context.get_settings() @@ -1053,7 +1155,10 @@ def update_play_info(provider, meta_data.get('thumbnails')) if image: if media_item.live: - image = ''.join((image, '?ct=', get_thumb_timestamp())) + if '?' in image: + image = ''.join((image, '&ct=', get_thumb_timestamp())) + elif image.endswith(('_live.jpg', '_live.webp')): + image = ''.join((image, '?ct=', get_thumb_timestamp())) media_item.set_image(image) if 'headers' in video_stream: @@ -1114,7 +1219,7 @@ def update_channel_info(provider, settings = context.get_settings() channel_name_aliases = settings.get_channel_name_aliases() - fanart_type = context.get_param('fanart_type') + fanart_type = context.get_param(FANART_TYPE) if fanart_type is None: fanart_type = settings.fanart_selection() use_channel_fanart = fanart_type == settings.FANART_CHANNEL @@ -1142,53 +1247,57 @@ def update_channel_info(provider, item.add_studio(channel_name) +PREFER_WEBP_THUMBS = False +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': { - 'url': 'https://i.ytimg.com/vi/{0}/default{1}.jpg', + 'name': 'default', 'width': 120, 'height': 90, 'size': 120 * 90, 'ratio': 120 / 90, # 4:3 }, 'medium': { - 'url': 'https://i.ytimg.com/vi/{0}/mqdefault{1}.jpg', + 'name': 'mqdefault', 'width': 320, 'height': 180, 'size': 320 * 180, 'ratio': 320 / 180, # 16:9 }, 'high': { - 'url': 'https://i.ytimg.com/vi/{0}/hqdefault{1}.jpg', + 'name': 'hqdefault', 'width': 480, 'height': 360, 'size': 480 * 360, 'ratio': 480 / 360, # 4:3 }, 'standard': { - 'url': 'https://i.ytimg.com/vi/{0}/sddefault{1}.jpg', + 'name': 'sddefault', 'width': 640, 'height': 480, 'size': 640 * 480, 'ratio': 640 / 480, # 4:3 }, '720': { - 'url': 'https://i.ytimg.com/vi/{0}/hq720{1}.jpg', + 'name': 'hq720', 'width': 1280, 'height': 720, 'size': 1280 * 720, 'ratio': 1280 / 720, # 16:9 }, 'oar': { - 'url': 'https://i.ytimg.com/vi/{0}/oardefault{1}.jpg', + 'name': 'oardefault', 'size': 0, 'ratio': 0, }, 'maxres': { - 'url': 'https://i.ytimg.com/vi/{0}/maxresdefault{1}.jpg', - 'width': 1920, - 'height': 1080, - 'size': 1920 * 1080, - 'ratio': 1920 / 1080, # 16:9 + 'name': 'maxresdefault', + 'size': 0, + 'ratio': 0, }, } @@ -1219,40 +1328,31 @@ def _sort_ratio_size(thumb): size = thumb['size'] ratio = thumb['ratio'] else: - return False, False + return False, False, False return ( ratio_limit and ratio_limit * 0.9 <= ratio <= ratio_limit * 1.1, - size <= size_limit and size if size_limit else size + not thumb.get('unverified', False), + size <= size_limit and size if size_limit else size, ) thumbnail = sorted(thumbnails.items() if is_dict else thumbnails, key=_sort_ratio_size, reverse=True)[0] url = (thumbnail[1] if is_dict else thumbnail).get('url') - if url and url.startswith('//'): + if not url: + return default_thumb + if url.startswith('//'): url = 'https:' + url - return url or default_thumb - - -def get_shelf_index_by_title(context, json_data, shelf_title): - shelf_index = None - - contents = json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}]) - for idx, shelf in enumerate(contents): - title = shelf.get('shelfRenderer', {}).get('title', {}).get('runs', [{}])[0].get('text', '') - if title.lower() == shelf_title.lower(): - shelf_index = idx - context.log_debug('Found shelf index |{index}| for |{title}|'.format( - index=shelf_index, title=shelf_title - )) - break - - if shelf_index is not None and 0 > shelf_index >= len(contents): - context.log_debug('Shelf index |{0}| out of range |0-{1}|' - .format(shelf_index, len(contents))) - shelf_index = None - - return shelf_index + 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 def add_related_video_to_playlist(provider, context, client, v3, video_id): @@ -1281,10 +1381,13 @@ def add_related_video_to_playlist(provider, context, client, v3, video_id): try: next_item = next(( item for item in result_items - if item - and not any((item.get_uri() == playlist_item.get('file') - or item.get_name() == playlist_item.get('title') - for playlist_item in playlist_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') + for playlist_item in playlist_items + ))) )) except StopIteration: page_token = json_data.get('nextPageToken') @@ -1317,22 +1420,36 @@ def filter_videos(items, accepted = [] rejected = [] for item in items: - if ((not item.callback or item.callback(item)) - and (not callback or callback(item)) - and (not custom or filter_parse(item, custom)) - and (not item.playable or not ( - (exclude and item.video_id in exclude) - or (not completed and item.completed) - or (not live and item.live and not item.upcoming) - or (not upcoming and item.upcoming) - or (not premieres and item.upcoming and not item.live) - or (not upcoming_live and item.upcoming and item.live) - or (not vod and item.vod) - or (not shorts and item.short) - ))): - accepted.append(item) - else: + rejected_reason = None + if item.callback and not item.callback(): + rejected_reason = 'Item callback' + elif callback and not callback(item): + rejected_reason = 'Collection callback' + elif custom and not filter_parse(item, custom): + rejected_reason = 'Custom filter' + elif item.playable: + if exclude and item.video_id in exclude: + rejected_reason = 'Is excluded' + elif not completed and item.completed: + rejected_reason = 'Is completed' + elif not live and item.live and not item.upcoming: + rejected_reason = 'Is live' + elif not upcoming and item.upcoming: + rejected_reason = 'Is upcoming' + elif not premieres and item.upcoming and not item.live: + rejected_reason = 'Is premiere' + elif not upcoming_live and item.upcoming and item.live: + rejected_reason = 'Is upcoming live' + elif not vod and item.vod: + rejected_reason = 'Is VOD' + elif not shorts and item.short: + rejected_reason = 'Is short' + + if rejected_reason: + item.set_filter_reason(rejected_reason) rejected.append(item) + else: + accepted.append(item) return accepted, rejected @@ -1370,8 +1487,8 @@ def filter_parse(item, input_2 = unquote(input_2[1:-1]) if input_1 is None: input_1 = '' - elif isinstance(input_1, (date, datetime)): - input_2 = datetime_parser.parse(input_2) + elif isinstance(input_1, (dt_date, dt_datetime)): + input_2 = parse_to_dt(input_2) else: input_2 = float(input_2) if input_1 is None: @@ -1389,18 +1506,16 @@ def filter_parse(item, result = not result if not result: break - except (AttributeError, TypeError, ValueError, re_error) as exc: - Logger.log_error('filter_parse - Error' - '\n\tException: {exc!r}' - '\n\tCriteria: |{criteria}|' - '\n\tinput_1: |{input_1}|' - '\n\top: |{op_str}|' - '\n\tinput_2: |{input_2}|' - .format(exc=exc, - criteria=criteria, - input_1=input_1, - op_str=op_str, - input_2=input_2)) + except (AttributeError, TypeError, ValueError, re_error): + logging.exception(('Error', + 'Criteria: {criteria!r}', + 'input_1: {input_1!r}', + 'op: {op_str!r}', + 'input_2: {input_2!r}'), + criteria=criteria, + input_1=input_1, + op_str=op_str, + input_2=input_2) break else: criteria_met = True @@ -1418,32 +1533,41 @@ def channel_filter_split(filters_string): return filters_string, channel_filters, custom_filters -def custom_filter_split(filter, +def custom_filter_split(filter_string, custom_filters, criteria_re=re_compile( r'{?{([^}]+)}{([^}]+)}{([^}]+)}}?' )): - criteria = criteria_re.findall(filter) + criteria = criteria_re.findall(filter_string) if not criteria: return True custom_filters.append(criteria) return False -def update_duplicate_items(item, - duplicates, - skip_keys=frozenset(( - '_bookmark_id', - '_bookmark_timestamp', - '_callback', - '_track_number', - )), +def update_duplicate_items(updated_item, + items, + channel_id=None, + channel_items_dict=None, + context_menu=None, + skip_keys=frozenset(('_bookmark_id', + '_bookmark_timestamp', + '_callback', + '_context_menu', + '_track_number', + '_uri')), skip_vals=(None, '', -1)): - item = item.__dict__ - keys = frozenset(item.keys()).difference(skip_keys) - for duplicate in duplicates: - duplicate = duplicate.__dict__ - for key in keys: - val = item[key] - if val not in skip_vals: - duplicate[key] = val + updates = { + key: val + for key, val in updated_item.__dict__.items() + if key not in skip_keys and val not in skip_vals + } + for item in items: + if item != updated_item: + item.__dict__.update(updates) + if context_menu: + item.add_context_menu(context_menu) + + if channel_id and channel_items_dict is not None: + channel_items = channel_items_dict.setdefault(channel_id, []) + channel_items.extend(items) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/v3.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/v3.py index 17be2d2841..9087e4dc01 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,9 +12,12 @@ import threading from collections import deque +from operator import methodcaller +from re import compile as re_compile from .utils import ( THUMB_TYPES, + THUMB_URL, filter_videos, get_thumbnail, make_comment_item, @@ -22,16 +25,27 @@ update_playlist_items, update_video_items, ) -from ...kodion import KodionException +from ...kodion import KodionException, logging from ...kodion.constants import ( + CHANNEL_ID, + FANART_TYPE, + FOLDER_URI, + HIDE_LIVE, + HIDE_MEMBERS, + HIDE_NEXT_PAGE, + HIDE_PLAYLISTS, + HIDE_SEARCH, + HIDE_SHORTS, + HIDE_VIDEOS, + INHERITED_PARAMS, + ITEM_FILTER, + PAGE, PATHS, - PLAY_FORCE_AUDIO, - PLAY_PROMPT_QUALITY, - PLAY_PROMPT_SUBTITLES, - PLAY_TIMESHIFT, - PLAY_WITH, + PLAYLIST_ID, + VIDEO_ID, ) from ...kodion.items import ( + BookmarkItem, CommandItem, DirectoryItem, MediaItem, @@ -40,7 +54,11 @@ VideoItem, menu_items, ) -from ...kodion.utils import datetime_parser, format_stack, strip_html_from_text +from ...kodion.utils.convert_format import strip_html_from_text +from ...kodion.utils.datetime import parse_to_dt, utc_to_local + + +_log = logging.getLogger(__name__) def _process_list_response(provider, @@ -52,42 +70,36 @@ def _process_list_response(provider, video_id_dict=None, channel_id_dict=None, playlist_id_dict=None, - subscription_id_dict=None): - yt_items = json_data.get('items', []) + subscription_id_dict=None, + log=_log): + yt_items = json_data.get('items') if not yt_items: - context.log_warning('v3 response: Items list is empty') + log.warning('Items list is empty') return None - if video_id_dict is None: - video_id_dict = {} - if channel_id_dict is None: - channel_id_dict = {} - if playlist_id_dict is None: - playlist_id_dict = {} + yt_items_dict = {} + new_video_id_dict = {} + new_playlist_id_dict = {} + new_channel_id_dict = {} if subscription_id_dict is None: subscription_id_dict = {} + channel_items_dict = {} items = [] + position = 0 do_callbacks = False params = context.get_params() new_params = { param: params[param] - for param in ( - 'addon_id', - 'incognito', - PLAY_FORCE_AUDIO, - PLAY_TIMESHIFT, - PLAY_PROMPT_QUALITY, - PLAY_PROMPT_SUBTITLES, - PLAY_WITH, - ) + for param in INHERITED_PARAMS if param in params } settings = context.get_settings() + thumb_re = re_compile(r'[^/._]+(?=[^/.]*?\.(?:jpg|webp))') thumb_size = settings.get_thumbnail_size() - fanart_type = params.get('fanart_type') + fanart_type = params.get(FANART_TYPE) if fanart_type is None: fanart_type = settings.fanart_selection() if fanart_type == settings.FANART_THUMBNAIL: @@ -98,18 +110,24 @@ def _process_list_response(provider, untitled = context.localize('untitled') for yt_item in yt_items: + if not yt_item: + continue kind, is_youtube, is_plugin, kind_type = _parse_kind(yt_item) if not (is_youtube or is_plugin) or not kind_type: - context.log_debug('v3 item discarded: |%s|' % kind) + log.debug('Item discarded: %r', kind) continue - item_params = yt_item.get('_params', {}) + item_params = yt_item.get('_params') or {} item_params.update(new_params) - if is_youtube: - item_id = yt_item.get('id') - snippet = yt_item.get('snippet', {}) + item_id = yt_item.get('id') + snippet = yt_item.get('snippet', {}) + + video_id = None + playlist_id = None + channel_id = None + if is_youtube: localised_info = snippet.get('localized') or {} title = (localised_info.get('title') or snippet.get('title') @@ -119,12 +137,44 @@ def _process_list_response(provider, or '') thumbnails = snippet.get('thumbnails') + if not thumbnails: + pass + elif isinstance(thumbnails, list): + _url = thumbnails[0].get('url') + thumbnails.extend([ + { + 'url': (thumb_re.sub(thumb['name'], _url, count=1) + if _url else + THUMB_URL.format(item_id, thumb['name'], '')), + 'size': thumb['size'], + 'ratio': thumb['ratio'], + 'unverified': True, + } + for thumb in THUMB_TYPES.values() + ]) + elif isinstance(thumbnails, dict): + _url = next(iter(thumbnails.values())).get('url') + thumbnails.update({ + thumb_type: { + 'url': (thumb_re.sub(thumb['name'], _url, count=1) + if _url else + THUMB_URL.format(item_id, thumb['name'], '')), + 'size': thumb['size'], + 'ratio': thumb['ratio'], + 'unverified': True, + } + for thumb_type, thumb in THUMB_TYPES.items() + if thumb_type not in thumbnails + }) + else: + thumbnails = None if not thumbnails: thumbnails = { thumb_type: { - 'url': thumb['url'].format(item_id, ''), + 'url': THUMB_URL.format(item_id, thumb['name'], ''), 'size': thumb['size'], 'ratio': thumb['ratio'], + 'unverified': True, } for thumb_type, thumb in THUMB_TYPES.items() } @@ -156,11 +206,17 @@ def _process_list_response(provider, 'position': 0, } else: - context.log_debug('v3 searchResult discarded: |%s|' % kind) + log.debug('searchResult discarded: %r', kind) continue + if item_id: + yt_items_dict[item_id] = yt_item + if kind_type == 'video': - item_params['video_id'] = item_id + video_id = item_id + channel_id = (snippet.get('videoOwnerChannelId') + or snippet.get('channelId')) + item_params[VIDEO_ID] = video_id item_uri = context.create_uri( (PATHS.PLAY,), item_params, @@ -170,13 +226,13 @@ def _process_list_response(provider, image=image, fanart=fanart, plot=description, - video_id=item_id, - channel_id=(snippet.get('videoOwnerChannelId') - or snippet.get('channelId'))) + channel_id=channel_id, + **item_params) elif kind_type == 'channel': + channel_id = item_id item_uri = context.create_uri( - (PATHS.CHANNEL, item_id,), + (PATHS.CHANNEL, channel_id,), item_params, ) item = DirectoryItem(ui.bold(title), @@ -185,8 +241,8 @@ def _process_list_response(provider, fanart=fanart, plot=description, category_label=title, - channel_id=item_id) - channel_id_dict[item_id] = item + channel_id=channel_id, + **item_params) elif kind_type == 'guidecategory': item_params['guide_id'] = item_id @@ -199,15 +255,16 @@ def _process_list_response(provider, image=image, fanart=fanart, plot=description, - category_label=title) + category_label=title, + **item_params) elif kind_type == 'subscription': subscription_id = item_id - item_id = snippet['resourceId']['channelId'] + channel_id = snippet['resourceId']['channelId'] # map channel id with subscription id - needed to unsubscribe - subscription_id_dict[item_id] = subscription_id + subscription_id_dict[channel_id] = subscription_id item_uri = context.create_uri( - (PATHS.CHANNEL, item_id,), + (PATHS.CHANNEL, channel_id,), item_params ) item = DirectoryItem(ui.bold(title), @@ -216,41 +273,70 @@ def _process_list_response(provider, fanart=fanart, plot=description, category_label=title, - channel_id=item_id, - subscription_id=subscription_id) - channel_id_dict[item_id] = item + channel_id=channel_id, + subscription_id=subscription_id, + **item_params) elif kind_type == 'searchfolder': - channel_id = snippet.get('channelId') - item = NewSearchItem(context, - ui.bold(title), - image=image, - fanart=fanart, - channel_id=channel_id) + if item_filter and item_filter.get(HIDE_SEARCH): + continue + channel_id = item_params[CHANNEL_ID] + item = NewSearchItem(context, **item_params) + channel_items = channel_items_dict.setdefault(channel_id, []) + channel_items.append(item) - elif kind_type == 'playlistfolder': - # set channel id to 'mine' if the path is for a playlist of our own - channel_id = snippet.get('channelId') - if context.get_path().startswith(PATHS.MY_PLAYLISTS): - uri_channel_id = 'mine' - else: - uri_channel_id = channel_id - if not uri_channel_id: + elif kind_type == 'playlistsfolder': + if item_filter and item_filter.get(HIDE_PLAYLISTS): continue - item_uri = context.create_uri( - (PATHS.CHANNEL, uri_channel_id, item_id,), - item_params, + channel_id = item_params[CHANNEL_ID] + item_params['uri'] = context.create_uri( + (PATHS.CHANNEL, channel_id, 'playlists',), ) - item = DirectoryItem(ui.bold(title), - item_uri, - image=image, - fanart=fanart, - plot=description, - category_label=title, - channel_id=channel_id, - playlist_id=item_id) + item_params['name'] = ui.bold(item_params.pop('title', '')) + item = DirectoryItem(**item_params) + channel_items = channel_items_dict.setdefault(channel_id, []) + channel_items.append(item) + + elif kind_type in {'livefolder', + 'membersfolder', + 'shortsfolder', + 'videosfolder'}: + if (item_filter and ( + ( + kind_type == 'livefolder' + and item_filter.get(HIDE_LIVE) + ) or ( + kind_type == 'membersfolder' + and item_filter.get(HIDE_MEMBERS) + ) or ( + kind_type == 'shortsfolder' + and item_filter.get(HIDE_SHORTS) + ) or ( + kind_type == 'videosfolder' + and item_filter.get(HIDE_VIDEOS) + ) + )): + continue + item = DirectoryItem(**item_params) - elif kind_type == 'playlist': + elif kind_type in {'playlist', + 'playlistlivefolder', + 'playlistmembersfolder', + 'playlistshortsfolder'}: + if (item_filter and ( + ( + kind_type == 'playlistlivefolder' + and item_filter.get(HIDE_LIVE) + ) or ( + kind_type == 'playlistmembersfolder' + and item_filter.get(HIDE_MEMBERS) + ) or ( + kind_type == 'playlistshortsfolder' + and item_filter.get(HIDE_SHORTS) + ) + )): + continue + playlist_id = item_id # set channel id to 'mine' if the path is for a playlist of our own channel_id = snippet.get('channelId') if context.get_path().startswith(PATHS.MY_PLAYLISTS): @@ -259,15 +345,15 @@ def _process_list_response(provider, uri_channel_id = channel_id if uri_channel_id: item_uri = context.create_uri( - (PATHS.CHANNEL, uri_channel_id, 'playlist', item_id,), + (PATHS.CHANNEL, uri_channel_id, 'playlist', playlist_id,), item_params, ) else: video_id = snippet.get('resourceId', {}).get('videoId') if video_id: - item_params['video_id'] = item_id + item_params[VIDEO_ID] = video_id item_uri = context.create_uri( - (PATHS.PLAYLIST, item_id,), + (PATHS.PLAYLIST, playlist_id,), item_params, ) item = DirectoryItem(ui.bold(title), @@ -277,14 +363,21 @@ def _process_list_response(provider, plot=description, category_label=title, channel_id=channel_id, - playlist_id=item_id) - playlist_id_dict[item_id] = item + playlist_id=playlist_id, + **item_params) item.available = yt_item.get('_available', False) elif kind_type == 'playlistitem': - playlist_item_id = item_id - item_id = snippet['resourceId']['videoId'] - item_params['video_id'] = item_id + 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') + item_params[VIDEO_ID] = video_id item_uri = context.create_uri( (PATHS.PLAY,), item_params, @@ -294,17 +387,16 @@ def _process_list_response(provider, image=image, fanart=fanart, plot=description, - video_id=item_id, - channel_id=(snippet.get('videoOwnerChannelId') - or snippet.get('channelId')), - playlist_id=snippet.get('playlistId'), - playlist_item_id=playlist_item_id) + channel_id=channel_id, + playlist_id=playlist_id, + playlist_item_id=playlist_item_id, + **item_params) # date time published_at = snippet.get('publishedAt') if published_at: - datetime = datetime_parser.parse(published_at) - local_datetime = datetime_parser.utc_to_local(datetime) + datetime = parse_to_dt(published_at) + local_datetime = utc_to_local(datetime) # If item is in a playlist, then set data added to playlist item.set_dateadded_from_datetime(local_datetime) @@ -312,12 +404,12 @@ def _process_list_response(provider, details = yt_item['contentDetails'] activity_type = snippet['type'] if activity_type == 'recommendation': - item_id = details['recommendation']['resourceId']['videoId'] + video_id = details['recommendation']['resourceId']['videoId'] elif activity_type == 'upload': - item_id = details['upload']['videoId'] + video_id = details['upload']['videoId'] else: continue - item_params['video_id'] = item_id + item_params[VIDEO_ID] = video_id item_uri = context.create_uri( (PATHS.PLAY,), item_params, @@ -327,7 +419,7 @@ def _process_list_response(provider, image=image, fanart=fanart, plot=description, - video_id=item_id) + **item_params) elif kind_type.startswith('comment'): if kind_type == 'commentthread': @@ -347,73 +439,118 @@ def _process_list_response(provider, snippet, uri=item_uri, reply_count=reply_count) - position = snippet.get('position') or len(items) - item.set_track_number(position + 1) - elif kind_type == 'pluginitem': - item = DirectoryItem(**item_params) + elif kind_type == 'bookmarkitem': + item = BookmarkItem(**item_params) elif kind_type == 'commanditem': item = CommandItem(context=context, **item_params) else: - item = None raise KodionException('Unknown kind: %s' % kind) if not item: continue - if '_context_menu' in yt_item: - item.add_context_menu(**yt_item['_context_menu']) + if not video_id and VIDEO_ID in item_params: + video_id = item_params[VIDEO_ID] + if not playlist_id: + if PLAYLIST_ID in item_params: + playlist_id = item_params[PLAYLIST_ID] + elif not video_id and not channel_id: + if CHANNEL_ID in item_params: + channel_id = item_params[CHANNEL_ID] + + for item_id, new_dict, complete_dict, allow_types, allow_kinds in ( + ( + video_id, + new_video_id_dict, + video_id_dict, + MediaItem, + None, + ), + ( + playlist_id, + new_playlist_id_dict, + playlist_id_dict, + DirectoryItem, + None, + ), + ( + channel_id, + new_channel_id_dict, + channel_id_dict, + DirectoryItem, + {'channel', 'bookmarkitem', 'subscription'}, + ), + ): + if (not item_id + or (allow_types and not isinstance(item, allow_types)) + or (allow_kinds and kind_type not in allow_kinds)): + continue - if isinstance(item, MediaItem): - # Set track number from playlist, or set to current list length to - # match "Default" (unsorted) sort order - if kind_type == 'playlistitem': - position = snippet.get('position') or len(items) - else: - position = len(items) - item.set_track_number(position + 1) - item_id = item.video_id - if item_id in video_id_dict: - if allow_duplicates: - fifo_queue = video_id_dict[item_id] - else: + if complete_dict is None: + complete_dict = new_dict + new_dict = None + + if item_id in complete_dict: + if not allow_duplicates: continue + stack = complete_dict[item_id] + else: + stack = deque() + complete_dict[item_id] = stack + + if new_dict is not None: + new_dict[item_id] = stack + + if is_youtube: + stack.append(item) else: - fifo_queue = deque() - video_id_dict[item_id] = fifo_queue - fifo_queue.appendleft(item) + stack.appendleft(item) + + if '_context_menu' in yt_item: + item.add_context_menu(**yt_item['_context_menu']) if '_callback' in yt_item: item.callback = yt_item.pop('_callback') do_callbacks = True + if not item.get_special_sort(): + # Set track number from playlist, or set to current list position to + # match "Default" (unsorted) sort order + item.set_track_number(snippet.get('position', position) + 1) + position += 1 + items.append(item) - # this will also update the channel_id_dict with the correct channel_id - # for each video. - channel_items_dict = {} + if progress_dialog: + delta = (len(new_video_id_dict) + + len(new_channel_id_dict) + + len(new_playlist_id_dict) + + len(subscription_id_dict)) + progress_dialog.grow_total(delta=delta) + progress_dialog.update(steps=delta) resource_manager = provider.get_resource_manager(context, progress_dialog) resources = { 1: { 'fetcher': resource_manager.get_videos, 'args': ( - video_id_dict, + new_video_id_dict, ), 'kwargs': { 'live_details': True, 'suppress_errors': True, 'defer_cache': True, - 'yt_items': yt_items, + 'yt_items_dict': yt_items_dict, }, 'thread': None, 'updater': update_video_items, 'upd_args': ( provider, context, - video_id_dict, + new_video_id_dict, channel_items_dict, ), 'upd_kwargs': { @@ -427,7 +564,7 @@ def _process_list_response(provider, 2: { 'fetcher': resource_manager.get_playlists, 'args': ( - playlist_id_dict, + new_playlist_id_dict, ), 'kwargs': { 'defer_cache': True, @@ -437,7 +574,7 @@ def _process_list_response(provider, 'upd_args': ( provider, context, - playlist_id_dict, + new_playlist_id_dict, channel_items_dict, ), 'upd_kwargs': { @@ -449,18 +586,18 @@ def _process_list_response(provider, 3: { 'fetcher': resource_manager.get_channels, 'args': ( - channel_id_dict, + new_channel_id_dict, ), 'kwargs': { '_force_run': True, - 'defer_cache': False, + 'defer_cache': True, }, 'thread': None, 'updater': update_channel_items, 'upd_args': ( provider, context, - channel_id_dict, + new_channel_id_dict, subscription_id_dict, channel_items_dict, ), @@ -487,6 +624,9 @@ def _process_list_response(provider, } def _fetch(resource): + active_thread_ids = threads['active_thread_ids'] + thread_id = threading.current_thread().ident + active_thread_ids.add(thread_id) try: data = resource['fetcher'](*resource['args'], **resource['kwargs']) @@ -500,20 +640,18 @@ def _fetch(resource): kwargs['data'] = data updater(*resource['upd_args'], **kwargs) - except Exception as exc: - msg = ('v3._process_list_response._fetch - Error' - '\n\tException: {exc!r}' - '\n\tStack trace (most recent call last):\n{stack}' - .format(exc=exc, stack=format_stack())) - context.log_error(msg) + except Exception: + log.exception('Error') finally: resource['complete'] = True - threads['current'].discard(resource['thread']) - threads['loop'].set() + active_thread_ids.discard(thread_id) + threads['loop_enable'].set() + active_thread_ids = set() + loop_enable = threading.Event() threads = { - 'current': set(), - 'loop': threading.Event(), + 'active_thread_ids': active_thread_ids, + 'loop_enable': loop_enable, } remaining = len(resources) @@ -522,27 +660,18 @@ def _fetch(resource): ]) completed = [] iterator = iter(resources) - threads['loop'].set() - - if progress_dialog: - delta = (len(video_id_dict) - + len(channel_id_dict) - + len(playlist_id_dict) - + len(subscription_id_dict)) - progress_dialog.grow_total(delta=delta) - progress_dialog.update(steps=delta) - - while threads['loop'].wait(): + loop_enable.set() + while loop_enable.wait(1) or active_thread_ids: try: resource_id = next(iterator) except StopIteration: - if remaining <= 0 and not threads['current']: - break - if threads['current']: - threads['loop'].clear() + if active_thread_ids: + loop_enable.clear() for resource_id in completed: del resources[resource_id] remaining = len(resources) + if remaining <= 0 and not active_thread_ids: + break deferred = len([ 1 for resource in resources.values() if resource['defer'] ]) @@ -564,17 +693,17 @@ def _fetch(resource): continue resource['defer'] = False - if not resource['thread']: - if (not resource['kwargs'].pop('_force_run', False) - and not any(resource['args'])): - resource['complete'] = True - continue + if resource['thread']: + continue + if (not resource['kwargs'].pop('_force_run', False) + and not any(resource['args'])): + resource['complete'] = True + continue - new_thread = threading.Thread(target=_fetch, args=(resource,)) - new_thread.daemon = True - threads['current'].add(new_thread) - resource['thread'] = new_thread - new_thread.start() + new_thread = threading.Thread(target=_fetch, args=(resource,)) + new_thread.daemon = True + resource['thread'] = new_thread + new_thread.start() return items, do_callbacks @@ -602,13 +731,16 @@ def response_to_items(provider, reverse=False, allow_duplicates=True, process_next_page=True, - item_filter=None): + item_filter=None, + hide_progress=None, + log=_log): params = context.get_params() settings = context.get_settings() + ui = context.get_ui() items_per_page = settings.items_per_page() - item_filter_param = params.get('item_filter') - current_page = params.get('page') or 1 + item_filter_param = params.get(ITEM_FILTER) + current_page = params.get(PAGE) or 1 exclude_current = params.get('exclude') if exclude_current: exclude_current = exclude_current[:] @@ -618,6 +750,7 @@ def response_to_items(provider, page_token = None remaining = items_per_page post_fill_attempts = 5 + post_filled = False filtered = 0 filtered_items = [] @@ -626,49 +759,44 @@ def response_to_items(provider, playlist_id_dict = {} subscription_id_dict = {} - with context.get_ui().create_progress_dialog( + with ui.create_progress_dialog( heading=context.localize('loading.directory'), message_template=context.localize('loading.directory.progress'), background=True, + hide_progress=hide_progress, ) as progress_dialog: while 1: kind, is_youtube, is_plugin, kind_type = _parse_kind(json_data) if not is_youtube and not is_plugin: - context.log_debug('v3.response_to_items - Response discarded' - '\n\tKind: |{kind}|' - .format(kind=kind)) + log.debug(('Response discarded', 'Kind: %r'), kind) break if kind_type not in _KNOWN_RESPONSE_KINDS: - context.log_error('v3.response_to_items - Unknown kind' - '\n\tKind: |{kind}|' - .format(kind=kind)) + log.error_trace(('Unknown kind', 'Kind: %r'), kind) break pre_filler = json_data.get('_pre_filler') - if not pre_filler: - pass - else: - if hasattr(pre_filler, 'func'): - _json_data = pre_fill( - filler=pre_filler, + if pre_filler: + if hasattr(pre_filler, '__nowrap__'): + _json_data = pre_filler( json_data=json_data, max_results=remaining, exclude=None if allow_duplicates else exclude_current, ) else: - _json_data = pre_filler( + _json_data = pre_fill( + filler=pre_filler, json_data=json_data, max_results=remaining, exclude=None if allow_duplicates else exclude_current, ) - if not _json_data: - break - json_data = _json_data + if _json_data: + json_data = _json_data _item_filter = settings.item_filter( update=(item_filter or json_data.get('_item_filter')), override=item_filter_param, + params=params, exclude=exclude_current, ) result = _process_list_response( @@ -681,7 +809,7 @@ def response_to_items(provider, video_id_dict=video_id_dict, channel_id_dict=channel_id_dict, playlist_id_dict=playlist_id_dict, - subscription_id_dict=subscription_id_dict + subscription_id_dict=subscription_id_dict, ) if result: items, do_callbacks = result @@ -699,12 +827,10 @@ def response_to_items(provider, ) if filtered_out: filtered += len(filtered_out) - context.debug_log and context.log_debug( - 'v3.response_to_item - Items filtered out' - '\n\tItems: [\n\t\t{filtered_out}\n\t]' - .format(filtered_out=',\n\t\t'.join([ - str(item) for item in filtered_out - ])) + log.debugging and log.debug( + 'Items filtered out: {items!e}', + items=map(methodcaller('__str_parts__', as_dict=True), + filtered_out), ) post_filler = json_data.get('_post_filler') @@ -727,22 +853,28 @@ def response_to_items(provider, if exclude_next: exclude_current.extend(exclude_next) - if remaining <= 0 or not post_filler or post_fill_attempts <= 0: + if remaining > 0: + if post_filled: + current_page += 1 + else: break - if hasattr(post_filler, 'func'): - _json_data = post_fill( - filler=post_filler, + if not post_filler or post_fill_attempts <= 0: + break + + if hasattr(post_filler, '__nowrap__'): + _json_data = post_filler( json_data=json_data, ) else: - _json_data = post_filler( + _json_data = post_fill( + filler=post_filler, json_data=json_data, ) if not _json_data: break json_data = _json_data - current_page += 1 + post_filled = True next_page = current_page + 1 items = filtered_items @@ -753,7 +885,7 @@ def response_to_items(provider, items.sort(key=sort, reverse=reverse) # no processing of next page item - if not json_data or not process_next_page or params.get('hide_next_page'): + if not json_data or not process_next_page or params.get(HIDE_NEXT_PAGE): return items # next page @@ -794,20 +926,16 @@ def response_to_items(provider, if (next_page - 1) * yt_results_per_page < yt_total_results: new_params['items_per_page'] = yt_results_per_page - elif context.is_plugin_path( - context.get_infolabel('Container.FolderPath'), - partial=True, - ): + elif ui.get_container_info(FOLDER_URI): next_page = 1 new_params['page'] = 1 else: return items - yt_visitor_data = json_data.get('visitorData') - if yt_visitor_data: - new_params['visitor'] = yt_visitor_data - - if next_page and next_page > 1: + if next_page > 1: + yt_visitor_data = json_data.get('visitorData') + if yt_visitor_data: + new_params['visitor'] = yt_visitor_data yt_click_tracking = json_data.get('clickTracking') if yt_click_tracking: new_params['click_tracking'] = yt_click_tracking @@ -828,8 +956,12 @@ def _parse_kind(item): def pre_fill(filler, json_data, max_results, exclude=None): - page_token = json_data and json_data.get('nextPageToken') + if not json_data: + return None + page_token = json_data.get('nextPageToken') if not page_token: + json_data['_pre_filler'] = None + json_data['_post_filler'] = None return None items = json_data.get('items') or [] @@ -858,13 +990,13 @@ def pre_fill(filler, json_data, max_results, exclude=None): all_items.append(item) num_items += 1 else: - page_token = json_data.get('nextPageToken') or page_token if num_items: remaining -= num_items - elif page_token and pre_fill_attempts > 0: + else: pre_fill_attempts -= 1 - if remaining <= 0 or not page_token or pre_fill_attempts <= 0: + page_token = json_data.get('nextPageToken') + if not page_token or remaining <= 0 or pre_fill_attempts <= 0: break next_response = filler( @@ -885,8 +1017,12 @@ def pre_fill(filler, json_data, max_results, exclude=None): def post_fill(filler, json_data): - page_token = json_data and json_data.get('nextPageToken') + if not json_data: + return None + page_token = json_data.get('nextPageToken') if not page_token: + json_data['_pre_filler'] = None + json_data['_post_filler'] = None return None pre_filler = json_data.get('_pre_filler') diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_login.py index 3fbb496ecc..88a43fb70d 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,130 +11,200 @@ from __future__ import absolute_import, division, unicode_literals from ..youtube_exceptions import LoginException +from ...kodion import logging -def process(mode, provider, context, sign_out_refresh=True): - addon_id = context.get_param('addon_id', None) - access_manager = context.get_access_manager() - localize = context.localize +SIGN_IN = 'in' +SIGN_OUT = 'out' + + +def _do_logout(provider, context, client=None, **kwargs): ui = context.get_ui() + if not context.get_param('confirmed') and not ui.on_yes_no_input( + context.localize('sign.out'), + context.localize('are_you_sure') + ): + return False - def _do_logout(): - refresh_tokens = access_manager.get_refresh_token() + if not client: client = provider.get_client(context) - if any(refresh_tokens): - for _refresh_token in frozenset(refresh_tokens): - try: - if _refresh_token: - client.revoke(_refresh_token) - except LoginException: - pass - access_manager.update_access_token( - addon_id, access_token='', expiry=-1, refresh_token='', - ) - provider.reset_client() - def _do_login(token_type): - _client = provider.get_client(context) + access_manager = context.get_access_manager() + addon_id = context.get_param('addon_id', None) + + success = True + refresh_tokens, num_refresh_tokens = access_manager.get_refresh_tokens() + if num_refresh_tokens: + for refresh_token in frozenset(refresh_tokens): + try: + if refresh_token: + client.revoke(refresh_token) + except LoginException: + success = False + + provider.reset_client(context=context, **kwargs) + access_manager.update_access_token( + addon_id, access_token='', expiry=-1, refresh_token='', + ) + return success + + +def _do_login(provider, context, client=None, **kwargs): + if not client: + client = provider.get_client(context) + + access_manager = context.get_access_manager() + addon_id = context.get_param('addon_id', None) + localize = context.localize + function_cache = context.get_function_cache() + ui = context.get_ui() + ui.on_ok(localize('sign.multi.title'), localize('sign.multi.text')) + + ( + access_tokens, + num_access_tokens, + expiry_timestamp, + ) = access_manager.get_access_tokens() + ( + refresh_tokens, + num_refresh_tokens, + ) = access_manager.get_refresh_tokens() + token_types = ['tv', 'user', 'vr', 'dev'] + new_access_tokens = dict.fromkeys(token_types, None) + for token_idx, token_type in enumerate(token_types): try: - json_data = _client.request_device_and_user_code(token_type) + access_token = access_tokens[token_idx] + refresh_token = refresh_tokens[token_idx] + if access_token and refresh_token: + new_access_tokens[token_type] = access_token + new_token = (access_token, expiry_timestamp, refresh_token) + token_types[token_idx] = new_token + continue + except IndexError: + pass + + if not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=True, + ): + break + + new_token = ('', expiry_timestamp, '') + try: + json_data = client.request_device_and_user_code(token_idx) if not json_data: - return None - except LoginException: - _do_logout() - raise - - interval = int(json_data.get('interval', 5)) - if interval > 60: - interval = 5 - device_code = json_data['device_code'] - user_code = json_data['user_code'] - verification_url = json_data.get('verification_url') - if verification_url: - if verification_url.startswith('https://www.'): - verification_url = verification_url[12:] - else: - verification_url = 'youtube.com/activate' - - message = ''.join(( - localize('sign.go_to') % ui.bold(verification_url), - '[CR]', - localize('sign.enter_code'), - ' ', - ui.bold(user_code), - )) - - with ui.create_progress_dialog( - heading=localize('sign.in'), message=message, background=False - ) as progress_dialog: - steps = ((10 * 60) // interval) # 10 Minutes - progress_dialog.set_total(steps) - for _ in range(steps): - progress_dialog.update() - try: - json_data = _client.request_access_token(token_type, - device_code) + continue + + interval = int(json_data.get('interval', 5)) + if interval > 60: + interval = 5 + device_code = json_data['device_code'] + user_code = json_data['user_code'] + verification_url = json_data.get('verification_url') + if verification_url: + if verification_url.startswith('https://www.'): + verification_url = verification_url[12:] + else: + verification_url = 'youtube.com/activate' + + message = ''.join(( + localize('sign.go_to', ui.bold(verification_url)), + '[CR]', + localize('sign.enter_code'), + ' ', + ui.bold(user_code), + )) + + with ui.create_progress_dialog( + heading=localize('sign.in'), + message=message, + background=False + ) as progress_dialog: + steps = ((10 * 60) // interval) # 10 Minutes + progress_dialog.set_total(steps) + for _ in range(steps): + progress_dialog.update() + json_data = client.request_access_token( + token_idx, device_code + ) if not json_data: break - except LoginException: - _do_logout() - raise - - log_data = json_data.copy() - if 'access_token' in log_data: - log_data['access_token'] = '' - if 'refresh_token' in log_data: - log_data['refresh_token'] = '' - context.log_debug('Requesting access token: |{data}|'.format( - data=log_data - )) - - if 'error' not in json_data: - _access_token = json_data.get('access_token', '') - _refresh_token = json_data.get('refresh_token', '') - if not _access_token and not _refresh_token: - _expiry = 0 - else: - _expiry = int(json_data.get('expires_in', 3600)) - return _access_token, _expiry, _refresh_token - - if json_data['error'] != 'authorization_pending': - message = json_data['error'] - title = '%s: %s' % (context.get_name(), message) - ui.show_notification(message, title) - context.log_error('Error requesting access token: |error|' - .format(error=message)) - - if progress_dialog.is_aborted(): - break - - context.sleep(interval) - return None - - if mode == 'out': - _do_logout() - if sign_out_refresh: - ui.refresh_container() - - elif mode == 'in': - ui.on_ok(localize('sign.multi.title'), localize('sign.multi.text')) - - tokens = ['tv', 'personal'] - for token_type, token in enumerate(tokens): - new_token = _do_login(token_type) or ('', -1, '') - tokens[token_type] = new_token - - context.log_debug('YouTube Login:' - '\n\tType: |{0}|' - '\n\tAccess token: |{1}|' - '\n\tRefresh token: |{2}|' - '\n\tExpires: |{3}|' - .format(token, - bool(new_token[0]), - bool(new_token[2]), - new_token[1])) - - provider.reset_client() - access_manager.update_access_token(addon_id, *zip(*tokens)) - ui.refresh_container() + + log_data = json_data.copy() + if 'access_token' in log_data: + log_data['access_token'] = '' + if 'refresh_token' in log_data: + log_data['refresh_token'] = '' + logging.debug('Requesting access token: {data!r}', + data=log_data) + + if 'error' not in json_data: + access_token = json_data.get('access_token', '') + refresh_token = json_data.get('refresh_token', '') + if not access_token and not refresh_token: + expiry = 0 + else: + expiry = int(json_data.get('expires_in', 3600)) + new_token = (access_token, expiry, refresh_token) + break + + if json_data['error'] != 'authorization_pending': + message = json_data['error'] + title = '%s: %s' % (context.get_name(), message) + ui.show_notification(message, title) + logging.error_trace('Access token request error - %s', + message) + break + + if progress_dialog.is_aborted(): + break + + context.sleep(interval) + except LoginException: + _do_logout(provider, context, client=client) + break + finally: + new_access_tokens[token_type] = new_token[0] + token_types[token_idx] = new_token + logging.debug(('YouTube Login:', + 'Type: {token!r}', + 'Access token: {has_access_token!r}', + 'Expires: {expiry!r}', + 'Refresh token: {has_refresh_token!r}'), + token=token_type, + has_access_token=bool(new_token[0]), + expiry=new_token[1], + has_refresh_token=bool(new_token[2])) + else: + provider.reset_client( + context=context, + access_tokens=new_access_tokens, + **kwargs + ) + access_manager.update_access_token(addon_id, *zip(*token_types)) + return True + return False + + +def process(mode, provider, context, client=None, refresh=True, **kwargs): + if mode == SIGN_OUT: + signed_out = _do_logout( + provider, + context, + client=client, + **kwargs + ) + return signed_out, {provider.FORCE_REFRESH: refresh} + + if mode == SIGN_IN: + signed_in = _do_login( + provider, + context, + client=client, + **kwargs + ) + return signed_in, {provider.FORCE_REFRESH: refresh and signed_in} + + return None, None diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 15ab5baad8..9d15af7c64 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,52 +12,58 @@ import json import random +from collections import defaultdict from ..helper import utils, v3 from ..youtube_exceptions import YouTubeException +from ...kodion import logging from ...kodion.compatibility import string_type, urlencode, urlunsplit, xbmc from ...kodion.constants import ( BUSY_FLAG, + CHANNEL_ID, CONTENT, FORCE_PLAY_PARAMS, + INCOGNITO, + ORDER, PATHS, PLAYBACK_INIT, PLAYER_DATA, + PLAYLIST_ID, + PLAYLIST_IDS, PLAYLIST_PATH, PLAYLIST_POSITION, PLAY_FORCE_AUDIO, PLAY_PROMPT_QUALITY, PLAY_STRM, - PLAY_WITH, + PLAY_USING, + SCREENSAVER, SERVER_WAKEUP, + TRAKT_PAUSE_FLAG, + VIDEO_ID, + VIDEO_IDS, ) from ...kodion.items import AudioItem, UriItem, VideoItem from ...kodion.network import get_connect_address -from ...kodion.utils import ( - datetime_parser, - find_video_id, - format_stack, - select_stream, -) +from ...kodion.utils.datetime import datetime_to_since +from ...kodion.utils.redact import redact_params def _play_stream(provider, context): ui = context.get_ui() params = context.get_params() - video_id = params.get('video_id') + video_id = params.get(VIDEO_ID) if not video_id: - message = context.localize('error.no_streams_found') - ui.show_notification(message, time_ms=5000) + ui.show_notification(context.localize('error.no_streams_found')) return False client = provider.get_client(context) settings = context.get_settings() - incognito = params.get('incognito', False) - screensaver = params.get('screensaver', False) + incognito = params.get(INCOGNITO, False) + screensaver = params.get(SCREENSAVER, False) audio_only = False - is_external = ui.get_property(PLAY_WITH) + is_external = ui.get_property(PLAY_USING) if ((is_external and settings.alternative_player_web_urls()) or settings.default_player_web_urls()): stream = { @@ -68,56 +74,47 @@ 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): + audio_only = not ask_for_quality and settings.audio_only() + if ui.pop_property(PLAY_FORCE_AUDIO): audio_only = True - else: - audio_only = settings.audio_only() use_mpd = ((not is_external or settings.alternative_player_mpd()) and settings.use_mpd_videos() - and context.wakeup(SERVER_WAKEUP, timeout=5)) + and context.ipc_exec(SERVER_WAKEUP, timeout=5)) try: - streams, yt_item = client.get_streams( - context, + streams, yt_item = client.load_stream_info( video_id=video_id, ask_for_quality=ask_for_quality, audio_only=audio_only, + incognito=incognito, use_mpd=use_mpd, ) except YouTubeException as exc: - msg = ('yt_play.play_video - Error' - '\n\tException: {exc!r}' - '\n\tStack trace (most recent call last):\n{stack}' - .format(exc=exc, - stack=format_stack())) - context.log_error(msg) + logging.exception('Error') ui.show_notification(message=exc.get_message()) return False if not streams: - message = context.localize('error.no_streams_found') - ui.show_notification(message, time_ms=5000) + ui.show_notification(context.localize('error.no_streams_found')) + logging.debug('No streams found') return False - stream = select_stream( + stream = _select_stream( context, streams, ask_for_quality=ask_for_quality, audio_only=audio_only, use_mpd=use_mpd, ) - if stream is None: return False video_type = stream.get('video') if video_type and video_type.get('rtmpe'): - message = context.localize('error.rtmpe_not_supported') - ui.show_notification(message, time_ms=5000) + ui.show_notification(context.localize('error.rtmpe_not_supported')) return False - play_suggested = settings.get_bool('youtube.suggested_videos', False) - if play_suggested and not screensaver: + if not screensaver and settings.get_bool(settings.PLAY_SUGGESTED): utils.add_related_video_to_playlist(provider, context, client, @@ -164,8 +161,8 @@ def _play_stream(provider, context): playback_stats = stream.get('playback_stats') playback_data = { - 'video_id': video_id, - 'channel_id': metadata.get('channel', {}).get('id', ''), + VIDEO_ID: video_id, + CHANNEL_ID: metadata.get('channel', {}).get('id', ''), 'video_status': metadata.get('status', {}), 'playing_file': media_item.get_uri(), 'play_count': play_count, @@ -179,7 +176,11 @@ def _play_stream(provider, context): 'refresh_only': screensaver } - ui.set_property(PLAYER_DATA, json.dumps(playback_data, ensure_ascii=False)) + ui.set_property(PLAYER_DATA, + value=playback_data, + process=json.dumps, + log_process=redact_params) + ui.set_property(TRAKT_PAUSE_FLAG, raw=True) context.send_notification(PLAYBACK_INIT, playback_data) return media_item @@ -192,11 +193,20 @@ def _play_playlist(provider, context): if not action and context.get_handle() == -1: action = 'play' - playlist_id = params.get('playlist_id') - playlist_ids = params.get('playlist_ids') - video_ids = params.get('video_ids') - if not playlist_ids and playlist_id: - playlist_ids = [params.get('playlist_id')] + playlist_ids = params.get(PLAYLIST_IDS) + if not playlist_ids: + playlist_id = params.get(PLAYLIST_ID) + if playlist_id: + playlist_ids = [playlist_id] + + video_ids = params.get(VIDEO_IDS) + if not playlist_ids and not video_ids: + video_id = params.get(VIDEO_ID) + if video_id: + video_ids = [video_id] + else: + logging.warning_trace('No playlist found to play') + return False, None resource_manager = provider.get_resource_manager(context) ui = context.get_ui() @@ -230,7 +240,8 @@ def _play_playlist(provider, context): result = v3.response_to_items(provider, context, chunk, - process_next_page=False) + process_next_page=False, + hide_progress=True) video_items.extend(result) progress_dialog.update(steps=len(result)) @@ -260,7 +271,7 @@ def _play_playlist(provider, context): def _play_channel_live(provider, context): - channel_id = context.get_param('channel_id') + channel_id = context.get_param(CHANNEL_ID) _, json_data = provider.get_client(context).search_with_params(params={ 'type': 'video', 'eventType': 'live', @@ -292,98 +303,76 @@ def _play_channel_live(provider, context): ) -def process(provider, context, **_kwargs): - """ - Plays a video, playlist, or channel live stream. - - Video: - plugin://plugin.video.youtube/play/?video_id= - - * VIDEO_ID: YouTube Video ID - - Playlist: - plugin://plugin.video.youtube/play/?playlist_id=[&order=][&action=] - - * PLAYLIST_ID: YouTube Playlist ID - * ORDER: [ask(default)|normal|reverse|shuffle] optional playlist order - * ACTION: [list|play|queue|None(default)] optional action to perform - - Channel live streams: - plugin://plugin.video.youtube/play/?channel_id=[&live=X] - - * CHANNEL_ID: YouTube Channel ID - * X: optional index of live stream to play if channel has multiple live streams. 1 (default) for first live stream - """ - ui = context.get_ui() - - params = context.get_params() - param_keys = params.keys() - - if ({'channel_id', 'playlist_id', 'playlist_ids', 'video_id', 'video_ids'} - .isdisjoint(param_keys)): - listitem_path = context.get_listitem_info('FileNameAndPath') - if context.is_plugin_path(listitem_path, PATHS.PLAY): - video_id = find_video_id(listitem_path) - if video_id: - context.set_params(video_id=video_id) - params['video_id'] = video_id - else: - return False - else: - return False - - video_id = params.get('video_id') - video_ids = params.get('video_ids') - playlist_id = params.get('playlist_id') - - force_play_params = FORCE_PLAY_PARAMS.intersection(param_keys) - - if video_id and not playlist_id and not video_ids: - for param in force_play_params: - del params[param] - ui.set_property(param) - - if context.get_handle() == -1: - # This is required to trigger Kodi resume prompt, along with using - # RunPlugin. Prompt will not be used if using PlayMedia - if force_play_params and not params.get(PLAY_STRM): - return UriItem('command://Action(Play)') - - return UriItem('command://{0}'.format( - context.create_uri( - (PATHS.PLAY,), - params, - play=(xbmc.PLAYLIST_MUSIC - if (ui.get_property(PLAY_FORCE_AUDIO) - or context.get_settings().audio_only()) else - xbmc.PLAYLIST_VIDEO), - ) - )) - - ui.set_property(BUSY_FLAG) - playlist_player = context.get_playlist_player() - position, _ = playlist_player.get_position() - items = playlist_player.get_items() - - media_item = _play_stream(provider, context) - if media_item: - if position and items: - ui.set_property(PLAYLIST_PATH, - items[position - 1]['file']) - ui.set_property(PLAYLIST_POSITION, str(position)) - else: - ui.clear_property(BUSY_FLAG) - for param in force_play_params: - ui.clear_property(param) - - return media_item - - if playlist_id or video_ids or 'playlist_ids' in params: - return _play_playlist(provider, context) +def _select_stream(context, + stream_data_list, + ask_for_quality, + audio_only, + use_mpd=True): + settings = context.get_settings() + if settings.use_isa(): + isa_capabilities = context.inputstream_adaptive_capabilities() + use_adaptive = bool(isa_capabilities) + use_live_adaptive = use_adaptive and 'live' in isa_capabilities + use_live_mpd = use_live_adaptive and settings.use_mpd_live_streams() + else: + use_adaptive = False + use_live_adaptive = False + use_live_mpd = False + + if audio_only: + logging.debug('Audio only') + stream_list = [item for item in stream_data_list + if 'video' not in item] + else: + stream_list = [ + item for item in stream_data_list + if (not item.get('adaptive') + or (not item.get('live') + and ((use_mpd and item.get('dash/video')) + or (use_adaptive and item.get('hls/video')))) + or (item.get('live') + and ((use_live_mpd and item.get('dash/video')) + or (use_live_adaptive and item.get('hls/video'))))) + ] + + if not stream_list: + logging.debug('No streams found') + return None + + def _stream_sort(_stream): + return _stream.get('sort', [0, 0, 0]) + + stream_list.sort(key=_stream_sort, reverse=True) + num_streams = len(stream_list) + + if logging.debugging: + def _default_NA(): + return 'N/A' + + logging.debug('%d available stream(s)', num_streams) + for idx, stream in enumerate(stream_list): + logging.debug(('Stream {idx}', + 'Container: {stream[container]}', + 'Adaptive: {stream[adaptive]}', + 'Audio: {stream[audio]}', + 'Video: {stream[video]}', + 'Sort: {stream[sort]}'), + idx=idx, + stream=defaultdict(_default_NA, stream)) + + 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], + ) + if selected_stream == -1: + logging.debug('No stream selected') + return None + else: + selected_stream = 0 - if 'channel_id' in params: - return _play_channel_live(provider, context) - return False + logging.debug('Stream %d selected', selected_stream) + return stream_list[selected_stream] def process_items_for_playlist(context, @@ -395,7 +384,7 @@ def process_items_for_playlist(context, params = context.get_params() if play_from is None: - play_from = params.get('video_id') + play_from = params.get(VIDEO_ID) if recent_days is None: recent_days = params.get('recent_days') @@ -404,7 +393,7 @@ def process_items_for_playlist(context, if num_items > 1: # select order if order is None: - order = params.get('order') + order = params.get(ORDER) if not order and play_from is None and recent_days is None: order = 'ask' if order == 'ask': @@ -457,7 +446,7 @@ def process_items_for_playlist(context, for idx, item in enumerate(items): if not item.playable: continue - if (recent_limit and datetime_parser.datetime_to_since( + if (recent_limit and datetime_to_since( context, item.get_dateadded(), as_seconds=True, @@ -492,7 +481,97 @@ def process_items_for_playlist(context, command = playlist_player.play_playlist_item(position, defer=True) return UriItem(command) - context.sleep(1) + context.sleep(0.1) else: playlist_player.play_playlist_item(position) return items[position - 1] + + +def process(provider, context, **_kwargs): + """ + Plays a video, playlist, or channel live stream. + + Video: + plugin://plugin.video.youtube/play/?video_id= + + * VIDEO_ID: YouTube Video ID + + Playlist: + plugin://plugin.video.youtube/play/?playlist_id=[&order=][&action=] + + * PLAYLIST_ID: YouTube Playlist ID + * ORDER: [ask(default)|normal|reverse|shuffle] optional playlist order + * ACTION: [list|play|queue|None(default)] optional action to perform + + Channel live streams: + plugin://plugin.video.youtube/play/?channel_id=[&live=X] + + * CHANNEL_ID: YouTube Channel ID + * X: optional index of live stream to play if channel has multiple live streams. 1 (default) for first live stream + """ + ui = context.get_ui() + + params = context.get_params() + param_keys = params.keys() + + if ({CHANNEL_ID, PLAYLIST_ID, PLAYLIST_IDS, VIDEO_ID, VIDEO_IDS} + .isdisjoint(param_keys)): + item_ids = context.parse_item_ids() + if item_ids and VIDEO_ID in item_ids: + context.set_params(**item_ids) + else: + return False + + video_id = params.get(VIDEO_ID) + video_ids = params.get(VIDEO_IDS) + playlist_id = params.get(PLAYLIST_ID) + + force_play_params = FORCE_PLAY_PARAMS.intersection(param_keys) + + if video_id and not playlist_id and not video_ids: + for param in force_play_params: + del params[param] + ui.set_property(param) + + if context.get_handle() == -1: + # This is required to trigger Kodi resume prompt, along with using + # RunPlugin. Prompt will not be used if using PlayMedia + if force_play_params and not params.get(PLAY_STRM): + return UriItem('command://Action(Play)') + + return UriItem('command://{0}'.format( + context.create_uri( + (PATHS.PLAY,), + params, + play=(xbmc.PLAYLIST_MUSIC + if (ui.get_property(PLAY_FORCE_AUDIO) + or context.get_settings().audio_only()) else + xbmc.PLAYLIST_VIDEO), + ) + )) + + if not context.get_system_version().compatible(22): + ui.set_property(BUSY_FLAG) + + media_item = _play_stream(provider, context) + if media_item: + playlist_player = context.get_playlist_player() + position, _ = playlist_player.get_position() + if position: + item_uri = playlist_player.get_item_path(position - 1) + if item_uri: + ui.set_property(PLAYLIST_PATH, item_uri) + ui.set_property(PLAYLIST_POSITION, str(position)) + else: + ui.clear_property(BUSY_FLAG) + for param in force_play_params: + ui.clear_property(param) + + return media_item + + if playlist_id or video_ids or PLAYLIST_IDS in params: + return _play_playlist(provider, context) + + if CHANNEL_ID in params: + return _play_channel_live(provider, context) + return False diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index a56c458279..4f4ab580d9 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,52 +11,64 @@ from __future__ import absolute_import, division, unicode_literals from .utils import get_thumbnail -from ...kodion import KodionException -from ...kodion.constants import CHANNEL_ID, PATHS, PLAYLISTITEM_ID, PLAYLIST_ID -from ...kodion.utils import find_video_id - - -def _process_add_video(provider, context, keymap_action=False): - listitem_path = context.get_listitem_info('FileNameAndPath') +from ...kodion import KodionException, logging +from ...kodion.constants import ( + CHANNEL_ID, + CONTEXT_MENU, + KEYMAP, + FOLDER_URI, + PATHS, + PLAYLIST_ID, + PLAYLIST_ITEM_ID, + TITLE, + URI, + VIDEO_ID, +) + + +def _process_add_video(provider, context): + ui = context.get_ui() + li_path = ui.get_listitem_info(URI) + li_video_id = ui.get_listitem_property(VIDEO_ID) client = provider.get_client(context) - logged_in = provider.is_logged_in() - if not logged_in: + if not client.logged_in: raise KodionException('Playlist/Add: not logged in') - playlist_id = context.get_param('playlist_id', '') + params = context.get_params() + + playlist_id = params.get(PLAYLIST_ID) if not playlist_id: raise KodionException('Playlist/Add: missing playlist_id') - - if playlist_id.lower() == 'watch_later': + elif playlist_id == 'watch_later': playlist_id = context.get_access_manager().get_watch_later_id() - notify_message = context.localize('watch_later.added_to') - else: - notify_message = context.localize('playlist.added_to') - video_id = context.get_param('video_id', '') + video_id = params.get(VIDEO_ID, li_video_id) if not video_id: - if context.is_plugin_path(listitem_path, PATHS.PLAY): - video_id = find_video_id(listitem_path) - keymap_action = True + video_id = context.parse_item_ids(li_path).get(VIDEO_ID) if not video_id: raise KodionException('Playlist/Add: missing video_id') - json_data = client.add_video_to_playlist(playlist_id=playlist_id, - video_id=video_id) - if not json_data: - context.log_debug('Playlist/Add: failed for playlist |{playlist_id}|' - .format(playlist_id=playlist_id)) - return False + localize = context.localize + success = client.add_video_to_playlist(playlist_id, video_id) + if not success: + logging.debug('Playlist/Add: failed for playlist {playlist_id!r}' + .format(playlist_id=playlist_id)) + ui.show_notification( + message=localize(('failed.x', ('add.to.x', 'playlist'))), + time_ms=2500, + audible=False, + ) + return False, {provider.FORCE_RETURN: True} - context.get_ui().show_notification( - message=notify_message, + ui.show_notification( + message=localize(('added.to.x', 'playlist')), time_ms=2500, audible=False, ) - if keymap_action: - context.get_ui().set_focus_next_item() + if params.get(KEYMAP) or not params.get(CONTEXT_MENU): + ui.set_focus_next_item() data_cache = context.get_data_cache() playlist_cache = data_cache.get_item_like(','.join((playlist_id, '%'))) @@ -65,85 +77,82 @@ def _process_add_video(provider, context, keymap_action=False): if cached_last_page: data_cache.update_item(cache_key, None) - return True + return True, {provider.FORCE_RETURN: True} def _process_remove_video(provider, context, playlist_id=None, + playlist_item_id=None, video_id=None, video_name=None, confirmed=None): - container_uri = context.get_infolabel('Container.FolderPath') - listitem_playlist_id = context.get_listitem_property(PLAYLIST_ID) - listitem_video_id = context.get_listitem_property(PLAYLISTITEM_ID) - listitem_video_name = context.get_listitem_info('Title') - keymap_action = False + ui = context.get_ui() + container_uri = ui.get_container_info(FOLDER_URI) + li_playlist_id = ui.get_listitem_property(PLAYLIST_ID) + li_playlist_item_id = ui.get_listitem_property(PLAYLIST_ITEM_ID) + li_video_id = ui.get_listitem_property(VIDEO_ID) + li_video_name = ui.get_listitem_info(TITLE) + + client = provider.get_client(context) + if not client.logged_in: + raise KodionException('Playlist/Remove: not logged in') params = context.get_params() + if playlist_id is None: - playlist_id = params.pop('playlist_id', None) + playlist_id = params.pop(PLAYLIST_ID, li_playlist_id) + if playlist_item_id is None: + playlist_item_id = params.pop(PLAYLIST_ITEM_ID, + li_playlist_item_id) if video_id is None: - video_id = params.pop('video_id', None) + video_id = params.pop(VIDEO_ID, li_video_id) if video_name is None: - video_name = params.pop('item_name', None) + video_name = params.pop('item_name', li_video_name) if confirmed is None: confirmed = params.pop('confirmed', False) - video_params = ( - {playlist_id, video_id} if confirmed else - {playlist_id, video_id, video_name} - ) - params_required = 2 if confirmed else 3 - if None in video_params or len(video_params) != params_required: - if len(video_params) != 1: - if confirmed: - return False - raise KodionException('Playlist/Remove: missing parameters |{0}|' - .format(video_params)) - - video_params = ( - {listitem_playlist_id, listitem_video_id} if confirmed else - {listitem_playlist_id, listitem_video_id, listitem_video_name} - ) - if '' in video_params or len(video_params) != params_required: - if confirmed: - return False - raise KodionException('Playlist/Remove: missing listitem info |{0}|' - .format(video_params)) - - playlist_id = listitem_playlist_id - video_id = listitem_video_id - video_name = listitem_video_name - keymap_action = True - - if playlist_id.strip().lower() in {'wl', 'hl'}: - context.log_debug('Playlist/Remove: failed for playlist |{playlist_id}|' - .format(playlist_id=playlist_id)) + if not playlist_id: + if confirmed: + return False + raise KodionException('Playlist/Remove: missing playlist ID') + elif playlist_id == 'watch_later': + playlist_id = context.get_access_manager().get_watch_later_id() + elif playlist_id.lower() == 'hl': + logging.debug('Playlist/Remove: failed for playlist {playlist_id!r}' + .format(playlist_id=playlist_id)) return False - if confirmed or context.get_ui().on_remove_content(video_name): + localize = context.localize + if confirmed or ui.on_remove_content(video_name): success = provider.get_client(context).remove_video_from_playlist( playlist_id=playlist_id, - playlist_item_id=video_id, + playlist_item_id=playlist_item_id, + video_id=video_id, ) if not success: + ui.show_notification( + message=localize(('failed.x', ('remove.from.x', 'playlist'))), + time_ms=2500, + audible=False, + ) return False - context.get_ui().show_notification( - message=context.localize('playlist.removed_from'), - time_ms=2500, - audible=False, - ) + if not confirmed: + ui.show_notification( + message=localize(('removed.from.x', 'playlist')), + time_ms=2500, + audible=False, + ) - if not context.is_plugin_path(container_uri): + if not container_uri: return True - if (keymap_action or video_id == listitem_video_id) and not confirmed: - context.get_ui().set_focus_next_item() + if params.get(KEYMAP) or not params.get(CONTEXT_MENU): + ui.set_focus_next_item() - if playlist_id in container_uri: - path, params = context.parse_uri(container_uri) + path, params = context.parse_uri(container_uri) + if path.rstrip('/').endswith('/'.join((PATHS.PLAYLIST, playlist_id))): if 'refresh' not in params: params['refresh'] = True else: @@ -160,49 +169,61 @@ def _process_remove_video(provider, def _process_remove_playlist(provider, context): - channel_id = context.get_listitem_property(CHANNEL_ID) + ui = context.get_ui() + channel_id = ui.get_listitem_property(CHANNEL_ID) + li_playlist_id = ui.get_listitem_property(PLAYLIST_ID) + li_playlist_name = ui.get_listitem_info(TITLE) params = context.get_params() - ui = context.get_ui() + localize = context.localize - playlist_id = params.get('playlist_id', '') + playlist_id = params.get(PLAYLIST_ID, li_playlist_id) if not playlist_id: raise KodionException('Playlist/Remove: missing playlist_id') - playlist_name = params.get('item_name', '') + playlist_name = params.get('item_name', li_playlist_name) if not playlist_name: - raise KodionException('Playlist/Remove: missing playlist_name') + raise KodionException('Playlist/Remove: missing item_name') if ui.on_delete_content(playlist_name): - json_data = provider.get_client(context).remove_playlist(playlist_id) - if not json_data: - return False + success = provider.get_client(context).remove_playlist(playlist_id) + if not success: + ui.show_notification( + message=localize(('failed.x', ('remove.x', 'playlist'))), + time_ms=2500, + audible=False, + ) + return False, None + + ui.show_notification( + message=localize('removed.name.x', playlist_name), + time_ms=2500, + audible=False, + ) if channel_id: data_cache = context.get_data_cache() data_cache.del_item(channel_id) - ui.refresh_container() - return False + return True, {provider.FORCE_REFRESH: True} + + return False, None def _process_select_playlist(provider, context): - # Get listitem path asap, relies on listitems focus - listitem_path = context.get_listitem_info('FileNameAndPath') + ui = context.get_ui() + li_path = ui.get_listitem_info(URI) + li_video_id = ui.get_listitem_property(VIDEO_ID) params = context.get_params() - ui = context.get_ui() - keymap_action = False page_token = '' current_page = 0 - video_id = params.get('video_id', '') + video_id = params.get(VIDEO_ID, li_video_id) if not video_id: - if context.is_plugin_path(listitem_path, PATHS.PLAY): - video_id = find_video_id(listitem_path) - if video_id: - context.set_params(video_id=video_id) - keymap_action = True - if not video_id: + item_ids = context.parse_item_ids(li_path) + if item_ids and VIDEO_ID in item_ids: + context.set_params(**item_ids) + else: raise KodionException('Playlist/Select: missing video_id') function_cache = context.get_function_cache() @@ -229,11 +250,13 @@ def _process_select_playlist(provider, context): while 1: current_page += 1 - json_data = function_cache.run(client.get_playlists_of_channel, - function_cache.ONE_MINUTE // 3, - _refresh=context.refresh_requested(), - channel_id='mine', - page_token=page_token) + json_data = function_cache.run( + client.get_playlists_of_channel, + function_cache.ONE_MINUTE // 3, + _refresh=context.refresh_requested(), + channel_id='mine', + page_token=page_token, + ) if not json_data: break playlists = json_data.get('items', []) @@ -241,14 +264,14 @@ def _process_select_playlist(provider, context): items = [] if current_page == 1: - # create playlist + # Create a new playlist items.append(( ui.bold(context.localize('playlist.create')), '', 'playlist.create', default_thumb, )) - # add the 'Watch Later' playlist + # Add the 'Watch Later' playlist if watch_later_id: items.append(( ui.bold(context.localize('watch_later')), '', @@ -256,8 +279,9 @@ def _process_select_playlist(provider, context): context.create_resource_path('media', 'watch_later.png') )) - # add the 'History' playlist - if watch_history_id: + # Add the custom 'History' playlist + # Can't directly add items to the YouTube Watch History list + if watch_history_id and watch_history_id.upper() != 'HL': items.append(( ui.bold(context.localize('history')), '', watch_history_id, @@ -283,7 +307,7 @@ def _process_select_playlist(provider, context): if page_token: next_page = current_page + 1 items.append(( - ui.bold(context.localize('page.next') % next_page), '', + ui.bold(context.localize('page.next', next_page)), '', 'playlist.next', 'DefaultFolder.png', )) @@ -304,55 +328,75 @@ def _process_select_playlist(provider, context): playlist_id = result if playlist_id: - new_params = dict(context.get_params(), playlist_id=playlist_id) + new_params = dict(params, playlist_id=playlist_id) new_context = context.clone(new_params=new_params) - _process_add_video(provider, new_context, keymap_action) + _process_add_video(provider, new_context) break def _process_rename_playlist(provider, context): - params = context.get_params() ui = context.get_ui() + li_playlist_id = ui.get_listitem_property(PLAYLIST_ID) + li_playlist_name = ui.get_listitem_info(TITLE) + + params = context.get_params() + localize = context.localize - playlist_id = params.get('playlist_id', '') + playlist_id = params.get(PLAYLIST_ID, li_playlist_id) if not playlist_id: - raise KodionException('playlist/rename: missing playlist_id') + raise KodionException('Playlist/Rename: missing playlist_id') result, text = ui.on_keyboard_input( - context.localize('rename'), default=params.get('item_name', ''), + localize('rename'), + default=params.get('item_name', li_playlist_name), ) if not result or not text: - return False + return False, None - json_data = provider.get_client(context).rename_playlist( + success = provider.get_client(context).rename_playlist( playlist_id=playlist_id, new_title=text, ) - if not json_data: - return False + if not success: + ui.show_notification( + message=localize(('failed.x', ('rename', 'playlist'))), + time_ms=2500, + audible=False, + ) + return False, None + + ui.show_notification( + message=localize('succeeded'), + time_ms=2500, + audible=False, + ) data_cache = context.get_data_cache() data_cache.del_item(playlist_id) - ui.refresh_container() - return False + return True, {provider.FORCE_REFRESH: True} def _playlist_id_change(context, playlist, command): - playlist_id = context.get_param('playlist_id', '') + ui = context.get_ui() + li_playlist_id = ui.get_listitem_property(PLAYLIST_ID) + li_playlist_name = ui.get_listitem_info(TITLE) + + playlist_id = context.get_param(PLAYLIST_ID, li_playlist_id) if not playlist_id: raise KodionException('{type}/{command}: missing playlist_id' .format(type=playlist, command=command)) - playlist_name = context.get_param('item_name', '') + + playlist_name = context.get_param('item_name', li_playlist_name) if not playlist_name: - raise KodionException('{type}/{command}: missing playlist_name' + raise KodionException('{type}/{command}: missing item_name' .format(type=playlist, command=command)) - if context.get_ui().on_yes_no_input( + if ui.on_yes_no_input( context.get_name(), context.localize('{type}.list.{command}.check'.format( type=playlist, command=command - )) % playlist_name + ), playlist_name), ): - if command == 'remove': + if command == 'unassign': playlist_id = None if playlist == 'watch_later': context.get_access_manager().set_watch_later_id(playlist_id) @@ -362,6 +406,84 @@ def _playlist_id_change(context, playlist, command): return False +def _process_rate_playlist(provider, + context, + rating, + playlist_id=None, + playlist_name=None, + confirmed=None): + ui = context.get_ui() + container_uri = ui.get_container_info(FOLDER_URI) + li_path = ui.get_listitem_info(URI) + li_playlist_id = ui.get_listitem_property(PLAYLIST_ID) + li_playlist_name = ui.get_listitem_info(TITLE) + + params = context.get_params() + if playlist_id is None: + playlist_id = params.pop(PLAYLIST_ID, li_playlist_id) + if playlist_name is None: + playlist_name = params.pop('item_name', li_playlist_name) + if confirmed is None: + confirmed = rating == 'like' or params.pop('confirmed', False) + + localize = context.localize + + if not playlist_id: + playlist_id = context.parse_item_ids(li_path).get(PLAYLIST_ID) + if not playlist_id: + raise KodionException('Playlist/Rate: missing playlist_id') + + client = provider.get_client(context) + if (rating == 'like' + or confirmed + or context.get_ui().on_remove_content(playlist_name)): + success = client.rate_playlist(playlist_id, rating) + else: + success = None + + if success: + ui.show_notification( + message=(localize('saved') + if rating == 'like' else + localize('removed.name.x', playlist_name)), + time_ms=2500, + audible=False, + ) + + if not container_uri: + return True + + if params.get(KEYMAP) or not params.get(CONTEXT_MENU): + ui.set_focus_next_item() + + path, params = context.parse_uri(container_uri) + if path.startswith(PATHS.SAVED_PLAYLISTS): + if 'refresh' not in params: + params['refresh'] = True + else: + path = params.pop('reload_path', False if confirmed else None) + + if path is not False: + provider.reroute( + context, + path=path, + params=params, + ) + return True + + elif success is False: + ui.show_notification( + message=(localize(('failed.x', 'save')) + if rating == 'like' else + localize('remove')), + time_ms=2500, + audible=False, + ) + return False + + return None + + def process(provider, context, re_match=None, @@ -374,26 +496,29 @@ def process(provider, if category is None: category = re_match.group('category') - if command == 'add' and category == 'video': - return _process_add_video(provider, context) + if category == 'video': + if command == 'add': + return _process_add_video(provider, context) - if command == 'remove' and category == 'video': - return _process_remove_video(provider, context, **kwargs) + if command == 'remove': + return _process_remove_video(provider, context, **kwargs) - if command == 'remove' and category == 'playlist': - return _process_remove_playlist(provider, context) + elif category == 'playlist': + if command == 'remove': + return _process_remove_playlist(provider, context) - if command == 'select' and category == 'playlist': - return _process_select_playlist(provider, context) + if command == 'select': + return _process_select_playlist(provider, context) - if command == 'rename' and category == 'playlist': - return _process_rename_playlist(provider, context) + if command == 'rename': + return _process_rename_playlist(provider, context) - if command in {'set', 'remove'} and category == 'watch_later': - return _playlist_id_change(context, category, command) + if command in {'like', 'unlike'}: + return _process_rate_playlist(provider, context, command) - if command in {'set', 'remove'} and category == 'history': - return _playlist_id_change(context, category, command) + elif category in {'watch_later', 'history'}: + if command in {'assign', 'unassign'}: + return _playlist_id_change(context, category, command) - raise KodionException('Unknown playlist category |{0}| or command |{1}|' + raise KodionException('Unknown playlist category {0!r} or command {1!r}' .format(category, command)) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index f0929067f2..b1eb592a4c 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -14,10 +14,10 @@ from ...kodion.compatibility import urlencode, xbmcvfs from ...kodion.constants import ADDON_ID, DATA_PATH, WAIT_END_FLAG -from ...kodion.network import httpd_status, get_listen_addresses +from ...kodion.network import get_listen_addresses, httpd_status from ...kodion.sql_store import PlaybackHistory, SearchHistory -from ...kodion.utils import to_unicode -from ...kodion.utils.datetime_parser import strptime +from ...kodion.utils.convert_format import to_unicode +from ...kodion.utils.datetime import since_epoch, strptime def process_pre_run(context): @@ -36,8 +36,7 @@ def process_language(context, step, steps, **_kwargs): step=step, steps=steps, ), - (localize('setup_wizard.prompt') - % localize('setup_wizard.prompt.locale')) + localize(('setup_wizard.prompt.x', 'setup_wizard.prompt.locale')), ): context.execute( 'RunScript({addon_id},config/language_region)'.format( @@ -60,8 +59,8 @@ def process_geo_location(context, step, steps, **_kwargs): step=step, steps=steps, ), - (localize('setup_wizard.prompt') - % localize('setup_wizard.prompt.my_location')) + localize(('setup_wizard.prompt.x', + 'setup_wizard.prompt.my_location')), ): context.execute( 'RunScript({addon_id},config/geo_location)'.format( @@ -86,15 +85,15 @@ def process_default_settings(context, step, steps, **_kwargs): step=step, steps=steps, ), - (localize('setup_wizard.prompt') - % localize('setup_wizard.prompt.settings.defaults')) + localize(('setup_wizard.prompt.x', + 'setup_wizard.prompt.settings.defaults')), ): settings.use_isa(True) settings.use_mpd_videos(True) settings.stream_select(4 if settings.ask_for_video_quality() else 3) settings.set_subtitle_download(False) if context.get_system_version().compatible(21): - settings.live_stream_type(3) + settings.live_stream_type(2) else: settings.live_stream_type(1) if not xbmcvfs.exists('special://profile/playercorefactory.xml'): @@ -102,10 +101,9 @@ def process_default_settings(context, step, steps, **_kwargs): settings.default_player_web_urls(False) settings.alternative_player_web_urls(False) settings.alternative_player_mpd(False) - if settings.cache_size() < 20: - settings.cache_size(20) - if context.get_infobool('System.Platform.Linux'): - settings.httpd_sleep_allowed(False) + if settings.cache_size() < 50: + settings.cache_size(50) + settings.httpd_sleep_allowed(True) with ui.create_progress_dialog( heading=localize('httpd'), message=localize('httpd.connect.wait'), @@ -113,8 +111,9 @@ def process_default_settings(context, step, steps, **_kwargs): background=False, ) as progress_dialog: progress_dialog.update() - if settings.httpd_listen() == '0.0.0.0': - settings.httpd_listen('127.0.0.1') + ip_address = settings.httpd_listen() + if ip_address == '0.0.0.0': + ip_address = settings.httpd_listen('127.0.0.1') if not httpd_status(context): port = settings.httpd_port() addresses = get_listen_addresses() @@ -122,15 +121,17 @@ def process_default_settings(context, step, steps, **_kwargs): for address in addresses: progress_dialog.update() if httpd_status(context, (address, port)): - settings.httpd_listen(address) + ip_address = settings.httpd_listen(address) break - context.sleep(5) + context.sleep(3) else: - ui.show_notification( - localize('httpd.connect.failed'), - header=localize('httpd'), - ) + ui.show_notification(localize('httpd.connect.failed'), + header=localize('httpd')) settings.httpd_listen('0.0.0.0') + ip_address = None + if ip_address: + ui.on_ok(context.get_name(), + context.localize('client.ip.is.x', ip_address)) return step @@ -146,8 +147,8 @@ def process_list_detail_settings(context, step, steps, **_kwargs): step=step, steps=steps, ), - (localize('setup_wizard.prompt') - % localize('setup_wizard.prompt.settings.list_details')) + localize(('setup_wizard.prompt.x', + 'setup_wizard.prompt.settings.list_details')), ): settings.show_detailed_description(False) settings.show_detailed_labels(False) @@ -170,48 +171,48 @@ def process_performance_settings(context, step, steps, **_kwargs): step=step, steps=steps, ), - (localize('setup_wizard.prompt') - % localize('setup_wizard.prompt.settings.performance')) + localize(('setup_wizard.prompt.x', + 'setup_wizard.prompt.settings.performance')), ): device_types = { '720p30': { 'max_resolution': 3, # 720p - 'stream_features': ('avc1', 'mp4a', 'filter', 'alt_sort'), + 'stream_features': ('avc1', '3d', 'vr', 'prefer_dub', 'prefer_auto_dub', 'mp4a', 'vtt', 'filter', 'alt_sort'), 'num_items': 10, }, '1080p30_avc': { 'max_resolution': 4, # 1080p - 'stream_features': ('avc1', 'vorbis', 'mp4a', 'filter', 'alt_sort'), + 'stream_features': ('avc1', '3d', 'vr', 'prefer_dub', 'prefer_auto_dub', 'vorbis', 'mp4a', 'vtt', 'filter', 'alt_sort'), 'num_items': 10, }, '1080p30': { 'max_resolution': 4, # 1080p - 'stream_features': ('avc1', 'vp9', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'filter', 'alt_sort'), + 'stream_features': ('avc1', 'vp9', '3d', 'vr', 'prefer_dub', 'prefer_auto_dub', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'vtt', 'filter', 'alt_sort'), 'num_items': 20, }, '1080p60': { 'max_resolution': 4, # 1080p - 'stream_features': ('avc1', 'vp9', 'hfr', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'filter'), + 'stream_features': ('avc1', 'vp9', 'hfr', '3d', 'vr', 'prefer_dub', 'prefer_auto_dub', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'vtt', 'filter'), 'num_items': 30, }, '4k30': { 'max_resolution': 6, # 4k - 'stream_features': ('avc1', 'vp9', 'hdr', 'hfr', 'no_hfr_max', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'filter'), + 'stream_features': ('avc1', 'vp9', 'hdr', 'hfr', '3d', 'vr', 'prefer_dub', 'prefer_auto_dub', 'no_hfr_max', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'vtt', 'filter'), 'num_items': 50, }, '4k60': { 'max_resolution': 6, # 4k - 'stream_features': ('avc1', 'vp9', 'hdr', 'hfr', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'filter'), + 'stream_features': ('avc1', 'vp9', 'hdr', 'hfr', '3d', 'vr', 'prefer_dub', 'prefer_auto_dub', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'vtt', 'filter'), 'num_items': 50, }, '4k60_av1': { 'max_resolution': 6, # 4k - 'stream_features': ('avc1', 'vp9', 'av01', 'hdr', 'hfr', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'filter'), + 'stream_features': ('avc1', 'vp9', 'av01', 'hdr', 'hfr', '3d', 'vr', 'prefer_dub', 'prefer_auto_dub', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'vtt', 'filter'), 'num_items': 50, }, 'max': { 'max_resolution': 7, # 8k - 'stream_features': ('avc1', 'vp9', 'av01', 'hdr', 'hfr', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'filter'), + 'stream_features': ('avc1', 'vp9', 'av01', 'hdr', 'hfr', '3d', 'vr', 'prefer_dub', 'prefer_auto_dub', 'vorbis', 'mp4a', 'ssa', 'ac-3', 'ec-3', 'dts', 'vtt', 'filter'), 'num_items': 50, }, } @@ -250,8 +251,8 @@ def process_subtitles(context, step, steps, **_kwargs): step=step, steps=steps, ), - (localize('setup_wizard.prompt') - % localize('setup_wizard.prompt.subtitles')) + localize(('setup_wizard.prompt.x', + 'setup_wizard.prompt.subtitles')), ): context.execute( 'RunScript({addon_id},config/subtitles)'.format( @@ -286,7 +287,7 @@ def process_old_search_db(context, step, steps, **_kwargs): def _convert_old_search_item(value, item): return { 'text': to_unicode(value), - 'timestamp': strptime(item[1]).timestamp(), + 'timestamp': since_epoch(strptime(item[1])), } search_history = context.get_search_history() @@ -340,7 +341,7 @@ def _convert_old_history_item(value, item): 'total_time': float(values[1]), 'played_time': float(values[2]), 'played_percent': int(values[3]), - 'timestamp': strptime(item[1]).timestamp(), + 'timestamp': since_epoch(strptime(item[1])), } playback_history = context.get_playback_history() @@ -390,3 +391,57 @@ def process_refresh_settings(context, step, steps, **_kwargs): wait_for=WAIT_END_FLAG, ) return step + + +def process_migrate_watch_history(context, step, steps, **_kwargs): + localize = context.localize + access_manager = context.get_access_manager() + watch_history_id = access_manager.get_watch_history_id().upper() + + step += 1 + if (watch_history_id != 'HL' and context.get_ui().on_yes_no_input( + '{youtube} - {setup_wizard} ({step}/{steps})'.format( + youtube=localize('youtube'), + setup_wizard=localize('setup_wizard'), + step=step, + steps=steps, + ), + localize('setup_wizard.prompt.migrate_watch_history'), + )): + access_manager.set_watch_history_id('HL') + context.get_settings().use_remote_history(True) + return step + + +def process_migrate_watch_later(context, step, steps, **_kwargs): + localize = context.localize + access_manager = context.get_access_manager() + watch_later_id = access_manager.get_watch_later_id().upper() + + step += 1 + if (watch_later_id != 'WL' and context.get_ui().on_yes_no_input( + '{youtube} - {setup_wizard} ({step}/{steps})'.format( + youtube=localize('youtube'), + setup_wizard=localize('setup_wizard'), + step=step, + steps=steps, + ), + localize('setup_wizard.prompt.migrate_watch_later'), + )): + access_manager.set_watch_later_id('WL') + return step + + +STEPS = [ + process_default_settings, + process_performance_settings, + process_language, + process_subtitles, + process_old_search_db, + process_old_history_db, + process_migrate_watch_history, + process_migrate_watch_later, + process_geo_location, + process_list_detail_settings, + process_refresh_settings, +] diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 4481548d63..e5c3e79899 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -12,11 +12,25 @@ from functools import partial -from . import UrlResolver, UrlToItemConverter, tv, utils, v3 -from ...kodion import KodionException -from ...kodion.constants import CONTENT, PATHS +from . import UrlResolver, UrlToItemConverter, utils, v3 +from ...kodion import KodionException, logging +from ...kodion.constants import ( + CHANNEL_ID, + CHANNEL_IDS, + CONTENT, + HIDE_FOLDERS, + HIDE_LIVE, + HIDE_SHORTS, + HIDE_VIDEOS, + INCOGNITO, + PAGE, + PATHS, + PLAYLIST_ID, + PLAYLIST_IDS, + VIDEO_ID, +) from ...kodion.items import DirectoryItem, UriItem -from ...kodion.utils import strip_html_from_text +from ...kodion.utils.convert_format import strip_html_from_text def _process_related_videos(provider, context, client): @@ -24,7 +38,7 @@ def _process_related_videos(provider, context, client): refresh = context.refresh_requested() params = context.get_params() - video_id = params.get('video_id') + video_id = params.get(VIDEO_ID) if video_id: json_data = function_cache.run( client.get_related_videos, @@ -45,6 +59,10 @@ def _process_related_videos(provider, context, client): ) json_data['_pre_filler'] = filler json_data['_post_filler'] = filler + category_label = context.localize( + 'video.related.to.x', + params.get('item_name') or context.localize('untitled'), + ) else: json_data = function_cache.run( client.get_related_for_home, @@ -53,6 +71,7 @@ def _process_related_videos(provider, context, client): ) if not json_data: return False, None + category_label = None result = v3.response_to_items( provider, @@ -64,7 +83,7 @@ def _process_related_videos(provider, context, client): provider.CONTENT_TYPE: { 'content_type': CONTENT.VIDEO_CONTENT, 'sub_type': None, - 'category_label': None, + 'category_label': category_label, }, } return result, options @@ -72,7 +91,7 @@ def _process_related_videos(provider, context, client): def _process_comments(provider, context, client): params = context.get_params() - video_id = params.get('video_id') + video_id = params.get(VIDEO_ID) parent_id = params.get('parent_id') if not video_id and not parent_id: return False, None @@ -96,7 +115,7 @@ def _process_comments(provider, context, client): options = { provider.CONTENT_TYPE: { 'content_type': CONTENT.LIST_CONTENT, - 'sub_type': 'comments', + 'sub_type': CONTENT.COMMENTS, 'category_label': params.get('item_name', video_id), }, } @@ -107,25 +126,37 @@ def _process_recommendations(provider, context, client): function_cache = context.get_function_cache() refresh = context.refresh_requested() params = context.get_params() - # source = client.get_recommended_for_home_tv - source = client.get_recommended_for_home_vr + + browse_id = 'FEwhat_to_watch' + browse_client = 'tv' + browse_paths = client.JSON_PATHS['tv_shelf_horizontal'] + # browse_client = 'android_vr' + # browse_paths = client.JSON_PATHS['vr_shelf'] json_data = function_cache.run( - source, + client.get_browse_items, function_cache.ONE_HOUR, _refresh=refresh, - visitor=params.get('visitor'), + browse_id=browse_id, + client=browse_client, + do_auth=True, page_token=params.get('page_token'), click_tracking=params.get('click_tracking'), + visitor=params.get('visitor'), + json_path=browse_paths, ) if not json_data: return False, None filler = partial( function_cache.run, - source, + client.get_browse_items, function_cache.ONE_HOUR, _refresh=refresh, + browse_id=browse_id, + client=browse_client, + do_auth=True, + json_path=browse_paths, ) json_data['_pre_filler'] = filler json_data['_post_filler'] = filler @@ -147,25 +178,13 @@ def _process_recommendations(provider, context, client): def _process_trending(provider, context, client): - function_cache = context.get_function_cache() - refresh = context.refresh_requested() - - json_data = function_cache.run( - client.get_trending_videos, - function_cache.ONE_HOUR, - _refresh=refresh, + json_data = client.get_trending_videos( page_token=context.get_param('page_token'), ) if not json_data: return False, None - filler = partial( - function_cache.run, - client.get_trending_videos, - function_cache.ONE_HOUR, - _refresh=refresh, - ) - json_data['_post_filler'] = filler + json_data['_post_filler'] = client.get_trending_videos result = v3.response_to_items(provider, context, json_data) options = { @@ -223,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: @@ -246,7 +267,7 @@ def _process_live_events(provider, context, client, event_type='live'): def _process_description_links(provider, context): params = context.get_params() - incognito = params.get('incognito', False) + incognito = params.get(INCOGNITO, False) addon_id = params.get('addon_id', '') def _extract_urls(video_id): @@ -285,11 +306,11 @@ def _extract_urls(video_id): res_urls.append(resolved_url) if progress_dialog.is_aborted(): - context.log_debug('Resolving urls aborted') + logging.debug('Resolving urls aborted') break url_to_item_converter = UrlToItemConverter() - url_to_item_converter.add_urls(res_urls, context) + url_to_item_converter.process_urls(res_urls, context) result = url_to_item_converter.get_items(provider, context) if not result: @@ -311,7 +332,7 @@ def _extract_urls(video_id): def _display_channels(channel_ids): item_params = {} if incognito: - item_params['incognito'] = incognito + item_params[INCOGNITO] = incognito if addon_id: item_params['addon_id'] = addon_id @@ -325,13 +346,15 @@ def _display_channels(channel_ids): ), channel_id=channel_id, ) - channel_id_dict[channel_id] = channel_item + channel_items = channel_id_dict.setdefault(channel_id, []) + channel_items.append(channel_item) utils.update_channel_items(provider, context, channel_id_dict) # clean up - remove empty entries result = [channel_item - for channel_item in channel_id_dict.values() + for channel_items in channel_id_dict.values() + for channel_item in channel_items if channel_item.get_name()] if not result: return False, None @@ -340,7 +363,10 @@ def _display_channels(channel_ids): provider.CONTENT_TYPE: { 'content_type': CONTENT.LIST_CONTENT, 'sub_type': None, - 'category_label': None, + 'category_label': context.localize( + 'video.description_links.from.x', + params.get('item_name') or context.localize('untitled'), + ), }, } return result, options @@ -348,7 +374,7 @@ def _display_channels(channel_ids): def _display_playlists(playlist_ids): item_params = {} if incognito: - item_params['incognito'] = incognito + item_params[INCOGNITO] = incognito if addon_id: item_params['addon_id'] = addon_id @@ -362,7 +388,8 @@ def _display_playlists(playlist_ids): ), playlist_id=playlist_id, ) - playlist_id_dict[playlist_id] = playlist_item + playlist_items = playlist_id_dict.setdefault(playlist_id, []) + playlist_items.append(playlist_item) channel_items_dict = {} utils.update_playlist_items(provider, @@ -373,7 +400,8 @@ def _display_playlists(playlist_ids): # clean up - remove empty entries result = [playlist_item - for playlist_item in playlist_id_dict.values() + for playlist_items in playlist_id_dict.values() + for playlist_item in playlist_items if playlist_item.get_name()] if not result: return False, None @@ -387,31 +415,66 @@ def _display_playlists(playlist_ids): } return result, options - video_id = params.get('video_id', '') + video_id = params.get(VIDEO_ID) if video_id: return _extract_urls(video_id) - channel_ids = params.get('channel_ids', []) + channel_ids = params.get(CHANNEL_IDS) if channel_ids: return _display_channels(channel_ids) - playlist_ids = params.get('playlist_ids', []) + playlist_ids = params.get(PLAYLIST_IDS) if playlist_ids: return _display_playlists(playlist_ids) - context.log_error('Missing video_id or playlist_ids for description links') + logging.error('Missing video_id or playlist_ids for description links') return False, None -def _process_saved_playlists_tv(provider, context, client): - json_data = client.get_saved_playlists( - page_token=context.get_param('next_page_token', 0), - offset=context.get_param('offset', 0) +def _process_saved_playlists(provider, context, client): + params = context.get_params() + + browse_id = 'FEplaylist_aggregation' + browse_response_type = 'playlists' + browse_client = 'tv' + browse_paths = client.JSON_PATHS['tv_grid'] + + own_channel = client.channel_id + if own_channel: + own_channel = (own_channel,) + + json_data = client.get_browse_items( + browse_id=browse_id, + client=browse_client, + skip_ids=own_channel, + response_type=browse_response_type, + do_auth=True, + page_token=params.get('page_token'), + click_tracking=params.get('click_tracking'), + visitor=params.get('visitor'), + json_path=browse_paths, ) if not json_data: return False, None - result = tv.saved_playlists_to_items(provider, context, json_data) + filler = partial( + client.get_browse_items, + browse_id=browse_id, + client=browse_client, + skip_ids=own_channel, + response_type=browse_response_type, + do_auth=True, + json_path=browse_paths, + ) + json_data['_pre_filler'] = filler + json_data['_post_filler'] = filler + + result = v3.response_to_items( + provider, + context, + json_data, + allow_duplicates=False, + ) options = { provider.CONTENT_TYPE: { 'content_type': CONTENT.LIST_CONTENT, @@ -427,8 +490,9 @@ def _process_my_subscriptions(provider, client, filtered=False, feed_type=None, - _feed_types={'videos', 'shorts', 'live'}): - logged_in = provider.is_logged_in() + _feed_types=frozenset(( + 'videos', 'shorts', 'live' + ))): refresh = context.refresh_requested() if feed_type not in _feed_types: @@ -441,10 +505,12 @@ def _process_my_subscriptions(provider, ) as progress_dialog: json_data = client.get_my_subscriptions( page_token=context.get_param('page', 1), - logged_in=logged_in, do_filter=filtered, feed_type=feed_type, refresh=refresh, + force_cache=(not refresh + and refresh is not False + and refresh is not None), progress_dialog=progress_dialog, ) if not json_data: @@ -452,11 +518,10 @@ def _process_my_subscriptions(provider, filler = partial( client.get_my_subscriptions, - logged_in=logged_in, do_filter=filtered, feed_type=feed_type, refresh=refresh, - use_cache=True, + force_cache=True, progress_dialog=progress_dialog, ) json_data['_post_filler'] = filler @@ -467,30 +532,50 @@ def _process_my_subscriptions(provider, my_subscriptions_path = PATHS.MY_SUBSCRIPTIONS params = context.get_params() - if params.get('page', 1) == 1 and not params.get('hide_folders'): - result = [ - DirectoryItem( - context.localize('my_subscriptions'), - context.create_uri(my_subscriptions_path), - image='{media}/new_uploads.png', - ) - if feed_type != 'videos' and not params.get('hide_videos') else - None, - DirectoryItem( - context.localize('shorts'), - context.create_uri((my_subscriptions_path, 'shorts')), - image='{media}/shorts.png', - ) - if feed_type != 'shorts' and not params.get('hide_shorts') else - None, - DirectoryItem( - context.localize('live'), - context.create_uri((my_subscriptions_path, 'live')), - image='{media}/live.png', - ) - if feed_type != 'live' and not params.get('hide_live') else - None, - ] + if params.get(PAGE, 1) == 1 and not params.get(HIDE_FOLDERS): + v3_response = { + 'kind': 'plugin#pluginListResponse', + 'items': [ + None + if feed_type == 'videos' or params.get(HIDE_VIDEOS) else + { + 'kind': 'plugin#videosFolder', + '_params': { + 'name': context.localize('my_subscriptions'), + 'uri': context.create_uri(my_subscriptions_path), + 'image': '{media}/new_uploads.png', + 'special_sort': 'top', + }, + }, + None + if feed_type == 'shorts' or params.get(HIDE_SHORTS) else + { + 'kind': 'plugin#shortsFolder', + '_params': { + 'name': context.localize('shorts'), + 'uri': context.create_uri( + (my_subscriptions_path, 'shorts') + ), + 'image': '{media}/shorts.png', + 'special_sort': 'top', + }, + }, + None + if feed_type == 'live' or params.get(HIDE_LIVE) else + { + 'kind': 'plugin#liveFolder', + '_params': { + 'name': context.localize('live'), + 'uri': context.create_uri( + (my_subscriptions_path, 'live') + ), + 'image': '{media}/live.png', + 'special_sort': 'top', + }, + }, + ], + } + result = v3.response_to_items(provider, context, v3_response) else: result = [] @@ -507,18 +592,57 @@ def _process_my_subscriptions(provider, 'live_folder': True, 'shorts': True, } if feed_type == 'live' else { - 'live': False, - 'shorts': True, - 'upcoming_live': False, - } if feed_type == 'shorts' else { - 'live': False, + 'live_folder': True, 'shorts': True, - 'upcoming_live': False, - } + 'vod': True, + }, )) return result, options +def _process_virtual_list(provider, context, _client, playlist_id=None): + params = context.get_params() + + playlist_id = playlist_id or params.get(PLAYLIST_ID) + if not playlist_id: + return False, None + playlist_id = playlist_id.upper() + context.parse_params({ + CHANNEL_ID: 'mine', + PLAYLIST_ID: playlist_id, + }) + + resource_manager = provider.get_resource_manager(context) + json_data = resource_manager.get_playlist_items( + batch_id=(playlist_id, 0), + page_token=params.get('page_token'), + ) + if not json_data: + return False, None + + filler = partial( + resource_manager.get_playlist_items, + batch_id=(playlist_id, 0), + ) + json_data['_pre_filler'] = filler + json_data['_post_filler'] = filler + + result = v3.response_to_items( + provider, + context, + json_data, + allow_duplicates=False, + ) + options = { + provider.CONTENT_TYPE: { + 'content_type': CONTENT.VIDEO_CONTENT, + 'sub_type': CONTENT.HISTORY if playlist_id == 'HL' else None, + 'category_label': None, + }, + } + return result, options + + def process(provider, context, re_match=None, category=None, sub_category=None): if re_match: if category is None: @@ -526,7 +650,6 @@ def process(provider, context, re_match=None, category=None, sub_category=None): if sub_category is None: sub_category = re_match.group('sub_category') - # required for provider.is_logged_in() client = provider.get_client(context) if category == 'related_videos': @@ -551,7 +674,7 @@ def process(provider, context, re_match=None, category=None, sub_category=None): ) if category == 'disliked_videos': - if provider.is_logged_in(): + if client.logged_in: return _process_disliked_videos(provider, context, client) return UriItem(context.create_uri(('sign', 'in'))) @@ -577,6 +700,9 @@ def process(provider, context, re_match=None, category=None, sub_category=None): return _process_comments(provider, context, client) if category == 'saved_playlists': - return _process_saved_playlists_tv(provider, context, client) + return _process_saved_playlists(provider, context, client) + + if category == 'playlist': + return _process_virtual_list(provider, context, client, sub_category) raise KodionException('YouTube special category "%s" not found' % category) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index f953491acf..3f17e77ad0 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -35,13 +35,14 @@ def _process_list(provider, context, client): def _process_add(_provider, context, client): - listitem_subscription_id = context.get_listitem_property(SUBSCRIPTION_ID) + ui = context.get_ui() + li_subscription_id = ui.get_listitem_property(SUBSCRIPTION_ID) - subscription_id = context.get_param('subscription_id', '') + subscription_id = context.get_param(SUBSCRIPTION_ID) if (not subscription_id - and listitem_subscription_id - and listitem_subscription_id.lower().startswith('uc')): - subscription_id = listitem_subscription_id + and li_subscription_id + and li_subscription_id.lower().startswith('uc')): + subscription_id = li_subscription_id if not subscription_id: return False @@ -50,7 +51,7 @@ def _process_add(_provider, context, client): if not json_data: return False - context.get_ui().show_notification( + ui.show_notification( context.localize('subscribed.to.channel'), time_ms=2500, audible=False, @@ -58,17 +59,18 @@ def _process_add(_provider, context, client): return True -def _process_remove(_provider, context, client): - listitem_subscription_id = context.get_listitem_property(SUBSCRIPTION_ID) - listitem_channel_id = context.get_listitem_property(CHANNEL_ID) +def _process_remove(provider, context, client): + ui = context.get_ui() + li_subscription_id = ui.get_listitem_property(SUBSCRIPTION_ID) + li_channel_id = ui.get_listitem_property(CHANNEL_ID) - subscription_id = context.get_param('subscription_id', '') - if not subscription_id and listitem_subscription_id: - subscription_id = listitem_subscription_id + subscription_id = context.get_param(SUBSCRIPTION_ID) + if not subscription_id and li_subscription_id: + subscription_id = li_subscription_id - channel_id = context.get_param('channel_id', '') - if not channel_id and listitem_channel_id: - channel_id = listitem_channel_id + channel_id = context.get_param(CHANNEL_ID) + if not channel_id and li_channel_id: + channel_id = li_channel_id if subscription_id: success = client.unsubscribe(subscription_id) @@ -78,15 +80,14 @@ def _process_remove(_provider, context, client): success = False if not success: - return False + return False, None - context.get_ui().refresh_container() - context.get_ui().show_notification( + ui.show_notification( context.localize('unsubscribed.from.channel'), time_ms=2500, audible=False, ) - return True + return True, {provider.FORCE_REFRESH: True} def process(provider, context, re_match): @@ -94,7 +95,7 @@ def process(provider, context, re_match): # we need a login client = provider.get_client(context) - if not provider.is_logged_in(): + if not client.logged_in: return UriItem(context.create_uri(('sign', 'in'))) if command == 'list': diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_video.py index 7e9e25cab4..f8a94a3b28 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,34 +11,38 @@ from __future__ import absolute_import, division, unicode_literals from ...kodion import KodionException -from ...kodion.constants import PATHS +from ...kodion.constants import URI, VIDEO_ID from ...kodion.items import menu_items -from ...kodion.utils import find_video_id def _process_rate_video(provider, context, re_match=None, video_id=None, - current_rating=None): - listitem_path = context.get_listitem_info('FileNameAndPath') - ratings = ['like', 'dislike', 'none'] + current_rating=None, + _ratings=('like', 'dislike', 'none')): + ui = context.get_ui() + li_path = ui.get_listitem_info(URI) + + localize = context.localize rating_param = context.get_param('rating', '') if rating_param: - rating_param = rating_param.lower() if rating_param.lower() in ratings else '' + rating_param = rating_param.lower() + if rating_param not in _ratings: + rating_param = '' if video_id is None: - video_id = context.get_param('video_id') + video_id = context.get_param(VIDEO_ID) if not video_id: try: - video_id = re_match.group('video_id') + video_id = re_match.group(VIDEO_ID) except IndexError: - if context.is_plugin_path(listitem_path, PATHS.PLAY): - video_id = find_video_id(listitem_path) - - if not video_id: - raise KodionException('video/rate/: missing video_id') + pass + if not video_id and li_path: + video_id = context.parse_item_ids(li_path).get(VIDEO_ID) + if not video_id: + raise KodionException('video/rate/: missing video_id') if current_rating is None: try: @@ -55,63 +59,66 @@ def _process_rate_video(provider, if items: current_rating = items[0].get('rating', '') - rating_items = [] if not rating_param: - for rating in ratings: - if rating != current_rating: - rating_items.append((context.localize('video.rate.%s' % rating), rating)) - result = context.get_ui().on_select(context.localize('video.rate'), rating_items) + result = ui.on_select(localize('video.rate'), [ + (localize('video.rate.%s' % rating), rating) + for rating in _ratings + if rating != current_rating + ]) elif rating_param != current_rating: result = rating_param else: result = -1 + notify_message = None + response = None if result != -1: - notify_message = '' - response = provider.get_client(context).rate_video(video_id, result) - if response: - # this will be set if we are in the 'Liked Video' playlist - if context.refresh_requested(): - context.get_ui().refresh_container() - if result == 'none': - notify_message = context.localize('unrated.video') + notify_message = localize(('removed.x', 'rating')) elif result == 'like': - notify_message = context.localize('liked.video') + notify_message = localize('liked.video') elif result == 'dislike': - notify_message = context.localize('disliked.video') + notify_message = localize('disliked.video') else: - notify_message = context.localize('failed') - - if notify_message: - context.get_ui().show_notification( - message=notify_message, - time_ms=2500, - audible=False, - ) - - return True + notify_message = localize('failed') + + if notify_message: + ui.show_notification( + message=notify_message, + time_ms=2500, + audible=False, + ) + + return ( + True, + { + # this will be set if we are in the 'Liked Video' playlist + provider.FORCE_REFRESH: response and context.refresh_requested(), + }, + ) def _process_more_for_video(context): params = context.get_params() - video_id = params.get('video_id') + video_id = params.get(VIDEO_ID) if not video_id: raise KodionException('video/more/: missing video_id') + item_name = params.get('item_name') + items = [ - menu_items.add_video_to_playlist(context, video_id), - menu_items.related_videos(context, video_id), - menu_items.video_comments(context, video_id, params.get('item_name')), - menu_items.content_from_description(context, video_id), - menu_items.rate_video(context, video_id), + menu_items.playlist_add_to_selected(context, video_id), + menu_items.video_related(context, video_id, item_name), + menu_items.video_comments(context, video_id, item_name), + menu_items.video_description_links(context, video_id, item_name), + menu_items.video_rate(context, video_id), ] if params.get('logged_in') else [ - menu_items.related_videos(context, video_id), - menu_items.video_comments(context, video_id, params.get('item_name')), - menu_items.content_from_description(context, video_id), + menu_items.video_related(context, video_id, item_name), + menu_items.video_comments(context, video_id, item_name), + menu_items.video_description_links(context, video_id, item_name), ] result = context.get_ui().on_select(context.localize('video.more'), items) diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/provider.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/provider.py index 4784b9afea..5d3fee4888 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/provider.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/provider.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,13 +11,11 @@ from __future__ import absolute_import, division, unicode_literals from atexit import register as atexit_register -from base64 import b64decode from functools import partial -from json import loads as json_loads from re import compile as re_compile from weakref import proxy -from .client import APICheck, YouTube +from .client import YouTubePlayerClient from .helper import ( ResourceManager, UrlResolver, @@ -33,16 +31,29 @@ ) from .helper.utils import channel_filter_split, update_duplicate_items from .youtube_exceptions import InvalidGrant, LoginException -from ..kodion import AbstractProvider +from ..kodion import AbstractProvider, logging from ..kodion.constants import ( ADDON_ID, CHANNEL_ID, CONTENT, - DEVELOPER_CONFIGS, + HIDE_CHANNELS, + HIDE_FOLDERS, + HIDE_LIVE, + HIDE_MEMBERS, + HIDE_PLAYLISTS, + HIDE_SEARCH, + HIDE_SHORTS, + HIDE_VIDEOS, + INCOGNITO, + PAGE, PATHS, + PLAYLIST_ID, + PLAY_COUNT, + VIDEO_ID, ) from ..kodion.items import ( BaseItem, + BookmarkItem, DirectoryItem, NewSearchItem, SearchItem, @@ -50,214 +61,218 @@ VideoItem, menu_items, ) -from ..kodion.utils import strip_html_from_text, to_unicode +from ..kodion.utils.convert_format import strip_html_from_text, to_unicode +from ..kodion.utils.datetime import now, since_epoch class Provider(AbstractProvider): + log = logging.getLogger(__name__) + def __init__(self): super(Provider, self).__init__() self._resource_manager = None self._client = None - self._api_check = None - self._logged_in = False - - self.on_video_x = self.register_path( - '^/video/(?P[^/]+)/?$', - yt_video.process, - ) - - self.on_playlist_x = self.register_path( - '^/playlist/(?P[^/]+)/(?P[^/]+)/?$', - yt_playlist.process, - ) - - self.register_path( - '^/play/?$', - yt_play.process, - ) - - self.on_specials_x = self.register_path( - '^/special/(?P[^/]+)(?:/(?P[^/]+))?/?$', - yt_specials.process, - ) - self.register_path( - '^/subscriptions/(?P[^/]+)/?$', - yt_subscriptions.process, - ) + self.on_video_x = self.register_path(r''.join(( + '^', + PATHS.VIDEO, + '/(?P[^/]+)/?$', + )), yt_video.process) + + self.on_playlist_x = self.register_path(r''.join(( + '^', + PATHS.PLAYLIST, + '/(?P[^/]+)/(?P[^/]+)/?$', + )), yt_playlist.process) + + self.register_path(r''.join(( + '^', + PATHS.PLAY, + '/?$', + )), yt_play.process) + + self.on_specials_x = self.register_path(r''.join(( + '^', + PATHS.SPECIAL, + '/(?P[^/]+)(?:/(?P[^/]+))?/?$', + )), yt_specials.process) + + self.register_path(r''.join(( + '^', + PATHS.SUBSCRIPTIONS, + '/(?P[^/]+)/?$', + )), yt_subscriptions.process) atexit_register(self.tear_down) @staticmethod def get_wizard_steps(): - steps = [ - yt_setup_wizard.process_default_settings, - yt_setup_wizard.process_performance_settings, - yt_setup_wizard.process_language, - yt_setup_wizard.process_subtitles, - yt_setup_wizard.process_geo_location, - yt_setup_wizard.process_old_search_db, - yt_setup_wizard.process_old_history_db, - yt_setup_wizard.process_list_detail_settings, - yt_setup_wizard.process_refresh_settings, - ] - return steps + return yt_setup_wizard.STEPS @staticmethod def pre_run_wizard_step(provider, context): yt_setup_wizard.process_pre_run(context) - def is_logged_in(self): - return self._logged_in - - @staticmethod - def get_dev_config(context, addon_id, dev_configs): - _dev_config = context.get_ui().pop_property(DEVELOPER_CONFIGS) - - dev_config = {} - if _dev_config: - context.log_warning('Using window property for developer keys is' - ' deprecated. Please use the' - ' youtube_registration module instead') - try: - dev_config = json_loads(_dev_config) - except ValueError: - context.log_error('Error loading developer key: |invalid json|') - if not dev_config and addon_id and dev_configs: - dev_config = dev_configs.get(addon_id) - - if dev_config and not context.get_settings().allow_dev_keys(): - context.log_debug('Developer config ignored') - return {} - - if dev_config: - dev_main = dev_origin = None - if {'main', 'origin'}.issubset(dev_config): - dev_main = dev_config['main'] - dev_origin = dev_config['origin'] - - if not {'system', 'key', 'id', 'secret'}.issubset(dev_main): - dev_main = None - - if not dev_main: - context.log_error('Invalid developer config: |{dev_config}|' - '\n\texpected: |{{' - ' "origin": ADDON_ID,' - ' "main": {{' - ' "system": SYSTEM_NAME,' - ' "key": API_KEY,' - ' "id": CLIENT_ID,' - ' "secret": CLIENT_SECRET' - '}}}}|'.format(dev_config=dev_config)) - return {} - - dev_system = dev_main['system'] - if dev_system == 'JSONStore': - dev_key = b64decode(dev_main['key']) - dev_id = b64decode(dev_main['id']) - dev_secret = b64decode(dev_main['secret']) - else: - dev_key = dev_main['key'] - dev_id = dev_main['id'] - dev_secret = dev_main['secret'] - context.log_debug('Using developer config: ' - '|origin: {origin}, system: {system}|' - .format(origin=dev_origin, system=dev_system)) - return { - 'origin': dev_origin, - 'main': { - 'system': dev_system, - 'id': dev_id, - 'secret': dev_secret, - 'key': dev_key, + def reset_client(self, **kwargs): + if self._client: + kwargs.setdefault( + 'configs', + { + 'dev': {}, + 'user': {}, + 'tv': {}, + 'vr': {}, } - } - - return {} - - def reset_client(self): - self._client = None - self._api_check = None + ) + kwargs.setdefault( + 'access_tokens', + { + 'dev': None, + 'user': None, + 'tv': None, + 'vr': None, + } + ) + self._client.reinit(**kwargs) - def get_client(self, context): + def get_client(self, context, refresh=False): access_manager = context.get_access_manager() + api_store = context.get_api_store() + settings = context.get_settings() - if not self._api_check: - self._api_check = APICheck(context) - configs = self._api_check.get_configs() + user = access_manager.get_current_user() + api_last_origin = access_manager.get_last_origin() + + client = self._client + if not client or not client.initialised: + synced = api_store.sync() + else: + synced = False + configs = api_store.get_configs() dev_id = context.get_param('addon_id') if not dev_id or dev_id == ADDON_ID: - dev_id = dev_keys = None origin = ADDON_ID + dev_id = None + if synced: + switch = api_store.get_current_switch() + key_details = api_store.get_key_set(switch) + self.log.debug(('Using personal API details', + 'Config: {config!r}', + 'User #: {user!r}', + 'Key set: {switch!r}'), + config=configs['user']['system'], + user=user, + switch=switch) + else: + switch = None + key_details = None else: - dev_config = self.get_dev_config( - context, dev_id, configs['developer'] + dev_config = api_store.get_developer_config(dev_id) + origin = dev_config.get('origin') + key_details = dev_config.get(origin) + if key_details: + configs[origin] = key_details + switch = 'developer' + self.log.debug(('Using developer provided API details', + 'Config: {config!r}', + 'User #: {user!r}', + 'Key set: {switch!r}'), + config=key_details['system'], + user=user, + switch=switch) + else: + key_details = configs['dev'] + switch = api_store.get_current_switch() + self.log.debug(('Using developer provided access tokens', + 'Config: {config!r}', + 'User #: {user!r}', + 'Key set: {switch!r}'), + config=key_details['system'], + user=user, + switch=switch) + + if not client: + client = YouTubePlayerClient( + context=context, + language=settings.get_language(), + region=settings.get_region(), + configs=configs, + ) + self._client = client + + if key_details: + keys_changed = access_manager.keys_changed( + addon_id=dev_id, + api_key=key_details['key'], + client_id=key_details['id'], + client_secret=key_details['secret'], ) - origin = dev_config.get('origin') or dev_id - dev_keys = dev_config.get('main') + if keys_changed and switch == 'user': + key_details = api_store.get_key_set('user_old') + keys_changed = access_manager.keys_changed( + addon_id=dev_id, + api_key=key_details['key'], + client_id=key_details['id'], + client_secret=key_details['secret'], + update_hash=False, + ) + if keys_changed: + self.log.info('API key set changed - Signing out') + yt_login.process(yt_login.SIGN_OUT, self, context) - api_last_origin = access_manager.get_last_origin() if api_last_origin != origin: - context.log_debug('API key origin changed: |{old}| to |{new}|' - .format(old=api_last_origin, new=origin)) + self.log.info(('API key origin changed - Resetting client', + 'Previous: {old!r}', + 'Current: {new!r}'), + old=api_last_origin, + new=origin) access_manager.set_last_origin(origin) - self.reset_client() + client.initialised = False + + if not client.initialised: + self.reset_client( + context=context, + language=settings.get_language(), + region=settings.get_region(), + items_per_page=settings.items_per_page(), + configs=configs, + ) - access_tokens = access_manager.get_access_token(dev_id) - if access_manager.is_access_token_expired(dev_id): - # reset access_token - access_tokens = [None, None] - access_manager.update_access_token(dev_id, access_token='') - elif self._client: - return self._client - - if not dev_id: - context.log_debug('Selecting YouTube config "{0}"' - .format(configs['main']['system'])) - elif dev_keys: - context.log_debug('Selecting YouTube developer config "{0}"' - .format(dev_id)) - configs['main'] = dev_keys + ( + access_tokens, + num_access_tokens, + _, + ) = access_manager.get_access_tokens(dev_id) + ( + refresh_tokens, + num_refresh_tokens, + ) = access_manager.get_refresh_tokens(dev_id) + + if num_access_tokens and client.logged_in: + self.log.debug('User is %s logged in', client.logged_in) + return client + if num_access_tokens or num_refresh_tokens: + self.log.debug(('# Access tokens: %d', + '# Refresh tokens: %d'), + num_access_tokens, + num_refresh_tokens) else: - dev_keys = configs['main'] - context.log_debug('Selecting YouTube config "{0}"' - ' w/ developer access tokens' - .format(dev_keys['system'])) - - refresh_tokens = access_manager.get_refresh_token(dev_id) - if any(refresh_tokens): - keys_changed = access_manager.dev_keys_changed( - dev_id, dev_keys['key'], dev_keys['id'], dev_keys['secret'] - ) if dev_id else self._api_check.changed - if keys_changed: - context.log_warning('API key set changed: Resetting client' - ' and updating access token') - access_tokens = [None, None] - refresh_tokens = [None, None] - access_manager.update_access_token( - dev_id, access_token='', expiry=-1, refresh_token='' - ) - self.reset_client() - - num_access_tokens = len([1 for token in access_tokens if token]) - num_refresh_tokens = len([1 for token in refresh_tokens if token]) - context.log_debug( - 'Access token count: |{0}|, refresh token count: |{1}|' - .format(num_access_tokens, num_refresh_tokens) - ) - - settings = context.get_settings() - client = YouTube(context=context, - language=settings.get_language(), - region=settings.get_region(), - items_per_page=settings.items_per_page(), - configs=configs) + self.log.debug('User is not logged in') + access_manager.update_access_token(dev_id, access_token='') + return client + # create new access tokens with client: - # create new access tokens + function_cache = context.get_function_cache() + if not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh or context.refresh_requested(), + ): + num_refresh_tokens = 0 if num_refresh_tokens and num_access_tokens != num_refresh_tokens: - access_tokens = [None, None] + access_tokens = [None, None, None, None] token_expiry = 0 try: for token_type, value in enumerate(refresh_tokens): @@ -295,31 +310,21 @@ def get_client(self, context): refresh_token = None access_manager.update_access_token( dev_id, + access_token='', refresh_token=refresh_token, ) - - num_access_tokens = len([1 for token in access_tokens if token]) - - if num_access_tokens and access_tokens[1]: - self._logged_in = True - context.log_debug('User is logged in') - client.set_access_token( - personal=access_tokens[1], - tv=access_tokens[0], - ) - else: - self._logged_in = False - context.log_debug('User is not logged in') - client.set_access_token(personal='', tv='') - - self._client = client - return self._client + client.set_access_token(access_tokens) + return client def get_resource_manager(self, context, progress_dialog=None): resource_manager = self._resource_manager - if not resource_manager or resource_manager.context_changed(context): + client = self.get_client(context) + if not resource_manager or resource_manager.context_changed( + context, client + ): new_resource_manager = ResourceManager(proxy(self), context, + client, progress_dialog) if not resource_manager: self._resource_manager = new_resource_manager @@ -347,15 +352,16 @@ def on_uri2addon(provider, context, uri=None, **_kwargs): if not resolved_url: return False, None - url_converter = UrlToItemConverter(flatten=True) - url_converter.add_url(resolved_url, context) - items = url_converter.get_items(provider=provider, - context=context, - skip_title=skip_title) + url_to_item_converter = UrlToItemConverter(flatten=True) + url_to_item_converter.process_url(resolved_url, context) + items = url_to_item_converter.get_items(provider=provider, + context=context, + skip_title=skip_title) if items: - return (items if listing else items[0]), None - - return False, None + if listing: + return items, None + return items[0], {provider.FORCE_RESOLVE: True} + return [], None @AbstractProvider.register_path( r'^/channel/(?P[^/]+)' @@ -369,23 +375,18 @@ def on_channel_playlists(provider, context, re_match): * CHANNEL_ID: YouTube Channel ID """ - channel_id = re_match.group('channel_id') - + channel_id = re_match.group(CHANNEL_ID) + new_params = { + CHANNEL_ID: channel_id, + } + context.parse_params(new_params) params = context.get_params() - page_token = params.get('page_token', '') - incognito = params.get('incognito') - addon_id = params.get('addon_id') - - new_params = {} - if incognito: - new_params['incognito'] = incognito - if addon_id: - new_params['addon_id'] = addon_id resource_manager = provider.get_resource_manager(context) playlists = resource_manager.get_related_playlists(channel_id) uploads = playlists.get('uploads') if playlists else None - if uploads and uploads.startswith('UU'): + if (params.get(PAGE, 1) == 1 and not params.get(HIDE_FOLDERS) + and uploads and uploads.startswith('UU')): result = [ { 'kind': 'youtube#playlist', @@ -399,9 +400,12 @@ def on_channel_playlists(provider, context, re_match): }, '_available': True, '_partial': True, + '_params': { + 'special_sort': 'top', + }, }, { - 'kind': 'youtube#playlist', + 'kind': 'youtube#playlistShortsFolder', 'id': uploads.replace('UU', 'UUSH', 1), 'snippet': { 'channelId': channel_id, @@ -411,9 +415,12 @@ def on_channel_playlists(provider, context, re_match): }}, }, '_partial': True, - }, + '_params': { + 'special_sort': 'top', + }, + } if not params.get(HIDE_SHORTS) else None, { - 'kind': 'youtube#playlist', + 'kind': 'youtube#playlistLiveFolder', 'id': uploads.replace('UU', 'UULV', 1), 'snippet': { 'channelId': channel_id, @@ -423,12 +430,17 @@ def on_channel_playlists(provider, context, re_match): }}, }, '_partial': True, - }, + '_params': { + 'special_sort': 'top', + }, + } if not params.get(HIDE_LIVE) else None, ] else: result = False - json_data = resource_manager.get_my_playlists(channel_id, page_token) + json_data = resource_manager.get_my_playlists( + channel_id, params.get('page_token', '') + ) if not json_data: return False, None @@ -445,10 +457,14 @@ def on_channel_playlists(provider, context, re_match): } return result, options - @AbstractProvider.register_path( - r'^/channel/(?P[^/]+)' - r'/(?:live|playlist/(?PUULV[^/]+))/?$' - ) + @AbstractProvider.register_path(r''.join(( + '^', + PATHS.CHANNEL, + '/(?P<', CHANNEL_ID, '>[^/]+)', + '(?:/live|', + PATHS.PLAYLIST, + '/(?P<', PLAYLIST_ID, '>UULV[^/]+))/?$', + ))) @staticmethod def on_channel_live(provider, context, @@ -470,8 +486,8 @@ def on_channel_live(provider, resource_manager = provider.get_resource_manager(context) if re_match: - channel_id = re_match.group('channel_id') - playlist_id = re_match.group('playlist_id') + channel_id = re_match.group(CHANNEL_ID) + playlist_id = re_match.group(PLAYLIST_ID) if not playlist_id: playlists = resource_manager.get_related_playlists(channel_id) playlist_id = playlists.get('uploads') if playlists else None @@ -481,8 +497,8 @@ def on_channel_live(provider, return False, None new_params = { - 'channel_id': channel_id, - 'playlist_id': playlist_id, + CHANNEL_ID: channel_id, + PLAYLIST_ID: playlist_id, } context.parse_params(new_params) @@ -491,7 +507,7 @@ def on_channel_live(provider, if not json_data: return False, None - live_streams = provider.get_client(context).get_browse_videos( + live_streams = provider.get_client(context).get_browse_items( channel_id=channel_id, route='streams', json_path={ @@ -549,10 +565,14 @@ def on_channel_live(provider, } return result, options - @AbstractProvider.register_path( - r'^/channel/(?P[^/]+)' - r'/(?:shorts|playlist/(?PUUSH[^/]+))/?$' - ) + @AbstractProvider.register_path(r''.join(( + '^', + PATHS.CHANNEL, + '/(?P<', CHANNEL_ID, '>[^/]+)', + '(?:/shorts|', + PATHS.PLAYLIST, + '/(?P<', PLAYLIST_ID, '>UUSH[^/]+))/?$', + ))) @staticmethod def on_channel_shorts(provider, context, @@ -574,21 +594,20 @@ def on_channel_shorts(provider, resource_manager = provider.get_resource_manager(context) if re_match: - channel_id = re_match.group('channel_id') - playlist_id = re_match.group('playlist_id') + channel_id = re_match.group(CHANNEL_ID) + playlist_id = re_match.group(PLAYLIST_ID) if not playlist_id: playlists = resource_manager.get_related_playlists(channel_id) playlist_id = playlists.get('uploads') if playlists else None if playlist_id and playlist_id.startswith('UU'): playlist_id = playlist_id.replace('UU', 'UUSH', 1) - if not playlist_id: + if not channel_id or not playlist_id: return False, None new_params = { - 'playlist_id': playlist_id, + CHANNEL_ID: channel_id, + PLAYLIST_ID: playlist_id, } - if channel_id: - new_params['channel_id'] = channel_id context.parse_params(new_params) batch_id = (playlist_id, context.get_param('page_token') or 0) @@ -611,10 +630,13 @@ def on_channel_shorts(provider, } return result, options - @AbstractProvider.register_path( - r'^(?:/channel/(?P[^/]+))?' - r'/playlist/(?P[^/]+)/?$' - ) + @AbstractProvider.register_path(r''.join(( + '^(?:', + PATHS.CHANNEL, + '/(?P<', CHANNEL_ID, '>[^/]+))?', + PATHS.PLAYLIST, + '/(?P<', PLAYLIST_ID, '>[^/]+)/?$', + ))) @staticmethod def on_playlist(provider, context, re_match): """ @@ -629,13 +651,13 @@ def on_playlist(provider, context, re_match): * CHANNEL_ID: ['mine'|YouTube Channel ID] * PLAYLIST_ID: YouTube Playlist ID """ - playlist_id = re_match.group('playlist_id') + playlist_id = re_match.group(PLAYLIST_ID) new_params = { - 'playlist_id': playlist_id, + PLAYLIST_ID: playlist_id, } - channel_id = re_match.group('channel_id') + channel_id = re_match.group(CHANNEL_ID) if channel_id: - new_params['channel_id'] = channel_id + new_params[CHANNEL_ID] = channel_id context.parse_params(new_params) resource_manager = provider.get_resource_manager(context) @@ -649,7 +671,7 @@ def on_playlist(provider, context, re_match): options = { provider.CONTENT_TYPE: { 'content_type': CONTENT.VIDEO_CONTENT, - 'sub_type': None, + 'sub_type': CONTENT.PLAYLIST, 'category_label': None, }, } @@ -668,7 +690,7 @@ def on_channel(provider, context, re_match): * ID_TYPE: channel|handle|user * ID: YouTube ID """ - listitem_channel_id = context.get_listitem_property(CHANNEL_ID) + li_channel_id = context.get_ui().get_listitem_property(CHANNEL_ID) client = provider.get_client(context) create_uri = context.create_uri @@ -680,11 +702,11 @@ def on_channel(provider, context, re_match): if (command == 'channel' and identifier and identifier.lower() == 'property' - and listitem_channel_id - and listitem_channel_id.lower().startswith(('mine', 'uc'))): + and li_channel_id + and li_channel_id.lower().startswith(('mine', 'uc'))): context.execute('ActivateWindow(Videos, {channel}, return)'.format( channel=create_uri( - (PATHS.CHANNEL, listitem_channel_id,), + (PATHS.CHANNEL, li_channel_id,), ) )) @@ -719,7 +741,7 @@ def on_channel(provider, context, re_match): return False context.parse_params({ - 'channel_id': channel_id, + CHANNEL_ID: channel_id, }) resource_manager = provider.get_resource_manager(context) @@ -737,36 +759,30 @@ def on_channel(provider, context, re_match): if uploads and not uploads.startswith('UU'): uploads = None - if params.get('page', 1) == 1 and not params.get('hide_folders'): + if params.get(PAGE, 1) == 1 and not params.get(HIDE_FOLDERS): v3_response = { - 'kind': 'youtube#pluginListResponse', + 'kind': 'plugin#pluginListResponse', 'items': [ { - 'kind': 'youtube#playlistFolder', - 'id': 'playlists', - 'snippet': { - 'channelId': channel_id, + 'kind': 'plugin#playlistsFolder', + '_params': { 'title': context.localize('playlists'), - 'thumbnails': {'default': { - 'url': '{media}/playlist.png', - }}, + 'image': '{media}/playlist.png', + CHANNEL_ID: channel_id, + 'special_sort': 'top', }, - '_partial': True, - } if not params.get('hide_playlists') else None, + } if not params.get(HIDE_PLAYLISTS) else None, { - 'kind': 'youtube#searchFolder', - 'id': 'search', - 'snippet': { - 'channelId': channel_id, + 'kind': 'plugin#searchFolder', + '_params': { 'title': context.localize('search'), - 'thumbnails': {'default': { - 'url': '{media}/search.png', - }}, + 'image': '{media}/search.png', + CHANNEL_ID: channel_id, + 'special_sort': 'top', }, - '_partial': True, - } if not params.get('hide_search') else None, + } if not params.get(HIDE_SEARCH) else None, { - 'kind': 'youtube#playlist', + 'kind': 'youtube#playlistShortsFolder', 'id': uploads.replace('UU', 'UUSH', 1), 'snippet': { 'channelId': channel_id, @@ -776,9 +792,12 @@ def on_channel(provider, context, re_match): }}, }, '_partial': True, - } if uploads and not params.get('hide_shorts') else None, + '_params': { + 'special_sort': 'top', + }, + } if uploads and not params.get(HIDE_SHORTS) else None, { - 'kind': 'youtube#playlist', + 'kind': 'youtube#playlistLiveFolder', 'id': uploads.replace('UU', 'UULV', 1), 'snippet': { 'channelId': channel_id, @@ -788,7 +807,25 @@ def on_channel(provider, context, re_match): }}, }, '_partial': True, - } if uploads and not params.get('hide_live') else None, + '_params': { + 'special_sort': 'top', + }, + } if uploads and not params.get(HIDE_LIVE) else None, + { + 'kind': 'youtube#playlistMembersFolder', + 'id': uploads.replace('UU', 'UUMO', 1), + 'snippet': { + 'channelId': channel_id, + 'title': context.localize('members_only'), + 'thumbnails': {'default': { + 'url': '{media}/sign_in.png', + }}, + }, + '_partial': True, + '_params': { + 'special_sort': 'top', + }, + } if uploads and not params.get(HIDE_MEMBERS) else None, ], } result.extend(v3.response_to_items(provider, context, v3_response)) @@ -820,7 +857,7 @@ def on_channel(provider, context, re_match): return result, options context.parse_params({ - 'playlist_id': filtered_uploads or uploads, + PLAYLIST_ID: filtered_uploads or uploads, }) if not filtered_uploads: @@ -834,6 +871,10 @@ def on_channel(provider, context, re_match): result.extend(v3.response_to_items( provider, context, json_data, item_filter={ + 'live_folder': True, + 'shorts': True, + 'vod': True, + } if filtered_uploads else { 'shorts': True, 'live': False, 'upcoming_live': False, @@ -865,7 +906,7 @@ def on_my_location(provider, context, **_kwargs): result.append(search_item) # completed live events - if settings.get_bool('youtube.folder.completed.live.show', True): + if settings.get_bool(settings.SHOW_COMPlETED_LIVE, True): live_events_item = DirectoryItem( localize('live.completed'), create_uri( @@ -877,7 +918,7 @@ def on_my_location(provider, context, **_kwargs): result.append(live_events_item) # upcoming live events - if settings.get_bool('youtube.folder.upcoming.live.show', True): + if settings.get_bool(settings.SHOW_UPCOMING_LIVE, True): live_events_item = DirectoryItem( localize('live.upcoming'), create_uri( @@ -911,27 +952,13 @@ def on_users(re_match, **_kwargs): @AbstractProvider.register_path('^/sign/(?P[^/]+)/?$') @staticmethod - def on_sign(provider, context, re_match): - sign_out_confirmed = context.get_param('confirmed') - mode = re_match.group('mode') - if mode == 'in': - refresh_tokens = context.get_access_manager().get_refresh_token() - if any(refresh_tokens): - yt_login.process('out', - provider, - context, - sign_out_refresh=False) - - if (not sign_out_confirmed and mode == 'out' - and context.get_ui().on_yes_no_input( - context.localize('sign.out'), - context.localize('are_you_sure') - )): - sign_out_confirmed = True - - if mode == 'in' or (mode == 'out' and sign_out_confirmed): - yt_login.process(mode, provider, context) - return True + def on_sign_x(provider, context, re_match): + return yt_login.process( + re_match.group('mode'), + provider, + context, + client=provider.get_client(context, refresh=True), + ) def _search_channel_or_playlist(self, context, @@ -962,9 +989,9 @@ def on_search_run(self, context, query=None): if query.startswith(('https://', 'http://')): return self.on_uri2addon(provider=self, context=context, uri=query) if context.is_plugin_path(query): - return UriItem(query), { + return False, { self.CACHE_TO_DISC: False, - self.FALLBACK: False, + self.FALLBACK: query, } result = self._search_channel_or_playlist(context, query) @@ -980,7 +1007,7 @@ def on_search_run(self, context, query=None): } result = [] - channel_id = params.get('channel_id') or params.get('channelId') + channel_id = params.get(CHANNEL_ID) or params.get('channelId') event_type = params.get('event_type') or params.get('eventType') location = params.get('location') page_token = params.get('page_token') or params.get('pageToken') or '' @@ -999,8 +1026,9 @@ def on_search_run(self, context, query=None): }, } - if params.get('page', 1) == 1 and not params.get('hide_folders'): - if event_type or search_type != 'video': + if params.get(PAGE, 1) == 1 and not params.get(HIDE_FOLDERS): + if ((event_type or search_type != 'video') + and not params.get(HIDE_VIDEOS)): video_params = dict(params, search_type='video', event_type='') @@ -1013,7 +1041,10 @@ def on_search_run(self, context, query=None): ) result.append(video_item) - if not channel_id and not location and search_type != 'channel': + if (not channel_id + and not location + and search_type != 'channel' + and not params.get(HIDE_CHANNELS)): channel_params = dict(params, search_type='channel', event_type='') @@ -1026,7 +1057,9 @@ def on_search_run(self, context, query=None): ) result.append(channel_item) - if not location and search_type != 'playlist': + if (not location + and search_type != 'playlist' + and not params.get(HIDE_PLAYLISTS)): playlist_params = dict(params, search_type='playlist', event_type='') @@ -1039,7 +1072,9 @@ def on_search_run(self, context, query=None): ) result.append(playlist_item) - if not channel_id and event_type != 'live': + if (not channel_id + and event_type != 'live' + and not params.get(HIDE_LIVE)): live_params = dict(params, search_type='video', event_type='live') @@ -1052,7 +1087,9 @@ def on_search_run(self, context, query=None): ) result.append(live_item) - if event_type and event_type != 'upcoming': + if (event_type + and event_type != 'upcoming' + and not params.get(HIDE_LIVE)): upcoming_params = dict(params, search_type='video', event_type='upcoming') @@ -1065,7 +1102,9 @@ def on_search_run(self, context, query=None): ) result.append(upcoming_item) - if event_type and event_type != 'completed': + if (event_type + and event_type != 'completed' + and not params.get(HIDE_LIVE)): completed_params = dict(params, search_type='video', event_type='completed') @@ -1102,7 +1141,7 @@ def on_search_run(self, context, query=None): return False, None # Store current search query - if not params.get('incognito') and not params.get('channel_id'): + if not params.get(INCOGNITO) and not params.get(CHANNEL_ID): context.get_search_history().add_item(search_params) result.extend(v3.response_to_items( @@ -1172,18 +1211,18 @@ def on_manage_my_subscription_filter(context, re_match, **_kwargs): ]) settings.subscriptions_filter(filter_list) - ui.show_notification(context.localize( - 'my_subscriptions.filter.added' - if command == 'add' else - 'my_subscriptions.filter.removed' - )) + ui.show_notification(context.localize(('added.to.x' + if command == 'add' else + 'removed.from.x', + 'my_subscriptions.filtered'))) return True, None - @AbstractProvider.register_path( - r'^/maintenance' - r'/(?P[^/]+)' - r'/(?P[^/]+)/?$' - ) + @AbstractProvider.register_path(r''.join(( + '^', + PATHS.MAINTENANCE, + '/(?P[^/]+)', + '/(?P[^/]+)/?$', + ))) @staticmethod def on_maintenance_actions(provider, context, re_match): target = re_match.group('target') @@ -1202,23 +1241,10 @@ def on_maintenance_actions(provider, context, re_match): if target == 'access_manager' and ui.on_yes_no_input( context.get_name(), localize('reset.access_manager.check') ): - addon_id = context.get_param('addon_id', None) access_manager = context.get_access_manager() - client = provider.get_client(context) - refresh_tokens = access_manager.get_refresh_token() - success = True - if any(refresh_tokens): - for refresh_token in frozenset(refresh_tokens): - try: - if refresh_token: - client.revoke(refresh_token) - except LoginException: - success = False - provider.reset_client() - access_manager.update_access_token( - addon_id, access_token='', expiry=-1, refresh_token='', - ) - ui.refresh_container() + success, _ = yt_login.process(yt_login.SIGN_OUT, provider, context) + if success: + success = access_manager.set_defaults(reset=True) ui.show_notification(localize('succeeded' if success else 'failed')) else: success = False @@ -1227,65 +1253,12 @@ def on_maintenance_actions(provider, context, re_match): @AbstractProvider.register_path('^/api/update/?$') @staticmethod def on_api_key_update(context, **_kwargs): - localize = context.localize - settings = context.get_settings() - ui = context.get_ui() - - params = context.get_params() - api_key = params.get('api_key') - client_id = params.get('client_id') - client_secret = params.get('client_secret') - enable = params.get('enable') - - updated_list = [] - log_list = [] - - if api_key: - settings.api_key(api_key) - updated_list.append(localize('api.key')) - log_list.append('Key') - if client_id: - settings.api_id(client_id) - updated_list.append(localize('api.id')) - log_list.append('Id') - if client_secret: - settings.api_secret(client_secret) - updated_list.append(localize('api.secret')) - log_list.append('Secret') - if updated_list: - ui.show_notification(localize('updated_') % ', '.join(updated_list)) - context.log_debug('Updated API keys: %s' % ', '.join(log_list)) - - client_id = settings.api_id() - client_secret = settings.api_secret() - api_key = settings.api_key - missing_list = [] - log_list = [] - - if enable and client_id and client_secret and api_key: - ui.show_notification(localize('api.personal.enabled')) - context.log_debug('Personal API keys enabled') - elif enable: - if not api_key: - missing_list.append(localize('api.key')) - log_list.append('Key') - if not client_id: - missing_list.append(localize('api.id')) - log_list.append('Id') - if not client_secret: - missing_list.append(localize('api.secret')) - log_list.append('Secret') - ui.show_notification(localize('api.personal.failed') - % ', '.join(missing_list)) - context.log_error('Failed to enable personal API keys. Missing: %s' - % ', '.join(log_list)) + context.get_api_store().update() @staticmethod def on_playback_history(provider, context, re_match): params = context.get_params() - command = re_match.group('command') - if not command: - return False, None + command = re_match.group('command') or 'list' localize = context.localize playback_history = context.get_playback_history() @@ -1296,6 +1269,10 @@ def on_playback_history(provider, context, re_match): if not items: return True, None + context_menu = ( + menu_items.history_local_remove(context), + menu_items.history_local_clear(context), + ) v3_response = { 'kind': 'youtube#videoListResponse', 'items': [ @@ -1304,14 +1281,7 @@ def on_playback_history(provider, context, re_match): 'id': video_id, '_partial': True, '_context_menu': { - 'context_menu': ( - menu_items.history_remove( - context, video_id - ), - menu_items.history_clear( - context - ), - ), + 'context_menu': context_menu, 'position': 0, } } @@ -1329,7 +1299,7 @@ def on_playback_history(provider, context, re_match): options = { provider.CONTENT_TYPE: { 'content_type': CONTENT.VIDEO_CONTENT, - 'sub_type': 'history', + 'sub_type': CONTENT.HISTORY, 'category_label': None, }, } @@ -1343,37 +1313,33 @@ def on_playback_history(provider, context, re_match): return False, {provider.FALLBACK: False} playback_history.clear() - ui.refresh_container() - ui.show_notification( localize('completed'), time_ms=2500, audible=False, ) - return True + return True, {provider.FORCE_REFRESH: True} - video_id = params.get('video_id') + video_id = params.get(VIDEO_ID) if not video_id: - return False + return False, None if command == 'remove': video_name = params.get('item_name') or video_id video_name = to_unicode(video_name) if not ui.on_yes_no_input( localize('content.remove'), - localize('content.remove.check') % video_name, + localize('content.remove.check.x', video_name), ): return False, {provider.FALLBACK: False} playback_history.del_item(video_id) - ui.refresh_container() - ui.show_notification( - localize('removed') % video_name, + localize('removed.name.x', video_name), time_ms=2500, audible=False, ) - return True + return True, {provider.FORCE_REFRESH: True} play_data = playback_history.get_item(video_id) if play_data: @@ -1387,7 +1353,15 @@ def on_playback_history(provider, context, re_match): 'played_percent': 0 } - if command == 'mark_unwatched': + if command == 'mark_as': + if ui.get_listitem_info(PLAY_COUNT): + play_data['play_count'] = 0 + play_data['played_time'] = 0 + play_data['played_percent'] = 0 + else: + play_data['play_count'] = 1 + + elif command == 'mark_unwatched': if play_data.get('play_count', 0) > 0: play_data['play_count'] = 0 play_data['played_time'] = 0 @@ -1402,8 +1376,7 @@ def on_playback_history(provider, context, re_match): play_data['played_percent'] = 0 playback_history_method(video_id, play_data) - ui.refresh_container() - return True + return True, {provider.FORCE_REFRESH: True} @staticmethod def on_root(provider, context, re_match): @@ -1413,8 +1386,7 @@ def on_root(provider, context, re_match): settings_bool = settings.get_bool bold = context.get_ui().bold - _ = provider.get_client(context) # required for self.is_logged_in() - logged_in = provider.is_logged_in() + logged_in = provider.get_client(context).logged_in # _.get_my_playlists() result = [] @@ -1425,7 +1397,8 @@ def on_root(provider, context, re_match): } # sign in - if not logged_in and settings_bool('youtube.folder.sign.in.show', True): + if ((not logged_in or logged_in == 'partially') + and settings_bool(settings.SHOW_SIGN_IN, True)): item_label = localize('sign.in') sign_in_item = DirectoryItem( bold(item_label), @@ -1436,7 +1409,7 @@ def on_root(provider, context, re_match): ) result.append(sign_in_item) - if settings_bool('youtube.folder.my_subscriptions.show', True): + if settings_bool(settings.SHOW_MY_SUBSCRIPTIONS, True): # my subscription item_label = localize('my_subscriptions') my_subscriptions_item = DirectoryItem( @@ -1447,7 +1420,7 @@ def on_root(provider, context, re_match): ) result.append(my_subscriptions_item) - if settings_bool('youtube.folder.my_subscriptions_filtered.show'): + if settings_bool(settings.SHOW_MY_SUBSCRIPTIONS_FILTERED): # my subscriptions filtered my_subscriptions_filtered_item = DirectoryItem( localize('my_subscriptions.filtered'), @@ -1462,8 +1435,7 @@ def on_root(provider, context, re_match): local_history = settings.use_local_history() # Home / Recommendations - if (logged_in - and settings_bool('youtube.folder.recommendations.show', True)): + if logged_in and settings_bool(settings.SHOW_RECOMMENDATIONS, True): recommendations_item = DirectoryItem( localize('recommendations'), create_uri(PATHS.RECOMMENDATIONS), @@ -1472,17 +1444,17 @@ def on_root(provider, context, re_match): result.append(recommendations_item) # Related - if settings_bool('youtube.folder.related.show', True): + if settings_bool(settings.SHOW_RELATED, True): if history_id or local_history: related_item = DirectoryItem( - localize('related_videos'), + localize('video.related'), create_uri(PATHS.RELATED_VIDEOS), image='{media}/related_videos.png', ) result.append(related_item) # Trending - if settings_bool('youtube.folder.popular_right_now.show', True): + if settings_bool(settings.SHOW_TRENDING, True): trending_item = DirectoryItem( localize('trending'), create_uri(PATHS.TRENDING), @@ -1491,13 +1463,13 @@ def on_root(provider, context, re_match): result.append(trending_item) # search - if settings_bool('youtube.folder.search.show', True): + if settings_bool(settings.SHOW_SEARCH, True): search_item = SearchItem( context, ) result.append(search_item) - if settings_bool('youtube.folder.quick_search.show'): + if settings_bool(settings.SHOW_QUICK_SEARCH): quick_search_item = NewSearchItem( context, name=localize('search.quick'), @@ -1505,7 +1477,7 @@ def on_root(provider, context, re_match): ) result.append(quick_search_item) - if settings_bool('youtube.folder.quick_search_incognito.show'): + if settings_bool(settings.SHOW_INCOGNITO_SEARCH): quick_search_incognito_item = NewSearchItem( context, name=localize('search.quick.incognito'), @@ -1515,7 +1487,7 @@ def on_root(provider, context, re_match): result.append(quick_search_incognito_item) # my location - if (settings_bool('youtube.folder.my_location.show', True) + if (settings_bool(settings.SHOW_MY_LOCATION, True) and settings.get_location()): my_location_item = DirectoryItem( localize('my_location'), @@ -1525,37 +1497,43 @@ def on_root(provider, context, re_match): result.append(my_location_item) # my channel - if logged_in and settings_bool('youtube.folder.my_channel.show', True): + if logged_in and settings_bool(settings.SHOW_MY_CHANNEL, True): my_channel_item = DirectoryItem( localize('my_channel'), - create_uri((PATHS.CHANNEL, 'mine')), + create_uri(PATHS.MY_CHANNEL), image='{media}/user.png', ) result.append(my_channel_item) # watch later - if settings_bool('youtube.folder.watch_later.show', True): + 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.CHANNEL, 'mine', 'playlist', watch_later_id,), - ), + create_uri(path), image='{media}/watch_later.png', ) context_menu = [ - menu_items.play_playlist( + menu_items.playlist_play( context, watch_later_id ), - menu_items.play_playlist_recently_added( + menu_items.playlist_play_recently_added( context, watch_later_id ), - menu_items.view_playlist( + menu_items.playlist_view( context, watch_later_id ), - menu_items.shuffle_playlist( + 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) @@ -1568,11 +1546,11 @@ def on_root(provider, context, re_match): context_menu = [ menu_items.watch_later_local_clear(context), menu_items.separator(), - menu_items.play_all_from( + menu_items.folder_play( context, path=PATHS.WATCH_LATER, ), - menu_items.play_all_from( + menu_items.folder_play( context, path=PATHS.WATCH_LATER, order='shuffle', @@ -1582,39 +1560,39 @@ def on_root(provider, context, re_match): result.append(watch_later_item) # liked videos - if (logged_in - and settings_bool('youtube.folder.liked_videos.show', True)): + if logged_in and settings_bool(settings.SHOW_LIKED, True): resource_manager = provider.get_resource_manager(context) 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.CHANNEL, 'mine', 'playlist', liked_list_id,), - ), + create_uri(path), image='{media}/likes.png', ) context_menu = [ - menu_items.play_playlist( + menu_items.playlist_play( context, liked_list_id ), - menu_items.play_playlist_recently_added( + menu_items.playlist_play_recently_added( context, liked_list_id ), - menu_items.view_playlist( + menu_items.playlist_view( context, liked_list_id ), - menu_items.shuffle_playlist( + 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) # disliked videos - if (logged_in - and settings_bool('youtube.folder.disliked_videos.show', True)): + if logged_in and settings_bool(settings.SHOW_DISLIKED, True): disliked_videos_item = DirectoryItem( localize('video.disliked'), create_uri(PATHS.DISLIKED_VIDEOS), @@ -1623,28 +1601,34 @@ def on_root(provider, context, re_match): result.append(disliked_videos_item) # history - if settings_bool('youtube.folder.history.show', False): + 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.CHANNEL, 'mine', 'playlist', history_id,), - ), + create_uri(path), image='{media}/history.png', ) context_menu = [ - menu_items.play_playlist( + menu_items.playlist_play( context, history_id ), - menu_items.play_playlist_recently_added( + menu_items.playlist_play_recently_added( context, history_id ), - menu_items.view_playlist( + menu_items.playlist_view( context, history_id ), - menu_items.shuffle_playlist( + 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) @@ -1655,15 +1639,15 @@ def on_root(provider, context, re_match): image='{media}/history.png', ) context_menu = [ - menu_items.history_clear( + menu_items.history_local_clear( context ), menu_items.separator(), - menu_items.play_all_from( + menu_items.folder_play( context, path=PATHS.HISTORY, ), - menu_items.play_all_from( + menu_items.folder_play( context, path=PATHS.HISTORY, order='shuffle', @@ -1673,38 +1657,34 @@ def on_root(provider, context, re_match): result.append(watch_history_item) # (my) playlists - if logged_in and settings_bool('youtube.folder.playlists.show', True): + if logged_in and settings_bool(settings.SHOW_PLAYLISTS, True): playlists_item = DirectoryItem( localize('playlists'), - create_uri( - (PATHS.CHANNEL, 'mine', 'playlists',), - ), + create_uri(PATHS.MY_PLAYLISTS), image='{media}/playlist.png', ) result.append(playlists_item) # saved playlists - # TODO: re-enable once functionality is restored - # if logged_in and settings_bool('youtube.folder.saved.playlists.show', True): - # playlists_item = DirectoryItem( - # localize('saved.playlists'), - # create_uri(('special', 'saved_playlists')), - # image='{media}/playlist.png', - # ) - # result.append(playlists_item) + if logged_in and settings_bool(settings.SHOW_SAVED_PLAYLISTS, True): + playlists_item = DirectoryItem( + localize('saved.playlists'), + create_uri(PATHS.SAVED_PLAYLISTS), + image='{media}/playlist.png', + ) + result.append(playlists_item) # subscriptions - if (logged_in - and settings_bool('youtube.folder.subscriptions.show', True)): + if logged_in and settings_bool(settings.SHOW_SUBSCRIPTIONS, True): subscriptions_item = DirectoryItem( localize('subscriptions'), - create_uri(('subscriptions', 'list')), + create_uri((PATHS.SUBSCRIPTIONS, 'list')), image='{media}/channels.png', ) result.append(subscriptions_item) # bookmarks - if settings_bool('youtube.folder.bookmarks.show', True): + if settings_bool(settings.SHOW_BOOKMARKS, True): bookmarks_item = DirectoryItem( localize('bookmarks'), create_uri((PATHS.BOOKMARKS, 'list')), @@ -1715,11 +1695,11 @@ def on_root(provider, context, re_match): context ), menu_items.separator(), - menu_items.play_all_from( + menu_items.folder_play( context, path=PATHS.BOOKMARKS, ), - menu_items.play_all_from( + menu_items.folder_play( context, path=PATHS.BOOKMARKS, order='shuffle', @@ -1729,17 +1709,16 @@ def on_root(provider, context, re_match): result.append(bookmarks_item) # browse channels - if (logged_in - and settings_bool('youtube.folder.browse_channels.show', True)): + if logged_in and settings_bool(settings.SHOW_BROWSE_CHANNELS, True): browse_channels_item = DirectoryItem( localize('browse_channels'), - create_uri(('special', 'browse_channels')), + create_uri((PATHS.SPECIAL, 'browse_channels')), image='{media}/browse_channels.png', ) result.append(browse_channels_item) # completed live events - if settings_bool('youtube.folder.completed.live.show', True): + if settings_bool(settings.SHOW_COMPlETED_LIVE, True): live_events_item = DirectoryItem( localize('live.completed'), create_uri(PATHS.LIVE_VIDEOS_COMPLETED), @@ -1748,7 +1727,7 @@ def on_root(provider, context, re_match): result.append(live_events_item) # upcoming live events - if settings_bool('youtube.folder.upcoming.live.show', True): + if settings_bool(settings.SHOW_UPCOMING_LIVE, True): live_events_item = DirectoryItem( localize('live.upcoming'), create_uri(PATHS.LIVE_VIDEOS_UPCOMING), @@ -1757,7 +1736,7 @@ def on_root(provider, context, re_match): result.append(live_events_item) # live events - if settings_bool('youtube.folder.live.show', True): + if settings_bool(settings.SHOW_LIVE, True): live_events_item = DirectoryItem( localize('live'), create_uri(PATHS.LIVE_VIDEOS), @@ -1766,7 +1745,7 @@ def on_root(provider, context, re_match): result.append(live_events_item) # switch user - if settings_bool('youtube.folder.switch.user.show', True): + if settings_bool(settings.SHOW_SWITCH_USER, True): switch_user_item = DirectoryItem( localize('user.switch'), create_uri(('users', 'switch')), @@ -1776,7 +1755,7 @@ def on_root(provider, context, re_match): result.append(switch_user_item) # sign out - if logged_in and settings_bool('youtube.folder.sign.out.show', True): + if logged_in and settings_bool(settings.SHOW_SIGN_OUT, True): sign_out_item = DirectoryItem( localize('sign.out'), create_uri(('sign', 'out')), @@ -1785,7 +1764,7 @@ def on_root(provider, context, re_match): ) result.append(sign_out_item) - if settings_bool('youtube.folder.settings.show', True): + if settings_bool(settings.SHOW_SETUP_WIZARD, True): settings_menu_item = DirectoryItem( localize('setup_wizard'), create_uri(('config', 'setup_wizard')), @@ -1794,7 +1773,7 @@ def on_root(provider, context, re_match): ) result.append(settings_menu_item) - if settings_bool('youtube.folder.settings.advanced.show'): + if settings_bool(settings.SHOW_SETTINGS): settings_menu_item = DirectoryItem( localize('settings'), create_uri(('config', 'youtube')), @@ -1808,19 +1787,43 @@ def on_root(provider, context, re_match): @staticmethod def on_bookmarks(provider, context, re_match): params = context.get_params() - command = re_match.group('command') - if not command: - return False, None + command = re_match.group('command') or 'list' + + ui = context.get_ui() + localize = context.localize + parse_item_ids = context.parse_item_ids if command in {'list', 'play'}: bookmarks_list = context.get_bookmarks_list() items = bookmarks_list.get_items() - if not items: - return True, None + + context_menu_custom = ( + menu_items.bookmark_edit(context), + menu_items.bookmark_remove(context), + menu_items.bookmarks_clear(context), + ) + context_menu = ( + menu_items.bookmark_edit(context), + menu_items.bookmark_remove(context), + menu_items.bookmarks_clear(context), + ) v3_response = { - 'kind': 'youtube#pluginListResponse', - 'items': [] + 'kind': 'plugin#pluginListResponse', + 'items': [ + { + 'kind': 'plugin#bookmarkItem', + '_params': { + 'name': localize('bookmarks.add'), + 'uri': context.create_uri( + (PATHS.BOOKMARKS, 'add_custom',), + ), + 'action': True, + 'playable': False, + 'special_sort': 'top', + }, + }, + ], } def _update_bookmark(context, _id, old_item): @@ -1832,10 +1835,10 @@ def _update(new_item): else: return True + new_item.callback = None if new_item.available: new_item.bookmark_id = _id new_item.set_bookmark_timestamp(bookmark_timestamp) - new_item.callback = None bookmarks_list.update_item( _id, repr(new_item), @@ -1855,72 +1858,96 @@ def _update(new_item): return _update for item_id, item in items.items(): - callback = _update_bookmark(context, item_id, item) - if isinstance(item, float): - kind = 'youtube#channel' - yt_id = item_id - item_name = '' - partial_result = True - elif isinstance(item, BaseItem): - partial_result = False - - if isinstance(item, VideoItem): - kind = 'youtube#video' - yt_id = item.video_id + item_name = '' + item_uri = None + kind = None + yt_id = None + partial_result = False + can_edit = False + item_params = {} + + while not kind: + if isinstance(item, float): + kind = 'youtube#channel' + yt_id = item_id + partial_result = True + continue + + if not isinstance(item, BaseItem): + break + item_name = item.get_name() + item_uri = item.get_uri() + + if isinstance(item, BookmarkItem): + kind = 'plugin#bookmarkItem' + yt_id = False + can_edit = True + item_params = { + 'name': item_name, + 'uri': item_uri, + 'bookmark_id': item_id, + 'plot': item_uri, + 'action': item.is_action(), + 'special_sort': False, + 'date_time': item.get_date(), + 'category_label': '__inherit__', + } else: - yt_id = getattr(item, 'playlist_id', None) + if isinstance(item, VideoItem): + kind = 'youtube#video' + yt_id = item.video_id + continue + + yt_id = getattr(item, PLAYLIST_ID, None) if yt_id: kind = 'youtube#playlist' - else: + continue + + yt_id = getattr(item, CHANNEL_ID, None) + if yt_id: kind = 'youtube#channel' - yt_id = getattr(item, 'channel_id', None) - item_name = item.get_name() - else: - kind = None - yt_id = None - item_name = '' - partial_result = False - - if not yt_id: - if isinstance(item, BaseItem): - item_ids = item.parse_item_ids_from_uri() - to_delete = False - for kind in ('video', 'playlist', 'channel'): - yt_id = item_ids.get(kind + '_id') - if not yt_id: - continue - if yt_id == 'None': - to_delete = True - continue - kind = 'youtube#' + kind - partial_result = True - break - else: - if to_delete: - bookmarks_list.del_item(item_id) continue - else: - continue - item = { - 'kind': kind, - 'id': yt_id, - '_partial': partial_result, - '_context_menu': { - 'context_menu': ( - menu_items.bookmark_remove( - context, item_id, item_name - ), - menu_items.bookmarks_clear( - context + item_ids = parse_item_ids(item_uri, from_listitem=False) + for _kind in ('video', 'playlist', 'channel'): + id_type = _kind + '_id' + _yt_id = item_ids.get(id_type) + if not _yt_id or _yt_id == 'None': + continue + item_params.setdefault(id_type, _yt_id) + if kind: + continue + yt_id = _yt_id + kind = 'youtube#' + _kind + + if kind: + partial_result = True + continue + break + else: + v3_response['items'].append({ + 'kind': kind, + 'id': yt_id, + '_partial': partial_result, + '_context_menu': { + 'context_menu': ( + context_menu_custom + if can_edit else + context_menu ), - ), - 'position': 0, - }, - } - if callback: - item['_callback'] = callback - v3_response['items'].append(item) + 'position': 0, + }, + '_callback': _update_bookmark(context, item_id, item), + '_params': item_params, + }) + continue + + provider.log.warning(('Deleting unknown bookmark type', + 'ID: {item_id}', + 'Item: {item!r}'), + item_id=item_id, + item=item) + bookmarks_list.del_item(item_id) bookmarks = v3.response_to_items(provider, context, v3_response) if command == 'play': @@ -1939,68 +1966,124 @@ def _update(new_item): } return bookmarks, options - ui = context.get_ui() - localize = context.localize - if command == 'clear': - if not ui.on_yes_no_input( - context.localize('bookmarks.clear'), - localize('bookmarks.clear.check') - ): + if not ui.on_yes_no_input(localize('bookmarks.clear'), + localize('bookmarks.clear.check')): return False, {provider.FALLBACK: False} context.get_bookmarks_list().clear() - ui.refresh_container() - ui.show_notification( localize('completed'), time_ms=2500, audible=False, ) - return True + return True, {provider.FORCE_REFRESH: True} item_id = params.get('item_id') + + if command in {'add_custom', 'edit'}: + results = ui.on_keyboard_input(localize('bookmarks.edit.uri'), + params.get('uri', '')) + if not results[0]: + return False, None + item_uri = results[1] + if not item_uri: + return False, None + + if item_uri.startswith(('https://', 'http://')): + item_uri = UrlToItemConverter().process_url( + UrlResolver(context).resolve(item_uri), + context, + as_uri=True, + ) + if not item_uri or not context.is_plugin_path(item_uri): + ui.show_notification( + localize('failed'), + time_ms=2500, + audible=False, + ) + return False, None + + results = ui.on_keyboard_input(localize('bookmarks.edit.name'), + params.get('item_name', item_uri)) + if not results[0]: + return False, None + item_name = results[1] + + item_date_time = now() + item = BookmarkItem(name=item_name, + uri=item_uri, + plot=item_uri, + date_time=item_date_time, + category_label='__inherit__') + if item_id: + item.bookmark_id = item_id + context.get_bookmarks_list().update_item(item_id, repr(item)) + else: + item_id = item.generate_id( + item_name, + item_uri, + since_epoch(item_date_time), + prefix='custom', + ) + item.bookmark_id = item_id + context.get_bookmarks_list().add_item(item_id, repr(item)) + + ui.show_notification( + localize('updated.x', item_name) + if item_id else + localize('bookmark.created'), + time_ms=2500, + audible=False, + ) + return True, {provider.FORCE_REFRESH: True} + if not item_id: - return False + return False, None if command == 'add': item = params.get('item') - context.get_bookmarks_list().add_item(item_id, item) + if not item: + return False, None + context.get_bookmarks_list().add_item(item_id, item) ui.show_notification( localize('bookmark.created'), time_ms=2500, audible=False, ) - return True + return ( + True, + { + provider.FORCE_REFRESH: context.get_path().startswith( + PATHS.BOOKMARKS + ), + }, + ) if command == 'remove': bookmark_name = params.get('item_name') or localize('bookmark') bookmark_name = to_unicode(bookmark_name) if not ui.on_yes_no_input( localize('content.remove'), - localize('content.remove.check') % bookmark_name, + localize('content.remove.check.x', bookmark_name), ): return False, {provider.FALLBACK: False} context.get_bookmarks_list().del_item(item_id) - ui.refresh_container() - ui.show_notification( - localize('removed') % bookmark_name, + localize('removed.name.x', bookmark_name), time_ms=2500, audible=False, ) - return True + return True, {provider.FORCE_REFRESH: True} - return False + return False, None @staticmethod def on_watch_later(provider, context, re_match): params = context.get_params() - command = re_match.group('command') - if not command: - return False, None + command = re_match.group('command') or 'list' localize = context.localize ui = context.get_ui() @@ -2010,6 +2093,10 @@ def on_watch_later(provider, context, re_match): if not items: return True, None + context_menu = ( + menu_items.watch_later_local_remove(context), + menu_items.watch_later_local_clear(context), + ) v3_response = { 'kind': 'youtube#videoListResponse', 'items': [ @@ -2018,14 +2105,7 @@ def on_watch_later(provider, context, re_match): 'id': video_id, '_partial': True, '_context_menu': { - 'context_menu': ( - menu_items.watch_later_local_remove( - context, video_id, item.get_name() - ), - menu_items.watch_later_local_clear( - context - ), - ), + 'context_menu': context_menu, 'position': 0, } } @@ -2057,116 +2137,111 @@ def on_watch_later(provider, context, re_match): return False, {provider.FALLBACK: False} context.get_watch_later_list().clear() - ui.refresh_container() - ui.show_notification( localize('completed'), time_ms=2500, audible=False, ) - return True + return True, {provider.FORCE_REFRESH: True} - video_id = params.get('video_id') + video_id = params.get(VIDEO_ID) if not video_id: - return False + return False, None if command == 'add': item = params.get('item') - if item: - context.get_watch_later_list().add_item(video_id, item) - return True + if not item: + return False, None + + context.get_watch_later_list().add_item(video_id, item) + ui.show_notification( + localize(('added.to.x', 'watch_later')), + time_ms=2500, + audible=False, + ) + return True, None if command == 'remove': video_name = params.get('item_name') or localize('untitled') video_name = to_unicode(video_name) if not ui.on_yes_no_input( localize('content.remove'), - localize('content.remove.check') % video_name, + localize('content.remove.check.x', video_name), ): return False, {provider.FALLBACK: False} context.get_watch_later_list().del_item(video_id) - ui.refresh_container() - ui.show_notification( - localize('removed') % video_name, + localize('removed.name.x', video_name), time_ms=2500, audible=False, ) - return True + return True, {provider.FORCE_REFRESH: True} - return False + return False, None def handle_exception(self, context, exception_to_handle): if not isinstance(exception_to_handle, (InvalidGrant, LoginException)): return False ok_dialog = False - message_timeout = 5000 - message = exception_to_handle.get_message() - msg = exception_to_handle.get_message() - log_message = exception_to_handle.get_message() - - error = '' - code = '' - if isinstance(msg, dict): - if 'error_description' in msg: - message = strip_html_from_text(msg['error_description']) - log_message = strip_html_from_text(msg['error_description']) - elif 'message' in msg: - message = strip_html_from_text(msg['message']) - log_message = strip_html_from_text(msg['message']) + if isinstance(message, dict): + log_msg = message.get('error_description') or message.get('message') + if log_msg: + log_msg = strip_html_from_text(log_msg) else: - message = 'No error message' - log_message = 'No error message' - - if 'error' in msg: - error = msg['error'] - - if 'code' in msg: - code = msg['code'] - - if error and code: - title = '%s: [%s] %s' % ('LoginException', code, error) - elif error: - title = '%s: %s' % ('LoginException', error) + log_msg = 'No error message provided' + + error_type = message.get('error', 'Unknown error') + error_code = message.get('code', 'N/A') + if error_type == 'deleted_client': + notification = context.localize('key.requirement') + context.get_access_manager().update_access_token( + context.get_param('addon_id', None), + access_token='', + expiry=-1, + refresh_token='', + ) + ok_dialog = True + elif error_type == 'invalid_client': + if log_msg == 'The OAuth client was not found.': + notification = context.localize('client.id.incorrect') + elif log_msg == 'Unauthorized': + notification = context.localize('client.secret.incorrect') + else: + notification = log_msg + else: + notification = log_msg else: - title = 'LoginException' - - context.log_error('%s: %s' % (title, log_message)) - - if error == 'deleted_client': - message = context.localize('key.requirement') - context.get_access_manager().update_access_token( - context.get_param('addon_id', None), - access_token='', - expiry=-1, - refresh_token='', - ) - ok_dialog = True - - if error == 'invalid_client': - if message == 'The OAuth client was not found.': - message = context.localize('client.id.incorrect') - message_timeout = 7000 - elif message == 'Unauthorized': - message = context.localize('client.secret.incorrect') - message_timeout = 7000 - + notification = log_msg = message + error_type = 'Unknown error' + error_code = 'N/A' + + self.log.error(('Error - {error_type} (code: {error_code})', + 'Message: {message}', + 'Exception: {exc!r}'), + error_type=error_type, + error_code=error_code, + message=log_msg, + exc=exception_to_handle) + + title = '{name}: {message} - {error_type} (code: {error_code})'.format( + name=context.get_name(), + message=exception_to_handle.get_message(), + error_type=error_type, + error_code=error_code, + ) if ok_dialog: - context.get_ui().on_ok(title, message) + context.get_ui().on_ok(title, notification) else: - context.get_ui().show_notification(message, - title, - time_ms=message_timeout) + context.get_ui().show_notification(notification, title) return True def tear_down(self): attrs = ( '_resource_manager', '_client', - '_api_check', ) for attr in attrs: try: diff --git a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/youtube_exceptions.py b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/youtube_exceptions.py index 7611878962..3401be683c 100644 --- a/plugin.video.youtube/resources/lib/youtube_plugin/youtube/youtube_exceptions.py +++ b/plugin.video.youtube/resources/lib/youtube_plugin/youtube/youtube_exceptions.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. diff --git a/plugin.video.youtube/resources/lib/youtube_registration.py b/plugin.video.youtube/resources/lib/youtube_registration.py index f43ecfe3ef..e2b8e48474 100644 --- a/plugin.video.youtube/resources/lib/youtube_registration.py +++ b/plugin.video.youtube/resources/lib/youtube_registration.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2018-2018 plugin.video.youtube + Copyright (C) 2018-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -9,11 +9,9 @@ from __future__ import absolute_import, division, unicode_literals -from base64 import b64encode - +from youtube_plugin.kodion import logging from youtube_plugin.kodion.constants import ADDON_ID from youtube_plugin.kodion.context import XbmcContext -from youtube_plugin.kodion.json_store import APIKeyStore def register_api_keys(addon_id, api_key, client_id, client_secret): @@ -44,38 +42,20 @@ def register_api_keys(addon_id, api_key, client_id, client_secret): :param client_secret: YouTube Data v3 Client secret """ - context = XbmcContext() - if not addon_id or addon_id == ADDON_ID: - context.log_error('Register API Keys: |%s| Invalid addon_id' % addon_id) + logging.error_trace('Invalid addon_id: %r', addon_id) return - api_jstore = APIKeyStore() - json_api = api_jstore.get_data() + context = XbmcContext() access_manager = context.get_access_manager() - - jkeys = json_api['keys']['developer'].get(addon_id, {}) - - api_key = b64encode(bytes(api_key, 'utf-8')).decode('ascii') - client_id = b64encode(bytes(client_id, 'utf-8')).decode('ascii') - client_secret = b64encode(bytes(client_secret, 'utf-8')).decode('ascii') - - api_keys = { - 'origin': addon_id, 'main': { - 'system': 'JSONStore', 'key': api_key, 'id': client_id, 'secret': client_secret - } - } - - if jkeys and jkeys == api_keys: - context.log_debug('Register API Keys: |%s| No update required' % addon_id) + if access_manager.add_new_developer(addon_id): + logging.debug('Creating developer user: %r', addon_id) + + api_store = context.get_api_store() + if api_store.update_developer_config( + addon_id, api_key, client_id, client_secret + ): + logging.debug('Keys registered: %r', addon_id) else: - json_api['keys']['developer'][addon_id] = api_keys - api_jstore.save(json_api) - context.log_debug('Register API Keys: |%s| Keys registered' % addon_id) - - developers = access_manager.get_developers() - if not developers.get(addon_id, None): - developers[addon_id] = access_manager.get_new_developer() - access_manager.set_developers(developers) - context.log_debug('Creating developer user: |%s|' % addon_id) + logging.debug('No update performed: %r', addon_id) diff --git a/plugin.video.youtube/resources/lib/youtube_requests.py b/plugin.video.youtube/resources/lib/youtube_requests.py index bf9aa8f616..1456ac7395 100644 --- a/plugin.video.youtube/resources/lib/youtube_requests.py +++ b/plugin.video.youtube/resources/lib/youtube_requests.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2017-2019 plugin.video.youtube + Copyright (C) 2017-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -477,7 +477,7 @@ def get_live(channel_id=None, user=None, url=None, addon_id=None): for pattern in patterns: match = re.search(pattern, url) if match: - matched_id = match.group('channel_id') + matched_id = match.group(CHANNEL_ID) matched_type = match.group('type') break diff --git a/plugin.video.youtube/resources/lib/youtube_resolver.py b/plugin.video.youtube/resources/lib/youtube_resolver.py index 4d936fd59e..baa8b27775 100644 --- a/plugin.video.youtube/resources/lib/youtube_resolver.py +++ b/plugin.video.youtube/resources/lib/youtube_resolver.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2017-2019 plugin.video.youtube + Copyright (C) 2017-2025 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -53,7 +53,7 @@ def resolve(video_id, sort=True, addon_id=None): break if matched_id: - streams, _ = client.get_streams(context=context, video_id=matched_id) + streams, _ = client.load_stream_info(video_id=matched_id) if sort and streams: streams = sorted(streams, key=lambda x: x.get('sort', (0, 0))) diff --git a/plugin.video.youtube/resources/settings.xml b/plugin.video.youtube/resources/settings.xml index dc793d9695..4b05cb9129 100644 --- a/plugin.video.youtube/resources/settings.xml +++ b/plugin.video.youtube/resources/settings.xml @@ -138,9 +138,11 @@ - no HFR at max quality - no fractional framerate hinting - no framerate hinting + - spatial audio - alternate sorting of unselected codecs + - ttml subtitles --> - avc1,vp9,av01,hdr,hfr,vorbis,mp4a,ssa,ac-3,ec-3,dts,filter + avc1,vp9,av01,hdr,hfr,3d,vr,prefer_dub,prefer_auto_dub,vorbis,mp4a,ssa,ac-3,ec-3,dts,vtt,filter @@ -151,6 +153,10 @@ + + + + @@ -158,6 +164,9 @@ + + + @@ -234,6 +243,7 @@ false + 21436 @@ -248,6 +258,11 @@ + + + + + , @@ -503,7 +518,6 @@ 0 false - false @@ -690,7 +704,7 @@ 0 - 3 + 2 @@ -736,7 +750,7 @@ 0 - 20 + 50 5 1 @@ -744,6 +758,7 @@ false + 37122 @@ -756,6 +771,7 @@ false + 21436 @@ -780,6 +796,7 @@ false + 14045 @@ -860,9 +877,12 @@ 1 - - - + + + + + + @@ -952,6 +972,7 @@ false + 14047 @@ -997,6 +1018,19 @@ 14045 + + 0 + 20 + + 5 + 1 + 100 + + + false + 37122 + + 0 1 @@ -1082,14 +1116,14 @@ - + 0 127.0.0.1 - 30643 + 14068 - + 0 true @@ -1099,7 +1133,15 @@ true - + + 0 + + true + + RunScript($ID,config/show_client_ip) + + + 0 50152 @@ -1107,7 +1149,7 @@ 65535 - 730 + 1018 @@ -1120,14 +1162,6 @@ 30629 - - 0 - - true - - RunScript($ID,config/show_client_ip) - - 0 true @@ -1139,19 +1173,33 @@ - - + + 0 0 - + + + + 0 + 0 + + 0 + 1 + 60 + + + False + 14045 + + @@ -1172,6 +1220,14 @@ RunScript($ID,maintenance/clear?target=data_cache) + + 0 + + true + + RunScript($ID,maintenance/clear?target=requests_cache) + + 0 @@ -1230,6 +1286,14 @@ RunScript($ID,maintenance/delete?target=data_cache) + + 0 + + true + + RunScript($ID,maintenance/delete?target=requests_cache) + + 0