bilibili-显示精确时间

bilibili动态与评论发布时间替换为精确时间,格式为“yyyy-MM-dd hh:mm:ss”。

  1. // ==UserScript==
  2. // @name bilibili-显示精确时间
  3. // @namespace http://tampermonkey.net/
  4. // @description bilibili动态与评论发布时间替换为精确时间,格式为“yyyy-MM-dd hh:mm:ss”。
  5. // @version 1.3.6
  6. // @author Y_jun
  7. // @license GPL-3.0
  8. // @icon https://www.bilibili.com/favicon.ico
  9. // @grant none
  10. // @match https://www.bilibili.com/*
  11. // @match https://live.bilibili.com/*
  12. // @match https://space.bilibili.com/*
  13. // @match https://t.bilibili.com/*
  14. // @run-at document-start
  15. // ==/UserScript==
  16.  
  17. // 1打开,0关闭
  18. const editDyn = 1; // 动态
  19. const editReply = 1; // 评论
  20. const editVideo = 1; // 视频
  21. const editPlaylist = 1; // 待播列表
  22. const editPics = 1; // 显示转发图片
  23.  
  24. const REPLY_API_PREFIX = 'https://api.bilibili.com/x/v2/reply';
  25. const DYN_API_PREFIX = 'https://api.bilibili.com/x/polymer/web-dynamic';
  26. const VIDEO_API_PREFIX = 'https://api.bilibili.com/x/web-interface/view';
  27. const VIDEO_DETAIL_API_PREFIX = 'https://api.bilibili.com/x/web-interface/wbi/view/detail';
  28. const SPACE_VIDEO_API_PREFIX = 'https://api.bilibili.com/x/space/wbi/arc/search';
  29. const SPACE_SEASONS_API_PREFIX = 'https://api.bilibili.com/x/polymer/web-space/seasons_archives_list';
  30. const SPACE_SERIES_API_PREFIX = 'https://api.bilibili.com/x/series/archives';
  31. const SPACE_HOME_COLLECTIONS_API_PREFIX = 'https://api.bilibili.com/x/polymer/web-space/home/seasons_series';
  32. const TOTAL_PLAYER_API_PREFIX = 'https://api.bilibili.com/x/player/online/total';
  33.  
  34. const TS_REGEX = /^\d{10}$/;
  35. const FULL_DATETIME_REGEX = /[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/;
  36. const MIN_DATETIME_REGEX = /[0-9]{2}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}/;
  37.  
  38. function getDateTime(ts, type) {
  39. if (TS_REGEX.test(ts)) {
  40. let date = new Date(ts * 1000);
  41. if (!isNaN(date.getTime())) {
  42. let y = date.getFullYear();
  43. if (y < 2000) return;
  44. let m = date.getMonth() + 1;
  45. let d = date.getDate();
  46. if (type === 'full') {
  47. return `${y}-${m < 10 ? "0" + m : m}-${d < 10 ? "0" + d : d} ${date.toTimeString().substring(0, 8)}`;
  48. } else if (type === 'min') {
  49. return `${y.toString().substring(2)}-${m < 10 ? "0" + m : m}-${d < 10 ? "0" + d : d} ${date.toTimeString().substring(0, 5)}`;
  50. } else if (type === 'date') {
  51. return `${y.toString().substring(2)}-${m < 10 ? "0" + m : m}-${d < 10 ? "0" + d : d}`;
  52. }
  53. }
  54. }
  55. return null;
  56. }
  57.  
  58. function getNewText(origTxt, datetime, isJoin) {
  59. origTxt = origTxt.trim();
  60. if (isJoin && /前|直播/.test(origTxt)) {
  61. return datetime + ' · ' + origTxt;
  62. }
  63. if (origTxt.includes(' · ')) {
  64. let origTxtArr = origTxt.split(' · ');
  65. origTxtArr[0] = datetime;
  66. return origTxtArr.join(' · ');
  67. }
  68. return datetime;
  69. }
  70.  
  71.  
  72. const console = Object.create(Object.getPrototypeOf(window.console), Object.getOwnPropertyDescriptors(window.console));
  73.  
  74. const addTimeToReply = function addTimeToReply(rootId, rpId, userId, datetime) {
  75. const id = rootId === 0 ? rpId : rootId;
  76. let intervalCount = 1;
  77. let interval = setInterval(() => {
  78. const container = document.querySelector(`.reply-wrap[data-id="${rpId}"]`);
  79. const containers = document.querySelectorAll(`[data-root-reply-id="${id}"][data-user-id="${userId}"]`);
  80. if (intervalCount > 20) clearInterval(interval);
  81. intervalCount++;
  82. if (container) {
  83. clearInterval(interval);
  84. // old page: 直接在对应评论元素更改时间
  85. const info = container.querySelector('.info');
  86. const time = info.querySelector('.reply-time,.time');
  87. if (time && !FULL_DATETIME_REGEX.test(time.textContent)) {
  88. time.textContent = getNewText(time.textContent, datetime, 1);
  89. }
  90. } else if (containers.length > 0) {
  91. clearInterval(interval);
  92. // new page: 由于无法直接定位评论元素,只能先定位其他有标识符的元素(比如用户头像),然后使用其父元素间接定位评论元素。
  93. for (let i = 0; i < containers.length; i++) {
  94. const container2 = containers[i];
  95. let parentElement = container2.parentElement;
  96. const isSub = parentElement.classList.toString().includes('sub-');
  97. if (isSub) {
  98. parentElement = parentElement.parentElement;
  99. }
  100. const info = parentElement.querySelector(isSub ? '.sub-reply-info' : '.reply-info');
  101. if (info && !FULL_DATETIME_REGEX.test(info.textContent)) {
  102. const time = info.querySelector('.reply-time,.sub-reply-time');
  103. if (time) {
  104. time.textContent = getNewText(time.textContent, datetime, 1);
  105. }
  106. break;
  107. }
  108. }
  109. }
  110. }, 100);
  111. };
  112.  
  113. const addTimeToDyn = function addTimeToDyn(dynId, datetime, isDetail) {
  114. let container;
  115. let intervalCount = 1;
  116. let interval = setInterval(() => {
  117. if (isDetail === 0) {
  118. container = document.querySelector(`.bili-dyn-list__item[data-did="${dynId}"]`);
  119. } else if (isDetail === 1) {
  120. container = document.querySelector(`.bili-dyn-item[data-did="${dynId}"]`);
  121. }
  122. if (intervalCount > 20) clearInterval(interval);
  123. intervalCount++;
  124. if (container) {
  125. clearInterval(interval);
  126. // old page: 直接在对应评论元素更改时间
  127. const dynMain = container.querySelector('.bili-dyn-item__main');
  128. const time = dynMain.querySelector('.bili-dyn-time');
  129. if (time && !FULL_DATETIME_REGEX.test(time.textContent)) {
  130. time.textContent = getNewText(time.textContent, datetime, 1);
  131. }
  132. }
  133. }, 100);
  134. };
  135.  
  136. const addDynPics = function addDynPics(dyn, isDetail) {
  137. const dynId = dyn.id_str;
  138. let container;
  139. let intervalCount = 1;
  140. let interval = setInterval(() => {
  141. if (isDetail === 0) {
  142. container = document.querySelector(`.bili-dyn-list__item[data-did="${dynId}"]`);
  143. } else if (isDetail === 1) {
  144. container = document.querySelector(`.bili-dyn-item[data-did="${dynId}"]`);
  145. }
  146. if (intervalCount > 20) clearInterval(interval);
  147. intervalCount++;
  148. if (container) {
  149. clearInterval(interval);
  150. // old page: 直接在对应评论元素更改时间
  151. const dynJsons = dyn.modules?.module_dynamic?.desc?.rich_text_nodes;
  152. if (dynJsons) {
  153. dynJsons.forEach(json => {
  154. if (json.type === 'RICH_TEXT_NODE_TYPE_VIEW_PICTURE') {
  155. const dynMain = container.querySelector('.bili-dyn-content__forw__desc');
  156. if (!dynMain.querySelector(".timer-show-pic")) {
  157. let picContainer = document.createElement("div");
  158. picContainer.className = 'timer-show-pic';
  159. picContainer.style.display = 'flex';
  160. picContainer.style.justifyContent = 'start';
  161. picContainer.style.position = 'relative';
  162. picContainer.style.overflow = 'auto';
  163. picContainer.style.flexWrap = 'wrap';
  164. dynMain.appendChild(picContainer);
  165. const pics = json.pics;
  166. pics.forEach(pic => {
  167. let picImg = document.createElement("img");
  168. picImg.onclick = new Function(`event.stopPropagation();window.open('${pic.src}')`);
  169. picImg.src = `${pic.src}@135h_!web-comment-note.webp`;
  170. picImg.style.width = '135px';
  171. picImg.style.height = '135px';
  172. picImg.style.objectFit = 'cover';
  173. picImg.style.objectFit = 'cover';
  174. picImg.style.borderRadius = '5px';
  175. picImg.style.margin = '2px';
  176. picImg.style.cursor = 'pointer';
  177. picContainer.appendChild(picImg);
  178. });
  179. }
  180. }
  181. });
  182. }
  183. }
  184. }, 100);
  185. };
  186.  
  187. const addTimeToVideo = function addTimeToVideo(videoId, datetime) {
  188. let intervalCount = 1;
  189. let interval = setInterval(() => {
  190. const containers = document.querySelectorAll(`[data-aid="${videoId}"]`);
  191. if (intervalCount > 20) clearInterval(interval);
  192. intervalCount++;
  193. if (containers.length > 0) {
  194. clearInterval(interval);
  195. for (let i = 0; i < containers.length; i++) {
  196. // old page: 直接在对应视频元素更改时间
  197. const container = containers[i];
  198. const time = container.querySelector('.time');
  199. if (time && !MIN_DATETIME_REGEX.test(time.textContent)) {
  200. time.textContent = getNewText(time.textContent, datetime, 0);
  201. time.style.overflow = 'visible';
  202. }
  203. }
  204. }
  205. }, 100);
  206. };
  207.  
  208. const addTimeToRelatedVideo = function addTimeToRelatedVideo(videoId, datetime) {
  209. let intervalCount = 1;
  210. let interval = setInterval(() => {
  211. const title = document.querySelector(`.info [href*="${videoId}"]`);
  212. if (intervalCount > 20) clearInterval(interval);
  213. intervalCount++;
  214. if (title) {
  215. clearInterval(interval);
  216. // old page: 直接在对应视频元素更改时间
  217. const container = title.parentElement;
  218. if (!container.textContent.includes(datetime)) {
  219. let time = document.createElement("div");
  220. time.textContent = datetime;
  221. time.style.overflow = 'visible';
  222. time.classList.add('playinfo');
  223. container.appendChild(time);
  224. }
  225. }
  226. }, 100);
  227. };
  228.  
  229. const addTimeToCollectionVideo = function addTimeToCollectionVideo(videoId, datetime) {
  230. let intervalCount = 1;
  231. let interval = setInterval(() => {
  232. const item = document.querySelector(`[data-key="${videoId}"]`);
  233. if (intervalCount > 20) clearInterval(interval);
  234. intervalCount++;
  235. if (item) {
  236. clearInterval(interval);
  237. // old page: 直接在对应视频元素更改时间
  238. const stats = item.querySelector('.stats');
  239. if (!stats.innerHTML.includes('stat-date')) {
  240. let dateStat = stats.firstChild.cloneNode();
  241. dateStat.classList.add("stat-date");
  242. dateStat.textContent = datetime;
  243. stats.appendChild(dateStat);
  244. }
  245. }
  246. }, 100);
  247. };
  248.  
  249. const handleComment = function handleReplies(replies) {
  250. replies.forEach((reply) => {
  251. const datetime = getDateTime(reply.ctime, 'full');
  252. if (datetime) {
  253. try {
  254. addTimeToReply(reply.root, reply.rpid, reply.mid, datetime);
  255. } catch (ex) {
  256. console.error(ex);
  257. }
  258. }
  259. if (reply.replies) {
  260. handleReplies(reply.replies);
  261. }
  262. });
  263. };
  264.  
  265. const handleDyns = function handleDyns(dyns, isDetail) {
  266. dyns.forEach((dyn) => {
  267. const ts = dyn?.modules?.module_author?.pub_ts || null;
  268. const datetime = getDateTime(ts, 'full');
  269. if (datetime) {
  270. try {
  271. addTimeToDyn(dyn.id_str, datetime, isDetail);
  272. } catch (ex) {
  273. console.error(ex);
  274. }
  275. }
  276. });
  277. };
  278.  
  279. const handleVideos = function handleVideos(videos) {
  280. videos.forEach((video) => {
  281. const datetime = getDateTime(video.created ?? video.pubdate, 'min');
  282. if (datetime) {
  283. try {
  284. addTimeToVideo(video.bvid, datetime);
  285. } catch (ex) {
  286. console.error(ex);
  287. }
  288. }
  289. });
  290. };
  291.  
  292. const handleRelatedVideos = function handleRelatedVideos(videos) {
  293. videos.forEach((video) => {
  294. const datetime = getDateTime(video.pubdate, 'full');
  295. if (datetime) {
  296. try {
  297. addTimeToRelatedVideo(video.bvid, datetime);
  298. } catch (ex) {
  299. console.error(ex);
  300. }
  301. }
  302. });
  303. };
  304.  
  305. const handleCollectionVideos = function handleCollectionVideos(videos) {
  306. videos.forEach((video) => {
  307. const datetime = getDateTime(video.arc.pubdate, 'date');
  308. if (datetime) {
  309. try {
  310. addTimeToCollectionVideo(video.bvid, datetime);
  311. } catch (ex) {
  312. console.error(ex);
  313. }
  314. }
  315. });
  316. };
  317.  
  318. const handleDynPics = function handleDynPics(dyns, isDetail) {
  319. dyns.forEach((dyn) => {
  320. if (dyn) {
  321. try {
  322. addDynPics(dyn, isDetail);
  323. } catch (ex) {
  324. console.error(ex);
  325. }
  326. }
  327. });
  328. };
  329.  
  330. const handleResponse = async function handleResponse(url, response) {
  331. if (editReply && url.startsWith(REPLY_API_PREFIX)) {
  332. const body = response instanceof Response ? await response.clone().text() : response.toString();
  333. try {
  334. const json = JSON.parse(body);
  335. if (json.code === 0) {
  336. setTimeout(() => {
  337. handleComment(Array.isArray(json.data.replies) ? json.data.replies : []);
  338. handleComment(Array.isArray(json.data.top_replies) ? json.data.top_replies : []);
  339. }, 50);
  340. }
  341. } catch (ex) {
  342. console.error(ex);
  343. }
  344. }
  345. if (editDyn && url.startsWith(DYN_API_PREFIX)) {
  346. const body = response instanceof Response ? await response.clone().text() : response.toString();
  347. try {
  348. const json = JSON.parse(body);
  349. if (json.code === 0) {
  350. setTimeout(() => {
  351. handleDyns(Array.isArray(json.data.items) ? json.data.items : [], 0);
  352. handleDyns(json.data.item ? [json.data.item] : [], 1);
  353. if (editPics) {
  354. handleDynPics(Array.isArray(json.data.items) ? json.data.items : [], 0)
  355. handleDynPics(json.data.item ? [json.data.item] : [], 1);
  356. }
  357. }, 50);
  358. }
  359. } catch (ex) {
  360. console.error(ex);
  361. }
  362. }
  363. if (editPlaylist && (url.startsWith(VIDEO_API_PREFIX) || url.startsWith(VIDEO_DETAIL_API_PREFIX))) {
  364. const body = response instanceof Response ? await response.clone().text() : response.toString();
  365. try {
  366. const json = JSON.parse(body);
  367. if (json.code === 0) {
  368. setTimeout(() => {
  369. handleRelatedVideos(Array.isArray(json.data.Related) ? json.data.Related : []);
  370. }, 50);
  371. }
  372. } catch (ex) {
  373. console.error(ex);
  374. }
  375. }
  376. if (editPlaylist && url.startsWith(TOTAL_PLAYER_API_PREFIX)) {
  377. const body = response instanceof Response ? await response.clone().text() : response.toString();
  378. try {
  379. const json = JSON.parse(body);
  380. if (json.code === 0) {
  381. let initialState = window.__INITIAL_STATE__;
  382. setTimeout(() => {
  383. if (initialState?.sectionsInfo?.sections) {
  384. initialState.sectionsInfo.sections.forEach((collection) => {
  385. handleCollectionVideos(Array.isArray(collection.episodes) ? collection.episodes : []);
  386. });
  387. }
  388. }, 50);
  389. }
  390. } catch (ex) {
  391. console.error(ex);
  392. }
  393. }
  394. if (editVideo && url.startsWith(SPACE_VIDEO_API_PREFIX)) {
  395. const body = response instanceof Response ? await response.clone().text() : response.toString();
  396. try {
  397. const json = JSON.parse(body);
  398. if (json.code === 0) {
  399. setTimeout(() => {
  400. handleVideos(Array.isArray(json.data.list?.vlist) ? json.data.list.vlist : []);
  401. }, 50);
  402. }
  403. } catch (ex) {
  404. console.error(ex);
  405. }
  406. }
  407. if (editVideo && (url.startsWith(SPACE_SERIES_API_PREFIX) || url.startsWith(SPACE_SEASONS_API_PREFIX))) {
  408. const body = response instanceof Response ? await response.clone().text() : response.toString();
  409. try {
  410. const json = JSON.parse(body);
  411. if (json.code === 0) {
  412. setTimeout(() => {
  413. handleVideos(Array.isArray(json.data.archives) ? json.data.archives : []);
  414. }, 50);
  415. }
  416. } catch (ex) {
  417. console.error(ex);
  418. }
  419. }
  420. if (editVideo && url.startsWith(SPACE_HOME_COLLECTIONS_API_PREFIX)) {
  421. const body = response instanceof Response ? await response.clone().text() : response.toString();
  422. try {
  423. const json = JSON.parse(body);
  424. if (json.code === 0) {
  425. setTimeout(() => {
  426. if (Array.isArray(json.data.items_lists?.season_list)) {
  427. json.data.items_lists.season_list.forEach((collection) => {
  428. handleVideos(Array.isArray(collection.archives) ? collection.archives : []);
  429. });
  430. }
  431. if (Array.isArray(json.data.items_lists?.series_list)) {
  432. json.data.items_lists.series_list.forEach((collection) => {
  433. handleVideos(Array.isArray(collection.archives) ? collection.archives : []);
  434. });
  435. }
  436. }, 50);
  437. }
  438. } catch (ex) {
  439. console.error(ex);
  440. }
  441. }
  442. };
  443.  
  444.  
  445. const $fetch = window.fetch;
  446.  
  447. window.fetch = async function fetchHacker() {
  448. const response = await $fetch(...arguments);
  449. if (response.status === 200 && response.headers.get('content-type')?.includes('application/json')) {
  450. await handleResponse(response.url, response);
  451. }
  452. return response;
  453. };
  454.  
  455. /**
  456. * @this XMLHttpRequest
  457. */
  458. const onReadyStateChange = function onReadyStateChange() {
  459. if (this.readyState === XMLHttpRequest.DONE && this.status === 200 && this.getAllResponseHeaders().split("\n").find((v) => v.toLowerCase().includes('content-type: application/json'))) {
  460. handleResponse(this.responseURL, this.response);
  461. }
  462. };
  463.  
  464. const jsonpHacker = new MutationObserver((mutationList) => {
  465. mutationList.forEach((mutation) => {
  466. mutation.addedNodes.forEach((node) => {
  467. if (node.nodeName.toLowerCase() !== 'script' || node.src.trim() === '') {
  468. return;
  469. }
  470. const u = new URL(node.src);
  471. if (u.searchParams.has('callback')) {
  472. const callbackName = u.searchParams.get('callback');
  473. const callback = window[callbackName];
  474. window[callbackName] = function (data) {
  475. handleResponse(u.href, JSON.stringify(data));
  476. callback(data);
  477. };
  478. }
  479. });
  480. });
  481. });
  482.  
  483. document.addEventListener('DOMContentLoaded', () => {
  484. jsonpHacker.observe(document.head, {
  485. childList: true,
  486. });
  487. });
  488.  
  489. window.XMLHttpRequest = class XMLHttpRequestHacker extends window.XMLHttpRequest {
  490. constructor() {
  491. super();
  492. this.addEventListener('readystatechange', onReadyStateChange.bind(this));
  493. }
  494. };

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址