Local YouTube Downloader - User
Local YouTube Downloader - User
Local YouTube Downloader - User
;(function () {
'use strict'
const DEBUG = true
const RESTORE_ORIGINAL_TITLE_FOR_CURRENT_VIDEO = true
const createLogger = (console, tag) =>
Object.keys(console)
.map(k => [k, (...args) => (DEBUG ? console[k](tag + ': ' +
args[0], ...args.slice(1)) : void 0)])
.reduce((acc, [k, fn]) => ((acc[k] = fn), acc), {})
const logger = createLogger(console, 'YTDL')
const sleep = ms => new Promise(res => setTimeout(res, ms))
let adaptive = []
if (playerResponse.streamingData.adaptiveFormats) {
adaptive = playerResponse.streamingData.adaptiveFormats.map(x =>
Object.assign({}, x, parseQuery(x.cipher ||
x.signatureCipher))
)
logger.log(`video %s adaptive: %o`, id, adaptive)
if (adaptive[0].sp && adaptive[0].sp.includes('sig')) {
for (const obj of adaptive) {
obj.s = decsig(obj.s)
obj.url += `&sig=${obj.s}`
}
}
}
logger.log(`video %s result: %o`, id, { stream, adaptive })
return { stream, adaptive, meta: obj, playerResponse }
}
const workerMessageHandler = async e => {
const decsig = await xf.get(e.data.path).text(parseDecsig)
try {
const result = await getVideo(e.data.id, decsig)
self.postMessage(result)
} catch (e) {
self.postMessage(e)
}
}
const ytdlWorkerCode = `
importScripts('https://unpkg.com/xfetch-js@0.3.4/xfetch.min.js')
const DEBUG=${DEBUG}
const logger=(${createLogger})(console, 'YTDL')
const escapeRegExp=${escapeRegExp}
const parseQuery=${parseQuery}
const parseDecsig=${parseDecsig}
const getVideo=${getVideo}
self.onmessage=${workerMessageHandler}`
const ytdlWorker = new Worker(URL.createObjectURL(new
Blob([ytdlWorkerCode])))
const workerGetVideo = (id, path) => {
logger.log(`workerGetVideo start: %s %s`, id, path)
return new Promise((res, rej) => {
const callback = e => {
ytdlWorker.removeEventListener('message', callback)
if (e.data === 'Adblock conflict') {
return rej(e.data)
}
logger.log('workerGetVideo end: %o', e.data)
res(e.data)
}
ytdlWorker.addEventListener('message', callback)
ytdlWorker.postMessage({ id, path })
})
}
const template = `
<div class="box" :class="{'dark':dark}">
<template v-if="!isLiveStream">
<div v-if="adaptive.length" class="of-h t-center c-pointer lh-20">
<a class="fs-14px" @click="dlmp4" v-text="strings.dlmp4"></a>
</div>
<div @click="hide=!hide" class="box-toggle div-a t-center fs-14px c-pointer lh-
20" v-text="strings.togglelinks"></div>
<div :class="{'hide':hide}">
<div class="t-center fs-14px" v-text="strings.videoid+id"></div>
<div class="d-flex">
<div class="f-1 of-h">
<div class="t-center fs-14px" v-text="strings.stream"></div>
<a class="ytdl-link-btn fs-14px" target="_blank" v-for="vid in stream"
:href="vid.url" :title="vid.type" v-text="formatStreamText(vid)"></a>
</div>
<div class="f-1 of-h">
<div class="t-center fs-14px" v-text="strings.adaptive"></div>
<a class="ytdl-link-btn fs-14px" target="_blank" v-for="vid in
adaptive" :href="vid.url" :title="vid.type" v-text="formatAdaptiveText(vid)"></a>
</div>
</div>
<div class="of-h t-center">
<a class="fs-14px" href="https://maple3142.github.io/mergemp4/"
target="_blank" v-text="strings.inbrowser_adaptive_merger"></a>
</div>
</div>
</template>
<template v-else>
<div class="t-center fs-14px lh-20" v-
text="strings.live_stream_disabled_message"></div>
</template>
</div>
`.slice(1)
const app = new Vue({
data() {
return {
hide: true,
id: '',
isLiveStream: false,
stream: [],
adaptive: [],
meta: null,
dark: false,
lang: findLang(navigator.language)
}
},
computed: {
strings() {
return LOCALE[this.lang.toLowerCase()]
}
},
methods: {
dlmp4() {
const r = JSON.parse(this.meta.player_response)
openDownloadModel(this.adaptive, r.videoDetails.title)
},
formatStreamText(vid) {
return [vid.qualityLabel, vid.quality].filter(x =>
x).join(': ')
},
formatAdaptiveText(vid) {
let str = [vid.qualityLabel, vid.mimeType].filter(x =>
x).join(': ')
if (vid.mimeType.includes('audio')) {
str += ` ${Math.round(vid.bitrate / 1000)}kbps`
}
return str
}
},
template
})
logger.log(`default language: %s`, app.lang)
// attach element
const shadowHost = $el('div')
const shadow = shadowHost.attachShadow ? shadowHost.attachShadow({ mode:
'closed' }) : shadowHost // no shadow dom
logger.log('shadowHost: %o', shadowHost)
const container = $el('div')
shadow.appendChild(container)
app.$mount(container)
const css = `
.hide{
display: none;
}
.t-center{
text-align: center;
}
.d-flex{
display: flex;
}
.f-1{
flex: 1;
}
.fs-14px{
font-size: 14px;
}
.of-h{
overflow: hidden;
}
.box{
padding-top: .5em;
padding-bottom: .5em;
border-bottom: 1px solid var(--yt-border-color);
font-family: Arial;
}
.box-toggle{
margin: 3px;
user-select: none;
-moz-user-select: -moz-none;
}
.ytdl-link-btn{
display: block;
border: 1px solid !important;
border-radius: 3px;
text-decoration: none !important;
outline: 0;
text-align: center;
padding: 2px;
margin: 5px;
color: black;
}
a, .div-a{
text-decoration: none;
color: var(--yt-button-color, inherit);
}
a:hover, .div-a:hover{
color: var(--yt-spec-call-to-action, blue);
}
.box.dark{
color: var(--ytd-video-primary-info-renderer-title-color, var(--yt-primary-
text-color));
}
.box.dark .ytdl-link-btn{
color: var(--ytd-video-primary-info-renderer-title-color, var(--yt-primary-
text-color));
}
.box.dark .ytdl-link-btn:hover{
color: rgba(200, 200, 255, 0.8);
}
.box.dark .box-toggle:hover{
color: rgba(200, 200, 255, 0.8);
}
.c-pointer{
cursor: pointer;
}
.lh-20{
line-height: 20px;
}
`
shadow.appendChild($el('style', { textContent: css }))
})()