Skip to content

Instantly share code, notes, and snippets.

@BrianLincoln
Last active November 15, 2025 15:51
Show Gist options
  • Select an option

  • Save BrianLincoln/982bd121c4414d89ff3785af93f65c7a to your computer and use it in GitHub Desktop.

Select an option

Save BrianLincoln/982bd121c4414d89ff3785af93f65c7a 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 to copy Liked songs to a playlist since YTM still doesn't have the functionality. I'm using this to copy songs out of YTM to another service, then unsubscribing. Thus, I won't be maintaining it (or ever using it again). It will only work while the YTM interface is the same as it is today (3/6/21) and will break once they make updates.

Steps to use:

  1. Create a new playlist
  2. Go to your Likes page (in chrome, on a desktop or laptop). Scroll to the bottom so all songs are loaded
  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

If it's working, you will see it log out the song titles and (1 of x).

let TARGET_PLAYLIST = "YOUR_PLAYLIST_NAME";

let songElements = document.querySelectorAll(
  "#contents > .ytmusic-playlist-shelf-renderer"
);
let addToPlaylistXpath = "//yt-formatted-string[text()='Add to playlist']";
let playlistXpath = `//yt-formatted-string[text()='${TARGET_PLAYLIST}']`;

function isHidden(element) {
  return element.offsetParent === null;
}

function waitForElementToDisplay(xpath, callback) {
  const timeout = 5000
  const checkFrequency = 30

  var startTimeInMs = Date.now();
  (function loopSearch() {
    const element = document.evaluate(
      xpath,
      document,
      null,
      XPathResult.FIRST_ORDERED_NODE_TYPE,
      null
    ).singleNodeValue;

    if (element && !isHidden(element)) {
      callback(element);
      return;
    } else {
      setTimeout(function () {
        if (timeout && Date.now() - startTimeInMs > timeout) return;
        loopSearch();
      }, checkFrequency);
    }
  })();
}

function copySong(songElement, index) {
  const dotsElement = songElement.querySelector("#button");
  const songTitle = songElement.querySelector(".yt-simple-endpoint").textContent;

  console.log(`Copying: ${songTitle} (${index} of ${songElements.length})`);

  // click dot action menu
  dotsElement.click();

  waitForElementToDisplay(addToPlaylistXpath, (addToPlaylistElement) => {
    // click "Add to playlist"
    addToPlaylistElement.click();

    waitForElementToDisplay(playlistXpath, (playlistElement) => {
      // click target playlist
      playlistElement.click();

      document.body.click();

      // call function again
      if (index <= songElements.length) {
        const nextIndex = index + 1;
        copySong(songElements[nextIndex], nextIndex);
      }
    });
  });
}

copySong(songElements[0], 0)
@FoxyRealm
Copy link

FoxyRealm commented Jun 11, 2025

@Tom-Whi @Mrkas121 (sorry for tag)
Hey i found treasure
go to chrome.google.com/webstore/detail/multiselect-for-youtube/gpgbiinpmelaihndlegbgfkmnpofgfei/related Download , install , and eneble
go to the playlist , scroll to bottom , select all videos , three dots , save to playlist and done

This is 100% the correct answer

Yooo that's amazing man thanks for sharing it ^^

btw the method that I shared was made to access the 3 dots menu and go through all that, but I guess thee keep changing something in the code which makes it impossible to keep that working lo

Anyways thanks again :)

@hallettj
Copy link

I made some tweaks to the script from @FoxyRealm to get something that seems to be working for me, at least in Firefox.

let TARGET_PLAYLIST = "YOUR_PLAYLIST_NAME"; // πŸ” Replace with your playlist name

let songElements = document.querySelectorAll("ytmusic-responsive-list-item-renderer");
let addToPlaylistXpath = "//yt-formatted-string[text()='Save to playlist']";
let playlistOptionButtonSelector = "button.ytmusic-playlist-add-to-option-renderer"

function isHidden(element) {
  return element.offsetParent === null;
}

function waitForElementToDisplay(xpath, callback) {
  const timeout = 10000;
  const checkFrequency = 100;
  const startTimeInMs = Date.now();

  (function loopSearch() {
    const element = document.evaluate(
      xpath,
      document,
      null,
      XPathResult.FIRST_ORDERED_NODE_TYPE,
      null
    ).singleNodeValue;

    if (element && !isHidden(element)) {
      callback(element);
    } else if (Date.now() - startTimeInMs < timeout) {
      setTimeout(loopSearch, checkFrequency);
    } else {
      console.warn("Element not found: " + xpath);
    }
  })();
}

function waitFor(test, failure_message, callback) {
  const timeout = 10000;
  const checkFrequency = 100;
  const startTimeInMs = Date.now();

  (function loopSearch() {
    const result = test()
    if (result) {
      callback(result)
    } else if (Date.now() - startTimeInMs < timeout) {
      setTimeout(loopSearch, checkFrequency);
    } else {
      console.warn("Element not found: " + failure_message);
    }
  })();
}

function queryPlaylistButton() {
  return document.querySelectorAll(playlistOptionButtonSelector).values().find(
    button => button.getAttribute('aria-label').trim() == TARGET_PLAYLIST
  )
}

function selectPlaylistAndAdd(songElement, index) {
  if (!songElement) {
    console.log("βœ… Done copying all songs.");
    return;
  }

  const dotsElement = songElement.querySelector("#button-shape");
  const dotsButton = dotsElement && dotsElement.querySelector("button");
  const songTitle = songElement.querySelector(".yt-simple-endpoint")?.textContent || "Unknown";

  if (!dotsButton) {
    console.warn(`⚠️ Could not find 3 dots. Skipping: ${songTitle}`);
    selectPlaylistAndAdd(songElements[index + 1], index + 1);
    return;
  }

  console.log(`🎡 Adding: ${songTitle} (${index + 1} of ${songElements.length})`);
  dotsButton.click();

  waitForElementToDisplay(addToPlaylistXpath, (addToPlaylistElement) => {
    console.log("Found 'Save to playlist' button.");
    setTimeout(() => {
      addToPlaylistElement.click();

      waitFor(queryPlaylistButton, "button for your playlist", targetPlaylistOption => {
        console.log(`Found the playlist: ${TARGET_PLAYLIST}`);
        targetPlaylistOption.click();

        setTimeout(() => {
          document.body.click();
          setTimeout(() => {
            selectPlaylistAndAdd(songElements[index + 1], index + 1);
          }, 800);
        }, 500);
      })
    }, 500);
  });
}

selectPlaylistAndAdd(songElements[0], 0);

@yurighensev
Copy link

yurighensev commented Oct 31, 2025

Hallett's script didn't quite work for me, it only added the few auto suggested songs to the new playlist.

So I asked gpt 5 to generate me a script to export the full playlist to a csv. This one worked quite well for my 1400 songs:

(async function () {
  console.log("🎧 Starting YouTube Music Liked Songs Exporter (with URLs)...");

  const SCROLL_PAUSE = 2000; // ms between scrolls
  const MAX_SCROLLS = 400;   // adjust if you have >2000 songs

  // Scroll until all songs are loaded
  let lastCount = 0;
  for (let i = 0; i < MAX_SCROLLS; i++) {
    window.scrollTo(0, document.body.scrollHeight);
    await new Promise(r => setTimeout(r, SCROLL_PAUSE));

    const items = document.querySelectorAll("ytmusic-responsive-list-item-renderer");
    if (items.length === lastCount) {
      console.log(`βœ… Fully loaded ${items.length} songs.`);
      break;
    }
    lastCount = items.length;
    console.log(`πŸ“œ Loaded ${items.length} so far...`);
  }

  const songs = Array.from(document.querySelectorAll("ytmusic-responsive-list-item-renderer")).map(el => {
    const titleEl = el.querySelector(".yt-simple-endpoint");
    const title = titleEl?.textContent?.trim() || "";
    const url = titleEl?.href || "";

    // artists are links inside the byline
    const artists = Array.from(el.querySelectorAll("yt-formatted-string.byline a"))
      .map(a => a.textContent.trim())
      .join(", ");

    // album is sometimes the second element in byline
    const bylineParts = Array.from(el.querySelectorAll("yt-formatted-string.byline a"))
      .map(a => a.textContent.trim());
    const album = bylineParts.length > 1 ? bylineParts[bylineParts.length - 1] : "";

    const duration = el.querySelector(".fixed-column")?.textContent?.trim() || "";

    return { title, artist: artists, album, duration, url };
  });

  console.log(`🎢 Extracted ${songs.length} songs.`);

  if (!songs.length) {
    console.warn("❌ No songs found. Make sure you're on https://music.youtube.com/library/likes");
    return;
  }

  // Convert to CSV
  function toCSV(rows) {
    const escape = v => `"${String(v ?? "").replace(/"/g, '""')}"`;
    const headers = Object.keys(rows[0]);
    const csv = [headers.join(",")].concat(rows.map(r => headers.map(h => escape(r[h])).join(",")));
    return csv.join("\n");
  }

  const csv = toCSV(songs);
  const blob = new Blob([csv], { type: "text/csv" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `ytmusic_liked_songs_${new Date().toISOString().slice(0,10)}.csv`;
  document.body.appendChild(a);
  a.click();
  a.remove();

  console.log("πŸ’Ύ CSV file with URLs downloaded successfully!");
})();
image

@hellosamblack
Copy link

Looks like they've changed it again. This worked for me:

(async function () {
  console.log("🎧 Starting YouTube Music Liked Songs Exporter (v3 - Final Fix)...");

  const SCROLL_PAUSE = 2000; // ms between scrolls
  const MAX_SCROLLS = 400;   // adjust if you have >2000 songs

  // Scroll until all songs are loaded
  let lastCount = 0;
  for (let i = 0; i < MAX_SCROLLS; i++) {
    window.scrollTo(0, document.body.scrollHeight);
    await new Promise(r => setTimeout(r, SCROLL_PAUSE));

    const items = document.querySelectorAll("ytmusic-responsive-list-item-renderer");
    if (items.length === lastCount) {
      console.log(`βœ… Fully loaded ${items.length} songs.`);
      break;
    }
    lastCount = items.length;
    console.log(`πŸ“œ Loaded ${items.length} so far...`);
  }

  const songs = Array.from(document.querySelectorAll("ytmusic-responsive-list-item-renderer")).map(el => {
    const titleEl = el.querySelector(".title .yt-simple-endpoint");
    const title = titleEl?.textContent?.trim() || "";
    const url = titleEl?.href || "";

    // --- START: MODIFIED ARTIST/ALBUM LOGIC (v3) ---
    // Based on your HTML:
    // Artist and Album are now in separate <yt-formatted-string class="flex-column"> tags.
    
    const bylineEls = el.querySelectorAll("yt-formatted-string.flex-column");

    let artist = "";
    let album = "";

    // The first "flex-column" is the artist
    if (bylineEls.length > 0) {
        artist = bylineEls[0]?.textContent?.trim() || "";
    }
    // The second "flex-column" is the album
    if (bylineEls.length > 1) {
        album = bylineEls[1]?.textContent?.trim() || "";
    }
    // --- END: MODIFIED ARTIST/ALBUM LOGIC (v3) ---

    const duration = el.querySelector(".fixed-column")?.textContent?.trim() || "";

    return { title, artist, album, duration, url };
  });

  console.log(`🎢 Extracted ${songs.length} songs.`);

  if (!songs.length) {
    console.warn("❌ No songs found. Make sure you're on https://music.youtube.com/library/likes");
    return;
  }

  // Convert to CSV
  function toCSV(rows) {
    const escape = v => `"${String(v ?? "").replace(/"/g, '""')}"`;
    const headers = Object.keys(rows[0]);
    const csv = [headers.join(",")].concat(rows.map(r => headers.map(h => escape(r[h])).join(",")));
    return csv.join("\n");
  }

  const csv = toCSV(songs);
  const blob = new Blob([csv], { type: "text/csv" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `ytmusic_liked_songs_${new Date().toISOString().slice(0,10)}.csv`;
  document.body.appendChild(a);
  a.click();
  a.remove();

  console.log("πŸ’Ύ CSV file with URLs downloaded successfully!");
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment