Risorse
index.html
<!DOCTYPE html>
<!--
=================================================================
RICERCE DI SU YOUTUBE CON jQuery e AJAX
=================================================================
Questo esercizio utilizza le Youtube Data API v3 per effettuare
ricerche di video su youtube.
In particolare viene utilizzato il servizio
"https://www.googleapis.com/youtube/v3/search" che, appunto,
consente di avere una lista di info sui video trovati in formato
JSON.
Il servizio, così come viene utilizzato nell'esercizio non
richiede alcun tipo di autenticazione. Per utilizzarlo
è, tuttavia, necessario registrarsi presso
https://developers.google.com ed ottenere una "api key" che va
passata come parametro obbligatorio ad ogni chiamata del
servizio stesso.
Nota bene: la "key" utilizzata nell'esercizio è da considerarsi
provvisoria, perché potrebbe essere in futuro eliminata o
limitata nell'usa.
L'esercizio è composta da:
- index.html
Questo file che contiene l'interfaccia per utilizzare il
servizio: un campo di testo in cui inserire la stringa da
cercare, un elemento select per scegliere se la stringa
è da utilizzare come frase da ricercare tra i metadati dei
video o come id di un canale, il pulsante di invio, i
pulsanti per gestire la paginazione dei risultati.
- js/YoutubeSearch.js
Dove viene definita la classe YoutubeSearch che si occupa
dell'interfaccia con il servizio Google
- js/main.js
Dove viene implementata la richiesta e l'impaginazione dei
dati
- css/loader.css
Dove viene definito un CSS spinner utilizzato come
animazione di attesa durante il caricamento dei dati.
Per ulteriori informazioni sul servizio Google e per testarlo
interattivamente:
https://developers.google.com/apis-explorer/?hl=it#p/youtube/v3/youtube.search.list
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="bower_components/bootstrap-4.1.1-dist/css/bootstrap.css">
<link rel="stylesheet" href="css/loader.css">
<link rel="stylesheet" href="bower_components/fancybox/dist/jquery.fancybox.min.css">
</head>
<body>
<div class="jumbotron jumbotron-fluid bg-dark text-light py-4">
<div class="container">
<h1 class="display-4">Cerca su Youtube</h1>
</div>
</div>
<div class="container py-4">
<div class="row">
<div class="col-md">
<!-- Immissione stringa da ricercare -->
<input type="search" id="query" class="form-control form-control-lg mb-3" placeholder="frase o id canale" />
</div>
<div class="col-md-auto">
<!-- La stringa inserita è una frase da ricercare o l'ID di un canale Youtube? -->
<select id="query-type" class="form-control form-control-lg">
<option value="1">query</option>
<option value="2">id canale</option>
</select>
</div>
<div class="col-md-auto">
<!-- Bottone di invio -->
<button id="search-btn" class="btn btn-danger btn-lg">Cerca</button>
</div>
</div>
<h1 class="text-center my-4">Risultati</h1>
<!-- Elemento in cui verranno inseriti i risultati -->
<div class="row" id="results"></div>
<!-- Bottoni per navigare tra le pagine dei risultati -->
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a id="prev" class="page-link" href="#" tabindex="-1">« prev</a>
</li>
<li class="page-item disabled">
<a id="next" class="page-link" href="#">succ »</a>
</li>
</ul>
</nav>
</div>
<script src="bower_components/jquery-3.3.1.min/index.js"></script>
<script src="bower_components/fancybox/dist/jquery.fancybox.min.js"></script>
<script src="bower_components/bootstrap-4.1.1-dist/js/bootstrap.bundle.min.js"></script>
<script src="js/YoutubeSearch.js"></script>
<script src="js/main.js"></script>
</body>
</html>
css/loader.css
#results {
min-height: 200px;
}
.loader {
color: rgba(0,0,0,0.6);
font-size: 20px;
margin: 100px auto;
width: 1em;
height: 1em;
border-radius: 50%;
position: relative;
text-indent: -9999em;
-webkit-animation: load4 1.3s infinite linear;
animation: load4 1.3s infinite linear;
-webkit-transform: translateZ(0);
transform: translateZ(0);
margin: auto;
}
@-webkit-keyframes load4 {
0%,
100% {
-webkit-box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
}
12.5% {
-webkit-box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
25% {
-webkit-box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
37.5% {
-webkit-box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em,
0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em,
0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
50% {
-webkit-box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em,
0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em,
0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
62.5% {
-webkit-box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
}
75% {
-webkit-box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em,
2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em,
-2em -2em 0 0;
box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em,
2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em,
-2em -2em 0 0;
}
87.5% {
-webkit-box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
}
}
@keyframes load4 {
0%,
100% {
-webkit-box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
}
12.5% {
-webkit-box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
25% {
-webkit-box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
37.5% {
-webkit-box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em,
0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em,
0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
50% {
-webkit-box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em,
0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em,
0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
62.5% {
-webkit-box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
}
75% {
-webkit-box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em,
2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em,
-2em -2em 0 0;
box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em,
2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em,
-2em -2em 0 0;
}
87.5% {
-webkit-box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
}
}
js/YoutubeSearch.js
/*
=============================================================
CLASSE YoutubeSearch
=============================================================
Interfaccia semplificata con il servizio di ricerca
dei video su Yuotube. Usa jQuery.
USO:
var youtubesearch = new YoutubeSearch(opitons)
dove options è un 'plain object' attraverso il quale
è possibile configurate tutte le proprietà dell'oggetto.
Proprietà minime che è necassario istanziare:
apikey: String
Deve contenere la key per l'acesso alle Youtube Data Api v3
da registrare presso developers.google.com
done: Function (items, textStatus, jqXHR)
Funzione di callback che viene eseguita quando viene
ricevuta la risposta dal servizio e che passa un array
contenete le informazioni sui video trovati.
=============================================================
*/
// Constructor: crea un oggetto YoutubeSearch e lo inizializza
var YoutubeSearch = function (options) {
// Variabile temporanea
var opt = {};
// Metto in opt il risultato del merging tra le proprietà
// contenute nella proprietà defaults (definita in
// prototype) e quelle passate in options.
// opt conterrà tutte le proprietà contenute in defaults
// aggiornate con i valori inseriti in options
$.extend(opt, this.defaults, options);
// Copio le proprietà in opt nell'oggetto
for (var prop in opt) {
this[prop] = opt[prop];
}
}
// Definizione del prototype dell'oggetto
YoutubeSearch.prototype = {
// Proprietà con valori di defaults
// Tutte queste proprietà, modificate nei valori dalle
// impostazioni contenute in options vengono copiate
// come proprietà dell'oggetto YoutubeSearch
defaults : {
// Url da richiamare per ottenere il servizio
service: "https://www.googleapis.com/youtube/v3/search",
// Frase di ricerca o id del canale di cui recuperare i contenuti
query: "",
// query viene usata come fras da cercare nei dati dei video o
// come id del canale di cui elencare i contenuti?
queryType: "video",
// Risultati per pagina
maxResults: 12,
// Key registrata su developers.google.com
apikey: "",
// Funzione da eseguire in caso di errore
fail: null,
// Funzione da eseguire quando i dati vengono recuperati senza errori
done: null,
// Funzione da eseguire sempre
always: null,
// Ordine in cui vano esposti i dati (date|relevance|title|viewCount)
order: 'date'
},
// Token che punta alla pagina successiva della ricerca (se esiste)
nextPageToken: '',
// Token che punta alla pagina precedente della ricerca (se esiste)
prevPageToken: '',
// Proprietà da impostare se si vuole richiedere una pagina di una ricerca
// in corso invece di una nuova ricerca da capo
pageToken:'',
// Metodo che costruisce l'oggetto con cui vengono passati i dati al servizio
getParam: function() {
// Valori di base
var data = {
key: this.apikey,
part: 'snippet,id',
type: 'video',
maxResults: this.maxResults,
order: this.order
};
if (this.queryType == 'video') {
// SE si ricerca una frase nei dati
data.q = this.query;
} else {
// SE si ricerca un canale tramite l'ID
data.channelId = this.query;
}
// Se si sta chiedendo un'altra pagina per la ricerca attiva
if(this.pageToken)
data.pageToken = this.pageToken;
return data;
},
// Gestione richiesta AJAX
getRequest: function () {
// Salvo il valore di this nella variabile self
var self = this;
// Lancio la richiesta con $.getJSON utilizzando l'url salvata in service
// e i dati costruiti con getParam
$.getJSON(self.service, self.getParam())
.fail(function (jqXHR, textStatus,errorThrown) {
// Gestione fallimento: controllo se la proprietà fail è impostata
if (self.fail)
// se sì lancio la funzione definita dall'utente
self.fail(jqXHR, textStatus,errorThrown)
else
// altrimenti lancio un errore Javascript
throw(textStatus);
})
.done (function (data, textStatus, jqXHR) {
// Salvo gli eventuali token che puntano alla successiva
// e alla pagina precedente nelle relative proprietà
self.nextPageToken = data.nextPageToken;
self.prevPageToken = data.prevPageToken;
if(self.done)
// Lancio la funzione di callback definita passando l'array
// dei dati dei video trovati
self.done(data.items, textStatus, jqXHR);
else
// SE la funzione done non è stata definita lancio un errore
throw ('Non è stato definito alcuna fubzione di callback.')
})
.always(function (data, textStatus, jqHXR) {
// Se la funzione 'always' è stata definita la lancio
if(self.always)
self.always(data, textStatus, jqHXR);
})
},
// Inizio una nuova ricerca
search: function (query, queryType) {
// se il parametro query è impostato lo salvo nella proprietà relativa
if (query)
this.query = query;
// se il parametro queryType è impostato lo salvo nella proprietà relativa
if (queryType)
this.queryType = queryType;
// Cancello pageToken perché eseguo una ricerca capo
this.pageToken = '';
// lancio getRequest
this.getRequest();
},
// Vado alla pagina successiva di una ricerca attiva
goNext: function() {
// Se nextPageToken è impostato lo copio in pageToken e richiamo getRequest
if (this.nextPageToken){
this.pageToken = this.nextPageToken;
this.getRequest();
}
},
goPrev: function () {
// Se prevtPageToken è impostato lo copio in pageToken e richiamo getRequest
if (this.prevPageToken) {
this.pageToken = this.prevPageToken;
this.getRequest();
}
}
}
js/main.js
$(document).ready(function () {
// Aggiungo tooltip al campo input#query prendendo il valore dall'attributo placeholder
$('#query').tooltip({
container: 'body',
trigger: 'focus',
placement: 'top',
title: function () {
return $(this).attr('placeholder');
}
});
// Formatta in un elemento HTML i dati contenuti in un elemento
// dell'array ottenuto dalla ricerca
function getOutput(item) {
// ID che serve a comporre l'url per la visualizzazione del video
// con fancybox
var videoID = item.id.videoId;
// Titolo del video
var title = item.snippet.title;
// url del thumbnail ad alta definizione del video
var thumb = item.snippet.thumbnails.high.url;
// Creo la div con le classi col-* che la rendono responsive
var $output = $('<div />').addClass('col-sm-6 col-md-4 col-lg-3 mb-3');
// Crea la miniatura del video
var $img = $('<img />').attr ({
src: thumb,
alt: title,
class: 'img-fluid mb-2'
});
// Inserisco il titolo
var $dida = $('<div />')
.addClass('text-center')
.html(title);
// Crea l'elemento <a> cliccando il quale vedrò il video
// L'attributo href contiene il link al video costruito grazie all'ID
// l'attributo data-fancybox fa in modo che il link sia gestito
// da fancybox
// Inserisco nell'elemento <a> sia limmagine che il titolo
var $a = $('<a />')
.attr({
href: "https://www.youtube.com/watch?v=" + videoID,
'data-fancybox': 'gallery'
}).append($img, $dida);
// Inserisco l'elemento <a> nel div creata prima e restituisco il risultato
return $output.append($a);
};
// Creazione oggeto YoutubeSearch che gestiche l'interfaccia con il servizio
var myYoutube = new YoutubeSearch({
apikey:'AIzaSyB2Vaw44AZOYSgItgbNYeo8QOInBbFB4W8'
});
// Gestione fallimento
myYoutube.fail = function(jqXHR, textStatus, erorrThrown ) {
if (erorrThrown) {
// Errore jQuery o Javascipt
alert("Errore: " + erorrThrown);
} else {
// Errore restituito dal server
var err = jqXHR.responseJSON;
if (err.error) {
alert('Errore ' + err.error.code + ", " + err.error.message);
}
}
};
// La richiesta è andata a buon fine
myYoutube.done = function (items, textStatus, jqXHR) {
// Per ogni elemento contenuto nell'aray Items
$.each(items, function (i,item) {
// Converto l'elemento nel corrispondente elemento HTML
var $video = getOutput(item);
// E lo aggiungo alla div#results
$('#results').append($video);
});
// Abilito/disabilito i bottone next e prev
// Se la proprietà nextPageToken è impostata abilito il bottone #next altrimenti lo disabilito
if (this.nextPageToken) {
$('#next').parent().removeClass('disabled');
} else {
$('#next').parent().addClass('disabled');
}
// Se la proprietà prevPageToken è impostata abilito il bottone #prev altrimenti lo disabilito
if (this.prevPageToken) {
$('#prev').parent().removeClass('disabled');
} else {
$('#prev').parent().addClass('disabled');
}
};
// In ogni caso elimino l'animazione di loading
myYoutube.always = function () {
$('.loader').remove();
}
// Gestione evento click per button#searh-btn
$('#search-btn').click(function (e) {
e.preventDefault();
// Estraggo il valore inserito in input#query
var q = $('#query').val();
// Estraggo il valore selezionato in select#query-type
var wich = $('#query-type').val();
// se q è impostato
if (q) {
// elimino il contenuto di div#results e attacco alla div l'animazione di loading
$('#results').html('').append('<div class="loader">Loading...</div>');
// Invoco il metodo search di myYoutube
myYoutube.search(q, wich == 1 ? 'video' : 'channel');
}
});
// Associo il metodo goNext al bottone #next
$('#next').click(function (e) {
e.preventDefault();
$('#results').html('').append('<div class="loader">Loading...</div>');
myYoutube.goNext();
});
// Associo il metodo goPrev al bottone #prev
$('#prev').click(function (e) {
e.preventDefault();
$('#results').html('').append('<div class="loader">Loading...</div>');
myYoutube.goPrev();
});
});