[Neko0] Iwara functional enhancements

Provide the function of "one-click copy name and like+follow+download" and the function of "copy name only" separately, which makes it convenient to save your favorite videos to your local device, so that you can still access them even if the author deletes their account. Automatically load the highest resolution video source. Detect resolution and frame rate, and low-quality videos will be marked with a red warning. Automatically click the "R18 warning button", eliminating the need to manually

  1. // ==UserScript==
  2. // @name [Neko0] Iwara functional enhancements
  3. // @name:zh [Neko0] Iwara 增强
  4. // @name:ja [Neko0] Iwara 機能強化
  5. // @description Provide the function of "one-click copy name and like+follow+download" and the function of "copy name only" separately, which makes it convenient to save your favorite videos to your local device, so that you can still access them even if the author deletes their account. Automatically load the highest resolution video source. Detect resolution and frame rate, and low-quality videos will be marked with a red warning. Automatically click the "R18 warning button", eliminating the need to manually close the prompt every time.
  6. // @description:zh 提供 "一键复制名字 并 喜欢+关注+下载" 与单独 "复制名字" 的功能, 便捷地收藏自己喜欢的视频到本地, 以免作者销号后就看不到作品了。自动加载最高分辨率视频源。侦测分辨率和帧率,过低的质量会以红色警示。自动点击“R18警告按钮”,不再需要每次手动关闭提示。
  7. // @description:ja 「名前を一括コピーして、いいね+フォロー+ダウンロード」および「名前のみコピー」の機能を提供し、自分の好きな動画を手軽にローカルに保存できるようにしました。作者がアカウントを削除しても作品を見ることができます。最高解像度の動画ソースを自動的に読み込みます。解像度とフレームレートを検出し、低品質の動画は赤い警告で表示されます。自動的に「R18警告ボタン」をクリックし、毎回手動でプロンプトを閉じる必要がなくなります。
  8. // @version 1.2.9
  9. // @author JoJunIori
  10. // @namespace neko0-web-tools
  11. // @icon https://www.iwara.tv/logo.png
  12. // @homepageURL https://github.com/nekozero/neko0-web-tools
  13. // @supportURL https://t.me/+URovzRdPTyHlWtQd
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM_setClipboard
  17. // @grant window.onurlchange
  18. // @run-at document-idle
  19. // @license AGPL-3.0-or-later
  20. // @require https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js
  21. // @match *://*.iwara.tv/*
  22. // ==/UserScript==
  23. /* global $ ClipboardJS */
  24. /* jshint esversion: 9 */
  25.  
  26. /** 初始化设定 开始 */
  27. // 设置项默认值
  28. let setting = {
  29. type: 'name',
  30. }
  31.  
  32. // 判断是否存在设定
  33. if (GM_getValue('iwara_setting') === undefined) {
  34. GM_setValue('iwara_setting', setting)
  35. } else {
  36. let store = GM_getValue('iwara_setting')
  37. $.each(setting, function (i) {
  38. if (store[i] === undefined) {
  39. store[i] = setting[i]
  40. }
  41. })
  42. GM_setValue('iwara_setting', store)
  43. }
  44. /** 初始化设定 结束 */
  45.  
  46. // 实时获取最新设置
  47. let getSet = () => {
  48. return GM_getValue('iwara_setting')
  49. }
  50. // 更改设置
  51. let setSet = (key, value) => {
  52. let store = GM_getValue('iwara_setting')
  53. store[key] = value
  54. GM_setValue('iwara_setting', store)
  55. }
  56. console.log('iwara_setting', getSet())
  57.  
  58. // 置入Style
  59. let style = `<style>
  60. .page-video__bottom {
  61. border-radius: 5px 5px 0 0 !important;
  62. }
  63. .one-tap, .copy-name {
  64. margin-left: 10px;
  65. }
  66. .detection {
  67. position: absolute;
  68. line-height: 20px;
  69. height: 20px;
  70. top: -30px;
  71. width: 100%;
  72. text-align: center;
  73. color: #3498db;
  74. white-space: nowrap;
  75. }
  76. .copy-name {
  77. position: relative
  78. }
  79. .copy-name ul {
  80. position: absolute;
  81. margin: 0;
  82. top: 100%;
  83. left: 0;
  84. z-index: 99;
  85. border-radius: 5px;
  86. background-color: var(--primary-text);
  87. color: var(--primary);
  88. text-align: left;
  89. padding: 10px;
  90. transition: 0.3s all;
  91. transform: scale(0);
  92. transform-origin: 0 0;
  93. }
  94. .copy-name:hover ul {
  95. transform: scale(1);
  96. }
  97. .copy-name ul li {
  98. cursor: pointer;
  99. list-style: none;
  100. padding: 2px 4px;
  101. margin: 2px 0;
  102. border-radius: 5px;
  103. }
  104. .copy-name ul li.s {
  105. background-color: var(--primary);
  106. color: var(--primary-text);
  107. }
  108. .page-video__actions .likeButton:has(svg.svg-inline--fa.fa-heart-crack) {
  109. background-color: var(--red);
  110. }
  111. // 根除R18警告
  112. .adultWarning {
  113. display: none !important;
  114. }
  115. </style>`
  116. $('head').append(style)
  117.  
  118. var timer = null
  119.  
  120. // 获取作品上传时间
  121. async function getCreateDate(callback) {
  122. let url = window.location.href
  123. let parts = url.split('/')
  124. let text = parts[4]
  125. await fetch('https://api.iwara.tv/video/' + text)
  126. .then(response => response.json())
  127. .then(data => {
  128. let date = new Date(data.createdAt)
  129. let formattedDate = []
  130. // 格式1 230101
  131. formattedDate.push(
  132. date.getFullYear().toString().slice(-2) +
  133. ('0' + (date.getMonth() + 1)).slice(-2) +
  134. ('0' + date.getDate()).slice(-2)
  135. )
  136. // 格式2 2023-1-1
  137. formattedDate.push(date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate())
  138.  
  139. callback(formattedDate)
  140. })
  141. }
  142.  
  143. // 视频播放页
  144. async function videoPage() {
  145. console.log('ƒ videoPage')
  146.  
  147. if ($('.one-tap')[0]) {
  148. clearInterval(timer)
  149. }
  150. console.log('has one-tap?', $('.one-tap')[0])
  151. console.log('about timer:', timer)
  152.  
  153. let video = document.querySelector('.vjs-tech')
  154. if (video) {
  155. clearInterval(timer)
  156.  
  157. // 自动点击R18警告的继续按钮
  158. if ($('.adultWarning')[0]) {
  159. $('.adultWarning__actions>button')[0].click()
  160. }
  161.  
  162. // 文件名
  163. let username = $('.username').attr('title')
  164. let title = $('.page-video__details > .text.mb-1.text--h1.text--bold').text()
  165. let filename = null
  166. let type_name = username + ' - ' + title
  167. let type_date1 = null
  168. let type_date2 = null
  169. let type_date3 = null
  170. let type_date4 = null
  171. await getCreateDate(function (formattedDate) {
  172. console.log(formattedDate)
  173. type_date1 = formattedDate[0] + ' - ' + title
  174. type_date2 = formattedDate[1] + ' - ' + title
  175. type_date3 = formattedDate[0] + ' - ' + username + ' - ' + title
  176. type_date4 = formattedDate[1] + ' - ' + username + ' - ' + title
  177. })
  178.  
  179. // 置入DOM
  180. let dom = `
  181. <button class="button copy-name button--primary button--solid" type="button"><div class="text text--small"><div class="icon mr-1">
  182. <svg class="svg-inline--fa fa-share-nodes " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path fill="currentColor" d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V428.7c-2.7 1.1-5.4 2-8.2 2.7l-60.1 15c-3 .7-6 1.2-9 1.4c-.9 .1-1.8 .2-2.7 .2H240c-6.1 0-11.6-3.4-14.3-8.8l-8.8-17.7c-1.7-3.4-5.1-5.5-8.8-5.5s-7.2 2.1-8.8 5.5l-8.8 17.7c-2.9 5.9-9.2 9.4-15.7 8.8s-12.1-5.1-13.9-11.3L144 381l-9.8 32.8c-6.1 20.3-24.8 34.2-46 34.2H80c-8.8 0-16-7.2-16-16s7.2-16 16-16h8.2c7.1 0 13.3-4.6 15.3-11.4l14.9-49.5c3.4-11.3 13.8-19.1 25.6-19.1s22.2 7.8 25.6 19.1l11.6 38.6c7.4-6.2 16.8-9.7 26.8-9.7c15.9 0 30.4 9 37.5 23.2l4.4 8.8h8.9c-3.1-8.8-3.7-18.4-1.4-27.8l15-60.1c2.8-11.3 8.6-21.5 16.8-29.7L384 203.6V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM549.8 139.7c-15.6-15.6-40.9-15.6-56.6 0l-29.4 29.4 71 71 29.4-29.4c15.6-15.6 15.6-40.9 0-56.6l-14.4-14.4zM311.9 321c-4.1 4.1-7 9.2-8.4 14.9l-15 60.1c-1.4 5.5 .2 11.2 4.2 15.2s9.7 5.6 15.2 4.2l60.1-15c5.6-1.4 10.8-4.3 14.9-8.4L512.1 262.7l-71-71L311.9 321z"/></svg>
  183. </div>复制名字</div>
  184. <ul>
  185. <div>»切换格式:</div>
  186. <li class="${getSet().type == 'name' ? 's' : ''} type_name">${type_name}</li>
  187. <li class="${getSet().type == 'date1' ? 's' : ''} type_date1">${type_date1}</li>
  188. <li class="${getSet().type == 'date2' ? 's' : ''} type_date2">${type_date2}</li>
  189. <li class="${getSet().type == 'date3' ? 's' : ''} type_date3">${type_date3}</li>
  190. <li class="${getSet().type == 'date4' ? 's' : ''} type_date4">${type_date4}</li>
  191. </ul>
  192. </button>
  193. <button class="button one-tap button--primary button--solid" type="button"><div class="text text--small"><div class="icon mr-1">
  194. <svg class="svg-inline--fa fa-share-nodes " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path fill="currentColor" d="M160 64c0-8.8 7.2-16 16-16s16 7.2 16 16V200c0 10.3 6.6 19.5 16.4 22.8s20.6-.1 26.8-8.3c3-3.9 7.6-6.4 12.8-6.4c8.8 0 16 7.2 16 16c0 10.3 6.6 19.5 16.4 22.8s20.6-.1 26.8-8.3c3-3.9 7.6-6.4 12.8-6.4c7.8 0 14.3 5.6 15.7 13c1.6 8.2 7.3 15.1 15.1 18s16.7 1.6 23.3-3.6c2.7-2.1 6.1-3.4 9.9-3.4c8.8 0 16 7.2 16 16l0 16V392c0 39.8-32.2 72-72 72H272 212.3h-.9c-37.4 0-72.4-18.7-93.2-49.9L50.7 312.9c-4.9-7.4-2.9-17.3 4.4-22.2s17.3-2.9 22.2 4.4L116 353.2c5.9 8.8 16.8 12.7 26.9 9.7s17-12.4 17-23V320 64zM176 0c-35.3 0-64 28.7-64 64V261.7C91.2 238 55.5 232.8 28.5 250.7C-.9 270.4-8.9 310.1 10.8 339.5L78.3 440.8c29.7 44.5 79.6 71.2 133.1 71.2h.9H272h56c66.3 0 120-53.7 120-120V288l0-16c0-35.3-28.7-64-64-64c-4.5 0-8.8 .5-13 1.3c-11.7-15.4-30.2-25.3-51-25.3c-6.9 0-13.5 1.1-19.7 3.1C288.7 170.7 269.6 160 248 160c-2.7 0-5.4 .2-8 .5V64c0-35.3-28.7-64-64-64zm48 304c0-8.8-7.2-16-16-16s-16 7.2-16 16v96c0 8.8 7.2 16 16 16s16-7.2 16-16V304zm48-16c-8.8 0-16 7.2-16 16v96c0 8.8 7.2 16 16 16s16-7.2 16-16V304c0-8.8-7.2-16-16-16zm80 16c0-8.8-7.2-16-16-16s-16 7.2-16 16v96c0 8.8 7.2 16 16 16s16-7.2 16-16V304z"/></svg>
  195. </div>一键喜欢关注下载</div>
  196. </button>
  197. `
  198. $('.container-fluid > .row > .col-12')[0].prepend($('.page-video__bottom')[0])
  199. $('.page-video__actions').append(dom)
  200. // 替换广告的位置
  201. $('.page-video__details')[0].after($('.contentBlock.mb-2')[0])
  202.  
  203. // 绑定 按键 事件
  204. $('li.type_name').click(function () {
  205. console.log('type', 'name')
  206. setSet('type', 'name')
  207. $(this).addClass('s').siblings().removeClass('s')
  208. })
  209. $('li.type_date1').click(function () {
  210. console.log('type', 'date1')
  211. setSet('type', 'date1')
  212. $(this).addClass('s').siblings().removeClass('s')
  213. })
  214. $('li.type_date2').click(function () {
  215. console.log('type', 'date2')
  216. setSet('type', 'date2')
  217. $(this).addClass('s').siblings().removeClass('s')
  218. })
  219. $('li.type_date3').click(function () {
  220. console.log('type', 'date3')
  221. setSet('type', 'date3')
  222. $(this).addClass('s').siblings().removeClass('s')
  223. })
  224. $('li.type_date4').click(function () {
  225. console.log('type', 'date4')
  226. setSet('type', 'date4')
  227. $(this).addClass('s').siblings().removeClass('s')
  228. })
  229. $('.copy-name').click(() => {
  230. if (getSet().type == 'date1') {
  231. filename = type_date1
  232. } else if (getSet().type == 'date2') {
  233. filename = type_date2
  234. } else if (getSet().type == 'date3') {
  235. filename = type_date3
  236. } else if (getSet().type == 'date4') {
  237. filename = type_date4
  238. } else {
  239. filename = type_name
  240. }
  241. // 替换windows文件名禁用字符
  242. filename = filename.replace(/[/\\:*?<>|]/g, ' ')
  243. GM_setClipboard(filename)
  244. })
  245. $('.one-tap').click(() => {
  246. // 拷贝名称
  247. $('.copy-name').click()
  248. // 喜欢
  249. $('.page-video__actions svg.svg-inline--fa.fa-heart').parent().parent().parent().click()
  250. // 关注
  251. $('.page-video__byline__actions svg.svg-inline--fa.fa-heart').parent().parent().parent().click()
  252.  
  253. // 下载
  254. $('.dropdown__content a:contains("Source")')[0].click()
  255. })
  256.  
  257. // 解决新版默认不以Source分辨率播放并无法记忆用户设置的问题
  258. const checkSource = function () {
  259. return new Promise((resolve, reject) => {
  260. console.log('ƒ checkSource')
  261. if (!video.src.match('_Source.mp4')) {
  262. $('.vjs-menu-item.resolution-Source').click()
  263. }
  264. resolve()
  265. })
  266. }
  267.  
  268. const checkResolution = function () {
  269. console.log('ƒ checkResolution start')
  270.  
  271. // video可播放后将分辨率标注出来
  272. video.oncanplay = function () {
  273. if (document.querySelector('.detection') !== null) return false
  274. console.log(this)
  275. console.log(this.videoWidth, this.videoHeight)
  276.  
  277. if (
  278. (this.videoWidth < 1920 && this.videoHeight < 1080) ||
  279. (this.videoWidth < 1080 && this.videoHeight < 1920)
  280. ) {
  281. $('.container-fluid > .row > .col-12')
  282. .eq(0)
  283. .prepend(
  284. `<div class="detection"><span class="resolution" style="color: red;">${this.videoWidth} x ${this.videoHeight}</span> <span class="fps"></span></div>`
  285. )
  286. } else {
  287. $('.container-fluid > .row > .col-12')
  288. .eq(0)
  289. .prepend(
  290. `<div class="detection"><span class="resolution">${this.videoWidth} x ${this.videoHeight}</span> <span class="fps"></span></div>`
  291. )
  292. }
  293.  
  294. // 获取帧率 from https://stackoverflow.com/a/73098112
  295. // Part 1:
  296. var vid = this
  297. var last_media_time, last_frame_num, fps
  298. var fps_rounder = []
  299. var frame_not_seeked = true
  300. // Part 2 (with some modifications):
  301. function ticker(useless, metadata) {
  302. var media_time_diff = Math.abs(metadata.mediaTime - last_media_time)
  303. var frame_num_diff = Math.abs(metadata.presentedFrames - last_frame_num)
  304. var diff = media_time_diff / frame_num_diff
  305. if (
  306. diff &&
  307. diff < 1 &&
  308. frame_not_seeked &&
  309. fps_rounder.length < 50 &&
  310. vid.playbackRate === 1 &&
  311. document.hasFocus()
  312. ) {
  313. fps_rounder.push(diff)
  314. fps = Math.round(1 / get_fps_average())
  315. document.querySelector('.fps').textContent =
  316. 'fps:' + fps + ', certainty:' + fps_rounder.length * 2 + '%'
  317. if (fps < 60) {
  318. $('.fps').attr('style', 'color: red')
  319. } else {
  320. $('.fps').attr('style', 'color: #3498db')
  321. }
  322. }
  323. frame_not_seeked = true
  324. last_media_time = metadata.mediaTime
  325. last_frame_num = metadata.presentedFrames
  326. vid.requestVideoFrameCallback(ticker)
  327. }
  328. vid.requestVideoFrameCallback(ticker)
  329. // Part 3:
  330. vid.addEventListener('seeked', function () {
  331. fps_rounder.pop()
  332. frame_not_seeked = false
  333. })
  334. // Part 4:
  335. function get_fps_average() {
  336. return fps_rounder.reduce((a, b) => a + b) / fps_rounder.length
  337. }
  338. }
  339. }
  340.  
  341. checkSource().then(checkResolution)
  342. }
  343. }
  344.  
  345. if (window.location.pathname.indexOf('/video/') !== -1) {
  346. timer = setInterval(videoPage, 1000)
  347. }
  348.  
  349. // 监测页面变换
  350. if (window.onurlchange === null) {
  351. window.addEventListener('urlchange', info => {
  352. console.log('urlchange', info)
  353. if (window.location.pathname.indexOf('/video/') !== -1 && !$('.one-tap')[0]) {
  354. timer = setInterval(videoPage, 1000)
  355. }
  356. })
  357. }