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:
- Create a new playlist
- Go to your Likes page (in chrome, on a desktop or laptop).
- Open Chrome's dev tools (F12 on windows), go to the console
- Paste the script below. Edit the first line, replace "YOUR_PLAYLIST_NAME" with your playlist's name
- 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) }})