kwaschny / unwanted-twitch

Hide unwanted streams, games, categories and channels on: twitch.tv
https://kwaschny.net
MIT License
104 stars 13 forks source link

Add blacklisted categories to Twitch's "Not Interested" list #75

Closed kwaschny closed 2 years ago

kwaschny commented 2 years ago

Originally posted by @TomasHubelbauer in https://github.com/kwaschny/unwanted-twitch/issues/55#issuecomment-974865100

Hey, I hide categories of all games (I watch Twitch for non-gaming streams) and tags for all languages I do not understand. With such a large number of ignored entries, the extension blocks the browser tab for several seconds at a time whenever a new page of the infinite scroll functionality is fetched.

I am not sure if this is because of the time it takes to filter the responses or because some responses are ignored wholesale and there's multiple "pages" of infinite scroll that are fetched and dropped.

In any case, if there was a way to send the Not Interested signal to Twitch as games, tags and categories are blocked, and the Twitch recommendations were tuned as a result, the amount of data to filter and the amount of dropped pages would be much lower and the performance better.

I checked the network activity and it seems as though Twitch uses GraphQL and whenever the drop-down Not Interested item is clicked, a GQL endpoint is hit:

image

I am aware my use case is rather niche, however, I still hope you might reconsider attempting to simulate the Not Interested drop-down item click as it could improve the performance of the extension for everyone with moderately large block-lists.

TomasHubelbauer commented 2 years ago

Hey, last time I did not realize I do not need to muck around with constructing the GraphQL request, it is possible to just find the overflow menu button relative to the category image, click it to make the overflow menu mount and in it find and click the "Not Interested" button. It is much simpler, check it out:

// Find all poster images on the https://www.twitch.tv/directory page
[...document.querySelectorAll('.tw-image[src*="ttv-boxart"]')]
  // Extract ID, name, the `img` element and the "Not Interested" `button` element
  .map(img => ({
    // Convert the ID to a number for comparisons
    id: +img.src.match(/\d+/)[0],
    // Extract the name from the `alt` tag (least likely to change format)
    name: img.alt.slice(0, -' cover image'.length),
    // Keep the `img` element reference to be able to hover over it and debug
    img,
    // Find the overflow menu button (it is the only button in the query)
    button: img.closest('a').parentNode.querySelector('button'),
  }))
  // Check all the results to drop unexpected data rather than cause damange
  .filter(category => category.id && category.name && category.img && category.button)
  // Drop categories I do not wish to remove:
  // Just Chatting: 509658, Music: 26936, Art: 509660, Chess: 743,
  // Retro Gaming: 27284, Makers & Crafting: 509673,
  // Software and Game Development: 1469308723, Science & Technology: 509670
  .filter(category =>  ![509658, 26936, 509660, 743, 27284, 509673, 1469308723, 509670].includes(category.id))
  // Limit to a handful for a test - change or re-run the script at your leisure
  .slice(0, 5)
  // Open the overflow menu and click the "Not Interested" button for each item
  .forEach(category => {
    // Open the overflow menu to make the "Not Interested" button mount in DOM
    category.button.click();
    // Find the "Not Interested" `button` through the shared parent of the elements
    const button = category.img.closest('a').parentNode.querySelector('button[data-a-target="rec-feedback-card-not-interested"]');
    if (!button) {
      console.log(category.id, category.name, 'Overflow button click did not reveal the "Not Interested" button');
      return;
    }

    button.click();
    console.log(category.id, category.name, '"Not Interested" button clicked');
  });
kwaschny commented 2 years ago

Hey, thanks for bringing this up. Always nice to see other people trying to find (better) ways to block/hide unwanted content.

the extension blocks the browser tab for several seconds at a time whenever a new page of the infinite scroll functionality is fetched.

FYI: That's Twitch's infinite scroll behavior. The more pages you load (i.e. the bigger the DOM becomes), the slower everything loads. Mass blocking may trigger multiple of these infinite scrolls, making JS block the UI for a noticeable time. The poor performance is on Twitch's side and can also be observed without using any extension. UTTV just happens to highlight this (and used to cause clientside bot checks with large blacklists due to the unnatural small intervals between fetching more pages).

Now regarding "Not Interested": If we start telling Twitch about blocks, we also need to tell Twitch about unblocks. That requires a change how the blacklist entries are stored, since we need to remember Twitch's IDs then. I also checked if this applies to channels and it does! Unfortunately we need channels IDs of the currently logged in user and the channel to add/remove from the "Not Interested" list. So at some point we need to actual query (and cache) data from the Twitch API, which requires OAuth.

Changing the storage entries and adjusting the fragmentation algorithm is only worth touching (remember: backward compatibility) if we can actually store categories AND channels IMO. And even then, I don't plan on going the "login to allow this extension to change something in your name" route, especially because the "Not Interested" list is not part of the official API (at least not yet?). The param is prefixed with v0, that figures. 😄 Relying on the ID in an external resource such as the category image is nice and all, but it's "risky" considering that the whole change is based on the assumption that this won't change anytime soon.

And as you already stated: maintaining the "Not Interested" list currently has no impact on the actual directory views. It is really just useful for recommended content.

tl;dr: I don't want to start communicating with Twitch in the name of the user as long as it doesn't grant "life-changing benefits".

TomasHubelbauer commented 2 years ago

Hey Alex, thanks for the response. I see your point and I agree. My request here is pretty niche and it looks like it would be a big change for a sake of questionable benefit especially because mass-marking-stuff-as-not-interested is super easy with the above snippet so if anyone else interested in this comes along, they can just open the DevTools and use the snippet themselves - it is not as comfortable but not a big deal either. Thanks again!

Idontneedaname commented 1 year ago

Hey, last time I did not realize I do not need to muck around with constructing the GraphQL request, it is possible to just find the overflow menu button relative to the category image, click it to make the overflow menu mount and in it find and click the "Not Interested" button. It is much simpler, check it out:

// Find all poster images on the https://www.twitch.tv/directory page
[...document.querySelectorAll('.tw-image[src*="ttv-boxart"]')]
  // Extract ID, name, the `img` element and the "Not Interested" `button` element
  .map(img => ({
    // Convert the ID to a number for comparisons
    id: +img.src.match(/\d+/)[0],
    // Extract the name from the `alt` tag (least likely to change format)
    name: img.alt.slice(0, -' cover image'.length),
    // Keep the `img` element reference to be able to hover over it and debug
    img,
    // Find the overflow menu button (it is the only button in the query)
    button: img.closest('a').parentNode.querySelector('button'),
  }))
  // Check all the results to drop unexpected data rather than cause damange
  .filter(category => category.id && category.name && category.img && category.button)
  // Drop categories I do not wish to remove:
  // Just Chatting: 509658, Music: 26936, Art: 509660, Chess: 743,
  // Retro Gaming: 27284, Makers & Crafting: 509673,
  // Software and Game Development: 1469308723, Science & Technology: 509670
  .filter(category =>  ![509658, 26936, 509660, 743, 27284, 509673, 1469308723, 509670].includes(category.id))
  // Limit to a handful for a test - change or re-run the script at your leisure
  .slice(0, 5)
  // Open the overflow menu and click the "Not Interested" button for each item
  .forEach(category => {
    // Open the overflow menu to make the "Not Interested" button mount in DOM
    category.button.click();
    // Find the "Not Interested" `button` through the shared parent of the elements
    const button = category.img.closest('a').parentNode.querySelector('button[data-a-target="rec-feedback-card-not-interested"]');
    if (!button) {
      console.log(category.id, category.name, 'Overflow button click did not reveal the "Not Interested" button');
      return;
    }

    button.click();
    console.log(category.id, category.name, '"Not Interested" button clicked');
  });

I'd like to use this, but I have no clue how to 'insert' this code into the extension, nor how to make it react to the user choices rather than fixed categories. Would you kindly point me to in the right direction for that?

TomasHubelbauer commented 1 year ago

@Idontneedaname This code is supposed to be run using the web browser developer tools. If you are not a developer, you might still be able to use it, but it is not going to be user-friendly and will require some effort.

Note that this script works as a whitelist, you tell it what categories to keep, not what categories to bulk remove! If you need the latter, I can't help you unfortunately.

The steps to do it would be to:

That should be it. It will do 5 categories at first as a test. Visually check the behavior to make sure it is good. If it looks good, press the up arrow in the Console to recall the script text again, remove this line: .slice(0, 5) and run it again. You might need to manually scroll, let Twitch load more categories and keep re-running the script. The script can't scroll on its own.

This is all assuming Twitch hasn't changed the structure of their page. I've not used this for a while, so if they did, I'd need to fix the script first and I am not sure I'll get to that if it is needed.

Idontneedaname commented 1 year ago

Hi, thanks for the reply! Alas, I couldn't get it to work. I'll just keep blocking the trash manually, it's a somewhat relaxing thing to do anyway.