import JSZip from 'jszip'; import mime from 'mime'; import { formatTimeStamp, sleep } from '#shared/utils/helpers'; import { request } from '#shared/utils/request'; import { getComment } from '~/apis'; import { getAssetCache, updateAssetCache } from '~/store/v2/assets'; import type { DownloadableArticle } from '~/types/types'; import type { AudioResource, VideoPageInfo } from '~/types/video'; import * as pool from '~/utils/pool'; import { extractCommentId } from './comment'; /** * 使用代理下载资源 * @param url 资源地址 * @param proxy 代理地址 * @param withCredential * @param timeout 超时时间(单位: 秒),默认 30 */ async function downloadAssetWithProxy( url: string, proxy: string | undefined, withCredential = false, timeout = 30 ) { const headers: Record = {}; if (withCredential) { try { const credentials = JSON.parse(window.localStorage.getItem('credentials')!); headers.cookie = `pass_ticket=${credentials.pass_ticket};wap_sid2=${credentials.wap_sid2}`; } catch (e) {} } let targetURL = proxy ? `${proxy}?url=${encodeURIComponent(url)}&headers=${encodeURIComponent(JSON.stringify(headers))}` : url; targetURL = targetURL.replace(/^http:\/\//, 'https://'); return await request(targetURL, { timeout: timeout * 1000, referrerPolicy: 'unsafe-url', }); } /** * 下载文章的 html * @param articleURL * @param title */ async function downloadArticleHTML(articleURL: string, title?: string) { let html = ''; const parser = new DOMParser(); const htmlDownloadFn = async (url: string, proxy: string) => { const fullHTML = await downloadAssetWithProxy(url, proxy, true); // 验证是否下载完整 const document = parser.parseFromString(fullHTML, 'text/html'); const $jsContent = document.querySelector('#js_content'); const $layout = document.querySelector('#js_fullscreen_layout_padding'); if (!$jsContent) { if ($layout) { console.warn(`文章(${title})已被删除,跳过下载`); return 0; } console.warn(`文章(${title})下载失败`); throw new Error('下载失败,请重试'); } html = fullHTML; return new Blob([html]).size; }; await pool.downloads([articleURL], htmlDownloadFn); if (!html) { throw new Error('下载html失败,请稍后重试'); } return html; } /** * 批量下载文章 html * @param articles * @param callback */ export async function downloadArticleHTMLs(articles: DownloadableArticle[], callback: (count: number) => void) { const parser = new DOMParser(); const results: DownloadableArticle[] = []; const htmlDownloadFn = async (article: DownloadableArticle, proxy: string) => { const fullHTML = await downloadAssetWithProxy(article.url, proxy, true); // 验证是否下载完整 const document = parser.parseFromString(fullHTML, 'text/html'); const $jsContent = document.querySelector('#js_content'); const $layout = document.querySelector('#js_fullscreen_layout_padding'); if (!$jsContent) { if ($layout) { console.warn(`文章(${article.title})已被删除,跳过下载`); return 0; } console.warn(`文章(${article.title})下载失败`); throw new Error('下载失败,请重试'); } article.html = fullHTML; results.push(article); callback(results.length); await sleep(2000); return new Blob([fullHTML]).size; }; await pool.downloads(articles, htmlDownloadFn); return results; } /** * 打包 html 中的资源 * @param fakeid * @param html * @param title * @param zip */ export async function packHTMLAssets(fakeid: string, html: string, title: string, zip?: JSZip) { if (!zip) { zip = new JSZip(); } zip.folder('assets'); const parser = new DOMParser(); const document = parser.parseFromString(html, 'text/html'); const $jsArticleContent = document.querySelector('#js_article')!; const $jsArticleBottomBar = document.querySelector('#js_article_bottom_bar')!; // #js_content 默认是不可见的(通过js修改为可见),需要移除该样式 $jsArticleContent.querySelector('#js_content')?.removeAttribute('style'); // 删除无用dom元素 $jsArticleContent.querySelector('#js_top_ad_area')?.remove(); $jsArticleContent.querySelector('#js_tags_preview_toast')?.remove(); $jsArticleContent.querySelector('#content_bottom_area')?.remove(); $jsArticleContent.querySelectorAll('script').forEach(el => { el.remove(); }); $jsArticleContent.querySelector('#js_pc_qr_code')?.remove(); $jsArticleContent.querySelector('#wx_stream_article_slide_tip')?.remove(); let bodyCls = document.body.className; // 渲染发布时间 function __setPubTime(oriTimestamp: number, dom: HTMLElement) { const dateObj = new Date(oriTimestamp * 1000); const padStart = function padStart(v: number) { return '0'.concat(v.toString()).slice(-2); }; const year = dateObj.getFullYear().toString(); const month = padStart(dateObj.getMonth() + 1); const date = padStart(dateObj.getDate()); const hour = padStart(dateObj.getHours()); const minute = padStart(dateObj.getMinutes()); const timeString = ''.concat(hour, ':').concat(minute); const dateString = ''.concat(year, '年').concat(month, '月').concat(date, '日'); const showDate = ''.concat(dateString, ' ').concat(timeString); if (dom) { dom.innerText = showDate; } } const pubTimeMatchResult = html.match(/var oriCreateTime = '(?\d+)'/); if (pubTimeMatchResult && pubTimeMatchResult.groups && pubTimeMatchResult.groups.date) { __setPubTime(parseInt(pubTimeMatchResult.groups.date), document.getElementById('publish_time')!); } // 渲染ip属地 function getIpWoridng(ipConfig: any) { let ipWording = ''; if (parseInt(ipConfig.countryId, 10) === 156) { ipWording = ipConfig.provinceName; } else if (ipConfig.countryId) { ipWording = ipConfig.countryName; } return ipWording; } const ipWrp = document.getElementById('js_ip_wording_wrp')!; const ipWording = document.getElementById('js_ip_wording')!; const ipWordingMatchResult = html.match(/window\.ip_wording = (?{\s+countryName: '[^']+',[^}]+})/s); if (ipWrp && ipWording && ipWordingMatchResult && ipWordingMatchResult.groups && ipWordingMatchResult.groups.data) { const json = ipWordingMatchResult.groups.data; // eslint-disable-next-line no-eval eval('window.ip_wording = ' + json); const ipWordingDisplay = getIpWoridng((window as any).ip_wording); if (ipWordingDisplay !== '') { ipWording.innerHTML = ipWordingDisplay; ipWrp.style.display = 'inline-block'; } } // 渲染 标题已修改 function __setTitleModify(isTitleModified: boolean) { const wrp = document.getElementById('js_title_modify_wrp')!; const titleModifyNode = document.getElementById('js_title_modify')!; if (!wrp) return; if (isTitleModified) { titleModifyNode.innerHTML = '标题已修改'; wrp.style.display = 'inline-block'; } else { wrp.parentNode?.removeChild(wrp); } } const titleModifiedMatchResult = html.match(/window\.isTitleModified = "(?\d*)" \* 1;/); if (titleModifiedMatchResult && titleModifiedMatchResult.groups && titleModifiedMatchResult.groups.data) { __setTitleModify(titleModifiedMatchResult.groups.data === '1'); } // 文本分享消息 const $js_text_desc = $jsArticleContent.querySelector('#js_text_desc') as HTMLElement | null; if ($js_text_desc) { // 文本分享样式 bodyCls += ' page_share_text'; // 顶部作者栏 const qmtplTextMatchResult = html.match(/(?window\.__QMTPL_SSR_DATA__\s*=\s*\{.+?};)/s); if (qmtplTextMatchResult && qmtplTextMatchResult.groups && qmtplTextMatchResult.groups.code) { const code = qmtplTextMatchResult.groups.code; // eslint-disable-next-line no-eval eval(code); const data = (window as any).__QMTPL_SSR_DATA__; if (data && typeof data.title === 'string' && !$js_text_desc.innerHTML.trim()) { let text = data.title as string; text = text.replace(/\r/g, '').replace(/\n/g, '
'); $js_text_desc.innerHTML = text; } $jsArticleContent.querySelector('#js_top_profile')?.classList.remove('profile_area_hide'); } // 正文内容 if (!$js_text_desc.innerHTML.trim()) { const textContentMatch = html.match( /var\s+TextContentNoEncode\s*=\s*window\.a_value_which_never_exists\s*\|\|\s*(?'[^']*')/s ); const contentMatch = html.match( /var\s+ContentNoEncode\s*=\s*window\.a_value_which_never_exists\s*\|\|\s*(?'[^']*')/s ); let desc: string | null = null; const assignFromMatch = (match: RegExpMatchArray | null, key: string) => { if (match && match.groups && match.groups.value) { const code = `window.${key} = ${match.groups.value}`; // eslint-disable-next-line no-eval eval(code); // @ts-ignore return (window as any)[key] as string; } return null; }; desc = assignFromMatch(textContentMatch, '__WX_TEXT_NO_ENCODE__'); if (!desc) { desc = assignFromMatch(contentMatch, '__WX_CONTENT_NO_ENCODE__'); } if (desc) { desc = desc.replace(/\r/g, '').replace(/\n/g, '
'); $js_text_desc.innerHTML = desc; } } } // 文章引用 const js_share_source = document.getElementById('js_share_source'); const contentTpl = document.getElementById('content_tpl'); if (js_share_source && contentTpl) { const html = contentTpl.innerHTML .replace(/]*>/g, '

[图片]

') .replace( /