// ==UserScript==
// @name 小宇宙FM-增加倍速选项
// @namespace http://tampermonkey.net/
// @version 0.8
// @description 给小宇宙播放页面增加倍速选项,方便在电脑上收听
// @author icheer.me
// @match https://www.xiaoyuzhoufm.com/episode/*
// @match https://www.xiaoyuzhoufm.com/podcast/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=xiaoyuzhoufm.com
// @grant none
// @license MIT
// @downloadURL none
// ==/UserScript==
const sleep = time => new Promise(resolve => setTimeout(resolve, time));
const $ = sel => document.querySelector(sel);
const CE = tag => document.createElement(tag);
(async function init(delay = 0) {
await sleep(delay);
const panel = $('.controls');
if (!panel) return console.error('panel not found');
const audio = $('audio');
if (!audio) return console.error('audio not found');
// v0.1 倍速选项
makePlaybackRateSelect(panel, audio);
// v0.2 下载按钮
makeDownloadButton(panel, audio);
// v0.3 循环播放
makeLoopCheckbox(panel, audio);
// v0.5 左右按键控制播放进度
makeKeyboardControl();
// v0.6 调出单集列表页面隐藏着的播放面板
fixPodcastPage(audio, delay);
// v0.4 二维码淡化
// v0.7 允许选中和复制网页内的文字
overrideStyles();
// v0.8 记录最近30个单集的播放进度,打开页面时自动恢复进度并播放
loadAudioProgress();
loopSaveAudioProgress();
})(500);
// 倍速下拉框
function makePlaybackRateSelect(panel, audio) {
const select = CE('select');
// 0.5倍速听谈话类节目太魔性了,是一个无用的选项,需要的话可以自己在下方添加
select.innerHTML = `
`;
select.style = 'width: 72px; height: 28px; margin: 0 10px; padding: 0 4px; border-radius: 4px; border: 1px solid #ccc; font-size: 14px; color: #333; outline: none';
panel.insertBefore(select, panel.children[0]);
// 最后一次选择的倍速偏好,自动带入
select.value = '1x';
if (localStorage.getItem('xyzRate')) {
const rate = parseFloat(localStorage.getItem('xyzRate')) || 1;
audio.playbackRate = rate;
select.value = rate + 'x';
}
// 选择倍速时,让倍速生效,并在localStorage中记录偏好,以便下次自动生效
select.addEventListener('change', e => {
const rate = parseFloat(e.target.value) || 1;
audio.playbackRate = rate;
localStorage.setItem('xyzRate', rate);
});
}
// 下载按钮
function makeDownloadButton(panel, audio) {
const download = CE('a');
download.innerText = '下载音频';
download.style = 'display: inline-block; width: 72px; margin: 0 75px 0 10px; text-align: left; color: var(--theme-color); font-size: 14px; text-decoration: none';
download.href = audio.src;
download.target = '_blank';
const title = $('h1') && $('h1').innerText.trim();
const fileName = audio.src.split('/').pop();
const extName = fileName.split('.').pop();
download.download = title ? `${title}.${extName}` : fileName;;
panel.appendChild(download);
}
// 循环播放复选框
function makeLoopCheckbox(panel, audio) {
const loopLabel = CE('label');
const loopBox = CE('input');
const loopSpan = CE('span');
loopLabel.style = 'margin: 0 10px; color: var(--theme-color); font-size: 14px';
loopBox.type = 'checkbox';
loopBox.style = 'display: inline-block; vertical-align: middle; margin-right: 4px; background: #fff; opacity: 0.15';
loopSpan.style = 'display: inline-block; vertical-align: middle'
loopSpan.innerText = '循环';
loopLabel.appendChild(loopBox);
loopLabel.appendChild(loopSpan);
panel.insertBefore(loopLabel, panel.children[0]);
// 切换循环播放时,使audio.loop生效
loopBox.addEventListener('change', e => {
audio.loop = e.target.checked;
loopBox.style.opacity = e.target.checked ? 1 : 0.15;
});
}
// 样式修改
function overrideStyles() {
const style = CE('style');
style.innerHTML = `
main aside {
opacity: 0.08;
}
main aside:hover {
opacity: 1;
}
div, img, span, p {
-webkit-user-select: auto;
-moz-user-select: auto;
-ms-user-select: auto;
user-select: auto;
-webkit-touch-callout: initial;
}
`;
$('head').appendChild(style);
}
// 左右按键控制播放进度
function makeKeyboardControl() {
const btnLeft = $('.controls button[aria-label^="后退"]');
const btnRight = $('.controls button[aria-label^="前进"]');
document.addEventListener('keyup', e => {
if (e.key === 'ArrowLeft') {
btnLeft && btnLeft.click();
} else if (e.key === 'ArrowRight') {
btnRight && btnRight.click();
}
});
}
// 调出单集列表页面隐藏着的播放面板
function fixPodcastPage(audio, delay) {
if (/^\/podcast\//.test(location.pathname)) {
$('section.wrap').style.transform = 'none';
audio.onplay = () => {
const rate = parseFloat(localStorage.getItem('xyzRate')) || 1;
if (audio.playbackRate !== rate) audio.playbackRate = rate;
};
}
// 解决在单集和列表之间切换时,功能失效的问题
if (delay === 500) {
history.pushState = () => init(150);
}
if ($('.podcast-title')) {
$('.podcast-title').onclick = () => init(150);
}
}
// 记录最近30个单集的播放进度,打开页面时自动恢复进度
const progressKey = 'xyzAudioProgress';
function loopSaveAudioProgress() {
if (window.audioLoopTimer) return;
window.audioLoopTimer = setInterval(() => {
const audio = $('audio');
if (!audio || !audio.src || audio.paused) return;
const trackId = /track_id=(\d+)/.exec(audio.src)?.[1];
if (!trackId) return;
let list = [];
try {
list = JSON.parse(localStorage.getItem(progressKey) || '[]');
} catch (e) {
console.error(e);
}
let item = list.find(i => i.id === trackId)
if (!item) {
item = { id: trackId };
list.unshift(item);
}
item.time = audio.currentTime;
list = list.slice(0, 30);
localStorage.setItem(progressKey, JSON.stringify(list));
}, 1000);
}
function loadAudioProgress() {
const audio = $('audio');
if (!audio || !audio.src) return;
const trackId = /track_id=(\d+)/.exec(audio.src)?.[1];
if (!trackId) return;
let list = [];
try {
list = JSON.parse(localStorage.getItem(progressKey) || '[]');
} catch (e) {
console.error(e);
}
const item = list.find(i => i.id === trackId);
if (item) {
audio.currentTime = item.time;
}
audio.play();
audio.onended = removeAudioProgress;
}
function removeAudioProgress() {
const audio = $('audio');
if (!audio || !audio.src) return;
const trackId = /track_id=(\d+)/.exec(audio.src)?.[1];
if (!trackId) return;
let list = [];
try {
list = JSON.parse(localStorage.getItem(progressKey) || '[]');
} catch (e) {
console.error(e);
}
list = list.filter(i => i.id !== trackId);
localStorage.setItem(progressKey, JSON.stringify(list));
}