|
|
// 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); |
|
|
// }
|
|
|
|