【GYAO!】動画ダウンロード-user.js

GYAO! Plus ~GYAO!の動画を保存するお手伝い~ TVer ・ FOD も対応

GYAO!の動画ダウンロードをサポートする、user.jsの配布ページです。GYAO!のサイトを使いやすくする機能も多数搭載しています。TVerとFODにも対応。

対応ブラウザ

  • Google Chrome

  • Microsoft Edge

  • Firefox

バージョンは、スクリプト更新時点のリリースバージョンです。これよりも古いブラウザについては、動作対象外となります。

必要な拡張機能・アドオン

Tampermonkey、またはViolentmonkeyが必要です。推奨はTampermonkeyです。

動作するページ

  • GYAO!の動画ページ

  • GYAO!のタイトル一覧ページ

  • GYAO!の新着ページ

  • TVerの動画ページ

  • FODの見逃し無料ページ

  • FODの動画ページ

機能

別のページで解説しています。

スクリプトの使い方の前に、動画の保存方法がわからないという場合はこちらをご覧ください。

スクリプト

このスクリプトは自動更新されません。更新するときは、直接コードを貼り付けてください。

// ==UserScript==
// @name         GYAO! Plus (Fork)
// @namespace    https://ssbsblg.blogspot.com/2020/10/gyao-download-userjs.html
// @version      2.3.1
// @description  GYAO!、Tver、FODの動画をダウンロードするコマンドを提供します。
// @author       Sasabee
// @match        *://gyao.yahoo.co.jp/titles*
// @match        *://gyao.yahoo.co.jp/arrivals*
// @match        *://gyao.yahoo.co.jp/p/*
// @match        *://gyao.yahoo.co.jp/title/*
// @match        *://gyao.yahoo.co.jp/episode/*
// @match        *://gyao.yahoo.co.jp/player/*
// @match        *://tver.jp/episode/*
// @match        *://tver.jp/corner/*
// @match        *://tver.jp/feature/*
// @match        *://fod.fujitv.co.jp/s/plus7/
// @match        *://fod.fujitv.co.jp/s/genre/*/ser*/*/
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_notification
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// @run-at       document-end
// @noframes
// ==/UserScript==

const init = {};
init.command_type = 'youtube-dl';
init.ytdl_command = `youtube-dl -w --no-mtime --hls-prefer-native --fragment-retries "10" --socket-timeout "60" --user-agent "#UA#" -o "#TITLE#.mp4" "#M3U8#"`;
init.ffmpeg_command = `ffmpeg -loglevel "error" -timeout "60000" -user_agent "#UA#" -i "#M3U8#" -codec "copy" "#TITLE#.mp4"`;
init.user_command = '-';
init.ytdlAll = true;
init.ytdlAll_commandB = `youtube-dl -w --no-mtime --hls-prefer-native --fragment-retries "10" --socket-timeout "60" --user-agent "#UA#" -f "best" -o "%%(title)s.%%(ext)s" "#URL#"`;
init.ytdlAll_commandW = `youtube-dl -w --no-mtime --hls-prefer-native --fragment-retries "10" --socket-timeout "60" --user-agent "#UA#" -f "worst" -o "%%(title)s.%%(ext)s" "#URL#"`;
init.sjif = false;
init.first_load_count = '15';
init.default_load_count = '15';
init.load_point = '10';
init.days_left_count = '-1';
init.days_left_strings = ' 🚩';
init.modal_height = '70%';
init.modal_width = '70%';
init.resize = false;
init.reverse = false;
init.commandStore = [];
init.m3u8Store = [];

((arr)=>{
	for(let i = 0, l = arr.length; i < l; i++) {
		(GM_getValue(arr[i][0]) === '' || GM_getValue(arr[i][0]) === undefined) && GM_setValue(arr[i][0], arr[i][1]);
	}
})(Object.entries(init));

const commandStore = (v)=>{
	GM_getValue('commandStore',[]) === undefined && GM_setValue('commandStore', []);
	const arr = [].concat(GM_getValue('commandStore',[]));
	arr.push(v);
	GM_setValue('commandStore',arr);
};

const m3u8Store = (v)=>{
	GM_getValue('m3u8Store',[]) === undefined && GM_setValue('m3u8Store', []);
	const arr = [].concat(GM_getValue('m3u8Store',[]));
	arr.push(v);
	GM_setValue('m3u8Store',arr);
};

const deleteStore = (name)=>{
	GM_setValue(name, []);
	GM_notification({text: '削除しました', title: 'GYAO! Plus', timeout: 5000});
};

const checkStore = (name)=>{
	if(GM_getValue(name,[]).length === 0 || !GM_getValue(name,[])) {
		GM_notification({text: 'ストックはありません', title: 'GYAO! Plus', timeout: 5000});
		return true;
	}
};

const mergeCopy = ()=>{
	if(checkStore('commandStore')) {
		return;
	}
	const store = GM_getValue('commandStore',[]);
	const arr = [];
	for(let i = 0, l = store.length; i <l; i++) {
		arr.push(store[i].command);
	}
	GM_setClipboard(arr.join(''));
	GM_notification({text: 'コピーしました', title: 'GYAO! Plus', timeout: 5000});
};

const Vdata = class {
	constructor(type, obj=null) {
		this.type = type;
		this.code = null;
		this.obj = obj;
		this.page = 0;
		this.pp = GM_getValue('first_load_count');
		this.drm = false;
		this.title = '';
	}
	fetch(url, headers={Accept:'*/*'}) {
		return fetch(url, {
			cache: 'no-cache',
			credentials: 'omit',
			headers: headers,
			referrerPolicy: 'strict-origin-when-cross-origin'
		});
	}
	xhr(url, headers={Accept:'*/*'}) {
		return new Promise((onFulfilled, onRejected)=>{
			GM_xmlhttpRequest({
				method: 'GET',
				url: url,
				headers: headers,
				onload: (response)=>{
					if(response.status === 200) {
							return onFulfilled(response);
						} else {
							GM_notification({text: '通信エラー', title: 'GYAO! Plus', timeout: 5000});
							console.log(`Error: GM_XHR | ${url}`);
							return onRejected(response);
						}
					}
			});
		});
	}
	mo() {
		return new Promise((onFulfilled)=>{
			new MutationObserver((mutations, self)=>{
				for(const mutation of mutations) {
					if(![mutation.target.dataset.videoId,mutation.target.dataset.account].includes(undefined)) {
						self.disconnect();
						return onFulfilled(mutation.target.dataset);
					}
				}
			}).observe(document.querySelector('div'), {childList: true, subtree: true, attributes: true, attributeFilter: ['videoId','account']});
		});
	}
	async av() {
		const ds = await this.mo();
		this.a = ds.account;
		this.v = ds.videoId;
	}
	pk() {
		const obj = {
			3971130137001: '1F2YPxbuFJzWtohXjxdgDgIJcsnWacQKaAuaf0gyu8yxCQUlca9Dh7V0Uu_8Rt5JUWZTpgcqzD_IT5hRVde8JIR7r1UYR73ne8S9iLSroqTOA2P-jtl2EUw_OrSMAtenvuaXRF',
			4031511847001: '2N0e6IdrmQn-kEZJ0jRi-Dlm0aUZ9mVF2lcadunJzMVYD6j_51UZzQ3mXuIeV8Zx_UUvbGeeJn73SSrpm0xD7qtiKULPP2NEsp_rgKoVxVWTNZAHN-JAHcuIpFJT7PvUj6gpZv',
			4235717419001: '1O4pwi3SZ75b8DE1c2l78PZ418NByBa33h737rWv6uhPJHYkaZ6xHINTj5oOqa0-zarOEvQ6e1EqKhBcCppkAUWuo5QSKWVC4HZjY2z-Lo_ptwEK3hxfKuvZXkdNuyOM5nNSWy',
			4394098882001: '1l5pA4XtMLusHj72LGzFewqKZzldpmNYTUQdoKnFL_GHhN3dg5FRnNQ5V7SOUKBl-tYFMt8CpSzuSzFAPhIHtVwmMz6F52VnMfu2UjDmeYfvvUqk0CWon46Yh-CZwIVp5vfXrZ',
			4394098883001: '2XqfdZX45o9xMUoyUbUrkEjt-dMFupSdYwCw6YH7Dgd_Aj4epNSPEGgyBOFGHmLa_IPqbf8qv8CWSZaI_8Cd8xkpoMSNkyZrzzX7_TGRmVjAmZ_q_KxemVvC2gsMyfCqCzRrRx',
			5102072603001: '3ZdH8iYjCnmIpuIRqzCn12gVrtpk_qOePK3J9B6h7MuqOw5T_qIqdzpLvuvb_hTvu7hs-7NsvXnPTYKd9Cgw7YiwI9kFfOOCDDEr20WDEYMjGiLptzWouXXdfE996WWM8myP3Z',
			5102072605001: '0_rzsjsYbC1k1wlJLU4HiAtfzjxdUmfvvLUQB-Ax6VA-p-9wOEZbCEm3u95qq2Y1CQQW1K9tPaMma9iAqUqhpISCmyXrgnlpx9soEmoVNuQpiyGsTpePGumWxSs1YoKziYB6Wz',
			5330942432001: '0kGrWxZoXJvJj5Uv6Lypjp4Nrwzz1ktDAuEbD1r_pj0oR1900CRG04FFkxo0ikc1_KmAlB4uvq_GnFwF4IsG_v9jhYOMajC9MkdVQ-QrpboS7vFV8RvK20V5v-St5WGPfXotPx',
			5718741494001: '1iwEgtqPGpn00-wo0b6i9Ki0TLO2j1xrVJmqm2G5QkRf9pO95HjEdSmnbDVs0bJ1QvKSxBNgI5efql9-BDLiipQjW-GaFw4_QPFuo5SZnUocYV8vch17gYtoS9dEhkldDS8Z41',
			5990430581001: '1ZUjiKsbH8c5efHSPEVRBhD7efI1cUvQC2lIT3-dzBx4quqsxvAFHz5TQl7_92OkaIaaDnd-ljUtowKtaLV5f9uQCRT55ckcoWaSFR-ffz8q5_yPCuzMorK42afx6eN0DyDqyP',
			5990430582001: '3wVMX7hDjYLSlc0uxbZxE3LlwGx5OJ12TpQUZEg_ecEmVLvI9VchsSOPqpaqWCKobHHOkR08_-RjxxI0b3Gu6TYx0muJ6Fbbc2S3QiHvwP4t61xjIMs3ZEDaEkTXJxY5lOOIxd',
			6191645753001: '3lNRkJh7lLX0-pEexRvKdHKVbysv2BVsiDm-Sgi2br_by23E-JvP7YviD7npaolSaQlR2yTYQChrOku8WvGhWeJ-cUEJMAJkDj6oPFeqe031ue8JWfsA0NeZ4GD0aTcwhnrplE'
		};
		return obj[this.a];
	}
	brightcove() {
		return [`https://edge.api.brightcove.com/playback/v1/accounts/${this.a}/videos/${this.v}`,{Accept:`application/json;pk=BCpkADawqM${this.pk()}`}];
	}
	async getPl() {
		const json = await this.fetch(...this.brightcove())
		.then(response=>response.json())
		.catch(e=>{
			GM_notification({text: '通信エラー', title: 'GYAO! Plus', timeout: 5000});
			console.log('Error: fetch | brightcove');
			console.log(e);
		});
		if(json.sources[0].key_systems === undefined) {
			const master = json.sources[0].src.replace('p:','ps:');
			return await this.fetch(master)
			.then(response=>response.text())
			.catch(e=>{
				GM_notification({text: '通信エラー', title: 'GYAO! Plus', timeout: 5000});
				console.log('Error: fetch | master.m3u8');
				console.log(e);
			});
		} else {
			this.drm = true;
			return null;
		}
	}
	async player() {
		if(!this.title && document.querySelector('.video-jumbotron-undelivered')) {
			GM_notification({text: '配信待ち', title: 'GYAO! Plus', timeout: 5000});
			return null;
		} else {
			this.title || (this.title = document.querySelector('h1').innerText);
			await this.av();
			const m3u8 = await this.getPl();
			return m3u8 ? this.sortPl(m3u8, location.href) : null;
		}
	}
	async tver() {
		this.title = `${document.querySelector('.title>.inner>h1').innerText} ${document.querySelector('.summary').innerText}${((a)=>a.indexOf(' ') > -1 ? ' '+/(?<= ).+/.exec(a)[0] : '')(document.querySelector('span.tv').innerText)}`;
		return this.player();
	}
	async titles() {
		this.page += 1;
		this.a = '4235717419001';
		const json = await this.fetch(`https://gyao.yahoo.co.jp/api/programs/${this.obj.id}/videos?page=${this.page}&serviceId=gy&perPage=${this.pp}`)
		.then(response=>response.json())
		.catch(e=>{
			GM_notification({text: '通信エラー', title: 'GYAO! Plus', timeout: 5000});
			console.log('Error: fetch | API | Videos');
			console.log(e);
		});
		if(json.videos.length) {
			const arr = [];
			const fn = async(i)=>{
				const obj = json.videos[i];
				if(obj.streamingAvailability === 'available') {
					const itArr = await this.g_api(obj.id);
					this.v = itArr[0];
					if(this.v) {
						const m3u8 = await this.getPl();
						m3u8 && (obj.commands = this.sortPl(m3u8, itArr[2], itArr[1]));
						arr[i] = obj;
					}
				}
			};
			if(json.videos.length === 1) {
				await fn(0);
			} else {
				const itr = [];
				for(let i = 0, l = json.videos.length; i < l; i++) {
					itr.push(fn(i));
				}
				await Promise.all(itr);
			}
			return {ended:json.ended, videos:arr};
		} else {
			if(1 < this.page) {
				return {ended:true,videos:[]};
			} else {
				GM_notification({text: '動画が見つかりません', title: 'GYAO! Plus', timeout: 5000});
				return null;
			}
		}
	}
	async arrivals() {
		this.a = '4235717419001';
		const itArr = await this.g_api(this.obj.id);
		this.v = itArr[0];
		this.title = itArr[1];
		const m3u8 = await this.getPl();
		return m3u8 ? this.sortPl(m3u8, itArr[2]) : null;
	}
	async cx() {
		this.title = this.obj.title;
		const url = `https://i.fod.fujitv.co.jp/abr/pc_html5/${this.obj.id}.m3u8`;
		const m3u8 = await this.fetch(url)
		.then(response=>response.text())
		.catch(e=>{
			GM_notification({text: '通信エラー', title: 'GYAO! Plus', timeout: 5000});
			console.log('Error: fetch | FOD | master.m3u8');
			console.log(e);
		});
		return this.sortPl(m3u8, this.obj.url);
	}
	async g_api(vid) {
		const obj = await this.fetch(`https://gyao.yahoo.co.jp/apis/playback/graphql?appId=dj00aiZpPUNJeDh2cU1RazU3UCZzPWNvbnN1bWVyc2VjcmV0Jng9NTk-&query=query Playback($videoId:ID!,$logicaAgent:LogicaAgent!,$clientSpaceId:String!,$os:Os!,$device:Device!){content(parameter:{contentId:$videoId logicaAgent:$logicaAgent clientSpaceId:$clientSpaceId os:$os device:$device view:WEB}){video{id title delivery{id drm}duration images{url width height}cpId playableAge maxPixel embeddingPermission playableAgents gyaoUrl}}}&variables={"videoId":"${vid}","logicaAgent":"PC_WEB","clientSpaceId":"${document.querySelector('script[data-spaceId]').dataset.spaceid}","os":"UNKNOWN","device":"PC"}`,{Accept:'*/*','Content-Type':'application/json'})
		.then(response=>response.json())
		.catch(e=>{
			GM_notification({text: '通信エラー', title: 'GYAO! Plus', timeout: 5000});
			console.log('Error: fetch | API | GraphQL');
			console.log(e);
		});
		return obj.data.content ? [obj.data.content.video.delivery.id, obj.data.content.video.title, obj.data.content.video.gyaoUrl] : [null];
	}
	command(arr) {
		const o = ['\\', '/', ':', '*', '?', '\"', '<', '>', '|', ' '];
		const n = ['¥', '/', ':', '*', '?', '”', '<', '>', '|', ' '];
		const fileName = (str)=>{
			for(let i = 0, l = o.length; i < l; i++) {
				str = str.split(o[i]).join(n[i]);
			}
			return str;
		};
		const repVar = (a)=>`${a.replace(/#TITLE#/g, fileName(arr[0])).replace(/#M3U8#/g, arr[1]).replace(/#URL#/g, arr[2]).replace(/#UA#/g, navigator.userAgent)}\r\n`;
		const obj = {
			'youtube-dl': repVar(GM_getValue('ytdl_command')),
			'ffmpeg': repVar(GM_getValue('ffmpeg_command')),
			'user': repVar(GM_getValue('user_command'))
		}
		return obj[arr[3]];
	}
	sortPl(m3u8, burl, title = this.title) {
		(m3u8.indexOf('#EXT-X-MEDIA:TYPE=SUBTITLES') !== -1) && (m3u8 = m3u8.replace(/#EXT-X-MEDIA:TYPE=SUBTITLES.+\n/g, ''));
		const urlArr = this.type === 'cx' ? m3u8.match(/https?.+\.m3u8/g) : /https?:/.test(m3u8) ? m3u8.match(/https?:\/\/.+%3D%3D/g) : m3u8.match(/\d{13}\/\d{13}_\d{13}_\d{13}\.m3u8/g).map(v=>`https://vod01-gyao.c.yimg.jp/${this.a}/${v}`);
		const qualityArr = m3u8.match(/(?<=#EXT-X-STREAM-INF:PROGRAM-ID=.+RESOLUTION=\d{3,4}x)\d{3,4}/g);
		const bandwidthArr = m3u8.match(/(?<=#EXT-X-STREAM-INF:PROGRAM-ID=.+,BANDWIDTH=)\d+/g);
		const arr = [];
		for(let i = 0, l = urlArr.length; i < l; i++) {
			arr.push({
				url: urlArr[i],
				quality: `${qualityArr[i]}p`,
				bandwidth:`00000000${bandwidthArr[i]}`.slice(-8),
				command: this.command([title, urlArr[i], burl, GM_getValue('command_type')])
			});
		}
		return arr.sort((a, b)=>parseInt(b.bandwidth,10) - parseInt(a.bandwidth,10));
	}
	get getter() {
		return this[this.type]();
	}
};

const Node = class {
	static settings() {
		const temp = document.createDocumentFragment();
		const div = document.createElement('div');
		div.classList.add('x-settings');
		const createRadio = (value, key)=>{
			const label = document.createElement('label');
			label.classList.add('x-radio');
			label.innerText = value;
			const input = document.createElement('input');
			input.type = 'radio';
			input.value = value;
			input.checked = init[key] === input.value;
			label.addEventListener('change',()=>{
				GM_setValue(key,input.value);
				const arr = document.querySelectorAll('.x-settings input[type="radio"]');
				for(let i = 0, l = arr.length; i < l; i++) {
					arr[i].checked = GM_getValue(key) === arr[i].value;
				}
			});
			label.insertBefore(input,label.firstChild);
			return label;
		};
		const createText = (key)=>{
			const currentValue = GM_getValue(key);
			const label = document.createElement('label');
			label.classList.add('x-text');
			const input = document.createElement('input');
			input.type = 'text';
			input.value = currentValue;
			label.addEventListener('change',()=>GM_setValue(key,input.value));
			label.appendChild(input);
			return label;
		};
		const createCheck = (key)=>{
			const currentValue = GM_getValue(key);
			const div = document.createElement('div');
			div.insertAdjacentHTML('afterbegin', currentValue ? Html.check() : Html.blank());
			div.addEventListener('click',()=>{
				const v = GM_getValue(key);
				GM_setValue(key, v ? false : true);
				div.firstChild.remove();
				div.insertAdjacentHTML('afterbegin', v ? Html.blank() : Html.check());
			});
			return div;
		};
		const createH2 = (str)=>{
			const h2 = document.createElement('h2');
			h2.innerText = str;
			return h2;
		};
		div.appendChild(createH2('コマンドの種類'));
		div.appendChild(createRadio('youtube-dl', 'command_type'));
		div.appendChild(createRadio('ffmpeg', 'command_type'));
		div.appendChild(createRadio('user', 'command_type'));
		div.appendChild(createH2('youtube-dlコマンド(半角英数字・#M3U8# #URL# #UA# #TITLE# が使用可)'));
		div.appendChild(createText('ytdl_command'));
		div.appendChild(createH2('FFmpegコマンド(半角英数字・#M3U8# #URL# #UA# #TITLE# が使用可)'));
		div.appendChild(createText('ffmpeg_command'));
		div.appendChild(createH2('ユーザーコマンド(半角英数字・#M3U8# #URL# #UA# #TITLE# が使用可)'));
		div.appendChild(createText('user_command'));
		div.appendChild(createH2('youtube-dl一括コマンド取得メニュー表示'));
		div.appendChild(createCheck('ytdlAll'));
		div.appendChild(createH2('一括コマンド Best(半角英数字・#UA# #URL# が使用可)'));
		div.appendChild(createText('ytdlAll_commandB'));
		div.appendChild(createH2('一括コマンド Worst(半角英数字・#UA# #URL# が使用可)'));
		div.appendChild(createText('ytdlAll_commandW'));
		div.appendChild(createH2('バッチファイルの文字化け対策'));
		div.appendChild(createCheck('sjif'));
		div.appendChild(createH2('初回読み込み数(半角数字)'));
		div.appendChild(createText('first_load_count'));
		div.appendChild(createH2('基本読み込み数(半角数字)'));
		div.appendChild(createText('default_load_count'));
		div.appendChild(createH2('次を読み込むタイミング(半角数字)'));
		div.appendChild(createText('load_point'));
		div.appendChild(createH2('もうすぐ配信終了のマーク(半角数字・日数・-1で無効)'));
		div.appendChild(createText('days_left_count'));
		div.appendChild(createH2('マークの文字列'));
		div.appendChild(createText('days_left_strings'));
		div.appendChild(createH2('ウィンドウの幅(半角英数字・CSS)'));
		div.appendChild(createText('modal_width'));
		div.appendChild(createH2('ウィンドウの高さ(半角英数字・CSS)'));
		div.appendChild(createText('modal_height'));
		div.appendChild(createH2('ウィンドウのリサイズを有効にする'));
		div.appendChild(createCheck('resize'));
		div.appendChild(createH2('ストックリストの並びを逆にする'));
		div.appendChild(createCheck('reverse'));
		temp.appendChild(div);
		return temp;
	}
	static menu(obj, str) {
		const temp = document.createDocumentFragment();
		const nav = document.createElement('nav');
		nav.classList.add('x-menu');
		nav.dataset.label = str;
		nav.addEventListener('mouseenter', ()=>{
			nav.classList.remove('vanish');
		});
		const ul = document.createElement('ul');
		ul.classList.add('x-list');
		const arr = obj.commands;
		const fn = (e)=>{
			const el = e.target;
			const wh = Math.max(el.clientWidth, el.clientHeight);
			const half = wh / 2;
			const span = document.createElement('span');
			span.style = `width:${wh}px;height:${wh}px;left:${e.layerX - half}px;top:${e.layerY - half}px`;
			span.classList.add('x-ripple');
			el.appendChild(span);
		};
		for(let i = 0, l = arr.length; i < l; ++i) {
			const li = document.createElement('li');
			const span = document.createElement('span');
			span.classList.add('x-list-item');
			span.textContent = `${arr[i].quality}${arr[i].bandwidth ? ` (${arr[i].bandwidth})` : ''}`;
			span.addEventListener('click', (e)=>{
				fn(e);
				GM_setClipboard(arr[i].command);
				commandStore({title: obj.title, quality: arr[i].quality,command: arr[i].command});
			});
			arr[i].url && span.addEventListener('contextmenu', (e)=>{
				e.preventDefault();
				if(arr[i].url) {
					fn(e);
					GM_setClipboard(arr[i].url);
					m3u8Store({title: obj.title, quality: arr[i].quality, url: arr[i].url});
				}
			});
			span.addEventListener('animationend', (e)=>{
				e.currentTarget.querySelector('.x-ripple').remove();
				nav.classList.add('vanish');
			});
			li.appendChild(span);
			ul.appendChild(li);
		}
		nav.appendChild(ul);
		temp.appendChild(nav);
		return temp;
	}
	static modal() {
		const temp = document.createDocumentFragment();
		const parent = document.createElement('div');
		parent.classList.add('x-mask');
		parent.addEventListener('click', ()=>{
			document.body.removeChild(parent);
		});
		const child = document.createElement('div');
		child.style.width = GM_getValue('modal_width');
		child.style.height = GM_getValue('modal_height');
		if(GM_getValue('resize')) {
			child.style.resize = 'both';
			new ResizeObserver((entries)=>{
				GM_setValue('modal_width', entries[0].target.style.width);
				GM_setValue('modal_height', entries[0].target.style.height);
			}).observe(child);
		}
		child.classList.add('x-dialog');
		child.addEventListener('click', (e)=>{
			e.stopPropagation();
		});
		parent.appendChild(child);
		temp.appendChild(parent);
		return temp;
	}
	static ytdlAll(title, url) {
		const temp = document.createDocumentFragment();
		const div = document.createElement('div');
		const repVar = (a)=>`${a.replace(/#UA#/g, navigator.userAgent).replace(/#URL#/g, url)}\r\n`;
		div.classList.add('x-all');
		div.appendChild(this.menu({title: title,
			commands:[
				{quality: 'best', command: repVar(GM_getValue('ytdlAll_commandB'))},
				{quality: 'worst', command: repVar(GM_getValue('ytdlAll_commandW'))}
			]},'コマンドを取得(一括)'));
		temp.appendChild(div);
		temp.appendChild(document.createElement('hr'));
		return temp;
	}
	static list(arr, store) {
		const temp = document.createDocumentFragment();
		const div = document.createElement('div');
		const delcop = document.createElement('div');
		delcop.classList.add('x-delcop');
		const swc = document.createElement('span');
		swc.addEventListener('click',()=>{
			document.querySelector('.x-store-wrap').classList.toggle('x-fd-toggle');
		});
		swc.insertAdjacentHTML('afterbegin', Html.switch());
		delcop.appendChild(swc);
		store === 'commandStore' && (()=>{
			const cop = document.createElement('span');
			cop.innerText = '全てコピー';
			cop.addEventListener('click',()=>mergeCopy());
			delcop.appendChild(cop);
		})();
		const del = document.createElement('span');
		del.innerText = '全て削除'
		del.addEventListener('click',()=>{
			deleteStore(store);
			document.querySelector('.x-mask').remove();
		});
		delcop.appendChild(del);
		div.appendChild(delcop);
		const ul = document.createElement('ul');
		ul.classList.add('x-store-wrap');
		for(let i = 0, l = arr.length; i < l; i++) {
			const li = document.createElement('li');
			li.classList.add('x-store-item');
			const span = document.createElement('span');
			span.insertAdjacentHTML('afterbegin', Html.close());
			span.addEventListener('click',()=>{
				GM_setValue(store, arr.filter(v=>v !== arr[i]));
				const dialog = document.querySelector('.x-dialog');
				dialog.firstChild.remove();
				dialog.appendChild(this.list(GM_getValue(store,[]), store));
			});
			const a = document.createElement('a');
			a.innerText = `[ ${arr[i].quality} ] ${arr[i].title}`;
			a.title = `[ ${arr[i].quality} ] ${arr[i].title}`;
			a.addEventListener('click',()=>{
				GM_setClipboard(store === 'commandStore' ? arr[i].command : arr[i].url);
				GM_notification({text: 'コピーしました', title: 'GYAO! Plus', timeout: 5000});
			});
			li.appendChild(span);
			li.appendChild(a);
			ul.appendChild(li);
		}
		div.appendChild(ul);
		temp.appendChild(div);
		return temp;
	}
	static article(arr, drm) {
		const temp = document.createDocumentFragment();
		for(let i = 0, l = arr.length; i < l; i++) {
			const article = document.createElement('article');
			const h2 = document.createElement('h2');
			const a = document.createElement('a');
			article.classList.add('x-article');
			a.innerText = arr[i].title;
			a.href = arr[i].webUrl;
			a.target = '_blank';
			const time = document.createElement('time');
			time.classList.add('x-date');
			time.datetime = arr[i].endDate;
			time.innerText = arr[i].endDateElements ? `終了: ${arr[i].endDateElements.month}月${arr[i].endDateElements.day}日(${arr[i].endDateElements.week}) ${arr[i].endDateElements.hour}:${arr[i].endDateElements.minute}${arr[i].endDateElements.countDown <= Number(GM_getValue('days_left_count','-1')) ? GM_getValue('days_left_strings',' 🚩') : ''}` : '終了: 未設定';
			const sec = document.createElement('section');
			sec.appendChild(time);
			drm || sec.appendChild(this.menu(arr[i], 'コマンドを取得'));
			h2.appendChild(a);
			article.appendChild(h2);
			article.appendChild(sec);
			temp.appendChild(article);
			temp.appendChild(document.createElement('hr'));
		}
		return temp;
	}
	static batchBtn() {
		const commands = ((arr)=>{
			let str = GM_getValue('sjif') ? 'chcp 65001\r\n' : '';
			for(let i = 0, l = arr.length; i < l; i++) {
				str += arr[i].command;
			}
			return str + 'pause';
		})(GM_getValue('commandStore'));
		const temp = document.createDocumentFragment();
		const span = document.createElement('span');
		span.classList.add('x-batch','x-fi');
		const a = document.createElement('a');
		a.innerText = 'バッチファイルを保存';
		a.download = 'Download.bat';
		a.addEventListener('click', (e)=>{
			const blob = new Blob([commands], {type: 'application/x-bat'});
			a.href = window.URL.createObjectURL(blob);
			span.addEventListener('animationend',()=>{
				document.querySelector('.x-batch').remove();
			});
			span.classList.replace('x-fi','x-fo');
		});
		span.appendChild(a);
		temp.appendChild(span);
		document.body.appendChild(temp);
	}
};

const Html = class {
	static style() {
		const str =
			 '<style>'
			+'@keyframes x-ripple{from{transform:scale(0);opacity:1}to{transform:scale(2);opacity:0}}'
			+'@keyframes x-fadein{0%{opacity:0}100%{opacity:1}}'
			+'@keyframes x-line{0%{left:-100%}50%{left:0}100%{left:100%}}'
			+'.x-mask{display:flex;justify-content:center;align-items:center;height:100%;width:100%;position:fixed;top:0;left:0;background-color:rgba(0,0,0,.5);z-index:100001}'
			+'.x-dialog{position:relative;color:#4C375A;display:block;padding:22px 22px 0 22px;overflow-y:auto;background-color:#EEEFE3;animation:.3s x-fadein linear}'
			+'.x-dialog::after{content:"";display:block;height:22px}'
			+'.x-all{display:flex;justify-content:flex-end}'
			+'.x-loading-wrap{position:absolute;top:0;left:0;z-index:1}'
			+'.x-loading-fixed{display:inline-block;height:4px;position:fixed;overflow:hidden}'
			+'.x-loading-fixed>span{display:inline-block;width:100%;height:100%;position:absolute}'
			+'.x-loading-base{background-color:rgba(229,0,100,.1)}'
			+'.x-loading-line{background-color:#E50064;animation:x-line 1s infinite}'
			+'.x-dialog hr{margin:2rem 0;border:none;width:100%;height:16px;position:relative;background:linear-gradient(-135deg,#DFD5E6 3px,transparent 0),linear-gradient(135deg,#DFD5E6 3px,transparent 0);background-color:transparent;background-position:left bottom;background-repeat:repeat-x;background-size:8px 8px}'
			+'.x-dialog hr::after{content:"";position:absolute;top:-3px;left:0;width:100%;height:100%;background:linear-gradient(-45deg,#DFD5E6 3px,transparent 0),linear-gradient(45deg,#DFD5E6 3px,transparent 0);background-color:transparent;background-position:left bottom;background-repeat:repeat-x;background-size:8px 8px}'
			+'.x-article{display:flex;flex-direction:column}'
			+'.x-article section{display:flex;justify-content:space-between;align-items:end;margin-bottom:1rem}'
			+'.x-article section:last-of-type{margin:0}'
			+'.x-article a{text-decoration:none;color:inherit}'
			+'.x-article a:hover{text-decoration:underline}'
			+'.x-article h2{font-size:1.8rem;margin-bottom:3rem}'
			+'.x-article h2{font-weight:600}'
			+'.x-date{font-size:1.4rem;background-color:#ccc}'
			+'.x-menu{width:200px;height:28px;line-height:28px;font-size:14px;color:#F6F7F1;position:relative}'
			+'.x-menu::before{content:attr(data-label);display:block;text-align:center;box-shadow:4px 4px 3px #4c375b;background-color:#8B64A5}'
			+'.x-menu:hover::before{box-shadow:3px 3px 2px #4c375b;background-color:#9774ae}'
			+'.x-list{z-index:2;position:absolute;top:28px;left:-28px;width:100%;transition:all .3s cubic-bezier(.86,0,.07,1);visibility:hidden;opacity:0}'
			+'.x-menu:not(.vanish):hover>.x-list{left:0px;visibility:visible;opacity:1}'
			+'.x-list-item{position:relative;overflow:hidden;cursor:pointer;display:block;width:100%;text-align:center;background-color:#27253C;border-bottom:1px solid #0A090F;height:28px}'
			+'.x-list>li:last-child>.list-item{border:none}'
			+'.x-list-item:hover{background-color:#14121E}'
			+'.x-ripple{position:absolute;border-radius:50%;animation:x-ripple .3s ease-out;background-color:rgba(255,255,255,.7)}'
			+'.x-last-article{display:flex;justify-content:flex-end}'
			+'.x-batch{position:fixed;top:24px;right:24px;font-size:24px;z-index:100001}'
			+'.x-batch a{display:inline-block;padding:12px;border-radius:12px;box-shadow:0px 3px 1px #4c375b;background-color:#8B64A5;color:#F6F7F1;cursor:pointer}'
			+'.x-batch a:hover:not(:active){background-color:#9774ae}'
			+'.x-batch a:active{box-shadow:0px 1px 1px #4c375b;background-color:#7d5a95}'
			+'@keyframes x-fi{0%{opacity:0;top:-50px}100%{opacity:1}}'
			+'@keyframes x-fo{0%{opacity:1}100%{opacity:0;top:-50px}}'
			+'.x-fi{animation:.3s x-fi ease-in}'
			+'.x-fo{animation:.3s x-fo ease-out}'
			+'@keyframes x-radio{to{transform:rotate(1turn)}}'
			+'.x-settings>h2{font-size:18px;font-weight:600;margin-bottom:12px;line-height:1}'
			+'.x-settings>h2:not(:nth-of-type(1)){margin-top:24px}'
			+'.x-settings>label{font-size:16px;font-weight:400}'
			+'.x-radio{cursor:pointer}'
			+'.x-settings>.x-radio:not(:nth-of-type(1)){padding-left:8px}'
			+'.x-radio>input{appearance:none;display:inline-block;margin-right:4px;width:16px;height:16px;padding:1px;background-clip:content-box;border:1px dashed #0D090F;background-color:#888;border-radius:50%;vertical-align:sub;cursor:pointer}'
			+'.x-radio>input:checked{animation:x-radio 1s ease;background-color:#E50064}'
			+'.x-text>input{appearance:none;border:none;font-size:16px;background-color:#F6F7F1;width:100%}'
			+'.x-settings svg{height:24px;width:24px;cursor:pointer;fill:#4C375A}'
			+`.x-store-wrap{display:flex;flex-direction:${GM_getValue('reverse')?'column-reverse':'column'}}`
			+`.x-fd-toggle{flex-direction:${GM_getValue('reverse')?'column':'column-reverse'}}`
			+'.x-store-item{margin-bottom:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}'
			+'.x-store-item>span{display:inline-block;width:24px;height:24px;border:1px solid #bfbfbf;margin-right:18px;cursor:pointer;vertical-align:middle}'
			+'.x-store-item>span:active{transform:translateY(1px)}'
			+'.x-store-item svg{fill:#4C375A}'
			+'.x-store-item>a{cursor:pointer;color:#4C375A;font-size:16px;line-height:2}'
			+'.x-store-item>a:hover{text-decoration:underline}'
			+'.x-delcop{display:flex;align-content:center;column-gap:18px;margin-bottom:18px}'
			+'.x-delcop>span{border:1px solid #bfbfbf;cursor:pointer}'
			+'.x-delcop>span:first-of-type{display:flex;justify-content:center;align-items:center;width:34px;height:34px}'
			+'.x-delcop>span:not(:first-of-type){flex-basis:50%;font-size:16px;line-height:2;text-align:center}'
			+'.x-delcop>span:active{transform:translateY(1px)}'
			+'.x-delcop svg{height:24px;width:24px;fill:#4C375A}'
			+'.x-mt15{margin-top:15px}'
			+'.x-w296{width:296px}'
			+'#vd_sns>.section>:not(:first-child){margin-left:1rem}'
			+'div#r15{display:none}'
			+'</style>';
		return str;
	}
	static line() {
		const str = `<div class="x-loading-wrap"><span class="x-loading-fixed" style="width:${GM_getValue('modal_width')}"><span class="x-loading-base"></span><span class="x-loading-line"></span></div>`;
		return str;
	}
	static check() {
		const str =
			 '<svg xmlns="http://www.w3.org/2000/svg">'
			+'<path d="M0 0h24v24H0V0z" fill="none"/>'
			+'<path d="M21 3H3v18h18V3zM10 17l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>'
			+'</svg>';
		return str;
	}
	static blank() {
		const str =
			 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">'
			+'<path d="M0 0h24v24H0V0z" fill="none"/>'
			+'<path d="M19 5v14H5V5h14m2-2H3v18h18V3z"/>'
			+'</svg>';
		return str;
	}
	static close() {
		const str =
			 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">'
			+'<path d="M0 0h24v24H0V0z" fill="none"/>'
			+'<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>'
			+'</svg>';
		return str;
	}
	static switch() {
		const str = 
			'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">'
			+'<path d="M6 7h2.5L5 3.5 1.5 7H4v10H1.5L5 20.5 8.5 17H6V7zm4-2v2h12V5H10zm0 14h12v-2H10v2zm0-6h12v-2H10v2z"/>'
			+'</svg>';
		return str;
	}
};

(async()=>{
	const autoClick = (arr, option)=>{
		for(let i = 0, l = arr.length; i < l; i++) {
			new IntersectionObserver((entries,self)=>{
				for(const entry of entries) {
					entry.clientHeight === 0 && self.disconnect();
					entry.intersectionRatio && entry.target.click();
				}
			}).observe(arr[i], option);
		}
	};
	const getList = async(url, globalName)=>{
		document.body.appendChild(Node.modal());
		const dialog = document.querySelector('.x-dialog');
		dialog.insertAdjacentHTML('afterbegin', Html.line());
		const ins = new Vdata('titles', {
			id: url.slice(url.lastIndexOf('/')+1)
		});
		let obj = await ins.getter;
		ins.drm && GM_notification({text: '暗号化された動画', title: 'GYAO! Plus', timeout: 5000});
		if(obj && obj.videos.length) {
			let loadPoint = GM_getValue('load_point');
			(obj.videos.length < loadPoint) && (loadPoint = obj.videos.length);
			dialog.removeChild(dialog.firstChild);
			GM_getValue('ytdlAll') && !ins.drm && dialog.appendChild(Node.ytdlAll(globalName, url));
			dialog.appendChild(Node.article(obj.videos, ins.drm));
			if(obj.ended) {
				GM_notification({text: '読み込み完了', title: 'GYAO! Plus', timeout: 5000});
			} else {
				new IntersectionObserver(async (entries, self)=>{
					for(const entry of entries) {
						if(entry.intersectionRatio) {
							self.disconnect();
							dialog.insertAdjacentHTML('afterbegin', Html.line());
							ins.pp = GM_getValue('default_load_count');
							obj = await ins.getter;
							ins.drm && GM_notification({text: '暗号化された動画', title: 'GYAO! Plus', timeout: 5000});
							dialog.removeChild(dialog.firstChild);
							obj.videos.length && dialog.appendChild(Node.article(obj.videos, ins.drm));
							if(obj.ended) {
								GM_notification({text: '読み込み完了', title: 'GYAO! Plus', timeout: 5000});
								return;
							} else {
								self.observe(document.querySelector(`.x-dialog>.x-article:nth-last-of-type(${loadPoint})`));
							}
						}
					}
				}).observe(document.querySelector(`.x-dialog>.x-article:nth-last-of-type(${loadPoint})`));
			}
		} else {
			document.querySelector('.x-mask').remove();
		}
	};
	const g_titles = async()=>{
		autoClick(document.querySelectorAll('div.more-link.program-list-more-link'), {rootMargin: "300px 0px 0px"});
		document.body.addEventListener('contextmenu', async(e)=>{
			if(e.target.className === 'image-lazy is-responsive' && e.target.parentNode.className === 'program-list-item-link') {
				e.preventDefault();
				const url = e.target.parentNode.href;
				getList(url, e.target.offsetParent.nextElementSibling.firstChild.firstChild.innerText);
			}
		});
	};
	const g_arrivals = async()=>{
		autoClick(document.querySelectorAll('div.more-link.video-list-more-link'), {rootMargin: "300px 0px 0px"});
		document.body.addEventListener('contextmenu', async(e)=>{
			if(e.target.className === 'image-lazy is-responsive' && e.target.parentNode.className === 'video-list-item-link') {
				e.preventDefault();
				if(e.target.offsetParent.parentNode.previousSibling) {
					e.target.offsetParent.parentNode.previousSibling.remove();
					return;
				}
				const ins = new Vdata('arrivals', {
					id: e.target.parentNode.parentNode.parentNode.parentNode.dataset.itemId
				});
				const arr = await ins.getter;
				ins.drm && GM_notification({text: '暗号化された動画', title: 'GYAO! Plus', timeout: 5000});
				if(arr && !ins.drm) {
					const menu = Node.menu({title: ins.title, commands: arr}, 'コマンドを取得');
					e.target.offsetParent.parentNode.parentNode.insertBefore(menu, e.target.offsetParent.parentNode);
					e.target.offsetParent.parentNode.previousSibling.style = `margin-top:${(e.target.offsetWidth - e.target.offsetHeight) / 2}px`;
				}
			}
		});
	};
	const g_player = async()=>{
		const ins = new Vdata('player');
		const arr = await ins.getter;
		ins.drm && GM_notification({text: '暗号化された動画', title: 'GYAO! Plus', timeout: 5000});
		if(arr && !ins.drm) {
			const menu = Node.menu({title: ins.title, commands: arr}, 'コマンドを取得');
			const el = document.querySelector('#vd_sns>.section');
			el.style.display = 'flex';
			el.appendChild(menu);
		}
		GM_registerMenuCommand('動画リストを表示', ()=>{
			const url = document.querySelector('.program-description a').href;
			getList(url.replace(/.+\//,''), url);
		});
		document.body.addEventListener('contextmenu', async(e)=>{
			if(e.target.className === 'image-lazy is-responsive' && e.target.parentNode.className === 'item-tile-item-thumbnail') {
				e.preventDefault();
				const url = e.target.parentNode.parentNode.href;
				getList(url.slice(url.lastIndexOf('/')+1), url);
			}
		});
	};
	const t_player = async()=>{
		const ins = new Vdata('tver');
		const arr = await ins.getter;
		ins.drm && GM_notification({text: '暗号化された動画', title: 'GYAO! Plus', timeout: 5000});
		document.querySelector('.title>.inner').appendChild(Node.menu({title: ins.title, commands: arr}, 'コマンドを取得'));
		document.querySelector('.x-menu').classList.add('x-mt15');
	};
	const f_player = async()=>{
		const ins = new Vdata('cx', {
			id: ((a)=>a.slice(a.lastIndexOf('/')+1))(location.href.slice(0,-1)),
			title: document.querySelector('meta[name="fod_page_title"]').content,
			url: location.href
		});
		const arr = await ins.getter;
		document.querySelector('ul.streaming.btnsBox').insertAdjacentHTML('beforeend', '<li style="margin-top:20px;"></li>');
		document.querySelector('ul.streaming.btnsBox').lastChild.appendChild(Node.menu({title: ins.title, commands: arr}, 'コマンドを取得'));
		document.querySelector('.x-menu').classList.add('x-w296');
	};
	const f_plus7 = async()=>{
		document.body.addEventListener('contextmenu', async(e)=>{
			if(e.target.className === 'clGif-thum') {
				e.preventDefault();
				if(e.target.offsetParent.previousElementSibling) {
					e.target.offsetParent.previousElementSibling.remove();
					return;
				}
				const ins = new Vdata('cx', {
					id: ((a)=>a.slice(a.lastIndexOf('/')+1))(e.target.parentNode.href.slice(0,-1)),
					title: ((a)=>`${a[0].innerText} ${a[1].innerText}`)(e.target.nextElementSibling.children),
					url: e.target.parentNode.href
				});
				const arr = await ins.getter;
				const menu = Node.menu({title: ins.title, commands: arr}, 'コマンドを取得');
				e.target.offsetParent.parentNode.insertBefore(menu, e.target.offsetParent);
				e.target.offsetParent.previousSibling.style = 'margin-bottom:8px;width:184px;';
			}
		});
	};
	document.head.insertAdjacentHTML('beforeend', Html.style());
	switch(true) {
		case /tver\.jp/.test(location.host):
			t_player();
		break;
		case location.host === 'gyao.yahoo.co.jp' && /^\/titles(.+?)?/.test(location.pathname):
			g_titles();
		break;
		case location.host === 'gyao.yahoo.co.jp' && /^\/arrivals(.+?)?/.test(location.pathname):
			g_arrivals();
		break;
		case location.host === 'gyao.yahoo.co.jp' && /^\/(title|episode|p|player)\//.test(location.pathname):
			g_player();
		break;
		case location.href === 'https://fod.fujitv.co.jp/s/plus7/':
			f_plus7();
		break;
		case /^\/s\/genre\//.test(location.pathname):
			f_player();
		break;
	}
})();

GM_registerMenuCommand('ストックしているコマンドを全てコピー', ()=>{
	if(checkStore('commandStore')) {
		return;
	}
	mergeCopy();
});
GM_registerMenuCommand('ストックしているコマンドを表示', ()=>{
	if(checkStore('commandStore')) {
		return;
	}
	const el = document.querySelector('.x-mask');
	el && el.remove();
	document.body.appendChild(Node.modal());
	const dialog = document.querySelector('.x-dialog');
	dialog.appendChild(Node.list(GM_getValue('commandStore',[]), 'commandStore'));
});
GM_registerMenuCommand('ストックしているm3u8を表示', ()=>{
	if(checkStore('m3u8Store')) {
		return;
	}
	const el = document.querySelector('.x-mask');
	el && el.remove();
	document.body.appendChild(Node.modal());
	const dialog = document.querySelector('.x-dialog');
	dialog.appendChild(Node.list(GM_getValue('m3u8Store',[]), 'm3u8Store'));
});
GM_registerMenuCommand('バッチファイル作成', ()=>{
	if(checkStore('commandStore')) {
		return;
	}
	Node.batchBtn();
});
GM_registerMenuCommand('設定', ()=>{
	const el = document.querySelector('.x-mask');
	el && el.remove();
	document.body.appendChild(Node.modal());
	const dialog = document.querySelector('.x-dialog');
	dialog.appendChild(Node.settings());
});
GM_registerMenuCommand('ストックしているコマンドを全て削除', ()=>deleteStore('commandStore'));
GM_registerMenuCommand('ストックしているm3u8を全て削除', ()=>deleteStore('m3u8Store'));
GM_registerMenuCommand('旧バージョンのゴミを削除', ()=>{
	const keys = Object.keys(init);
	const arr = GM_listValues();
	let count = 0;
	const del = (a)=>{
		GM_deleteValue(a);
		console.log(`旧設定値: ${a} を削除しました。`);
		count += 1;
	};
	for(let i = 0, l = arr.length; i < l; i++) {
		const v = arr[i];
		!keys.includes(v) && del(v);
	}
	GM_notification({text: count ? `削除完了(${count})` : 'ゴミはありません' , title: 'GYAO! Plus', timeout: 5000});
});
GYAO!の動画ダウンロード・保存をサポートするuser.jsの配布ページです。TVerにも対応。
スポンサードリンク
スポンサードリンク