Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommendations empty & rightclick=>More=>Related Videos error #508

Closed
dobbelina opened this issue Aug 14, 2023 · 22 comments · Fixed by #551
Closed

Recommendations empty & rightclick=>More=>Related Videos error #508

dobbelina opened this issue Aug 14, 2023 · 22 comments · Fixed by #551
Labels
bug Something isn't working

Comments

@dobbelina
Copy link

Context

Recommendations is empty starting yesterday.
And right-clicking a link when choosing More=>Related Videos produces an error
as in the screenshot below, debug log link below as well.
image

Please provide any relevant information about your setup

  • Add-on Version: 7.0.1
  • Kodi Version: 20.2
  • Kodi GUI Language: English
  • Operating System: Win 10 64 bit
  • Operating System Language: English

Expected Behavior

Please describe the behavior you are expecting.


Current Behavior

What is the current behavior?


Steps to Reproduce

Please provide detailed steps for reproducing the issue.


Log

https://paste.kodi.tv/yukugisoco.kodi
Please include a complete debug log.


Additional Information

Please provide any additional information that may be helpful.


@dobbelina dobbelina added the bug Something isn't working label Aug 14, 2023
@MoojMidge
Copy link
Collaborator

I think this is a Youtube API issue. The same error occurs when accessing via the Data API explorer and there are reports of this effecting official clients: https://nitter.net/TeamYouTube/status/1689826896224137216?s=20#m

@dobbelina
Copy link
Author

dobbelina commented Aug 16, 2023

When right-clicking a link and choosing More=>Related Videos the error has now changed to below image,
probably needs fixing.
And Recommendations being empty needs fixing to.
These 2 features are one of the main advantages with the addon.
90% of the channels i watch have been found using them.
image

@MoojMidge
Copy link
Collaborator

Apparently the relatedToVideoId parameter is now deprecated in the search endpoint of the V3 Data API.

Nothing to fix, except to remove the functionality.

Unfortunately there doesn't seem to be any equivalent functionality available through the API. When I have some time might try and see how the app and website fetch related videos.

@dobbelina
Copy link
Author

dobbelina commented Aug 16, 2023

Probably a good idea to leave the api all together and remake the addon as a scraper addon.
Perhaps taking help of yt-dlp

@MoojMidge
Copy link
Collaborator

yt-dlp doesn't scrape the website for anything except for some javascript. It also doesn't try to be a client so it can limit the scope of its functionality. Invidious is the only project that I can think of that may try to provide this functionality without using the API.

Regardless scraping is even more of a moving target than what the current situation is, and would mean having to implement a javascript parser/interpreter (like yt-dlp has done) but in the constrained python module environment that Kodi supports, without relying on existing projects that use C modules compiled for different platforms. It is all more work that no one has time for I think.

@dobbelina
Copy link
Author

Thanks for the thorough explanation.
I think YT really shot themself in the foot when removing this functionality from the V3 Data API.
Almost like they want people to leave their API and look for alternatives?
If you can figure out a way to get this or similar functionality back to the addon it would be much appreciated.

@malversoft
Copy link

malversoft commented Aug 17, 2023

As a side effect of this issue, the option ”Autoplay suggested videos” (automatically chain first video related to the one currently playing) does not work either.

That is a true pity for me. It was very relaxing to select a video and let Kodi play related ones for hours.

Issue confirmed with version 7.0.2-alpha7, as expected.

@dobbelina
Copy link
Author

dobbelina commented Aug 17, 2023

There is an option under "More" that's called "More links from the description" that doesn't work either, however if that one could be re-programmed to do an actual search from the link description you would have something that's a step in the right direction.

If i do a manual search from a link description,(it's title) i get some alternatives to watch.
It's not as good as the old Recommendations, but the next best thing.
Maybe it's doable?

@MoojMidge
Copy link
Collaborator

Related videos are still available via the next endpoint of the V1 API.

Can change to using that instead, but I don't have time or access to git at the moment, so will have to wait.

@AerYareli
Copy link

I hope this isn't against any rules but, is there any work around currently? anything I can do on my end to alleviate some of these issues like auto-play not working?

@MoojMidge
Copy link
Collaborator

I am not aware of any workaround, no. The plugin will need to be updated to use a different API endpoint.

@RNavega
Copy link
Contributor

RNavega commented Sep 3, 2023

I just realized I use this feature a lot heh...
I'm testing a drop-in fix, it worked fine on the few videos I tested.

To try it, you copy all of the code at the bottom of this comment, then paste it so as to overwrite the entire get_related_videos function which spans lines 612 ~ 629 of file /resources/lib/youtube_plugin/youtube/client/youtube.py.
Edit: it might be easier to just click the little "clipboard" button on the code view below to grab all of it, without missing a space character.

The modifications include a modified get_related_videos function itself, as well as some helper code that's needed.
After pasting it, restart Kodi so it reloads the add-on, then use the "Related Videos" context menu as usual.
If you're not on a desktop device but on some TV box and such, maybe it'll be easier to do the modifications to the contents of an installation ZIP file for this add-on, then install that modified ZIP on Kodi.

As always, be careful when copy-pasting unknown code that you find online.

    DESKTOP_HEADERS = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' \
                      '(KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
        'DNT': '1',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'en-US,en;q=0.5'
    }

    # Extracts the stringified JSON object found in the parameter 'text', with its
    # initial curly bracket right after the pattern 'start_pattern'.
    # Example:
    # >> text = '"this": 1, "that": {"client": "asdf"}}}},'
    # >> client.parse_json_object(text, 'that": ')
    # '{"client": "asdf"}'
    def parse_json_object(self, text, start_pattern):
        start_index = text.find(start_pattern + '{')
        if start_index == -1:
            return None
        level = 1
        start_index += len(start_pattern)
        next_index = start_index + 1
        while level > 0:
            index_open = text.find('{', next_index)
            index_close = text.find('}', next_index)
            if index_open == -1:
                if index_close == -1:
                    return None
                else:
                    level -= 1
                    next_index = index_close
            elif index_close == -1:
                return None
            elif index_open < index_close:
                level += 1
                next_index = index_open
            else:
                level -= 1
                next_index = index_close
            next_index += 1
        return text[start_index:next_index]


    def get_player_key(self, html):
        pattern = 'INNERTUBE_API_KEY":"'
        start_index = html.find(pattern)
        if start_index != -1:
            start_index += len(pattern)
            end_index = html.find('"', start_index)
            #self._context.log_debug('Player key found')
            return html[start_index:end_index]
        else:
            return None


    def get_watch_page(self, video_id):
        url = 'https://www.youtube.com/watch?v={video_id}'.format(video_id=video_id)
        result = requests.get(url, headers=self.DESKTOP_HEADERS, verify=False,
                              timeout=16, allow_redirects=True)
        if result.ok:
            return result
        else:
            return None


    def get_related_videos(self, video_id, page_token='', max_results=0):
        # prepare page token
        if not page_token:
            page_token = ''

        max_results = self._max_results if max_results <= 0 else max_results

        items_list = []

        result = self.get_watch_page(video_id)
        if result:
            html = result.text
            continuation_item = None
            # Primary "Watch Next" items, on the page.
            if 'secondaryResults' in html:
                pattern = 'secondaryResults":'
                secondary_results = self.parse_json_object(html, pattern)
                if secondary_results:
                    secondary_results = json.loads(secondary_results)
                    secondary_results = secondary_results.get('secondaryResults', {}) \
                                                         .get('results', ())
                    items_list.extend(item['compactVideoRenderer']
                                      for item in secondary_results
                                      if 'compactVideoRenderer' in item)
                    if 'continuationItemRenderer' in secondary_results[-1]:
                        continuation_item = secondary_results[-1]['continuationItemRenderer']

            # Additional "Watch Next" items, from the /next API endpoint.
            if continuation_item:
                player_key = self.get_player_key(html)
                if player_key:
                    pattern = 'INNERTUBE_CONTEXT":'
                    client_context = self.parse_json_object(html, pattern)
                    if client_context:
                        client_context = json.loads(client_context)
                        data = None
                        try:
                            client_context['clickTracking']['clickTrackingParams'] = \
                                continuation_item['continuationEndpoint']['clickTrackingParams']
                            data = {'context': client_context,
                                    'continuation': continuation_item['continuationEndpoint'] \
                                                                     ['continuationCommand']['token']}
                        except:
                            pass

                        if data:
                            url = 'https://www.youtube.com/youtubei/v1/next?key=' \
                                  '%s&prettyPrint=false' % player_key
                            result = requests.post(url, json=data, headers=self.DESKTOP_HEADERS,
                                                   timeout=16, verify=False)
                            if result.ok:
                                try:
                                    secondary_results = result.json()['onResponseReceivedEndpoints'][0] \
                                                             ['appendContinuationItemsAction'] \
                                                             ['continuationItems']
                                    items_list.extend(item['compactVideoRenderer']
                                                      for item in secondary_results
                                                      if 'compactVideoRenderer' in item)
                                    # Note that secondary_results[-1] should be a continuation
                                    # item (a dict with a 'continuationItemRenderer' key), that can
                                    # be used to load more items.
                                except:
                                    pass

        if items_list:
            # Create a dummy videoListResponse resource to be used by 'v3.py'.
            snippet_items_list = [{'kind': "youtube#video", 'id': item['videoId'],
                                   'snippet': {'title': item['title']['simpleText'],
                                               'thumbnails': {'medium': item['thumbnail']['thumbnails'][0]}}}
                                  for item in items_list]
            return {'kind': 'youtube#videoListResponse',
                    'items': snippet_items_list}
        else:
            return {}

@Asxetos21
Copy link

Asxetos21 commented Sep 4, 2023

I just realized I use this feature a lot heh...

Me too :)

Your drop-in code is working fine...
Thanks.

@dobbelina
Copy link
Author

Thanks for the drop-in code @RNavega it works nice. 👍
Could some tinkering with that code also be done to get Recommendations to work?

@RNavega
Copy link
Contributor

RNavega commented Sep 6, 2023

@dobbelina it seems there was an API route to get recommended Home videos, but it was deprecated (removed on purpose):
https://stackoverflow.com/q/43048009

I think this is Google not wanting to have APIs that empower competitor apps, because of ads on the home screen.

Edit: maybe there is a hidden API that the official YouTube app uses, but I haven't been able to use mitmproxy to inspect how it requests these things.
Other clients try to reimplement the Home screen from scratch:

Invidious:
iv-org/invidious#1099 (comment)

NewPipe (this is a cool method, based on watch history):
TeamNewPipe/NewPipe#2256 (comment)

@MoojMidge
Copy link
Collaborator

maybe there is a hidden API that the official YouTube app uses, but I haven't been able to use mitmproxy to inspect how it requests these things.

The website mainly uses the browse endpoint to populate the home page. I think it uses a request body that includes something like the following to get the different type of trending or recommended videos (haven't looked into exactly how the requests are constructed). Not sure what the apps do, but I'm guessing something very similar.

"browseEndpoint": {
    "browseId": "FEtrending"
}
"browseEndpoint": {
    "browseId": "FEwhat_to_watch"
}

Other clients try to reimplement the Home screen from scratch:

This plugin does something very similar already and it should work if the modified get_related_videos returns the same type of snippet collection as what the previous v3 API response did. My guess is the minimal constructed videoListResponse is just missing some additional items. I will probably start having a look at this in greater detail next month if someone (😉) doesn't do so before that.

def _get_recommendations_for_home(self):
# YouTube has deprecated this API, so use history and related items to form
# a recommended set. We cache aggressively because searches incur a high
# quota cost of 100 on the YouTube API.
# Note this is a first stab attempt and can be refined a lot more.
payload = {
'kind': 'youtube#activityListResponse',
'items': []
}
watch_history_id = _context.get_access_manager().get_watch_history_id()
if not watch_history_id or watch_history_id == 'HL':
return payload
cache = _context.get_data_cache()
# Do we have a cached result?
cache_home_key = 'get-activities-home'
cached = cache.get_item(cache.ONE_HOUR * 4, cache_home_key)
if cache_home_key in cached and cached[cache_home_key].get('items'):
return cached[cache_home_key]
# Fetch existing list of items, if any
items = []
cache_items_key = 'get-activities-home-items'
cached = cache.get_item(cache.ONE_WEEK * 2, cache_items_key)
if cache_items_key in cached:
items = cached[cache_items_key]
# Fetch history and recommended items. Use threads for faster execution.
def helper(video_id, responses):
_context.log_debug(
'Method get_activities: doing expensive API fetch for related'
'items for video %s' % video_id
)
di = self.get_related_videos(video_id, max_results=10)
if 'items' in di:
# Record for which video we fetched the items
for item in di['items']:
item['plugin_fetched_for'] = video_id
responses.extend(di['items'])
history = self.get_playlist_items(watch_history_id, max_results=50)
if not history.get('items'):
return payload
threads = []
candidates = []
already_fetched_for_video_ids = [item['plugin_fetched_for'] for item in items]
history_items = [item for item in history['items']
if re.match(r'(?P<video_id>[\w-]{11})',
item['snippet']['resourceId']['videoId'])]
# TODO:
# It would be nice to make this 8 user configurable
for item in history_items[:8]:
video_id = item['snippet']['resourceId']['videoId']
if video_id not in already_fetched_for_video_ids:
thread = threading.Thread(target=helper, args=(video_id, candidates))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
# Prepend new candidates to items
seen = [item['id']['videoId'] for item in items]
for candidate in candidates:
vid = candidate['id']['videoId']
if vid not in seen:
seen.append(vid)
candidate['plugin_created_date'] = datetime_parser.now().strftime('%Y-%m-%dT%H:%M:%SZ')
items.insert(0, candidate)
# Truncate items to keep it manageable, and cache
items = items[:500]
cache.set(cache_items_key, json.dumps(items))
# Build the result set
items.sort(
key=lambda a: datetime_parser.parse(a['plugin_created_date']),
reverse=True
)
sorted_items = []
counter = 0
channel_counts = {}
while items:
counter += 1
# Hard stop on iteration. Good enough for our purposes.
if counter >= 1000:
break
# Reset channel counts on a new page
if counter % 50 == 0:
channel_counts = {}
# Ensure a single channel isn't hogging the page
item = items.pop()
channel_id = item.get('snippet', {}).get('channelId')
if not channel_id:
continue
channel_counts.setdefault(channel_id, 0)
if channel_counts[channel_id] <= 3:
# Use the item
channel_counts[channel_id] = channel_counts[channel_id] + 1
item["page_number"] = counter // 50
sorted_items.append(item)
else:
# Move the item to the end of the list
items.append(item)
# Finally sort items per page by date for a better distribution
now = datetime_parser.now()
sorted_items.sort(
key=lambda a: (
a['page_number'],
datetime_parser.total_seconds(
now - datetime_parser.parse(a['snippet']['publishedAt'])
)
),
)
# Finalize result
payload['items'] = sorted_items
"""
# TODO:
# Enable pagination
payload['pageInfo'] = {
'resultsPerPage': 50,
'totalResults': len(sorted_items)
}
"""
# Update cache
cache.set(cache_home_key, json.dumps(payload))
# If there are no sorted_items we fall back to default API behaviour
return payload

@RNavega
Copy link
Contributor

RNavega commented Sep 13, 2023

I think it uses a request body that includes something like the following to get the different type of trending or recommended videos (haven't looked into exactly how the requests are constructed). Not sure what the apps do, but I'm guessing something very similar.

I noticed that "FEwhat_to_watch" value.
What I'm thinking is, if this request is done incognito, then it will recommend a generic set of videos based on the little information it has on the visitor ("they're from Canada, but we don't know who they are"), which is probably not what users will expect from a logged-in Home screen.

There must be some cookie that identifies the account of the user, so the Home screen can show relevant videos based on their browsing history and other data from them. That's why I'm thinking we cannot make a Home screen under such a logged-in state, because the account login methods used by the official app or the website --needed to get the session cookies that identify the user-- are probably behind lots of anti-bot measures like reCaptcha v3. I mean, even on desktop platforms people need to use "headless browsers" to log-in to scrape YouTube stuff, I don't think it's feasible in a cross-platform way with Kodi.

@MoojMidge
Copy link
Collaborator

@RNavega - I guess it depends on what the aim is, but I think (haven't tested it) that the Authorization header with OAuth access token can also be used with the V1 API requests.

This may not be sufficient for a multi-user setup using a common API key, or for someone logged in across multiple different clients, however the only other relatively easy (though not particularly user friendly) alternative would be similar to what youtube-dl/yt-dlp does with manually extracting cookies from a browser session to retrieve the SAPISID cookie (or equivalent) in order to calculate the SAPISIDHASH value for the Authorization header.

While related video information is quite useful, personally I don't use the existing recommended video functionality at all, and the effort required to develop a new Kodi specific but platform agnostic login, authentication and tracking mechanism for this would be too high, with too little personal benefit, for me to bother with trying to implement this. You are also probably right that it may not be feasible even if someone did try.

I think the better option is to just use the existing functionality (even if it is a little rough around the edges) as it should be able to be fixed.

One thing I did try to do was add the ability to enable remote and local history tracking independently of each other, so perhaps the way forward would be:

  1. Add option to use authenticated browse V1 API endpoint requests for recommended videos, which should be suitable for single user/client setups (may or may not work depending on how the request needs to be created, and if remote history tracking is enabled).
  2. Update get_related_video to use a next V1 API endpoint request (similar to what you have already done).
  3. If the user has created a watched video history playlist then use get_related_video for entries in this playlist (this is what the existing functionality already does, get_related_video just needs to be fixed).
  4. Otherwise if the user has not created a history playlist, but has enabled local history tracking, then use get_related_video for entries in the local history database (same as item 3, but without the need to login)
  5. Improve the _get_recommendations_for_home to use a single session connection pool, and try to improve filtering/sorting/distribution of results.

@drelephant
Copy link

drelephant commented Oct 31, 2023

I just realized I use this feature a lot heh... I'm testing a drop-in fix, it worked fine on the few videos I tested.

To try it, you copy all of the code at the bottom of this comment, then paste it so as to overwrite the entire get_related_videos function which spans lines 612 ~ 629 of file /resources/lib/youtube_plugin/youtube/client/youtube.py. Edit: it might be easier to just click the little "clipboard" button on the code view below to grab all of it, without missing a space character.

The modifications include a modified get_related_videos function itself, as well as some helper code that's needed. After pasting it, restart Kodi so it reloads the add-on, then use the "Related Videos" context menu as usual. If you're not on a desktop device but on some TV box and such, maybe it'll be easier to do the modifications to the contents of an installation ZIP file for this add-on, then install that modified ZIP on Kodi.

As always, be careful when copy-pasting unknown code that you find online.

    DESKTOP_HEADERS = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' \
                      '(KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
        'DNT': '1',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'en-US,en;q=0.5'
    }

    # Extracts the stringified JSON object found in the parameter 'text', with its
    # initial curly bracket right after the pattern 'start_pattern'.
    # Example:
    # >> text = '"this": 1, "that": {"client": "asdf"}}}},'
    # >> client.parse_json_object(text, 'that": ')
    # '{"client": "asdf"}'
    def parse_json_object(self, text, start_pattern):
        start_index = text.find(start_pattern + '{')
        if start_index == -1:
            return None
        level = 1
        start_index += len(start_pattern)
        next_index = start_index + 1
        while level > 0:
            index_open = text.find('{', next_index)
            index_close = text.find('}', next_index)
            if index_open == -1:
                if index_close == -1:
                    return None
                else:
                    level -= 1
                    next_index = index_close
            elif index_close == -1:
                return None
            elif index_open < index_close:
                level += 1
                next_index = index_open
            else:
                level -= 1
                next_index = index_close
            next_index += 1
        return text[start_index:next_index]


    def get_player_key(self, html):
        pattern = 'INNERTUBE_API_KEY":"'
        start_index = html.find(pattern)
        if start_index != -1:
            start_index += len(pattern)
            end_index = html.find('"', start_index)
            #self._context.log_debug('Player key found')
            return html[start_index:end_index]
        else:
            return None


    def get_watch_page(self, video_id):
        url = 'https://www.youtube.com/watch?v={video_id}'.format(video_id=video_id)
        result = requests.get(url, headers=self.DESKTOP_HEADERS, verify=False,
                              timeout=16, allow_redirects=True)
        if result.ok:
            return result
        else:
            return None


    def get_related_videos(self, video_id, page_token='', max_results=0):
        # prepare page token
        if not page_token:
            page_token = ''

        max_results = self._max_results if max_results <= 0 else max_results

        items_list = []

        result = self.get_watch_page(video_id)
        if result:
            html = result.text
            continuation_item = None
            # Primary "Watch Next" items, on the page.
            if 'secondaryResults' in html:
                pattern = 'secondaryResults":'
                secondary_results = self.parse_json_object(html, pattern)
                if secondary_results:
                    secondary_results = json.loads(secondary_results)
                    secondary_results = secondary_results.get('secondaryResults', {}) \
                                                         .get('results', ())
                    items_list.extend(item['compactVideoRenderer']
                                      for item in secondary_results
                                      if 'compactVideoRenderer' in item)
                    if 'continuationItemRenderer' in secondary_results[-1]:
                        continuation_item = secondary_results[-1]['continuationItemRenderer']

            # Additional "Watch Next" items, from the /next API endpoint.
            if continuation_item:
                player_key = self.get_player_key(html)
                if player_key:
                    pattern = 'INNERTUBE_CONTEXT":'
                    client_context = self.parse_json_object(html, pattern)
                    if client_context:
                        client_context = json.loads(client_context)
                        data = None
                        try:
                            client_context['clickTracking']['clickTrackingParams'] = \
                                continuation_item['continuationEndpoint']['clickTrackingParams']
                            data = {'context': client_context,
                                    'continuation': continuation_item['continuationEndpoint'] \
                                                                     ['continuationCommand']['token']}
                        except:
                            pass

                        if data:
                            url = 'https://www.youtube.com/youtubei/v1/next?key=' \
                                  '%s&prettyPrint=false' % player_key
                            result = requests.post(url, json=data, headers=self.DESKTOP_HEADERS,
                                                   timeout=16, verify=False)
                            if result.ok:
                                try:
                                    secondary_results = result.json()['onResponseReceivedEndpoints'][0] \
                                                             ['appendContinuationItemsAction'] \
                                                             ['continuationItems']
                                    items_list.extend(item['compactVideoRenderer']
                                                      for item in secondary_results
                                                      if 'compactVideoRenderer' in item)
                                    # Note that secondary_results[-1] should be a continuation
                                    # item (a dict with a 'continuationItemRenderer' key), that can
                                    # be used to load more items.
                                except:
                                    pass

        if items_list:
            # Create a dummy videoListResponse resource to be used by 'v3.py'.
            snippet_items_list = [{'kind': "youtube#video", 'id': item['videoId'],
                                   'snippet': {'title': item['title']['simpleText'],
                                               'thumbnails': {'medium': item['thumbnail']['thumbnails'][0]}}}
                                  for item in items_list]
            return {'kind': 'youtube#videoListResponse',
                    'items': snippet_items_list}
        else:
            return {}

@RNavega Any chance you can do your magic for 7.0.2.2 unofficial matrix?

I tried to add the lines myself but I end up with a python error.

I did try to get the indentation correct but apparently didn't succeed. Probably it's not good to create such Frankenstein code. I was hoping the different versions would be similar enough in structure.

2023-10-31 15:50:31.384 T:3666844   error <general>: EXCEPTION Thrown (PythonToCppException) : -->Python callback/script returned the following error<--
                                                    - NOTE: IGNORING THIS CAN LEAD TO MEMORY LEAKS!
                                                   Error Type: <class 'TabError'>
                                                   Error Contents: inconsistent use of tabs and spaces in indentation (youtube.py, line 753)
                                                   Traceback (most recent call last):
                                                     File "/home/redacted/.kodi/addons/plugin.video.youtube/resources/lib/default.py", line 12, in <module>
                                                       from youtube_plugin import youtube
                                                     File "/home/redacted/.kodi/addons/plugin.video.youtube/resources/lib/youtube_plugin/youtube/__init__.py", line 11, in <module>
                                                       from .provider import Provider
                                                     File "/home/redacted/.kodi/addons/plugin.video.youtube/resources/lib/youtube_plugin/youtube/provider.py", line 22, in <module>
                                                       from ..youtube.client import YouTube
                                                     File "/home/redacted/.kodi/addons/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/__init__.py", line 11, in <module>
                                                       from .youtube import YouTube
                                                     File "/home/redacted/.kodi/addons/plugin.video.youtube/resources/lib/youtube_plugin/youtube/client/youtube.py", line 753
                                                       def get_parent_comments(self, video_id, page_token='', max_results=0):
                                                                                                                            ^
                                                   TabError: inconsistent use of tabs and spaces in indentation
                                                   -->End of Python script error report<--
                                                   
2023-10-31 15:50:31.384 T:3666844   debug <general>: onExecutionDone(13, /home/redacted/.kodi/addons/plugin.video.youtube/resources/lib/default.py)
2023-10-31 15:50:31.397 T:3658252   debug <general>: ------ Window Init (DialogNotification.xml) ------
2023-10-31 15:50:31.425 T:3666844   debug <general>: .
2023-10-31 15:50:31.494 T:3666844   debug <general>: Python interpreter stopped
2023-10-31 15:50:31.494 T:3666844   debug <general>: Thread LanguageInvoker 140137224709888 terminating
2023-10-31 15:50:31.513 T:3666842   error <general>: GetDirectory - Error getting plugin://plugin.video.youtube/
2023-10-31 15:50:31.513 T:3666842   debug <general>: Thread waiting 140137266673408 terminating
2023-10-31 15:50:31.513 T:3658252   debug <general>: ------ Window Deinit (DialogBusy.xml) ------
2023-10-31 15:50:31.514 T:3658252   error <general>: CGUIMediaWindow::GetDirectory(plugin://plugin.video.youtube/) failed
2023-10-31 15:50:31.514 T:3658252   debug <general>: CGUIMediaWindow::GetDirectory (addons://sources/video)
2023-10-31 15:50:31.514 T:3658252   debug <general>:   ParentPath = []
2023-10-31 15:50:31.514 T:3666880   debug <general>: Thread waiting start, auto delete: false
2023-10-31 15:50:31.514 T:3666880   debug <general>: Thread waiting 140137266673408 terminating
2023-10-31 15:50:31.515 T:3666881   debug <general>: Thread BackgroundLoader start, auto delete: false
2023-10-31 15:50:31.517 T:3666881   debug <general>: Thread BackgroundLoader 140139456087808 terminating
2023-10-31 15:50:36.768 T:3658252   debug <general>: ------ Window Deinit (DialogNotification.xml) ------

@RNavega
Copy link
Contributor

RNavega commented Oct 31, 2023

For your situation alone (7.0.2.2 matrix unofficial), the modified youtube.py file is this:
https://gist.github.com/RNavega/4f11569a72eb3f1ec37d8106066c495c

Click on the "Raw" button on the top-right corner of the code viewer to download the file for replacing (or rather, right-click or long-press, then "Save Link As...").
I should add that I just did the copy-pasting, I didn't test it as I'm a caveman on Kodi 18.6.

@drelephant
Copy link

@RNavega Thanks! That worked!! Happy to get Related Videos back!

@RNavega
Copy link
Contributor

RNavega commented Nov 1, 2023

@drelephant thanks for the follow-up.
One thing I'd like to look into later is that, while this temporary solution does work, any time that the list reloads (like when playback ends and the Kodi video player disappears), the items change at random. So if there was another related video that you wanted to watch, sometimes it's gone.
I think the solution to that is to add the same caching mechanism being used by the other list screens in the plugin, so the results stay the same for the Kodi session

MoojMidge added a commit to MoojMidge/plugin.video.youtube that referenced this issue Jan 7, 2024
- Partially fix anxdpanic#508
- TODO: Improve support of InnerTube requests to handle configured
  max_result number of results
- TODO: Result is coerced into a V3 Data API result. Support native
  JSON result in ResourceManager
MoojMidge added a commit to MoojMidge/plugin.video.youtube that referenced this issue Jan 7, 2024
- Use new Youtube.get_related_videos and local history
- Fix anxdpanic#508
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
7 participants