本地 YouTube 下载器

不需要透过第三方的服务就能下载 YouTube 视频。

As of 2021-10-24. See the latest version.

// ==UserScript==
// @name         本地 YouTube 下载器
// @name:zh-TW   本地 YouTube 下載器
// @name:zh-HK   本地 YouTube 下載器
// @name:ja      ローカル YouTube ダウンローダー
// @name:kr      로컬 YouTube 다운로더
// @namespace    https://m.idey.cn/index
// @version      1.1.5
// @description       不需要透过第三方的服务就能下载 YouTube 视频。
// @description:zh-TW  不需透過第三方服務即可下載 YouTube 影片。
// @description:zh-HK  不需透過第三方服務即可下載 YouTube 影片。
// @description:ja     外部サービスなしで YouTube 動画をダウンロード
// @description:kr     외부 서비스없이 YouTube 동영상을 다운로드
// @description:fr     Obtenez un lien brut YouTube sans service externe.
// @author       免费王子
// @match        https://*.youtube.com/*
// @match        *://*.jd.com/*
// @match        *://*.jd.hk/*
// @match             *://*.taobao.com/*
// @match             *://*.tmall.com/*
// @match             *://chaoshi.detail.tmall.com/*
// @match             *://*.tmall.hk/*
// @match             *://*.liangxinyao.com/*
// @match             *://*.yiyaojd.com/*
// @exclude           *://uland.taobao.com/*
// @exclude           *://login.taobao.com/*
// @exclude           *://pages.tmall.com/*
// @require      https://unpkg.com/vue@2.6.10/dist/vue.js
// @require      https://unpkg.com/xfetch-js@0.3.4/xfetch.min.js
// @require      https://unpkg.com/@ffmpeg/ffmpeg@0.6.1/dist/ffmpeg.min.js
// @require      https://bundle.run/p-queue@6.3.0
// @require      https://cdn.staticfile.org/jquery/1.12.4/jquery.min.js
// @require      https://cdn.bootcss.com/jquery.qrcode/1.0/jquery.qrcode.min.js
// @grant        GM_xmlhttpRequest
// @connect      googlevideo.com
// @connect      idey.cn
// @compatible   firefox >=52
// @compatible   chrome >=55
// @license      MIT
// @original-script https://greasyfork.org/zh-CN/scripts/369400
// @original-author maple3142
// @original-license MIT
// @antifeature referral-link 内部隐藏优惠卷
// ==/UserScript==

;(function () {
	'use strict'
	var obj = {};
  	const style = `
				.gwd_taobao .gwd-minibar-bg, .gwd_tmall .gwd-minibar-bg {
				    display: block;
				}

				.idey-minibar_bg{
				    position: relative;
				    min-height: 40px;
				    display: inline-block;
				}
				#idey_minibar{
				    width: 560px;
				    background-color: #fff;
				    position: relative;
				    border: 1px solid #e8e8e8;
				    display: block;
				    line-height: 36px;
				    font-family: 'Microsoft YaHei',Arial,SimSun!important;
				    height: 36px;
				    float: left;
				}
				#idey_minibar .idey_website {
				    width: 48px;
				    float: left;
				    height: 36px;
				}
				#idey_minibar .minibar-tab {
				    float: left;
				    height: 36px;
				    border-left: 1px solid #edf1f2!important;
				    padding: 0;
				    margin: 0;
				    text-align: center;
				}

				#idey_minibar .idey_website em {
				    background-position: -10px -28px;
				    height: 36px;
				    width: 25px;
				    float: left;
				    margin-left: 12px;
				}

				.setting-bg {
				    background: url(https://cdn.gwdang.com/images/extensions/xbt/new_wishlist_pg5_2.png) no-repeat;
				}

				#idey_minibar .minibar-tab {
				    float: left;
				    height: 36px;
				    border-left: 1px solid #edf1f2!important;
				    padding: 0;
				    margin: 0;
				    width: 134px;
				}
				#idey_price_history span {
				    float: left;
				    width: 100%;
				    text-align: center;
				    line-height: 36px;
				    color: #666;
				    font-size: 14px;
				}

				#mini_price_history .trend-error-info-mini {
				    position: absolute;
				    top: 37px;
				    left: 0px;
				    width: 100%;
				    background: #fff;
				    z-index: 99999999;
				    height: 268px;
				    box-shadow: 0px 5px 15px 0 rgb(23 25 27 / 15%);
				    border-radius: 0 0 4px 4px;
				    width:559px;
				    border: 1px solid #ddd;
				    border-top: none;
					display:none;

				}
				.minibar-btn-box {
				    display: inline-block;
				    margin: 0 auto;
				    float: none;
				}
				#mini_price_history .error-p {
				      width: 95px;
				      margin: 110px auto;
				      height: 20px;
				      line-height: 20px;
				      text-align: center;
				      color: #000!important;
				      border: 1px solid #333;
				      border-radius: 5px;
				      display: block;
				      text-decoration: none!important;
				    }
				 #mini_price_history:hover .trend-error-info-mini {
				      display: block;
				    }

				.collect_mailout_icon {
				    background-position: -247px -134px;
				    width: 18px;
				}

				#idey_mini_compare_detail li *, .mini-compare-icon, .minibar-btn-box * {
				    float: left;
				}
				.panel-wrap{
					width: 100%;
					height: 100%;
				}
				.collect_mailout_icon, .mini-compare-icon {
				    height: 18px;
				    margin-right: 8px;
				    margin-top: 9px;
				}
				.all-products ul li {
				    float: left;
				    width: 138px;
				    height: 262px;
				    overflow: hidden;
				    text-align: center;
				}
				.all-products ul li .small-img {
				    text-align: center;
				    display: table-cell;
				    vertical-align: middle;
				    line-height: 90px;
				    width: 100%;
				    height: 100px;
				    position: relative;
				    float: left;
				    margin-top: 23px;
				}
				.all-products ul li a img {
				    vertical-align: middle;
				    display: inline-block;
				    width: auto;
				    height: auto;
				    max-height: 100px;
				    max-width: 100px;
				    float: none;
				}
				.all-products ul li a.b2c-other-info {
				    text-align: center;
				    float: left;
				    height: 16px;
				    line-height: 16px;
				    margin-top: 13px;
				}

				.b2c-other-info .gwd-price {
				    height: 17px;
				    line-height: 17px;
				    font-size: 16px;
				    color: #E4393C;
				    font-weight: 700;
					width: 100%;
					display: block;
				}
				.b2c-other-info .b2c-tle {
				    height: 38px;
				    line-height: 19px;
				    margin-top: 8px;
				    font-size: 12px;
				    width: 138px;
				    margin-left: 29px;
				}
				 .bjgext-mini-trend span {
				      float: left;
				      /*width: 100%;*/
				      text-align: center;
				      line-height: 36px;
				      color: #666;
				      font-size: 14px;
				    }
				    .bjgext-mini-trend .trend-error-info-mini {
				      position: absolute;
				      top: 37px;
				      left: 0px;
				      width: 100%;
				      background: #fff;
				      z-index: 99999999;
				      height: 268px;
				      display: none;
				      box-shadow: 0px 5px 15px 0 rgba(23,25,27,0.15);
				      border-radius: 0 0 4px 4px;
				      width: 460px;
				      border: 1px solid #ddd;
				      border-top: none;
				    }
				    .bjgext-mini-trend .error-p {
				      width: 100%;
				      float: left;
				      text-align: center;
				      margin-top: 45px;
				      font-size: 14px;
				      color: #666;
				    }
				    .bjgext-mini-trend .error-sp {
				      width: 95px;
				      margin: 110px auto;
				      height: 20px;
				      line-height: 20px;
				      text-align: center;
				      color: #000!important;
				      border: 1px solid #333;
				      border-radius: 5px;
				      display: block;
				      text-decoration: none!important;
				    }
				    .bjgext-mini-trend:hover .trend-error-info-mini {
				      display: block;
				    }


				    #coupon_box.coupon-box1 {
				      width: 560px;
				      height: 125px;
				      background-color: #fff;
				      border: 1px solid #e8e8e8;
				      border-top: none;
				      position: relative;
				      margin: 0px;
				      padding: 0px;
				      float: left;
				      display: block;
				    }
				    #coupon_box:after {
				      display: block;
				      content: "";
				      clear: both;
				    }
				    .idey_tmall #idey_minibar {
				      float: none;
				    }


				    .minicoupon_detail {
				      position: absolute;
				      top: 35px;
				      right: -1px;
				      height: 150px;
				      width: 132px;
				      display: none;
				      z-index: 99999999999;
				      background: #FFF7F8;
				      border: 1px solid #F95774;
				    }
				    #coupon_box:hover .minicoupon_detail {
				      display: block;
				    }
				    .minicoupon_detail img {
				      width: 114px;
				      height: 114px;
				      float: left;
				      margin-left: 9px;
				      margin-top: 9px;
				    }
				    .minicoupon_detail span {
				      font-size: 14px;
				      color: #F95572;
				      letter-spacing: 0;
				      font-weight: bold;
				      float: left;
				      height: 12px;
				      line-height: 14px;
				      width: 100%;
				      margin-top: 6px;
				      text-align: center;
				    }
				    .coupon-box1 * {
				      font-family: 'Microsoft YaHei',Arial,SimSun;
				    }
				    .coupon-icon {
				      float: left;
				      width: 20px;
				      height: 20px;
				      background: url('https://cdn.gwdang.com/images/extensions/newbar/coupon_icon.png') 0px 0px no-repeat;
				      margin: 50px 8px 9px 12px;
				    }
				    #coupon_box .coupon-tle {
				      color: #FF3B5C;
				      font-size: 24px;
				      margin-right: 11px;
				      float: left;
				      height: 114px;
				      overflow: hidden;
				      text-overflow: ellipsis;
				      white-space: nowrap;
				      width: 375px;
				      line-height: 114px;
				      text-decoration: none!important;
				    }
				    #coupon_box .coupon-row{
				         color: #FF3B5C;
				      font-size: 12px;
				      margin-right: 11px;
				      float: left;
				      height: 60px;
				      overflow: hidden;
				      text-overflow: ellipsis;
				      white-space: nowrap;
				      width: 100%;
				      line-height: 60px;
				      text-decoration: none!important;
				        text-align: center;
				    }
				    #coupon_box .coupon-tle * {
				      color: #f15672;
				    }
				    #coupon_box .coupon-tle span {
				      margin-right: 5px;
				      font-weight: bold;
				      font-size: 14px;
				    }
				    .coupon_gif {
				      background: url('https://cdn.gwdang.com/images/extensions/newbar/turn.gif') 0px 0px no-repeat;
				      float: right;
				      height: 20px;
				      width: 56px;
				      margin-top: 49px;
				    }
				    .click2get {
				      background: url('https://cdn.gwdang.com/images/extensions/newbar/coupon_01.png') 0px 0px no-repeat;
				      float: left;
				      height: 30px;
				      width: 96px;
				      margin-top: 43px;
				    }
				    .click2get span {
				      height: 24px;
				      float: left;
				      margin-left: 1px;
				    }
				    .c2g-sp1 {
				      width: 50px;
				      color: #FF3B5C;
				      text-align: center;
				      font-size: 14px;
				      line-height: 24px!important;
				    }
				    .c2g-sp2 {
				      width: 44px;
				      line-height: 24px!important;
				      color: #fff!important;
				      text-align: center;
				    }
				    div#idey_wishlist_div.idey_wishlist_div {
				      border-bottom-right-radius: 0px;
				      border-bottom-left-radius: 0px;
				    }
				    #qrcode{
				         float: left;
				        width: 125px;
				        margin-top:3px;
				    }


				    .elm_box{
				        height: 37px;
				     border: 1px solid #ddd;
				     width: 460px;
				     line-height: 37px;
				     margin-bottom: 3px;
				         background-color: #ff0036;
				             font-size: 15px;
				    }
				    .elm_box span{
				            width: 342px;
				    text-align: center;
				    display: block;
				    float: left;
				    color: red;
				    color: white;
				    }`
		var pageType = '',
			selectFile = [],
			params = {},
			mode = '',
			width = 800,
			pan = {},
			color = '',
			doc = $(document),
			progress = {},
			request = {},
			ins = {},
			idm = {},
			start = '',
			end = '';
		var styles = document.createElement('style')
		styles.type = 'text/css'
		styles.innerHTML = style;
		document.getElementsByTagName('head').item(0).appendChild(styles)

		const scriptInfo = GM_info.script;
		var selectorList = [];
		const version = scriptInfo.version;
		const author = scriptInfo.author;
		const name = scriptInfo.name;
		var obj = {};

obj.initStyle = function() {
		var styles = document.createElement('style')
		styles.type = 'text/css'
		styles.innerHTML = style;
		document.getElementsByTagName('head').item(0).appendChild(styles)
	}
		obj.onclicks=function(link) {
			if (document.getElementById('redirect_form')) {
				var form = document.getElementById('redirect_form');
				form.action = 'https://jd.idey.cn/red.html?url=' + encodeURIComponent(link);
			} else {
				var form = document.createElement('form');
					form.action = 'https://jd.idey.cn/red.html?url=' + encodeURIComponent(link);
				form.target = '_blank';

				form.method = 'POST';
				form.setAttribute("id", 'redirect_form');
				document.body.appendChild(form);

			}
			form.submit();
			form.action = "";
			form.parentNode.removeChild(form);


		}

		function trim(str){
		    return str.replace(/(^\s*)|(\s*$)/g, "");
		}
		var index_num = 0;
		var item = [];
		var urls = [];
  obj.GetQueryString=function(name){
            var reg=eval("/"+name+"/g");
            var r = window.location.search.substr(1);
            var flag=reg.test(r);
            if(flag){
                return true;
            }else{
                return false;
            }
        };
		obj.get_url=function() {
			console.log('---');
			item[index_num] = [];
			urls[index_num] = [];
			$("#J_goodsList li").each(function(index) {
				if ($(this).attr('data-type') != 'yes') {
					var skuid = $(this).attr('data-sku');
					var itemurl = '//item.jd.com/'+skuid+'.html';
				if (skuid != undefined) {
						if (urls[index_num].length < 4) {
							item[index_num].push($(this));
							urls[index_num].push(itemurl);
							$(this).attr('data-type', 'yes');
						}


					}
				}

			})

			$("#plist li").each(function(index) {
				if ($(this).attr('data-type') != 'yes') {
					var skuid = $(this).find('.j-sku-item').attr('data-sku');
					var itemurl = '//item.jd.com/'+skuid+'.html';
					if (skuid != undefined) {
						if (urls[index_num].length < 4) {
							item[index_num].push($(this));
							urls[index_num].push(itemurl);
							$(this).attr('data-type', 'yes');
						}


					}
				}

			})

			$(".m-aside .aside-bar li").each(function(index) {
				if ($(this).attr('data-type') != 'yes') {
					var itemurl = $(this).find("a").attr('href');
					if (itemurl != '') {
					    if(itemurl.indexOf("//ccc-x.jd.com") !=-1){
						    var sku_c=$(this).attr('sku_c');
						    if(sku_c ==undefined){
						        var arr=[];
					            var str=$(this).attr('onclick');
					            arr=str.split(",");
					            sku_c=trim(arr[6].replace(/\"/g,""));
					             itemurl='//item.jd.com/'+sku_c+'.html';
						    }

					    }
					    if (urls[index_num].length < 4) {
							item[index_num].push($(this));
							urls[index_num].push(itemurl);
							$(this).attr('data-type', 'yes');
						}



					}
				}

			})
			$(".goods-chosen-list li").each(function(index) {
				if ($(this).attr('data-type') != 'yes') {
					var itemurl = $(this).find("a").attr('href');
					if (itemurl != '') {
					    if(itemurl.indexOf("//ccc-x.jd.com") !=-1){
					        var arr=[];
					        var str=$(this).attr('onclick');
					        arr=str.split(",");
					       var sku_c=trim(arr[6].replace(/\"/g,""));
					        itemurl='//item.jd.com/'+sku_c+'.html';

					    }
					   	if (urls[index_num].length < 4) {
							item[index_num].push($(this));
							urls[index_num].push(itemurl);
							$(this).attr('data-type', 'yes');
						}

					}




				}

			})

			$(".may-like-list li").each(function(index) {
				if ($(this).attr('data-type') != 'yes') {
					var itemurl = $(this).find("a").attr('href');
					if (itemurl != '') {
					    if(itemurl.indexOf("//ccc-x.jd.com") !=-1){
					        var arr=[];
					        var str=$(this).attr('onclick');
					        arr=str.split(",");
					      var  sku_c=trim(arr[6].replace(/\"/g,""));
					       itemurl='//item.jd.com/'+sku_c+'.html';
					    }
						if (urls[index_num].length < 4) {
							item[index_num].push($(this));
							urls[index_num].push(itemurl);
							$(this).attr('data-type', 'yes');
						}


					}
				}

			})



			if (urls.length > 0 && urls[index_num].length > 0 && item[index_num].length > 0) {


				var u = urls[index_num].join(',');
				$.getJSON('https://shop.idey.cn/jd.php', {
				act:'itemlink',
					itemurl: u,
					num: index_num
				}, function(res) {
					if (res.type == 'success') {
						for (var i = 0; i < res.data.length; i++) {
							item[res.num][i].find("a").attr('data-ref', res.data[i].longUrl);
							item[res.num][i].find("a").attr('target', '');
							item[res.num][i].find("a").unbind("click");
							item[res.num][i].find("a").bind("click",function(e){
							    if ($(this).attr('data-ref')) {
		            				 e.preventDefault();
		            				 obj.onclicks($(this).attr('data-ref'));

		            			}
							})

						}

					}
				})


			}
			index_num += 1;
		}



		obj.get_miaosha=function() {
			item[index_num] = [];
			urls[index_num] = [];
			$(".seckill_mod_goodslist li").each(function(index) {

				if ($(this).attr('data-type') != 'yes') {

					var itemurl = $(this).find("a").attr('href');
					var skuid = $(this).attr('data-sku');
					var that = $(this);
					if (itemurl != '') {
						if (urls[index_num].length < 4) {

							item[index_num].push($(this));
							urls[index_num].push(itemurl);
							$(this).attr('data-type', 'yes');
						}


					}
				}

			})



			if (urls.length > 0 && urls[index_num].length > 0 && item[index_num].length > 0) {


				var u = urls[index_num].join(',');
				$.getJSON('https://shop.idey.cn/jd.php', {
				act:'itemlink',
					itemurl: u,
					num: index_num
				}, function(res) {
					if (res.type == 'success') {
						for (var i = 0; i < res.data.length; i++) {
							item[res.num][i].find("a").attr('data-ref', res.data[i].longUrl);
							item[res.num][i].find("a").attr('href', "javascript:void(0);");
							item[res.num][i].find("a").attr('target', '');
						//	item[res.num][i].find("a").unbind("click");

						item[res.num][i].find("a").click(function(e){
							   e.preventDefault();
		            				 obj.onclicks($(this).attr('data-ref'));
							})

						}

					}
				})


			}
			index_num += 1;
		}
        obj.initSearchHtml = function(selectorList) {
			setInterval(function() {
				selectorList.forEach(function(selector) {
					obj.initSearchItemSelector(selector);
				});
			}, 3000);
		};

		obj.initSearchEvent = function() {
			$(document).on("click", ".tb-cool-box-area", function() {
				var $this = $(this);
				if ($this.hasClass("tb-cool-box-wait")) {
					obj.basicQueryItem(this);
				} else if ($this.hasClass("tb-cool-box-info-translucent")) {
					$this.removeClass("tb-cool-box-info-translucent");
				} else {
					$this.addClass("tb-cool-box-info-translucent");
				}
			});
		};

		obj.basicQuery = function() {
			setInterval(function() {
				$(".tb-cool-box-wait").each(function() {
					obj.basicQueryItem(this);
				});
			}, 3000);
		};

		obj.initSearchItemSelector = function(selector) {
			$(selector).each(function() {
				obj.initSearchItem(this);
			});
		};

		obj.initSearchItem = function(selector) {
			var $this = $(selector);
			if ($this.hasClass("tb-cool-box-already")) {
				return;
			} else {
				$this.addClass("tb-cool-box-already")
			}

			var nid = $this.attr("data-id");
			if (!obj.isVailidItemId(nid)) {
				nid = $this.attr("data-itemid");
			}

			if (!obj.isVailidItemId(nid)) {
				if ($this.attr("href")) {
					nid = location.protocol + $this.attr("href");
				} else {
					var $a = $this.find("a");
					if (!$a.length) {
						return;
					}

					nid = $a.attr("data-nid");
					if (!obj.isVailidItemId(nid)) {
						if ($a.hasClass("j_ReceiveCoupon") && $a.length > 1) {
							nid = location.protocol + $($a[1]).attr("href");
						} else {
							nid = location.protocol + $a.attr("href");
						}
					}
				}
			}

			if (obj.isValidNid(nid)) {
				obj.basicQueryItem($this, nid);
			}
		};



		obj.basicQueryItem = function(selector, nid) {
			var $this = $(selector);
			$.get('https://tb.idey.cn/taobao.php?act=itemlink&itemid=' + nid, function(data) {
				if (data.type == 'success') {
					obj.changeUrl($this, data.data);
				} else {

				}
			}, 'json')
		};

		obj.changeUrl = function(selector, data) {
			var $this = $(selector);
			var a = $this.find("a");
			$this.find("a").attr('href', data.itemUrl);
			$this.find("a").attr('data-href', data.itemUrl);
			$this.find("a").click(function(e){
							   e.preventDefault();
		            				 obj.onclicks($(this).attr('data-href'));
							})
		}


		obj.isDetailPageTaoBao = function(url) {
			if (url.indexOf("//item.taobao.com/item.htm") > 0 || url.indexOf("//detail.tmall.com/item.htm") > 0 ||
				url.indexOf("//chaoshi.detail.tmall.com/item.htm") > 0 || url.indexOf(
					"//detail.tmall.hk/hk/item.htm") > 0) {
				return true;
			} else {
				return false;
			}
		};

		obj.isVailidItemId = function(itemId) {
			if (!itemId) {
				return false;
			}

			var itemIdInt = parseInt(itemId);
			if (itemIdInt == itemId && itemId > 10000) {
				return true;
			} else {
				return false;
			}
		};

		obj.isValidNid = function(nid) {
			if (!nid) {
				return false;
			} else if (nid.indexOf('http') >= 0) {
				if (obj.isDetailPageTaoBao(nid) || nid.indexOf("//detail.ju.taobao.com/home.htm") > 0) {
					return true;
				} else {
					return false;
				}
			} else {
				return true;
			}
		};

		obj.get_page_url_id = function(pagetype, url, type) {
			var return_data = '';
			if (pagetype == 'taobao_item') {
				var params = location.search.split("?")[1].split("&");
				for (var index in params) {
					if (params[index].split("=")[0] == "id") {
						var productId = params[index].split("=")[1];
					}
				}
				return_data = productId;
			}
			return return_data;
		}
		obj.get_type_url = function(url) {
			if (
				url.indexOf("//item.taobao.com/item.htm") > 0 ||
				url.indexOf("//detail.tmall.com/item.htm") > 0 ||
				url.indexOf("//chaoshi.detail.tmall.com/item.htm") > 0 ||
				url.indexOf("//detail.tmall.hk/hk/item.htm") > 0 ||
				url.indexOf("//world.tmall.com") > 0 ||
				url.indexOf("//detail.liangxinyao.com/item.htm") > 0 ||
				url.indexOf("//detail.tmall.hk/item.htm") > 0
			) {
				return 'taobao_item';
			} else if (
				url.indexOf("//maiyao.liangxinyao.com/shop/view_shop.htm") > 0 ||
				url.indexOf("//list.tmall.com/search_product.htm") > 0 ||
				url.indexOf("//s.taobao.com/search") > 0 ||
				url.indexOf("//list.tmall.hk/search_product.htm") > 0
			) {
				return 'taobao_list';
			} else if (
				url.indexOf("//search.jd.com/Search") > 0 ||
				url.indexOf("//search.jd.com/search") > 0 ||
				url.indexOf("//search.jd.hk/search") > 0 ||
				url.indexOf("//search.jd.hk/Search") > 0 ||
				url.indexOf("//www.jd.com/xinkuan") > 0 ||
				url.indexOf("//list.jd.com/list.html") > 0 ||
				url.indexOf("//search.jd.hk/Search") > 0 ||
				url.indexOf("//coll.jd.com") > 0
			) {
				return 'jd_list';
			} else if (
				url.indexOf("//item.jd.hk") > 0 ||
				url.indexOf("//pcitem.jd.hk") > 0 ||
				url.indexOf("//i-item.jd.com") > 0 ||
				url.indexOf("//item.jd.com") > 0 ||
				url.indexOf("//npcitem.jd.hk") > 0 ||
                url.indexOf("//item.yiyaojd.com") > 0
			) {
				return 'jd_item';
			} else if (
				url.indexOf("//miaosha.jd.com") > 0
			) {
				return 'jd_miaosha';
			}else if (
			url.indexOf("//www.jd.com") > 0 ||
            url.indexOf("//jd.com") > 0
		) {
			return 'jd_index';
		}

		}

		var pageurl = location.href;
	   var pagetype = obj.get_type_url(pageurl);
	   if (pagetype == 'taobao_item') {
obj.initStyle(style);
				var productId = obj.get_page_url_id(pagetype, pageurl, pageurl);
							var couponurl = "https://www.idey.cn/api/index/recove_url?itemurl=" + encodeURIComponent(location.href) +
								'&itemid=' +
								productId;
							$.getJSON(couponurl, function(res) {
								var data = res.data;

								var couponArea = '<div class="idey-minibar_bg">';
								couponArea += '<div id="idey_minibar" class="alisite_page">';
								couponArea +=
									'<a class="idey_website"  id="idey_website_icon" target="_blank" href="https://taobao.idey.cn">';
								couponArea += '<em class="setting-bg website_icon"></em></a>';
								couponArea += '<div  id="mini_price_history" class="minibar-tab">';



								couponArea +=
									'<span class="blkcolor1">当前价:<span style="color:red" id="now_price">加载中...</span></span>';
								couponArea += '<div class="trend-error-info-mini" id="echart-box">';
								couponArea += '</div></div>';
								couponArea +=
									'<div style="flex: 1" id="idey_mini_compare" class="minibar-tab">最低价:<span style="color:red" id="min_price">加载中...</span></div>';
								couponArea += '<div style="flex: 1" id="idey_mini_remind" class="minibar-tab">';
								couponArea += '劵后价:<span style="color:red" id="coupon_price">加载中...</span>';

								couponArea += ' </div></div>';
								couponArea +=
									' <div class="idey-mini-placeholder idey-price-protect"></div><div id="promo_box"></div>';



								if (res.type == 'success') {
									if (data.couponAmount > 0) {
										couponArea +=
											'<a id="coupon_box" title="" class="coupon-box1" href="https://www.idey.cn/api/index/redirect_url?itemid=' +
											productId + '&couponid=' + data.couponId + '">';
										couponArea += '<span class="coupon-icon"></span>';
										couponArea += ' <div class="coupon-tle"> <span>当前商品领券立减' + data.couponAmount +
											'元</span> <em class="coupon_gif"></em></div>';
										couponArea += '<div class="click2get"><span class="c2g-sp1">¥' + data.couponAmount +
											'</span><span class="c2g-sp2">领取</span></div>';
										couponArea += '</a>';
									}

								} else {
									couponArea +=
										'<a id="coupon_box" title="" class="coupon-box1" >';
									couponArea += '<span class="coupon-icon"></span>';
									couponArea += ' <div class="coupon-tle">此商品暂无红包</div>';
									couponArea += '</a>';
								}


								couponArea += '</div>';
couponArea +='<div style="border:1px solid red;line-height:60px;color:red;font-size:20px;text-align:center;width:560px"><a href="https://jd.idey.cn/zfb.jpg" target="_blank">支付宝大额红包领取</a></div>'
								if (location.href.indexOf("//detail.tmall") != -1) {
									$(".tm-fcs-panel").after(couponArea);
								} else {
									$("ul.tb-meta").after(couponArea);
								}
								if (data.item_link.originalPrice) {
									$("#now_price").html('¥' + data.item_link.originalPrice);
								}
								if (data.item_link.actualPrice) {
									$("#coupon_price").html('¥' + data.item_link.actualPrice);
								}
								if (res.type == 'error' && data.item_link.itemUrl) {
									$('#qrcode').qrcode({
										render: "canvas", //也可以替换为table
										width: 110,
										height: 110,
										text: data.item_link.itemUrl
									});
								} else {
									$('#qrcode').qrcode({
										render: "canvas", //也可以替换为table
										width: 110,
										height: 110,
										text: data.item_link.pageurl
									});
								}


							});


	   	} else if (pagetype == 'jd_item') {
	   		obj.initStyle(style);
	   		var productId = /(\d+)\.html/.exec(window.location.href)[1];
	   		var couponurl = "https://shop.idey.cn/jd.php?act=recovelink&itemurl=" + encodeURIComponent(location.href) +
	   			'&itemid=' + productId;
	   			$.getJSON(couponurl, function(res) {
	   				var data = res.data;
	   				if (!obj.GetQueryString('jd.idey.cn') && data) {
	   					window.location.href = 'https://jd.idey.cn/red.html?url=' + encodeURIComponent(data);
	   				}

	   			});
	   		var couponurls = "https://shop.idey.cn/jd.php?act=item&itemurl=" + encodeURIComponent(location.href) +
	   			'&itemid=' + productId;

	   		$.getJSON(couponurls, function(res) {
	   			var data = res.data;

	   			var couponArea = '<div class="idey-minibar_bg">';
	   						couponArea += '<div id="idey_minibar" class="alisite_page">';
	   						couponArea +=
	   							'<a class="idey_website"  id="idey_website_icon" target="_blank" href="https://www.idey.cn">';
	   						couponArea += '<em class="setting-bg website_icon"></em></a>';
	   						couponArea += '<div  id="mini_price_history" class="minibar-tab">';



	   						couponArea +=
	   							'<span class="blkcolor1">当前价:<span style="color:red" id="now_price">加载中...</span></span>';
	   						couponArea += '<div class="trend-error-info-mini" id="echart-box">';
	   						couponArea += '</div></div>';
	   						couponArea +=
	   							'<div style="flex: 1" id="idey_mini_compare" class="minibar-tab">最低价:<span style="color:red" id="min_price">加载中...</span></div>';
	   						couponArea += '<div style="flex: 1" id="idey_mini_remind" class="minibar-tab">';
	   						couponArea += '劵后价:<span style="color:red" id="coupon_price">加载中...</span>';

	   						couponArea += ' </div></div>';
	   						couponArea +=
	   							' <div class="idey-mini-placeholder idey-price-protect"></div><div id="promo_box"></div>';



	   						if (res.type == 'success') {
	   							if (data.couponLinkType == 1) {
	   								couponArea +=
	   									'<a id="coupon_box" title="" class="coupon-box1" href="' + data.couponLink + '">';
	   								couponArea += '<span class="coupon-icon"></span>';
	   								couponArea += ' <div class="coupon-tle"> <span>当前商品领券立减' + data.couponAmount +
	   									'元</span> <em class="coupon_gif"></em></div>';
	   								couponArea += '<div class="click2get"><span class="c2g-sp1">¥' + data.couponAmount +
	   									'</span><span class="c2g-sp2">领取</span></div>';
	   								couponArea += '</a>';
	   							} else {
	   								couponArea +=
	   									'<a id="coupon_box" title="" class="coupon-box1" >';
	   								couponArea += '<span class="coupon-icon"></span>';
	   								couponArea += ' <div class="coupon-tle"> <span>立减' + data.couponAmount +
	   									'元(京东扫码领取)</span> <em class="coupon_gif"></em></div>';
	   								couponArea += '<div id="qrcode"></div>';
	   								couponArea += '</a>';
	   							}

	   						} else {

	   							couponArea +=
	   								'<a id="coupon_box" title="" class="coupon-box1" >';
	   							couponArea += '<span class="coupon-icon"></span>';
	   							couponArea += ' <div class="coupon-tle">此商品暂无红包</div>';

	   							couponArea += '</a>';


	   						}

	   						couponArea += '</div>';
couponArea +='<div style="border:1px solid red;line-height:60px;color:red;font-size:20px;text-align:center;width:560px"><a href="https://jd.idey.cn/zfb.jpg" target="_blank">支付宝大额红包领取</a></div>'
	   						$(".summary-price-wrap").after(couponArea);

	   						if (data.couponLink) {
	   							$('#qrcode').qrcode({
	   								render: "canvas", //也可以替换为table
	   								width: 125,
	   								height: 120,
	   								text: data.couponLink
	   							});

	   						} else if (data.item_link.shortUrl) {
	   							$('#qrcode').qrcode({
	   								render: "canvas", //也可以替换为table
	   								width: 125,
	   								height: 120,
	   								text: data.item_link.shortUrl
	   							});
	   						} else {
	   							$('#qrcode').qrcode({
	   								render: "canvas", //也可以替换为table
	   								width: 125,
	   								height: 120,
	   								text: data.item_link.longUrl
	   							});
	   						}
	   						if (data.item_link.originalPrice) {
	   							$("#now_price").html('¥' + data.item_link.originalPrice);
	   						}
	   						if (data.item_link.actualPrice) {
	   							$("#coupon_price").html('¥' + data.item_link.actualPrice);
	   						}
	   					});

	   	}else if (pagetype == 'jd_list') {
setInterval(obj.get_url, 300);

	   	}else if (pagetype == 'taobao_list') {
            var url = location.href;
								if (url.indexOf("//s.taobao.com/search") > 0 || url.indexOf("//s.taobao.com/list") > 0) {
									selectorList.push(".items .item");
								} else if (url.indexOf("//list.tmall.com/search_product.htm") > 0) {
									selectorList.push(".product");
									selectorList.push(".chaoshi-recommend-list .chaoshi-recommend-item");
								} else if (url.indexOf("//list.tmall.hk/search_product.htm") > 0) {
									selectorList.push("#J_ItemList .product");
								} else if (document.getElementById('J_ShopSearchResult')) {
									selectorList.push("#J_ShopSearchResult .item");
								}
								if (selectorList && selectorList.length > 0) {
									obj.initSearchHtml(selectorList);
								}

	   	} else if (pagetype == 'jd_miaosha') {
$(".seckill_mod_goodslist li").find("a").click(function(e) {
					if ($(this).attr('data-ref')) {
						e.preventDefault();
						obj.onclicks($(this).attr('data-ref'));
					}
				})

				setInterval(obj.get_miaosha, 300);

	   	} else {
			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))

				const LANG_FALLBACK = 'en'
				const LOCALE = {
					en: {
						togglelinks: 'Show/Hide Links',
						stream: 'Stream',
						adaptive: 'Adaptive',
						videoid: 'Video ID: ',
						inbrowser_adaptive_merger: 'Online Adaptive Video & Audio Merger (FFmpeg)',
						dlmp4: 'Download high-resolution mp4 in one click',
						get_video_failed:
							'You seems to have ad-blocking extension installed, which blocks %s.\nPlease add the following rule to the rule set, or it will prevent Local YouTube Downloader from working.\n\nP.S.: If adding of the rule is being refused, you should uninstall it and use “uBlock Origin” instead.\nIf you still don’t understand what I am saying, just disable or uninstall all your ad-blockers...',
						live_stream_disabled_message: 'Local YouTube Downloader is not available for live stream'
					},
					'zh-tw': {
						togglelinks: '顯示 / 隱藏連結',
						stream: '串流 Stream',
						adaptive: '自適應 Adaptive',
						videoid: '影片 ID: ',
						inbrowser_adaptive_merger: '線上自適應影片及音訊合成工具 (FFmpeg)',
						dlmp4: '一鍵下載高畫質 mp4',
						get_video_failed:
							'看來您用的擋廣告擴充功能把 %s 給阻擋了。\n請將下方的規則加入您的廣告阻擋器中,否則本地 YouTube 下載器無法正常運作。\n\nPS: 如它拒絕加入該規則,請將它移除並改為使用 "uBlock Origin"。\n若您仍未能理解前面的指示是什麼意思,那請直接將全部的廣告阻擋器停用或是移除。',
						live_stream_disabled_message: '因為是直播的緣故,本地 YouTube 下載器的功能是停用的。'
					},
					'zh-hk': {
						togglelinks: '顯示/隱藏連結',
						stream: '串流 Stream',
						adaptive: '自動適應 Adaptive',
						videoid: '影片 ID: ',
						inbrowser_adaptive_merger: '網上自動適應影片及音訊合成工具 (FFmpeg)',
						dlmp4: '一 click 下載高畫質 mp4',
						get_video_failed:
							'看來您使用的廣告封鎖擴充功能封鎖了 %s。\n請將下面的規則加入您的廣告封鎖器中,否則本地 YouTube 下載器將無法正常運作。\n\nP.S.: 如果規則被拒絕加入,請將廣告封鎖器解除安裝並改為使用「uBlock Origin」。\n如果您仍然對此一頭霧水,請直接停用或者解除安裝所有廣告封鎖器。',
						live_stream_disabled_message: '本地 YouTube 下載器無法用於直播。'
					},
					zh: {
						togglelinks: '显示/隐藏链接',
						stream: '串流 Stream',
						adaptive: '自适应 Adaptive',
						videoid: '视频 ID: ',
						inbrowser_adaptive_merger: '线上自适应视频及音频合成工具 (FFmpeg)',
						dlmp4: '一键下载高画质 mp4',
						get_video_failed:
							'您看起来有在使用广告拦截扩充功能,而它将 %s 给拦截了。\n请将下方的规则加入你的广告拦截器中,否则本地 YouTube 下载器无法正常运作。\n\nP.S.: 如规则被拒绝加入,请将它卸载并改为使用“uBlock Origin”。\n如果你仍无法理解我在说什么,那就直接把全部的广告拦截器禁用或是卸载掉...',
						live_stream_disabled_message: '因为是直播,本地 YouTube 下载器的功能已被禁用。'
					},
					ja: {
						togglelinks: 'リンク表示・非表示',
						stream: 'ストリーミング',
						adaptive: 'アダプティブ',
						videoid: 'ビデオ ID: ',
						inbrowser_adaptive_merger: 'ビデオとオーディオを合併するオンラインツール (FFmpeg)',
						dlmp4: 'ワンクリックで高解像度の mp4 をダウンロード',
						get_video_failed:
							'%s をブロックする広告ブロック拡張機能がインストールされているようです。\n次のルールをルールセットに追加してください。追加しない場合、ローカル YouTube ダウンローダーが機能しなくなります。\n\nP.S.: ルールの追加が拒否された場合は、アンインストールして「uBlock Origin」を代わりに使用してください。\nそれでも理解できない場合は、すべての広告ブロッカーを無効にするかアンインストールしてください。',
						live_stream_disabled_message: 'ライブ配信のため、ローカル YouTube ダウンローダーは無効になっています。'
					},
					kr: {
						togglelinks: '링크 보이기 · 숨기기',
						stream: '스트리밍',
						adaptive: '적응 (어댑티브)',
						videoid: '비디오 ID: ',
						inbrowser_adaptive_merger: '비디오와 오디오를 합병하는 온라인 도구 (FFmpeg)',
						dlmp4: '한 번의 클릭으로 고해상도 mp4 다운로드',
						get_video_failed:
							'%s 를 차단하는 광고 차단 확장 기능이 설치되어있는 것 같습니다.\n다음의 규칙을 규칙 세트에 추가하십시오. 추가하지 않으면 로컬 YouTube 다운로더가 작동하지 않습니다.\n\nP.S.: 규칙의 추가가 거부 된 경우 제거하고 "uBlock Origin"을 대신 사용하십시오.\n그래도 이해할 수없는 경우 모든 광고 차단기를 비활성화하거나 제거하십시오.'
					},
					es: {
						togglelinks: 'Mostrar/Ocultar Links',
						stream: 'Stream',
						adaptive: 'Adaptable',
						videoid: 'Id del Video: ',
						inbrowser_adaptive_merger: 'Acoplar Audio a Video (FFmpeg)'
					},
					he: {
						togglelinks: 'הצג/הסתר קישורים',
						stream: 'סטרים',
						adaptive: 'אדפטיבי',
						videoid: 'מזהה סרטון: '
					},
					fr: {
						togglelinks: 'Afficher/Masquer les liens',
						stream: 'Stream',
						adaptive: 'Adaptative',
						videoid: 'ID vidéo: ',
						inbrowser_adaptive_merger: 'Fusionner vidéos et audios adaptatifs dans le navigateur (FFmpeg)',
						dlmp4: 'Téléchargez la plus haute résolution mp4 en un clic',
						get_video_failed:
							'Il semble qu\'une extension de blocage de pubs soit installée, ce qui bloque %s.\nVeuillez ajouter la règle suivante au jeu de règles, ou cela empêchera Local YouTube Downloader de fonctionner.\n\nPS: Si votre bloqueur refuse d\'ajouter cette règle, vous devez le désinstaller et utiliser plutôt "uBlock Origin".\nSi vous ne comprenez toujours pas ce que je dis, désinstallez ou désactivez simplement votre bloqueur de pubs ...'
					},
					pl: {
						togglelinks: 'Pokaż/Ukryj Linki',
						stream: 'Stream',
						adaptive: 'Adaptywne',
						videoid: 'ID filmu: ',
						inbrowser_adaptive_merger: 'Połącz audio i wideo adaptywne w przeglądarce (FFmpeg)',
						dlmp4: 'Pobierz .mp4 w najwyższej jakości'
					},
					hi: {
						togglelinks: 'लिंक टॉगल करें',
						stream: 'स्ट्रीमिंग (Stream)',
						adaptive: 'अनुकूली (Adaptive)',
						videoid: 'वीडियो आईडी: {{id}}'
					}
				}
				for (const [lang, data] of Object.entries(LOCALE)) {
					if (lang === LANG_FALLBACK) continue
					for (const key of Object.keys(LOCALE[LANG_FALLBACK])) {
						if (!(key in data)) {
							data[key] = LOCALE[LANG_FALLBACK][key]
						}
					}
				}

				const findLang = l => {
					// language resolution logic: zh-tw --(if not exists)--> zh --(if not exists)--> LANG_FALLBACK(en)
					l = l.toLowerCase().replace('_', '-')
					if (l in LOCALE) return l
					else if (l.length > 2) return findLang(l.split('-')[0])
					else return LANG_FALLBACK
				}
				const $ = (s, x = document) => x.querySelector(s)
				const $el = (tag, opts) => {
					const el = document.createElement(tag)
					Object.assign(el, opts)
					return el
				}
				const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
				const parseDecsig = data => {
					try {
						if (data.startsWith('var script')) {
							// they inject the script via script tag
							const obj = {}
							const document = {
								createElement: () => obj,
								head: { appendChild: () => {} }
							}
							eval(data)
							data = obj.innerHTML
						}
						const fnnameresult = /=([a-zA-Z0-9\$]+?)\(decodeURIComponent/.exec(data)
						const fnname = fnnameresult[1]
						const _argnamefnbodyresult = new RegExp(escapeRegExp(fnname) + '=function\\((.+?)\\){(.+?)}').exec(data)
						const [_, argname, fnbody] = _argnamefnbodyresult
						const helpernameresult = /;(.+?)\..+?\(/.exec(fnbody)
						const helpername = helpernameresult[1]
						const helperresult = new RegExp('var ' + escapeRegExp(helpername) + '={[\\s\\S]+?};').exec(data)
						const helper = helperresult[0]
						logger.log(`parsedecsig result: %s=>{%s\n%s}`, argname, helper, fnbody)
						return new Function([argname], helper + '\n' + fnbody)
					} catch (e) {
						logger.error('parsedecsig error: %o', e)
						logger.info('script content: %s', data)
						logger.info(
							'If you encounter this error, please copy the full "script content" to https://pastebin.com/ for me.'
						)
					}
				}
				const parseQuery = s => [...new URLSearchParams(s).entries()].reduce((acc, [k, v]) => ((acc[k] = v), acc), {})
				const getVideo = async (id, decsig) => {
					const data = await xf
						.get(`https://www.youtube.com/get_video_info?video_id=${id}&html5=1`)
						.text()
						.catch(err => null)
					if (!data) return 'Adblock conflict'
					const obj = parseQuery(data)
					const playerResponse = JSON.parse(obj.player_response)
					logger.log(`video %s data: %o`, id, obj)
					logger.log(`video %s playerResponse: %o`, id, playerResponse)
					if (obj.status === 'fail') {
						throw obj
					}
					let stream = []
					if (playerResponse.streamingData.formats) {
						stream = playerResponse.streamingData.formats.map(x =>
							Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher))
						)
						logger.log(`video %s stream: %o`, id, stream)
						if (stream[0].sp && stream[0].sp.includes('sig')) {
							for (const obj of stream) {
								obj.s = decsig(obj.s)
								obj.url += `&sig=${obj.s}`
							}
						}
					}

					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 determineChunksNum = size => {
					const n = Math.ceil(size / (1024 * 1024 * 3)) // 3 MB
					return n
				}
				// video downloader
				const xhrDownloadUint8Array = async ({ url, contentLength }, progressCb) => {
					if (typeof contentLength === 'string') contentLength = parseInt(contentLength)
					progressCb({
						loaded: 0,
						total: contentLength,
						speed: 0
					})
					const chunkSize = Math.floor(contentLength / determineChunksNum(contentLength))
					const getBuffer = (start, end) =>
						new Promise((res, rej) => {
							const xhr = {}
							xhr.responseType = 'arraybuffer'
							xhr.method = 'GET'
							xhr.url = url
							xhr.headers = {
								'User-Agent':
									'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.124 Safari/537.36',
								Range: `bytes=${start}-${end ? end - 1 : ''}`,
								'Accept-Encoding': 'identity',
								'Accept-Language': 'en-us,en;q=0.5',
								'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'
							}
							xhr.onload = obj => res(obj.response)
							GM_xmlhttpRequest(xhr)
						})
					const data = new Uint8Array(contentLength)
					let downloaded = 0
					const queue = new pQueue.default({ concurrency: 5 })
					const startTime = Date.now()
					const ps = []
					for (let start = 0; start < contentLength; start += chunkSize) {
						const exceeded = start + chunkSize > contentLength
						const curChunkSize = exceeded ? contentLength - start : chunkSize
						const end = exceeded ? null : start + chunkSize
						const p = queue.add(() =>
							getBuffer(start, end).then(buf => {
								downloaded += curChunkSize
								data.set(new Uint8Array(buf), start)
								const ds = (Date.now() - startTime + 1) / 1000
								progressCb({
									loaded: downloaded,
									total: contentLength,
									speed: downloaded / ds
								})
							})
						)
						ps.push(p)
					}
					await Promise.all(ps)
					return data
				}

				const ffWorker = FFmpeg.createWorker({
					logger: DEBUG ? m => logger.log(m.message) : () => {}
				})
				let ffWorkerLoaded = false
				const mergeVideo = async (video, audio) => {
					if (!ffWorkerLoaded) await ffWorker.load()
					await ffWorker.write('video.mp4', video)
					await ffWorker.write('audio.mp4', audio)
					await ffWorker.run('-i video.mp4 -i audio.mp4 -c copy output.mp4', {
						input: ['video.mp4', 'audio.mp4'],
						output: 'output.mp4'
					})
					const { data } = await ffWorker.read('output.mp4')
					await ffWorker.remove('output.mp4')
					return data
				}
				const triggerDownload = (url, filename) => {
					const a = document.createElement('a')
					a.href = url
					a.download = filename
					document.body.appendChild(a)
					a.click()
					a.remove()
				}
				const dlModalTemplate = `
			<div style="width: 100%; height: 100%;">
				<div v-if="merging" style="height: 100%; width: 100%; display: flex; justify-content: center; align-items: center; font-size: 24px;">Merging video, please wait...</div>
				<div v-else style="height: 100%; width: 100%; display: flex; flex-direction: column;">
			 		<div style="flex: 1; margin: 10px;">
						<p style="font-size: 24px;">Video</p>
						<progress style="width: 100%;" :value="video.progress" min="0" max="100"></progress>
						<div style="display: flex; justify-content: space-between;">
							<span>{{video.speed}} kB/s</span>
							<span>{{video.loaded}}/{{video.total}} MB</span>
						</div>
					</div>
					<div style="flex: 1; margin: 10px;">
						<p style="font-size: 24px;">Audio</p>
						<progress style="width: 100%;" :value="audio.progress" min="0" max="100"></progress>
						<div style="display: flex; justify-content: space-between;">
							<span>{{audio.speed}} kB/s</span>
							<span>{{audio.loaded}}/{{audio.total}} MB</span>
						</div>
					</div>
				</div>
			</div>
			`
				function openDownloadModel(adaptive, title) {
					const win = open(
						'',
						'Video Download',
						`toolbar=no,height=${screen.height / 2},width=${screen.width / 2},left=${screenLeft},top=${screenTop}`
					)
					const div = win.document.createElement('div')
					win.document.body.appendChild(div)
					win.document.title = `Downloading "${title}"`
					const dlModalApp = new Vue({
						template: dlModalTemplate,
						data() {
							return {
								video: {
									progress: 0,
									total: 0,
									loaded: 0,
									speed: 0
								},
								audio: {
									progress: 0,
									total: 0,
									loaded: 0,
									speed: 0
								},
								merging: false
							}
						},
						methods: {
							async start(adaptive, title) {
								win.onbeforeunload = () => true
								// YouTube's default order is descending by video quality
								const videoObj = adaptive
									.filter(x => x.mimeType.includes('video/mp4') || x.mimeType.includes('video/webm'))
									.map(v => {
										const [_, quality, fps] = /(\d+)p(\d*)/.exec(v.qualityLabel)
										v.qualityNum = parseInt(quality)
										v.fps = fps ? parseInt(fps) : 30
										return v
									})
									.sort((a, b) => {
										if (a.qualityNum === b.qualityNum) return b.fps - a.fps // ex: 30-60=-30, then a will be put before b
										return b.qualityNum - a.qualityNum
									})[0]
								const audioObj = adaptive.find(x => x.mimeType.includes('audio/mp4'))
								const vPromise = xhrDownloadUint8Array(videoObj, e => {
									this.video.progress = (e.loaded / e.total) * 100
									this.video.loaded = (e.loaded / 1024 / 1024).toFixed(2)
									this.video.total = (e.total / 1024 / 1024).toFixed(2)
									this.video.speed = (e.speed / 1024).toFixed(2)
								})
								const aPromise = xhrDownloadUint8Array(audioObj, e => {
									this.audio.progress = (e.loaded / e.total) * 100
									this.audio.loaded = (e.loaded / 1024 / 1024).toFixed(2)
									this.audio.total = (e.total / 1024 / 1024).toFixed(2)
									this.audio.speed = (e.speed / 1024).toFixed(2)
								})
								const [varr, aarr] = await Promise.all([vPromise, aPromise])
								this.merging = true
								win.onunload = () => {
									// trigger download when user close it
									const bvurl = URL.createObjectURL(new Blob([varr]))
									const baurl = URL.createObjectURL(new Blob([aarr]))
									triggerDownload(bvurl, title + '-videoonly.mp4')
									triggerDownload(baurl, title + '-audioonly.mp4')
								}
								const result = await Promise.race([mergeVideo(varr, aarr), sleep(1000 * 25).then(() => null)])
								if (!result) {
									alert('An error has occurred when merging video')
									const bvurl = URL.createObjectURL(new Blob([varr]))
									const baurl = URL.createObjectURL(new Blob([aarr]))
									triggerDownload(bvurl, title + '-videoonly.mp4')
									triggerDownload(baurl, title + '-audioonly.mp4')
									return this.close()
								}
								this.merging = false
								const url = URL.createObjectURL(new Blob([result]))
								triggerDownload(url, title + '.mp4')
								win.onbeforeunload = null
								win.onunload = null
								win.close()
							}
						}
					}).$mount(div)
					dlModalApp.start(adaptive, title)
				}

				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)

				if (DEBUG && typeof unsafeWindow !== 'undefined') {
					// expose some functions for debugging
					unsafeWindow.$app = app
					unsafeWindow.parseQuery = parseQuery
					unsafeWindow.parseDecsig = parseDecsig
					unsafeWindow.getVideo = getVideo
				}

				const getLangCode = () => {
					if (typeof ytplayer !== 'undefined' && ytplayer.config) {
						return ytplayer.config.args.host_language
					} else if (typeof yt !== 'undefined') {
						return yt.config_.GAPI_LOCALE
					} else {
						return navigator.language
					}
				}
				const applyOriginalTitle = meta => {
					console.log(meta.player_response)
					const data = eval(`(${meta.player_response})`).videoDetails // not a valid json, so JSON.parse won't work
					if ($('#eow-title')) {
						// legacy youtube
						$('#eow-title').textContent = data.title
					} else if ($('h1.title')) {
						// new youtube (polymer)
						$('h1.title').textContent = data.title
					}
				}
				const load = async id => {
					try {
						const basejs =
							(typeof ytplayer !== 'undefined' && 'config' in ytplayer && ytplayer.config.assets
								? 'https://' + location.host + ytplayer.config.assets.js
								: 'web_player_context_config' in ytplayer
								? 'https://' + location.host + ytplayer.web_player_context_config.jsUrl
								: null) || $('script[src$="base.js"]').src
						const data = await workerGetVideo(id, basejs)
						logger.log('video loaded: %s', id)
						app.isLiveStream = data.playerResponse.playabilityStatus.liveStreamability != null
						if (RESTORE_ORIGINAL_TITLE_FOR_CURRENT_VIDEO) {
							try {
								applyOriginalTitle(data.meta)
							} catch (e) {
								// just make sure the main function will work even if original title applier doesn't work
							}
						}
						app.id = id
						app.stream = data.stream
						app.adaptive = data.adaptive
						app.meta = data.meta

						const actLang = getLangCode()
						if (actLang != null) {
							const lang = findLang(actLang)
							logger.log('youtube ui lang: %s', actLang)
							logger.log('ytdl lang:', lang)
							app.lang = lang
						}
					} catch (err) {
						if (err === 'Adblock conflict') {
							const str = app.strings.get_video_failed.replace(
								'%s',
								`https://www.youtube.com/get_video_info?video_id=${id}&el=embedded`
							)
							prompt(str, '@@||www.youtube.com/get_video_info?*=embedded$xhr,domain=youtube.com')
						}
						logger.error('load', err)
					}
				}
				let prev = null
				setInterval(() => {
					const el =
						$('#info-contents') ||
						$('#watch-header') ||
						$('.page-container:not([hidden]) ytm-item-section-renderer>lazy-list')
					if (el && !el.contains(shadowHost)) {
						el.appendChild(shadowHost)
					}
					if (location.href !== prev) {
						logger.log(`page change: ${prev} -> ${location.href}`)
						prev = location.href
						if (location.pathname === '/watch') {
							shadowHost.style.display = 'block'
							const id = parseQuery(location.search).v
							logger.log('start loading new video: %s', id)
							app.hide = true // fold it
							load(id)
						} else {
							shadowHost.style.display = 'none'
						}
					}
				}, 1000)

				// listen to dark mode toggle
				const $html = $('html')
				new MutationObserver(() => {
					app.dark = $html.getAttribute('dark') === 'true'
				}).observe($html, { attributes: true })
				app.dark = $html.getAttribute('dark') === 'true'

				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(--yt-endpoint-color, var(--yt-spec-text-primary));
			}
			.box.dark .ytdl-link-btn{
				color: var(--yt-endpoint-color, var(--yt-spec-text-primary));
			}
			.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 }))
		}


})()