{"body":"// ==UserScript==\n\n// @name           Import Discogs releases to MusicBrainz\n// @description    Add a button to import Discogs releases to MusicBrainz and add links to matching MusicBrainz entities for various Discogs entities (artist,release,master,label)\n// @version        2021.8.10.1\n// @namespace      http://userscripts.org/users/22504\n// @downloadURL    https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/discogs_importer.user.js\n// @updateURL      https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/discogs_importer.user.js\n// @include        http*://www.discogs.com/*\n// @include        http*://*.discogs.com/*release/*\n// @exclude        http*://*.discogs.com/*release/*?f=xml*\n// @exclude        http*://www.discogs.com/release/add\n// @require        https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js\n// @require        lib/mbimport.js\n// @require        lib/logger.js\n// @require        lib/mblinks.js\n// @require        lib/mbimportstyle.js\n// @icon           https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/assets/images/Musicbrainz_import_logo.png\n// ==/UserScript==\n\n// prevent JQuery conflicts, see http://wiki.greasespot.net/@grant\nthis.$ = this.jQuery = jQuery.noConflict(true);\n\nconst DEBUG = false;\nif (DEBUG) {\n    LOGGER.setLevel('debug');\n}\n\n////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n/*\n * Test cases:\n * - http://www.discogs.com/release/1566223 : Artist credit of tracks contains an ending ',' join phrase\n */\n\nconst mbLinks = new MBLinks('DISCOGS_MBLINKS_CACHE', '1');\n\n$(document).ready(function () {\n    MBImportStyle();\n    MBSearchItStyle();\n\n    const current_page_key = getDiscogsLinkKey(\n        window.location.href.replace(/\\?.*$/, '').replace(/#.*$/, '').replace('/master/view/', '/master/')\n    );\n    if (!current_page_key) return;\n\n    // disable evil pjax (used for artist page navigation)\n    // it causes various annoying issues with our code;\n    // it should be possible to react to pjax events\n    $('div#pjax_container').attr('id', 'pjax_disabled');\n\n    // Display links of equivalent MusicBrainz entities\n    insertMBLinks(current_page_key);\n\n    // Add an import button in a new section in sidebar, if we're on a release page\n    let current_page_info = link_infos[current_page_key];\n    if (current_page_info.type === 'release') {\n        // Discogs Webservice URL\n        let discogsWsUrl = `https://api.discogs.com/releases/${current_page_info.id}`;\n\n        $.ajax({\n            url: discogsWsUrl,\n            dataType: 'json',\n            crossDomain: true,\n            success: data => {\n                LOGGER.debug('Discogs JSON Data from API:', data);\n                try {\n                    let release = parseDiscogsRelease(data);\n                    insertMBSection(release, current_page_key);\n                } catch (e) {\n                    $('div.musicbrainz').remove();\n                    let mbUI = $('<div class=\"section musicbrainz\"><h3>MusicBrainz</h3></div>').hide();\n                    let mbContentBlock = $('<div class=\"section_content\"></div>');\n                    mbUI.append(mbContentBlock);\n                    let mbError = $(\n                        `<p><small>${e}<br /><b>Please <a href=\"https://github.com/murdos/musicbrainz-userscripts/issues\">report</a> this error, along the current page URL.</b></small></p>`\n                    );\n                    mbContentBlock.prepend(mbError);\n                    insertMbUI(mbUI);\n                    mbError.css({ 'background-color': '#fbb', 'margin-top': '4px', 'margin-bottom': '4px' });\n                    mbUI.slideDown();\n                    throw e;\n                }\n            },\n            error: function (jqXHR, textStatus, errorThrown) {\n                LOGGER.error('AJAX Status: ', textStatus);\n                LOGGER.error('AJAX error thrown: ', errorThrown);\n            },\n        });\n    }\n});\n\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n//                              Display links of equivalent MusicBrainz entities                                      //\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n// Insert MusicBrainz links in a section of the page\nfunction insertMBLinks(current_page_key) {\n    function searchAndDisplayMbLinkInSection($tr, discogs_type, mb_type, nosearch) {\n        if (!mb_type) mb_type = defaultMBtype(discogs_type);\n        $tr.find(`a[mlink^=\"${discogs_type}/\"]`).each(function () {\n            const $link = $(this);\n            if ($link.attr('mlink_stop')) return; // for places\n            const mlink = $link.attr('mlink');\n            // ensure we do it only once per link\n            const done = ($link.attr('mlink_done') || '').split(',');\n            for (let i = 0; i < done.length; i++) {\n                if (mb_type == done[i]) return;\n            }\n            done.push(mb_type);\n            $link.attr(\n                'mlink_done',\n                done\n                    .filter(function (e) {\n                        return e != '';\n                    })\n                    .join(',')\n            );\n            if (link_infos[mlink] && link_infos[mlink].type === discogs_type) {\n                const discogs_url = link_infos[mlink].clean_url;\n                let cachekey = getCacheKeyFromInfo(mlink, mb_type);\n                const has_wrapper = $link.closest('span.mb_wrapper').length;\n                if (!has_wrapper) {\n                    $link.wrap('<span class=\"mb_wrapper\"><span class=\"mb_valign\"></span></span>');\n                }\n                if (!nosearch) {\n                    // add search link for the current link text\n                    const entities = {\n                        artist: { mark: 'A' },\n                        release: { mark: 'R' },\n                        'release-group': { mark: 'G' },\n                        place: { mark: 'P' },\n                        label: { mark: 'L' },\n                    };\n                    let mark = '';\n                    let entity_name = 'entity';\n                    if (mb_type in entities) {\n                        mark = entities[mb_type].mark;\n                        entity_name = mb_type.replace(/[_-]/g, ' ');\n                    }\n                    $link\n                        .closest('span.mb_wrapper')\n                        .prepend(\n                            `<span class=\"mb_valign mb_searchit\"><a class=\"mb_search_link\" target=\"_blank\" title=\"Search this ${entity_name} on MusicBrainz (open in a new tab)\" href=\"${MBImport.searchUrlFor(\n                                mb_type,\n                                $link.text()\n                            )}\"><small>${mark}</small>?</a></span>`\n                        );\n                }\n                const insert_normal = function (link) {\n                    $link.closest('span.mb_valign').before(`<span class=\"mb_valign\">${link}</span>`);\n                    $link.closest('span.mb_wrapper').find('.mb_searchit').remove();\n                };\n\n                const insert_stop = function (link) {\n                    insert_normal(link);\n                    $link.attr('mlink_stop', true);\n                };\n\n                let insert_func = insert_normal;\n                if (mb_type == 'place') {\n                    // if a place link was added we stop, we don't want further queries for this 'label'\n                    insert_func = insert_stop;\n                }\n                mbLinks.searchAndDisplayMbLink(discogs_url, mb_type, insert_func, cachekey);\n            }\n        });\n    }\n\n    function debug_color(what, n, id) {\n        let colors = [\n            '#B3C6FF',\n            '#C6B3FF',\n            '#ECB3FF',\n            '#FFB3EC',\n            '#FFB3C6',\n            '#FFC6B3',\n            '#FFECB3',\n            '#ECFFB3',\n            '#C6FFB3',\n            '#B3FFC6',\n            '#B3FFEC',\n            '#B3ECFF',\n            '#7598FF',\n        ];\n        if (DEBUG) {\n            $(what).css('border', `2px dotted ${colors[n % colors.length]}`);\n            let debug_attr = $(what).attr('debug_discogs');\n            if (!id) id = '';\n            if (debug_attr) {\n                $(what).attr('debug_discogs', `${debug_attr} || ${id}(${n})`);\n            } else {\n                $(what).attr('debug_discogs', `${id}(${n})`);\n            }\n        }\n    }\n\n    let add_mblinks_counter = 0;\n    function add_mblinks(_root, selector, types, nosearch) {\n        // types can be:\n        // 'discogs type 1'\n        // ['discogs_type 1', 'discogs_type 2']\n        // [['discogs_type 1', 'mb type 1'], 'discogs_type 2']\n        // etc.\n        if (!$.isArray(types)) {\n            // just one string\n            types = [types];\n        }\n        $.each(types, function (idx, val) {\n            if (!$.isArray(val)) {\n                types[idx] = [val, undefined];\n            }\n        });\n\n        LOGGER.debug(`add_mblinks: ${selector} / ${JSON.stringify(types)}`);\n\n        _root.find(selector).each(function () {\n            const node = $(this).get(0);\n            magnifyLinks(node);\n            debug_color(this, ++add_mblinks_counter, selector);\n            const that = this;\n            $.each(types, function (idx, val) {\n                const discogs_type = val[0];\n                const mb_type = val[1];\n                searchAndDisplayMbLinkInSection($(that), discogs_type, mb_type, nosearch);\n            });\n        });\n    }\n\n    // Find MB link for the current page and display it next to page title\n    let mbLinkInsert = function (link) {\n        const $h1 = $('h1');\n        const $titleSpan = $h1.children('span[itemprop=\"name\"]');\n        if ($titleSpan.length > 0) {\n            $titleSpan.before(link);\n        } else {\n            $h1.prepend(link);\n        }\n    };\n    const current_page_info = link_infos[current_page_key];\n    const mb_type = defaultMBtype(current_page_info.type);\n    const cachekey = getCacheKeyFromInfo(current_page_key, mb_type);\n    mbLinks.searchAndDisplayMbLink(current_page_info.clean_url, mb_type, mbLinkInsert, cachekey);\n\n    const $root = $('body');\n    // artist/label/master pages, release pages (before the 2021-08-09 update)\n    add_mblinks($root, 'div.profile', ['artist', 'label']);\n    add_mblinks($root, 'tr[data-object-type=\"release\"] td.artist,td.title', 'artist');\n    add_mblinks($root, 'tr[data-object-type=\"release\"] td.title', 'release');\n    add_mblinks($root, 'tr[data-object-type=\"release\"]', 'label');\n    add_mblinks($root, 'tr[data-object-type~=\"master\"]', ['master', 'artist', 'label']);\n    // release pages (since the 2021-08-09 update)\n    add_mblinks($root, '#release-header', ['artist', 'label']);\n    setInterval(() => add_mblinks($root, '#release-other-versions', ['artist', 'release', 'label']), 500); // Discogs loads this dynamically, wait a moment\n    add_mblinks($root, '#release-tracklist', 'artist');\n    add_mblinks($root, '#release-companies', [['label', 'place'], 'label']);\n    add_mblinks($root, '#release-credits', ['label', 'artist']);\n    add_mblinks($root, '#release-actions', 'master', true);\n    // release pages (before the 2021-08-09 update, TODO: remove after the new layout becomes permanent)\n    add_mblinks($root, 'div#tracklist', 'artist');\n    add_mblinks($root, 'div#companies', [['label', 'place'], 'label']);\n    add_mblinks($root, 'div#credits', ['label', 'artist']);\n    add_mblinks($root, 'div#page_aside div.section_content:first', 'master', true);\n}\n\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n//                                 Normalize Discogs URLs in a DOM tree                                               //\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\nlet mlink_processed = 0;\n\n// Normalize Discogs URLs in a DOM tree\nfunction magnifyLinks(rootNode) {\n    if (!rootNode) {\n        rootNode = document.body;\n    }\n\n    // Check if we already added links for this content\n    if (rootNode.hasAttribute('mlink_processed')) return;\n    rootNode.setAttribute('mlink_processed', ++mlink_processed);\n\n    let elems = rootNode.getElementsByTagName('a');\n    for (let i = 0; i < elems.length; i++) {\n        let elem = elems[i];\n\n        // Ignore empty links\n        if (!elem.href || $.trim(elem.textContent) === '' || elem.textContent.substring(4, 0) === 'http') continue;\n        if (!elem.hasAttribute('mlink')) {\n            elem.setAttribute('mlink', getDiscogsLinkKey(elem.href));\n        }\n    }\n}\n\n// contains infos for each link key\nconst link_infos = {};\n\n// Parse discogs url to extract info, returns a key and set link_infos for this key\n// the key is in the form discogs_type/discogs_id\nfunction getDiscogsLinkKey(url) {\n    const re = /^https?:\\/\\/(?:www|api)\\.discogs\\.com\\/(?:(?:(?!sell).+|sell.+)\\/)?(master|release|artist|label)s?\\/(\\d+)(?:[^?#]*)(?:\\?noanv=1|\\?anv=[^=]+)?$/i;\n    const m = re.exec(url);\n    if (m !== null) {\n        const key = `${m[1]}/${m[2]}`;\n        if (!link_infos[key]) {\n            link_infos[key] = {\n                type: m[1],\n                id: m[2],\n                clean_url: `https://www.discogs.com/${m[1]}/${m[2]}`,\n            };\n            LOGGER.debug(`getDiscogsLinkKey:${url} --> ${key}`);\n        } else {\n            LOGGER.debug(`getDiscogsLinkKey:${url} --> ${key} (key exists)`);\n        }\n        return key;\n    }\n    LOGGER.debug(`getDiscogsLinkKey:${url} ?`);\n    return false;\n}\n\nfunction getCleanUrl(url, discogs_type) {\n    try {\n        const key = getDiscogsLinkKey(url);\n        if (key) {\n            if (!discogs_type || link_infos[key].type === discogs_type) {\n                LOGGER.debug(`getCleanUrl: ${key}, ${url} --> ${link_infos[key].clean_url}`);\n                return link_infos[key].clean_url;\n            } else {\n                LOGGER.debug(`getCleanUrl: ${key}, ${url} --> unmatched type: ${discogs_type}`);\n            }\n        }\n    } catch (err) {\n        LOGGER.error(err);\n    }\n    LOGGER.debug(`getCleanUrl: ${url} (${discogs_type}) failed`);\n    return false;\n}\n\nfunction defaultMBtype(discogs_type) {\n    if (discogs_type === 'master') return 'release-group';\n    return discogs_type;\n}\n\nfunction getCacheKeyFromInfo(info_key, mb_type) {\n    const inf = link_infos[info_key];\n    if (inf) {\n        if (!mb_type) mb_type = defaultMBtype(inf.type);\n        return `${inf.type}/${inf.id}/${mb_type}`;\n    }\n    return '';\n}\n\nfunction getCacheKeyFromUrl(url, discogs_type, mb_type) {\n    try {\n        const key = getDiscogsLinkKey(url);\n        if (key) {\n            if (!discogs_type || link_infos[key].type == discogs_type) {\n                const cachekey = getCacheKeyFromInfo(key, mb_type);\n                LOGGER.debug(`getCacheKeyFromUrl: ${key}, ${url} --> ${cachekey}`);\n                return cachekey;\n            } else {\n                LOGGER.debug(`getCacheKeyFromUrl: ${key}, ${url} --> unmatched type: ${discogs_type}`);\n            }\n        }\n    } catch (err) {\n        LOGGER.error(err);\n    }\n    LOGGER.debug(`getCacheKeyFromUrl: ${url} (${discogs_type}) failed`);\n    return false;\n}\n\nfunction MBIDfromUrl(url, discogs_type, mb_type) {\n    const cachekey = getCacheKeyFromUrl(url, discogs_type, mb_type);\n    if (!cachekey) return '';\n    return mbLinks.resolveMBID(cachekey);\n}\n\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n//                             Insert MusicBrainz section into Discogs page                                           //\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\nfunction insertMbUI(mbUI) {\n    let e;\n    if ((e = $('#release-marketplace')) && e.length) {\n        e.before(mbUI);\n    }\n    // FIXME: the following selectors are broken since the 2021-08-09 release page update, not sure why there are three alternative selectors\n    else if ((e = $('div.section.collections')) && e.length) {\n        e.after(mbUI);\n    } else if ((e = $('#statistics')) && e.length) {\n        e.before(mbUI);\n    } else if ((e = $('div.section.social')) && e.length) {\n        e.before(mbUI);\n    }\n}\n\n// Insert links in Discogs page\nfunction insertMBSection(release, current_page_key) {\n    const current_page_info = link_infos[current_page_key];\n\n    const mbUI = $('<div class=\"section musicbrainz\"><header><h3>MusicBrainz</h3></header></div>').hide();\n\n    if (DEBUG) mbUI.css({ border: '1px dotted red' });\n\n    const mbContentBlock = $('<div class=\"section_content\"></div>');\n    mbUI.append(mbContentBlock);\n\n    if (release.maybe_buggy) {\n        const warning_buggy = $(\n            '<p><small><b>Warning</b>: this release has perhaps a buggy tracklist, please check twice the data you import.</small><p'\n        ).css({ color: 'red', 'margin-top': '4px', 'margin-bottom': '4px' });\n        mbContentBlock.prepend(warning_buggy);\n    }\n\n    // Form parameters\n    const edit_note = MBImport.makeEditNote(current_page_info.clean_url, 'Discogs');\n    const parameters = MBImport.buildFormParameters(release, edit_note);\n\n    // Build form + search button\n    const innerHTML = `<div id=\"mb_buttons\">${MBImport.buildFormHTML(parameters)}${MBImport.buildSearchButton(release)}</div>`;\n    mbContentBlock.append(innerHTML);\n\n    insertMbUI(mbUI);\n\n    // FIXME: duplicates some of Discogs' CSS because they seem to use dynamically generated class names since the 2021-08-09 release page update\n    $('.musicbrainz header').css({\n        // Discogs selector is \".header_W2hzl\" (at least now and for me)\n        'border-bottom': '1px solid #e5e5e5',\n        'padding-left': '5px',\n    });\n    $('.musicbrainz h3').css({\n        // Discogs selector is \".header_W2hzl h3\"\n        'font-size': '15px',\n        'line-height': '20px',\n        margin: '0 0 0 -5px',\n        padding: '5px',\n    });\n    $('#mb_buttons').css({\n        display: 'inline-block',\n        width: '100%',\n        'margin-top': '5px', // FIXME: related to the above CSS hacks\n    });\n    $('form.musicbrainz_import').css({ width: '49%', display: 'inline-block' });\n    $('form.musicbrainz_import_search').css({ float: 'right' });\n    $('form.musicbrainz_import > button').css({ width: '100%', 'box-sizing': 'border-box' });\n\n    mbUI.slideDown();\n}\n\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n//                                               Parsing of Discogs data                                              //\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\nfunction cleanup_discogs_artist_credit(obj) {\n    // Fix some odd Discogs release (e.g. http://api.discogs.com/releases/1566223) that have a ',' join phrase after the last artist\n    // Discogs set a join phrase even there's only one artist or when extraartists is set (ie. remix)\n    const last = obj.artist_credit.length - 1;\n    if (last === 0 || obj.artist_credit[last].joinphrase === ', ') {\n        obj.artist_credit[last].joinphrase = '';\n    }\n}\n\n// Returns the name without the numerical suffic Discogs adds as disambiguation\n// ie. \"ABC (123)\" -> \"ABC\"\nfunction artistNoNum(artist_name) {\n    return artist_name.replace(/ \\(\\d+\\)$/, '');\n}\n\n// Parse a US date string and set object properties year, month, day\nfunction parse_YYYY_MM_DD(date, obj) {\n    if (!date) return;\n    const m = date.split(/\\D+/, 3).map(function (e) {\n        return parseInt(e, 10);\n    });\n    if (m[0] !== undefined) {\n        obj.year = m[0];\n        if (m[1] !== undefined) {\n            obj.month = m[1];\n            if (m[2] !== undefined) {\n                obj.day = m[2];\n            }\n        }\n    }\n}\n\n// Analyze Discogs data and return a release object\nfunction parseDiscogsRelease(discogsRelease) {\n    const release = {\n        discs: [],\n    };\n\n    //buggy tracklist indicator, used to warn user\n    release.maybe_buggy = false;\n\n    // Release artist credit\n    release.artist_credit = [];\n    $.each(discogsRelease.artists, function (index, artist) {\n        let ac = {\n            artist_name: artistNoNum(artist.name),\n            credited_name: artist.anv != '' ? artist.anv : artistNoNum(artist.name),\n            joinphrase: decodeDiscogsJoinphrase(artist.join),\n            mbid: MBIDfromUrl(artist.resource_url, 'artist'),\n        };\n        if (artist.id === 194) {\n            // discogs place holder for various\n            ac = MBImport.specialArtist('various_artists', ac);\n        }\n        release.artist_credit.push(ac);\n    });\n    cleanup_discogs_artist_credit(release);\n\n    // ReleaseGroup\n    if (discogsRelease.master_url) {\n        release.release_group_mbid = MBIDfromUrl(discogsRelease.master_url, 'master');\n    }\n\n    // Release title\n    release.title = discogsRelease.title;\n\n    // Release date\n    if (discogsRelease.released) {\n        parse_YYYY_MM_DD(discogsRelease.released, release);\n    }\n\n    // Release country\n    if (discogsRelease.country) {\n        release.country = Countries[discogsRelease.country];\n    }\n\n    // Release labels\n    release.labels = [];\n    if (discogsRelease.labels) {\n        $.each(discogsRelease.labels, function (index, label) {\n            const labelInfo = {\n                name: label.name,\n                catno: label.catno === 'none' ? '[none]' : label.catno,\n                mbid: MBIDfromUrl(label.resource_url, 'label'),\n            };\n            release.labels.push(labelInfo);\n        });\n    }\n\n    // Release URL\n    release.urls = [{ url: getCleanUrl(discogsRelease.uri, 'release'), link_type: MBImport.URL_TYPES.discogs }];\n\n    // Release format\n    let release_formats = [];\n    release.secondary_types = [];\n\n    if (discogsRelease.formats.length > 0) {\n        for (let i = 0; i < discogsRelease.formats.length; i++) {\n            // Release format\n            const discogs_format = discogsRelease.formats[i].name;\n            let mb_format = undefined;\n            if (discogs_format in MediaTypes) {\n                mb_format = MediaTypes[discogs_format];\n            }\n\n            if (discogsRelease.formats[i].descriptions) {\n                $.each(discogsRelease.formats[i].descriptions, function (index, desc) {\n                    if (!(discogs_format in ['Box Set'])) {\n                        // Release format: special handling of Vinyl and Shellac 7\", 10\" and 12\"\n                        if (desc.match(/7\"|10\"|12\"/) && discogs_format.concat(desc) in MediaTypes)\n                            mb_format = MediaTypes[discogs_format.concat(desc)];\n                        // Release format: special handling of specific CD/DVD formats\n                        if (desc.match(/^VCD|SVCD|CD\\+G|HDCD|DVD-Audio|DVD-Video/) && desc in MediaTypes) mb_format = MediaTypes[desc];\n                    }\n                    // Release format: special handling of Vinyl, LP == 12\" (http://www.discogs.com/help/submission-guidelines-release-format.html#LP)\n                    if (discogs_format === 'Vinyl' && desc === 'LP') {\n                        mb_format = '12\" Vinyl';\n                    }\n                    // Release format: special handling of CD, Mini == 8cm CD\n                    if (discogs_format === 'CD' && desc === 'Mini') {\n                        mb_format = '8cm CD';\n                    }\n                    // Release status\n                    if (desc.match(/Promo|Smplr/)) {\n                        release.status = 'promotion';\n                    }\n                    if (desc.match(/Unofficial Release/)) {\n                        release.status = 'bootleg';\n                    }\n                    // Release type\n                    if (desc.match(/Compilation/)) {\n                        release.secondary_types.push('compilation');\n                    }\n                    if (desc.match(/^Album/)) {\n                        release.type = 'album';\n                    }\n                    if (desc.match(/Single(?! Sided)/)) {\n                        release.type = 'single';\n                    }\n                    if (desc.match(/EP|Mini-Album/)) {\n                        release.type = 'ep';\n                    }\n                });\n            }\n\n            if (mb_format) {\n                for (let j = 0; j < discogsRelease.formats[i].qty; j++) {\n                    release_formats.push(mb_format);\n                }\n            }\n\n            // Release packaging\n            if (discogsRelease.formats[i].text) {\n                const freetext = discogsRelease.formats[i].text.toLowerCase().replace(/[\\s-]/g, '');\n                if (freetext.match(/cardboard|paper/)) {\n                    release.packaging = 'cardboard/paper sleeve';\n                } else if (freetext.match(/digi[\\s\\-‐]?pac?k/)) {\n                    release.packaging = 'digipak';\n                } else if (freetext.match(/keepcase/)) {\n                    release.packaging = 'keep case';\n                } else if (freetext.match(/jewel/)) {\n                    release.packaging = freetext.match(/slim/) ? 'slim jewel case' : 'jewel case';\n                } else if (freetext.match(/gatefold|digisleeve/)) {\n                    release.packaging = 'gatefold cover';\n                }\n            }\n        }\n    }\n\n    // Barcode\n    if (discogsRelease.identifiers) {\n        $.each(discogsRelease.identifiers, function (index, identifier) {\n            if (identifier.type === 'Barcode') {\n                release.barcode = identifier.value.replace(/ /g, '');\n                return false;\n            }\n        });\n    }\n\n    // Inspect tracks\n    let heading = '';\n    let releaseNumber = 1;\n    let lastPosition = 0;\n    $.each(discogsRelease.tracklist, function (index, discogsTrack) {\n        if (discogsTrack.type_ === 'heading') {\n            heading = discogsTrack.title;\n            return;\n        }\n        if (discogsTrack.type_ !== 'track' && discogsTrack.type_ !== 'index') {\n            return;\n        }\n\n        let track = {};\n\n        track.title = discogsTrack.title.replace(/´/g, '’');\n        track.duration = MBImport.hmsToMilliSeconds(discogsTrack.duration); // MB in milliseconds\n\n        // Track artist credit\n        track.artist_credit = [];\n        if (discogsTrack.artists) {\n            $.each(discogsTrack.artists, function (index, artist) {\n                const ac = {\n                    artist_name: artistNoNum(artist.name),\n                    credited_name: artist.anv !== '' ? artist.anv : artistNoNum(artist.name),\n                    joinphrase: decodeDiscogsJoinphrase(artist.join),\n                    mbid: MBIDfromUrl(artist.resource_url, 'artist'),\n                };\n                track.artist_credit.push(ac);\n            });\n            cleanup_discogs_artist_credit(track);\n        }\n\n        // Track position and release number\n        let trackPosition = discogsTrack.position;\n\n        // Handle sub-tracks\n        if (trackPosition === '' && discogsTrack.sub_tracks) {\n            trackPosition = discogsTrack.sub_tracks[0].position;\n            // Append titles of sub-tracks to main track title\n            const subtrack_titles = [];\n            let subtrack_total_duration = 0;\n            $.each(discogsTrack.sub_tracks, function (subtrack_index, subtrack) {\n                if (subtrack.type_ !== 'track') {\n                    return;\n                }\n                if (subtrack.duration) {\n                    subtrack_total_duration += MBImport.hmsToMilliSeconds(subtrack.duration);\n                }\n                if (subtrack.title) {\n                    subtrack_titles.push(subtrack.title);\n                } else {\n                    subtrack_titles.push('[unknown]');\n                }\n            });\n            if (subtrack_titles.length) {\n                if (track.title) {\n                    track.title += ': ';\n                }\n                track.title += subtrack_titles.join(' / ');\n            }\n            if (isNaN(track.duration) && !isNaN(subtrack_total_duration)) {\n                track.duration = subtrack_total_duration;\n            }\n        }\n\n        // Skip special tracks\n        if (trackPosition.match(/^(?:video|mp3)/i)) {\n            trackPosition = '';\n        }\n\n        // Possible track position:\n        // A1 or A    => Vinyl or Cassette : guess releaseNumber from vinyl side\n        // 1-1 or 1.1 => releaseNumber.trackNumber\n        // 1          => trackNumber\n        let buggyTrackNumber = false;\n        const tmp = trackPosition.match(/(\\d+|[A-Z])(?:[.-]+(\\d+))?/i);\n        if (tmp) {\n            tmp[1] = parseInt(tmp[1], 10);\n            let prevReleaseNumber = releaseNumber;\n\n            if (Number.isInteger(tmp[1])) {\n                if (tmp[2]) {\n                    // 1-1, 1-2, 2-1, ... - we can get release number and track number from this\n                    releaseNumber = tmp[1];\n                    lastPosition = parseInt(tmp[2], 10);\n                } else if (tmp[1] <= lastPosition) {\n                    // 1, 2, 3, ... - We've moved onto a new medium\n                    releaseNumber++;\n                    lastPosition = tmp[1];\n                } else {\n                    lastPosition = tmp[1];\n                }\n            } else {\n                if (trackPosition.match(/^[A-Z]\\d*$/i)) {\n                    // Vinyl or cassette, handle it specially\n                    // A,B -> 1; C,D -> 2; E,F -> 3, etc...\n                    releaseNumber = (((32 | trackPosition.charCodeAt(0)) - 97) >> 1) + 1;\n                    lastPosition++;\n                } else if (trackPosition.match(/^[A-Z]+\\d*$/i)) {\n                    // Vinyl or cassette, handle it specially\n                    // something like AA1, exemple : http://www.discogs.com/release/73531\n                    // TODO: find a better fix\n                    buggyTrackNumber = true;\n                }\n            }\n\n            if (releaseNumber > release_formats.length) {\n                // something went wrong in track position parsing\n                buggyTrackNumber = true;\n                releaseNumber = prevReleaseNumber;\n            }\n            if (buggyTrackNumber) {\n                // well, it went wrong so ...\n                lastPosition++;\n            }\n        }\n\n        // Create release if needed\n        let discindex = releaseNumber - 1;\n        if (!release.discs[discindex]) {\n            let newdisc = {\n                tracks: [],\n                format: release_formats[discindex],\n            };\n            if (heading) {\n                newdisc.title = heading;\n                heading = '';\n            }\n            release.discs.push(newdisc);\n        }\n\n        // Track number (only for Vinyl and Cassette)\n        if (\n            buggyTrackNumber ||\n            (release.discs[discindex].format.match(/(Vinyl|Cassette)/) && discogsTrack.position.match(/^[A-Z]+[.-]?\\d*/i))\n        ) {\n            track.number = discogsTrack.position;\n        }\n\n        // Trackposition is empty e.g. for release title\n        if (trackPosition !== '' && trackPosition != null) {\n            release.discs[discindex].tracks.push(track);\n        }\n\n        if (buggyTrackNumber && !release.maybe_buggy) {\n            release.maybe_buggy = true;\n        }\n    });\n\n    if (release.discs.length === 1 && release.discs[0].title) {\n        // remove title if there is only one disc\n        // https://github.com/murdos/musicbrainz-userscripts/issues/69\n        release.discs[0].title = '';\n    }\n\n    LOGGER.info('Parsed release: ', release);\n    return release;\n}\n\nfunction decodeDiscogsJoinphrase(join) {\n    let joinphrase = '';\n    const trimedjoin = join.replace(/^\\s*/, '').replace(/\\s*$/, '');\n    if (trimedjoin === '') {\n        return trimedjoin;\n    }\n    if (trimedjoin !== ',') {\n        joinphrase += ' ';\n    }\n    joinphrase += trimedjoin;\n    joinphrase += ' ';\n    return joinphrase;\n}\n\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n//                                   Discogs -> MusicBrainz mapping                                                   //\n////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\nconst MediaTypes = {\n    '8-Track Cartridge': 'Cartridge',\n    Acetate: 'Vinyl',\n    Betamax: 'Betamax',\n    'Blu-ray': 'Blu-ray',\n    'Blu-ray-R': 'Blu-ray',\n    Cassette: 'Cassette',\n    CD: 'CD',\n    CDr: 'CD-R',\n    CDV: 'CDV',\n    'CD+G': 'CD+G',\n    Cylinder: 'Wax Cylinder',\n    DAT: 'DAT',\n    Datassette: 'Other',\n    DCC: 'DCC',\n    DVD: 'DVD',\n    DVDr: 'DVD',\n    'DVD-Audio': 'DVD-Audio',\n    'DVD-Video': 'DVD-Video',\n    'Edison Disc': 'Vinyl',\n    File: 'Digital Media',\n    'Flexi-disc': 'Vinyl',\n    'Floppy Disk': 'Other',\n    HDCD: 'HDCD',\n    'HD DVD': 'HD-DVD',\n    'HD DVD-R': 'HD-DVD',\n    Hybrid: 'Other',\n    Laserdisc: 'LaserDisc',\n    'Memory Stick': 'USB Flash Drive',\n    Microcassette: 'Other',\n    Minidisc: 'MiniDisc',\n    MVD: 'Other',\n    'Reel-To-Reel': 'Reel-to-reel',\n    SACD: 'SACD',\n    SelectaVision: 'Other',\n    Shellac: 'Shellac',\n    'Shellac7\"': '7\" Shellac',\n    'Shellac10\"': '10\" Shellac',\n    'Shellac12\"': '12\" Shellac',\n    SVCD: 'SVCD',\n    UMD: 'UMD',\n    VCD: 'VCD',\n    VHS: 'VHS',\n    'Video 2000': 'Other',\n    Vinyl: 'Vinyl',\n    'Vinyl7\"': '7\" Vinyl',\n    'Vinyl10\"': '10\" Vinyl',\n    'Vinyl12\"': '12\" Vinyl',\n    'Lathe Cut': 'Phonograph record',\n};\n\nconst Countries = {\n    Afghanistan: 'AF',\n    Albania: 'AL',\n    Algeria: 'DZ',\n    'American Samoa': 'AS',\n    Andorra: 'AD',\n    Angola: 'AO',\n    Anguilla: 'AI',\n    Antarctica: 'AQ',\n    'Antigua and Barbuda': 'AG',\n    Argentina: 'AR',\n    Armenia: 'AM',\n    Aruba: 'AW',\n    Australia: 'AU',\n    Austria: 'AT',\n    Azerbaijan: 'AZ',\n    Bahamas: 'BS',\n    Bahrain: 'BH',\n    Bangladesh: 'BD',\n    Barbados: 'BB',\n    'Barbados, The': 'BB',\n    Belarus: 'BY',\n    Belgium: 'BE',\n    Belize: 'BZ',\n    Benin: 'BJ',\n    Bermuda: 'BM',\n    Bhutan: 'BT',\n    Bolivia: 'BO',\n    Croatia: 'HR',\n    Botswana: 'BW',\n    'Bouvet Island': 'BV',\n    Brazil: 'BR',\n    'British Indian Ocean Territory': 'IO',\n    'Brunei Darussalam': 'BN',\n    Bulgaria: 'BG',\n    'Burkina Faso': 'BF',\n    Burundi: 'BI',\n    Cambodia: 'KH',\n    Cameroon: 'CM',\n    Canada: 'CA',\n    'Cape Verde': 'CV',\n    'Cayman Islands': 'KY',\n    'Central African Republic': 'CF',\n    Chad: 'TD',\n    Chile: 'CL',\n    China: 'CN',\n    'Christmas Island': 'CX',\n    'Cocos (Keeling) Islands': 'CC',\n    Colombia: 'CO',\n    Comoros: 'KM',\n    Congo: 'CG',\n    'Cook Islands': 'CK',\n    'Costa Rica': 'CR',\n    'Virgin Islands, British': 'VG',\n    Cuba: 'CU',\n    Cyprus: 'CY',\n    'Czech Republic': 'CZ',\n    Denmark: 'DK',\n    Djibouti: 'DJ',\n    Dominica: 'DM',\n    'Dominican Republic': 'DO',\n    Ecuador: 'EC',\n    Egypt: 'EG',\n    'El Salvador': 'SV',\n    'Equatorial Guinea': 'GQ',\n    Eritrea: 'ER',\n    Estonia: 'EE',\n    Ethiopia: 'ET',\n    'Falkland Islands (Malvinas)': 'FK',\n    'Faroe Islands': 'FO',\n    Fiji: 'FJ',\n    Finland: 'FI',\n    France: 'FR',\n    'French Guiana': 'GF',\n    'French Polynesia': 'PF',\n    'French Southern Territories': 'TF',\n    Gabon: 'GA',\n    Gambia: 'GM',\n    Georgia: 'GE',\n    Germany: 'DE',\n    Ghana: 'GH',\n    Gibraltar: 'GI',\n    Greece: 'GR',\n    Greenland: 'GL',\n    Grenada: 'GD',\n    Guadeloupe: 'GP',\n    Guam: 'GU',\n    Guatemala: 'GT',\n    Guinea: 'GN',\n    'Guinea-Bissau': 'GW',\n    Guyana: 'GY',\n    Haiti: 'HT',\n    'Virgin Islands, U.S.': 'VI',\n    Honduras: 'HN',\n    'Hong Kong': 'HK',\n    Hungary: 'HU',\n    Iceland: 'IS',\n    India: 'IN',\n    Indonesia: 'ID',\n    'Wallis and Futuna': 'WF',\n    Iraq: 'IQ',\n    Ireland: 'IE',\n    Israel: 'IL',\n    Italy: 'IT',\n    Jamaica: 'JM',\n    Japan: 'JP',\n    Jordan: 'JO',\n    Kazakhstan: 'KZ',\n    Kenya: 'KE',\n    Kiribati: 'KI',\n    Kuwait: 'KW',\n    Kyrgyzstan: 'KG',\n    \"Lao People's Democratic Republic\": 'LA',\n    Latvia: 'LV',\n    Lebanon: 'LB',\n    Lesotho: 'LS',\n    Liberia: 'LR',\n    'Libyan Arab Jamahiriya': 'LY',\n    Liechtenstein: 'LI',\n    Lithuania: 'LT',\n    Luxembourg: 'LU',\n    Montserrat: 'MS',\n    Macedonia: 'MK',\n    Madagascar: 'MG',\n    Malawi: 'MW',\n    Malaysia: 'MY',\n    Maldives: 'MV',\n    Mali: 'ML',\n    Malta: 'MT',\n    'Marshall Islands': 'MH',\n    Martinique: 'MQ',\n    Mauritania: 'MR',\n    Mauritius: 'MU',\n    Mayotte: 'YT',\n    Mexico: 'MX',\n    'Micronesia, Federated States of': 'FM',\n    Morocco: 'MA',\n    Monaco: 'MC',\n    Mongolia: 'MN',\n    Mozambique: 'MZ',\n    Myanmar: 'MM',\n    Namibia: 'NA',\n    Nauru: 'NR',\n    Nepal: 'NP',\n    Netherlands: 'NL',\n    'Netherlands Antilles': 'AN',\n    'New Caledonia': 'NC',\n    'New Zealand': 'NZ',\n    Nicaragua: 'NI',\n    Niger: 'NE',\n    Nigeria: 'NG',\n    Niue: 'NU',\n    'Norfolk Island': 'NF',\n    'Northern Mariana Islands': 'MP',\n    Norway: 'NO',\n    Oman: 'OM',\n    Pakistan: 'PK',\n    Palau: 'PW',\n    Panama: 'PA',\n    'Papua New Guinea': 'PG',\n    Paraguay: 'PY',\n    Peru: 'PE',\n    Philippines: 'PH',\n    Pitcairn: 'PN',\n    Poland: 'PL',\n    Portugal: 'PT',\n    'Puerto Rico': 'PR',\n    Qatar: 'QA',\n    Reunion: 'RE',\n    Romania: 'RO',\n    'Russian Federation': 'RU',\n    Russia: 'RU',\n    Rwanda: 'RW',\n    'Saint Kitts and Nevis': 'KN',\n    'Saint Lucia': 'LC',\n    'Saint Vincent and The Grenadines': 'VC',\n    Samoa: 'WS',\n    'San Marino': 'SM',\n    'Sao Tome and Principe': 'ST',\n    'Saudi Arabia': 'SA',\n    Senegal: 'SN',\n    Seychelles: 'SC',\n    'Sierra Leone': 'SL',\n    Singapore: 'SG',\n    Slovenia: 'SI',\n    'Solomon Islands': 'SB',\n    Somalia: 'SO',\n    'South Africa': 'ZA',\n    Spain: 'ES',\n    'Sri Lanka': 'LK',\n    Sudan: 'SD',\n    Suriname: 'SR',\n    Swaziland: 'SZ',\n    Sweden: 'SE',\n    Switzerland: 'CH',\n    'Syrian Arab Republic': 'SY',\n    Tajikistan: 'TJ',\n    'Tanzania, United Republic of': 'TZ',\n    Thailand: 'TH',\n    Togo: 'TG',\n    Tokelau: 'TK',\n    Tonga: 'TO',\n    'Trinidad & Tobago': 'TT',\n    Tunisia: 'TN',\n    Turkey: 'TR',\n    Turkmenistan: 'TM',\n    'Turks and Caicos Islands': 'TC',\n    Tuvalu: 'TV',\n    Uganda: 'UG',\n    Ukraine: 'UA',\n    'United Arab Emirates': 'AE',\n    UK: 'GB',\n    US: 'US',\n    'United States Minor Outlying Islands': 'UM',\n    Uruguay: 'UY',\n    Uzbekistan: 'UZ',\n    Vanuatu: 'VU',\n    'Vatican City State (Holy See)': 'VA',\n    Venezuela: 'VE',\n    'Viet Nam': 'VN',\n    'Western Sahara': 'EH',\n    Yemen: 'YE',\n    Zambia: 'ZM',\n    Zimbabwe: 'ZW',\n    Taiwan: 'TW',\n    '[Worldwide]': 'XW',\n    Europe: 'XE',\n    USSR: 'SU',\n    'East Germany (historical, 1949-1990)': 'XG',\n    Czechoslovakia: 'XC',\n    'Congo, Republic of the': 'CD',\n    Slovakia: 'SK',\n    'Bosnia & Herzegovina': 'BA',\n    \"Korea (North), Democratic People's Republic of\": 'KP',\n    'North Korea': 'KP',\n    'Korea (South), Republic of': 'KR',\n    'South Korea': 'KR',\n    Montenegro: 'ME',\n    'South Georgia and the South Sandwich Islands': 'GS',\n    'Palestinian Territory': 'PS',\n    Macao: 'MO',\n    'Timor-Leste': 'TL',\n    '<85>land Islands': 'AX',\n    Guernsey: 'GG',\n    'Isle of Man': 'IM',\n    Jersey: 'JE',\n    Serbia: 'RS',\n    'Saint Barthélemy': 'BL',\n    'Saint Martin': 'MF',\n    Moldova: 'MD',\n    Yugoslavia: 'YU',\n    'Serbia and Montenegro': 'CS',\n    \"Côte d'Ivoire\": 'CI',\n    'Heard Island and McDonald Islands': 'HM',\n    'Iran, Islamic Republic of': 'IR',\n    'Saint Pierre and Miquelon': 'PM',\n    'Saint Helena': 'SH',\n    'Svalbard and Jan Mayen': 'SJ',\n};\n","name":"","extension":"txt","url":"https://www.irccloud.com/pastebin/6an6NUyU","modified":1654111692,"id":"6an6NUyU","size":39820,"lines":1118,"own_paste":false,"theme":"","date":1654111692}