Skip to content

Instantly share code, notes, and snippets.

@mihailik
Forked from BrianLincoln/YTMLikesToPlaylist.md
Last active December 15, 2024 07:35
Show Gist options
  • Save mihailik/9d82287489e53a918890bcf686bbc1dd to your computer and use it in GitHub Desktop.
Save mihailik/9d82287489e53a918890bcf686bbc1dd to your computer and use it in GitHub Desktop.
YouTube Music Likes to Playlist

Copy Likes to a playlist in YouTube Music

This is a very hacky solution updated from earlier hacky solution to copy Liked songs to a playlist since YTM still doesn't have the functionality. The changes are meant to provide clearer indications of failure, so it can be updated when YouTube Music tweaks the rendering again.

Steps to use:

  1. Create a new playlist
  2. Go to your Likes page (in chrome, on a desktop or laptop).
  3. Open Chrome's dev tools (F12 on windows), go to the console
  4. Paste the script below. Edit the first line, replace "YOUR_PLAYLIST_NAME" with your playlist's name
  5. Press enter

First it will scroll to the bottom of the list to load all the contents of the list. That can take a little time for a long list, i.e. if you liked a lot of songs.

Then it will start adding songs to your target list one by one, highlighting them in shade of blue as it going.

DevTools console will print out some progress info and diagnostics, so if you're proficient in JS and see issues, you can try to fix the script for your use case.

[{TARGET_PLAYLIST: "ma"}].map(async (opt) => { try {

  window.songElements = [...document.querySelectorAll('#contents > .ytmusic-playlist-shelf-renderer')];
  console.log('scrolling to the bottom of the list...');
  while (true) {
    songElements[songElements.length-1].scrollIntoView();
    await new Promise(resolve => setTimeout(resolve, 30));
    const count = songElements.length;
    window.songElements = [...document.querySelectorAll('#contents > .ytmusic-playlist-shelf-renderer')];
    if (songElements.length === count && !document.querySelector('#continuations #spinnerContainer')) break;
    if (songElements.length !== count)
      console.log('   ...'+songElements.length+' ' + txt(songElements[songElements.length-1]));
  }

  document.documentElement.scrollTop = 0;
  await new Promise(resolve => setTimeout(resolve, 30));
  console.log('Found ', songElements.length, ' songs from '+txt(songElements[0])+' to ' + txt(songElements[songElements.length-1]) + '.');


  for (let index = 0; index < songElements.length; index++) {
    for (let iTry = 0; iTry < 5; iTry++) {
      try {
        await add(songElements[index], index);
        break;
      } catch (error) {
        console.log('    ...'+(iTry + 1) + ' try, ' + error.message);
      }
    }
  }

  console.log('Copied all!');

  function txt(elem) { return (elem.textContent || '').replace(/\s+/g, ' '); }

  async function add(songElement, index) {
    // document.documentElement.scrollTop = songElement.getBoundingClientRect().top;
    songElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    await new Promise(resolve => setTimeout(resolve, 30));

    songElement.style.border = 'solid 1px cornflowerblue';

    const log = [`Copying: ${txt(songElement)} (${index + 1} of ${songElements.length}) ...`];
    let longCopyTimeout = setTimeout(() => {
        console.log(...log);
    }, 700);

    const dotsElement = songElement.querySelector("#button");

    // click dot action menu
    log.push('\n  >dotsElement.click() ', dotsElement, '\n\n');
    dotsElement.click();

    const addToPlaylistElement = await waitFor(() => [...document.querySelectorAll('ytmusic-menu-navigation-item-renderer a')].find(it=>/to playlist/i.test(it.textContent ||'')));   
    // click "Add to playlist"
    log.push('\n  >addToPlaylistElement.click() ', addToPlaylistElement, '\n\n');
    addToPlaylistElement.click();

    const playlistElement = await waitFor(() => [...document.querySelectorAll('button.ytmusic-playlist-add-to-option-renderer')].find(btn=> (btn.textContent ||'').trim().split('\n')[0] === opt.TARGET_PLAYLIST));
    
    // click target playlist
    log.push('\n  >playlistElement.click() ', playlistElement, '\n\n');
    playlistElement.click();

    waitFor('tp-yt-paper-toast yt-icon-button#close-button button#button').then(close=>close.click());

    log.push('\n  >document.body.click()');
    document.body.click();
    clearTimeout(longCopyTimeout);

    console.log(`  copied: ${txt(songElement)} (${index + 1} of ${songElements.length})`);
    songElement.style.background = '#324669';
  }

  async function waitFor(test) {
    return new Promise(async (resolve, reject) => {
      const timeoutAt = Date.now() + 5000;
      let element;
  
      while (Date.now() < timeoutAt) {
        element = typeof test === 'function' ? test() : document.querySelector(test);
      
        if (element?.offsetParent)
          return resolve(element);
  
        await new Promise(resolve => setTimeout(resolve, 30));
      }
  
      if (element) reject(new Error('Timed out because element is hidden: '+ test));
      else reject(new Error('Timed out waiting for element to appear: '+test));
    });
  }

} catch(err) { console.error(err) }})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment