// ==UserScript==
// @name 「bilibili」水水自用 | B 币领取提醒、播放进度更新至网址等;
// @namespace wdssmq.com
// @version 1.0.1
// @author 沉冰浮水
// @description B 币领取提醒、稍后再看列表导出为 *.url 等
// @url https://greasyfork.org/scripts/398415
// @null ----------------------------
// @contributionURL https://github.com/wdssmq#%E4%BA%8C%E7%BB%B4%E7%A0%81
// @contributionAmount 5.93
// @null ----------------------------
// @link https://github.com/wdssmq/userscript
// @link https://afdian.net/@wdssmq
// @link https://greasyfork.org/zh-CN/users/6865-wdssmq
// @null ----------------------------
// @include https://www.bilibili.com/*
// @include https://t.bilibili.com/*
// @include https://manga.bilibili.com/account-center*
// @include https://account.bilibili.com/account/big/myPackage
// @match https://space.bilibili.com/44744006/fans/follow*
// @icon https://www.bilibili.com/favicon.ico
// @noframes
// @run-at document-end
// @grant GM_setClipboard
// @grant GM_notification
// @grant GM.openInTab
// ==/UserScript==
/* jshint esversion: 6 */
/* eslint-disable */
(function () {
'use strict';
const gm_name = "later";
// 初始常量或函数
const curUrl = window.location.href;
const curDate = new Date();
// ---------------------------------------------------
const _curUrl = () => { return window.location.href };
const _getDateStr = (date = curDate) => {
const options = { year: "numeric", month: "2-digit", day: "2-digit" };
return date.toLocaleDateString("zh-CN", options).replace(/\//g, "-");
};
// ---------------------------------------------------
const _log = (...args) => console.log(`[${gm_name}]\n`, ...args);
const _warn = (...args) => console.warn(`[${gm_name}]\n`, ...args);
// ---------------------------------------------------
// const $ = window.$ || unsafeWindow.$;
function $n(e) {
return document.querySelector(e);
}
function $na(e) {
return document.querySelectorAll(e);
}
// ---------------------------------------------------
// 添加内容到指定元素后面
function fnAfter($ne, e) {
const $e = typeof e === "string" ? $n(e) : e;
$e.parentNode.insertBefore($ne, $e.nextSibling);
}
// 指定元素内查找子元素
function fnFindDom(el, selector) {
el = typeof el === "string" ? $n(el) : el;
const queryList = el.querySelectorAll(selector);
if (queryList.length === 1) {
return queryList[0];
}
return queryList.length > 1 ? queryList : null;
}
// ---------------------------------------------------
// 元素变化监听
const fnElChange = (el, fn = () => { }, onetime = true) => {
const observer = new MutationObserver((mutationRecord, mutationObserver) => {
// _log('mutationRecord = ', mutationRecord);
// _log('mutationObserver === observer', mutationObserver === observer);
fn(mutationRecord, mutationObserver);
if (onetime) {
mutationObserver.disconnect(); // 取消监听,正常应该在回调函数中根据条件决定是否取消
}
});
observer.observe(el, {
// attributes: false,
// attributeFilter: ["class"],
childList: true,
// characterData: false,
subtree: true,
});
};
// 点击指定元素复制内容
function fnCopy(eTrig, content, fnCB = () => { }) {
// 判断 eTrig 是否为字符串
const $eTrig = typeof eTrig === "string" ? $n(eTrig) : eTrig;
$eTrig.addEventListener("click", function (e) {
GM_setClipboard(content);
fnCB(e);
this.style.color = "gray";
});
}
// cookie 封装
const ckeObj = {
setItem: function (key, value) {
const Days = 137;
const exp = new Date();
exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
document.cookie = key + "=" + encodeURIComponent(value) + ";path=/;domain=.bilibili.com;expires=" + exp.toGMTString();
},
getItem: function (key, def = "") {
const reg = new RegExp("(^| )" + key + "=([^;]*)(;|$)");
const arr = document.cookie.match(reg);
if (arr) {
return decodeURIComponent(arr[2]);
}
return def;
},
};
/* global GM_notification GM */
// 日期转字符串
const getDateStr = (date) => {
const options = { year: "numeric", month: "2-digit", day: "2-digit" };
return date.toLocaleDateString("zh-CN", options);
};
// 判断日期间隔
const diffDateDays = (date1, date2) => {
const diff = date1.getTime() - date2.getTime();
return diff / (1000 * 60 * 60 * 24);
};
// B 币领取提醒
(() => {
const ckeName = "bilibili-helper-bcoin-nxtDateStr";
const curDateStr = getDateStr(curDate);
const nxtDateStr = ckeObj.getItem(ckeName, curDateStr);
const bcoinUrl = "https://account.bilibili.com/account/big/myPackage";
_log(`当前日期: ${curDateStr}`);
_log(`下次领取: ${nxtDateStr}`);
// 通知事件封装
const notify = (title, body) => {
_log(`通知标题: ${title}`);
GM_notification({
title: title,
text: body,
timeout: 0,
onclick: () => {
// window.location.href = bcoinUrl;
GM.openInTab(bcoinUrl, false);
},
});
};
// 判断是否已经领取过
const fnCheckByDOM = () => {
const $bcoin = $n(".bcoin-wrapper");
// _log("fnCheckByDOM", $bcoin);
// $bcoin && _log("fnCheckByDOM", $bcoin.innerHTML);
if ($bcoin && $bcoin.innerText.includes("本次已领")) {
const match = $bcoin.innerText.match(/\d{4}\/\d+\/\d+/);
if (match && match[0] !== nxtDateStr) {
ckeObj.setItem(ckeName, match[0]);
_log("已领取过,更新下次领取日期", match[0]);
return true;
}
} else {
fnElChange($n("body"), fnCheckByDOM);
}
return false;
};
// _log($n("body").innerHTML);
// _log(nxtDateStr, curMonth);
// 对比日期
const iniDiff = diffDateDays(curDate, new Date(nxtDateStr));
if (iniDiff > 0) {
_log(curUrl, "\n", bcoinUrl);
if (curUrl.indexOf(bcoinUrl) > -1) {
fnCheckByDOM();
} else {
notify("B 币领取提醒", "点击查看 B 币领取情况");
}
}
})();
// 番剧链接改为我的追番
(() => {
let isDone = false;
// 获取 uid
const getUidByUrlOrCookie = (url) => {
let uid = null;
const match = url.match(/\d+/);
if (match) {
uid = match[0];
ckeObj.setItem("bilibili-helper-uid", uid);
} else {
uid = ckeObj.getItem("bilibili-helper-uid");
}
return uid;
};
// 更新链接
const fnCheckByDOM = () => {
if (!isDone) {
fnElChange($n("body"), fnCheckByDOM);
}
const $pick = $n("a[href*='/anime/']");
const $pick2 = $n("a.header-entry-avatar");
const uid = $pick2 ? getUidByUrlOrCookie($pick2.href) : null;
if (!$pick || !$pick2 || !uid) {
return;
}
// debug
// _warn($pick, $pick2);
const url = `https://space.bilibili.com/${uid}/bangumi`;
$pick.href = url;
_warn("番剧链接改为我的追番", url);
isDone = true;
};
fnCheckByDOM();
})();
_log("_later2url.js", "开始");
// 构造 Bash Shell 脚本
function fnMKShell(arrList, prefix = "") {
const curDateStr = _getDateStr();
let strRlt =
"if [ ! -d \"prefix-date\" ]; then\n" +
"mkdir prefix-date\n" +
"fi\n" +
"cd prefix-date\n\n";
strRlt = strRlt.replace(/prefix/g, prefix);
strRlt = strRlt.replace(/date/g, curDateStr);
/**
* e {title:"", href:""}
*/
arrList.forEach(function (e, i) {
const serial = i + 1;
// _log(e);
// 移除不能用于文件名的字符
let title = e.title || e.innerText;
title = title.replace(/\\|\/|:|\*|!|\?]|<|>/g, "");
title = title.replace(/["'\s]/g, "");
// _log(title);
const lenTitle = title.length;
if (lenTitle >= 155) {
title = `标题过长丨${lenTitle}`;
}
// 获取文章链接
const href = e.href || e.url;
// url 文件名
const urlFileName = `${serial}丨${title}.url`;
strRlt += `echo [InternetShortcut] > "${urlFileName}"\n`;
strRlt += `echo "URL=${href}" >> "${urlFileName}"\n`;
strRlt += "\n";
});
{
strRlt += "exit\n\n";
}
return strRlt;
}
// Ajax 封装
function fnGetAjax(callback = function () { }) {
$.ajax({
url: "https://api.bilibili.com/x/v2/history/toview/web",
type: "GET",
xhrFields: {
withCredentials: true, // 这里设置了withCredentials
},
success: function (data) {
// _log();
callback(data.data.list);
},
error: function (err) {
console.error(err);
},
});
}
// 分 p 链接获取
(() => {
const selectorMap = {
epListBox: ".eplist_ep_list_wrapper__PzLHa",
epListTitle: ".eplist_list_title__JwP3d h4",
// epList: ".imageList_wrap__pDHvN",
epList: ".imageList_wrap__pDHvN .imageListItem_wrap__HceXs a",
};
const $$epListBox = $na(selectorMap.epListBox);
if ($$epListBox.length === 0) {
return;
}
const fnGetLinkList = ($epList) => {
// _log("fnOnCLick", $epList);
const arrList = Array.prototype.map.call($epList, ($item, index) => {
const href = $item.href;
const title = $item.innerText.replace(/\n.+/g, "");
return {
href,
title,
};
});
_log("arrList", arrList);
return arrList;
};
const elListDom = [];
// 遍历 NodeList,不能直接使用 forEach
Array.prototype.forEach.call($$epListBox, ($epListBox, index) => {
const $epListTitle = fnFindDom($epListBox, selectorMap.epListTitle);
const $epList = fnFindDom($epListBox, selectorMap.epList);
const textTitle = $epListTitle.innerText;
// 注册点击复制
fnCopy($epListTitle, fnMKShell(fnGetLinkList($epList), textTitle));
elListDom.push({
box: $epListBox,
title: $epListTitle,
list: $epList,
});
});
console.log(elListDom);
})();
// 导出稍后再看为 .lnk 文件
(function () {
// 跳转到标准播放页
const urlMatch = /list\/watchlater\?bvid=(\w+)/.exec(location.href);
if (urlMatch) {
const bvid = urlMatch[1];
location.href = `https://www.bilibili.com/video/${bvid}`;
return;
}
if (/watchlater/.test(location.href)) {
let tmpHTML = $("span.t").html();
fnGetAjax(function (list) {
const arrRlt = [];
list.forEach((item, index) => {
arrRlt.push({
title: item.title,
href: `https://www.bilibili.com/video/${item.bvid}`,
bvid: item.bvid,
});
// _log(item, index);
});
// _log("稍后再看", arrRlt.length);
tmpHTML = tmpHTML.replace(/0\//g, arrRlt.length + "/");
$("span.t").html(tmpHTML + "「点击这里复制 bash shell 命令」");
let appCon = "「已复制」";
if (arrRlt.length > 37) {
appCon = "「已复制,数量过多建议保存为 .sh 文件执行」";
}
// 注册点击复制
fnCopy("span.t", fnMKShell(arrRlt, "bilibili"), () => {
$("span.t").html(tmpHTML + appCon);
});
});
return false;
}
})();
_log("_later2url.js", "结束");
// 关注列表增强
(() => {
const gob = {
done: false,
_rssUrl: (uid) => {
let RSSHub = localStorage.RSSHub ? localStorage.RSSHub : "https://rsshub.app/bilibili/user/video/:uid/";
localStorage.RSSHub = RSSHub;
return RSSHub.replace(":uid", uid);
},
};
// 获取关注列表
const fnGetFollowList = () => {
let $list = $na("li.list-item");
return $list;
};
// 针对每个关注列表项
const fnUpView = ($up) => {
const url = $up.querySelector("a.cover").href;
const name = $up.querySelector("span.fans-name").innerText;
const { uid, uname } = ((url, name) => {
const uid = url.match(/\/(\d+)\/$/)[1];
let uname = name.replace(/\b/g, "_").replace(/_+/g, "_");
uname = uname.replace(/^_|_$/g, "");
// _log(`${uid} ${uname}`);
return { uid, uname };
})(url, name);
const $desc = $up.querySelector("p.auth-description") || $up.querySelector("p.desc");
if ($desc && $desc.dataset.fnUpView === "done") {
return;
}
$desc.dataset.fnUpView = "done";
// _log($desc);
// 创建新 p 元素
const $p = document.createElement("p");
const RSSUrl = gob._rssUrl(uid);
$p.className = "desc";
$p.innerHTML = `
<hr>
<a href="${RSSUrl}" target="_blank">「RSS」</a> |
${uid} ${uname}
`;
fnAfter($p, $desc);
};
// 页面元素更新监听
const fnCheckByDOM = () => {
const $ul = $n("ul.relation-list");
const $list = fnGetFollowList();
if ($list.length > 0 && !$ul.dataset.fnCheckByDOM !== "done") {
$ul.dataset.fnCheckByDOM = "done";
_log("$list.length = ", $list.length);
// nodeList to array
const $listArr = Array.from($list);
$listArr.forEach(($li) => {
fnUpView($li);
});
}
fnElChange($n("body"), fnCheckByDOM);
};
fnCheckByDOM();
})();
// 姑且引入一些大概会用到的函数 ↑
const $body = $n("body");
// 设置一个标记
let isDone = false;
// 用来实现实际功能的函数
const fnMain = () => {
// 视频播放时页面一直在变化,所以这里会一直调用
_warn("_cover => fnMain");
const $listCover = $na(".header-history-video__image");
// isDone 则控制这里只执行一次
if ($listCover.length > 0 && !isDone) {
isDone = true;
Array.from($listCover).forEach(($cover, index) => {
// _warn(`.header-history-video__image[${index}] = `, $cover);
// _warn("---");
const $coverImg = $cover.querySelector("img");
const $coverSource = $cover.querySelector("source");
// src 或 srcset 属性
let imgUrl = $coverImg.src;
let imgSrcset = $coverSource.getAttribute("srcset");
_warn("imgUrl = ", imgUrl);
_warn("imgSrcset = ", imgSrcset);
// 使用正则替换去除后边的内容
imgUrl = imgUrl.replace(/@.+$/, "");
imgSrcset = imgSrcset.replace(/@.+$/, "");
// 设置回去
$coverImg.src = imgUrl;
$coverSource.setAttribute("srcset", imgSrcset);
});
}
};
// 当页面内容产生变化时,触发函数 fnMain
fnElChange($body, fnMain, false);
// 时间轴书签
const gob = {
lstTime: 0,
curTime: 0,
curPart: 0,
isRunning: false,
title: "",
step: 73,
};
const fnUpTitle = (part, time) => {
if (gob.title === "") {
gob.title = $n("h1.video-title").innerHTML;
}
const strAdd = ((obj) => {
let str = "";
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const val = obj[key];
if (!val) {
continue;
}
const s = key === "part" ? "P" : "播放至 ";
str += `_${s}${val}`;
}
}
return str;
})({ part, time });
$n("title").innerHTML = `${gob.title}${strAdd}_bilibili`;
// debug
_warn(`title: ${$n("title").innerHTML}`);
};
const fnUpUrl = (p, time) => {
let url = _curUrl();
let urlNew = url;
// 清理参数
url = url.split("?")[0];
// _warn("fnUpUrl", { p, time });
// 新参数构造
const params = new URLSearchParams();
for (const key in { p, time }) {
if (Object.hasOwnProperty.call({ p, time }, key)) {
const item = { p, time }[key];
if (item) {
params.append(key, item);
}
}
}
// 拼接参数
urlNew = `${url}?${params.toString()}`;
// 更新至地址栏
window.history.pushState(null, null, urlNew);
// debug
_warn(`url: ${urlNew}`);
};
const fnGetTime = () => {
const $timLabel = $n(".bpx-player-ctrl-time-label");
const $curTime = $n(".bpx-player-ctrl-time-current");
if ($timLabel && $curTime) {
const str = $curTime.innerHTML;
const arr = str.split(":");
if (arr.length === 3) {
return parseInt(arr[0]) * 3600 + parseInt(arr[1]) * 60 + parseInt(arr[2]);
} else {
return parseInt(arr[0]) * 60 + parseInt(arr[1]);
}
}
return 0;
};
const fnGetPart = () => {
const $list = $n("ul.list-box");
const $cur = $list?.querySelector("li.on");
const $curHref = $cur?.querySelector("a");
if ($cur && $curHref) {
// _warn($cur, $curHref);
const str = $curHref.href;
const arr = str.split("?p=");
if (arr.length === 2) {
return parseInt(arr[1]);
}
}
return null;
};
const fnDelay = (time, fnc = () => { }) => {
if (gob.isRunning) {
return;
}
gob.isRunning = true;
setTimeout(() => {
fnc();
gob.isRunning = false;
}, time);
};
document.addEventListener(
"mouseover",
function (e) {
// const $target = e.target;
fnDelay(1000, () => {
// bpx-player-container bpx-state-no-cursor
const $container = $n(".bpx-player-container");
let bolFlag = false;
if ($container && !$container.classList.contains("bpx-state-no-cursor")) {
gob.curTime = fnGetTime();
gob.curPart = fnGetPart();
if (gob.curTime > 0 && gob.curTime - gob.lstTime > gob.step) {
bolFlag = true;
}
if (gob.curTime < gob.lstTime) {
bolFlag = true;
}
if (gob.lstTime <= parseInt(gob.step / 4)) {
bolFlag = true;
}
if (bolFlag) {
fnUpTitle(gob.curPart, gob.curTime);
fnUpUrl(gob.curPart, gob.curTime);
gob.lstTime = gob.curTime;
}
// _warn("进度条触发", e.target);
// _warn("进度条触发", gob);
return;
}
});
},
false,
);
})();