VKDownloadMedia

Скачать фото/аудио/видео-файлы с соц. сети ВКонтакте.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        VKDownloadMedia
// @description Скачать фото/аудио/видео-файлы с соц. сети ВКонтакте.
// @namespace   https://github.com/KJ86/VKDownloadMedia
// @version     6.1.3
// @date        2019-10-14
// @author      KJ86
// @icon        data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjODI4YTk5IiBkPSJtIDEwLDYgaCA0IHYgNiBoIDMgbCAtNSw2IC01LC02IGggMyB6IiBwYWludC1vcmRlcj0ibWFya2VycyBzdHJva2UgZmlsbCIvPjwvc3ZnPg==
// @homepage    https://greasyfork.org/ru/scripts/7385-vkdownloadmedia
// @supportURL  https://vk.com/vkdownloadmedia
// @include     https://vk.com/*
// @run-at      document-end
// @grant       unsafeWindow
// @grant       GM_xmlhttpRequest
// @grant       GM_download
// @grant       GM_addStyle
// @connect     self
// @connect     *
// @nocompat    Chrome
// @noframes
// ==/UserScript==
(function () {
    'use strict';

    /** @var {Object} unsafeWindow */
    /** @var {Function} GM_addStyle */
    /** @var {Function} GM_download */
    /** @var {Function} GM_xmlhttpRequest */
    /** @var {Function} Promise */
    /** @var {Function} geByClass */
    /** @var {Function} geByClass1 */
    /** @var {Function} ge */
    /** @var {Function} se */
    /** @var {Function} re */
    /** @var {Function} ce */
    /** @var {Function} addEvent */
    /** @var {Function} cancelEvent */
    /** @var {Function} domInsertAfter */
    /** @var {Object} Videoview */
    /** @var {Object} tooltips */
    /** @var {Object} mvcur */
    /** @var {Object} ap */
    /** @var {Function} domInsertBefore */
    /** @var {Function} matchesSelector */
    /** @var {Function} showTooltip */
    /** @var {Function} uiActionsMenu */
    /** @var {Function} domClosest */
    /** @var {Function} getProgressHtml */
    /** @var {Function} setStyle */
    /** @var {Function} showFastBox */
    /** @var {Function} domQuery */
    /** @var {Function} data */
    /** @var {Function} each */

    /**
     * @type {Window}
     */
    var win = unsafeWindow || window;

    // Check supported browsers
    if (typeof win.Promise === 'undefined') return;

    /**
     * @function
     */
    var DOMNodeInserted = (function () {
        var _callbacks = [];
        var element = document.documentElement;
        var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

        if (MutationObserver) {
            (new MutationObserver(function (mutations) {
                var addedNodes = [];

                mutations.forEach(function (mutation) {
                    if (mutation.addedNodes.length) {
                        for (var i = 0, l = mutation.addedNodes.length; i < l; i++) {
                            if (mutation.addedNodes[i].nodeType === Node.ELEMENT_NODE) {
                                addedNodes.push(mutation.addedNodes[i]);
                            }
                        }
                    }
                });

                if (addedNodes.length) {
                    fire(addedNodes);
                }
            })).observe(element, {
                childList: true,
                subtree: true
            });
        } else { // Fallback
            element.addEventListener('DOMNodeInserted', function (e) {
                if (e.target.nodeType === Node.ELEMENT_NODE) {
                    fire([e.target]);
                }
            }, false);
        }

        function fire(addedNodes) {
            addedNodes.forEach(function (node) {
                for (var i = 0, l = _callbacks.length; i < l; i++) {
                    var selector = _callbacks[i].selector;
                    var callback = _callbacks[i].callback;
                    var isOnce = _callbacks[i].isOnce;

                    if (matchesSelector(node, selector)) {
                        callback.call(_callbacks, node);

                        if (isOnce) {
                            _callbacks.splice(i, 1);
                            return;
                        }
                    }

                    var found = node.querySelectorAll(selector);

                    if (found.length) {
                        for (var j = 0, l2 = found.length; j < l2; j++) {
                            callback.call(_callbacks, found[j]);

                            if (isOnce) {
                                _callbacks.splice(i, 1);
                                return;
                            }
                        }
                    }
                }
            });
        }

        return function (selector, callback, isOnce) {
            _callbacks.push({
                selector: selector,
                callback: callback,
                isOnce: !!isOnce
            });
        };
    })();

    /**
     * @namespace
     */
    var VKDM = {
        /**
         * @param {Array|String} [mask]
         * @returns {Promise}
         */
        audioUnmaskSource: function fn(mask) {
            if (typeof fn.module === 'undefined') {
                var ORIGIN = 'https://m.vk.com';

                fn.module = request({
                    url: ORIGIN,
                    _isAjax: false
                }).then(function (xhr) {
                    var match = xhr.response.match(/src="(.+?common\..+?js.*?)"/);
                    var commonJsSrc = match && match[1];

                    if (commonJsSrc) {
                        return request({
                            url: ORIGIN + commonJsSrc,
                            _isAjax: false
                        }).then(function (xhr) {
                            var modules = {};

                            // Inject and exec
                            eval(xhr.response.replace(/^!function.*?\{/, '$& var a0 = arguments[0]; for (var p in a0) {if (a0[p].toString().indexOf(".audioUnmaskSource=") !== -1) {a0[p](null, modules);break;}} return;'));

                            if ('audioUnmaskSource' in modules) {
                                return modules;
                            }

                            return Promise.reject();
                        });
                    }

                    return Promise.reject();
                });
            }

            return fn.module.then(function (modules) {
                if (mask) {
                    if (Array.isArray(mask)) {
                        return mask.map(function (item) {
                            return modules.audioUnmaskSource(item);
                        });
                    }

                    return modules.audioUnmaskSource(mask);
                }
            });
        },

        /**
         * @param ids
         * @returns {Promise}
         */
        audioRequestData: function (ids) {
            return request({
                url: '/al_audio.php',
                method: 'POST',
                data: {
                    'act': 'reload_audio',
                    'al': '1',
                    'ids': ids
                },
                _isMobile: true
            }).then(function (xhr) {
                var items;

                try {
                    var json = JSON.parse(xhr.response);

                    items = json.payload[1][0];
                } catch (e) {
                    // Deprecated
                    try {
                        if (xhr.response.indexOf('<!>') !== -1) {
                            items = JSON.parse(xhr.response.split('<!>')[5].replace('<!json>', ''));
                        }
                    } catch (e) {}
                }

                if (Array.isArray(items)) {
                    return items;
                }

                return Promise.reject();
            });
        },

        audioShowActionTooltip: function (btn) {
            var audioRow = domClosest('audio_row', btn);
            var getTText = function () {
                var duration = data(audioRow, 'vkdm_audio_row_duration');
                var fileSize = data(audioRow, 'vkdm_audio_row_file_size');
                var fileSizeMByte = '...';
                var bitrate = '...';

                if (duration && fileSize) {
                    fileSizeMByte = (fileSize / 1024 / 1024).toFixed(1) + ' МБ';
                    bitrate = '~' + (fileSize * 8 / duration / 1000).toFixed(0) + ' кбит/с';
                }

                return 'Скачать аудиозапись<br>Битрейт: ' + bitrate + '<br>Размер: ' + fileSizeMByte;
            };

            if (!data(audioRow, 'vkdm_audio_row_duration') && !btn.classList.contains('vkdm_ajax_in_progress')) {
                var audioData = JSON.parse(audioRow.getAttribute('data-audio'));
                var hashes = audioData[13].split('/');
                var ids = audioRow.getAttribute('data-full-id') + '_' + hashes[2] + '_' + hashes[5];

                btn.classList.add('vkdm_ajax_in_progress');

                VKDM.audioRequestData(ids).then(function (items) {
                    VKDM.audioUnmaskSource(items[0][2]).then(function (url) {
                        request({
                            url: url,
                            method: 'HEAD'
                        }).then(function (xhr) {
                            var contentLength = xhr.getResponseHeader('content-length');

                            data(audioRow, 'vkdm_audio_row_duration', parseInt(items[0][5]));
                            data(audioRow, 'vkdm_audio_row_file_size', parseInt(contentLength));

                            if (document.body.contains(btn)) {
                                btn.classList.remove('vkdm_ajax_in_progress');
                                geByClass1('tt_text', btn.tt.container).innerHTML = getTText();
                                tooltips.rePositionTT(btn.tt);
                            }
                        });
                    });
                });
            }

            showTooltip(btn, {
                text: getTText,
                black: 1,
                shift: [7, 5, 0],
                needLeft: true
            });
        },

        downloadAudio: function (btn) {
            var audioRow = domClosest('audio_row', btn);
            var audioData = JSON.parse(audioRow.getAttribute('data-audio'));
            var hashes = audioData[13].split('/');
            var ids = audioRow.getAttribute('data-full-id') + '_' + hashes[2] + '_' + hashes[5];

            VKDM.audioRequestData(ids).then(function (items) {
                VKDM.audioUnmaskSource(items[0][2]).then(function (url) {
                    GM_download(url, ce('div', {innerHTML: items[0][4] + ' &ndash; ' + items[0][3]}).textContent + getExtension(url));
                });
            });
        },

        downloadPhotoAlbumsList: function () {
            var isFastBoxClosed = false;
            var downloadListBtnWrapID = getRandomID();
            var fileName = document.title.replace(/"/g, '');
            var progress = se(getProgressHtml('', 'pr_medium'));

            setStyle(progress, {
                'opacity': 1,
                'padding-top': '11px',
                'padding-bottom': '12px'
            });

            showFastBox({
                title: 'Скачать альбом',
                dark: 1,
                hideButtons: 1,
                onShow: function () {
                    var items = [];
                    var match = location.pathname.match(/album(-?[0-9]+)_([0-9]+)/);
                    var ownerId = match[1];
                    var albumId = match[2];
                    var count = 1000;

                    switch (albumId) {
                        case '0':
                            albumId = 'profile';
                            break;
                        case '00':
                            albumId = 'wall';
                            break;
                        case '000':
                            albumId = 'saved';
                            break;
                    }

                    void function getAllItems(offset) {
                        if (isFastBoxClosed === true) return;

                        var params = [
                            'owner_id=' + ownerId,
                            'album_id=' + albumId,
                            'count=' + count,
                            'offset=' + offset,
                            'access_token=08ee532d08ee532d08ee532dbc08a6df23008ee08ee532d54ae800969245a7851077ff5',
                            'v=5.53'
                        ];

                        request.getJSONP('https://api.vk.com/method/photos.get?' + params.join('&'), function (data) {
                            items = items.concat(data.items);

                            if (items.length === data.count) {
                                var srcArr = [];

                                items.forEach(function (item) {
                                    var nSizes = [];

                                    for (var prop in item) {
                                        var match = prop.match(/photo_(\d+)/);

                                        if (match) {
                                            nSizes.push(match[1]);
                                        }
                                    }

                                    if (nSizes.length) {
                                        srcArr.push(item['photo_' + Math.max.apply(null, nSizes)]);
                                    }
                                });

                                var downloadListBtnWrap = ge(downloadListBtnWrapID);

                                if (downloadListBtnWrap) {
                                    var url = createFile('text/plain;charset=utf-8', srcArr.join('\r\n'));

                                    downloadListBtnWrap.innerHTML = '<a href="' + url + '" class="flat_button secondary" download="' + fileName + '.txt">Список URL</a>';
                                }
                            } else {
                                getAllItems(offset + count);
                            }
                        });
                    }(0);
                },
                onBeforeHide: function () {
                    isFastBoxClosed = true;

                    each(domQuery('#' + downloadListBtnWrapID + ' a'), function (i, el) {
                        (window.URL || window.webkitURL).revokeObjectURL(el.href);
                    });
                }
            }, (
            '<div style="text-align: center;">' +
            '<h4 class="subheader">' + fileName + '</h4>' +
            '<div id="' + downloadListBtnWrapID + '">' + progress.outerHTML + '</div>' +
            '</div>'
            ));
        },

        downloadCurrentAudioPlayList: function () {
            var audioIdsArr = [];
            var isFastBoxClosed = false;
            var downloadListBtnWrapID = getRandomID();
            var playListTitle = '';

            (function () {
                var items = [];
                var title = '';

                if ('_list' in ap._currentPlaylist) {
                    items = ap._currentPlaylist._list;
                    title = ap._currentPlaylist._title;
                } else {
                    for (var prop in ap._currentPlaylist) {
                        if ('_list' in ap._currentPlaylist[prop]) {
                            items = ap._currentPlaylist[prop]['_list'];
                            title = ap._currentPlaylist[prop]['_title'];
                            break;
                        }
                    }
                }

                each(items, function (i, el) {
                    var hashes = el[13].split('/');

                    audioIdsArr.push(el[1] + '_' + el[0] + '_' + hashes[2] + '_' + hashes[5]);
                });

                playListTitle = (title || 'playlist') + ' (' + items.length + ')';
            })();

            if (audioIdsArr.length) {
                var progress = se(getProgressHtml('', 'pr_medium'));

                setStyle(progress, {
                    'opacity': 1,
                    'padding-top': '11px',
                    'padding-bottom': '12px'
                });

                showFastBox({
                    title: 'Скачать плейлист',
                    dark: 1,
                    hideButtons: 1,
                    onShow: function () {
                        var dataArr = [];

                        void function getAudioLinks(ids) {
                            if (isFastBoxClosed === true) return;

                            VKDM.audioRequestData(ids).then(function (items) {
                                if (!Array.isArray(items)) {
                                    setTimeout(function () {
                                        getAudioLinks(ids);
                                    }, 10000);

                                    return;
                                }

                                items.forEach(function (item) {
                                    if (item[2]) {
                                        dataArr.push({
                                            mask: item[2],
                                            name: item[4] + ' – ' + item[3],
                                            duration: item[5]
                                        });
                                    }
                                });

                                if (audioIdsArr.length) {
                                    getAudioLinks(audioIdsArr.splice(0, 10).join(','));
                                } else {
                                    var textFileData = [];
                                    var m3uFileData = ['#EXTM3U'];

                                    VKDM.audioUnmaskSource(dataArr.map(function (el) {
                                        return el.mask;
                                    })).then(function (urlArr) {
                                        dataArr.forEach(function (el, i) {
                                            textFileData.push(urlArr[i]);
                                            m3uFileData.push('#EXTINF:' + el.duration + ', ' + el.name + '\r\n' + urlArr[i]);
                                        });

                                        var textFileURL = createFile('text/plain;charset=utf-8', textFileData.join('\r\n'));
                                        var m3uFileURL = createFile('audio/x-mpegurl;charset=utf-8', m3uFileData.join('\r\n'));
                                        var downloadListBtnWrap = ge(downloadListBtnWrapID);

                                        if (textFileURL && m3uFileURL) {
                                            downloadListBtnWrap.innerHTML = (
                                            '<a href="' + m3uFileURL + '" class="flat_button secondary" download="' + playListTitle + '.m3u8">M3U</a>&nbsp;' +
                                            '<a href="' + textFileURL + '" class="flat_button secondary" download="' + playListTitle + '.txt">Список URL</a>'
                                            );
                                        } else {
                                            downloadListBtnWrap.textContent = 'Не удалось создать файл.';
                                        }
                                    });
                                }
                            });
                        }(audioIdsArr.splice(0, 10).join(','));
                    },
                    onBeforeHide: function () {
                        isFastBoxClosed = true;

                        each(domQuery('#' + downloadListBtnWrapID + ' a'), function (i, el) {
                            (window.URL || window.webkitURL).revokeObjectURL(el.href);
                        });
                    }
                }, (
                '<div style="text-align: center;">' +
                '<h4 class="subheader">' + playListTitle + '</h4>' +
                '<div id="' + downloadListBtnWrapID + '">' + progress.outerHTML + '</div>' +
                '</div>'
                ));
            }
        }
    };

    // region Init
    // Add CSS rules
    GM_addStyle(
        '.audio_row .audio_row__action_download {background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjODI4YTk5IiBkPSJtIDEwLDYgaCA0IHYgNiBoIDMgbCAtNSw2IC01LC02IGggMyB6IiBwYWludC1vcmRlcj0ibWFya2VycyBzdHJva2UgZmlsbCIvPjwvc3ZnPg==");}' +
        '.audio_layer_container .audio_page__footer_download_playlist {float: right; cursor: pointer;}' +
        '.audio_layer_container .audio_page__footer_download_playlist:hover {text-decoration: underline;}'
    );

    // Preload module
    VKDM.audioUnmaskSource('').then(null);

    // Adding audio download button
    DOMNodeInserted('.audio_row:not(.audio_claimed) .audio_row__actions', function (node) {
        // When module ready
        VKDM.audioUnmaskSource('').then(function () {
            var btn = se('<button aria-label="Скачать аудиозапись" data-action="download" class="audio_row__action audio_row__action_download"></button>');

            addEvent(btn, 'click', function (e) {
                VKDM.downloadAudio(this);
                cancelEvent(e);
            });
            addEvent(btn, 'mouseover', function () {
                VKDM.audioShowActionTooltip(this);
            });
            domInsertBefore(btn, node.firstElementChild);
        });
    });

    // Adding video download button
    DOMNodeInserted('#video_player', function () {
        if (Videoview && Videoview.showVideo) {
            var addAction = function () {
                try {
                    if (mvcur && mvcur.player) {
                        var videoData = mvcur.player.getVars();

                        if (videoData) {
                            var prop, match;
                            var items = [];

                            for (prop in videoData) {
                                match = prop.match(/url(\d+)/);

                                if (match) {
                                    items.push([parseInt(match[1], 10), videoData[prop]]);
                                }
                            }

                            if (items.length) {
                                var itemsHTML = items
                                    .sort(function (a, b) {
                                        if (a[0] < b[0]) return -1;
                                        if (a[0] > b[0]) return 1;
                                        return 0;
                                    })
                                    .map(function (item) {
                                        return (
                                        '<a class="ui_actions_menu_item" href="' + item[1] + '" tabindex="0" role="link">' +
                                        item[0] + 'p' + (item[0] >= 720 ? ' <span style="font-size:10px;color:#939393;vertical-align:top;">HD</span>' : '') +
                                        '</a>'
                                        );
                                    })
                                    .join('');

                                var actionEl = se(
                                    '<div class="ui_actions_menu_wrap _ui_menu_wrap " onmouseover="uiActionsMenu.show(this, event, {autopos: true, dy: 6});" onmouseout="uiActionsMenu.hide(this);">' +
                                    '<div class="ui_actions_menu_icons"  tabindex="0" aria-label="Действия" role="button" onclick="uiActionsMenu.keyToggle(this, event);">' +
                                    '<span class="blind_label">Действия</span>' +
                                    '<a class="ui_actions_menu_more">Скачать</a>' +
                                    '</div>' +
                                    '<div class="ui_actions_menu _ui_menu" >' +
                                    itemsHTML +
                                    '</div>' +
                                    '</div>'
                                );

                                geByClass('ui_actions_menu_item', actionEl).forEach(function (element) {
                                    addEvent(element, 'click', function (e) {
                                        e.originalEvent.preventDefault();
                                        GM_download(this.href, ge('mv_title').textContent + getExtension(this.href));
                                    });
                                });

                                domInsertAfter(actionEl, geByClass1('ui_actions_menu_more').parentNode.parentNode);
                            }
                        }
                    }
                } catch (e) {
                }
            };
            var _showVideo = Videoview.showVideo;

            Videoview.showVideo = function () {
                _showVideo.apply(this, arguments);
                addAction();
            };

            addAction();
        }
    }, true);

    // Adding photo download button
    DOMNodeInserted('.photos_album_intro_info', (function () {
        var element = geByClass1('photos_album_intro_info');

        if (element) {
            handler(element);
        }

        function handler(node) {
            if (!ge('vkdm_download_album')) {
                var act = se('<a id="vkdm_download_album" href="javascript:;">Скачать альбом</a>');
                var actWrap = se('<span class="photos_album_info"></span>');

                addEvent(act, 'click', function () {
                    VKDM.downloadPhotoAlbumsList();
                    return false;
                });

                actWrap.appendChild(act);
                node.appendChild(se('<span class="divide">|</span>'));
                node.appendChild(actWrap);
            }
        }

        return handler;
    })());

    // Adding playlist download button
    DOMNodeInserted('#page_header .audio_page__footer_clear_playlist', function (node) {
        var act = se('<span class="audio_page__footer_download_playlist">Скачать плейлист</span>');

        addEvent(act, 'click', function () {
            VKDM.downloadCurrentAudioPlayList();
        });

        domInsertAfter(act, node);
    });
    // endregion Init

    // region Helpers
    /**
     * @param {Object} options
     * @returns {Promise}
     */
    function request(options) {
        return new Promise(function (resolve, reject) {
            options.method = options.method || 'GET';
            options.headers = options.headers || {};

            if (options._isAjax !== false) {
                options.headers['x-requested-with'] = 'XMLHttpRequest';
            }

            if (options._isMobile) {
                options.headers['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240';
            }

            // POST
            if (options.method.toUpperCase() === 'POST') {
                options.headers['content-type'] = 'application/x-www-form-urlencoded';

                if (typeof options.data === 'object') {
                    var dataArr = [];

                    for (var p in options.data) {
                        dataArr.push(encodeURIComponent(p) + '=' + encodeURIComponent(options.data[p]));
                    }

                    options.data = dataArr.join('&');
                }
            }

            options.onload = function (xhr) {
                if (xhr.status === 200) {
                    xhr.getResponseHeader = function (name) {
                        var header;
                        var headers = xhr.responseHeaders.split('\n');

                        for (var i = 0; i < headers.length; i++) {
                            header = headers[i].split(':');

                            if (header[0].trim().toLowerCase() === name.toLowerCase()) {
                                return header[1].trim();
                            }
                        }

                        return null;
                    };

                    resolve(xhr);
                } else {
                    reject(xhr);
                }
            };

            options.onerror = function (xhr) {
                reject(xhr);
            };

            GM_xmlhttpRequest(options);
        });
    }

    /**
     * @param {String} url
     * @param {Function} [success]
     */
    request.getJSONP = function (url, success) {
        var tempFuncName = 'vkdmFunc' + Date.now();

        win[tempFuncName] = function (response) {
            success(response.response);
            delete win[tempFuncName];
        };

        document.body.appendChild(ce('script', {
            src: url + '&callback=' + tempFuncName,
            onload: function () {
                re(this);
            },
            onerror: function () {
                re(this);
            }
        }));
    };

    function createFile(type, data) {
        return (window.URL || window.webkitURL).createObjectURL(new Blob([data], {type: type}));
    }

    function getExtension(url) {
        return url.split('?')[0].match(/\.\w+$/)[0];
    }

    function getRandomID(name) {
        var prefix = 'vkdm' + (name ? '_' + name + '_' : '_');

        return prefix + Math.random().toString().slice(2, 10);
    }
    // endregion Helpers
})();