Skip to content

Instantly share code, notes, and snippets.

@FranciscoG
Last active December 13, 2023 18:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FranciscoG/1eb040ff2de25fba98fb6524cded27a7 to your computer and use it in GitHub Desktop.
Save FranciscoG/1eb040ff2de25fba98fb6524cded27a7 to your computer and use it in GitHub Desktop.
(function (turntable) {
const modal = `
<div class='playlistFeatures'>
<p>Deep-Cut to Queup exporter</p>
<button type="button" class='exportPlaylist'>Export Playlist: <span id="exporting-playlist"></span></button>
<hr />
<p>Experimental: Export all playlists at once</p>
<p>Warning: this can be slow and will lock up the UI while processing, do not do this if you are DJ-ing</p>
<button type="button" class='exportPlaylistAll'>Export All Playlists</button>
<div id="pluginTT-loading">
<div class="pluginTT-spinner"></div>
</div>
</div>
`;
const addStyles = () => {
const css = `
.pluggedTT {
position: absolute;
z-index: 10;
bottom: 5em;
padding: 0.5em;
border-radius: 0.5em;
background-color: rgba(255, 255, 255, 0.1);
color: #eee;
right: 5em;
}
.pluggedTT p {
margin-bottom: 5px;
}
.miniplayer .pluggedTT {
z-index: 1000;
background-color: rgba(0, 0, 0, 0.5);
right: 1em;
}
.pluggedTT input {
padding: 0.1em;
}
#pluginTT-loading {
display: none;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background-color: rgba(255,255,255,.2);
}
#pluginTT-loading.show {
display: flex;
}
.pluginTT-spinner {
display: inline-block;
width: 25px;
height: 25px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
}
`;
/**
* Insert css into <head>
* remove existing style tag to make it easier to develop
*/
const head = document.head || document.getElementsByTagName("head")[0];
const style = document.createElement("style");
const id = "tt-export-playlist";
style.id = id;
document.getElementById(id)?.remove();
head.appendChild(style);
style.appendChild(document.createTextNode(css));
};
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function loadScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.onload = resolve;
script.onerror = reject;
script.src = url;
document.head.appendChild(script);
});
}
function makeData(sc = false) {
const songs = turntable.playlist.fileids
.map((id) => turntable.playlist.songsByFid[id])
.filter((song) => {
if (!song?.sourceid) return false;
const isYT = song.source === "yt" || song.metadata.ytid;
const isSC = song.source === "sc" || song.metadata.scid;
if (sc && isSC) return true;
if (!sc && isYT) return true;
return false;
})
.map((song) => {
if (sc) {
return `https://api.soundcloud.com/tracks/${song.sourceid}`;
}
return `https://www.youtube.com/watch?v=${song.sourceid}`;
});
return songs.join("\n").trim();
}
function updateExportingPlaylist() {
document.querySelector("#exporting-playlist").innerHTML = turntable.playlist.activePlaylist;
}
function isPlayListElem(elem) {
return elem.classList.contains("option") && elem.classList.contains("playlist");
}
/**
*
* @param {Event} e
*/
function onPlaylistClick(e) {
if (
isPlayListElem(e.target) ||
isPlayListElem(e.target.parentElement) ||
isPlayListElem(e.target.parentElement.parentElement) ||
isPlayListElem(e.target.parentElement.parentElement.parentElement)
) {
setTimeout(() => {
updateExportingPlaylist();
}, 1000);
}
}
function switchPlaylist(name) {
return new Promise((resolve) => {
turntable.playlist.switchPlaylist(name).done(() => {
turntable.playlist.setActivePlaylist(name);
turntable.playlist.loadList().done(resolve);
});
});
}
const runTurntableCode = () => {
let pluggedTTContainer = document.querySelector(".pluggedTT");
// remove old existing modal if there is one
if (pluggedTTContainer?.parentElement) {
pluggedTTContainer.parentElement.removeChild(pluggedTTContainer);
}
// re-create the modal
pluggedTTContainer = document.createElement("div");
pluggedTTContainer.classList.add("pluggedTT");
pluggedTTContainer.innerHTML = modal;
document.querySelector(".room-container").appendChild(pluggedTTContainer);
// set the current playlist name in the UI and also listen for playlist changes
// so that we can update the UI to show current playlist name
updateExportingPlaylist(pluggedTTContainer);
document.getElementById("queue-view").addEventListener("click", onPlaylistClick);
const loading = document.querySelector("#pluginTT-loading");
// export SINGLE playlist
// this will generate a zip file containing two text files
// one for soundcloud and one for youtube
const exportPlaylistButton = pluggedTTContainer.querySelector(".exportPlaylist");
if (exportPlaylistButton) {
exportPlaylistButton.addEventListener("click", async () => {
loading.classList.add("show");
const zip = new JSZip();
const playlistName = turntable.playlist.activePlaylist;
const scData = makeData(true);
if (scData) zip.file(`playlist-${playlistName}-sc.txt`, scData);
const ytData = makeData(false);
if (ytData) zip.file(`playlist-${playlistName}-yt.txt`, ytData);
const content = await zip.generateAsync({ type: "blob" });
saveAs(content, `deepcut-fm-playlist-${playlistName}.zip`);
loading.classList.remove("show");
});
}
// export ALL playlists
// this could be slow depending on how many playlists and how large they are.
// It will lock up the UI while processing so user should not be DJ-ing and
// won't be able to chat. I purposedly included a 1 second delay between each
// playlist to not overload the api
const exportPlaylistAll = pluggedTTContainer.querySelector(".exportPlaylistAll");
if (exportPlaylistAll) {
exportPlaylistAll.addEventListener("click", async () => {
loading.classList.add("show");
const zip = new JSZip();
for (const playlistName of turntable.playlist.playlists) {
await switchPlaylist(playlistName);
const scData = makeData(true);
if (scData) zip.file(`playlist-${playlistName}-sc.txt`, scData);
const ytData = makeData(false);
if (ytData) zip.file(`playlist-${playlistName}-yt.txt`, ytData);
await sleep(1000);
}
const content = await zip.generateAsync({ type: "blob" });
saveAs(content, "deepcut-fm-all-playlists.zip");
loading.classList.remove("show");
});
}
};
const init = async () => {
addStyles();
const oldRequire = window.require;
const oldDefine = window.define
window.require = null;
window.define = null;
await loadScript("https://www.unpkg.com/jszip@3.10.1/dist/jszip.min.js");
await loadScript("https://unpkg.com/file-saver@2.0.5/dist/FileSaver.min.js");
window.require = oldRequire;
window.define = oldDefine;
const waitForTurntable = () => {
if ("turntable" in window) {
runTurntableCode();
} else {
console.log("waiting for window.turntable");
setTimeout(waitForTurntable, 500);
}
};
waitForTurntable();
};
init();
})(window.turntable);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment