From ee487c5fd7865e9e4d9640e4ee55d5c73cd871c4 Mon Sep 17 00:00:00 2001 From: tfl Date: Wed, 12 Oct 2022 17:53:29 +0200 Subject: [PATCH] Added search function using YT data API. Fixed slider and scrollbar on web kit. Minor changes --- README.md | 5 +- css/main.css | 186 +++++++++++++++++++++++++++++++-------------------- index.html | 33 ++++++++- js/main.js | 107 +++++++++++++++++++++++++++-- 4 files changed, 249 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 28e0bb0..94880a9 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,15 @@ It's a simple way to play music from Youtube and smoothly fade between tracks th ## What's next? ### Maybe one day -- Search videos straight from the app (needs API key and calls) +- ~~Search videos straight from the app (needs API key and calls)~~ - Compatibility with Bandcamp music (not sure if it can be done without too much hacks) +- A playlist of to-be-played tracks that can be added manually ("add url") or through the search +- A history of played tracks? ### Should be done quickly - Clean code (especially old P5JS) - Sanitize it (naming, organization, comments...) - Add current tracks/playlists as hashes in the URL so we can share track combinations +- Better indication of the video current status (playing or paused),maybe using `outline` on the iframe, or changing the `box-shadow color`? ## External resources and inspiration ### Inspiration diff --git a/css/main.css b/css/main.css index cd68970..72c26ac 100644 --- a/css/main.css +++ b/css/main.css @@ -1,5 +1,5 @@ :root { - --bg-color: #223; + --bg-color: 34, 34, 51; /* #223 */ --text-color: #eee; --text-color-light: #bbb; --accent-color-1: #ff91d8; @@ -9,7 +9,6 @@ --crossfader-thumb-width: 30px; --crossfader-thumb-height: 60px; --crossfader-thumb-radius: 15px; - } @font-face { @@ -79,6 +78,11 @@ textarea { resize: vertical; } +a, a:visited { + color: inherit; + text-decoration: none; +} + #bg { z-index: -2; overflow: hidden; @@ -105,8 +109,7 @@ textarea { #players, #search-area { display: flex; justify-content: center; - padding-bottom: 2em; - gap: 15px; + /* gap: 15px; */ } .hidden { @@ -142,15 +145,20 @@ iframe { /* right: 0; */ } +input[type=button], button { + cursor: pointer; + border: none; + background-color: var(--accent-color-2); + color: rgb(var(--bg-color)); + border-radius: 5px; + font-weight: 900; + transition: transform 0.2s ease; +} + +input[type=button]:hover, button:hover { + transform: scale(1.1); +} .load-button { - /* flex-grow: 2; */ - cursor: pointer; - border: none; - background-color: var(--accent-color-2); - color: var(--bg-color); - font-weight: 900; - border-radius: 5px; - /* padding: 2em; */ width: 50%; } @@ -244,6 +252,8 @@ input[type=range]:focus { input[type=range]::-webkit-slider-runnable-track { background: var(--accent-color-1); border-radius: 25px; + margin-bottom: 0.5em; + height: 4px; width: 100%; cursor: pointer; } @@ -251,12 +261,13 @@ input[type=range]::-webkit-slider-thumb { background: var(--accent-color-2); border: solid var(--accent-color-1-light); border-width: 0px; + margin-top: -0.4em; + width: 1em; + height: 1em; + border-radius: 1em; cursor: pointer; -webkit-appearance: none; } -input[type=range]:focus::-webkit-slider-runnable-track { - background: var(--accent-color-1-light); -} input[type=range]::-moz-range-track { background: var(--accent-color-1); width: 100%; @@ -265,39 +276,10 @@ input[type=range]::-moz-range-track { } input[type=range]::-moz-range-thumb { background: var(--accent-color-2); - /* border: solid var(--accent-color-1-light); */ cursor: pointer; border-width: 0px; border-radius: var(--crossfader-thumb-radius); } -input[type=range]::-ms-track { - background: transparent; - border-color: transparent; - color: transparent; - width: 100%; - cursor: pointer; -} -input[type=range]::-ms-fill-lower { - background: var(--accent-color-1); - border-radius: 25px; -} -input[type=range]::-ms-fill-upper { - background: var(--accent-color-1); - border-radius: 25px; -} -input[type=range]::-ms-thumb { - background: var(--accent-color-2); - /* border: solid var(--accent-color-1-light); */ - cursor: pointer; - margin-top: 0px; - /*Needed to keep the Edge thumb centred*/ -} -input[type=range]:focus::-ms-fill-lower { - background: var(--accent-color-1); -} -input[type=range]:focus::-ms-fill-upper { - background: var(--accent-color-1-light); -} /*------------------------*/ input[type=range]#crossfader::-webkit-slider-runnable-track { @@ -307,12 +289,11 @@ input[type=range]#crossfader::-webkit-slider-thumb { margin-top: -20.65px; width: var(--crossfader-thumb-width); height: var(--crossfader-thumb-height); - /* border-width: 5px; - border-radius: 12px; */ + border-radius: var(--crossfader-thumb-radius); } input[type=range]#crossfader::-moz-range-track { - /* border-radius: 25px; */ + border-radius: var(--crossfader-thumb-radius); width: 100%; height: 13.3px; cursor: pointer; @@ -320,39 +301,100 @@ input[type=range]#crossfader::-moz-range-track { input[type=range]#crossfader::-moz-range-thumb { width: var(--crossfader-thumb-width); height: var(--crossfader-thumb-height); - /* border-width: 5px; */ - /* border-radius: 12px; */ + border-radius: var(--crossfader-thumb-radius); cursor: pointer; } -input[type=range]#crossfader::-ms-track { - /* border-width: 18.75px 0; */ - height: 13.3px; + +.search-wrapper { + /* margin-bottom: 2em; */ } -input[type=range]#crossfader::-ms-fill-lower { - /* border-width: 2.3px; - border-radius: 50px; */ -} -input[type=range]#crossfader::-ms-fill-upper { - /* border-radius: 50px; */ -} -input[type=range]#crossfader::-ms-thumb { - width: var(--crossfader-thumb-width); - height: var(--crossfader-thumb-height); - /* border-width: 5px; - border-radius: 12px; */ - cursor: pointer; - margin-top: 0px; - /*Needed to keep the Edge thumb centred*/ +.search-bar-wrapper { + display: flex; + justify-content: center; + align-items: center; + height: 2em; + margin-bottom: 1em; } +#search-bar { + display: flex; + column-gap: 1em; +} +#search-bar input, #search-bar button { + height: 2em;; +} +#search-button { + padding: 0.2em 1em; +} + +#search-results { + display: flex; + overflow: auto; + width: 95vw; + /* flex-direction: column; */ + column-gap: 1em; + /* padding-bottom: 1em; */ + + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.8) rgba(0,0,0,0); + border-radius: 6px; +} + +#search-results::-webkit-scrollbar { + height: 9px; +} +#search-results::-webkit-scrollbar-track { + background: rgba(0,0,0,0); +} +#search-results::-webkit-scrollbar-thumb { + background-color: rgba(255,255,255,0.8); + width: 9px; +} + +.search-item { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: column; + position: relative; +} + +.search-item-info { + position: relative; +} + +.search-item-title { + position: absolute; + top: 0; + margin: 0.5em; +} + +.search-item-thumbnail img { + /* width: 80%; */ +} +.search-item-title span { + display: inline; + background-color: rgba(var(--bg-color), 0.8); + padding: 0.3em 0; + line-height: 1.8em; +} + +.search-item-controls { + display: flex; + /* justify-content: center; */ + column-gap: 1em; + margin: 0.5em; + position: absolute; + bottom: 0; +} + +.load-to-A, .load-to-B { + padding: 0.5em; +} .footer { font-weight: 100; - padding-top: 2em; -} -.footer a, .footer a:visited { - color: var(--text-color); - text-decoration: none; + /* padding-top: 2em; */ } .footer span { diff --git a/index.html b/index.html index 5d8bf32..1383c49 100644 --- a/index.html +++ b/index.html @@ -84,24 +84,53 @@ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+ + +
+
+ +
+
+

🔊 Mix like a pro youtube DJ! 🎧

+ - + diff --git a/js/main.js b/js/main.js index 4e76a01..3db5e1d 100644 --- a/js/main.js +++ b/js/main.js @@ -116,6 +116,10 @@ class player { 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]; @@ -201,9 +205,9 @@ function get_playlist_id(url) { let regPlaylist = /[?&]list=([^#\&\?]+)/; let regIndex = /[?&]index=([^#]+)/; let match = url.match(regPlaylist); - console.log('match ' + match); + // console.log('match ' + match); let matchIndex = url.match(regIndex); - console.log('matchindex ' + matchIndex); + // console.log('matchindex ' + matchIndex); // console.log('regexp ' + match.match(/=LL([^#\&\?]*)/)); let index = 0; if (matchIndex != null) { @@ -313,8 +317,6 @@ function newShootingStar() { 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'); @@ -346,8 +348,6 @@ document.getElementById('shooting-stars-enable').addEventListener('click', () => } }); - - // Enable/disable shooting stars (with P5JS background) // if (typeof processingSketch !== 'undefined') { // let separatorSpan = document.createElement('span'); @@ -371,7 +371,6 @@ document.getElementById('shooting-stars-enable').addEventListener('click', () => - EMAIL OBFUSCATION - found here http://www.grall.name/posts/1/antiSpam-emailAddressObfuscation.html -------------------------------------------------------*/ document.getElementById('contact-me').addEventListener('click', (e) => { - console.log('okay'); var y = decode("pbagnpg@gsypy.klm"); //https://rot13.com/ e.target.setAttribute("href", "mailto:" + y); }) @@ -383,3 +382,97 @@ function decode(a) { 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')); +displaySearchResults(results);