// ==UserScript==
// @name Export Youtube Playlist in plaintext
// @namespace 1N07
// @version 0.9.4
// @description Shows a list of the playlist video names/channels/URLs in plaintext to be easily copied
// @author 1N07
// @license unlicense
// @compatible firefox v0.9.4 Tested on Firefox v137.0.1 and Tampermonkey 5.3.3 (Likely to work on other userscript managers, but not tested)
// @compatible chrome v0.9.2 Tested on Chrome v132.0.6834.84 and Tampermonkey 5.3.3 (Likely to work on other userscript managers, but not tested)
// @compatible opera untested, but likely works with at least Tampermonkey
// @compatible edge untested, but likely works with at least Tampermonkey
// @compatible safari untested, but likely works with at least Tampermonkey
// @icon https://www.google.com/s2/favicons?domain=youtube.com
// @match https://www.youtube.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// ==/UserScript==
(() => {
let getVideoTitle = GM_getValue("getVideoTitle", true);
let getVideoChannel = GM_getValue("getVideoChannel", false);
let getVideoURL = GM_getValue("getVideoURL", false);
let videoListSeperator = GM_getValue("videoListSeperator", " ; ");
let listCreationAllowed = true;
let urlAtLastCheck = "";
let buttonInsertInterval;
let gmMenuButton;
//add some CSS
GM_addStyle(`
tp-yt-paper-listbox#items { overflow-x: hidden; }
#exportPlainTextList {
cursor: pointer;
height: 36px;
width: 100%;
display: flex;
align-items: center;
}
#exportPlainTextList > img {
height: 24px; width: 24px;
color: rgb(144, 144, 144);
padding: 0 13px 0 16px;
filter: contrast(0%);
}
#exportPlainTextList > span {
font-family: "Roboto","Arial",sans-serif;
color: #d9d9d9;
white-space: nowrap;
font-size: 1.4rem;
line-height: 2rem;
font-weight: 400;
}
#exportPlainTextList:hover { background-color: rgba(255,255,255,0.1); }
ytd-menu-popup-renderer.ytd-popup-container { overflow-x: hidden !important; max-height: none !important; }
#listDisplayContainer {
position: fixed;
z-index: 9999;
margin: 0 auto;
background-color: #464646;
padding: 10px;
border-radius: 5px;
left: 0;
right: 0;
max-width: 100vw;
width: 1200px;
height: 900px;
max-height: 90vh;
top: 5vh;
resize: both;
overflow: hidden;
}
#listDisplayContainer p {
text-align: center;
}
#listDisplayContainer .title {
font-size: 21px;
font-weight: bold;
color: #d9d9d9;
}
#listDisplayContainer ul {
list-style: none;
font-size: 12px;
scale: 1.4;
color: #d9d9d9;
width: -moz-fit-content;
width: fit-content;
margin: 40px auto;
}
#listDisplayContainer > textarea {
box-sizing: border-box;
width: 100%;
margin: 10px 0;
height: calc(100% - 40px);
background-color: #262626;
border: none;
color: #EEE;
border-radius: 5px;
resize: none;
}
#listDisplayContainer #listDisplayGetListButton {
position: relative;
margin: 10px 0;
font-size: 13px;
left: 50%;
transform: translateX(-50%);
}
#closeTheListThing {
float: right;
font-weight: bold;
background-color: RGBA(255,255,255,0.25);
border: none;
font-size: 17px;
border-radius: 10px;
height: 25px;
width: 25px;
cursor: pointer;
}
#closeTheListThing:hover { background-color: rgba(255,255,255,0.5); }
tp-yt-iron-dropdown.ytd-popup-container #contentWrapper > yt-sheet-view-model.ytd-popup-container {
max-height: unset !important;
}
.yt-pl-export-loading-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #262626;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 9999;
border-radius: 8px;
text-align: center;
}
.yt-pl-export-loading-popup-message {
font-size: 2rem;
color: #d9d9d9;
}
`);
setInterval(() => {
if (urlAtLastCheck !== window.location.href) {
urlAtLastCheck = window.location.href;
if (urlAtLastCheck.includes("/playlist?list=")) {
gmMenuButton = GM_registerMenuCommand("Export Playlist", () => {
if (document.querySelector("ytd-playlist-video-list-renderer > #contents.ytd-playlist-video-list-renderer")?.hasChildNodes()) {
ScrollUntillAllVisible();
}
else {
const popup = CreatePopup("No videos found in this playlist. Either this playlist has no videos, or they have not loaded in yet.");
setTimeout(() => { popup.close() }, 3000);
}
});
InsertButtonASAP();
}
else {
if (gmMenuButton != null)
GM_unregisterMenuCommand(gmMenuButton);
clearInterval(buttonInsertInterval);
}
}
}, 100);
function InsertButtonASAP() {
buttonInsertInterval = setInterval(() => {
//wait for possible previous buttons to stop existing (due to how youtube loads pages) and for the space for the new button to be available
if (!document.getElementById("exportPlainTextList")) {
let place = document.querySelector("tp-yt-iron-dropdown.ytd-popup-container .yt-list-view-model-wiz[role='menu']");
if (!place)
place = document.querySelector("tp-yt-iron-dropdown.ytd-popup-container tp-yt-paper-listbox.ytd-menu-popup-renderer[role='listbox']");
if (place) {
place.appendChild(
CreateElement("div", {
attributes: { id: "exportPlainTextList" },
children: [
CreateElement("img", {
attributes: { src: "https://i.imgur.com/emlur3a.png" }
}),
CreateElement("span", {
properties: { textContent: "Export Playlist" }
})
],
events: {
click: ScrollUntillAllVisible
}
})
);
}
}
}, 100);
}
function ScrollUntillAllVisible() {
if (!listCreationAllowed)
return;
document.querySelector("ytd-browse[page-subtype='playlist']").click();
const popup = CreatePopup("Scrolling to load all videos in the playlist. Please wait...");
listCreationAllowed = false;
const scrollInterval = setInterval(() => {
if (document.querySelector("ytd-continuation-item-renderer.ytd-playlist-video-list-renderer")) {
window.scrollTo(0, (document.documentElement || document.body).scrollHeight);
} else {
popup.close();
DisplayListOptions();
clearInterval(scrollInterval);
}
}, 100);
}
function DisplayListOptions() {
document.body.appendChild(
CreateElement("div", {
attributes: { id: "listDisplayContainer" },
children: [
CreateElement("p", {
children: [
CreateElement("span", {
attributes: { class: "title" },
children: ["Playlist in plain text"]
}),
CreateElement("button", {
attributes: { id: "closeTheListThing" },
properties: { textContent: "X" },
events: {
click: () => {
document.getElementById("listDisplayContainer").remove();
listCreationAllowed = true;
}
}
})
]
}),
CreateElement("textarea", {
attributes: { style: "display: none;" }
}),
CreateElement("ul", {
attributes: { id: "listDisplayOptions" },
children: [
CreateListItemWithCheckbox('Get titles', 'getVideoTitleCB', getVideoTitle, function () {
getVideoTitle = this.checked;
GM_setValue("getVideoTitle", getVideoTitle);
}),
CreateListItemWithCheckbox('Get channel names', 'getVideoChannelCB', getVideoChannel, function () {
getVideoChannel = this.checked;
GM_setValue("getVideoChannel", getVideoChannel);
}),
CreateListItemWithCheckbox('Get URLs', 'getVideoURLCB', getVideoURL, function () {
getVideoURL = this.checked;
GM_setValue("getVideoURL", getVideoURL);
}),
CreateElement("li", {
children: [
CreateElement("label", {
children: [
CreateElement("input", {
attributes: { type: "text", id: "videoListSeperatorInput", name: "videoListSeperatorInput" },
properties: { value: videoListSeperator, style: "width: 40px; text-align: center;" },
events: {
change: function () {
videoListSeperator = this.value;
GM_setValue("videoListSeperator", videoListSeperator);
}
}
}),
" Name/Author/URL separator"
]
})
]
}),
CreateElement("li", {
children: [
CreateElement("button", {
attributes: { id: "listDisplayGetListButton" },
properties: { textContent: "Get list" },
events: {
click: BuildAndDisplayList
}
})
]
})
]
}),
]
})
);
}
function BuildAndDisplayList() {
document.getElementById("listDisplayOptions").style.display = "none";
document.querySelector("#listDisplayContainer > textarea").style.display = "block";
const videoTitleArr = [];
const videoChannelArr = [];
const videoURLArr = [];
let videoCount = 0;
for (const element of document.querySelectorAll("ytd-playlist-video-list-renderer > #contents.ytd-playlist-video-list-renderer > ytd-playlist-video-renderer #content")) {
if (getVideoTitle)
videoTitleArr.push(element.querySelector("#video-title").getAttribute("title"));
if (getVideoURL)
videoURLArr.push(`https://www.youtube.com${element.querySelector("#video-title").getAttribute("href").split("&")[0]}`);
if (getVideoChannel)
videoChannelArr.push(element.querySelector("#channel-name yt-formatted-string.ytd-channel-name > a").textContent);
videoCount++;
}
let list = "";
for (let i = 0; i < videoCount; i++) {
if (getVideoTitle)
list += videoTitleArr[i];
if (getVideoChannel)
list += (getVideoTitle ? `${videoListSeperator}` : "") + videoChannelArr[i];
if (getVideoURL)
list += (getVideoTitle || getVideoChannel ? `${videoListSeperator}` : "") + videoURLArr[i];
list += "\n";
}
document.querySelector("#listDisplayContainer > textarea").value = list;
}
function CreateElement(tag, {
attributes = {},
properties = {},
styles = {},
events = {},
children = []
} = {}) {
const el = document.createElement(tag);
// Set attributes (id, type, value, etc.)
for (const [attr, value] of Object.entries(attributes)) {
el.setAttribute(attr, value);
}
// Set direct properties (like textContent, checked, disabled)
for (const [prop, value] of Object.entries(properties)) {
el[prop] = value;
}
// Apply styles
for (const [key, value] of Object.entries(styles)) {
el.style[key] = value;
}
// Add event listeners
for (const [event, handler] of Object.entries(events)) {
el.addEventListener(event, handler);
}
// Append children
for (const child of children) {
el.appendChild(
typeof child === 'string' ? document.createTextNode(child) : child
);
}
return el;
}
function CreateListItemWithCheckbox(labelText, checkboxId, checked, functionOnChange) {
return CreateElement("li", {
children: [
CreateElement("label", {
children: [
CreateElement("input", {
attributes: { type: "checkbox", id: checkboxId, name: checkboxId, value: checkboxId },
properties: { checked: checked },
events: {
change: functionOnChange ? functionOnChange : () => { }
}
}),
labelText
]
})
]
});
}
function CreatePopup(message) {
const popup = CreateElement("div", {
attributes: { id: "yt-pl-export-loading-popup" },
children: [
CreateElement("p", {
attributes: { class: "yt-pl-export-loading-popup-message" },
properties: { textContent: message }
})
]
});
document.body.appendChild(popup);
// Return an object that can be used to close the popup later
return {
close: closePopup
};
// Function to close the popup
function closePopup() {
document.body.removeChild(popup);
}
}
})();