feat(plugin): playlist and album search plugin #3202

Open
donovan-yohan wants to merge 5 commits from donovan-yohan/feat/playlist-search into master
donovan-yohan commented 2025-04-06 07:44:54 +00:00 (Migrated from github.com)

Was recently very irritated by this lack of functionality on ytmusic and decided to implement a pretty naive monkey brained solution that I think is about on par with the fact that this feature hasn't been implemented in over 7 years of complaints and requests.

Nothing too crazy going on here, it just leverages the built-in infinite scroll listener for loading playlists to hide elements that don't match the filter query, triggering a load of more elements which are in turn hidden if they don't meet the query either

for giant playlists (like mine of over 3000 imported from spotify) it can take almost a minute to resolve, but assuming you sort by newest, or some method that'll get you to what you want faster, you can simply click on what you want and ignore the rest. I'd say it's better than nothing.

Bonus points, all those tracks are loaded now, so since it just hides the dom elements, you can change your search on the already loaded items and get results much quicker.

It works using by injecting the exact same component as the search bar, so hopefully it won't conflict with anybody's existing themes (except for one override on the border-radius to keep it from showing the suggestions built-in to the regular youtube search)

Honestly, it seems that the performance of YTMusic in general is not great while content is loading, even on the original browser app. It blocks itself from loading or updating any existing images while a page search is happening. You can type but not see the results until the next page finishes loading, and also the ability to click play on items while it's searching is a little finnicky, but I did this work so I'm putting it here to get thoughts from others.

The translations files looked like they were too pretty to be edited manually, but please let me know what process I should follow to at least get the english started for these. There isn't much, just the plugin name and some placeholder strings for the search bar.

image

Since the Title, Artist, and Album are all already loaded by default, it can check against all of them for a match
image

image

Was recently very irritated by this lack of functionality on ytmusic and decided to implement a pretty naive monkey brained solution that I think is about on par with the fact that this feature hasn't been implemented in over 7 years of complaints and requests. Nothing too crazy going on here, it just leverages the built-in infinite scroll listener for loading playlists to hide elements that don't match the filter query, triggering a load of more elements which are in turn hidden if they don't meet the query either for giant playlists (like mine of over 3000 imported from spotify) it can take almost a minute to resolve, but assuming you sort by newest, or some method that'll get you to what you want faster, you can simply click on what you want and ignore the rest. I'd say it's better than nothing. Bonus points, all those tracks are loaded now, so since it just hides the dom elements, you can change your search on the already loaded items and get results much quicker. It works using by injecting the exact same component as the search bar, so hopefully it won't conflict with anybody's existing themes (except for one override on the border-radius to keep it from showing the suggestions built-in to the regular youtube search) Honestly, it seems that the performance of YTMusic in general is not great while content is loading, even on the original browser app. It blocks itself from loading or updating any existing images while a page search is happening. You can type but not see the results until the next page finishes loading, and also the ability to click play on items while it's searching is a little finnicky, but I did this work so I'm putting it here to get thoughts from others. The translations files looked like they were too pretty to be edited manually, but please let me know what process I should follow to at least get the english started for these. There isn't much, just the plugin name and some placeholder strings for the search bar. ![image](https://github.com/user-attachments/assets/b681895a-78cf-4817-bc43-16856c97bd16) Since the Title, Artist, and Album are all already loaded by default, it can check against all of them for a match ![image](https://github.com/user-attachments/assets/ae577ea6-bd6f-4cbe-8c3a-b404e6276e84) ![image](https://github.com/user-attachments/assets/96b85ef1-d895-4ce6-8d3c-8c8300287cbc)
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2025-04-06 13:54:00 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Copilot reviewed 1 out of 2 changed files in this pull request and generated no comments.

Files not reviewed (1)
  • src/plugins/playlist-search/playlist-search.css: Language not supported
Comments suppressed due to low confidence (1)

src/plugins/playlist-search/index.ts:73

  • The getElementById method should be called with the id string without the '#' prefix. Use PlaylistSearchBoxID instead of #${PlaylistSearchBoxID} to properly locate the element.
const existingContainer = document.getElementById(`#${PlaylistSearchBoxID}`);
Copilot reviewed 1 out of 2 changed files in this pull request and generated no comments. <details> <summary>Files not reviewed (1)</summary> * **src/plugins/playlist-search/playlist-search.css**: Language not supported </details> <details> <summary>Comments suppressed due to low confidence (1)</summary> **src/plugins/playlist-search/index.ts:73** * The getElementById method should be called with the id string without the '#' prefix. Use PlaylistSearchBoxID instead of `#${PlaylistSearchBoxID}` to properly locate the element. ``` const existingContainer = document.getElementById(`#${PlaylistSearchBoxID}`); ``` </details>
Laesx commented 2025-04-06 22:18:21 +00:00 (Migrated from github.com)

I tested it and seems like a really good idea, though from a quick test it seems a bit slow.

For big lists as it's loading the next entries it flashes the list a few times until it finds the song.

I think virtual lists were being implemented but until they are if you load the entire playlist it slows the app to a crawl for long lists, this is not really an issue of this plugin though but my list is only 700 entries, I can't even imagine how bad it must be with several thousand big lists hahaha.

Also maybe add the cross to delete the input and the backdrop shadow when text is inputted but the search is out of focus. Both of those are in the native youtube search bar you can just copy them from there.

I tested it and seems like a really good idea, though from a quick test it seems a bit slow. For big lists as it's loading the next entries it flashes the list a few times until it finds the song. I think virtual lists were being implemented but until they are if you load the entire playlist it slows the app to a crawl for long lists, this is not really an issue of this plugin though but my list is only 700 entries, I can't even imagine how bad it must be with several thousand big lists hahaha. Also maybe add the cross to delete the input and the backdrop shadow when text is inputted but the search is out of focus. Both of those are in the native youtube search bar you can just copy them from there.
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2025-04-16 14:14:25 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull Request Overview

This PR introduces a playlist and album search plugin for YTMusic that injects a customized search bar into playlist and album shelves to filter items based on a user’s search term.

  • Injects a custom search box into playlist or album pages.
  • Uses a debounce mechanism on user input to filter playlist items.
  • Implements a MutationObserver to automatically reapply filters when new content loads.
Files not reviewed (2)
  • src/i18n/resources/en.json: Language not supported
  • src/plugins/playlist-search/playlist-search.css: Language not supported
## Pull Request Overview This PR introduces a playlist and album search plugin for YTMusic that injects a customized search bar into playlist and album shelves to filter items based on a user’s search term. - Injects a custom search box into playlist or album pages. - Uses a debounce mechanism on user input to filter playlist items. - Implements a MutationObserver to automatically reapply filters when new content loads. <details> <summary>Files not reviewed (2)</summary> * **src/i18n/resources/en.json**: Language not supported * **src/plugins/playlist-search/playlist-search.css**: Language not supported </details>
@ -0,0 +155,4 @@
this.initializeSearch(shelf);
this.waiting = false;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-04-16 14:14:25 +00:00

[nitpick] There is a redundant assignment to 'this.waiting' after it was already set to false on line 149. Removing the redundant assignment may improve code clarity.


[nitpick] There is a redundant assignment to 'this.waiting' after it was already set to false on line 149. Removing the redundant assignment may improve code clarity. ```suggestion ```
@ -0,0 +159,4 @@
},
start(): void {
this.onPageChange();
window.navigation.addEventListener('navigate', this.onPageChange);
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-04-16 14:14:24 +00:00

The event listener is set with this.onPageChange without explicit binding, which might lead to an incorrect 'this' context. Consider binding the method or converting it to an arrow function.

      const boundOnPageChange = this.onPageChange.bind(this);
      window.navigation.addEventListener('navigate', boundOnPageChange);
      this.onPageChange = boundOnPageChange;
The event listener is set with this.onPageChange without explicit binding, which might lead to an incorrect 'this' context. Consider binding the method or converting it to an arrow function. ```suggestion const boundOnPageChange = this.onPageChange.bind(this); window.navigation.addEventListener('navigate', boundOnPageChange); this.onPageChange = boundOnPageChange; ```
arjix left a comment
Owner

I am mostly nitpicking, doing essentially what Copilot's review should have done smh.
Feel free to ignore my review, it's just nitpicks of how I'd write my code, not necessarily improvements.

if I had one general suggestion to improve the quality of the code, it would be "please use generic types as they are intended"

not that the code itself had any issues, but code of that style is bound to have weird typescript issues on a larger scale.
try to never have to use as, always try to make typescript happy w/o resorting to it


PS: If I sound condescending, please know that when I give reviews, I envision it as giving a review to my past self.
I have no intention of making you upset or anything, this is amazing work.

I am mostly nitpicking, doing essentially what Copilot's review should have done smh. Feel free to ignore my review, it's just nitpicks of how I'd write my code, not necessarily improvements. if I had one general suggestion to improve the quality of the code, it would be "please use generic types as they are intended" not that the code itself had any issues, but code of that style is bound to have weird typescript issues on a larger scale. try to never have to use `as`, always try to make typescript happy w/o resorting to it --- PS: If I sound condescending, please know that when I give reviews, I envision it as giving a review to my past self. I have no intention of making you upset or anything, this is amazing work.
@ -0,0 +73,4 @@
const existingContainer = document.getElementById(PlaylistSearchBoxID);
if (existingContainer) {
this.searchContainer = existingContainer as HTMLDivElement;
Owner

nitpick

      const existingContainer = document.getElementById<HTMLDivElement>(PlaylistSearchBoxID);
      if (existingContainer) {
        this.searchContainer = existingContainer;
nitpick ```suggestion const existingContainer = document.getElementById<HTMLDivElement>(PlaylistSearchBoxID); if (existingContainer) { this.searchContainer = existingContainer; ```
@ -0,0 +84,4 @@
this.currentSearchTerm = '';
this.searchContainer = document.createElement(
'ytmusic-search-box'
) as HTMLDivElement;
Owner

nitpick

      this.searchContainer = document.createElement<'div'>('ytmusic-search-box');
nitpick ```suggestion this.searchContainer = document.createElement<'div'>('ytmusic-search-box'); ```
@ -0,0 +121,4 @@
const newSearchTerm = (e.target as HTMLInputElement).value;
this.currentSearchTerm = newSearchTerm;
debouncedFilter(newSearchTerm, shelf);
});
Owner

nitpick

        this.searchInput.addEventListener('input', (e) => {
          this.currentSearchTerm = this.searchInput.value;
          debouncedFilter(newSearchTerm, shelf);
        });
nitpick ```suggestion this.searchInput.addEventListener('input', (e) => { this.currentSearchTerm = this.searchInput.value; debouncedFilter(newSearchTerm, shelf); }); ```
@ -0,0 +131,4 @@
if (this.currentSearchTerm) {
debouncedFilter(this.currentSearchTerm, shelf);
}
});
Owner

nitpick

        this.songListObserver?.disconnect();
        this.songListObserver = new MutationObserver(() => {
          if (this.currentSearchTerm) {
            debouncedFilter(this.currentSearchTerm, shelf);
          }
        });
nitpick ```suggestion this.songListObserver?.disconnect(); this.songListObserver = new MutationObserver(() => { if (this.currentSearchTerm) { debouncedFilter(this.currentSearchTerm, shelf); } }); ```
@ -0,0 +149,4 @@
const shelf =
document.querySelector(PlaylistShelfComponent) ??
document.querySelector(AlbumShelfComponent);
Owner

nitpick

      if (this.waiting) return;

      this.waiting = true;
      await waitForElement<HTMLElement>('#continuations');
      this.waiting = false;

      const shelf = document.querySelector(`${PlaylistShelfComponent}, ${AlbumShelfComponent}`);
nitpick ```suggestion if (this.waiting) return; this.waiting = true; await waitForElement<HTMLElement>('#continuations'); this.waiting = false; const shelf = document.querySelector(`${PlaylistShelfComponent}, ${AlbumShelfComponent}`); ```
JellyBrick (Migrated from github.com) requested changes 2025-05-10 11:52:16 +00:00
JellyBrick (Migrated from github.com) left a comment

Can you fix the things we pointed out in the review?

Can you fix the things we pointed out in the review?
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2025-09-05 20:09:15 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull Request Overview

This PR adds a new plugin that enables search functionality within playlists and albums in YouTube Music. The plugin addresses a long-standing missing feature by implementing a client-side filtering solution.

Key changes:

  • Implements a search box that filters playlist/album items by hiding non-matching elements
  • Uses infinite scroll to load more content when current items don't match the search query
  • Supports searching across title, artist, and album fields with accent normalization

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/plugins/playlist-search/playlist-search.css Styling for the search container and input box
src/plugins/playlist-search/index.ts Main plugin implementation with search logic and DOM manipulation
src/i18n/resources/en.json English translations for plugin name, description, and placeholder text

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

## Pull Request Overview This PR adds a new plugin that enables search functionality within playlists and albums in YouTube Music. The plugin addresses a long-standing missing feature by implementing a client-side filtering solution. Key changes: - Implements a search box that filters playlist/album items by hiding non-matching elements - Uses infinite scroll to load more content when current items don't match the search query - Supports searching across title, artist, and album fields with accent normalization ### Reviewed Changes Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments. | File | Description | | ---- | ----------- | | src/plugins/playlist-search/playlist-search.css | Styling for the search container and input box | | src/plugins/playlist-search/index.ts | Main plugin implementation with search logic and DOM manipulation | | src/i18n/resources/en.json | English translations for plugin name, description, and placeholder text | --- <sub>**Tip:** Customize your code reviews with copilot-instructions.md. <a href="/th-ch/youtube-music/new/master/.github?filename=copilot-instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Create the file</a> or <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">learn how to get started</a>.</sub>
@ -0,0 +43,4 @@
// methods
containsSearchTerm(element: Element, searchTerm: string): boolean {
//normalize the text to remove accents for easier search
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-09-05 20:09:14 +00:00

Comment should start with a capital letter and end with proper punctuation: 'Normalize the text to remove accents for easier search.'

      // Normalize the text to remove accents for easier search.
Comment should start with a capital letter and end with proper punctuation: 'Normalize the text to remove accents for easier search.' ```suggestion // Normalize the text to remove accents for easier search. ```
@ -0,0 +94,4 @@
shelf.insertBefore(this.searchContainer, shelf.firstChild);
// modify the ytmusic search box to our needs after it's inserted
this.searchInput = this.searchContainer.querySelector('input')!;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-09-05 20:09:14 +00:00

Using the non-null assertion operator (!) without proper null checking is risky. If the querySelector fails to find an input element, this will cause a runtime error. Consider adding proper null checking or error handling.

        this.searchInput = this.searchContainer.querySelector('input');
        if (!this.searchInput) {
          // Could not find input element, abort initialization
          return;
        }
Using the non-null assertion operator (!) without proper null checking is risky. If the querySelector fails to find an input element, this will cause a runtime error. Consider adding proper null checking or error handling. ```suggestion this.searchInput = this.searchContainer.querySelector('input'); if (!this.searchInput) { // Could not find input element, abort initialization return; } ```
@ -0,0 +135,4 @@
this.songListObserver.observe(shelf.querySelector('#contents')!, {
childList: true,
});
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-09-05 20:09:14 +00:00

Using the non-null assertion operator (!) without null checking. If shelf.querySelector('#contents') returns null, this will cause a runtime error. Add proper null checking before calling observe.

        const contentsEl = shelf.querySelector('#contents');
        if (contentsEl) {
          this.songListObserver.observe(contentsEl, {
            childList: true,
          });
        }
Using the non-null assertion operator (!) without null checking. If `shelf.querySelector('#contents')` returns null, this will cause a runtime error. Add proper null checking before calling observe. ```suggestion const contentsEl = shelf.querySelector('#contents'); if (contentsEl) { this.songListObserver.observe(contentsEl, { childList: true, }); } ```
@ -0,0 +155,4 @@
this.initializeSearch(shelf);
this.waiting = false;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2025-09-05 20:09:14 +00:00

The waiting flag is set to false twice in the same function - once after waitForElement (line 148) and again at the end (line 158). The second assignment is redundant and should be removed.


The `waiting` flag is set to false twice in the same function - once after `waitForElement` (line 148) and again at the end (line 158). The second assignment is redundant and should be removed. ```suggestion ```
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin donovan-yohan/feat/playlist-search:donovan-yohan/feat/playlist-search
git switch donovan-yohan/feat/playlist-search

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch master
git merge --no-ff donovan-yohan/feat/playlist-search
git switch donovan-yohan/feat/playlist-search
git rebase master
git switch master
git merge --ff-only donovan-yohan/feat/playlist-search
git switch donovan-yohan/feat/playlist-search
git rebase master
git switch master
git merge --no-ff donovan-yohan/feat/playlist-search
git switch master
git merge --squash donovan-yohan/feat/playlist-search
git switch master
git merge --ff-only donovan-yohan/feat/playlist-search
git switch master
git merge donovan-yohan/feat/playlist-search
git push origin master
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: YTMD/youtube-music#3202
No description provided.