# 解决 Hexo+Shoka 背景音乐无法播放的问题 - Hexo - Docker - 极空间 | ☆∵∴Hito∴∵★ = 涛声依旧 = 天下事有难易乎？为之，则难者亦易矣
Keywords: 博客,Hexo,Shoka,背景音乐

#### # 问题描述

今天突然发现博客的音乐无法播放了，经过一顿查找，发现是因为 https://api.i-meto.com/meting/api 获取的音乐地址出现了问题，估计是被网易云给封禁了。shoka 主题应该是通过这个网站提取音乐地址、图片等信息。通过断点发现网易播放列表还是能正常获取的，只是列表中的具体音乐、图片出现了问题。因为 meto 是把音乐的 url 指向自己的服务地址，然后通过参数获取的最终音乐，所以我通过参数拼凑出最终的网易云音乐地址，自此问题基本解决。只针对网易播放列表，图片只能使用一张固定的（因为图片的具体规律不知道 O (∩_∩) O~）。

#### # 解决方法

到 themes\shoka\source\js\_app 目录下打开 player.js 文件，替换成如下代码。

    var NOWPLAYING = null

    const isMobile = /mobile/i.test(window.navigator.userAgent);

    const mediaPlayer = function (t, config) {

        var option = {

            type: 'audio',

            mode: 'random',

            btns: ['play-pause', 'music'],

            controls: ['mode', 'backward', 'play-pause', 'forward', 'volume'],

            events: {

                "play-pause": function (event) {

                    if (source.paused) {

                        t.player.play()

                    } else {

                        t.player.pause()

                    }

                },

                "music": function (event) {

                    if (info.el.hasClass('show')) {

                        info.hide()

                    } else {

                        info.el.addClass('show');

                        playlist.scroll().title()

                    }

                }

            }

        }, utils = {

            random: function (len) {

                return Math.floor((Math.random() * len))

            },

            parse: function (link) {

                var result = [];

                [

                    ['music.163.com.*song.*id=(\\d+)', 'netease', 'song'],

                    ['music.163.com.*album.*id=(\\d+)', 'netease', 'album'],

                    ['music.163.com.*artist.*id=(\\d+)', 'netease', 'artist'],

                    ['music.163.com.*playlist.*id=(\\d+)', 'netease', 'playlist'],

                    ['music.163.com.*discover/toplist.*id=(\\d+)', 'netease', 'playlist'],

                    ['y.qq.com.*song/(\\w+).html', 'tencent', 'song'],

                    ['y.qq.com.*album/(\\w+).html', 'tencent', 'album'],

                    ['y.qq.com.*singer/(\\w+).html', 'tencent', 'artist'],

                    ['y.qq.com.*playsquare/(\\w+).html', 'tencent', 'playlist'],

                    ['y.qq.com.*playlist/(\\w+).html', 'tencent', 'playlist'],

                    ['xiami.com.*song/(\\w+)', 'xiami', 'song'],

                    ['xiami.com.*album/(\\w+)', 'xiami', 'album'],

                    ['xiami.com.*artist/(\\w+)', 'xiami', 'artist'],

                    ['xiami.com.*collect/(\\w+)', 'xiami', 'playlist'],

                ].forEach(function (rule) {

                    var patt = new RegExp(rule[0])

                    var res = patt.exec(link)

                    if (res !== null) {

                        result = [rule[1], rule[2], res[1]]

                    }

                })

                return result

            },

            fetch: function (source) {

                var list = []

                return new Promise(function (resolve, reject) {

                    source.forEach(function (raw) {

                        var meta = utils.parse(raw)

                        if (meta[0]) {

                            var skey = JSON.stringify(meta)

                            var playlist = store.get(skey)

                            if (playlist) {

                                // 自己修改 - start

                                var audioInfos = JSON.parse(playlist);

                                audioInfos.forEach(function (item, i) {

                                    item.company = meta[0];

                                });

                                list.push.apply(list, audioInfos);

                                resolve(list);

                                // 自己修改 - end

                            } else {

                                fetch('https://api.i-meto.com/meting/api?server=' + meta[0] + '&type=' + meta[1] + '&id=' + meta[2] + '&r=' + Math.random())

                                    .then(function (response) {

                                        return response.json()

                                    }).then(function (json) {

                                        store.set(skey, JSON.stringify(json))

                                        list.push.apply(list, json);

                                        resolve(list);

                                    }).catch(function (ex) { })

                            }

                        } else {

                            list.push(raw);

                            resolve(list);

                        }

                    })

                })

            },

            secondToTime: function (second) {

                var add0 = function (num) {

                    return isNaN(num) ? '00' : (num < 10 ? '0' + num : '' + num)

                };

                var hour = Math.floor(second / 3600);

                var min = Math.floor((second - hour * 3600) / 60);

                var sec = Math.floor(second - hour * 3600 - min * 60);

                return (hour > 0 ? [hour, min, sec] : [min, sec]).map(add0).join(':');

            },

            nameMap: {

                dragStart: isMobile ? 'touchstart' : 'mousedown',

                dragMove: isMobile ? 'touchmove' : 'mousemove',

                dragEnd: isMobile ? 'touchend' : 'mouseup',

            }

        }, source = null;

        t.player = {

            _id: utils.random(999999),

            group: true,

            // 加载播放列表

            load: function (newList) {

                var d = ""

                var that = this

                if (newList && newList.length > 0) {

                    if (this.options.rawList !== newList) {

                        this.options.rawList = newList;

                        playlist.clear()

                        // 获取新列表

                        //this.fetch()

                    }

                } else {

                    // 没有列表时，隐藏按钮

                    d = "none"

                    this.pause()

                }

                for (var el in buttons.el) {

                    buttons.el[el].display(d)

                }

                return this

            },

            fetch: function () {

                var that = this;

                return new Promise(function (resolve, reject) {

                    if (playlist.data.length > 0) {

                        resolve()

                    } else {

                        if (that.options.rawList) {

                            var promises = [];

                            that.options.rawList.forEach(function (raw, index) {

                                promises.push(new Promise(function (resolve, reject) {

                                    var group = index

                                    var source

                                    if (!raw.list) {

                                        group = 0

                                        that.group = false

                                        source = [raw]

                                    } else {

                                        that.group = true

                                        source = raw.list

                                    }

                                    utils.fetch(source).then(function (list) {

                                        playlist.add(group, list)

                                        resolve()

                                    })

                                }))

                            })

                            Promise.all(promises).then(function () {

                                resolve(true)

                            })

                        }

                    }

                }).then(function (c) {

                    if (c) {

                        playlist.create()

                        controller.create()

                        that.mode()

                    }

                })

            },

            // 根据模式切换当前曲目 index

            mode: function () {

                var total = playlist.data.length;

                if (!total || playlist.errnum == total)

                    return;

                var step = controller.step == 'next' ? 1 : -1

                var next = function () {

                    var index = playlist.index + step

                    if (index > total || index < 0) {

                        index = controller.step == 'next' ? 0 : total - 1;

                    }

                    playlist.index = index;

                }

                var random = function () {

                    var p = utils.random(total)

                    if (playlist.index !== p) {

                        playlist.index = p

                    } else {

                        next()

                    }

                }

                switch (this.options.mode) {

                    case 'random':

                        random()

                        break;

                    case 'order':

                        next()

                        break;

                    case 'loop':

                        if (controller.step)

                            next()

                        if (playlist.index == -1)

                            random()

                        break;

                }

                this.init()

            },

            // 直接设置当前曲目 index

            switch: function (index) {

                if (typeof index == 'number'

                    && index != playlist.index

                    && playlist.current()

                    && !playlist.current().error) {

                    playlist.index = index;

                    this.init()

                }

            },

            // 更新 source 为当前曲目 index

            init: function () {

                var item = playlist.current()

                if (!item || item['error']) {

                    this.mode();

                    return;

                }

                var playing = false;

                if (!source.paused) {

                    playing = true

                    this.stop()

                }

                source.attr('src', item.url);

                source.attr('title', item.name + ' - ' + item.artist);

                this.volume(store.get('_PlayerVolume') || '0.7')

                this.muted(store.get('_PlayerMuted'))

                progress.create()

                if (this.options.type == 'audio')

                    preview.create()

                if (playing == true) {

                    this.play()

                }

            },

            play: function () {

                NOWPLAYING && NOWPLAYING.player.pause()

                if (playlist.current().error) {

                    this.mode();

                    return;

                }

                var that = this

                source.play().then(function () {

                    playlist.scroll()

                }).catch(function (e) { });

            },

            pause: function () {

                source.pause()

                document.title = originTitle

            },

            stop: function () {

                source.pause();

                source.currentTime = 0;

                document.title = originTitle;

            },

            seek: function (time) {

                time = Math.max(time, 0)

                time = Math.min(time, source.duration)

                if (isNaN(time)) {

                    time = 0;

                }

                source.currentTime = time;

                progress.update(time / source.duration)

            },

            muted: function (status) {

                if (status == 'muted') {

                    source.muted = status

                    store.set('_PlayerMuted', status)

                    controller.update(0)

                } else {

                    store.del('_PlayerMuted')

                    source.muted = false

                    controller.update(source.volume)

                }

            },

            volume: function (percentage) {

                if (!isNaN(percentage)) {

                    controller.update(percentage)

                    store.set('_PlayerVolume', percentage)

                    source.volume = percentage

                }

            },

            mini: function () {

                info.hide()

            }

        };

        var info = {

            el: null,

            create: function () {

                if (this.el)

                    return;

                this.el = t.createChild('div', {

                    className: 'player-info',

                    innerHTML: (t.player.options.type == 'audio' ? '<div class="preview"></div>' : '') + '<div class="controller"></div><div class="playlist"></div>'

                }, 'after');

                preview.el = this.el.child(".preview");

                playlist.el = this.el.child(".playlist");

                controller.el = this.el.child(".controller");

            },

            hide: function () {

                var el = this.el

                el.addClass('hide');

                window.setTimeout(function () {

                    el.removeClass('show hide')

                }, 300);

            }

        }

        var playlist = {

            el: null,

            data: [],

            index: -1,

            errnum: 0,

            add: function (group, list) {

                var that = this

                list.forEach(function (item, i) {

                    item.group = group;

                    item.name = item.name || item.title || 'Meida name';

                    item.artist = item.artist || item.author || 'Anonymous';

                    item.cover = item.cover || item.pic;

                    item.type = item.type || 'normal';

                    that.data.push(item);

                });

            },

            clear: function () {

                this.data = []

                this.el.innerHTML = ""

                if (this.index !== -1) {

                    this.index = -1

                    t.player.fetch()

                }

            },

            create: function () {

                var el = this.el

                this.data.map(function (item, index) {

                    if (item.el)

                        return

                    var id = 'list-' + t.player._id + '-' + item.group

                    var tab = $('#' + id)

                    if (!tab) {

                        tab = el.createChild('div', {

                            id: id,

                            className: t.player.group ? 'tab' : '',

                            innerHTML: '<ol></ol>',

                        })

                        if (t.player.group) {

                            tab.attr('data-title', t.player.options.rawList[item.group]['title'])

                                .attr('data-id', t.player._id)

                        }

                    }

                    item.el = tab.child('ol').createChild('li', {

                        title: item.name + ' - ' + item.artist,

                        innerHTML: '<span class="info"><span>' + item.name + '</span><span>' + item.artist + '</span></span>',

                        onclick: function (event) {

                            var current = event.currentTarget;

                            if (playlist.index === index && progress.el) {

                                if (source.paused) {

                                    t.player.play();

                                } else {

                                    t.player.seek(source.duration * progress.percent(event, current))

                                }

                                return;

                            }

                            t.player.switch(index);

                            t.player.play();

                        }

                    })

                    return item

                })

                tabFormat()

            },

            current: function () {

                return this.data[this.index]

            },

            scroll: function () {

                var item = this.current()

                var li = this.el.child('li.active')

                li && li.removeClass('active')

                var tab = this.el.child('.tab.active')

                tab && tab.removeClass('active')

                li = this.el.find('.nav li')[item.group]

                li && li.addClass('active')

                tab = this.el.find('.tab')[item.group]

                tab && tab.addClass('active')

                pageScroll(item.el, item.el.offsetTop)

                return this

            },

            title: function () {

                if (source.paused)

                    return

                var current = this.current()

                document.title = 'Now Playing...' + current['name'] + ' - ' + current['artist'] + ' | ' + originTitle;

            },

            error: function () {

                var current = this.current()

                current.el.removeClass('current').addClass('error')

                current.error = true

                this.errnum++

            }

        }

        var lyrics = {

            el: null,

            data: null,

            index: 0,

            create: function (box) {

                var current = playlist.index

                var that = this

                var raw = playlist.current().lrc

                var callback = function (body) {

                    if (current !== playlist.index)

                        return;

                    that.data = that.parse(body)

                    var lrc = ''

                    that.data.forEach(function (line, index) {

                        lrc += '<p' + (index === 0 ? ' class="current"' : '') + '>' + line[1] + '</p>';

                    })

                    that.el = box.createChild('div', {

                        className: 'inner',

                        innerHTML: lrc

                    }, 'replace')

                    that.index = 0;

                }

                if (raw && raw.startsWith('http'))

                    this.fetch(raw, callback)

                else

                    callback(raw)

            },

            update: function (currentTime) {

                if (!this.data)

                    return

                if (this.index > this.data.length - 1 || currentTime < this.data[this.index][0] || (!this.data[this.index + 1] || currentTime >= this.data[this.index + 1][0])) {

                    for (var i = 0; i < this.data.length; i++) {

                        if (currentTime >= this.data[i][0] && (!this.data[i + 1] || currentTime < this.data[i + 1][0])) {

                            this.index = i;

                            var y = -(this.index - 1);

                            this.el.style.transform = 'translateY(' + y + 'rem)';

                            this.el.style.webkitTransform = 'translateY(' + y + 'rem)';

                            this.el.getElementsByClassName('current')[0].removeClass('current');

                            this.el.getElementsByTagName('p')[i].addClass('current');

                        }

                    }

                }

            },

            parse: function (lrc_s) {

                if (lrc_s) {

                    lrc_s = lrc_s.replace(/([^\]^\n])\[/g, function (match, p1) { return p1 + '\n[' });

                    const lyric = lrc_s.split('\n');

                    var lrc = [];

                    const lyricLen = lyric.length;

                    for (var i = 0; i < lyricLen; i++) {

                        // match lrc time

                        const lrcTimes = lyric[i].match(/\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/g);

                        // match lrc text

                        const lrcText = lyric[i]

                            .replace(/.*\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/g, '')

                            .replace(/<(\d{2}):(\d{2})(\.(\d{2,3}))?>/g, '')

                            .replace(/^\s+|\s+$/g, '');

                        if (lrcTimes) {

                            // handle multiple time tag

                            const timeLen = lrcTimes.length;

                            for (var j = 0; j < timeLen; j++) {

                                const oneTime = /\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/.exec(lrcTimes[j]);

                                const min2sec = oneTime[1] * 60;

                                const sec2sec = parseInt(oneTime[2]);

                                const msec2sec = oneTime[4] ? parseInt(oneTime[4]) / ((oneTime[4] + '').length === 2 ? 100 : 1000) : 0;

                                const lrcTime = min2sec + sec2sec + msec2sec;

                                lrc.push([lrcTime, lrcText]);

                            }

                        }

                    }

                    // sort by time

                    lrc = lrc.filter(function (item) { return item[1] });

                    lrc.sort(function (a, b) { return a[0] - b[0] });

                    return lrc;

                } else {

                    return [];

                }

            },

            fetch: function (url, callback) {

                fetch(url)

                    .then(function (response) {

                        return response.text()

                    }).then(function (body) {

                        callback(body)

                    }).catch(function (ex) { })

            }

        }

        var preview = {

            el: null,

            create: function () {

                var current = playlist.current()

                this.el.innerHTML = '<div class="cover"><div class="disc"><img src="' + (current.cover) + '" class="blur" /></div></div>'

                    + '<div class="info"><h4 class="title">' + current.name + '</h4><span>' + current.artist + '</span>'

                    + '<div class="lrc"></div></div>'

                this.el.child('.cover').addEventListener('click', t.player.options.events['play-pause'])

                lyrics.create(this.el.child('.lrc'))

            }

        }

        var progress = {

            el: null,

            bar: null,

            create: function () {

                var current = playlist.current().el

                if (current) {

                    if (this.el) {

                        this.el.parentNode.removeClass('current')

                            .removeEventListener(utils.nameMap.dragStart, this.drag)

                        this.el.remove()

                    }

                    this.el = current.createChild('div', {

                        className: 'progress'

                    })

                    this.el.attr('data-dtime', utils.secondToTime(0))

                    this.bar = this.el.createChild('div', {

                        className: 'bar',

                    })

                    current.addClass('current')

                    current.addEventListener(utils.nameMap.dragStart, this.drag);

                    playlist.scroll()

                }

            },

            update: function (percent) {

                this.bar.width(Math.floor(percent * 100) + '%')

                this.el.attr('data-ptime', utils.secondToTime(percent * source.duration))

            },

            seeking: function (type) {

                if (type)

                    this.el.addClass('seeking')

                else

                    this.el.removeClass('seeking')

            },

            percent: function (e, el) {

                var percentage = ((e.clientX || e.changedTouches[0].clientX) - el.left()) / el.width();

                percentage = Math.max(percentage, 0);

                return Math.min(percentage, 1)

            },

            drag: function (e) {

                e.preventDefault()

                var current = playlist.current().el

                var thumbMove = function (e) {

                    e.preventDefault()

                    var percentage = progress.percent(e, current)

                    progress.update(percentage)

                    lyrics.update(percentage * source.duration);

                };

                var thumbUp = function (e) {

                    e.preventDefault()

                    current.removeEventListener(utils.nameMap.dragEnd, thumbUp)

                    current.removeEventListener(utils.nameMap.dragMove, thumbMove)

                    var percentage = progress.percent(e, current)

                    progress.update(percentage)

                    t.player.seek(percentage * source.duration)

                    source.disableTimeupdate = false

                    progress.seeking(false)

                };

                source.disableTimeupdate = true

                progress.seeking(true)

                current.addEventListener(utils.nameMap.dragMove, thumbMove)

                current.addEventListener(utils.nameMap.dragEnd, thumbUp)

            }

        }

        var controller = {

            el: null,

            btns: {},

            step: 'next',

            create: function () {

                if (!t.player.options.controls)

                    return

                var that = this

                t.player.options.controls.forEach(function (item) {

                    if (that.btns[item])

                        return;

                    var opt = {

                        onclick: function (event) {

                            that.events[item] ? that.events[item](event) : t.player.options.events[item](event)

                        }

                    }

                    switch (item) {

                        case 'volume':

                            opt.className = ' ' + (source.muted ? 'off' : 'on')

                            opt.innerHTML = '<div class="bar"></div>'

                            opt['on' + utils.nameMap.dragStart] = that.events['volume']

                            opt.onclick = null

                            break;

                        case 'mode':

                            opt.className = ' ' + t.player.options.mode

                            break;

                        default:

                            opt.className = ''

                            break;

                    }

                    opt.className = item + opt.className + ' btn'

                    that.btns[item] = that.el.createChild('div', opt)

                })

                that.btns['volume'].bar = that.btns['volume'].child('.bar')

            },

            events: {

                mode: function (e) {

                    switch (t.player.options.mode) {

                        case 'loop':

                            t.player.options.mode = 'random'

                            break;

                        case 'random':

                            t.player.options.mode = 'order'

                            break;

                        default:

                            t.player.options.mode = 'loop'

                    }

                    controller.btns['mode'].className = 'mode ' + t.player.options.mode + ' btn'

                    store.set('_PlayerMode', t.player.options.mode)

                },

                volume: function (e) {

                    e.preventDefault()

                    var current = e.currentTarget

                    var drag = false

                    var thumbMove = function (e) {

                        e.preventDefault()

                        t.player.volume(controller.percent(e, current))

                        drag = true

                    };

                    var thumbUp = function (e) {

                        e.preventDefault()

                        current.removeEventListener(utils.nameMap.dragEnd, thumbUp)

                        current.removeEventListener(utils.nameMap.dragMove, thumbMove)

                        if (drag) {

                            t.player.muted()

                            t.player.volume(controller.percent(e, current))

                        } else {

                            if (source.muted) {

                                t.player.muted()

                                t.player.volume(source.volume)

                            } else {

                                t.player.muted('muted')

                                controller.update(0)

                            }

                        }

                    };

                    current.addEventListener(utils.nameMap.dragMove, thumbMove)

                    current.addEventListener(utils.nameMap.dragEnd, thumbUp)

                },

                backward: function (e) {

                    controller.step = 'prev'

                    t.player.mode()

                },

                forward: function (e) {

                    controller.step = 'next'

                    t.player.mode()

                },

            },

            update: function (percent) {

                controller.btns['volume'].className = 'volume ' + (!source.muted && percent > 0 ? 'on' : 'off') + ' btn'

                controller.btns['volume'].bar.width(Math.floor(percent * 100) + '%')

            },

            percent: function (e, el) {

                var percentage = ((e.clientX || e.changedTouches[0].clientX) - el.left()) / el.width();

                percentage = Math.max(percentage, 0);

                return Math.min(percentage, 1);

            }

        }

        var events = {

            onerror: function () {

                // 自己修改 - start

                var item = playlist.current();

                if (!item.repair && item.company && item.company == "netease") {

                    var id = item.url.substring(item.url.indexOf("id=") + 3, item.url.indexOf("&auth="));

                    item.url = "https://music.163.com/song/media/outer/url?id=" + id; // 修正的网易云 url

                    item.pic = "https://p3.music.126.net/Vji3PQJAZ2C7gS_6X51NFQ==/109951164723650033.jpg?param=200y200"; // 使用网易云上固定的一张封面

                    item.cover = item.pic;

                    item.repair = true; // 已修正

                    t.player.init(); // 重新播放

                } else {

                    playlist.error()

                    t.player.mode()

                }

                // 自己修改 - end

            },

            ondurationchange: function () {

                if (source.duration !== 1) {

                    progress.el.attr('data-dtime', utils.secondToTime(source.duration))

                }

            },

            onloadedmetadata: function () {

                t.player.seek(0)

                progress.el.attr('data-dtime', utils.secondToTime(source.duration))

            },

            onplay: function () {

                t.parentNode.addClass('playing')

                showtip(this.attr('title'))

                NOWPLAYING = t

            },

            onpause: function () {

                t.parentNode.removeClass('playing')

                NOWPLAYING = null

            },

            ontimeupdate: function () {

                if (!this.disableTimeupdate) {

                    progress.update(this.currentTime / this.duration)

                    lyrics.update(this.currentTime)

                }

            },

            onended: function (argument) {

                t.player.mode()

                t.player.play()

            }

        }

        var buttons = {

            el: {},

            create: function () {

                if (!t.player.options.btns)

                    return

                var that = this

                t.player.options.btns.forEach(function (item) {

                    if (that.el[item])

                        return;

                    that.el[item] = t.createChild('div', {

                        className: item + ' btn',

                        onclick: function (event) {

                            t.player.fetch().then(function () {

                                t.player.options.events[item](event)

                            })

                        }

                    });

                });

            }

        }

        var init = function (config) {

            if (t.player.created)

                return;

            t.player.options = Object.assign(option, config);

            t.player.options.mode = store.get('_PlayerMode') || t.player.options.mode

            // 初始化 button、controls 以及 click 事件

            buttons.create()

            // 初始化 audio or video

            source = t.createChild(t.player.options.type, events);

            // 初始化播放列表、预览、控件按钮等

            info.create();

            t.parentNode.addClass(t.player.options.type)

            t.player.created = true;

        }

        init(config)

        return t;

    }

博客 Hexo Shoka 背景音乐