diff --git a/addon.xml b/addon.xml index 19205741b..4fe7c8693 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 4e2799ca1..a46721359 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +## v7.0.9.2 +### Fixed +- Fix various Kodi 18 listitem setInfo compatibility issues +- Additional fixes and compatibility shims for playing AudioItems #873 +- Fix early thread loop termination in My Subscriptions #888 + ## v7.0.9.1 ### Fixed - Add playlist_type_hint property to workaround issues with CPlayListPlayer::Play #873 diff --git a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py index 9e62f6373..8e702ce5e 100644 --- a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py +++ b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py @@ -137,9 +137,9 @@ def to_str(value): # Kodi v20+ if hasattr(xbmcgui.ListItem, 'setDateTime'): - def datetime_infolabel(datetime_obj): + def datetime_infolabel(datetime_obj, *_args, **_kwargs): return datetime_obj.replace(microsecond=0, tzinfo=None).isoformat() # Compatibility shims for Kodi v18 and v19 else: - def datetime_infolabel(datetime_obj): - return datetime_obj.strftime('%d.%m.%Y') + def datetime_infolabel(datetime_obj, str_format='%Y-%m-%d %H:%M:%S'): + return datetime_obj.strftime(str_format) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 3e44fc11a..e286e3cce 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -141,7 +141,7 @@ def set_date_from_datetime(self, date_time): def get_date(self, as_text=False, short=False, as_info_label=False): if self._date: if as_info_label: - return datetime_infolabel(self._date) + return datetime_infolabel(self._date, '%d.%m.%Y') if short: return self._date.date().strftime('%x') if as_text: diff --git a/resources/lib/youtube_plugin/kodion/items/media_item.py b/resources/lib/youtube_plugin/kodion/items/media_item.py index ac0efeedd..4118e59e1 100644 --- a/resources/lib/youtube_plugin/kodion/items/media_item.py +++ b/resources/lib/youtube_plugin/kodion/items/media_item.py @@ -220,7 +220,7 @@ def set_mediatype(self, mediatype): self._mediatype = self._DEFAULT_MEDIATYPE def get_mediatype(self): - return self._mediatype + return self._mediatype or self._DEFAULT_MEDIATYPE def set_plot(self, plot): try: @@ -381,7 +381,7 @@ def playlist_item_id(self, value): class AudioItem(MediaItem): - _ALLOWABLE_MEDIATYPES = {'song', 'album', 'artist'} + _ALLOWABLE_MEDIATYPES = {CONTENT.AUDIO_TYPE, 'song', 'album', 'artist'} _DEFAULT_MEDIATYPE = CONTENT.AUDIO_TYPE def __init__(self, name, uri, image='DefaultAudio.png', fanart=None): @@ -396,7 +396,7 @@ def get_album_name(self): class VideoItem(MediaItem): - _ALLOWABLE_MEDIATYPES = {'video', + _ALLOWABLE_MEDIATYPES = {CONTENT.VIDEO_TYPE, 'movie', 'tvshow', 'season', 'episode', 'musicvideo'} diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index 48f016716..f7396f929 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -113,7 +113,7 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): else: return - value = item.get_artists_string() + value = item.get_artists() if value is not None: info_labels['artist'] = value @@ -321,12 +321,20 @@ 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 resume_time and duration: - info_tag.setResumePoint(resume_time, float(duration)) - elif resume_time: - info_tag.setResumePoint(resume_time) - if info_type == 'video' and duration: - info_tag.addVideoStream(xbmc.VideoStreamDetail(duration=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: + 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: + properties['ResumeTime'] = str(resume_time) + if duration: + properties['TotalTime'] = str(duration) # duration: int # As seconds @@ -346,7 +354,10 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): value = item.get_play_count() if value is not None: if set_play_count: - info_tag.setPlaycount(value) + if info_type == 'video': + info_tag.setPlaycount(value) + elif info_type == 'music': + info_tag.setPlayCount(value) properties[PLAY_COUNT] = value # count: int diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index 4c060c4d6..16c4963b5 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -41,6 +41,7 @@ def cert_verify(self, conn, url, verify, cert): self._ssl_context.check_hostname = bool(verify) return super(SSLHTTPAdapter, self).cert_verify(conn, url, verify, cert) + class BaseRequestsClass(object): _session = Session() _session.mount('https://', SSLHTTPAdapter( @@ -50,7 +51,6 @@ class BaseRequestsClass(object): total=3, backoff_factor=0.1, status_forcelist={500, 502, 503, 504}, - allowed_methods=None, ) )) atexit.register(_session.close) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 542f4f12e..a004e15b4 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1636,7 +1636,6 @@ def _threaded_fetch(kwargs, input_wait.release() if kwargs: continue - complete = True break else: complete = True @@ -1644,7 +1643,7 @@ def _threaded_fetch(kwargs, try: success, complete = worker(output, **_kwargs) - except Exception as exc: + except Exception: msg = 'get_my_subscriptions._threaded_fetch - {exc}' self._context.log_error(msg.format(exc=format_exc())) continue @@ -1654,14 +1653,13 @@ def _threaded_fetch(kwargs, else: threads['balance'].clear() - current_thread = threading.current_thread() - threads['available'].release() + threads['counter'].release() if complete: - threads['pool_counts'][pool_id] = None + threads['counts'][pool_id] = None else: - threads['pool_counts'][pool_id] -= 1 - threads['pool_counts']['all'] -= 1 - threads['current'].discard(current_thread) + threads['counts'][pool_id] -= 1 + threads['counts']['all'] -= 1 + threads['current'].discard(threading.current_thread()) threads['loop'].set() try: @@ -1669,16 +1667,21 @@ def _threaded_fetch(kwargs, except NotImplementedError: num_cores = 1 max_threads = min(32, 2 * (num_cores + 4)) + counts = { + 'all': 0, + } + current_threads = set() + counter = threading.Semaphore(max_threads) + balance_enable = threading.Event() + loop_enable = threading.Event() threads = { - 'max': max_threads, - 'available': threading.Semaphore(max_threads), - 'current': set(), - 'pool_counts': { - 'all': 0, - }, - 'balance': threading.Event(), - 'loop': threading.Event(), + 'balance': balance_enable, + 'loop': loop_enable, + 'counter': counter, + 'counts': counts, + 'current': current_threads, } + payloads = {} if logged_in: payloads[1] = { @@ -1688,6 +1691,7 @@ def _threaded_fetch(kwargs, 'threads': threads, 'limit': 1, 'input_wait': None, + 'input_wait_for': None, } payloads.update({ 2: { @@ -1697,6 +1701,7 @@ def _threaded_fetch(kwargs, 'threads': threads, 'limit': 1, 'input_wait': threading.Lock(), + 'input_wait_for': 1, }, 3: { 'worker': _get_feed, @@ -1705,18 +1710,19 @@ def _threaded_fetch(kwargs, 'threads': threads, 'limit': None, 'input_wait': threading.Lock(), + 'input_wait_for': 2, }, }) completed = [] iterator = iter(payloads) - threads['loop'].set() - while threads['loop'].wait(): + loop_enable.set() + while loop_enable.wait(): try: pool_id = next(iterator) except StopIteration: - threads['loop'].clear() - if not threads['current']: + loop_enable.clear() + if not current_threads: break for pool_id in completed: del payloads[pool_id] @@ -1726,7 +1732,7 @@ def _threaded_fetch(kwargs, payload = payloads[pool_id] payload['pool_id'] = pool_id - current_num = threads['pool_counts'].setdefault(pool_id, 0) + current_num = counts.setdefault(pool_id, 0) if current_num is None: completed.append(pool_id) continue @@ -1736,15 +1742,18 @@ def _threaded_fetch(kwargs, if input_wait and input_wait.locked(): input_wait.release() else: + input_wait_for = payload['input_wait_for'] + if not input_wait_for or input_wait_for not in payloads: + completed.append(pool_id) continue - available = threads['max'] - threads['pool_counts']['all'] + available = max_threads - counts['all'] limit = payload['limit'] if limit: if current_num >= limit: continue if available <= 0: - threads['balance'].set() + balance_enable.set() elif available <= 0: continue @@ -1753,10 +1762,10 @@ def _threaded_fetch(kwargs, kwargs=payload, ) new_thread.daemon = True - threads['current'].add(new_thread) - threads['pool_counts'][pool_id] += 1 - threads['pool_counts']['all'] += 1 - threads['available'].acquire(True) + current_threads.add(new_thread) + counts[pool_id] += 1 + counts['all'] += 1 + counter.acquire(True) new_thread.start() items = _parse_feeds(threaded_output['feeds']) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 178dcee16..b492a07de 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -15,7 +15,7 @@ from math import log10 from ...kodion.constants import CONTENT, LICENSE_TOKEN, LICENSE_URL, PATHS -from ...kodion.items import DirectoryItem, menu_items +from ...kodion.items import AudioItem, DirectoryItem, menu_items from ...kodion.utils import ( datetime_parser, friendly_number, @@ -431,7 +431,9 @@ def update_video_infos(provider, context, video_id_dict, media_item = video_id_dict[video_id] media_item.set_mediatype( - CONTENT.AUDIO_TYPE if audio_only else CONTENT.VIDEO_TYPE + CONTENT.AUDIO_TYPE + if audio_only or isinstance(media_item, AudioItem) else + CONTENT.VIDEO_TYPE ) play_data = use_play_data and yt_item.get('play_data')