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">&laquo; prev</a>
              </li>
              <li class="page-item disabled">
                <a id="next" class="page-link" href="#">succ &raquo;</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();
    });


});

 

Torna all'inizio