<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wool-Tube</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #000066;
min-height: 100vh;
padding: 20px;
transition: background 0.3s;
}
body.light-mode {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.logo {
width: 80px;
height: 80px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 45px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.search-box {
background: linear-gradient(135deg, #4a0e34 0%, #2d1b3d 100%);
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
margin-bottom: 30px;
transition: background 0.3s;
}
body.light-mode .search-box {
background: white;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
input[type="text"] {
flex: 1;
padding: 15px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s, background 0.3s;
background: #000000;
color: white;
}
body.light-mode input[type="text"] {
background: white;
color: #333;
border: 2px solid #e0e0e0;
}
input[type="text"]::placeholder {
color: rgba(255,255,255,0.5);
}
body.light-mode input[type="text"]::placeholder {
color: rgba(0,0,0,0.5);
}
input[type="text"]:focus {
outline: none;
border-color: #7b1fa2;
background: #1a1a1a;
}
body.light-mode input[type="text"]:focus {
background: #f9f9f9;
border-color: #7b1fa2;
}
button {
padding: 15px 30px;
background: linear-gradient(135deg, #7b1fa2 0%, #4a148c 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(123,31,162,0.5);
}
button:active {
transform: translateY(0);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.player-section {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
margin-bottom: 30px;
}
#player {
width: 100%;
aspect-ratio: 16/9;
border-radius: 10px;
background: #000;
}
#playerContainer {
width: 100%;
aspect-ratio: 16/9;
border-radius: 10px;
overflow: hidden;
background: #000;
}
video {
width: 100%;
height: 100%;
border-radius: 10px;
}
.controls {
display: flex;
gap: 10px;
margin-top: 15px;
flex-wrap: wrap;
}
.controls button {
flex: 1;
min-width: 100px;
}
.quality-controls {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
margin-top: 15px;
}
.quality-controls button {
min-width: 120px;
padding: 12px 20px;
font-size: 14px;
}
.quality-controls button.active {
background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
box-shadow: 0 3px 10px rgba(76,175,80,0.4);
}
.results {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.video-card {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
}
.video-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
}
.video-card img {
width: 100%;
height: 180px;
object-fit: cover;
}
.video-info {
padding: 15px;
}
.video-title {
font-weight: bold;
margin-bottom: 8px;
font-size: 14px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.video-channel {
color: #666;
font-size: 12px;
}
.loading {
text-align: center;
color: white;
font-size: 18px;
margin: 20px 0;
}
.error {
background: #ff5252;
color: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.info {
background: #4CAF50;
color: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
#currentVideo {
text-align: center;
margin-bottom: 15px;
font-size: 18px;
font-weight: bold;
color: #333;
}
.stream-url {
margin-top: 15px;
padding: 10px;
background: #f5f5f5;
border-radius: 5px;
font-size: 12px;
word-break: break-all;
}
.api-note {
font-size: 12px;
color: rgba(255,255,255,0.9);
margin-top: 10px;
transition: color 0.3s;
}
body.light-mode .api-note {
color: #666;
}
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
background: rgba(255,255,255,0.2);
border: none;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
z-index: 1000;
}
.theme-toggle:hover {
transform: scale(1.1);
background: rgba(255,255,255,0.3);
}
body.light-mode .theme-toggle {
background: rgba(0,0,0,0.1);
}
body.light-mode .theme-toggle:hover {
background: rgba(0,0,0,0.2);
}
</style>
</head>
<body>
<button class="theme-toggle" onclick="toggleTheme()" id="themeToggle">🌙</button>
<div class="container">
<h1>
<div class="logo">🐑</div>
Wool-Tube
</h1>
<div class="search-box">
<div class="input-group">
<input type="text" id="apiKey" placeholder="Google API Key(オプション)">
</div>
<div class="input-group">
<input type="text" id="searchQuery" placeholder="検索キーワードまたはYouTube URLを入力" onkeypress="if(event.key==='Enter') searchVideos()">
<button onclick="searchVideos()">🔍 検索</button>
</div>
<div class="api-note">
※Google API Keyを入力すると、YouTube Data API v3で検索します(より正確な検索結果)<br>
APIキーなしでも、代替APIで検索可能です<br>
🌐 GitHub Pages対応: CORSプロキシ経由で通信します
</div>
</div>
<div class="player-section" id="playerSection" style="display: none;">
<div id="currentVideo"></div>
<div id="playerContainer">
<iframe id="player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
<div class="controls">
<button onclick="setPlayMode('stream')" id="btnStream">🎬 ストリーム再生</button>
<button onclick="setPlayMode('embed')">📺 埋め込み再生</button>
<button onclick="setPlayMode('nocookie')">🍪 No Cookie</button>
<button onclick="setPlayMode('education')">📚 Education</button>
</div>
<div class="controls">
<button onclick="downloadVideo('video')" id="btnDownloadVideo">📥 動画DL</button>
<button onclick="downloadVideo('audio')" id="btnDownloadAudio">🎵 音声DL</button>
</div>
<div class="quality-controls" id="qualityControls" style="display: none;"></div>
<div class="stream-url" id="streamUrl"></div>
<div style="margin-top: 10px; padding: 10px; background: #1a1a1a; border-radius: 5px; color: #00ff00; font-family: monospace; font-size: 11px; max-height: 200px; overflow-y: auto; display: none;" id="debugLog"></div>
</div>
<div id="loading" class="loading" style="display: none;">読み込み中...</div>
<div id="error" class="error" style="display: none;"></div>
<div id="info" class="info" style="display: none;"></div>
<div class="results" id="results"></div>
</div>
<script>
let currentVideoId = null;
let availableStreams = [];
let currentPlayMode = 'embed';
let debugMessages = [];
let currentQuality = null;
function addDebug(msg) {
debugMessages.push(`[${new Date().toLocaleTimeString()}] ${msg}`);
const debugLog = document.getElementById('debugLog');
if (debugLog) {
debugLog.style.display = 'block';
debugLog.innerHTML = debugMessages.slice(-15).join('<br>');
}
console.log(msg);
}
// CORSプロキシ設定
const CORS_PROXIES = [
'https://api.allorigins.win/raw?url=',
'https://corsproxy.io/?',
'https://api.codetabs.com/v1/proxy?quest='
];
let currentProxyIndex = 0;
function getProxiedUrl(url) {
const proxy = CORS_PROXIES[currentProxyIndex];
return `${proxy}${encodeURIComponent(url)}`;
}
function rotateProxy() {
currentProxyIndex = (currentProxyIndex + 1) % CORS_PROXIES.length;
addDebug(`🔄 プロキシ切り替え: ${CORS_PROXIES[currentProxyIndex]}`);
}
const streamAPIs = [
{
name: 'Invidious',
servers: [
'https://nyc1.iv.ggtyler.dev',
'https://invid-api.poketube.fun',
'https://cal1.iv.ggtyler.dev',
'https://invidious.nikkosphere.com',
'https://lekker.gay',
'https://invidious.f5.si',
'https://invidious.lunivers.trade',
'https://pol1.iv.ggtyler.dev',
'https://eu-proxy.poketube.fun',
'https://iv.melmac.space',
'https://invidious.reallyaweso.me',
'https://invidious.dhusch.de',
'https://usa-proxy2.poketube.fun',
'https://id.420129.xyz',
'https://invidious.darkness.service',
'https://iv.datura.network',
'https://invidious.jing.rocks',
'https://invidious.private.coffee',
'https://youtube.mosesmang.com',
'https://iv.duti.dev',
'https://invidious.projectsegfau.lt',
'https://invidious.perennialte.ch',
'https://invidious.einfachzocken.eu',
'https://invidious.adminforge.de',
'https://inv.nadeko.net',
'https://invidious.esmailelbob.xyz',
'https://invidious.0011.lt',
'https://invidious.ducks.party',
'https://invidious.fdn.fr',
'https://invidious.privacydev.net',
'https://iv.nboeck.de',
'https://invidious.protokolla.fi',
'https://invidious.slipfox.xyz',
'https://inv.bp.projectsegfau.lt',
'https://yt.artemislena.eu',
'https://invidious.flokinet.to',
'https://invidious.kavin.rocks',
'https://vid.puffyan.us',
'https://inv.riverside.rocks',
'https://invidious.tiekoetter.com',
'https://inv.vern.cc',
'https://invidious.nerdvpn.de',
'https://inv.us.projectsegfau.lt',
'https://invidious.lunar.icu',
'https://inv.in.projectsegfau.lt',
'https://yt.drgnz.club',
'https://inv.tux.pizza',
'https://iv.ggtyler.dev',
'https://inv.citw.lgbt',
'https://inv.odyssey346.dev',
'https://yewtu.be',
'https://invidious.snopyta.org',
'https://vid.mint.lgbt',
'https://invidious.sethforprivacy.com',
'https://invidious.namazso.eu'
],
getUrl: (server, videoId) => `${server}/api/v1/videos/${videoId}`,
parseResponse: (data) => {
const streams = [];
if (data.formatStreams && data.formatStreams.length > 0) {
data.formatStreams.forEach(s => {
if (s.url) {
streams.push({
url: s.url,
quality: s.qualityLabel || s.quality || 'unknown',
type: 'Invidious',
hasAudio: true
});
}
});
}
if (data.adaptiveFormats && data.adaptiveFormats.length > 0) {
data.adaptiveFormats.forEach(s => {
if (s.url && s.type && s.type.includes('video')) {
streams.push({
url: s.url,
quality: s.qualityLabel || 'auto',
type: 'Invidious',
hasAudio: false
});
}
});
}
return streams;
}
},
{
name: 'Piped',
servers: [
'https://pipedapi.kavin.rocks',
'https://api-piped.mha.fi',
'https://pipedapi.tokhmi.xyz',
'https://pipedapi.adminforge.de',
'https://api.piped.projectsegfau.lt',
'https://pipedapi.pfcd.me',
'https://api.piped.privacydev.net',
'https://pipedapi.in.projectsegfau.lt',
'https://pipedapi.osphost.fi'
],
getUrl: (server, videoId) => `${server}/streams/${videoId}`,
parseResponse: (data) => {
if (!data.videoStreams) return [];
return data.videoStreams.map(s => ({
url: s.url,
quality: s.quality,
type: 'Piped',
hasAudio: true
}));
}
},
{
name: 'Poké',
servers: [
'https://poke.tchncs.de',
'https://poke.ggtyler.dev',
'https://poke.privacydev.net'
],
getUrl: (server, videoId) => `${server}/api/v1/videos/${videoId}`,
parseResponse: (data) => {
const streams = [];
if (data.formatStreams) {
data.formatStreams.forEach(s => {
if (s.url) streams.push({
url: s.url,
quality: s.qualityLabel || s.quality,
type: 'Poké',
hasAudio: true
});
});
}
return streams;
}
},
{
name: 'Tubo',
servers: [
'https://tubo.migalmoreno.com',
'https://tubo.reallyaweso.me'
],
getUrl: (server, videoId) => `${server}/api/streams/${videoId}`,
parseResponse: (data) => {
if (!data.videoStreams) return [];
return data.videoStreams.map(s => ({
url: s.url,
quality: s.quality,
type: 'Tubo',
hasAudio: true
}));
}
},
{
name: 'ViewTube',
servers: ['https://api.viewtube.io'],
getUrl: (server, videoId) => `${server}/videos/${videoId}`,
parseResponse: (data) => {
if (!data.adaptiveFormats) return [];
return data.adaptiveFormats
.filter(f => f.mimeType && f.mimeType.includes('video') && f.url)
.map(f => ({
url: f.url,
quality: f.qualityLabel || 'auto',
type: 'ViewTube',
hasAudio: false
}));
}
},
{
name: 'FreeTube',
servers: [
'https://api.freetube.app',
'https://freetube-api.vercel.app'
],
getUrl: (server, videoId) => `${server}/api/v1/videos/${videoId}`,
parseResponse: (data) => {
const streams = [];
if (data.formatStreams && data.formatStreams.length > 0) {
data.formatStreams.forEach(s => {
if (s.url) {
streams.push({
url: s.url,
quality: s.qualityLabel || s.quality,
type: 'FreeTube',
hasAudio: true
});
}
});
}
if (data.adaptiveFormats && data.adaptiveFormats.length > 0) {
data.adaptiveFormats.forEach(s => {
if (s.url && s.type && s.type.includes('video')) {
streams.push({
url: s.url,
quality: s.qualityLabel || 'auto',
type: 'FreeTube',
hasAudio: false
});
}
});
}
return streams;
}
},
{
name: 'CloudTube',
servers: [
'https://tube.cadence.moe',
'https://cloudtube.rknight.me'
],
getUrl: (server, videoId) => `${server}/api/v1/videos/${videoId}`,
parseResponse: (data) => {
const streams = [];
if (data.formatStreams) {
data.formatStreams.forEach(s => {
if (s.url) {
streams.push({
url: s.url,
quality: s.qualityLabel || s.quality,
type: 'CloudTube',
hasAudio: true
});
}
});
}
if (data.adaptiveFormats) {
data.adaptiveFormats.forEach(s => {
if (s.url && s.type && s.type.includes('video')) {
streams.push({
url: s.url,
quality: s.qualityLabel || 'auto',
type: 'CloudTube',
hasAudio: false
});
}
});
}
return streams;
}
},
{
name: 'LightTube',
servers: [
'https://tube.kuylar.dev'
],
getUrl: (server, videoId) => `${server}/api/player?v=${videoId}`,
parseResponse: (data) => {
if (!data.formats) return [];
return data.formats
.filter(f => f.url)
.map(f => ({
url: f.url,
quality: f.quality || f.qualityLabel || 'auto',
type: 'LightTube',
hasAudio: true
}));
}
},
{
name: 'Clipious',
servers: [
'https://api.clipious.app'
],
getUrl: (server, videoId) => `${server}/videos/${videoId}`,
parseResponse: (data) => {
const streams = [];
if (data.formatStreams) {
data.formatStreams.forEach(s => {
if (s.url) {
streams.push({
url: s.url,
quality: s.qualityLabel || s.quality,
type: 'Clipious',
hasAudio: true
});
}
});
}
return streams;
}
},
{
name: 'YTMous',
servers: [
'https://ytmous.vercel.app'
],
getUrl: (server, videoId) => `${server}/api/video/${videoId}`,
parseResponse: (data) => {
if (!data.formats) return [];
return data.formats
.filter(f => f.mimeType && f.mimeType.includes('video') && f.url)
.map(f => ({
url: f.url,
quality: f.qualityLabel || f.quality || 'auto',
type: 'YTMous',
hasAudio: true
}));
}
},
{
name: 'YT-DLP',
servers: [
'https://yt-dl-test.vercel.app',
'https://youtube-dl-api.vercel.app'
],
getUrl: (server, videoId) => `${server}/api/info?url=https://www.youtube.com/watch?v=${videoId}`,
parseResponse: (data) => {
if (!data.formats) return [];
return data.formats
.filter(f => f.url && f.vcodec && f.vcodec !== 'none')
.map(f => ({
url: f.url,
quality: f.height ? `${f.height}p` : (f.format_note || 'auto'),
type: 'YT-DLP',
hasAudio: f.acodec && f.acodec !== 'none',
format_id: f.format_id,
ext: f.ext
}));
}
},
{
name: 'GoogleVideo',
servers: ['https://corsproxy.io/?'],
getUrl: (server, videoId) => `${server}https://www.youtube.com/get_video_info?video_id=${videoId}`,
parseResponse: (data) => {
try {
const params = new URLSearchParams(data);
const playerResponse = JSON.parse(params.get('player_response') || '{}');
if (!playerResponse.streamingData) return [];
const streams = [];
const streamingData = playerResponse.streamingData;
if (streamingData.formats) {
streamingData.formats.forEach(f => {
if (f.url) {
streams.push({
url: f.url,
quality: f.qualityLabel || f.quality || 'auto',
type: 'GoogleVideo',
hasAudio: true,
itag: f.itag
});
}
});
}
if (streamingData.adaptiveFormats) {
streamingData.adaptiveFormats.forEach(f => {
if (f.url && f.mimeType && f.mimeType.includes('video')) {
streams.push({
url: f.url,
quality: f.qualityLabel || 'auto',
type: 'GoogleVideo',
hasAudio: false,
itag: f.itag
});
}
});
}
return streams;
} catch (err) {
console.error('GoogleVideo parse error:', err);
return [];
}
}
},
{
name: 'Cobalt',
servers: [
'https://api.cobalt.tools',
'https://co.wuk.sh'
],
getUrl: (server, videoId) => `${server}/api/json`,
parseResponse: (data) => {
if (!data.url) return [];
return [{
url: data.url,
quality: data.quality || 'max',
type: 'Cobalt',
hasAudio: true
}];
},
customFetch: async (server, videoId) => {
const url = server;
const proxiedUrl = getProxiedUrl(url);
const response = await fetch(proxiedUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
url: `https://www.youtube.com/watch?v=${videoId}`,
vQuality: 'max'
}),
signal: AbortSignal.timeout(8000)
});
return response;
}
},
{
name: 'Y2Mate',
servers: [
'https://www.y2mate.com/mates/analyzeV2/ajax'
],
getUrl: (server, videoId) => server,
parseResponse: (data) => {
if (!data.links || !data.links.mp4) return [];
const streams = [];
Object.entries(data.links.mp4).forEach(([quality, info]) => {
if (info.url) {
streams.push({
url: info.url,
quality: quality,
type: 'Y2Mate',
hasAudio: true
});
}
});
return streams;
},
customFetch: async (server, videoId) => {
const formData = new FormData();
formData.append('k_query', `https://www.youtube.com/watch?v=${videoId}`);
formData.append('k_page', 'home');
formData.append('hl', 'en');
formData.append('q_auto', '0');
const proxiedUrl = getProxiedUrl(server);
const response = await fetch(proxiedUrl, {
method: 'POST',
body: formData,
signal: AbortSignal.timeout(8000)
});
return response;
}
},
{
name: 'SaveFrom',
servers: [
'https://savefrom.net/download'
],
getUrl: (server, videoId) => `${server}?url=https://www.youtube.com/watch?v=${videoId}`,
parseResponse: (data) => {
if (!data.url || !Array.isArray(data.url)) return [];
return data.url
.filter(u => u.url && u.type === 'video')
.map(u => ({
url: u.url,
quality: u.quality || u.name || 'auto',
type: 'SaveFrom',
hasAudio: true
}));
}
},
{
name: 'SSYoutube',
servers: [
'https://ssyoutube.com/api/convert'
],
getUrl: (server, videoId) => server,
parseResponse: (data) => {
if (!data.formats) return [];
return data.formats
.filter(f => f.url)
.map(f => ({
url: f.url,
quality: f.qualityLabel || f.quality || 'auto',
type: 'SSYoutube',
hasAudio: true
}));
},
customFetch: async (server, videoId) => {
const proxiedUrl = getProxiedUrl(server);
const response = await fetch(proxiedUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: `https://www.youtube.com/watch?v=${videoId}`
}),
signal: AbortSignal.timeout(8000)
});
return response;
}
},
{
name: 'KeepVid',
servers: [
'https://keepvid.pro/api/video'
],
getUrl: (server, videoId) => `${server}?url=https://www.youtube.com/watch?v=${videoId}`,
parseResponse: (data) => {
if (!data.formats) return [];
return data.formats
.filter(f => f.url && f.type === 'video')
.map(f => ({
url: f.url,
quality: f.quality || 'auto',
type: 'KeepVid',
hasAudio: true
}));
}
},
{
name: 'YouTubeToMP3',
servers: [
'https://youtubetomp3music.com/api/convert'
],
getUrl: (server, videoId) => server,
parseResponse: (data) => {
if (!data.url) return [];
return [{
url: data.url,
quality: data.quality || 'default',
type: 'YouTubeToMP3',
hasAudio: true
}];
},
customFetch: async (server, videoId) => {
const proxiedUrl = getProxiedUrl(server);
const response = await fetch(proxiedUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: `https://www.youtube.com/watch?v=${videoId}`,
format: 'mp4'
}),
signal: AbortSignal.timeout(8000)
});
return response;
}
},
{
name: 'YTBGoat',
servers: [
'https://ytbgoat.com/api/video'
],
getUrl: (server, videoId) => `${server}/${videoId}`,
parseResponse: (data) => {
if (!data.streams) return [];
return data.streams
.filter(s => s.url)
.map(s => ({
url: s.url,
quality: s.quality || s.resolution || 'auto',
type: 'YTBGoat',
hasAudio: s.hasAudio !== false
}));
}
},
{
name: 'loader.to',
servers: [
'https://loader.to/ajax/download.php'
],
getUrl: (server, videoId) => server,
parseResponse: (data) => {
if (!data.url) return [];
return [{
url: data.url,
quality: 'default',
type: 'loader.to',
hasAudio: true
}];
},
customFetch: async (server, videoId) => {
const formData = new FormData();
formData.append('url', `https://www.youtube.com/watch?v=${videoId}`);
const proxiedUrl = getProxiedUrl(server);
const response = await fetch(proxiedUrl, {
method: 'POST',
body: formData,
signal: AbortSignal.timeout(8000)
});
return response;
}
}
];
function toggleTheme() {
const body = document.body;
const themeToggle = document.getElementById('themeToggle');
body.classList.toggle('light-mode');
if (body.classList.contains('light-mode')) {
themeToggle.textContent = '☀️';
} else {
themeToggle.textContent = '🌙';
}
}
async function searchVideos() {
const query = document.getElementById('searchQuery').value.trim();
const apiKey = document.getElementById('apiKey').value.trim();
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const info = document.getElementById('info');
const results = document.getElementById('results');
error.style.display = 'none';
info.style.display = 'none';
results.innerHTML = '';
if (!query) {
showError('検索キーワードまたはURLを入力してください');
return;
}
const videoIdMatch = query.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/);
if (videoIdMatch) {
playVideo(videoIdMatch[1], 'YouTube動画');
return;
}
loading.style.display = 'block';
showInfo('検索中...');
if (apiKey) {
try {
const response = await fetch(
`https://www.googleapis.com/youtube/v3/search?part=snippet&maxResults=12&q=${encodeURIComponent(query)}&type=video&key=${apiKey}`,
{ signal: AbortSignal.timeout(10000) }
);
if (response.ok) {
const data = await response.json();
if (data.items && data.items.length > 0) {
displayGoogleResults(data.items);
loading.style.display = 'none';
showInfo('Google YouTube Data API で検索しました');
return;
}
}
} catch (err) {
console.log('Google API error:', err);
}
}
showError('検索に失敗しました。YouTube URLを直接入力するか、Google API Keyを入力してみてください。');
loading.style.display = 'none';
}
function displayGoogleResults(items) {
const results = document.getElementById('results');
items.forEach(item => {
const videoId = item.id.videoId;
const card = document.createElement('div');
card.className = 'video-card';
card.onclick = () => playVideo(videoId, item.snippet.title);
const thumbnail = item.snippet.thumbnails.medium.url;
card.innerHTML = `
<img src="${thumbnail}" alt="${item.snippet.title}">
<div class="video-info">
<div class="video-title">${item.snippet.title}</div>
<div class="video-channel">${item.snippet.channelTitle}</div>
</div>
`;
results.appendChild(card);
});
}
function playVideo(videoId, title) {
const playerSection = document.getElementById('playerSection');
const currentVideo = document.getElementById('currentVideo');
const btnStream = document.getElementById('btnStream');
currentVideoId = videoId;
availableStreams = [];
debugMessages = [];
currentQuality = null;
addDebug(`🎬 動画ID: ${videoId}`);
addDebug(`📝 タイトル: ${title}`);
playerSection.style.display = 'block';
currentVideo.textContent = title;
const playerContainer = document.getElementById('playerContainer');
playerContainer.innerHTML = `<iframe id="player" width="100%" height="100%" src="https://www.youtube.com/embed/${videoId}?autoplay=1" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
const streamUrl = document.getElementById('streamUrl');
streamUrl.textContent = `埋め込み再生中 | https://www.youtube.com/watch?v=${videoId}`;
addDebug('✅ 埋め込みプレーヤー作成完了');
btnStream.disabled = true;
btnStream.textContent = '🎬 取得中...';
addDebug('🔍 ストリーム情報の取得を開始...');
fetchStreamUrls(videoId).then(streams => {
if (streams.length > 0) {
availableStreams = streams;
btnStream.disabled = false;
btnStream.textContent = '🎬 ストリーム再生';
addDebug(`✅ ${streams.length}個のストリームを取得成功!`);
addDebug(`📊 API: ${streams[0].type}`);
addDebug(`🎚️ 画質: ${streams.map(s => s.quality).join(', ')}`);
showInfo(`ストリーム再生が利用可能です(${streams[0].type})`);
// ストリーム取得後、自動的にストリーム再生モードに切り替え
addDebug('🚀 自動的にストリーム再生を開始します');
setPlayMode('stream');
} else {
btnStream.disabled = true;
btnStream.textContent = '🎬 利用不可';
addDebug('❌ ストリーム取得失敗');
addDebug('💡 埋め込み再生をご利用ください');
}
}).catch(err => {
addDebug(`❌ エラー: ${err.message}`);
btnStream.disabled = true;
btnStream.textContent = '🎬 エラー';
});
playerSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
async function fetchStreamUrls(videoId) {
let attemptCount = 0;
const maxAttempts = streamAPIs.reduce((sum, api) => sum + api.servers.length, 0);
addDebug(`🌐 ${maxAttempts}個のエンドポイントを試行`);
for (const api of streamAPIs) {
addDebug(`\n📡 ${api.name} API (${api.servers.length}サーバー)`);
for (const server of api.servers) {
attemptCount++;
try {
addDebug(` [${attemptCount}/${maxAttempts}] ${server.replace('https://', '')}`);
let response;
if (api.customFetch) {
response = await api.customFetch(server, videoId);
} else {
const apiUrl = api.getUrl(server, videoId);
const proxiedUrl = getProxiedUrl(apiUrl);
try {
response = await fetch(proxiedUrl, {
signal: AbortSignal.timeout(8000)
});
} catch (proxyErr) {
addDebug(` ⚠️ プロキシ1失敗、再試行...`);
rotateProxy();
const retryUrl = getProxiedUrl(apiUrl);
response = await fetch(retryUrl, {
signal: AbortSignal.timeout(8000)
});
}
}
addDebug(` → HTTP ${response.status}`);
if (!response.ok) {
continue;
}
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
addDebug(` → データ取得成功`);
const streams = api.parseResponse(data);
if (streams.length > 0) {
addDebug(` ✅ ${streams.length}個のストリーム取得!`);
streams.slice(0, 3).forEach((s, i) => {
addDebug(` [${i+1}] ${s.quality} ${s.hasAudio ? '🔊' : '🔇'}`);
});
return streams;
} else {
addDebug(` ⚠️ ストリーム0個`);
}
} catch (err) {
if (err.name === 'AbortError') {
addDebug(` ⏱️ タイムアウト(8秒)`);
} else {
addDebug(` ❌ ${err.message}`);
}
continue;
}
}
}
addDebug('\n❌ すべてのAPIで失敗しました');
return [];
}
function setPlayMode(mode) {
if (!currentVideoId) {
addDebug('❌ 動画IDがありません');
return;
}
currentPlayMode = mode;
const playerContainer = document.getElementById('playerContainer');
const streamUrl = document.getElementById('streamUrl');
const qualityControls = document.getElementById('qualityControls');
streamUrl.textContent = '';
qualityControls.style.display = 'none';
addDebug(`\n🎮 再生モード切り替え: ${mode}`);
if (mode === 'stream') {
if (availableStreams.length === 0) {
showError('ストリーム情報が取得できていません');
addDebug('❌ ストリーム情報なし');
return;
}
addDebug(`📺 video要素を作成`);
playerContainer.innerHTML = '<video id="player" controls autoplay style="width:100%;height:100%;"></video>';
qualityControls.style.display = 'grid';
// 画質オプションを動的に生成
createQualityButtons();
// 最高画質で自動再生
playBestQuality();
return;
}
addDebug(`📺 iframe埋め込みを作成: ${mode}`);
let embedUrl = '';
if (mode === 'embed') {
embedUrl = `https://www.youtube.com/embed/${currentVideoId}?autoplay=1`;
streamUrl.textContent = `埋め込み再生 | https://www.youtube.com/watch?v=${currentVideoId}`;
} else if (mode === 'nocookie') {
embedUrl = `https://www.youtube-nocookie.com/embed/${currentVideoId}?autoplay=1`;
streamUrl.textContent = `No Cookie埋め込み | プライバシー重視`;
} else if (mode === 'education') {
const eduParams = [
'autoplay=1',
'mute=0',
'controls=1',
'start=0',
'playsinline=1',
'showinfo=0',
'rel=0',
'iv_load_policy=3',
'modestbranding=1',
'fs=1',
'cc_load_policy=0',
'enablejsapi=1'
].join('&');
embedUrl = `https://www.youtube.com/embed/${currentVideoId}?${eduParams}`;
streamUrl.textContent = `Education埋め込み | 教育プラットフォーム向け設定`;
}
playerContainer.innerHTML = `<iframe id="player" width="100%" height="100%" src="${embedUrl}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
addDebug(`✅ 埋め込み完了: ${embedUrl}`);
}
function createQualityButtons() {
const qualityControls = document.getElementById('qualityControls');
qualityControls.innerHTML = '';
// 音声付きストリームを優先
const audioStreams = availableStreams.filter(s => s.hasAudio);
const sortableStreams = audioStreams.length > 0 ? audioStreams : availableStreams;
// 画質ごとにグループ化
const qualityMap = new Map();
sortableStreams.forEach(stream => {
const quality = String(stream.quality);
if (!qualityMap.has(quality)) {
qualityMap.set(quality, stream);
}
});
// 画質を数値でソート
const sortedQualities = Array.from(qualityMap.entries()).sort((a, b) => {
const getQualityValue = (q) => {
const match = q.match(/(\d+)/);
return match ? parseInt(match[1]) : 0;
};
return getQualityValue(b[0]) - getQualityValue(a[0]);
});
addDebug(`🎚️ ${sortedQualities.length}個の画質オプションを生成`);
// 各画質のボタンを作成
sortedQualities.forEach(([quality, stream], index) => {
const button = document.createElement('button');
button.textContent = `${quality} ${stream.hasAudio ? '🔊' : '🔇'}`;
button.onclick = () => changeQualityByIndex(index);
button.dataset.index = index;
qualityControls.appendChild(button);
});
}
function playBestQuality() {
changeQualityByIndex(0);
}
function changeQualityByIndex(index) {
const audioStreams = availableStreams.filter(s => s.hasAudio);
const sortableStreams = audioStreams.length > 0 ? audioStreams : availableStreams;
// 画質ごとにグループ化
const qualityMap = new Map();
sortableStreams.forEach(stream => {
const quality = String(stream.quality);
if (!qualityMap.has(quality)) {
qualityMap.set(quality, stream);
}
});
// 画質を数値でソート
const sortedQualities = Array.from(qualityMap.entries()).sort((a, b) => {
const getQualityValue = (q) => {
const match = q.match(/(\d+)/);
return match ? parseInt(match[1]) : 0;
};
return getQualityValue(b[0]) - getQualityValue(a[0]);
});
if (index >= sortedQualities.length) {
addDebug('❌ 無効なインデックス');
return;
}
const [quality, selectedStream] = sortedQualities[index];
currentQuality = quality;
// ボタンのアクティブ状態を更新
const qualityControls = document.getElementById('qualityControls');
const buttons = qualityControls.querySelectorAll('button');
buttons.forEach((btn, i) => {
if (i === index) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
playStream(selectedStream);
}
function changeQuality(qualityLevel) {
if (availableStreams.length === 0) return;
addDebug(`\n🎚️ 画質変更: ${qualityLevel}`);
const audioStreams = availableStreams.filter(s => s.hasAudio);
const sortableStreams = audioStreams.length > 0 ? audioStreams : availableStreams;
const sortedStreams = [...sortableStreams].sort((a, b) => {
const getQualityValue = (q) => {
const match = String(q).match(/(\d+)/);
return match ? parseInt(match[1]) : 0;
};
return getQualityValue(b.quality) - getQualityValue(a.quality);
});
let selectedStream;
switch(qualityLevel) {
case 'best':
selectedStream = sortedStreams[0];
break;
case 'high':
selectedStream = sortedStreams.find(s => {
const q = String(s.quality);
return q.includes('720') || q.includes('480');
}) || sortedStreams[Math.floor(sortedStreams.length / 2)];
break;
case 'medium':
selectedStream = sortedStreams.find(s => {
const q = String(s.quality);
return q.includes('360') || q.includes('240');
}) || sortedStreams[sortedStreams.length - 1];
break;
}
if (selectedStream) {
playStream(selectedStream);
}
}
function playStream(stream) {
const player = document.getElementById('player');
const streamUrl = document.getElementById('streamUrl');
if (!stream || !stream.url) {
addDebug('❌ ストリームの選択に失敗');
showError('ストリームの読み込みに失敗しました');
return;
}
addDebug(`✅ 選択: ${stream.quality} (${stream.hasAudio ? '音声あり' : '音声なし'})`);
addDebug(`🔗 URL: ${stream.url.substring(0, 80)}...`);
player.src = stream.url;
player.play().then(() => {
addDebug('▶️ 再生開始');
}).catch(e => {
addDebug(`❌ 再生エラー: ${e.message}`);
showError('ストリームの再生に失敗しました');
});
streamUrl.textContent = `画質: ${stream.quality} | API: ${stream.type} ${stream.hasAudio ? '🔊' : '🔇'}`;
}
async function downloadVideo(type) {
if (!currentVideoId) {
showError('動画が選択されていません');
return;
}
addDebug(`\n📥 ダウンロード開始: ${type}`);
const btnDownloadVideo = document.getElementById('btnDownloadVideo');
const btnDownloadAudio = document.getElementById('btnDownloadAudio');
btnDownloadVideo.disabled = true;
btnDownloadAudio.disabled = true;
btnDownloadVideo.textContent = '⏳ 処理中...';
btnDownloadAudio.textContent = '⏳ 処理中...';
try {
let downloadUrl = null;
if (availableStreams.length > 0) {
addDebug('🔍 取得済みストリームURLを使用');
const stream = availableStreams.find(s => s.hasAudio) || availableStreams[0];
if (stream) {
downloadUrl = stream.url;
addDebug(`✅ ストリームURL使用: ${stream.quality}`);
}
}
if (!downloadUrl) {
addDebug('🔍 新規取得を試行');
const streams = await fetchStreamUrls(currentVideoId);
if (streams.length > 0) {
const stream = streams.find(s => s.hasAudio) || streams[0];
downloadUrl = stream.url;
addDebug(`✅ 新規URL取得: ${stream.quality}`);
}
}
if (downloadUrl) {
addDebug('📥 ダウンロード開始...');
const a = document.createElement('a');
a.href = downloadUrl;
a.download = `${currentVideoId}.${type === 'audio' ? 'mp3' : 'mp4'}`;
a.target = '_blank';
a.rel = 'noopener noreferrer';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
showInfo(`${type === 'video' ? '動画' : '音声'}のダウンロードを開始しました`);
addDebug('✅ ダウンロードリンククリック完了');
} else {
throw new Error('ダウンロードURLの取得に失敗しました');
}
} catch (err) {
addDebug(`❌ ダウンロード失敗: ${err.message}`);
showError('ダウンロードに失敗しました。ストリーム再生をお試しください。');
} finally {
btnDownloadVideo.disabled = false;
btnDownloadAudio.disabled = false;
btnDownloadVideo.textContent = '📥 動画DL';
btnDownloadAudio.textContent = '🎵 音声DL';
}
}
function showError(message) {
const error = document.getElementById('error');
error.textContent = message;
error.style.display = 'block';
setTimeout(() => {
error.style.display = 'none';
}, 5000);
}
function showInfo(message) {
const info = document.getElementById('info');
info.textContent = message;
info.style.display = 'block';
setTimeout(() => {
info.style.display = 'none';
}, 3000);
}
</script>
</body>
</html>