Mix Youtube videos like a true DJ! https://dj.tflcl.xyz
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

481 lines
16 KiB

// Check if client is using a mobile phone
const getMobileOS = () => {
const ua = navigator.userAgent
if (/android/i.test(ua)) {
return "Android"
}
else if (/iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) {
return "iOS"
}
return "Other"
}
const isRunningOn = getMobileOS();
if (isRunningOn != 'Other') {
alert('Whoop, it seems that you attempt to access this website on a ' + isRunningOn + ' mobile/tablet, which might not work properly. You should better use a computer instead.');
}
// Loads the Youtube IFrame Player API code asynchronously.
let tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
let firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
//THE DJ CROSSFADER!!!
let crossfader = document.querySelector('#crossfader');
class player {
trackID;
player;
crossfaderCoef;
constructor(id) {
this.id = id;
this.iframe = document.querySelector('#'+this.id);
this.node = this.iframe.parentNode;
this.volume = this.node.querySelector('.volume-slider').value;
this.volSliderVal = this.node.querySelector('.volume-slider-value');
this.volVal = this.node.querySelector('.volume-value')
this.volSliderVal.innerHTML = this.volume + '%';
this.loadButton = this.node.querySelector('.load-button');
this.speedSlider = this.node.querySelector('.speed-slider');
this.speedSlider.nextElementSibling.querySelector('.speed-value').innerHTML = this.speedSlider.value;
}
init(trackID) {
this.trackID = trackID;
let src = `https://www.youtube-nocookie.com/embed/${trackID}?disablekb=1&enablejsapi=1&playsinline=1&autoplay=0&rel=0&modestbranding=1&iv_load_policy=3&version=3`;
this.iframe.setAttribute('src', src);
this.player = new YT.Player(this.id, {
events: {
'onReady': this.onPlayerReady.bind(this),
'onStateChange': this.onPlayerStateChange.bind(this),
'onError': this.onPlayerError.bind(this)
}
});
}
onPlayerReady(e) {
this.player.setVolume(this.volume);
this.node.querySelector('.volume-slider').addEventListener('input', (e) => {
this.volume = e.target.value;
this.volSliderVal.innerHTML = this.volume + '%';
this.volumeUpdate();
});
this.loadButton.addEventListener('click', () => {
let url = prompt('Enter a YouTube URL (track or playlist):', 'https://www.youtube.com/watch?v=' + this.trackID);
if (url) {
let resp = check_url(url);
if (resp < 0) {
alert('Wrong URL...');
console.log('Nothing to load!');
} else if (resp == 0) {
let newTrackID = get_video_id(url);
let newPlaylistID = get_playlist_id(url);
if (newPlaylistID[0] != 0) {
console.log('playlistid to load: ' + newPlaylistID);
this.loadPlaylist(newPlaylistID);
} else if (newTrackID != 0) {
console.log('track to load: ' + newTrackID);
this.load(newTrackID);
} else {
alert('Wrong URL...');
console.log('Nothing to load!');
}
} else if (resp == 1) {
// fetch_bandcamp_url(url);
alert('Wrong URL...');
console.log('Nothing to load!');
}
}
});
this.speedSlider.addEventListener('input', (e) => {
this.speedSlider.nextElementSibling.querySelector('.speed-value').innerHTML = e.target.value;
this.player.setPlaybackRate(Number(e.target.value));
});
this.volumeUpdate();
}
onPlayerStateChange(e) {
console.log(this.id + ' state change: ' + e.data);
}
onPlayerError(e) {
console.log(this.id + ' - Error: ' + e.data);
}
load(trackID) {
this.player.loadVideoById(trackID);
}
cue(trackID) {
this.player.cueVideoById(trackID);
}
loadPlaylist(playlistID) {
//First element should be the playlist ID, second element the index of the video to play
let list = playlistID[0];
let index = playlistID[1];
this.player.loadPlaylist({
list: list,
listType: 'playlist',
index: index
});
}
volumeUpdate() {
let outputVolume = Math.round(this.volume * this.crossfaderCoef);
this.player.setVolume(outputVolume);
this.volVal.innerHTML = outputVolume + '%';
let boxShadowStyle = '0 0 3em #ddbddd'
this.iframe.style.boxShadow = boxShadowStyle + componentToHex(Math.round(this.crossfaderCoef*255));
}
}
let playerA = new player('playerA');
let playerB = new player('playerB');
function onYouTubeIframeAPIReady() {
playerA.init('anWaxGNDRJ4')
playerB.init('PhEuAuhH418');
}
calcCrossfaderCoefs(crossfader.value);
crossfader.addEventListener('input', (e) => {
//Makes the slider stick on the middle
if (Math.abs(crossfader.value - 500) < 20) {
crossfader.value = 500;
}
calcCrossfaderCoefs(crossfader.value);
playerA.volumeUpdate();
playerB.volumeUpdate();
// updateCrossfader(crossfader.value);
});
function calcCrossfaderCoefs(input) {
let normVal = input / 1000;
//Classic 'transition' curve
let coefB = Math.min(normVal*2, 1);
let coefA = Math.min((1-normVal)*2, 1);
//Cheap smooth curve
// let coefB = normVal ** 2;
// let coefA = (1 - normVal) ** 2;
//Amplitude-preserving curve (but not power)
// let coefB = (normVal ** 2) * (3 - 2 * normVal);
// let coefA = 1 - coefB;
playerA.crossfaderCoef = coefA;
playerB.crossfaderCoef = coefB;
}
function check_url(url) {
let test = url.indexOf('youtu');
if (test > -1 && test < 13) {
return 0; //It's most likely a youtube link
} else if (url.indexOf('.bandcamp') > -1 || url.indexOf('/album/') > -1 || url.indexOf('/track/') > -1) {
return 1; //It's most likely a bandcamp link
} else {
return -1; //Wrong link
}
}
// From https://stackoverflow.com/a/55616161/14182148
function get_playlist_id(url) {
let VID_REGEX = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
let regPlaylist = /[?&]list=([^#\&\?]+)/;
let regIndex = /[?&]index=([^#]+)/;
let match = url.match(regPlaylist);
// console.log('match ' + match);
let matchIndex = url.match(regIndex);
// console.log('matchindex ' + matchIndex);
// console.log('regexp ' + match.match(/=LL([^#\&\?]*)/));
let index = 0;
if (matchIndex != null) {
index = matchIndex[1] - 1;
}
if (match != null && match[1].lenght > 2) { // lenght > 2 prevents from trying to load a "list=LL" (liked videos of signed-in user)
return [match[1], index];
} else {
return [0,0];
}
}
function get_video_id(url) { //originally 'video_id_from_playlist'
let VID_REGEX = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
let video_id = url.match(VID_REGEX);
if (video_id != null) {
return video_id[2];
} else {
return 0;
}
}
function fetch_bandcamp_url(url) {
let iframe = document.createElement('iframe');
iframe.setAttribute('class','hidden');
iframe.setAttribute('src',url);
playerB.node.parentNode.appendChild(iframe);
console.log(iframe);
let scrap = iframe.contentWindow.document.querySelector('script[data-tralbum]');
console.log('scrap: ' + scrap);
}
// Thumbnails: https://i1.ytimg.com/vi/{trackID}/mqdefault.jpg
// Converts values from 0-255 to 00-ff
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
// -----------------------------------
// BANDCAMP Tests
// -----------------------------------
// let surl = 'https://bandcamp.com/api/bcsearch_public_api/1/autocomplete_elastic';
// let req = {"search_text": "techno",
// "search_filter": "",
// "full_page": false,
// "fan_id": null
// };
// let bcURL = 'https://clett.bandcamp.com/track/placenta';
// let request = new Request(bcURL);
// let headers = new Headers({
// 'X-Requested-With': 'com.bandcamp.android',
// 'Content-Type': 'application/json',
// 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9; Unknown Device)',
// 'Host': 'bandcamp.com',
// });
//
// let data;
// fetch(bcURL, {
// method: 'GET',
// headers: headers
// })
// .then(response => response.json())
// .then(data => console.log('data: ' + data));
// console.log('response: ' + data);
// console.log('data: ' + data);
// let html = async await fetch(bcURL).text(); // html as text
// let doc = new DOMParser().parseFromString(html, 'text/html');
// doc.title; doc.body;
// let Http = new XMLHttpRequest();
// Http.open("GET", bcURL);
// Http.send();
//
// Http.onreadystatechange = (e) => {
// console.log(Http.responseText)
// }
// -----------------------------------
// BACKGROUND
// -----------------------------------
let areStarsShooting = true;
let shootingStarWrapper = document.getElementsByClassName('shooting-stars-wrapper')[0];
let shootingStar;
function newShootingStar() {
shootingStar = document.createElement('div');
let topStart = Math.floor(0.6 * window.innerHeight * Math.random());
let leftStart = Math.floor(window.innerWidth * (0.1 + 0.8 * Math.random()));
let duration = 1000 + Math.floor(Math.random() * 5000);
let distance = 200 + Math.floor(Math.random() * 500);
let angle = Math.floor(Math.random() * 30) - 15;
let direction = Math.random() < 0.5 ? -1 : 1;
shootingStar.style.top = topStart + 'px';
shootingStar.style.left = leftStart + 'px';
// root.style.setProperty('--shooting-star-duration', duration + "px");
// shootingStarCSS.insertRule(':root{--shooting-star-duration: '+ duration + 'ms}');
document.documentElement.style.setProperty('--shooting-star-duration', duration + "ms");
document.documentElement.style.setProperty('--shooting-star-translate', distance + "px");
document.documentElement.style.setProperty('--shooting-star-angle', angle + "deg");
document.documentElement.style.setProperty('--shooting-star-direction', direction);
shootingStarWrapper.appendChild(shootingStar);
shootingStar.classList.add('shooting-star');
setTimeout(() => {
shootingStar.remove();
// let nextIn = 10;
let nextIn = 7000 + Math.floor(Math.random() * 5000);
if (areStarsShooting) {
setTimeout(() => {
newShootingStar();
}, nextIn);
}
}, duration);
// setTimeout('newShootingStar()', duration + nextIn);
}
newShootingStar();
document.getElementById('shooting-stars-enable').innerHTML = '⭐Disable shooting stars⭐';
document.getElementById('shooting-stars-enable').addEventListener('click', () => {
if (areStarsShooting) {
document.getElementById('shooting-stars-enable').innerHTML = '⭐Enable shooting stars⭐';
areStarsShooting = false;
} else {
document.getElementById('shooting-stars-enable').innerHTML = '⭐Disable shooting stars⭐';
areStarsShooting = true;
newShootingStar();
}
});
// Enable/disable shooting stars (with P5JS background)
// if (typeof processingSketch !== 'undefined') {
// let separatorSpan = document.createElement('span');
// separatorSpan.innerHTML = ' - ';
// document.getElementById('shooting-stars-enable').after(separatorSpan);
// document.getElementById('shooting-stars-enable').innerHTML = 'Disable shooting stars⭐';
// }
// document.getElementById('shooting-stars-enable').addEventListener('click', () => {
// if (processingIsOn) {
// processingSketch.frameRate(0);
// document.getElementById('shooting-stars-enable').innerHTML = 'Enable shooting stars⭐';
// processingIsOn = false;
// } else {
// processingSketch.frameRate(24);
// document.getElementById('shooting-stars-enable').innerHTML = 'Disable shooting stars⭐';
// processingIsOn = true;
// }
// });
/*-------------------------------------------------------
- EMAIL OBFUSCATION - found here http://www.grall.name/posts/1/antiSpam-emailAddressObfuscation.html
-------------------------------------------------------*/
document.getElementById('contact-me').addEventListener('click', (e) => {
var y = decode("pbagnpg@gsypy.klm"); //https://rot13.com/
e.target.setAttribute("href", "mailto:" + y);
})
function decode(a) {
// ROT13 : a Caesar cipher
// letter -> letter' such that code(letter') = (code(letter) + 13) modulo 26
return a.replace(/[a-zA-Z]/g, function(c){
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
})
};
/**
SEARCH WITH YOUTUBE API
*/
let lastSearchQuery;
let results;
gapi.load("client", loadClient);
// https://developers.google.com/youtube/v3/docs/search/list
function loadClient() {
gapi.client.setApiKey("AIzaSyCuz5tGc9oDxw6RZnpibakYFHzMvsd8bUs");
return gapi.client.load("https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest")
.then(function() { console.log("GAPI client loaded for API"); },
function(err) { console.error("Error loading GAPI client for API", err); });
}
function execute(query) {
// Make sure the client is loaded before calling this method.
return gapi.client.youtube.search.list({
"part": [
"snippet"
],
"maxResults": 15,
"q": query,
"type": [
"video"
],
"fields": "items(id/videoId,snippet(title,thumbnails/medium))"
})
.then(function(response) {
if (response.status == 200) {
results = response.result;
localStorage.setItem('sres', JSON.stringify(results));
displaySearchResults(results);
}
// Handle the results here (response.result has the parsed body).
console.log("Response", response);
},
function(err) {
console.error("Execute error", err);
alert("Whoops, something went wrong. \n Maybe all of my daily Youtube Data API requests have been used. Try again later! \n Maybe it's something else. Try again later?");
});
}
const searchForm = document.getElementById('search-bar');
const searchQueryObj = document.getElementById('search-query');
const searchButtonObj = document.getElementById('search-button');
const searchResultsObj = document.getElementById('search-results');
//When the page has loaded, store the hidden search-item class div as a template for future searchs
const searchResultItemObj = searchResultsObj.querySelector('.search-item').cloneNode(true);
searchResultsObj.querySelector('.search-item').remove();
searchForm.addEventListener('submit', (e) => {
e.preventDefault(); // We handle the form submit by ourselve
if (searchQueryObj.value != lastSearchQuery) { // prevent useless requests
console.log('Search for: ' + searchQueryObj.value);
execute(searchQueryObj.value);
lastSearchQuery = searchQueryObj.value;
}
return false;
});
function displaySearchResults(res) {
// First we clean previous search results
while (searchResultsObj.firstChild) {
searchResultsObj.removeChild(searchResultsObj.firstChild);
}
res.items.forEach((item) => {
let itemElmt = searchResultItemObj.cloneNode(true);
itemElmt.querySelector('.search-item-info img').setAttribute('src', item.snippet.thumbnails.medium.url);
itemElmt.querySelector('.search-item-info span').innerHTML = item.snippet.title;
itemElmt.querySelector('.search-item-info a').setAttribute('href', 'https://www.youtube.com/watch?v=' + item.id.videoId)
itemElmt.dataset.id = item.id.videoId;
itemElmt.querySelector('.load-to-A').addEventListener('click', () => {
playerA.cue(item.id.videoId);
});
itemElmt.querySelector('.load-to-B').addEventListener('click', () => {
playerB.cue(item.id.videoId);
});
console.log(itemElmt);
searchResultsObj.appendChild(itemElmt);
});
}
// For testing purpose
// results = JSON.parse(localStorage.getItem('sres'));
// if (results) {
// displaySearchResults(results);
// }