uc.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. import req from './req.js';
  2. import {ENV} from './env.js';
  3. import COOKIE from './cookieManager.js';
  4. import '../libs_drpy/crypto-js.js';
  5. import {join} from 'path';
  6. import fs from 'fs';
  7. import {PassThrough} from 'stream';
  8. class UCHandler {
  9. constructor() {
  10. this.regex = /https:\/\/drive\.uc\.cn\/s\/([^\\|#/]+)/;
  11. this.pr = 'pr=UCBrowser&fr=pc';
  12. this.baseHeader = {
  13. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) uc-cloud-drive/1.8.5 Chrome/100.0.4896.160 Electron/18.3.5.16-b62cf9c50d Safari/537.36 Channel/ucpan_other_ch',
  14. Referer: 'https://drive.uc.cn/',
  15. };
  16. this.apiUrl = 'https://pc-api.uc.cn/1/clouddrive';
  17. this.shareTokenCache = {};
  18. this.saveDirName = 'drpy';
  19. this.saveDirId = null;
  20. this.saveFileIdCaches = {};
  21. this.currentUrlKey = '';
  22. this.cacheRoot = (process.env['NODE_PATH'] || '.') + '/uc_cache';
  23. this.maxCache = 1024 * 1024 * 100;
  24. this.urlHeadCache = {};
  25. this.subtitleExts = ['.srt', '.ass', '.scc', '.stl', '.ttml'];
  26. this.Addition = {
  27. DeviceID: '07b48aaba8a739356ab8107b5e230ad4',
  28. RefreshToken: '',
  29. AccessToken: ''
  30. }
  31. this.conf = {
  32. api: "https://open-api-drive.uc.cn",
  33. clientID: "5acf882d27b74502b7040b0c65519aa7",
  34. signKey: "l3srvtd7p42l0d0x1u8d7yc8ye9kki4d",
  35. appVer: "1.6.8",
  36. channel: "UCTVOFFICIALWEB",
  37. codeApi: "http://api.extscreen.com/ucdrive",
  38. };
  39. }
  40. // 使用 getter 定义动态属性
  41. get cookie() {
  42. // console.log('env.cookie.uc:',ENV.get('uc_cookie'));
  43. return ENV.get('uc_cookie');
  44. }
  45. get token() {
  46. return ENV.get('uc_token_cookie');
  47. }
  48. getShareData(url) {
  49. let matches = this.regex.exec(url);
  50. if (matches[1].indexOf("?") > 0) {
  51. matches[1] = matches[1].split('?')[0];
  52. }
  53. if (matches) {
  54. return {
  55. shareId: matches[1],
  56. folderId: '0',
  57. };
  58. }
  59. return null;
  60. }
  61. async initQuark(db, cfg) {
  62. if (this.cookie) {
  63. console.log("cookie 获取成功");
  64. } else {
  65. console.log("cookie 获取失败")
  66. }
  67. }
  68. lcs(str1, str2) {
  69. if (!str1 || !str2) {
  70. return {
  71. length: 0,
  72. sequence: '',
  73. offset: 0,
  74. };
  75. }
  76. var sequence = '';
  77. var str1Length = str1.length;
  78. var str2Length = str2.length;
  79. var num = new Array(str1Length);
  80. var maxlen = 0;
  81. var lastSubsBegin = 0;
  82. for (var i = 0; i < str1Length; i++) {
  83. var subArray = new Array(str2Length);
  84. for (var j = 0; j < str2Length; j++) {
  85. subArray[j] = 0;
  86. }
  87. num[i] = subArray;
  88. }
  89. var thisSubsBegin = null;
  90. for (i = 0; i < str1Length; i++) {
  91. for (j = 0; j < str2Length; j++) {
  92. if (str1[i] !== str2[j]) {
  93. num[i][j] = 0;
  94. } else {
  95. if (i === 0 || j === 0) {
  96. num[i][j] = 1;
  97. } else {
  98. num[i][j] = 1 + num[i - 1][j - 1];
  99. }
  100. if (num[i][j] > maxlen) {
  101. maxlen = num[i][j];
  102. thisSubsBegin = i - num[i][j] + 1;
  103. if (lastSubsBegin === thisSubsBegin) {
  104. sequence += str1[i];
  105. } else {
  106. lastSubsBegin = thisSubsBegin;
  107. sequence = ''; // clear it
  108. sequence += str1.substr(lastSubsBegin, i + 1 - lastSubsBegin);
  109. }
  110. }
  111. }
  112. }
  113. }
  114. return {
  115. length: maxlen,
  116. sequence: sequence,
  117. offset: thisSubsBegin,
  118. };
  119. }
  120. findBestLCS(mainItem, targetItems) {
  121. const results = [];
  122. let bestMatchIndex = 0;
  123. for (let i = 0; i < targetItems.length; i++) {
  124. const currentLCS = this.lcs(mainItem.name, targetItems[i].name);
  125. results.push({target: targetItems[i], lcs: currentLCS});
  126. if (currentLCS.length > results[bestMatchIndex].lcs.length) {
  127. bestMatchIndex = i;
  128. }
  129. }
  130. const bestMatch = results[bestMatchIndex];
  131. return {allLCS: results, bestMatch: bestMatch, bestMatchIndex: bestMatchIndex};
  132. }
  133. delay(ms) {
  134. return new Promise((resolve) => setTimeout(resolve, ms));
  135. }
  136. async api(url, data, headers, method, retry) {
  137. headers = headers || {};
  138. Object.assign(headers, this.baseHeader);
  139. Object.assign(headers, {
  140. 'Content-Type': 'application/json',
  141. Cookie: this.cookie || '',
  142. });
  143. method = method || 'post';
  144. const resp =
  145. method === 'get' ? await req.get(`${this.apiUrl}/${url}`, {
  146. headers: headers,
  147. }).catch((err) => {
  148. console.error(err);
  149. return err.response || {status: 500, data: {}};
  150. }) : await req.post(`${this.apiUrl}/${url}`, data, {
  151. headers: headers,
  152. }).catch((err) => {
  153. console.error(err);
  154. return err.response || {status: 500, data: {}};
  155. });
  156. const leftRetry = retry || 3;
  157. if (resp.status === 429 && leftRetry > 0) {
  158. await this.delay(1000);
  159. return await this.api(url, data, headers, method, leftRetry - 1);
  160. }
  161. return resp.data || {};
  162. }
  163. async clearSaveDir() {
  164. const listData = await this.api(`file/sort?${this.pr}&pdir_fid=${this.saveDirId}&_page=1&_size=200&_sort=file_type:asc,updated_at:desc`, {}, {}, 'get');
  165. if (listData.data && listData.data.list && listData.data.list.length > 0) {
  166. const del = await this.api(`file/delete?${this.pr}`, {
  167. action_type: 2,
  168. filelist: listData.data.list.map((v) => v.fid),
  169. exclude_fids: [],
  170. });
  171. console.log(del);
  172. }
  173. }
  174. async createSaveDir(clean) {
  175. if (this.saveDirId) {
  176. if (clean) await this.clearSaveDir();
  177. return;
  178. }
  179. const listData = await this.api(`file/sort?${this.pr}&pdir_fid=0&_page=1&_size=200&_sort=file_type:asc,updated_at:desc`, {}, {}, 'get');
  180. if (listData.data && listData.data.list)
  181. for (const item of listData.data.list) {
  182. if (item.file_name === this.saveDirName) {
  183. this.saveDirId = item.fid;
  184. await this.clearSaveDir();
  185. break;
  186. }
  187. }
  188. if (!this.saveDirId) {
  189. const create = await this.api(`file?${this.pr}`, {
  190. pdir_fid: '0',
  191. file_name: this.saveDirName,
  192. dir_path: '',
  193. dir_init_lock: false,
  194. });
  195. console.log(create);
  196. if (create.data && create.data.fid) {
  197. this.saveDirId = create.data.fid;
  198. }
  199. }
  200. }
  201. async getShareToken(shareData) {
  202. if (!this.shareTokenCache[shareData.shareId]) {
  203. delete this.shareTokenCache[shareData.shareId];
  204. const shareToken = await this.api(`share/sharepage/token?${this.pr}`, {
  205. pwd_id: shareData.shareId,
  206. passcode: shareData.sharePwd || '',
  207. });
  208. if (shareToken.data && shareToken.data.stoken) {
  209. this.shareTokenCache[shareData.shareId] = shareToken.data;
  210. }
  211. }
  212. }
  213. async getFilesByShareUrl(shareInfo) {
  214. const shareData = typeof shareInfo === 'string' ? this.getShareData(shareInfo) : shareInfo;
  215. if (!shareData) return [];
  216. await this.getShareToken(shareData);
  217. if (!this.shareTokenCache[shareData.shareId]) return [];
  218. const videos = [];
  219. const subtitles = [];
  220. const listFile = async (shareId, folderId, page) => {
  221. const prePage = 200;
  222. page = page || 1;
  223. const listData = await this.api(`share/sharepage/detail?${this.pr}&pwd_id=${shareId}&stoken=${encodeURIComponent(this.shareTokenCache[shareId].stoken)}&pdir_fid=${folderId}&force=0&_page=${page}&_size=${prePage}&_sort=file_type:asc,file_name:asc`, {}, {}, 'get');
  224. if (!listData.data) return [];
  225. const items = listData.data.list;
  226. if (!items) return [];
  227. const subDir = [];
  228. for (const item of items) {
  229. if (item.dir === true) {
  230. subDir.push(item);
  231. } else if (item.file === true && item.obj_category === 'video') {
  232. if (item.size < 1024 * 1024 * 5) continue;
  233. item.stoken = this.shareTokenCache[shareData.shareId].stoken;
  234. videos.push(item);
  235. } else if (item.type === 'file' && this.subtitleExts.some((x) => item.file_name.endsWith(x))) {
  236. subtitles.push(item);
  237. }
  238. }
  239. if (page < Math.ceil(listData.metadata._total / prePage)) {
  240. const nextItems = await listFile(shareId, folderId, page + 1);
  241. for (const item of nextItems) {
  242. items.push(item);
  243. }
  244. }
  245. for (const dir of subDir) {
  246. const subItems = await listFile(shareId, dir.fid);
  247. for (const item of subItems) {
  248. items.push(item);
  249. }
  250. }
  251. return items;
  252. };
  253. await listFile(shareData.shareId, shareData.folderId);
  254. if (subtitles.length > 0) {
  255. videos.forEach((item) => {
  256. var matchSubtitle = this.findBestLCS(item, subtitles);
  257. if (matchSubtitle.bestMatch) {
  258. item.subtitle = matchSubtitle.bestMatch.target;
  259. }
  260. });
  261. }
  262. return videos;
  263. }
  264. async save(shareId, stoken, fileId, fileToken, clean) {
  265. await this.createSaveDir(clean);
  266. if (clean) {
  267. const saves = Object.keys(this.saveFileIdCaches);
  268. for (const save of saves) {
  269. delete this.saveFileIdCaches[save];
  270. }
  271. }
  272. if (!this.saveDirId) return null;
  273. if (!stoken) {
  274. await this.getShareToken({
  275. shareId: shareId,
  276. });
  277. if (!this.shareTokenCache[shareId]) return null;
  278. }
  279. const saveResult = await this.api(`share/sharepage/save?${this.pr}`, {
  280. fid_list: [fileId],
  281. fid_token_list: [fileToken],
  282. to_pdir_fid: this.saveDirId,
  283. pwd_id: shareId,
  284. stoken: stoken || this.shareTokenCache[shareId].stoken,
  285. pdir_fid: '0',
  286. scene: 'link',
  287. });
  288. if (saveResult.data && saveResult.data.task_id) {
  289. let retry = 0;
  290. while (true) {
  291. const taskResult = await this.api(`task?${this.pr}&task_id=${saveResult.data.task_id}&retry_index=${retry}`, {}, {}, 'get');
  292. if (taskResult.data && taskResult.data.save_as && taskResult.data.save_as.save_as_top_fids && taskResult.data.save_as.save_as_top_fids.length > 0) {
  293. return taskResult.data.save_as.save_as_top_fids[0];
  294. }
  295. retry++;
  296. if (retry > 5) break;
  297. await this.delay(1000);
  298. }
  299. }
  300. return true;
  301. }
  302. async getLiveTranscoding(shareId, stoken, fileId, fileToken) {
  303. if (!this.saveFileIdCaches[fileId]) {
  304. const saveFileId = await this.save(shareId, stoken, fileId, fileToken, true);
  305. if (!saveFileId) return null;
  306. this.saveFileIdCaches[fileId] = saveFileId;
  307. }
  308. const transcoding = await this.api(`file/v2/play?${this.pr}`, {
  309. fid: this.saveFileIdCaches[fileId],
  310. resolutions: 'normal,low,high,super,2k,4k',
  311. supports: 'fmp4',
  312. });
  313. if (transcoding.data && transcoding.data.video_list) {
  314. return transcoding.data.video_list;
  315. }
  316. return null;
  317. }
  318. async refreshUcCookie(from = '') {
  319. const nowCookie = this.cookie;
  320. const cookieSelfRes = await axios({
  321. url: "https://pc-api.uc.cn/1/clouddrive/config?pr=UCBrowser&fr=pc",
  322. method: "GET",
  323. headers: {
  324. "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch',
  325. Origin: 'https://drive.uc.cn',
  326. Referer: 'https://drive.uc.cn/',
  327. Cookie: nowCookie
  328. }
  329. });
  330. const cookieResDataSelf = cookieSelfRes.headers;
  331. const resCookie = cookieResDataSelf['set-cookie'];
  332. if (!resCookie) {
  333. console.log(`${from}自动更新UC cookie: 没返回新的cookie`);
  334. return
  335. }
  336. const cookieObject = COOKIE.parse(resCookie);
  337. // console.log(cookieObject);
  338. if (cookieObject.__puus) {
  339. const oldCookie = COOKIE.parse(nowCookie);
  340. const newCookie = COOKIE.stringify({
  341. __pus: oldCookie.__pus,
  342. __puus: cookieObject.__puus,
  343. });
  344. console.log(`${from}自动更新UC cookie: ${newCookie}`);
  345. ENV.set('uc_cookie', newCookie);
  346. }
  347. }
  348. generateDeviceID(timestamp) {
  349. return CryptoJS.MD5(timestamp).toString().slice(0, 16); // 取前16位
  350. }
  351. generateReqId(deviceID, timestamp) {
  352. return CryptoJS.MD5(deviceID + timestamp).toString().slice(0, 16);
  353. }
  354. generateXPanToken(method, pathname, timestamp, key) {
  355. const data = method + '&' + pathname + '&' + timestamp + '&' + key;
  356. return CryptoJS.SHA256(data).toString();
  357. }
  358. async getDownload(shareId, stoken, fileId, fileToken, clean) {
  359. if (!this.saveFileIdCaches[fileId]) {
  360. const saveFileId = await this.save(shareId, stoken, fileId, fileToken, clean);
  361. if (!saveFileId) return null;
  362. this.saveFileIdCaches[fileId] = saveFileId;
  363. }
  364. if (this.token) {
  365. let video = []
  366. const pathname = '/file';
  367. const timestamp = Math.floor(Date.now() / 1000).toString() + '000'; // 13位时间戳需调整
  368. const deviceID = this.Addition.DeviceID || this.generateDeviceID(timestamp);
  369. const reqId = this.generateReqId(deviceID, timestamp);
  370. const x_pan_token = this.generateXPanToken("GET", pathname, timestamp, this.conf.signKey);
  371. let config = {
  372. method: 'GET',
  373. url: `https://open-api-drive.uc.cn/file`,
  374. params: {
  375. req_id: reqId,
  376. access_token: this.token,
  377. app_ver: this.conf.appVer,
  378. device_id: deviceID,
  379. device_brand: 'Xiaomi',
  380. platform: 'tv',
  381. device_name: 'M2004J7AC',
  382. device_model: 'M2004J7AC',
  383. build_device: 'M2004J7AC',
  384. build_product: 'M2004J7AC',
  385. device_gpu: 'Adreno (TM) 550',
  386. activity_rect: '{}',
  387. channel: this.conf.channel,
  388. method: "streaming",
  389. group_by: "source",
  390. fid: this.saveFileIdCaches[fileId],
  391. resolution: "low,normal,high,super,2k,4k",
  392. support: "dolby_vision"
  393. },
  394. headers: {
  395. 'User-Agent': 'Mozilla/5.0 (Linux; U; Android 9; zh-cn; RMX1931 Build/PQ3A.190605.05081124) AppleWebKit/533.1 (KHTML, like Gecko) Mobile Safari/533.1',
  396. 'Connection': 'Keep-Alive',
  397. 'Accept-Encoding': 'gzip',
  398. 'x-pan-tm': timestamp,
  399. 'x-pan-token': x_pan_token,
  400. 'content-type': 'text/plain;charset=UTF-8',
  401. 'x-pan-client-id': this.conf.clientID
  402. }
  403. }
  404. let req = await axios.request(config);
  405. if (req.status === 200) {
  406. let videoInfo = req.data.data.video_info
  407. videoInfo.forEach((item) => {
  408. video.push({
  409. name: item.resolution,
  410. url: item.url
  411. })
  412. })
  413. return video;
  414. }
  415. } else {
  416. const down = await this.api(`file/download?${this.pr}`, {
  417. fids: [this.saveFileIdCaches[fileId]],
  418. });
  419. if (down.data) {
  420. const low_url = down.data[0].download_url;
  421. const low_cookie = this.cookie;
  422. const low_headers = {
  423. "Referer": "https://drive.uc.cn/",
  424. "cookie": low_cookie,
  425. "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch'
  426. };
  427. // console.log('low_url:', low_url);
  428. const test_result = await this.testSupport(low_url, low_headers);
  429. // console.log('test_result:', test_result);
  430. if (!test_result[0]) {
  431. try {
  432. await this.refreshUcCookie('getDownload');
  433. } catch (e) {
  434. console.log(`getDownload:自动刷新UC cookie失败:${e.message}`)
  435. }
  436. }
  437. return down.data[0];
  438. }
  439. }
  440. return null;
  441. }
  442. async getLazyResult(downCache, mediaProxyUrl) {
  443. const urls = [];
  444. downCache.forEach((it) => {
  445. urls.push(it.name, it.url);
  446. });
  447. return {parse: 0, url: urls}
  448. /*
  449. // 旧的加速写法
  450. const downUrl = downCache.download_url;
  451. const headers = {
  452. "Referer": "https://drive.uc.cn/",
  453. "cookie": this.cookie,
  454. "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch'
  455. };
  456. urls.push("UC原画", downUrl);
  457. urls.push("原代服", mediaProxyUrl + `?thread=${ENV.get('thread') || 6}&form=urlcode&randUa=1&url=` + encodeURIComponent(downUrl) + '&header=' + encodeURIComponent(JSON.stringify(headers)));
  458. if (ENV.get('play_local_proxy_type', '1') === '2') {
  459. urls.push("原代本", `http://127.0.0.1:7777/?thread=${ENV.get('thread') || 6}&form=urlcode&randUa=1&url=` + encodeURIComponent(downUrl) + '&header=' + encodeURIComponent(JSON.stringify(headers)));
  460. } else {
  461. urls.push("原代本", `http://127.0.0.1:5575/proxy?thread=${ENV.get('thread') || 6}&chunkSize=256&url=` + encodeURIComponent(downUrl));
  462. }
  463. return {
  464. parse: 0,
  465. url: urls,
  466. header: headers,
  467. }
  468. */
  469. }
  470. async testSupport(url, headers) {
  471. const resp = await req
  472. .get(url, {
  473. responseType: 'stream',
  474. headers: Object.assign(
  475. {
  476. Range: 'bytes=0-0',
  477. },
  478. headers,
  479. ),
  480. })
  481. .catch((err) => {
  482. // console.error(err);
  483. console.error('[testSupport] error:', err.message);
  484. return err.response || {status: 500, data: {}};
  485. });
  486. if (resp && resp.status === 206) {
  487. const isAccept = resp.headers['accept-ranges'] === 'bytes';
  488. const contentRange = resp.headers['content-range'];
  489. const contentLength = parseInt(resp.headers['content-length']);
  490. const isSupport = isAccept || !!contentRange || contentLength === 1;
  491. const length = contentRange ? parseInt(contentRange.split('/')[1]) : contentLength;
  492. delete resp.headers['content-range'];
  493. delete resp.headers['content-length'];
  494. if (length) resp.headers['content-length'] = length.toString();
  495. return [isSupport, resp.headers];
  496. } else {
  497. console.log('[testSupport] resp.status:', resp.status);
  498. return [false, null];
  499. }
  500. }
  501. delAllCache(keepKey) {
  502. try {
  503. fs.readdir(this.cacheRoot, (_, files) => {
  504. if (files)
  505. for (const file of files) {
  506. if (file === keepKey) continue;
  507. const dir = join(this.cacheRoot, file);
  508. fs.stat(dir, (_, stats) => {
  509. if (stats && stats.isDirectory()) {
  510. fs.readdir(dir, (_, subFiles) => {
  511. if (subFiles)
  512. for (const subFile of subFiles) {
  513. if (!subFile.endsWith('.p')) {
  514. fs.rm(join(dir, subFile), {recursive: true}, () => {
  515. });
  516. }
  517. }
  518. });
  519. }
  520. });
  521. }
  522. });
  523. } catch (error) {
  524. console.error(error);
  525. }
  526. }
  527. async chunkStream(inReq, outResp, url, urlKey, headers, option) {
  528. urlKey = urlKey || CryptoJS.enc.Hex.stringify(CryptoJS.MD5(url)).toString();
  529. if (this.currentUrlKey !== urlKey) {
  530. this.delAllCache(urlKey);
  531. this.currentUrlKey = urlKey;
  532. }
  533. if (!this.urlHeadCache[urlKey]) {
  534. const [isSupport, urlHeader] = await this.testSupport(url, headers);
  535. if (!isSupport || !urlHeader['content-length']) {
  536. outResp.redirect(url);
  537. return;
  538. }
  539. this.urlHeadCache[urlKey] = urlHeader;
  540. }
  541. let exist = true;
  542. await fs.promises.access(join(this.cacheRoot, urlKey)).catch((_) => (exist = false));
  543. if (!exist) {
  544. await fs.promises.mkdir(join(this.cacheRoot, urlKey), {recursive: true});
  545. }
  546. const contentLength = parseInt(this.urlHeadCache[urlKey]['content-length']);
  547. let byteStart = 0;
  548. let byteEnd = contentLength - 1;
  549. const streamHeader = {};
  550. if (inReq.headers.range) {
  551. const ranges = inReq.headers.range.trim().split(/=|-/);
  552. if (ranges.length > 2 && ranges[2]) {
  553. byteEnd = parseInt(ranges[2]);
  554. }
  555. byteStart = parseInt(ranges[1]);
  556. Object.assign(streamHeader, this.urlHeadCache[urlKey]);
  557. streamHeader['content-length'] = (byteEnd - byteStart + 1).toString();
  558. streamHeader['content-range'] = `bytes ${byteStart}-${byteEnd}/${contentLength}`;
  559. outResp.code(206);
  560. } else {
  561. Object.assign(streamHeader, this.urlHeadCache[urlKey]);
  562. outResp.code(200);
  563. }
  564. option = option || {chunkSize: 1024 * 256, poolSize: 5, timeout: 1000 * 10};
  565. const chunkSize = option.chunkSize;
  566. const poolSize = option.poolSize;
  567. const timeout = option.timeout;
  568. let chunkCount = Math.ceil(contentLength / chunkSize);
  569. let chunkDownIdx = Math.floor(byteStart / chunkSize);
  570. let chunkReadIdx = chunkDownIdx;
  571. let stop = false;
  572. const dlFiles = {};
  573. for (let i = 0; i < poolSize && i < chunkCount; i++) {
  574. new Promise((resolve) => {
  575. (async function doDLTask(spChunkIdx) {
  576. if (stop || chunkDownIdx >= chunkCount) {
  577. resolve();
  578. return;
  579. }
  580. if (spChunkIdx === undefined && (chunkDownIdx - chunkReadIdx) * chunkSize >= this.maxCache) {
  581. setTimeout(doDLTask, 5);
  582. return;
  583. }
  584. const chunkIdx = spChunkIdx || chunkDownIdx++;
  585. const taskId = `${inReq.id}-${chunkIdx}`;
  586. try {
  587. const dlFile = join(this.cacheRoot, urlKey, `${inReq.id}-${chunkIdx}.p`);
  588. let exist = true;
  589. await fs.promises.access(dlFile).catch((_) => (exist = false));
  590. if (!exist) {
  591. const start = chunkIdx * chunkSize;
  592. const end = Math.min(contentLength - 1, (chunkIdx + 1) * chunkSize - 1);
  593. console.log(inReq.id, chunkIdx);
  594. const dlResp = await req.get(url, {
  595. responseType: 'stream',
  596. timeout: timeout,
  597. headers: Object.assign(
  598. {
  599. Range: `bytes=${start}-${end}`,
  600. },
  601. headers,
  602. ),
  603. });
  604. const dlCache = join(this.cacheRoot, urlKey, `${inReq.id}-${chunkIdx}.dl`);
  605. const writer = fs.createWriteStream(dlCache);
  606. const readTimeout = setTimeout(() => {
  607. writer.destroy(new Error(`${taskId} read timeout`));
  608. }, timeout);
  609. const downloaded = new Promise((resolve) => {
  610. writer.on('finish', async () => {
  611. if (stop) {
  612. await fs.promises.rm(dlCache).catch((e) => console.error(e));
  613. } else {
  614. await fs.promises.rename(dlCache, dlFile).catch((e) => console.error(e));
  615. dlFiles[taskId] = dlFile;
  616. }
  617. resolve(true);
  618. });
  619. writer.on('error', async (e) => {
  620. console.error(e);
  621. await fs.promises.rm(dlCache).catch((e1) => console.error(e1));
  622. resolve(false);
  623. });
  624. });
  625. dlResp.data.pipe(writer);
  626. const result = await downloaded;
  627. clearTimeout(readTimeout);
  628. if (!result) {
  629. setTimeout(() => {
  630. doDLTask(chunkIdx);
  631. }, 15);
  632. return;
  633. }
  634. }
  635. setTimeout(doDLTask, 5);
  636. } catch (error) {
  637. console.error(error);
  638. setTimeout(() => {
  639. doDLTask(chunkIdx);
  640. }, 15);
  641. }
  642. })();
  643. });
  644. }
  645. outResp.headers(streamHeader);
  646. const stream = new PassThrough();
  647. new Promise((resolve) => {
  648. let writeMore = true;
  649. (async function waitReadFile() {
  650. try {
  651. if (chunkReadIdx >= chunkCount || stop) {
  652. stream.end();
  653. resolve();
  654. return;
  655. }
  656. if (!writeMore) {
  657. setTimeout(waitReadFile, 5);
  658. return;
  659. }
  660. const taskId = `${inReq.id}-${chunkReadIdx}`;
  661. if (!dlFiles[taskId]) {
  662. setTimeout(waitReadFile, 5);
  663. return;
  664. }
  665. const chunkByteStart = chunkReadIdx * chunkSize;
  666. const chunkByteEnd = Math.min(contentLength - 1, (chunkReadIdx + 1) * chunkSize - 1);
  667. const readFileStart = Math.max(byteStart, chunkByteStart) - chunkByteStart;
  668. const dlFile = dlFiles[taskId];
  669. delete dlFiles[taskId];
  670. const fd = await fs.promises.open(dlFile, 'r');
  671. const buffer = Buffer.alloc(chunkByteEnd - chunkByteStart - readFileStart + 1);
  672. await fd.read(buffer, 0, chunkByteEnd - chunkByteStart - readFileStart + 1, readFileStart);
  673. await fd.close().catch((e) => console.error(e));
  674. await fs.promises.rm(dlFile).catch((e) => console.error(e));
  675. writeMore = stream.write(buffer);
  676. if (!writeMore) {
  677. stream.once('drain', () => {
  678. writeMore = true;
  679. });
  680. }
  681. chunkReadIdx++;
  682. setTimeout(waitReadFile, 5);
  683. } catch (error) {
  684. setTimeout(waitReadFile, 5);
  685. }
  686. })();
  687. });
  688. stream.on('close', async () => {
  689. Object.keys(dlFiles).forEach((reqKey) => {
  690. if (reqKey.startsWith(inReq.id)) {
  691. fs.rm(dlFiles[reqKey], {recursive: true}, () => {
  692. });
  693. delete dlFiles[reqKey];
  694. }
  695. });
  696. stop = true;
  697. });
  698. return stream;
  699. }
  700. }
  701. export const UC = new UCHandler();