tobsch / GoogleMusicEnhancer (GME)

// ==UserScript==
// @name            GoogleMusicEnhancer (GME)
// @version         0.7.2
// @namespace       http://www.tobsch.org/
// @author          Tobias Schneider
// @homepage        http://www.tobsch.org/
// @licence         http://www.opensource.org/licenses/mit-license.php
// @description     Improvements and better usability for google music
//
// @include         https://play.google.com/music/listen*
//
// @require         http://code.jquery.com/jquery-2.1.3.min.js
//
// @grant           GM_addStyle
// @grant           GM_xmlhttpRequest
// @grant           GM_setValue
// @grant           GM_getValue
// @grant           GM_deleteValue
// @grant           GM_info
//
// ==/UserScript==

GM_addStyle('.lyrics-clip {background-color: #FB8521;color: #FFFFFF;font-size: 16px;height: 25px;left: -63px;margin-top: -10px;position: absolute;text-align: center;top: 47px;transform: rotate(-90deg);transform-origin: 50% 50% 0;width: 100px;}.lyrics-clip:hover {cursor: pointer;}.lyrics-panel {color: #333333;background-color: white;border: 1px solid #D1D1D1;position: fixed;right: -370px;height: 70%;width: 350px;z-index: 1000000;padding: 10px;}.lyrics-header {font-size: 16px;position: relative;min-height: 30px;width: 100%;text-align: center;border-bottom: 1px solid #D1D1D1;}.lyrics-body {color: #707070;font-size: 13px;position: relative;height: 90%;width: 100%;margin-top: 15px;padding-left: 5px;overflow-y: auto;}#lyrics-panel.clicked {right: 0;transition: 0.5s;}#lyrics-panel.un-clicked {right: -370px;transition: 0.3s;}.lyrics-not-found-text {text-align: center;}.lyrics-not-found-link {text-align: center;}.space-top {margin-top: 10px;}.update-box {z-index: 102;border: 1px solid;border-radius: 5px;font-size: 1.1em;width: 400px;height: 400px;position: absolute;top: 15%;background-color: white;}.update-body {text-align: center;}.update-header {text-align: center;}.update-header h2 {margin: 20px auto;}');

$(window).load(function () {
    'use strict';

    var persist = new Persist();
    var build = new Build();

    new Update(persist, build)
        .registerUpdateButtonEvent($('.menu-logo'))
        .check(false);

    new LyricContainer($('#main'), build)
        .registerToggler()
        .registerEvents();

    new Lyric($('#lyrics-panel'), persist)
        .registerSongChangeListener('#playerSongInfo', '#player-song-title', '#player-artist');
});


function Build() {
    'use strict';

    return {
        div: function (options) {
            return merge($('<div></div>'), options);
        },
        link: function (options) {
            return merge($('<a></a>'), options);
        }
    };

    function merge($element, options) {
        if (typeof options === 'undefined') {
            return $element.clone();
        }

        if (options.text) {
            $element.html(options.text);
        }
        if (options.attr) {
            $element.attr(options.attr);
        }
        if (options.css) {
            $element.css(options.css);
        }
        return $element.clone();
    }
}


function Lyric($panel, persist) {
    'use strict';

    if (typeof $panel === 'undefined' || typeof persist === 'undefined') {
        throw 'Panel and/or persist object undefined';
    }

    var lastSearchParameter;
    var $lyricsPanel = $panel;
    var Persist = persist;

    return {
        search: function (parameter, strategy) {
            if (typeof parameter === 'undefined' || typeof strategy !== 'object') {
                throw 'Wrong parameters or strategy object';
            }

            findBy(strategy, parameter);

            return this;
        },

        registerSongChangeListener: function (changeListenerSelector, titleSelector, artistSelector) {
            var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
            var myObserver = new MutationObserver(mutationHandler);
            myObserver.observe(document.querySelector(changeListenerSelector), { childList: true });

            function mutationHandler() {
                var searchParameter = {artist: $(artistSelector).text(), title: $(titleSelector).text()};

                if (!lastSearchParameter || (lastSearchParameter && JSON.stringify(lastSearchParameter) !== JSON.stringify(searchParameter))) {
                    try {
                        findBy(new LyricsWiki(new Build()), searchParameter);
                        lastSearchParameter = searchParameter;
                    } catch (e) {
                        console.log('GME: ' + e.message);
                    }
                }
            }

            return this;
        }
    };

    function findBy(strategy, parameter) {
        if (typeof parameter === 'undefined' || !parameter.artist || !parameter.title || typeof strategy !== 'object') {
            throw 'Wrong parameters or strategy object';
        }

        $lyricsPanel.trigger('update', {artist: parameter.artist, title: parameter.title});

        var persistLyric = Persist.findBy('lyric:' + parameter.artist + '-' + parameter.title);
        if (persistLyric && persistLyric.lyric) {
            $lyricsPanel.trigger('update', {lyric: persistLyric.lyric});
        }
        else {
            $lyricsPanel.trigger('add-loading-overlay');

            strategy.execute(parameter.artist, parameter.title,
                function (lyric) {
                    Persist.persist('lyric:' + parameter.artist + '-' + parameter.title, {lyric: lyric});
                    $lyricsPanel.trigger('update', {lyric: lyric});
                },
                function ($object) {
                    $lyricsPanel.trigger('update', {lyric: $object});
                }
            );
        }
    }
}


function LyricContainer($parent, build) {
    'use strict';

    var $lyricsClip, $lyricsPanel, $lyricsHeader, $lyricsBody;
    var $overlayLoading;


    if (typeof build === 'undefined') {
        throw 'Build object undefined';
    }

    if (typeof $parent === 'undefined') {
        throw 'Please define a selector';
    }
    var Build = build;

    init($parent);

    return {
        registerToggler: function () {
            if ($lyricsClip) {
                $lyricsClip.on('click', function () {
                    $lyricsPanel.toggleClass('clicked un-clicked');
                });
            }

            return this;
        },

        registerEvents: function () {
            if ($lyricsPanel) {
                $lyricsPanel
                    .on('add-loading-overlay', function () {
                        if ($overlayLoading) {
                            $lyricsBody.append($overlayLoading);
                        }
                    })
                    .on('update', function (event, parameter) {
                        if (parameter) {
                            if (parameter.title && parameter.artist) {
                                $lyricsHeader.html(parameter.artist + ' - ' + parameter.title);
                            }
                            if (parameter.lyric) {
                                $lyricsBody.html(parameter.lyric);
                                $lyricsBody.scrollTop(0);
                            }
                        }
                    });
            }

            return this;
        }
    };

    function init(){
        $lyricsPanel = Build.div({attr: {class: 'lyrics-panel un-clicked', id: 'lyrics-panel'}});
        $lyricsClip = Build.div({attr: {class: 'lyrics-clip', id: 'lyrics-clip'}, text: 'Lyric'});
        $lyricsHeader = Build.div({attr: {class: 'lyrics-header', id: 'lyrics-header'}, text: 'Lyrics Panel'});
        $lyricsBody = Build.div({attr: {class: 'lyrics-body', id: 'lyrics-body'}, text: 'I can not hear a sound. Play something loud!' });

        $parent.append($lyricsPanel.append($lyricsClip).append($lyricsHeader).append($lyricsBody));

        $overlayLoading = $('<div id="loading-overlay" data-type="regular-loading-overlay"></div>');
    }
}


function LyricsWiki(build) {
    'use strict';

    var name = 'LyricsWiki';
    var baseLyricsWikiUrl = 'https://lyrics.wikia.com/api.php?action=lyrics&fmt=realjson';

    var baseGoogleUrl = 'http://www.google.com/';

    if (typeof build === 'undefined') {
        throw 'Build object undefined';
    }

    var Build = build;

    return {
        execute: function (artist, title, success, error) {
            if (typeof artist === 'undefined' || typeof title === 'undefined' ||
                typeof success !== 'function' || typeof error !== 'function') {
                throw 'Wrong or undefined arguments';
            }

            var songObject;
            get(prepareUrl(artist, title), function (response) {
                songObject = JSON.parse(response.responseText);

                if (songObject && songObject.lyrics !== 'Not found' && songObject.url) {
                    get(songObject.url, function (responseLyric) {
                        success(extractLyric(responseLyric));
                    });
                }
                else {
                    error(getErrorMessage(artist, title, songObject.url));
                }
            });
        }
    };

    function getErrorMessage(artist, title, url) {
        var googleSearchString = encodeURI(baseGoogleUrl + '?q=lyrics+"' + artist +
            ' ' + title + '"#q=lyrics+"' + artist + ' ' + title + '"');

        return $('<div></div>')
            .append(
                Build.div({text: '<b>Oh No. No lyric found.<b>', attr: {class: 'lyrics-not-found-text space-top'}})
            ).append(
                Build.div({text: 'Add one and make this experience a little bit better!',
                    attr: {class: 'lyrics-not-found-text'}})
            ).append(
                Build.div({text: '<b>First</b> search for the correct lyric. YippieYeah!',
                    attr: {class: 'lyrics-not-found-text space-top'}})
            ).append(
                Build.div({attr: {class: 'lyrics-not-found-link'}})
                    .append(Build.link({attr: {href: googleSearchString},
                        text: 'Search for \'' + artist + ' - ' + title + '\''}))
            ).append(
                Build.div({text: '<b>Then</b> add it to ' + name + '. Awesome!',
                    attr: {class: 'lyrics-not-found-text space-top'}})
            ).append(
                Build.div({attr: {class: 'lyrics-not-found-link'}})
                    .append(Build.link({attr: {href: url}, text: 'Add this great song!'}))
            ).append(
                Build.div({text: '<b>At Last</b> refresh to load the lyric you provided',
                    attr: { class: 'lyrics-not-found-text space-top'}})
            );
    }

    function get(url, callback) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: callback
        });
    }

    function extractLyric(response) {
        var lyricWithComment = $(response.responseText).find('.lyricbox').clone().children(':not(br)').remove().end().html();
        return lyricWithComment.substr(0, lyricWithComment.indexOf('<!--'));
    }

    function prepareUrl(artist, title) {
        return encodeURI(baseLyricsWikiUrl + '&artist=' + toTitleCase(artist) + '&song=' + toTitleCase(title));
    }

    function toTitleCase(str) {
        return str.replace(/\w\S*/g, function (txt) {
            return txt.charAt(0).toUpperCase() + txt.substr(1);
        });
    }
}


function Persist() {
    'use strict';

    var prefix = 'gme:';

    return {
        persist: function (key, value) {
            if (typeof key === 'undefined') {
                return false;
            }

            key = prefix + '' + key;
            return GM_setValue(key, JSON.stringify(value));
        },

        findBy: function (key) {
            if (typeof key === 'undefined') {
                return undefined;
            }

            key = prefix + '' + key;
            var storedObject = GM_getValue(key);
            if (storedObject) {
                try {
                    return JSON.parse(storedObject);
                } catch (e) {
                    return undefined;
                }
            }
            return undefined;
        },

        remove: function (key) {
            if (typeof key === 'undefined') {
                return undefined;
            }

            key = prefix + '' + key;
            return GM_deleteValue(key);
        }
    };
}


function Update(persist, build) {
    'use strict';

    if (typeof persist === 'undefined') {
        throw 'Persist object undefined';
    }
    if (typeof build === 'undefined') {
        throw 'Build object undefined';
    }

    var version = GM_info.script.version;
    var updateString = 'last-update';

    var name = 'GoogleMusicEnhancer (GME)';
    var linkToNewVersion = 'http://www.tobsch.org/downloads/GoogleMusicEnhancer.user.js';
    var newVersionCheckUrl = 'http://tobsch.org/?site=GoogleMusicEnhancer';

    var Persist = persist;
    var Build = build;

    return {
        registerUpdateButtonEvent: function ($updateButton) {
            if (typeof $updateButton === 'undefined' ) {
                throw 'Update button object undefined';
            }

            if ($updateButton) {
                var that = this;
                $updateButton.on('click', function () {
                    that.check(true);
                });
            }

            return this;
        },

        check: function (force) {
            var lastUpdateTime = Persist.findBy(updateString);

            if (force === true || lastUpdateTime === undefined ||
                parseInt(lastUpdateTime) + parseInt(24 * 60 * 60 * 1000) < parseInt(String(new Date().getTime()))) {

                var newVersion = this.getNewVersion();

                if (newVersion) {
                    var actVersionStripped = parseInt(version.replace(/\./g, ''));
                    var newVersionStripped = parseInt(newVersion.replace(/\./g, ''));

                    if (!!newVersionStripped && newVersionStripped > actVersionStripped) {
                        draw({'newVersion': newVersion});
                    }
                    else {
                        console.log('No new version of the ' + name + ' available ' + newVersion + ' <= ' + version);
                    }

                    Persist.persist(updateString, String(new Date().getTime()));
                }
            }

            return this;
        },

        getNewVersion: function() {
            var response = GM_xmlhttpRequest({
                method: 'GET',
                synchronous: 'true',
                url: newVersionCheckUrl
            });

            if (response.status === 200) {
                return $(response.responseText).find('#projectVersion').text();
            }
            return undefined;
        }
    };

    function draw(options) {

        var newVersionText = 'A new version of the ' + name + ' is available.<br><br>';
        newVersionText += 'Your version: ' + version + '<br>';
        newVersionText += 'Brand new version: ' + options.newVersion + '<br><br><br>';

        blackOut({classesToHide: '.update-box'});

        var updateDiv = $('<div></div>')
            .css({
                left: ($(document).width() / 2) - 250,
                'z-index': '500'
            })
            .attr({
                id: 'update-box',
                class: 'update-box'
            })
            .append($('<div><h2>New version available</h2></div>').addClass('update-header'))
            .append($('<div>' + newVersionText + '</div>')
                .addClass('update-body')
                .append(
                $('<a>' + name + ' ' + options.newVersion + '</a>')
                    .attr({href: linkToNewVersion, title: name + ' ' + version})
            )
        );

        $('body').append(updateDiv);
    }

    function blackOut(options) {
        var blackCurtain = Build.div(
            {
                css: {
                    'width': $(document).width(),
                    'height': $(document).height(),
                    'top': '0',
                    'left': '0',
                    'position': 'absolute',
                    'z-index': '499',
                    'background-color': 'rgb(0, 0, 0)'
                },
                attr: {
                    id: 'black-curtain'
                }
            })
            .fadeTo('slow', 0.7)
            .click(function () {
                $(this).hide();
                $(options.classesToHide).hide();
            });

        $('body').prepend(blackCurtain);
    }
}