iwara download

Download videos from iwara.tv. NOTE: may need grant download privilege to browser extension like tampermonkey.

  1. // ==UserScript==
  2. // @name iwara download
  3. // @name:zh-CN iwara 下载
  4. // @description Download videos from iwara.tv. NOTE: may need grant download privilege to browser extension like tampermonkey.
  5. // @description:zh-CN 下载 iwara 视频。 注意:可能需要给 tampermonkey 等插件设置下载权限。
  6. // @namespace https://sleazyfork.org/zh-CN/scripts/425903-iwara-download
  7. // @version 1.0.0
  8. // @author oajsdfk
  9. // @match https://*.iwara.tv/*
  10. //
  11. // @grant GM_download
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. if (!jQuery) { return; }
  18.  
  19. const PARALLEL = 2;
  20.  
  21. let downloaded_str = localStorage.getItem('downloaded');
  22. let downloaded = [];
  23. if (downloaded_str) {
  24. downloaded = downloaded_str.split(' ');
  25. }
  26.  
  27.  
  28. const is_downloaded = (id) => downloaded.indexOf(id) != -1;
  29. const add_downloaded = (id) => {
  30. if (is_downloaded(id)) return;
  31. let s = localStorage.getItem('downloaded');
  32. if (s != downloaded_str) {
  33. let n = s.indexOf(downloaded_str);
  34. if (n == 0) {
  35. let add = s.substr(downloaded_str.length + 1).split(' ');
  36. console.log("downloaded in other window:", add);
  37. downloaded.push(...add);
  38. downloaded_str = s;
  39. } else {
  40. s.split(' ').forEach((id) => {
  41. if (is_downloaded(id)) return;
  42. console.log("downloaded in other window:", id);
  43. downloaded.push(id);
  44. downloaded_str += ' ' + id;
  45. });
  46. }
  47. }
  48. if (is_downloaded(id)) return;
  49. downloaded.push(id);
  50. downloaded_str += ' ' + id;
  51. localStorage.setItem('downloaded', downloaded_str);
  52. };
  53.  
  54. let downloading_str = '';
  55. let downloading = [];
  56.  
  57. const remove_downloading = (vid) => {
  58. let n = downloading.indexOf(vid);
  59. if (n != -1) {
  60. downloading.splice(n, 1);
  61. downloading_str = downloading.join(' ');
  62. localStorage.setItem('downloading', downloading_str);
  63. }
  64. }
  65.  
  66. const add_downloading = (vid) => {
  67. downloading.push(vid);
  68. downloading_str = downloading.join(' ');
  69. localStorage.setItem('downloading', downloading_str);
  70. }
  71.  
  72. window.onstorage = (e) => {
  73. if (e.key == 'downloading') {
  74. downloading_str = e.newValue;
  75. downloading = downloading_str.split(' ');
  76. return;
  77. }
  78. if (e.key == 'downloaded') {
  79. let s = e.newValue;
  80. if (s != downloaded_str) {
  81. let n = s.indexOf(downloaded_str);
  82. if (n == 0) {
  83. let add = s.substr(downloaded_str.length + 1).split(' ');
  84. console.log("downloaded in other window:", add);
  85. downloaded.push(...add);
  86. downloaded_str = s;
  87. } else {
  88. s.split(' ').forEach((id) => {
  89. if (is_downloaded(id)) return;
  90. console.log("downloaded in other window:", id);
  91. downloaded.push(id);
  92. downloaded_str += ' ' + id;
  93. });
  94. localStorage.setItem('downloaded', downloaded_str);
  95. }
  96. }
  97. return;
  98. }
  99. };
  100.  
  101. var $ = jQuery.noConflict();
  102.  
  103. var view_page = window.location.pathname.startsWith('/videos/')
  104.  
  105. function fname(str) {
  106. return str.replace(/\\/g, '¥')
  107. .replace(/\//g, '/')
  108. .replace(/:/g, ':')
  109. .replace(/\*/g, '*')
  110. .replace(/\?/g, '?')
  111. .replace(/"/g, '”')
  112. .replace(/</g, '<')
  113. .replace(/>/g, '>')
  114. .replace(/\|/g, '|')
  115. .replace(/\t/g, ' ')
  116. .replace(/~/g, '~');
  117. };
  118.  
  119.  
  120. $('.node-video').append('<input type="checkbox" class="dl_chk" checked/>');
  121.  
  122. $('body').append(`<style>
  123. #dlboxs input {
  124. background-color: transparent;
  125. padding: 0px;
  126. margin: 0px;
  127. border: 0px;
  128. }
  129.  
  130. #dlboxs input:hover {
  131. background-color: limegreen;
  132. border: 1px;
  133. }
  134. </style><div id="dlboxs" style="position: fixed; left: 0px; bottom: 0px; z-index: 500; background-color:transparent;">
  135. <input id="set_downloaded" type="button" value="⚐"
  136. title="标记所选已被下载"> <input id="sel_dl_all" type="button" value="◼"
  137. title="全选"><input id="sel_dl_invert" type="button" value="⬗" title="反选"><input
  138. id="sel_dl_none" type="button" value="◻" title="空选"> <input id="download" type="button"
  139. value=" ⭳ " title="下载所选">
  140. </div>`)
  141. $('#sel_dl_all').on('click', function (e) {
  142. $('.dl_chk:enabled').each(function () {
  143. this.checked = true;
  144. });
  145. });
  146. $('#sel_dl_invert').on('click', function (e) {
  147. $('.dl_chk:enabled').each(function () {
  148. this.checked = !this.checked;
  149. });
  150. });
  151. $('#sel_dl_none').on('click', function (e) {
  152. $('.dl_chk:enabled').each(function () {
  153. this.checked = false;
  154. });
  155. });
  156.  
  157. $('#set_downloaded').on('click', function (e) {
  158. let vs = $('.node-video').has('.dl_chk:checked:enabled');
  159. //console.log("set_downloaded:", vs)
  160.  
  161. if (vs.length === 0) { return; }
  162.  
  163. vs.toArray().forEach(v => {
  164. let video = $(v)
  165.  
  166. let b = parse_video(video)
  167. if (!b) return null
  168. let [like, view, vid, title, user] = b
  169.  
  170. console.log('add_downloaded:', '♥' + like + ' 👁' + view + ' ' + user + '/' + title + ' [' + vid + ']');
  171.  
  172. add_downloaded(vid)
  173. set_downloaded(video)
  174. })
  175. });
  176.  
  177.  
  178. function set_downloaded(video) {
  179. if (!view_page) {
  180. let i = video.find('.dl_chk');
  181. i.attr('disabled', true);
  182. i.removeClass('dl_chk');
  183. } else {
  184. $('#download').attr('disabled', true);
  185. }
  186. };
  187.  
  188. function check_downloaded() {
  189. let vs = $('.node-video')//.has('.dl_chk:checked:enabled');
  190.  
  191. if (vs.length === 0) { return; }
  192.  
  193. $(this).val('checking');
  194.  
  195. vs.toArray().forEach(v => {
  196. let video = $(v)
  197. let b = parse_video(video)
  198. if (!b) return null
  199. let [like, view, vid, title, user] = b
  200.  
  201. if (is_downloaded(vid)) {
  202. set_downloaded(video)
  203. return;
  204. }
  205.  
  206. if (downloading.indexOf(vid) != -1) {
  207. set_downloaded(video)
  208. return;
  209. }
  210.  
  211. //console.log('check_downloaded:', '♥' + like + ' 👁' + view + ' ' + user + '/' + title + ' [' + vid + ']');
  212. })
  213. }
  214. check_downloaded()
  215.  
  216. $('#download').on('click', function () {
  217. if (view_page) {
  218. download();
  219. return;
  220. }
  221.  
  222. let checked = $('.node-video').has('.dl_chk:checked:enabled');
  223. if (checked.length === 0) return;
  224.  
  225. async_pool(checked.toArray().map(v => $(v)), download, PARALLEL);
  226. });
  227.  
  228. function parse_video(video) {
  229. let like;
  230. let view;
  231. let vid;
  232. let title;
  233. let user;
  234. if (!view_page) {
  235. let t = video.find('.field-item > a');
  236. if (t) {
  237. let href = t.attr('href')
  238. if (!href) return null
  239. vid = href.replace('/videos/', '');
  240.  
  241. t = t.find('img')
  242. title = t.attr('title') || t.attr('title');
  243. }
  244.  
  245. if (!title || !vid) {
  246. t = video.find('.title > a');
  247. vid = t.attr('href').replace('/videos/', '');
  248. title = t.text();
  249. }
  250.  
  251. like = video.find('.likes-icon').has('.glyphicon-heart').text().trim();
  252. view = video.find('.likes-icon').has('.glyphicon-eye-open').text().trim();
  253. user = video.find('.username').text();
  254. } else {
  255. vid = window.location.pathname.replace('/videos/', '');
  256. title = $('.node-info').find('.title').text();
  257. let t = $('.node-views').has('.glyphicon-heart').has('.glyphicon-eye-open').text().trim().split(/\s+/);
  258. like = t[0];
  259. view = t[1];
  260. user = $('.node-info').find('.username').text();
  261. }
  262.  
  263. if (user.endsWith('.')) {
  264. let n = user.replace(/\.$/, '_');
  265. console.log("rename user:", user, n)
  266. user = n
  267. }
  268.  
  269. //console.log('download:', '♥' + like + ' 👁' + view + ' ' + user + '/' + title + ' [' + vid + ']');
  270. return [like, view, vid, title, user]
  271. }
  272.  
  273. async function download(video) {
  274. let b = parse_video(video)
  275. if (!b) return null
  276. let [like, view, vid, title, user] = b
  277.  
  278. console.log('download:', '♥' + like + ' 👁' + view + ' ' + user + '/' + title + ' [' + vid + ']');
  279.  
  280. let filename = fname(user) + '/' + fname(title) + ' [' + vid + ']';
  281.  
  282. if (is_downloaded(vid)) {
  283. console.log('already downloaded suc:', filename);
  284. set_downloaded(video);
  285. return;
  286. }
  287.  
  288. if (downloading.indexOf(vid) != -1) {
  289. console.log(vid, 'is downloading');
  290. return;
  291. }
  292.  
  293. add_downloading(vid);
  294.  
  295. return new Promise((resolve, reject) => {
  296.  
  297. $.get('/api/video/' + vid, function (res) {
  298. if (res[0]) {
  299. console.log(vid, "urls: ", res[0]);
  300. let t = res[0].mime.split('/');
  301. let f = filename + '.' + t[t.length - 1];
  302.  
  303. let url = 'https:' + res[0].uri;
  304. console.log('downloading file:', f, 'url:', url);
  305.  
  306. GM_download({
  307. url: url,
  308. name: 'iwara/' + f,
  309. onload: () => {
  310. console.log('download suc:', f);
  311. set_downloaded(video);
  312. add_downloaded(vid);
  313. resolve();
  314. },
  315. onprogress: (e) => {
  316. let v = e.loaded / e.total * 100;
  317. console.log('downloading:', f, e, v + '%');
  318. },
  319. onerror: (e) => {
  320. console.error('download failed:', f, e);
  321. remove_downloading(vid);
  322. reject(e);
  323. },
  324. ontimeout: (e) => {
  325. console.error('download timeout:', f, e);
  326. remove_downloading(vid);
  327. reject(e);
  328. },
  329. saveAs: false
  330. });
  331.  
  332. } else {
  333. console.error("no video:", vid);
  334. remove_downloading(vid);
  335. reject("no video");
  336. }
  337. }, 'JSON').fail(function (err) {
  338. console.error("get_url_failed", vid, err);
  339. remove_downloading(vid);
  340. reject(err);
  341. });
  342.  
  343. });
  344. }
  345.  
  346. async function async_pool(args, fn, limit) {
  347. return new Promise((resolve) => {
  348. const argQueue = [...args].reverse();
  349. let count = 0;
  350. const outs = [];
  351. const pollNext = () => {
  352. if (argQueue.length === 0 && count === 0) {
  353. resolve(outs);
  354. } else {
  355. while (count < limit && argQueue.length) {
  356. const index = args.length - argQueue.length;
  357. const arg = argQueue.pop();
  358. count += 1;
  359. const out = fn(arg);
  360. const processOut = (out, index) => {
  361. outs[index] = out;
  362. count -= 1;
  363. pollNext();
  364. };
  365. if (typeof out === 'object' && out.then && out.then) {
  366. out.then(out => processOut({ status: 'fulfilled', value: out }, index))
  367. .catch(err => processOut({ status: 'rejected', reason: err }, index));
  368. } else {
  369. processOut(out, index);
  370. }
  371. }
  372. }
  373. };
  374. pollNext();
  375. });
  376. }
  377. })();