MusicBrainz: Import from Amazon

Import releases from Amazon

// ==UserScript==
// @name        MusicBrainz: Import from Amazon
// @namespace   https://github.com/Goram/MusicBrainz-Import-from-Amazon
// @include     *://www.amazon.*
// @version     0.98.5
// @grant       none
// @author      Gore (based on https://github.com/dufferzafar/Userscripts)
// @description Import releases from Amazon
// @require     https://code.jquery.com/jquery-2.2.3.min.js
// @require     https://code.jquery.com/ui/1.11.4/jquery-ui.min.js
// @require     https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js
// @run-at      document-end
// ==/UserScript==

'use strict';

var jquery = {};

jquery = jQuery.noConflict(true);

if (jquery('#rightCol').length) {
    jquery('#rightCol').prepend(
        '<data-gore-mbifa-bootstrap data-ng-app="goreMbifa" data-ng-controller="mbifaController">'
    );
} else if (jquery('.buyingDetailsGrid').length) {
    jquery('.buyingDetailsGrid').prepend(
        '<tr><td><data-gore-mbifa-bootstrap data-ng-app="goreMbifa" data-ng-controller="mbifaController"></td></tr>'
    );
}

var goreMbifa = angular.module('goreMbifa', []);

goreMbifa.constant('config', {
    'sites': {
        'www.amazon.de': {
            'languages': {
                'de': {
                    'months': { 'Januar': '1', 'Februar': '2', 'März': '3', 'April': '4', 'Mai': '5', 'Juni': '6', 'Juli': '7', 'August': '8', 'September': '9', 'Oktober': '10', 'November': '11', 'Dezember': '12' },
                    'disc': 'Disk',
                    'releaseDateOrder': [0, 1, 2],
                    'regexReleaseDate': /Audio CD  \((.*)\)/,
                    'regexNumDiscs': /Anzahl Disks\/Tonträger: (.*)/,
                    'regexReleaseLabel': /Label: (.*)/
                }
            }
        },
        'www.amazon.com': {
            'languages': {
                'en': {
                    'months': { 'January': '1', 'February': '2', 'March': '3', 'April': '4', 'May': '5', 'June': '6', 'July': '7', 'August': '8', 'September': '9', 'October': '10', 'November': '11', 'December': '12' },
                    'disc': 'Disc',
                    'releaseDateOrder': [1, 0, 2],
                    'regexReleaseDate': /Audio CD  \((.*)\)/,
                    'regexNumDiscs': /Number of Discs: (.*)/,
                    'regexReleaseLabel': /Label: (.*)/
                }
            }
        },
        'www.amazon.co.uk': {
            'languages': {
                'en': {
                    'months': { 'Jan.': '1', 'Feb.': '2', 'March': '3', 'April': '4', 'May': '5', 'June': '6', 'July': '7', 'Aug.': '8', 'Sept.': '9', 'Oct.': '10', 'Nov.': '11', 'Dec': '12' },
                    'disc': 'Disc',
                    'releaseDateOrder': [0, 1, 2],
                    'regexReleaseDate': /Audio CD  \((.*)\)/,
                    'regexNumDiscs': /Number of Discs: (.*)/,
                    'regexReleaseLabel': /Label: (.*)/
                }
            }
        },
        'www.amazon.ca': {
            'languages': {
                'en': {
                    'months': { 'Jan': '1', 'Feb': '2', 'March': '3', 'April': '4', 'May': '5', 'June': '6', 'July': '7', 'Aug': '8', 'Sept': '9', 'Oct': '10', 'Nov': '11', 'Dec': '12' },
                    'disc': 'Disc',
                    'releaseDateOrder': [1, 0, 2],
                    'regexReleaseDate': /Audio CD  \((.*)\)/,
                    'regexNumDiscs': /Number of Discs: (.*)/,
                    'regexReleaseLabel': /Label: (.*)/
                },
                'fr': {
                    'months': { 'janvier': '1', 'février': '2', 'mars': '3', 'avril': '4', 'mai': '5', 'juin': '6', 'juillet': '7', 'août': '8', 'septembre': '9', 'octobre': '10', 'novembre': '11', 'décembre': '12' },
                    'disc': 'Disc',
                    'releaseDateOrder': [0, 1, 2],
                    'regexReleaseDate': /Audio CD  \((.*)\)/,
                    'regexNumDiscs': /Quantité de disques : (.*)/,
                    'regexReleaseLabel': /Étiquette : (.*)/
                }
            }
        },
        'www.amazon.fr': {
            'languages': {
                'fr': {
                    'months': { 'janvier': '1', 'février': '2', 'mars': '3', 'avril': '4', 'mai': '5', 'juin': '6', 'juillet': '7', 'août': '8', 'septembre': '9', 'octobre': '10', 'novembre': '11', 'décembre': '12' },
                    'disc': 'Disc',
                    'releaseDateOrder': [0, 1, 2],
                    'regexReleaseDate': /CD  \((.*)\)/,
                    'regexNumDiscs': /Nombre de disques: (.*)/,
                    'regexReleaseLabel': /Label: (.*)/
                }
            }
        }
    },
    'form': {
        'method': 'post',
        'action': 'https://musicbrainz.org/release/add',
        'acceptCharset': 'UTF-8',
        'primaryType': {
            'name': 'type',
            'types': [
                {'key': '', 'value': 'Primary Type'},
                {'key': 'Album', 'value': 'Album'},
                {'key': 'Single', 'value': 'Single'},
                {'key': 'EP', 'value': 'EP'},
                {'key': 'Broadcast', 'value': 'Broadcast'},
                {'key': 'Other', 'value': 'Other'}
            ]
        },
        'editNode': 'Release added using the MB-Import-From-Amazon userscript from page: '
    },
    'link': {
        'href': 'https://musicbrainz.org/search',
        'type': 'release',
        'limit': '25',
        'method': 'direct'
    }
});

goreMbifa.config(function ($sceDelegateProvider) {
    $sceDelegateProvider.resourceUrlWhitelist([
      'https://musicbrainz.org/**'
    ]);
});


goreMbifa.directive('goreMbifaBootstrap', function () {
    return {
        template:
           `<style>
                #gorembifa-app {
                    border: 1px solid #ddd;
                    border-radius: 4px;
                    padding: 14px 18px 12px 18px;
                    margin: 0px 0px 12px 0px;
                }

                #gorembifa-app select {
                    width: 100%;
                    border: 1px solid #ddd;
                    border-radius: 4px;
                    padding: 3px;
                }

                #gorembifa-app form {
                    width: 100%;
                    display: block;
                    margin-bottom: 10px;
                }

                #gorembifa-app input[type="submit"] {
                    width: 100%;
                    height: 24px;
                    border: 1px solid #ddd;
                    border-radius: 3px;
                    background: linear-gradient(to bottom, #f7f8fa, #e7e9ec);
                    margin: 10px 0px 0px 0px;
                    font-family: Arial, sans-serif;
                }

                #gorembifa-app #search-direct, #search-indexed {
                    width: 100%;
                    text-align: center;
                }

                #gorembifa-app #search-indexed div {
                    text-align: left;
                    margin: 10px 0px 0px 0px;
                }
            </style>
            <div id="gorembifa-app">
                <form method="{{ form.method }}" action="{{ form.action }}" accept-charset="{{ form.acceptCharset }}">
                    <select name="{{ form.primaryType.name }}" data-ng-model="form.primaryType.selectedType" data-ng-options="type.value for type in form.primaryType.types track by type.key"/>
                    <input type="hidden" name="name" value="{{ form.title }}"/>
                    <input type="hidden" name="artist_credit.names.0.name" value="{{ form.artist }}"/>
                    <input type="hidden" name="status" value="{{ form.status }}"/>
                    <input type="hidden" name="events.0.date.day" value="{{ form.releaseDate[0] }}"/>
                    <input type="hidden" name="events.0.date.month" value="{{ form.releaseDate[1] }}"/>
                    <input type="hidden" name="events.0.date.year" value="{{ form.releaseDate[2] }}"/>
                    <input type="hidden" name="labels.0.name" value="{{ form.label }}"/>
                    <input type="hidden" name="urls.0.link_type" value="{{ form.externalLinkType }}"/>
                    <input type="hidden" name="urls.0.url" value="{{ form.asin }}"/>
                    <discs data-ng-repeat="disc in form.tracklist.discs track by $index">
                        <input type="hidden" name="mediums.{{ $index }}.format" value="CD"/>
                        <tracks data-ng-repeat="track in disc.tracks" track by $index>
                            <input type="hidden" name="mediums.{{ $parent.$index }}.track.{{ $index }}.number" value="{{ track.number }}"/>
                            <input type="hidden" name="mediums.{{ $parent.$index }}.track.{{ $index }}.name" value="{{ track.title }}"/>
                            <input type="hidden" name="mediums.{{ $parent.$index }}.track.{{ $index }}.length" value="{{ track.length }}"/>
                        </tracks>
                    </discs>
                    <input type="hidden" name="edit_note" value="{{ form.editNote }}"/>
                    <input type="submit" value="Export to MusicBrainz"/>
                </form>
                <div id="search-direct">
                    <a href="{{ link.href }}?query={{ link.query }}&type={{ link.type }}&limit={{ link.limit }}&method={{ link.method }}"
                       title="Direct search uses exact matches only, but provides up to the minute correct results. If the result is empty, modify your search or try the indexed one.">
                       Direct search on MusicBrainz
                    </a>
                </div>
                <div id="search-indexed">
                    <a href="" title=" Search indexes are updated every 3 hours.">Indexed search results</a>
                    <div>
                        <ul data-ng-if="searchIndexed.error === false" data-ng-repeat="release in searchIndexed.releases">
                            <li>
                                {{ release['artist-credit'][0].artist.name }} - {{ release.title }}<br/>
                                Score: {{ release.score }}<br/>
                                Format: <media data-ng-repeat="media in release.media">{{ media.format }} </media>
                            </li>
                        </ul>
                        <span data-ng-if="searchIndexed.error == true">The indexed search was not available.</span>
                    </div>
                </div>
            </div>`
    };
});

goreMbifa.controller('mbifaController', function ($scope, $http, config, dataCollectorService) {
    var data = dataCollectorService.collectData();
    var documentLocationHref = document.location.href.split('?')[0]

    $scope.form = {
        'method': config.form.method,
        'action': config.form.action,
        'acceptCharset': config.form.acceptCharset,
        'primaryType': {
            'name': config.form.primaryType.name,
            'types': config.form.primaryType.types,
            'selectedType': config.form.primaryType.types[0]
        },
        'title': data['title'],
        'artist': data['artist'],
        'status': 'official',
        'releaseDate': data['releaseDate'],
        'label': data['label'],
        'externalLinkType': '77',
        'asin': documentLocationHref,
        'tracklist': data['tracklist'],
        'editNote': config.form.editNode + documentLocationHref
    };

    $scope.link = {
        'href': config.link.href,
        'query': encodeURIComponent(data['title']),
        'type': config.link.type,
        'limit': config.link.limit,
        'method': config.link.method
    };

    $scope.searchIndexed = {
        'releases':{},
        'error': false
    }

    $http({
        method: 'GET',
        url: 'https://musicbrainz.org/ws/2/release?query=' + encodeURIComponent(data['title']) + '&fmt=json'
    }).then(
    function (response) {
        $scope.searchIndexed.releases = response.data.releases;
    },
    function (response) {
        $scope.searchIndexed.error = true;
    });

    jquery('#search-indexed').accordion({
        heightStyle: 'content',
        active: false,
        collapsible: true
    });
});

goreMbifa.service('dataCollectorService', function (config, siteLookupService, languageLookupService) {
    this.collectData = function () {
        var siteSpecificConfig = config.sites[siteLookupService].languages[languageLookupService]

        var title = '';

        if (jquery('#productTitle').length) {
            title = jquery('#productTitle').text().trim();
        } else if (jquery('.buying').length) {
            title = jquery('#btAsinTitle span').clone().find('span').remove().end().html().trim();
        }

        var artist = '';

        if (jquery('.author a').length) {
            artist = jquery('.author a').text().trim();
        } else if (jquery('.buying').length) {
            artist = jquery('.buying span a').html().trim()
        }

        return {
            'title': title,
            'artist': artist,
            'releaseDate': this.collectReleaseDate(siteSpecificConfig),
            'label': this.collectLabel(siteSpecificConfig),
            'tracklist': this.collectTracklist(siteSpecificConfig)
        };
    };

    this.collectReleaseDate = function (siteSpecificConfig) {
        var releaseDate = [];

        var releaseDateElement = jquery('#productDetailsTable li').filter(function () {
            return siteSpecificConfig.regexReleaseDate.test(jquery(this).text());
        });

        var releaseDateMatch = siteSpecificConfig.regexReleaseDate.exec(releaseDateElement.text());

        if (releaseDateMatch) {
            var releaseDateParts = releaseDateMatch[1].replace(/[.,]/g, '').split(' ');

            for (var i = 0; i < siteSpecificConfig.releaseDateOrder.length; i++) {
                if (i == 1) {
                    releaseDate.push(siteSpecificConfig.months[releaseDateParts[siteSpecificConfig.releaseDateOrder[i]]]);
                    continue;
                }

                releaseDate.push(releaseDateParts[siteSpecificConfig.releaseDateOrder[i]]);
            }
        }

        return releaseDate;
    }

    this.collectLabel = function (siteSpecificConfig) {
        var label = '';

        var labelElement = jquery('#productDetailsTable li').filter(function () {
            return siteSpecificConfig.regexReleaseLabel.test(jquery(this).text());
        });

        var labelMatch = siteSpecificConfig.regexReleaseLabel.exec(labelElement.text());

        if (labelMatch) {
            label = labelMatch[1];
        }

        return label;
    }

    this.collectTracklist = function (siteSpecificConfig) {
        var tracklist = { 'discs': [] };
        var disc = 0;
        var tracks = { 'tracks': [] };

        // Amazon has more than one tracklist...
        if (jquery('#dmusic_tracklist_content').length) {
            /*
                Tracklists:
                One Disk: http://www.amazon.de/01-Fl%C3%BCsterer-Remastered-Gabriel-Burns/dp/B00N29D69I
                Multiple Disks: http://www.amazon.de/Book-Souls-limited-Deluxe/dp/B00ZVFYVMM
            */
            var tracklistContent = jquery('#dmusic_tracklist_content tr');

            for (var i = 1; i < tracklistContent.length; i++) {
                if (tracklistContent[i].id == 'dmusic_tracklist_player_disc_' + (disc + 2)) {
                    tracklist['discs'][disc] = tracks;

                    disc++;
                    var tracks = { 'tracks': [] };

                    continue;
                }

                var trackDetails = tracklistContent[i].getElementsByTagName('td');

                if (trackDetails[0].getElementsByClassName('TrackNumber')[0]) {
                    tracks['tracks'].push({
                        'number': trackDetails[0].getElementsByClassName('TrackNumber')[0].textContent.trim(),
                        'title': trackDetails[1].getElementsByClassName('TitleLink')[0].textContent.trim(),
                        'length': trackDetails[2].getElementsByTagName("span")[0].textContent.trim()
                    });
                }
            }
        } else if (jquery('#dmusic_tracklist_player').length) {
            /*
                Tracklists:
                One Disk: http://www.amazon.de/Seventh-Son-Iron-Maiden/dp/B0000251W3
                Multiple Disks: http://www.amazon.de/Deceiver-Gods-Amon-Amarth/dp/B00CEJ2H6K
            */
            var tracklistContent = jquery('#dmusic_tracklist_player .a-row');

            for (var i = 1; i < tracklistContent.length; i++) {
                if (tracklistContent[i].textContent.trim() == siteSpecificConfig.disc + ": " + (disc + 2)) {
                    tracklist['discs'][disc] = tracks;

                    disc++;
                    var tracks = { 'tracks': [] };

                    continue;
                }

                var trackDetails = tracklistContent[i].textContent.split('. ');

                tracks['tracks'].push({
                    'number': trackDetails[0].trim(),
                    'title': trackDetails[1].trim()
                });
            }
        } else if (jquery('#musicTracksFeature').length) {
            /*
                Tracklists:
                One Disk: http://www.amazon.ca/gp/product/B00062PWOQ
                Multiple Disks: http://www.amazon.ca/The-Book-Souls-Deluxe-Hardcover/dp/B00ZVFYVMM
            */

            var tracklistContent = jquery('#musicTracksFeature tr');

            for (var i = 0; i < tracklistContent.length; i++) {
                if (tracklistContent[i].classList.contains('sampleTracksHeader')) {
                    if (i == 0) {
                        continue;
                    }

                    tracklist['discs'][disc] = tracks;

                    disc++;
                    var tracks = { 'tracks': [] };

                    continue;
                }

                if (tracklistContent[i].hasAttribute('class')) {
                    var trackDetails = tracklistContent[i].getElementsByTagName('td')[0].textContent.split('. ');

                    tracks['tracks'].push({
                        'number': trackDetails[0].trim(),
                        'title': trackDetails[1].trim()
                    });
                }
            }
        }

        tracklist['discs'][disc] = tracks;

        return tracklist;
    }
});

goreMbifa.factory('languageLookupService', function () {
    var navLocale = jquery('header').attr('class').split(' ')[1];

    return navLocale.substring(9, 11);
});

goreMbifa.value('siteLookupService', document.domain);