|
1 | 1 | // ==UserScript== |
2 | 2 | // @name YouTube Save to Playlist filter |
3 | 3 | // @namespace fred.vatin.yt-playlists-filter |
4 | | -// @version 1.1.5 |
| 4 | +// @version 1.1.6 |
5 | 5 | // @description Tap P key to open the “save to playlist” menu where your can type to filter |
6 | 6 | // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com |
7 | 7 | // @author Fred Vatin, Flemming Steffensen |
|
31 | 31 | // 1. Click the "…" menu button where there is the "save to playlist" menu entry |
32 | 32 | // 2. In the dev tool > inspector, search for the selector: yt-button-shape > button[aria-label] |
33 | 33 | // It should be around the 7th match. Copy the [aria-label] value and add it here. |
34 | | - const MoreActionsButtonText = [ |
35 | | - "More actions", |
36 | | - "Autres actions", |
37 | | - "Mehr Aktionen", |
38 | | - "Más acciones", |
39 | | - "Altre azioni", |
40 | | - "Mais ações", |
41 | | - ]; |
| 34 | + const MoreActionsButtonText = ["More actions", "Autres actions", "Mehr Aktionen", "Más acciones", "Altre azioni", "Mais ações"]; |
42 | 35 | // 3. search for the selector: #items > ytd-menu-service-item-renderer yt-formatted-string |
43 | 36 | // copy the text content |
44 | 37 | const SaveButtonText = ["Save", "Enregistrer", "Speichern", "Guardar", "Salva"]; |
|
61 | 54 | * ℹ GLOBAL DEFINITION |
62 | 55 | ===========================================================================*/ |
63 | 56 | let URL = window.location.href; |
| 57 | + let PLAYLISTS = null; |
64 | 58 | console.log("URL (at first loading): ", URL); |
| 59 | + const selector_MoreActionSubMenuItems = "#items > ytd-menu-service-item-renderer yt-formatted-string"; |
65 | 60 | const selector_MenuType1 = "yt-contextual-sheet-layout"; |
66 | 61 | const selector_MenuType2 = "ytd-add-to-playlist-renderer.ytd-popup-container"; |
67 | 62 | const selector_HeaderType1 = "yt-panel-header-view-model"; |
|
182 | 177 | */ |
183 | 178 | let items = 0; |
184 | 179 | function callback_NewMenu(mutationsList, obsv_PlaylistContainer) { |
185 | | - |
186 | 180 | for (const mutation of mutationsList) { |
187 | 181 | if (mutation.type === "childList" && mutation.addedNodes.length > 0) { |
188 | 182 | // Iterate only on newly added nodes |
|
240 | 234 | console.log(`❌ header not detected in menu`); |
241 | 235 | } |
242 | 236 | } |
243 | | - |
244 | 237 | } else { |
245 | | - console.log(`❌ new node seems to match selectors_PlaylistsMenu but for some unknown reason it doesn’t match selector_MenuType1 or selector_MenuType2`, node); |
| 238 | + console.log( |
| 239 | + `❌ new node seems to match selectors_PlaylistsMenu but for some unknown reason it doesn’t match selector_MenuType1 or selector_MenuType2`, |
| 240 | + node, |
| 241 | + ); |
246 | 242 | } |
247 | | - |
248 | | - |
249 | 243 | } |
250 | 244 | } |
251 | 245 | } |
|
255 | 249 | } |
256 | 250 |
|
257 | 251 | /** |
258 | | - * MutationObserver callback called when a new playlists menu is added to DOM (detected by callback_NewMenu) |
259 | | - * |
260 | | - * Iterates over the provided MutationRecord list and, for each mutation (added nodes), |
261 | | - * check if the required menu elements are there. |
262 | | - * If yes, it adds the input filter. |
263 | | - * |
264 | | - * @param {MutationRecord[]} mutationsList - Array of MutationRecord objects provided by the observer. |
265 | | - * @param {MutationObserver} obsv_MenuAvailable - The MutationObserver instance that invoked this callback (not used) |
266 | | - * @returns {void} |
267 | | - * @callback MutationCallback |
268 | | - * @listens {MutationEvent} childList - Listens for changes in child elements |
269 | | - * |
270 | | - */ |
| 252 | + * MutationObserver callback called when a new playlists menu is added to DOM (detected by callback_NewMenu) |
| 253 | + * |
| 254 | + * Iterates over the provided MutationRecord list and, for each mutation (added nodes), |
| 255 | + * check if the required menu elements are there. |
| 256 | + * If yes, it adds the input filter. |
| 257 | + * |
| 258 | + * @param {MutationRecord[]} mutationsList - Array of MutationRecord objects provided by the observer. |
| 259 | + * @param {MutationObserver} obsv_MenuAvailable - The MutationObserver instance that invoked this callback (not used) |
| 260 | + * @returns {void} |
| 261 | + * @callback MutationCallback |
| 262 | + * @listens {MutationEvent} childList - Listens for changes in child elements |
| 263 | + * |
| 264 | + */ |
271 | 265 | function callback_MenuAvailable(mutationsList, obsv_MenuAvailable) { |
272 | | - |
273 | 266 | let header = null; |
274 | 267 | let list = null; |
275 | 268 |
|
|
354 | 347 | for (const mutation of mutationsList) { |
355 | 348 | const AriaHidden = mutation.target.ariaHidden; |
356 | 349 |
|
357 | | - // console.log( |
358 | | - // `mutation type: ${mutation.type}, AriaHidden: ${AriaHidden}, attribute changed: ${mutation.attributeName}`, |
359 | | - // mutation.target |
360 | | - // ); |
| 350 | + console.log( |
| 351 | + `callback_MenuOpen triggered. mutation type: ${mutation.type}, AriaHidden: ${AriaHidden}, attribute changed: ${mutation.attributeName}`, |
| 352 | + mutation.target |
| 353 | + ); |
361 | 354 |
|
362 | 355 | if (!AriaHidden) { |
363 | 356 | setFocus(InputId); |
| 357 | + if (PLAYLISTS) { |
| 358 | + console.log(`Use PLAYLISTS`, PLAYLISTS); |
| 359 | + autoTopList(PLAYLISTS); |
| 360 | + } |
364 | 361 | } |
365 | 362 | } |
366 | 363 | } |
|
416 | 413 | * isPlayer('https://youtu.be/dQw4w9WgXcQ'); |
417 | 414 | */ |
418 | 415 | function isPlayer(url) { |
419 | | - if ( |
420 | | - url.startsWith("https://www.youtube.com/watch?v=") || |
421 | | - url.startsWith("https://www.youtube.com/live/") |
422 | | - ) { |
| 416 | + if (url.startsWith("https://www.youtube.com/watch?v=") || url.startsWith("https://www.youtube.com/live/")) { |
423 | 417 | console.log("✅ Player detected !"); |
424 | 418 | return true; |
425 | 419 | } else { |
|
440 | 434 | function addFilterInput(elements = {}) { |
441 | 435 | const { list = null, header = null } = elements; |
442 | 436 |
|
443 | | - autoTopList(list); |
| 437 | + PLAYLISTS = list; |
| 438 | + |
| 439 | + autoTopList(PLAYLISTS); |
444 | 440 |
|
445 | 441 | const existingFilterInput = document.getElementById(InputId); |
446 | 442 |
|
447 | 443 | if (!existingFilterInput) { |
448 | | - |
449 | 444 | // handle MenuType1 vs MenuType2 |
450 | 445 | const isType2 = header.matches(selector_HeaderType2); |
451 | 446 | let title = null; |
452 | 447 | let selector_OpenMenuParent = null; |
453 | 448 |
|
454 | 449 | if (isType2) { |
455 | 450 | title = header?.querySelector('span[role="text"]'); |
| 451 | + title.style.marginRight = "20px"; |
456 | 452 | selector_OpenMenuParent = selector_OpenMenuParentType2; |
457 | 453 | } else { |
458 | 454 | title = header?.firstElementChild; |
|
557 | 553 | let selector_ListItems = null; |
558 | 554 |
|
559 | 555 | if (isType2) { |
560 | | - selector_ListItems = selector_ListItemsType2 |
| 556 | + selector_ListItems = selector_ListItemsType2; |
561 | 557 | selector_SelItems = selector_SelItemsType2; |
562 | 558 | } else { |
563 | | - selector_ListItems = selector_ListItemsType1 |
| 559 | + selector_ListItems = selector_ListItemsType1; |
564 | 560 | selector_SelItems = selector_SelItemsType1; |
565 | 561 | } |
566 | 562 |
|
567 | 563 | // Collect all items |
568 | 564 | listItems = Array.from(playlists.querySelectorAll(selector_ListItems)); |
569 | | - console.log("✅ autoTopList(playlist), listItems number: ", listItems.length); |
| 565 | + console.log("✅ autoTopList(playlists), listItems number: ", listItems.length); |
570 | 566 |
|
571 | 567 | // Separates the selected ones from the rest |
572 | 568 | const selected = listItems.filter((item) => item.querySelector(selector_SelItems)); |
573 | 569 | const unselected = listItems.filter((item) => !item.querySelector(selector_SelItems)); |
574 | 570 |
|
575 | 571 | if (selected.length > 0) { |
576 | | - console.log(`✅ autoTopList(playlist), this video belongs to ${selected.length} playlist(s)`); |
| 572 | + console.log(`✅ autoTopList(playlists), this video belongs to ${selected.length} playlist(s)`); |
577 | 573 | // Reinsert in the desired order |
578 | 574 | [...selected, ...unselected].forEach((item) => { |
579 | 575 | playlists.appendChild(item); |
580 | 576 | }); |
581 | 577 |
|
582 | | - console.log(`✅ autoTopList(playlist), playlists have been sorted.`); |
| 578 | + console.log(`✅ autoTopList(playlists), playlists have been sorted.`); |
583 | 579 |
|
584 | 580 | const sortedItems = Array.from(playlists.querySelectorAll(selector_ListItems)); |
585 | | - console.log("✅ autoTopList(playlist), sortedItems number: ", sortedItems.length); |
| 581 | + console.log("✅ autoTopList(playlists), sortedItems number: ", sortedItems.length); |
586 | 582 | } else { |
587 | | - console.log("❌ autoTopList(playlist): this video doesn’t belong to any existing playlist. Not sorting needed."); |
| 583 | + console.log("❌ autoTopList(playlists): this video doesn’t belong to any existing playlist. Not sorting needed."); |
588 | 584 | } |
589 | 585 | } |
590 | 586 |
|
|
595 | 591 | * |
596 | 592 | * @param {string} el - The ID of the input element to focus. |
597 | 593 | */ |
598 | | - function setFocus(el) { |
| 594 | + async function setFocus(el) { |
599 | 595 | const input = document.getElementById(el); |
600 | 596 | if (input) { |
601 | 597 | // reset filter |
|
612 | 608 | const timeout = 100; |
613 | 609 | console.log(`✅ "input" found. Set focus! Timeout = ${timeout}`); |
614 | 610 | // delay required to set the focus |
615 | | - setTimeout(() => { |
616 | | - input.focus(); |
617 | | - }, timeout); |
| 611 | + await sleep(timeout); |
| 612 | + input.focus(); |
618 | 613 | } else { |
619 | 614 | console.log(`❌ "input" not found. Focus not set!`); |
620 | 615 | } |
|
630 | 625 | * |
631 | 626 | * @returns {void} |
632 | 627 | */ |
633 | | - function openSaveToPlaylistDialog() { |
634 | | - |
| 628 | + async function openSaveToPlaylistDialog() { |
635 | 629 | let directSaveButton = null; |
636 | 630 |
|
637 | 631 | for (const text of DirectSaveButtonText) { |
|
646 | 640 | console.log("✅ openSaveToPlaylistDialog(), direct save button found. Click it", directSaveButton); |
647 | 641 | directSaveButton.click(); |
648 | 642 | } else { |
649 | | - console.log( |
650 | | - "❌ openSaveToPlaylistDialog(), direct save button NOT found. Search for More Actions menu" |
651 | | - ); |
| 643 | + console.log("❌ openSaveToPlaylistDialog(), direct save button NOT found. Search for More Actions menu"); |
652 | 644 |
|
653 | 645 | let moreActionsButton = null; |
654 | 646 | for (const text of MoreActionsButtonText) { |
|
662 | 654 | if (moreActionsButton) { |
663 | 655 | console.log("✅ openSaveToPlaylistDialog(), More Actions menu found. Click it"); |
664 | 656 | moreActionsButton.click(); |
665 | | - setTimeout(() => { |
666 | | - const submenuItems = document.querySelectorAll( |
667 | | - "#items > ytd-menu-service-item-renderer yt-formatted-string" |
668 | | - ); |
669 | | - let found = false; |
670 | | - submenuItems.forEach((item) => { |
671 | | - if (SaveButtonText.includes(item.textContent.trim())) { |
672 | | - item.click(); |
673 | | - console.log("✅ Click on Save item:", item); |
674 | | - found = true; |
675 | | - } |
676 | | - }); |
677 | | - }, 250); |
| 657 | + await sleep(250); |
| 658 | + const submenuItems = document.querySelectorAll(selector_MoreActionSubMenuItems); |
| 659 | + submenuItems.forEach((item) => { |
| 660 | + if (SaveButtonText.includes(item.textContent.trim())) { |
| 661 | + item.click(); |
| 662 | + console.log("✅ Click on Save item:", item); |
| 663 | + } |
| 664 | + }); |
678 | 665 | } else { |
679 | 666 | console.error("❌ Neither a direct 'Save' button nor a 'More actions' button was found."); |
680 | 667 | } |
|
729 | 716 | IS_KEYPRESS_LISTENER_ACTIVE = false; |
730 | 717 | } |
731 | 718 | } |
| 719 | + |
| 720 | + /** |
| 721 | + * Pauses execution for a specified duration. |
| 722 | + * @param {number} ms - The number of milliseconds to sleep. |
| 723 | + * @returns {Promise<void>} A promise that resolves after the specified delay. |
| 724 | + */ |
| 725 | + function sleep(ms) { |
| 726 | + return new Promise((resolve) => setTimeout(resolve, ms)); |
| 727 | + } |
732 | 728 | })(); |
0 commit comments