返回首頁 

Greasy Fork is available in English.

抖音/快手/微视/instagram/TIKTOK/小红书/微博/今日头条 主页视频下载

在抖音/快手/微视/instagram/TIKTOK/小红书/微博/今日头条 主页右小角显示视频下载按钮


Installer dette script?
// ==UserScript==// @name         抖音/快手/微视/instagram/TIKTOK/小红书/微博/今日头条 主页视频下载// @namespace    shortvideo_homepage_downloader// @version      1.3.2// @description  在抖音/快手/微视/instagram/TIKTOK/小红书/微博/今日头条 主页右小角显示视频下载按钮// @author       hunmer// @match        https://pixabay.com/videos/search/*// @match        https://www.xinpianchang.com/discover/*// @match        https://www.douyin.com/user/*// @match        https://www.douyin.com/search/*// @match        https://www.douyin.com/video/*// @match        https://www.douyin.com/note/*// @match        https://www.toutiao.com/c/user/token/*// @match        https://www.kuaishou.com/profile/*// @match        https://www.kuaishou.com/search/video*// @match1       https://www.youtube.com/@*/shorts// @match        https://x.com/*/media// @match        https://weibo.com/u/*?tabtype=newVideo*// @match        https://isee.weishi.qq.com/ws/app-pages/wspersonal/index.html*// @match        https://www.instagram.com/*// @match        https://www.xiao####shu.com/user/profile/*// @match        https://www.xiao####shu.com/search_r###lt/*// @match        https://www.xiao####shu.com/explore*// @match        https://www.tiktok.com/@*// @match        https://www.tiktok.com/search*// @match        https://artlist.io/stock-footage/story/*// @match        https://artlist.io/stock-footage/search?*// @icon         https://lf1-cdn-tos.bytegoofy.com/goofy/ies/douyin_web/public/favicon.ico// @grant        GM_download// @grant        GM_addStyle// @grant        GM_setValue// @grant        GM_getValue// @grant        GM_addElement// @grant        unsafeWindow// @grant        GM_xmlhttpRequest// @run-at       document-start// @license      MIT// ==/UserScript==const $ = selector => document.querySelectorAll('#_dialog '+selector)const ERROR = -1, WAITTING = 0, DOWNLOADING = 1, DOWNLOADED = 2const VERSION = '1.3.2', RELEASE_DATE = '2025/03/04'const DEBUGING = falseconst DEBUG = (...args) => DEBUGING && console.log.apply(this, args)const toArr = arr => Array.isArray(arr) ? arr : [arr]const guid = () => {return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {var r = Math.random() * 16 | 0,v = c == 'x' ? r : (r & 0x3 | 0x8)return v.toString(16)})}Date.prototype.format = function (fmt) {var o = {"M+": this.getMonth() + 1,"d+": this.getDate(),"h+": this.getHours(),"m+": this.getMinutes(),"s+": this.getSeconds(),"q+": Math.floor((this.getMonth() + 3) / 3),"S": this.getMilliseconds()};if (/(y+)/.test(fmt)) {fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length))}for (var k in o) {if (new RegExp("(" + k + ")").test(fmt)) {fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)))}}return fmt}const flattenArray = arr => {if(!Array.isArray(arr)) return []var r###lt = []for (var i = 0; i < arr.length; i++) {if (Array.isArray(arr[i])) {r###lt = r###lt.concat(flattenArray(arr[i]))} else {r###lt.push(arr[i])}}return r###lt}const getExtName = name => {switch(name){case 'video':return 'mp4'case 'image':case 'photo':return 'jpg'}return name ?? 'mp4'}const escapeHTMLPolicy = typeof(trustedTypes) != 'undefined' ? trustedTypes.createPolicy("forceInner", {createHTML: html => html,}) : {createHTML: html => html}const createHTML = html => escapeHTMLPolicy.createHTML(html)const openFileDialog = ({callback, accept = '*'}) => {let input = document.createElement('input')input.type = 'file'input.style.display = 'none'input.accept = acceptdocument.body.appendChild(input)input.addEventListener('change', ev => callback(ev.target.files) & input.remove())input.click()}const loadRes = (files, callback) => {return new Promise(reslove => {files = [...files]var next = () => {let url = files.shift()if (url == undefined) {callback && callback()return reslove()}let fileref, ext = url.split('.').at(-1)if (ext == 'js') {fileref = GM_addElement('script', {src: url,type: ext == 'js' ? "text/javascript" : 'module'})} else if (ext == "css") {fileref = GM_addElement('link', {href: url,rel: "stylesheet",type: "text/css"})}if (fileref != undefined) {let el = document.getElementsByTagName("head")[0].appendChild(fileref)el.onload = next, el.onerror = next} else {next()}}next()})}const cutString = (s_text, s_start, s_end, i_start = 0, fill = false) => {i_start = s_text.indexOf(s_start, i_start)if (i_start === -1) return ''i_start += s_start.lengthi_end = s_text.indexOf(s_end, i_start)if (i_end === -1) {if (!fill) return ''i_end = s_text.length}return s_text.substr(i_start, i_end - i_start)}const getParent = (el, callback) => {let par = elwhile(par && !callback(par)){par = par.parentElement}return par}const chooseObject = (cb, ...objs) => {let callback = typeof(cb) == 'function' ? cb : obj => obj?.[cb]return objs.find(callback)}// 样式GM_addStyle(`._dialog {input[type=checkbox] {-webkit-appearance: auto !important;}color: white !important;font-size: large !important;font-family: unset !important;input {color: white;border: 1px solid;}table tr td, table tr th {vertical-align: middle;}input[type=text], button {color: white !important;background-color: unset !important;}table input[type=checkbox] {width: 20px;height: 20px;transform: scale(1.5);-webkit-appearance: checkbox;}}body:has(dialog[open]) {overflow: hidden;}`);unsafeWindow._downloader = _downloader = {loadRes,resources: [], running: false, downloads: {},options: Object.assign({threads: 8,firstRun: true,autoRename: false,alert_done: true,douyin_host: 1, // 抖音默认第二个线路timeout: 1000 * 60,retry_max: 60,autoScroll: true,aria2c_port: 6800,aria2c_saveTo: './downloads'}, GM_getValue('config', {})),saveOptions(opts = {}){opts = Object.assign(this.options, opts)GM_setValue('config', opts)},_aria_callbacks: [],bindAria2Event(method, gid, callback){this._aria_callbacks.push({method: 'aria2.' + method,gid, callback})},enableAria2c(enable){if(enable){if(!this.aria2c){loadRes(['https://www.unpkg.com/[email protected]/bundle.js', 'https://www.unpkg.com/[email protected]/bundle.js'], () => {this.writeLog('正在连接aria2,请等待连接成功后再开始下载!!!', 'ARIA2C')var aria2 = this.aria2c = new unsafeWindow.Aria2({host: 'localhost',port: this.options.aria2c_port,secure: false,secret: '',path: '/jsonrpc',jsonp: false})aria2.open().then(() => {aria2.opening = truethis.writeLog('aria2成功连接!', 'ARIA2C')$('[data-for="useAria2c"]')[0].checked = true})aria2.onclose =  () => {aria2.opening = falsethis.writeLog('aria2失去连接!', 'ARIA2C')$('[data-for="useAria2c"]')[0].checked = false}aria2.onmessage = ({ method: _method, id, r###lt, params }) => {console.log({_method, r###lt, params})switch (_method) {// case 'aria2.onDownloadError': // 下载完成了还莫名触发?case 'aria2.onDownloadComplete':for (let i = this._aria_callbacks.length - 1; i >= 0; i--) {let { gid, method, callback } = this._aria_callbacks[i]if (gid == params[0].gid) {if (method == _method) { // 如果gid有任何一个事件成功了则删除其他事件绑定callback()}this._aria_callbacks.splice(i, 1)}}return}}})}}else{if(this.aria2c){this.aria2c.close()this.aria2c = undefined}}},addDownload(opts){console.log(opts)let _id = guid()var {id, url, name, error, success, download, downloadTool} = optsif(download){ // 命名规则let {ext, type, title} = downloadext ||= getExtName(type)name = this.safeFileName(this.getDownloadName(id) ?? title) + (ext != '' ? '.' + ext : '')}const callback = (status, msg) => {let cb = opts[status]cb && cb(msg)this.removeDownload(_id)}var abort, timervar headers = this.getHeaders(url)if(downloadTool == 'm3u8dl'){let base64 = new Base64().encode(`"${url}" --workDir "${this.options.aria2c_saveTo}" --saveName "${name}" --enableDelAfterDone --headers "Referer:https://artlist.io/" --maxThreads "6" --downloadRange "0-1"`)unsafeWindow.open(`m3u8dl://`+base64, '_blank')return callback('success', '下载完成...')}if(this.aria2c){var _guidthis.aria2c.send("addUri", [url], {dir: this.options.aria2c_saveTo,header: Object.entries(headers).map(([k, v]) => `${k}: ${v}`),out: name,}).then(guid => {_guid = guidthis.bindAria2Event('onDownloadComplete', guid, () => callback('success', '下载完成...'))this.bindAria2Event('onDownloadError', guid, () => callback('error', '下载失败'))})abort = () => _guid && this.aria2c.send("remove", [_guid])}else{var fileStreamabort = () => fileStream.abort()timer = setTimeout(() => {callback('error', '超时')this.removeDownload(_id, true)}, this.options.timeout)const writeStream = readableStream => {if (unsafeWindow.WritableStream && readableStream.pipeTo) {return readableStream.pipeTo(fileStream).then(() => callback('success', '下载完成...')).catch(err => callback('error', '下载失败'))}}let isTiktok = location.host == 'www.tiktok.com'if(isTiktok) headers.Referer = urlGM_xmlhttpRequest({url, headers,redirect: 'follow', responseType: 'blob', method: "GET",onload: ({response, status}) => {console.log({response, status})// BUG 不知为啥tiktok无法使用流保存if(isTiktok || typeof(streamSaver) == 'undefined'){return unsafeWindow.saveAs(response, name) & callback('success', '下载完成...')}let res = new Response(response).clone()fileStream = streamSaver.createWriteStream(name, {size: response.size})writeStream(res.body)//writeStream(response.stream())}})}return this.downloads[_id] = {abort, timer}},removeDownload(id, cancel = false){let {timer, abort} = this.downloads[id] ?? {}if(timer) clearTimeout(timer)cancel && abort()delete this.downloads[id]},setBadge(html){let fv = document.querySelector('#_ftb')if(!fv){fv = document.createElement('div')fv.id = '_ftb'fv.style.cssText = `position: fixed;bottom: 50px;right: 50px;border-radius: 20px;background-color: #fe2c55;color: white;z-index: 999;cursor: pointer;padding: 5px;`fv.onclick = () => this.showList()fv.oncontextmenu = ev => {this.setList([], false)fv.remove()ev.stopPropagation(true) & ev.preventDefault()}document.body.append(fv)}fv.innerHTML = createHTML(html)},init(){ // 初始化const parseDouyinList = data => {let {video, desc, images} = datalet author = data.author ?? data.authorInfolet aweme_id = data.aweme_id ?? data.awemeIdlet create_time = data.create_time ?? data.createTime//let {uri, height} = video.play_addr || {}let xl = this.options.douyin_hostreturn {status: WAITTING,id: aweme_id,url: 'https://www.douyin.com/video/'+aweme_id,cover: (video?.cover?.url_list || video?.coverUrlList)[0],author_name: author.nickname,create_time: create_time * 1000,urls: images ? images.map(({height, width, download_url_list, downloadUrlList}, index) => {return {url: (download_url_list ?? downloadUrlList)[0], type: 'photo'}}) : video.play_addr.url_list.at(xl),title: desc,data}}this.HOSTS = { // 网站规则'x.com': {title: '推特', id: 'twitter',rules: [{url: 'https://x.com/i/api/graphql/(.*?)/UserMedia',type: 'network',parseList: json => json?.data?.user?.r###lt?.timeline_v2?.timeline?.instructions?.[0]?.moduleItems,parseItem: data => {let {legacy, user_r###lts, core, views: {count: view_count}} = data.item.itemContent.tweet_r###lts.r###ltlet {description: author_desc, name: author_name, id: author_id,} = core.user_r###lts.r###ltlet {created_at, full_text: title, lang, extended_entities, favorite_count, bookmark_count, quote_count, reply_count, retweet_count, id_str: id} = legacyif(extended_entities?.media) return extended_entities.media.map(({type, media_url_https: url, original_info: {height, width}}, index) => {return {status: WAITTING,url: 'https://x.com/pentarouX/status/'+id,cover: url+'?format=jpg&name=360x360',id: id, author_name, urls: [{url, type}], title, index, create_time: created_at,data}})}}]},'www.youtube.com': {title: '油管', id: 'youtube',getVideoURL: item => new Promise(reslove => {fetch(item.url).then(resp => resp.text()).then(text => {let json = JSON.parse(cutString(text, '"noteDetailMap":', ',"serverRequestInfo":'))let meta = item.meta = json[item.id]reslove(meta.note.video.media.stream.h264[0].masterUrl)})}),rules: [{url: 'https://www.youtube.com/youtubei/v1/browse',type: 'fetch',parseList: json => json?.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems,parseItem: data => {if(!data.richItemRenderer) returnlet {videoId, headline, thumbnail} = data.richItemRenderer.content.reelItemRendererreturn {status: WAITTING,id: videoId,url: 'https://www.youtube.com/shorts/'+videoId,cover: thumbnail.thumbnails[0].url,author_name: '', urls: '', title: headline.simpleText,data}}}]},'pixabay.com': {title: 'pixabay', id: 'pixabay',rules: [{type: 'object',getObject: window => window?.__BOOTSTRAP__?.page?.r###lts,parseList: json => json,parseItem: data => {let {id, description, href , user, uploadDate, name, sources} = datareturn {status: WAITTING,id, url: 'https://pixabay.com'+href, cover: sources.thumbnail,author_name: user.username,urls: sources.mp4.replace('_tiny', ''),title: `【${name}】${description}`, create_time: uploadDate,data}}}]},'weibo.com': {title: '微博', id: 'weibo',rules: [{url: 'https://weibo.com/ajax/profile/getWaterFallContent',type: 'network',parseList: json => json?.data?.list,parseItem: data => {let {page_info, created_at, text_raw} = datalet {short_url, object_id, media_info, page_pic} = page_inforeturn {status: WAITTING,id: object_id,url: short_url,cover: page_pic,author_name: media_info.author_name,urls: media_info.playback_list[0].play_info.url,title: text_raw, create_time: created_at,data}}}]},'www.xinpianchang.com': {title: '新片场', id: 'xinpianchang',runAtWindowLoaded: false,getVideoURL: item => new Promise(reslove => {fetch(`https://mod-api.xinpianchang.com/mod/api/v2/media/${item.media_id}?appKey=61a2f329348b3bf77&extend=userInfo%2CuserStatus`).then(resp => resp.json()).then(json => {reslove(json.data.resource.progressive.find(({url}) => url != '').url)})}),rules: [{url: 'https://www.xinpianchang.com/_next/data/',type: 'json',parseList: json => {return flattenArray((json?.pageProps?.squareData?.section || []).map(({articles}) => articles || []))},parseItem: data => {console.log(data)let {author, content, cover, media_id, title, web_url, publish_time, id} = datareturn {status: WAITTING,id, url: web_url, cover, title, media_id,author_name: author.userinfo.username,create_time: publish_time,data}}}]},'www.xiao####shu.com': {title: '小红书', id: 'xhs',getVideoURL: item => new Promise(reslove => {fetch(item.url).then(resp => resp.text()).then(text => {let json = JSON.parse(cutString(text, '"noteDetailMap":', ',"serverRequestInfo":'))let note = json[item.id].noteObject.assign(item, {create_time: note.time, meta: note})console.log(note)reslove(note.type == 'video' ? {url: note.video.media.stream.h265[0].masterUrl, type: 'video'} : note.imageList.map(({urlDefault}) => {return {url: urlDefault, type: 'photo'}}))})}),rules: [{type: 'object',getObject: window => location.href.startsWith('https://www.xiao####shu.com/explore/') ? window?.__INITIAL_STATE__?.note?.noteDetailMap : {},parseList: json => {let list = Object.values(json).filter(({note}) => note).map(({note}) => note)return list},parseItem: data => {let { desc, imageList = [], noteId: id, time, user, xsecToken, title, type, video} = datalet images = imageList.map(({urlDefault}) => {return {url: urlDefault, type: 'photo'}})let urls = type == 'normal' ? images : video.media.stream.h265[0].masterUrlreturn {status: WAITTING, author_name: user.nickname, id, url: 'https://www.xiao####shu.com/explore/'+id, urls,cover: images[0].url,title: desc, data}}},{type: 'object',getObject: window => chooseObject(obj => flattenArray(obj).length > 0, window?.__INITIAL_STATE__?.user.notes?._rawValue, window?.__INITIAL_STATE__?.search.feeds?._rawValue,  window?.__INITIAL_STATE__?.feed.feeds?._rawValue),parseList: json => {let list = Array.isArray(json) ? (json.length == 4 ? json[0] : json) : []return list},parseItem: data => {let { cover, displayTitle, noteId, type, user, xsecToken} = data?.noteCard || {}let id = noteId ?? data.idxsecToken ??= data.xsecTokenif(xsecToken) {return {status: WAITTING, author_name: user.nickname, id, url: `https://www.xiao####shu.com/explore/${id}?source=webshare&xhsshare=pc_web&xsec_token=${xsecToken.slice(0, -1)}=&xsec_source=pc_share`,// +'?xsec_token='+xsecToken+'=&xsec_source=pc_user',cover: cover.urlDefault,title: (displayTitle ?? '').replaceAll('🥹', ''), data}}}}]},'isee.weishi.qq.com': {title: '微视', id: 'weishi',rules: [{url: 'https://api.weishi.qq.com/trpc.weishi.weishi_h5_proxy.weishi_h5_proxy/GetPersonalFeedList',type: 'network',parseList: json => json?.rsp_body?.feeds,parseItem: data => {let {feed_desc, id, poster, publishtime, urls, video_cover, createtime } = datareturn {status: WAITTING, author_name: poster?.nick, id, url: 'https://isee.weishi.qq.com/ws/app-pages/share/index.html?id='+id,cover: video_cover.static_cover.url,urls, title: feed_desc,create_time: createtime * 1000,data}}}]},'www.kuaishou.com': {title: '快手', id: 'kuaishou',rules: [{url: 'https://www.kuaishou.com/graphql',type: 'json',parseList: json => {let href = location.hrefif(href.startsWith('https://www.kuaishou.com/profile/')){return json?.data?.visionProfileLikePhotoList?.feeds || json?.data?.visionProfilePhotoList?.feeds}if(href.startsWith('https://www.kuaishou.com/search/')){return json?.data?.visionSearchPhoto?.feeds}},parseItem: data => {let {photo, author} = datareturn {status: WAITTING, author_name: author.name, id: photo.id, url: 'https://www.kuaishou.com/short-video/'+photo.id,cover: photo.coverUrl,urls: photo.photoUrl,create_time: photo.timestamp,// urls: photo.videoResource.h264.adaptationSet[0].representation[0].url,title: photo.originCaption,data}}}],},'www.toutiao.com': {title: '今日头条短视频', id: 'toutiao',rules: [{url: 'https://www.toutiao.com/api/pc/list/user/feed',type: 'json',parseList: json => json?.data,parseItem: data => {let {video, title, id, user, thumb_image_list, create_time} = datareturn {status: WAITTING, id, title, data,url: 'https://www.toutiao.com/video/'+id,cover: thumb_image_list[0].url,author_name: user.info.name,create_time: create_time * 1000,urls: video.download_addr.url_list[0],}}}],},'www.douyin.com': {title: '抖音', id: 'douyin',scrollContainer: {'https://www.douyin.com/user/': '.route-scroll-container'},hosts: [0, 1, 2], // 3个线路runAtWindowLoaded: false,bindVideoElement: {initElement: node => {let par = getParent(node, el => el?.dataset?.e2eVid)if(par) return {id: par.dataset.e2eVid}let id = cutString(location.href + '?', '/video/', '?')if(id) return {id}}},timeout: {'/user/': 500,'/note/': 500,'/video/': 500,'/search/': 500,},rules: [{type: 'object',getObject: window => {let noteId = cutString(window.location.href + '#', '/note/', '#')if(noteId){let raw = cutString((window?.self?.__pace_f ?? []).filter(arr => arr.length == 2).map(([k, v]) => v || '').join(''), '"aweme":{', ',"comment').replaceAll(`\\"`, '')if(raw.at(-1) == '}'){let json = JSON.parse("{"+raw)if(json.detail.awemeId == noteId) return json}}},parseList: json => {return json ? [json.detail] : []},parseItem: parseDouyinList},{ // 个人喜欢url: 'https://www.douyin.com/aweme/v1/web/aweme/favorite/',type: 'network',parseList: json => location.href == 'https://www.douyin.com/user/self?from_tab_name=main&showTab=like' ? json?.aweme_list : [],parseItem: parseDouyinList,},{ // 个人收藏url: 'https://www.douyin.com/aweme/v1/web/aweme/listcollection/',type: 'network',parseList: json => location.href == 'https://www.douyin.com/user/self?from_tab_name=main&showTab=favorite_collection' ? json?.aweme_list : [],parseItem: parseDouyinList,},{url: 'https://(.*?).douyin.com/aweme/v1/web/aweme/post/',type: 'network',parseList: json => location.href.startsWith('https://www.douyin.com/user/') ? json?.aweme_list : [],parseItem: parseDouyinList}, {url: 'https://www.douyin.com/aweme/v1/web/general/search/single/',type: 'network',parseList: json => json?.data,parseItem: data => parseDouyinList(data.aweme_info)},{url: 'https://www.douyin.com/aweme/v1/web/aweme/detail/',type: 'network',parseList: json => location.href.startsWith('https://www.douyin.com/video/') ? [json.aweme_detail] : [],parseItem: parseDouyinList},]},'www.tiktok.com': {title: '国际版抖音', id: 'tiktok',rules: [{url: 'https://www.tiktok.com/api/post/item_list/',type: 'respone.json',parseList: json => json?.itemList,parseItem: data => {let {video, desc, author, id, createTime} = datareturn {status: WAITTING, id,url: 'https://www.tiktok.com/@'+ author.uniqueId +'/video/'+id,cover: video.originCover,author_name: author.nickname,create_time: createTime * 1000,//urls: video.downloadAddr,urls: video?.bitrateInfo?.[0]?.PlayAddr.UrlList[0],title: desc,data}}},{url: 'https://www.tiktok.com/api/search/general/full/',type: 'respone.json',parseList: json => json?.data,parseItem: data => {let {video, desc, author, id, createTime} = data.itemreturn {status: WAITTING, id,url: 'https://www.tiktok.com/@'+ author.uniqueId +'/video/'+id,cover: video.originCover,author_name: author.nickname,create_time: createTime * 1000,urls: video?.bitrateInfo?.[0]?.PlayAddr.UrlList?.at(-1),title: desc,data}}}]},'www.instagram.com': {title: 'INS', id: 'instagram',rules: [{url: 'https://www.instagram.com/graphql/query',type: 'network',parseList: json => json?.data?.xdt_api__v1__feed__user_timeline_graphql_connection?.edges,parseItem: data => {// media_type == 2let {code, owner, product_type, image_versions2, video_versions, caption } = data.nodeif(product_type == "clips") return {// owner.idstatus: WAITTING, id: code,url: 'https://www.instagram.com/reel/'+code+'/',cover: image_versions2.candidates[0].url,author_name: owner.username,urls: video_versions[0].url,create_time: caption.created_at * 1000,title: caption.text,data}}}]},'artlist.io': {title: 'artlist', id: 'artlist',rules: [{// url: 'https://search-api.artlist.io/v1/graphql',type: 'json',parseList: json => {return json?.data?.story?.clips || json?.data?.clipList?.exactR###lts},parseItem: data => {let {thumbnailUrl, clipPath, clipName, orientation, id, clipNameForUrl, storyNameForURL } = datareturn {status: WAITTING, id, downloadTool: 'm3u8dl',url: 'https://artlist.io/stock-footage/clip/'+clipNameForUrl+'/'+id,cover: thumbnailUrl,author_name: storyNameForURL,urls: [{url: clipPath.replace('playlist', '1080p'), type: ""}],title: clipName,data}}}]}}let DETAIL = this.DETAIL = this.HOSTS[location.host]if(!DETAIL) returnconsole.log(DETAIL)var originalParse, originalSend, originalFetch, originalResponseJsonconst initFun = () => {originalParse = JSON.parse, originalSend = XMLHttpRequest.prototype.send, originalFetch = unsafeWindow._fetch = unsafeWindow.fetch, originalResponseJson = Response.prototype.jsonif(this.options.firstRun){this.options.firstRun = falsethis.saveOptions()alert("欢迎使用此视频批量下载脚本,以下是常见问题:\n【1】.Q:为什么没有显示下载入口?A:当前网址不支持\n【2】Q:为什么捕获不到视频?A:试着滚动视频列表,让他进行加载\n【3】Q:为什么抖音主页显示用户未找到?A:多刷新几次【4】Q:提示下载失败怎么办?A:可以批量导出链接用第三方软件进行下载(如IDM)")}this.setBadge("等待滚动捕获数据中...")}var resources = this.resources, object_callbacks = []const hook = () => {let json_callbacks = [], network_callbacks = [], fetch_callbacks = [], respone_json_callbacks = []DETAIL.rules.forEach(({type, parseList, parseItem, url, getObject, match}, rule_index) => {const callback = json => {// console.log(json)try {// TODO sortlet cnt = resources.push(...(flattenArray((parseList(json) || []).map(item => toArr(parseItem(item)).map(_item => Object.assign(_item || {}, {rule_index})))).filter(item => item.id && !resources.find(({id, index}) => id == item.id && index == item.index))))if(cnt <= 0) returnthis.tryAutoRenameAll()this.setBadge(`下载 ${cnt} 个视频`)} catch(err){console.error(err)}}switch(type){case 'object':let obj = getObject(unsafeWindow)return callback(obj)case 'json':return json_callbacks.push(json => callback(Object.assign({}, json)))case 'network':return network_callbacks.push({url, callback})case 'fetch':return fetch_callbacks.push({url, callback})case 'respone.json':return respone_json_callbacks.push(json => callback(Object.assign({}, json)))}})if(json_callbacks.length){JSON.parse = function(...args) {let json = originalParse.apply(JSON, args)json_callbacks.forEach(cb => cb(json))return json}}if(respone_json_callbacks.length){Object.defineProperty(Response.prototype, 'json', {value: function() {let ret = originalResponseJson.apply(this, arguments)ret.then(json => respone_json_callbacks.forEach(cb => cb(json)))return ret},writable: true,enumerable: false,configurable: true});}const cb = (callbacks, {fullURL, raw}) => {callbacks.forEach(({url, callback}) => {if(new RegExp(url).test(fullURL) && typeof(raw) == 'string' && (raw.startsWith('{') && raw.endsWith('}') || raw.startsWith('[') && raw.endsWith(']'))){callback(JSON.parse(raw))}})}if(network_callbacks.length){XMLHttpRequest.prototype.send = function() {this.addEventListener('load', function() {if(['', 'text'].includes(this.responseType)) cb(network_callbacks ,{fullURL: this.responseURL, raw: this.responseText})})originalSend.apply(this, arguments)}}if(fetch_callbacks.length){unsafeWindow.fetch = function() {return originalFetch.apply(this, arguments).then(response => {if (response.status == 200) {response.clone().text().then(raw => {cb(fetch_callbacks, {fullURL: response.url, raw})})}return response})}}}let timeout = Object.entries(DETAIL.timeout || {}).find(([path, ms]) => (unsafeWindow.location.pathname || '').startsWith(path))?.[1] || 0const start = () => {if(!this.inited){this.inited = truesetTimeout(() => initFun() & hook() & setInterval(() => hook(), 250), timeout)}}if(!DETAIL.runAtWindowLoaded) start()window.onload = () => start() & (DETAIL.bindVideoElement && this.bindVideoElement(DETAIL.bindVideoElement)) & this.initAction()},tryAutoRenameAll(){if(this.options.autoRename && this.isShowing()){if(!this.initedRename){this.initedRename = truelet lastName = this.options.lastRenameif(typeof(lastName) == 'string') $('#_filename')[0].value = lastName}this.applyRenameAll()}},autoScroll_timer: -1, autoScroll: false,switchAutoScroll(enable){if(this.autoScroll_timer){clearInterval(this.autoScroll_timer)this.autoScroll_timer = -1}if(this.autoScroll = enable ?? !this.autoScroll){let auto_download = confirm('捕获结束后是否开启自动下载?(不要最小化浏览器窗口!!!)')var auto_rename = falseif(auto_download) auto_rename = confirm('下载前是否应用名称更改?')this.writeLog(`开启自动滚动捕获,自动下载【${auto_download ? '开' : '关'}】`)let _max = 10, _retry = 0const next = () => {let scrollContainer = Object.entries(this.DETAIL.scrollContainer ?? {}).find(([host, selector]) => new RegExp(host).test(location.href))if(scrollContainer){let container = document.querySelectorAll(scrollContainer[1])[0]if(container) container.scrollTop = container.scrollHeight}else{unsafeWindow.scrollTo(0, document.body.scrollHeight)}let _old = this.resources.lengthsetTimeout(() => {let _new = this.resources.lengthif(_old == _new){this.writeLog(`没有捕获到视频,将会在重试${_max - _retry}次后结束`)if(_max - _retry++ <= 0){this.writeLog('成功捕获所有的视频')this.switchAutoScroll(false)if(auto_download){auto_rename && this.applyRenameAll()this.switchRunning(true)}return}}else{this.writeLog(`捕获到${_new - _old}个视频,当前视频总数${_new}`)this.updateTable()}setTimeout(() => next(), 500)}, 2000)}next()}else{this.writeLog(`开启关闭滚动捕获`)}},setList(list, refresh = true){this.resources = listrefresh && this.refresh()},refresh(){this.showList()document.querySelector('#_ftb').innerHTML = createHTML(`下载 ${this.resources.length} 个视频`)},bindVideoElement({callback, initElement}){returnconst observer = new MutationObserver((mutations) => {for (const mutation of mutations) {if (mutation.type !== 'childList') returnmutation.addedNodes.forEach((node) => {if (node.nodeType === Node.ELEMENT_NODE && node.nodeName == 'VIDEO') {let {id} = initElement(node) || {}let item = this.findItem(id)if(!item) returnlet url = item.urls || node.currentSrc || node.querySelector('source')?.src// if(!url || url.startsWith('blob')){ }if(!node.querySelector('._btn_download')){let el = document.createElement('div')el.classList.className = '_btn_download'el.style.cssText = 'width: 30px;margin: 5px;background-color: rgba(0, 0, 0, .4);color: white;cursor: pointer;position: relative;left: 0;top: 0;z-index: 9999;'el.onclick = ev => {const onError = () => false && alert(`下载失败`)GM_download({url, name: this.safeFileName(item.title) + '.mp4', headers:  this.getHeaders(url),onload: ({status}) => {if(status == 502 || status == 404){onError()}},ontimeout: onError,onerror: onError,})el.remove() & ev.stopPropagation(true) & ev.preventDefault()}el.innerHTML = createHTML('下载')el.title = '点击下载'node.before(el)}}})}})observer.observe(document.body, {childList: true, // 观察子节点的增减subtree: true     // 观察后代节点})},getHeaders(url){return {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',// 'Referer': url,'Range': 'bytes=0-','Referer': location.protocol+'//'+ location.host}},showList(){ // 展示主界面let threads = this.options['threads']this.showDialog({id: '_dialog',html: `<div style="display: inline-flex;flex-wrap: wrap;width: 100%;justify-content: space-around;height: 5%;min-height: 30px;"><div><button id="_selectAll">全选</button><button id="_reverSelectAll">反选</button><button id="_clear_log">清空日志</button></div><div>命名规则:<input type="text" id="_filename" value="【{发布者}】{标题}({id})" title="允许的变量:{发布者} {标题} {id}"><button id="_apply_filename">应用</button><button id="_apply_filename_help">帮助</button></div><div>下载线程数:<input id="_threads" type="range" value=${threads} step=1 min=1 max=32><span id="_threads_span">${threads}</span><span style="margin-right: 10px;">Aria2下载</span><input type="checkbox" data-for="useAria2c" ${this.options.useAria2c ? 'checked': ''}></div><div><button id="_settings">设置</button><button id="_autoScroll">滚动捕获</button><button id="_clearDownloads">清空已下载</button><button id="_reDownloads">重新下载</button><button id="_switchRunning" disabled>开始</button></div></div><div style="height: 70%;overflow-y: scroll;"><table width="90%" border="2" style="margin: 0 auto;"></table></div><div style="height: 25%; width: 100%;border-top: 2px solid white;"><div style="position: relative;height: 100%;"><div style="position: absolute;right: 0;top: 0;padding: 10px;"><span style="margin-right: 10px;">自动滚动</span><input type="checkbox" data-for="autoScroll" ${this.options.autoScroll ? 'checked': ''}></div><pre id="_log" style="background-color: rgba(255, 255, 255, .2);color: rgba(0, 0, 0, .8);overflow-y: scroll;height: 90%;"></pre></div></div>`,callback: dialog => {if(!this.aria2c) this.enableAria2c(this.options.useAria2c)this.initInputs(dialog) & this.updateTable()this.tryAutoRenameAll()},onClose: () => this.resources.forEach(item => item.status = WAITTING)}) & this.bindEvents() & [`欢迎使用本脚本!当前版本: ${VERSION} 发布日期: ${RELEASE_DATE}`,`此脚本仅供学习交流使用!!请勿用于非法用途!`].forEach(msg => this.writeLog(msg, '声明')) & this.loadDownloader()},loadDownloader(){this.writeLog('正在加载下载功能模块...')loadRes(['https://cdn.jsdelivr.net/npm/[email protected]/dist/ponyfill.min.js', 'https://cdn.jsdelivr.net/npm/[email protected]/StreamSaver.min.js', 'https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js'], () => {this.writeLog('加载下载功能模块成功!')$('#_switchRunning')[0].disabled = false/*unsafeWindow.onunload = () => {writableStream.abort()writer.abort()}unsafeWindow.onbeforeunload = evt => {if (!done) {evt.returnValue = `Are you sure you want to leave?`;}}*/})},updateTable(){$('table')[0].innerHTML = createHTML(`<tr align="center"><th>编号</th><th>选中</th><th>封面</th><th>标题</th><th>状态</th></tr>${this.resources.map((item, index) => {let {urls, title, cover, url, id} = item || {}return `<tr align="center" data-id="${id}"><td style="width: 50px;">${index+1}<p><a href="#" data-action="addDownload" style="color:blue">下载</a></p></td><td style="width: 50px;"><input type="checkbox" style="transform: scale(1.5);" checked></td><td style="width: 100px;"><a href="${url}" target="_blank"><img loading="lazy" src="${cover}" style="width: 100px;min-height: 100px;"></a></td><td contenteditable style="width: 400px;max-width: 400px;">${title}</td><td style="width: 100px;">等待中...</td></tr>`}).join('')}`)},getDialog(id){return document.querySelector('#'+id)},isShowing(id = '_dialog'){return this.getDialog(id) !== null},showDialog({html, id, callback, onClose}){ // 弹窗let dialog = this.getDialog(id)dialog && dialog.remove()document.body.insertAdjacentHTML('beforeEnd', createHTML(`<dialog class="_dialog" id="${id}" style="top: 0;left: 0;width: 100%;height: 100%;position: fixed;z-index: 9999;background-color: rgba(0, 0, 0, .8);color: #fff;padding: 10px;overflow: auto; overscroll-behavior: contain;" open><a href="#" style="position: absolute;right: 20px;top: 20px;padding: 10px;background-color: rgba(255, 255, 255, .4);" class="_dialog_close">X</a>${html}<dialog>`))setTimeout(() => {let dialog = this.getDialog(id)dialog.querySelector('._dialog_close').onclick = () => dialog.remove() & (onClose && onClose())callback && callback(dialog)}, 500)},applyRenameAll(){let format = $('#_filename')[0].valuethis.saveOptions({lastRename: format})for(let tr of $('table tr[data-id]')){this.applyRename(tr.dataset.id, tr, format)}},applyRename(tid, tr, format){tr ??= this.findElement(tid)if(!tr) returnlet item = this.findItem(tid)if(!item) returnformat ??= $('#_filename')[0].valueif(typeof(format) != 'string' || format == '') returnlet {title, author_name, id, create_time} = Object.assign(item, {renamed: true})let s = format.replace('{标题}', title ?? '').replace('{id}', id).replace('{发布者}', author_name ?? '')if(create_time){s = new Date(create_time).format(s)}tr.querySelector('td[contenteditable]').innerHTML = createHTML(s)},bindEvents(){ // 绑定DOM事件$('#_threads')[0].oninput = function(ev){$('#_threads_span')[0].innerHTML = createHTML(this.value)}$('#_apply_filename')[0].onclick = () => this.applyRenameAll() & (['www.xiao####shu.com'].includes(location.host) && alert("请注意:小红书网站上日期规则预览不会立刻生效,只有在开始下载的时候才会生效!"))$('#_apply_filename_help')[0].onclick = () => this.showDialog({id: '_dialog_rename_help',html: `<p><h1>变量<h1><h3>{标题} {id} {发布者} yyyy年MM月dd日_hh时mm分ss秒<h3></p><p><h1>常见问题<h1><h3><pre>为什么没有显示入口按钮?(可能是脚本插入时机慢了,可以多滚动或者多刷新几次)为什么下载显示失败(常见于抖音,抖音每个视频有三个线路,但并不是每个线路都是有视频存在的。所以目前的解决是 每个线路都尝试下载一次)为什么捕获的数量不等于主页作品数量(目前只能捕获视频作品,而非图文作品)为什么只能下载一个文件?(请检查网站是否有开启允许同时下载多个文件选项)为什么只能捕获一页的数据/翻页不了(有些不常用的站点可能存在这些问题待修复)</pre><h3></p><p><h1>测试页面<h1><h3><pre>https://isee.weishi.qq.com/ws/app-pages/wspersonal/index.html?id=1538201906643006https://www.douyin.com/user/MS4wLjABAAAANfnAjG-xB__cCOB4hTXFBvG6yZFWNl-FkgCWvpwGN2Mhttps://www.douyin.com/search/%E6%88%91%E4%BB%AChttps://www.kuaishou.com/profile/3xqyyjytuef8nsqhttps://www.tiktok.com/@simonboyyyyyyyhttps://www.xiao####shu.com/user/profile/60f0ecec0000000001004874https://www.instagram.com/rohman__oficial/https://weibo.com/u/2328516855?tabtype=newVideohttps://x.com/pentarouX/mediahttps://www.toutiao.com/c/user/token/MS4wLjABAAAAzCbyoWKVhqhvIgUd49i5o43v4-YcICXye1glC0Xefok/?entrance_gid=7417305773065929267&log_from=f6060c90895cc_1727227709729&tab=video</pre><h3></p><p><h1>使用Aria2c下载<h1><h3><pre>如何安装? 从https://wwas.lanzouj.com/b032c68ozc 密码:36yz 下载解压,双击bat文件开启</pre><h3></p>`,})$('#_selectAll')[0].onclick = () => $('table input[type=checkbox]').forEach(el => el.checked = true)$('#_reverSelectAll')[0].onclick = () => $('table input[type=checkbox]').forEach(el => el.checked = !el.checked)$('#_clear_log')[0].onclick = () => $('#_log')[0].innerHTML = createHTML('')$('#_switchRunning')[0].onclick = () => this.switchRunning()$('#_autoScroll')[0].onclick = () => this.switchAutoScroll()$('#_settings')[0].onclick = () => {this.showDialog({id: '_dialog_settings',html: `<div style="display: flex;width: 100%;gap: 20px;"><div><h3>线路设置</h3>${Object.values(this.HOSTS).map(({hosts, title, id}) => {hosts ??= []let html = `${title}线路: <select data-for="${id}">${hosts.map(host => `<option ${this.options[id+'_host'] == host ? 'selected' : ''}>${host}</option>`).join('')}</select>`return hosts.length ? html : ''}).join('')}</div><div><h3>下载设置</h3><div>下载结束提示<input type="checkbox" data-for="alert_done" ${this.options.alert_done ? 'checked': ''}></div><div>自动重命名<input type="checkbox" data-for="autoRename" ${this.options.autoRename ? 'checked': ''}></div><div>超时时间(毫秒): <input type="number" value="${this.options.timeout}" data-for="timeout"></div><div>重试次数: <input type="number" value="${this.options.retry_max}" data-for="retry_max"></div></div><div><h3>数据设置</h3><div><button data-action="exportData">导出数据</button><button data-action="exportUrls">导出视频链接</button><button data-action="importData">导入数据</button></div></div><div><h3>Aria2c设置</h3><div><div>端口: <input type="number" value="${this.options.aria2c_port}" data-for="aria2c_port"></div><div>保存目录: <input type="text" value="${this.options.aria2c_saveTo}" data-for="aria2c_saveTo"></div></div></div></div>`,callback: dialog => this.initInputs(dialog),onClose: () => this.resources = this.resources.map(item => this.DETAIL.rules[item.rule_index].parseItem(item.data))})}$('#_clearDownloads')[0].onclick = () => this.clearDownloads()$('#_reDownloads')[0].onclick = () => this.reDownloads()},initAction(){const onEvent = ev => {let {srcElement} = evlet {action} = srcElement.datasetswitch(action){case 'addDownload':let par = getParent(srcElement, el => el?.dataset?.id)if(par){this.downloadItem(this.findItem(par.dataset.id), true)}returncase 'exportUrls':return this.addDownload({url: URL.createObjectURL(new Blob([flattenArray(this.resources.map(({urls}) => Array.isArray(urls) ? urls.map(({url}) => url) : urls)).join("\r\n")])),name: '导出链接.txt'})case 'exportData':// todo csvif(!this.resources.length) return alert('没有任何数据')return this.addDownload({url: URL.createObjectURL(new Blob([JSON.stringify(this.resources)])),name: '导出数据.txt'})case 'importData':return openFileDialog({accept: '.txt',callback: files => {let reader = new FileReader()reader.readAsText(files[0])reader.onload = e => {try {json = JSON.parse(reader.r###lt)let cnt = json.lengthif(cnt){if(confirm(`发现${cnt}条数据!是否重置下载状态?`)) json = json.map(item => Object.assign(item, {status: WAITTING}))this.setList(json) & this.writeLog('成功导入数据')}} catch (err) {alert(err.toString())}}}})default:return}ev.stopPropagation(true) & ev.preventDefault()}document.body.addEventListener('click', onEvent)},initInputs(dialog){const self = thisfor(let select of dialog.querySelectorAll('select')) select.onchange = function(){self.saveOptions({[`${this.dataset.for}_host`]: this.value})}for(let input of dialog.querySelectorAll('input')) input.onchange = function(){let value, key = this.dataset.forswitch(this.type){case 'checkbox':case 'switch':value = this.checkedbreakdefault:value = this.value}self.saveOptions({[key]: value})if(key == 'useAria2c') self.enableAria2c(value)}},clearDownloads(){this.eachItems(DOWNLOADED, ({tr, item, index}) => {this.resources.splice(index, 1)tr && tr.remove()})},reDownloads(){this.cancelDownloads()let cnt = this.eachItems([DOWNLOADING, ERROR], ({tr, item}) => {if(tr){let td = tr.querySelectorAll('td')td[4].style.backgroundColor = 'unset'td[4].innerHTML = createHTML('等待中...')}item.status = WAITTING}).lengthcnt ? this.writeLog(`重新下载${cnt}个视频`) & this.switchRunning(true) : alert('没有需要重新下载的任务')},cancelDownloads(){Object.keys(this.downloads).forEach(id => this.removeDownload(id))this.writeLog(`成功取消所有下载`)},eachItems(status_id, callback){let ret = []status_id = toArr(status_id)for(let i=this.resources.length-1;i>=0;i--){let item = this.resources[i]ret.push(item)let {status, id} = itemif(status_id.includes(status)){let tr = this.findElement(id)callback({tr, item, index: i})}}return ret},checkFinishTimer: -1,switchRunning(running){ // 切换运行状态this.running = running ??= !this.running$('#_switchRunning')[0].innerHTML = createHTML(running ? '暂停' : '运行')if(running){let threads = parseInt($('#_threads')[0].value)let cnt = threads - this.getItems(DOWNLOADING).lengthif(cnt){this.writeLog('开始线程下载:'+cnt)this.saveOptions({threads})for(let i=0;i<cnt;i++) this.nextDownload()}}},getItems(_status){ // 获取指定状态任务return this.resources.filter(({status}) => status == _status)},getDownloadName(id){let tr = this.findElement(id)if(tr){let td = tr.querySelectorAll('td')return td[3].outerText}return null},downloadItem(item, checked){let {status, id, urls, rule_index, downloadTool} = itemif(status == WAITTING){let tr = this.findElement(id)if(!tr) returnlet td = tr.querySelectorAll('td')checked ??= td[1].querySelector('input[type=checkbox]').checkedif(checked){item.status = DOWNLOADINGconst log = ({msg, color, next = true, status}) => {this.writeLog(msg, `<a href="${item.url}" target="_blank" style="color: white;">${this.safeFileName(item.title)}</a>`, color)status ??= {success: DOWNLOADED, error: ERROR}[color]this.setItemStatus({id, color, msg, el: tr, item, status})if(next) this.nextDownload()}log({msg: '正在下载', color: 'primary', next: false})// 预先下载并尝试重试(多线程下需要重试才能正常下载)let retry = 0const httpRequest = url => {toArr(url).forEach(download => {if(typeof(download) == 'string') download = {url: download, type: 'video', title: item.title}var {url} = downloadconst done = (url, headers) => this.addDownload({download, url, id, headers, downloadTool,error: msg => log({msg, color: 'error'}),success: msg => log({msg, color: 'success'}),})return done(url)/*if(this.aria2c){done(url)}else{GM_xmlhttpRequest({method: "GET", url, headers: this.getHeaders(url),redirect: 'follow',//responseType: "blob",timeout: this.options.timeout,anonymous: true,onload: ({status, response, finalUrl}) => {console.log({status, finalUrl, response})if (status === 200) {if(!response){if(!finalUrl) return log({msg: `请求错误`, color: 'error'})done(finalUrl)}else{done(blobUrl)}}elseif(retry++ < this.options.retry_max){// console.log('下载失败,重试中...', urls)setTimeout(() => httpRequest(), 500)}else{log({msg: `重试下载错误`, color: 'error'})}},onerror: err => console.error({msg: '获取链接失败', err}) & done(url)})}*/})}if(!urls){let getVideoURL = this.DETAIL[rule_index]?.getVideoURL || this.DETAIL.getVideoURLif(!getVideoURL) return log({msg: `无下载地址`, color: 'error'})getVideoURL(item).then(urls => {if(item.renamed){ // 获取详细信息后再改变名称delete item.renamedthis.applyRename(item.id)}httpRequest(Object.assign(item, {urls}).urls)})}else{httpRequest(urls)}return true}}},nextDownload(){ // 进行下一次下载if(!this.running) returnlet {resources} = thisif(!resources.some(item => this.downloadItem(item))){if(this.running){clearInterval(this.checkFinishTimer)this.checkFinishTimer = setInterval(() => {if(this.getItems(WAITTING).length == 0 && this.getItems(DOWNLOADING).length == 0){clearInterval(this.checkFinishTimer)this.switchRunning(false)let msg = '所有任务下载完成!'this.writeLog(msg) & (this.options.alert_done && alert(msg))}}, 1000)}}},findElement: id => $(`tr[data-id="${id}"]`)[0],  // 根据Id查找domwriteLog(msg, prefix = '提示', color = 'info'){ // 输出日志let div = $('#_log')[0]div.insertAdjacentHTML('beforeEnd', createHTML(`<p style="color: ${this.getColor(color)}">【${prefix}】 ${msg}</p>`))if(this.options.autoScroll) div.scrollTop = div.scrollHeight},getColor: color => ({success: '#8bc34a', error: '#a31545', info: '#fff', primary: '#3fa9fa' })[color] || color,setItemStatus({id, color, msg, el, item, status}){item ??= this.findItem(id)if(!item) returnif(status !== undefined) item.status = statusif(el === false) returnel ??= this.findElement(id)let td = el.querySelectorAll('td')if(td[4]){td[4].style.backgroundColor = this.getColor(color)td[4].innerHTML = createHTML(msg)}},findItem(id, method = 'find'){ // 根据Item查找资源信息return this.resources[method](_item => _item.id == id)},safeFileName: str => str.replaceAll('\n', ' ').replaceAll('(', '(').replaceAll(')', ')').replaceAll(':', ':').replaceAll('*', '*').replaceAll('?', '?').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>').replaceAll("|", "|").replaceAll('\\', '\').replaceAll('/', '/')}_downloader.init()function Base64() {// private property_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";// public method for encodingthis.encode = function (input) {var output = "";var chr1, chr2, chr3, enc1, enc2, enc3, enc4;var i = 0;input = _utf8_encode(input);while (i < input.length) {chr1 = input.charCodeAt(i++);chr2 = input.charCodeAt(i++);chr3 = input.charCodeAt(i++);enc1 = chr1 >> 2;enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);enc4 = chr3 & 63;if (isNaN(chr2)) {enc3 = enc4 = 64;} else if (isNaN(chr3)) {enc4 = 64;}output = output +_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +_keyStr.charAt(enc3) + _keyStr.charAt(enc4);}return output;}// public method for decodingthis.decode = function (input) {var output = "";var chr1, chr2, chr3;var enc1, enc2, enc3, enc4;var i = 0;input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");while (i < input.length) {enc1 = _keyStr.indexOf(input.charAt(i++));enc2 = _keyStr.indexOf(input.charAt(i++));enc3 = _keyStr.indexOf(input.charAt(i++));enc4 = _keyStr.indexOf(input.charAt(i++));chr1 = (enc1 << 2) | (enc2 >> 4);chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);chr3 = ((enc3 & 3) << 6) | enc4;output = output + String.fromCharCode(chr1);if (enc3 != 64) {output = output + String.fromCharCode(chr2);}if (enc4 != 64) {output = output + String.fromCharCode(chr3);}}output = _utf8_decode(output);return output;}// private method for UTF-8 encoding_utf8_encode = function (string) {string = string.replace(/\r\n/g,"\n");var utftext = "";for (var n = 0; n < string.length; n++) {var c = string.charCodeAt(n);if (c < 128) {utftext += String.fromCharCode(c);} else if((c > 127) && (c < 2048)) {utftext += String.fromCharCode((c >> 6) | 192);utftext += String.fromCharCode((c & 63) | 128);} else {utftext += String.fromCharCode((c >> 12) | 224);utftext += String.fromCharCode(((c >> 6) & 63) | 128);utftext += String.fromCharCode((c & 63) | 128);}}return utftext;}// private method for UTF-8 decoding_utf8_decode = function (utftext) {var string = "";var i = 0;var c = c1 = c2 = 0;while ( i < utftext.length ) {c = utftext.charCodeAt(i);if (c < 128) {string += String.fromCharCode(c);i++;} else if((c > 191) && (c < 224)) {c2 = utftext.charCodeAt(i+1);string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));i += 2;} else {c2 = utftext.charCodeAt(i+1);c3 = utftext.charCodeAt(i+2);string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));i += 3;}}return string;}}