<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>仲良しTube</title>
<style>
:root {
--bg-color: #ffffff;
--text-color: #0f0f0f;
--text-secondary: #606060;
--border-color: #e5e5e5;
--hover-color: #f2f2f2;
--primary-color: #ff0000;
--header-bg: #ffffff;
--search-bg: #ffffff;
--search-border: #cccccc;
--comment-bg: #f2f2f2;
--scrollbar-bg: #cccccc;
--desc-bg: #f2f2f2;
--desc-hover: #e5e5e5;
--chip-bg: #f2f2f2;
--chip-hover: #e5e5e5;
--active-sidebar-bg: #e8e8e8;
font-family: 'Roboto', 'Arial', sans-serif;
}
[data-theme="dark"] {
--bg-color: #0f0f0f;
--text-color: #f1f1f1;
--text-secondary: #aaaaaa;
--border-color: #3f3f3f;
--hover-color: #272727;
--header-bg: #0f0f0f;
--search-bg: #121212;
--search-border: #303030;
--comment-bg: #272727;
--scrollbar-bg: #717171;
--desc-bg: #272727;
--desc-hover: #3f3f3f;
--chip-bg: #272727;
--chip-hover: #3f3f3f;
--active-sidebar-bg: #3f3f3f;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background-color: var(--bg-color); color: var(--text-color); overflow-x: hidden; font-size: 14px;}
a { color: inherit; text-decoration: none; }
button { cursor: pointer; border: none; background: none; color: inherit; font-family: inherit; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: var(--scrollbar-bg); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }

header {
position: fixed; top: 0; width: 100%; height: 56px;
background-color: var(--header-bg); display: flex; align-items: center;
justify-content: space-between; padding: 0 16px; z-index: 100;
border-bottom: 1px solid var(--border-color);
}
.header-left { display: flex; align-items: center; gap: 16px; }
.logo { display: flex; align-items: center; gap: 4px; font-size: 20px; font-weight: bold; cursor: pointer; letter-spacing: -1px; }
.logo svg { fill: var(--primary-color); width: 30px; height: 30px; }
.header-center { flex: 1; max-width: 720px; display: flex; justify-content: center; margin: 0 40px; }
.search-bar { display: flex; width: 100%; max-width: 600px; height: 40px; border: 1px solid var(--search-border); border-radius: 20px; overflow: hidden; background-color: var(--search-bg); box-shadow: inset 0 1px 2px rgba(0,0,0,0.01); }
.search-bar input { flex: 1; padding: 0 16px 0 24px; border: none; background: transparent; color: var(--text-color); outline: none; font-size: 16px; }
.search-bar button { width: 64px; background-color: var(--chip-bg); border-left: 1px solid var(--search-border); display: flex; align-items: center; justify-content: center; }
.search-bar button:hover { background-color: var(--chip-hover); }
.header-right { display: flex; align-items: center; gap: 8px; }
.icon-btn { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s; }
.icon-btn:hover { background-color: var(--hover-color); }
.icon-btn svg { width: 24px; height: 24px; fill: currentColor; }

.categories-bar {
position: fixed; top: 56px; left: 72px; right: 0; height: 56px;
background-color: var(--header-bg); z-index: 98; display: flex;
align-items: center; gap: 12px; padding: 0 24px; overflow-x: auto;
transition: left 0.3s ease; border-bottom: 1px solid var(--border-color);
}
.categories-bar::-webkit-scrollbar { display: none; }
.sidebar.open ~ .categories-bar { left: 240px; }
.category-chip {
background: var(--chip-bg); padding: 6px 12px; border-radius: 8px;
font-size: 14px; font-weight: 500; white-space: nowrap; cursor: pointer; transition: 0.2s;
}
.category-chip:hover { background: var(--chip-hover); }
.category-chip.active { background: var(--text-color); color: var(--bg-color); }

.sidebar {
position: fixed; top: 56px; left: 0; bottom: 0; width: 72px;
background-color: var(--header-bg); padding: 8px 0; z-index: 99;
transition: width 0.3s ease; overflow-x: hidden; overflow-y: auto;
}
.sidebar.open { width: 240px; }
.sidebar-item {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 4px; padding: 14px 0; cursor: pointer; font-size: 10px; font-weight: 500;
border-radius: 10px; margin: 2px 4px; transition: background 0.15s;
color: var(--text-secondary); position: relative; white-space: nowrap; overflow: hidden;
}
.sidebar.open .sidebar-item {
flex-direction: row; gap: 18px; padding: 10px 16px; font-size: 14px;
margin: 2px 8px; justify-content: flex-start;
}
.sidebar-item:hover { background-color: var(--hover-color); color: var(--text-color); }
.sidebar-item.active { background-color: var(--active-sidebar-bg); color: var(--text-color); font-weight: 700; }
.sidebar-item.active::before {
content: ''; position: absolute; left: 0; top: 20%; bottom: 20%;
width: 3px; background: var(--primary-color); border-radius: 0 2px 2px 0;
}
.sidebar.open .sidebar-item.active::before { display: none; }
.sidebar-item svg {
width: 24px; height: 24px; fill: none; stroke: currentColor;
stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0;
}
.sidebar-item.active svg { fill: currentColor; stroke: none; }
.logo svg, .icon-btn svg, .header-right svg { fill: var(--primary-color) !important; stroke: none !important; }
.sidebar-item .label { font-size: 10px; line-height: 1; }
.sidebar.open .sidebar-item .label { font-size: 14px; }
.sidebar-divider { height: 1px; background-color: var(--border-color); margin: 8px 0; display: none; }
.sidebar.open .sidebar-divider { display: block; }
.sidebar-section-title { display: none; font-size: 16px; font-weight: 700; padding: 8px 28px; color: var(--text-color); }
.sidebar.open .sidebar-section-title { display: block; }
.sidebar-channel-item {
display: none; flex-direction: row; gap: 12px; padding: 8px 16px; font-size: 13px; margin: 0 8px;
cursor: pointer; border-radius: 10px; position: relative; align-items: center;
}
.sidebar.open .sidebar-channel-item { display: flex; }
.sidebar-channel-item:hover { background-color: var(--hover-color); }
.sidebar-channel-item img { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.sidebar-channel-item .channel-name-sidebar { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.live-badge { background: var(--primary-color); color: #fff; font-size: 9px; font-weight: bold; padding: 1px 4px; border-radius: 3px; flex-shrink: 0; }
#sidebar-sub-divider, #sidebar-sub-title { display: none; }
.sidebar.open #sidebar-sub-divider, .sidebar.open #sidebar-sub-title { display: block; }

.subs-page-header { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; }
.subs-page-header h2 { font-size: 22px; font-weight: bold; }
#subs-grid .sub-channel-card {
display: flex; align-items: center; gap: 16px; padding: 14px; border-radius: 12px;
cursor: pointer; border: 1px solid var(--border-color); transition: background 0.15s; margin-bottom: 10px;
}
#subs-grid .sub-channel-card:hover { background: var(--hover-color); }
#subs-grid .sub-channel-card img { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
#subs-grid .sub-channel-info { flex: 1; }
#subs-grid .sub-channel-name { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
#subs-grid .sub-channel-meta { font-size: 12px; color: var(--text-secondary); }
#subs-grid .unsub-btn { padding: 7px 16px; border-radius: 20px; background: var(--chip-bg); font-size: 13px; font-weight: 500; }
#subs-grid .unsub-btn:hover { background: var(--chip-hover); }

main { margin-top: 112px; margin-left: 72px; padding: 24px; min-height: calc(100vh - 112px); transition: margin-left 0.3s; }
main.sidebar-open { margin-left: 240px; }
.view { display: none; }
.view.active { display: block; }

.video-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; row-gap: 40px; }
@media (max-width: 1100px) { .video-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 760px) { .video-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .video-grid { grid-template-columns: 1fr; } }

.video-card { cursor: pointer; display: flex; flex-direction: column; gap: 12px; }
.thumbnail-container { width: 100%; aspect-ratio: 16 / 9; background-color: var(--hover-color); border-radius: 12px; overflow: hidden; position: relative; }
.thumbnail-container img { width: 100%; height: 100%; object-fit: cover; transition: 0.2s;}
.video-card:hover .thumbnail-container img { transform: scale(1.05); }
.video-info { display: flex; gap: 12px; }
.channel-avatar { width: 36px; height: 36px; border-radius: 50%; background-color: var(--hover-color); flex-shrink: 0; overflow: hidden; }
.channel-avatar img { width: 100%; height: 100%; object-fit: cover; }
.video-details { display: flex; flex-direction: column; overflow: hidden; flex: 1; }
.video-title { font-size: 14px; font-weight: 500; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.4; margin-bottom: 4px;}
.video-meta { font-size: 12px; color: var(--text-secondary); display: flex; flex-direction: column; gap: 2px; }
.duration-badge {
position: absolute; bottom: 6px; right: 6px;
background: rgba(0,0,0,0.82); color: #fff;
font-size: 12px; font-weight: 600; padding: 2px 5px; border-radius: 4px; pointer-events: none;
}
.live-thumb-badge { position: absolute; bottom: 8px; left: 8px; background: var(--primary-color); color: #fff; font-size: 11px; font-weight: bold; padding: 2px 6px; border-radius: 4px; }
.archived-badge { position: absolute; bottom: 8px; left: 8px; background: rgba(0,0,0,0.75); color: #ccc; font-size: 10px; font-weight: bold; padding: 2px 6px; border-radius: 4px; }

.recommend-section { margin-bottom: 32px; }
.recommend-title { display: flex; align-items: center; gap: 8px; font-size: 18px; font-weight: bold; margin-bottom: 16px; }
.recommend-title svg { width: 22px; height: 22px; fill: var(--primary-color); stroke: none; }

.search-results-list { display: flex; flex-direction: column; gap: 12px; }
.search-result-item {
display: flex; gap: 16px; cursor: pointer; padding: 4px; border-radius: 8px; transition: background 0.15s;
}
.search-result-item:hover { background: var(--hover-color); }
.search-result-thumb {
flex-shrink: 0; width: 360px; aspect-ratio: 16/9; border-radius: 10px;
overflow: hidden; background: var(--hover-color); position: relative;
}
.search-result-thumb img { width: 100%; height: 100%; object-fit: cover; }
.search-result-info { flex: 1; display: flex; flex-direction: column; gap: 8px; padding: 4px 0; }
.search-result-title { font-size: 18px; font-weight: 400; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.search-result-meta { font-size: 13px; color: var(--text-secondary); display: flex; align-items: center; gap: 8px; }
.search-result-channel { display: flex; align-items: center; gap: 8px; margin-top: 4px; cursor: pointer; }
.search-result-channel-icon { width: 24px; height: 24px; border-radius: 50%; overflow: hidden; flex-shrink: 0; }
.search-result-channel-icon img { width: 100%; height: 100%; object-fit: cover; }
.search-result-channel-name { font-size: 13px; color: var(--text-secondary); }
.search-result-channel-name:hover { color: var(--text-color); }
@media (max-width: 900px) { .search-result-thumb { width: 240px; } .search-result-title { font-size: 15px; } }
@media (max-width: 600px) { .search-result-item { flex-direction: column; } .search-result-thumb { width: 100%; } }

.search-shorts-section { margin-bottom: 32px; padding-bottom: 24px; border-bottom: 1px solid var(--border-color); }
.search-shorts-title { display: flex; align-items: center; gap: 8px; font-size: 18px; font-weight: bold; margin-bottom: 16px; }
.search-shorts-title svg { fill: #ff0000; stroke: none; width: 22px; height: 22px; }
.search-shorts-scroll { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: none; }
.search-shorts-scroll::-webkit-scrollbar { display: none; }

.home-shorts-section { margin-bottom: 40px; border-bottom: 4px solid var(--border-color); padding-bottom: 20px; }
.home-shorts-title { display: flex; align-items: center; gap: 8px; font-size: 18px; font-weight: bold; margin-bottom: 16px; }
.home-shorts-title svg { fill: #ff0000; stroke: none; width: 24px; height: 24px; }
.home-shorts-scroll { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: none; }
.home-shorts-scroll::-webkit-scrollbar { display: none; }
.home-short-card { flex: 0 0 160px; cursor: pointer; }
.home-short-thumb { width: 100%; aspect-ratio: 9/16; border-radius: 12px; background: #000; overflow: hidden; margin-bottom: 8px; position: relative; }
.home-short-thumb img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.2s; }
.home-short-card:hover .home-short-thumb img { transform: scale(1.04); }
.home-short-title { font-size: 13px; font-weight: 500; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

.channel-banner-wrap { width: 100%; border-radius: 12px; overflow: hidden; margin-bottom: 0; background: #000; }
.channel-banner-img { width: 100%; height: 180px; object-fit: cover; display: block; }
.channel-banner-placeholder { width: 100%; height: 180px; display: block; }
.channel-header-row { display: flex; align-items: flex-start; gap: 24px; padding: 16px 0 16px 0; }
.channel-header-icon-wrap { position: relative; flex-shrink: 0; }
.channel-header-icon { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; background: var(--hover-color); }
.channel-header-info { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.channel-header-title { font-size: 24px; font-weight: 700; line-height: 1.2; }
.channel-header-handle { font-size: 14px; color: var(--text-secondary); margin-top: 2px; }
.channel-header-meta { font-size: 13px; color: var(--text-secondary); }
.channel-action-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
.subscribe-btn {
background-color: var(--text-color); color: var(--bg-color);
padding: 10px 16px; border-radius: 20px; font-weight: 500; font-size: 14px;
transition: opacity 0.2s; cursor: pointer;
}
.subscribe-btn:hover { opacity: 0.85; }
.subscribe-btn.subscribed { background-color: var(--hover-color); color: var(--text-color); border: 1px solid var(--border-color); }
.channel-bell-btn { width: 40px; height: 40px; border-radius: 50%; background: var(--hover-color); display: flex; align-items: center; justify-content: center; cursor: pointer; }
.channel-bell-btn svg { width: 20px; height: 20px; fill: currentColor; stroke: none; }
.channel-join-btn { background: none; border: 1px solid var(--border-color); padding: 10px 16px; border-radius: 20px; font-size: 14px; font-weight: 500; color: var(--text-color); cursor: pointer; }
.channel-join-btn:hover { background: var(--hover-color); }
.channel-tabs-bar {
display: flex; gap: 0; border-bottom: 1px solid var(--border-color);
margin-bottom: 24px; padding: 0; overflow-x: auto; scrollbar-width: none;
position: sticky; top: 56px; background: var(--bg-color); z-index: 10;
}
.channel-tabs-bar::-webkit-scrollbar { display: none; }
.channel-tab {
padding: 12px 16px; font-size: 14px; font-weight: 500; cursor: pointer;
border: none; background: none; color: var(--text-secondary);
border-bottom: 3px solid transparent; transition: color 0.1s;
white-space: nowrap; position: relative; bottom: -1px;
}
.channel-tab:hover { color: var(--text-color); background: var(--hover-color); }
.channel-tab.active { color: var(--text-color); border-bottom: 3px solid var(--text-color); font-weight: 600; }
.channel-featured { display: flex; flex-direction: column; gap: 24px; }
.channel-featured-video { display: flex; gap: 16px; padding: 16px; background: var(--hover-color); border-radius: 12px; cursor: pointer; }
.channel-featured-thumb { width: 280px; flex-shrink: 0; aspect-ratio: 16/9; border-radius: 8px; overflow: hidden; }
.channel-featured-thumb img { width: 100%; height: 100%; object-fit: cover; }
.channel-featured-info { flex: 1; display: flex; flex-direction: column; gap: 8px; }
.channel-featured-title { font-size: 18px; font-weight: 600; }
.channel-filter-bar { display: flex; gap: 8px; margin-bottom: 20px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.channel-filter-bar::-webkit-scrollbar { display: none; }
.channel-filter-chip { padding: 6px 12px; border-radius: 8px; background: var(--chip-bg); font-size: 14px; font-weight: 500; white-space: nowrap; cursor: pointer; border: none; color: var(--text-color); }
.channel-filter-chip.active { background: var(--text-color); color: var(--bg-color); }
.channel-filter-chip:hover { background: var(--chip-hover); }

.channel-shorts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
.channel-short-card { cursor: pointer; }
.channel-short-thumb { width: 100%; aspect-ratio: 9/16; border-radius: 12px; background: #000; overflow: hidden; margin-bottom: 8px; position: relative; }
.channel-short-thumb img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.2s; }
.channel-short-card:hover .channel-short-thumb img { transform: scale(1.04); }
.channel-short-title { font-size: 13px; font-weight: 500; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

.shorts-full-page {
position: fixed; top: 56px; left: 72px; right: 0; bottom: 0;
background: var(--bg-color); overflow-y: scroll; scroll-snap-type: y mandatory; scrollbar-width: none;
}
.shorts-full-page::-webkit-scrollbar { display: none; }
main.sidebar-open .shorts-full-page { left: 240px; }
.short-snap-item {
width: 100%; height: calc(100vh - 56px);
display: flex; align-items: center; justify-content: center;
scroll-snap-align: start; position: relative; background: var(--bg-color);
}
.short-center { display: flex; align-items: flex-end; gap: 16px; height: 100%; max-height: 900px; padding: 12px 0; }
.short-video-wrap {
position: relative; height: 100%;
aspect-ratio: 9/16; background: #000; border-radius: 12px; overflow: hidden; flex-shrink: 0;
}
.short-video-wrap iframe { width: 100%; height: 100%; border: none; display: block; }
.short-video-wrap video { width: 100%; height: 100%; display: block; background: #000; }
.short-side-actions { display: flex; flex-direction: column; gap: 20px; padding-bottom: 40px; color: var(--text-color); }
.short-action-btn { display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; }
.short-action-btn .icon { width: 48px; height: 48px; border-radius: 50%; background: var(--hover-color); display: flex; align-items: center; justify-content: center; }
.short-action-btn svg { width: 24px; height: 24px; fill: var(--text-color); stroke: none; }
.short-action-btn span { font-size: 12px; font-weight: 500; color: var(--text-color); }
.short-overlay-bottom {
position: absolute; bottom: 0; left: 0; right: 0; padding: 60px 16px 16px;
background: linear-gradient(transparent, rgba(0,0,0,0.75));
color: #fff; pointer-events: none; border-radius: 0 0 12px 12px;
}
.short-overlay-bottom .short-channel { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; pointer-events: all; cursor: pointer; flex-wrap: wrap; }
.short-overlay-bottom .short-channel img { width: 40px; height: 40px; border-radius: 50%; border: 2px solid #fff; flex-shrink:0; }
.short-overlay-bottom .short-channel-name { font-weight: bold; font-size: 15px; color: #fff; flex:1; }
.short-overlay-bottom .short-title { font-size: 14px; line-height: 1.4; pointer-events: all; color: #fff; margin-bottom:8px; }
.short-sub-btn { background: #fff; color: #000; border: none; border-radius: 20px; padding: 4px 12px; font-size: 12px; font-weight: 700; cursor: pointer; pointer-events: all; flex-shrink:0; }
.short-sub-btn.subscribed { background: rgba(255,255,255,0.3); color: #fff; border: 1px solid #fff; }
.short-avatar-btn { background: none !important; overflow: visible !important; }
.short-action-btn .icon { background: rgba(255,255,255,0.15) !important; backdrop-filter: blur(4px); }
[data-theme="dark"] .short-action-btn .icon { background: rgba(255,255,255,0.1) !important; }
/* ショートストリームセレクタ: 3ボタン */
.short-stream-selector {
position: absolute; top: 12px; right: 12px;
display: flex; background: rgba(0,0,0,0.6); backdrop-filter: blur(6px);
border-radius: 20px; padding: 3px; gap: 2px; z-index: 10;
}
.short-stream-btn { padding: 5px 10px; border-radius: 16px; font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.7); transition: 0.2s; cursor: pointer; border: none; background: none; }
.short-stream-btn.active { background: rgba(255,255,255,0.25); color: #fff; }
.short-gv-loading {
position: absolute; inset: 0; background: #000;
display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 12px;
border-radius: 12px; z-index: 5;
}
.short-gv-loading .spinner {
width: 36px; height: 36px; border: 3px solid rgba(255,255,255,0.2);
border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite;
}
.short-gv-loading span { color: rgba(255,255,255,0.8); font-size: 13px; }
@keyframes spin { to { transform: rotate(360deg); } }

.watch-layout { display: flex; gap: 24px; max-width: 1500px; margin: 0 auto; flex-wrap: wrap; }
.watch-main { flex: 1; min-width: 65%; }
.watch-sidebar { width: 400px; flex-shrink: 0; }
.player-container { width: 100%; aspect-ratio: 16 / 9; background-color: black; border-radius: 12px; overflow: hidden; position: relative; display: flex; align-items: center; justify-content: center; }
.player-container iframe, .player-container video { width: 100%; height: 100%; border: none; outline: none; }
.watch-title { font-size: 20px; font-weight: bold; margin: 12px 0; }
.watch-actions { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 16px; }
.watch-channel-info { display: flex; align-items: center; gap: 12px; cursor: pointer; }
.watch-channel-name { font-weight: bold; font-size: 16px; }
.watch-channel-subs { font-size: 12px; color: var(--text-secondary); }
.stream-selector { display: flex; background: var(--hover-color); border-radius: 20px; padding: 4px; flex-wrap: wrap; gap: 2px; }
.stream-btn { padding: 8px 16px; border-radius: 16px; font-size: 14px; font-weight: 500; }
.stream-btn.active { background: var(--text-color); color: var(--bg-color); }
.action-buttons { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.action-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; background-color: var(--hover-color); border-radius: 18px; font-size: 14px; font-weight: 500; }

.quality-wrap { position: relative; }
.quality-panel {
display: none; position: absolute; bottom: 48px; right: 0;
background: var(--header-bg); border: 1px solid var(--border-color);
border-radius: 12px; padding: 8px; min-width: 200px; z-index: 20;
box-shadow: 0 4px 20px rgba(0,0,0,0.18);
}
.quality-panel.open { display: block; }
.quality-panel-title { padding: 6px 16px 4px; font-size: 11px; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; }
.quality-option {
padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 14px;
display: flex; align-items: center; gap: 8px; transition: background 0.1s;
}
.quality-option:hover { background: var(--hover-color); }
.quality-option.active { font-weight: bold; color: var(--primary-color); }
.quality-option .q-badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: rgba(255,0,0,0.12); color: var(--primary-color); font-weight: 700; }

.download-panel {
display: none; position: absolute; bottom: 48px; right: 0;
background: var(--header-bg); border: 1px solid var(--border-color);
border-radius: 12px; padding: 8px; min-width: 220px; z-index: 20;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.download-panel.open { display: block; }
.download-option {
padding: 10px 16px; border-radius: 8px; cursor: pointer; font-size: 14px;
display: flex; align-items: center; gap: 10px; transition: background 0.15s;
}
.download-option:hover { background: var(--hover-color); }
.download-option svg { width: 18px; height: 18px; fill: currentColor; stroke: none; flex-shrink: 0; }
.download-option .dl-label { display: flex; flex-direction: column; }
.download-option .dl-title { font-weight: 500; }
.download-option .dl-desc { font-size: 11px; color: var(--text-secondary); }
.download-wrap { position: relative; }

.dl-toast {
position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
background: #323232; color: #fff; padding: 12px 24px; border-radius: 24px;
font-size: 14px; z-index: 9999; display: none; gap: 12px; align-items: center;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.dl-toast.show { display: flex; }
.dl-toast-bar { width: 160px; height: 4px; background: rgba(255,255,255,0.3); border-radius: 2px; overflow: hidden; }
.dl-toast-fill { height: 100%; background: #4caf50; border-radius: 2px; transition: width 0.3s; width: 0%; }

.video-description { background-color: var(--desc-bg); border-radius: 12px; padding: 12px; margin-bottom: 24px; font-size: 14px; cursor: pointer; }
.video-description:hover { background-color: var(--desc-hover); }
.desc-content { overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; line-height: 1.5; }
.desc-content.expanded { display: block; -webkit-line-clamp: unset; }
.related-video { display: flex; gap: 8px; margin-bottom: 12px; cursor: pointer; padding: 4px; border-radius: 8px; }
.related-video:hover { background-color: var(--hover-color); }
.related-thumb { width: 168px; height: 94px; flex-shrink: 0; border-radius: 8px; background: var(--hover-color); overflow: hidden; position: relative; }
.related-thumb img { width: 100%; height: 100%; object-fit: cover; }
.related-info { flex: 1; }
.related-title { font-size: 14px; font-weight: 500; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 4px; }
.comment { display: flex; gap: 12px; margin-bottom: 16px; }
.comment-avatar { width: 40px; height: 40px; border-radius: 50%; background: #eee; flex-shrink: 0; }
.comment-body { flex: 1; }

.loader { text-align: center; padding: 40px; color: var(--text-secondary); font-weight: bold; width: 100%; }
.hidden { display: none !important; }
#hidden-cse-container { display: none; background: white; padding: 20px; box-shadow: 0 0 20px rgba(0,0,0,0.5); border-radius: 10px; }

.welcome-container { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: calc(100vh - 150px); max-width: 800px; margin: 0 auto; text-align: center; }
.welcome-logo { font-size: 48px; font-weight: bold; margin-bottom: 40px; letter-spacing: -2px; display: flex; align-items: center; gap: 8px; }
.welcome-logo svg { fill: var(--primary-color); stroke: none; width: 60px; height: 60px; }
.welcome-search { width: 100%; height: 56px; border: 1px solid var(--search-border); border-radius: 28px; display: flex; overflow: hidden; background-color: var(--search-bg); box-shadow: 0 2px 6px rgba(0,0,0,0.05); margin-bottom: 48px; }
.welcome-search input { flex: 1; padding: 0 24px; border: none; background: transparent; color: var(--text-color); font-size: 18px; outline: none; }
.welcome-search button { width: 80px; background-color: var(--chip-bg); border-left: 1px solid var(--search-border); display: flex; align-items: center; justify-content: center; }

.settings-panel { width: 100%; background: var(--header-bg); border: 1px solid var(--border-color); border-radius: 16px; padding: 32px; text-align: left; }
.settings-section { margin-bottom: 28px; padding-bottom: 24px; border-bottom: 1px solid var(--border-color); }
.settings-section:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
.settings-section-title { font-size: 13px; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 16px; }
.setting-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; gap: 12px; }
.setting-item:last-child { margin-bottom: 0; }
.setting-info { display: flex; flex-direction: column; flex: 1; }
.setting-title { font-size: 15px; font-weight: 500; margin-bottom: 3px; }
.setting-desc { font-size: 12px; color: var(--text-secondary); line-height: 1.4; }
.toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; }
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: var(--primary-color); }
input:checked + .slider:before { transform: translateX(20px); }
.select-modern { padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border-color); background-color: var(--bg-color); color: var(--text-color); font-size: 14px; outline: none; min-width: 160px; }
.start-btn { background-color: var(--primary-color); color: white; padding: 12px 32px; border-radius: 24px; font-size: 16px; font-weight: bold; border: none; cursor: pointer; }
.subs-empty { text-align: center; padding: 40px; color: var(--text-secondary); }

/* ストリームオプション: 3列グリッド */
.stream-group { display: flex; flex-direction: column; gap: 8px; }
.stream-group-label { font-size: 11px; color: var(--text-secondary); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 8px; }
.stream-options-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.stream-option-btn {
padding: 10px 12px; border-radius: 10px; border: 2px solid var(--border-color);
background: var(--bg-color); color: var(--text-color); font-size: 13px; font-weight: 600;
cursor: pointer; transition: all 0.15s; display: flex; flex-direction: column; gap: 3px; align-items: flex-start;
}
.stream-option-btn:hover { border-color: var(--text-secondary); background: var(--hover-color); }
.stream-option-btn.selected { border-color: var(--primary-color); background: rgba(255,0,0,0.06); color: var(--primary-color); }
.stream-option-btn .s-label { font-size: 11px; font-weight: 400; color: var(--text-secondary); }
.stream-option-btn.selected .s-label { color: rgba(255,0,0,0.7); }

@media (max-width: 1100px) { .watch-sidebar { width: 100%; } }
@media (max-width: 768px) {
.sidebar { transform: translateX(-100%); width: 240px; }
.sidebar.open { transform: translateX(0); }
.sidebar-item { flex-direction: row; gap: 24px; padding: 10px 24px; font-size: 14px; margin: 0 12px; }
main { margin-left: 0; } main.sidebar-open { margin-left: 0; }
.categories-bar { left: 0; } .sidebar.open ~ .categories-bar { left: 0; }
.shorts-full-page { left: 0; }
.channel-featured-video { flex-direction: column; }
.channel-featured-thumb { width: 100%; }
.channel-header-icon { width: 64px; height: 64px; }
.channel-header-title { font-size: 18px; }
.channel-banner-img, .channel-banner-placeholder { height: 100px; }
}

.share-panel {
display: none; position: fixed; inset: 0; z-index: 9999;
align-items: center; justify-content: center; background: rgba(0,0,0,0.5);
}
.share-panel.open { display: flex; }
.share-panel-box {
background: var(--header-bg); border-radius: 16px; padding: 24px;
width: 90%; max-width: 400px; box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.share-panel-title { font-size: 18px; font-weight: bold; margin-bottom: 16px; }
.share-url-row { display: flex; gap: 8px; margin-bottom: 16px; }
.share-url-row input { flex: 1; padding: 10px 14px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-color); color: var(--text-color); font-size: 14px; outline: none; }
.share-copy-btn { padding: 10px 16px; background: var(--primary-color); color: #fff; border-radius: 8px; font-weight: 600; font-size: 14px; }
.share-close-btn { width: 100%; padding: 10px; border-radius: 8px; background: var(--hover-color); font-size: 14px; font-weight: 500; margin-top: 8px; }

/* Manifest Hunterステータス表示 */
.manifest-status {
position: absolute; top: 8px; left: 8px;
background: rgba(0,0,0,0.7); color: #0f0;
font-size: 10px; font-family: monospace; padding: 4px 8px;
border-radius: 4px; z-index: 10; max-width: 200px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
</style>
</head>
<body>
<div id="hidden-cse-container"></div>
<div class="dl-toast" id="dl-toast">
<span id="dl-toast-msg">ダウンロード準備中...</span>
<div class="dl-toast-bar"><div class="dl-toast-fill" id="dl-toast-fill"></div></div>
</div>

<div class="share-panel" id="share-panel" onclick="if(event.target===this)closeSharePanel()">
<div class="share-panel-box">
<div class="share-panel-title">🔗 共有</div>
<div class="share-url-row">
<input type="text" id="share-url-input" readonly>
<button class="share-copy-btn" onclick="copyShareUrl()">コピー</button>
</div>
<button class="share-close-btn" onclick="closeSharePanel()">閉じる</button>
</div>
</div>

<header>
<div class="header-left">
<button class="icon-btn" onclick="toggleSidebar()">
<svg viewBox="0 0 24 24" style="fill:currentColor;stroke:none;"><path d="M21,6H3V5h18V6z M21,11H3v1h18V11z M21,17H3v1h18V17z"></path></svg>
</button>
<div class="logo" onclick="navigate('home')">
<svg viewBox="0 0 24 24"><path d="M21.58,7.19C21.34,6.31,20.69,5.66,19.81,5.42C18.25,5,12,5,12,5s-6.25,0-7.81,0.42c-0.88,0.24-1.53,0.89-1.77,1.77 C2,8.75,2,12,2,12s0,3.25,0.42,4.81c0.24,0.88,0.89,1.53,1.77,1.77C5.75,19,12,19,12,19s6.25,0,7.81-0.42 c0.88-0.24,1.53-0.89,1.77-1.77C22,15.25,22,12,22,12S22,8.75,21.58,7.19z M10,15.5v-7l6,3.5L10,15.5z"></path></svg>
仲良しTube
</div>
</div>
<div class="header-center">
<form class="search-bar" onsubmit="handleSearch(event)">
<input type="text" id="search-input" placeholder="検索" autocomplete="off">
<button type="submit">
<svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:currentColor;stroke:none;"><path d="M20.87,20.17l-5.59-5.59C16.35,13.35,17,11.75,17,10c0-3.87-3.13-7-7-7s-7,3.13-7,7s3.13,7,7,7c1.75,0,3.35-0.65,4.58-1.71 l5.59,5.59L20.87,20.17z M10,16c-3.31,0-6-2.69-6-6s2.69-6,6-6s6,2.69,6,6S13.31,16,10,16z"></path></svg>
</button>
</form>
</div>
<div class="header-right">
<button class="icon-btn" onclick="toggleTheme()" title="テーマ切り替え">
<svg viewBox="0 0 24 24" style="fill:currentColor;stroke:none;"><path d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"></path></svg>
</button>
</div>
</header>

<div class="categories-bar" id="categories-bar">
<button class="category-chip active" onclick="handleCategorySearch('すべて', this)">すべて</button>
<button class="category-chip" onclick="handleCategorySearch('音楽', this)">音楽</button>
<button class="category-chip" onclick="handleCategorySearch('ゲーム', this)">ゲーム</button>
<button class="category-chip" onclick="handleCategorySearch('ニュース', this)">ニュース</button>
<button class="category-chip" onclick="handleCategorySearch('ライブ', this)">ライブ</button>
<button class="category-chip" onclick="handleCategorySearch('アニメ', this)">アニメ</button>
<button class="category-chip" onclick="handleCategorySearch('料理', this)">料理</button>
</div>

<div class="sidebar" id="sidebar">
<div class="sidebar-item" id="nav-home" onclick="navigate('home')">
<svg viewBox="0 0 24 24"><path d="M12,3L4,9v12h5v-7h6v7h5V9L12,3z"/></svg>
<span class="label">ホーム</span>
</div>
<div class="sidebar-item" id="nav-shorts" onclick="navigate('shorts')">
<svg viewBox="0 0 24 24"><path d="M17.77,10.32l-1.2-.5L18,9.06c1.84-.96,2.53-3.23,1.56-5.06s-3.24-2.53-5.07-1.56L6,6.94c-1.29.68-2.07,2.04-2,3.49.07,1.42.93,2.67,2.22,3.25.03.01,1.2.5,1.2.5L6,14.94c-1.84.96-2.53,3.23-1.56,5.06s3.24,2.53,5.07,1.56l8.5-4.5c1.29-.68,2.06-2.04,1.99-3.49C19.93,12.16,19.08,10.91,17.77,10.32Z"/><path d="M10,14.65v-5.3L15,12Z" style="fill:currentColor;stroke:none;"/></svg>
<span class="label">ショート</span>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-item" id="nav-subscriptions" onclick="navigate('subscriptions')">
<svg viewBox="0 0 24 24"><path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/></svg>
<span class="label">登録CH</span>
</div>
<div class="sidebar-item" id="nav-history" onclick="navigate('history')">
<svg viewBox="0 0 24 24"><path d="M14.97,16.95L10,13.87V7h2v5.76l4.03,2.49L14.97,16.95z M12,3c-4.96,0-9,4.04-9,9s4.04,9,9,9s9-4.04,9-9S16.96,3,12,3"/></svg>
<span class="label">履歴</span>
</div>
<div class="sidebar-divider" id="sidebar-sub-divider"></div>
<div class="sidebar-section-title" id="sidebar-sub-title">登録チャンネル</div>
<div id="sidebar-subscriptions"></div>
<div class="sidebar-divider"></div>
<div class="sidebar-item" id="nav-settings" onclick="navigate('settings')">
<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
<span class="label">設定</span>
</div>
</div>

<main id="main-content">

<!-- ウェルカム -->
<div id="view-welcome" class="view">
<div class="welcome-container">
<div class="welcome-logo">
<svg viewBox="0 0 24 24"><path d="M21.58,7.19C21.34,6.31,20.69,5.66,19.81,5.42C18.25,5,12,5,12,5s-6.25,0-7.81,0.42c-0.88,0.24-1.53,0.89-1.77,1.77 C2,8.75,2,12,2,12s0,3.25,0.42,4.81c0.24,0.88,0.89,1.53,1.77,1.77C5.75,19,12,19,12,19s6.25,0,7.81-0.42 c0.88-0.24,1.53-0.89,1.77-1.77C22,15.25,22,12,22,12S22,8.75,21.58,7.19z M10,15.5v-7l6,3.5L10,15.5z"></path></svg>
仲良しTube
</div>
<form class="welcome-search" onsubmit="handleWelcomeSearch(event)">
<input type="text" id="welcome-search-input" placeholder="何を学びますか?" autocomplete="off">
<button type="submit">
<svg viewBox="0 0 24 24" style="width:24px;height:24px;fill:currentColor;stroke:none;"><path d="M20.87,20.17l-5.59-5.59C16.35,13.35,17,11.75,17,10c0-3.87-3.13-7-7-7s-7,3.13-7,7s3.13,7,7,7c1.75,0,3.35-0.65,4.58-1.71 l5.59,5.59L20.87,20.17z M10,16c-3.31,0-6-2.69-6-6s2.69-6,6-6s6,2.69,6,6S13.31,16,10,16z"></path></svg>
</button>
</form>
<div class="settings-panel">
<div style="font-size:18px; font-weight:bold; margin-bottom:24px;">⚙️ 初期設定</div>
<div class="settings-section">
<div class="settings-section-title">ネットワーク</div>
<div class="setting-item">
<div class="setting-info"><div class="setting-title">CORSプロキシを利用する</div><div class="setting-desc">外部APIへの通信時にCORSプロキシを経由します(推奨)</div></div>
<label class="toggle-switch"><input type="checkbox" id="setting-proxy" checked onchange="saveSettings()"><span class="slider"></span></label>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">動画ストリーム(ウォッチページ)</div>
<!-- 3ボタン: m3u8削除 -->
<div class="stream-options-grid" id="welcome-stream-options">
<button class="stream-option-btn selected" data-val="1" onclick="selectWelcomeStream(1)">
Nocookie<span class="s-label">標準・安定</span>
</button>
<button class="stream-option-btn" data-val="2" onclick="selectWelcomeStream(2)">
Edu<span class="s-label">教育版・Premium</span>
</button>
<button class="stream-option-btn" data-val="3" onclick="selectWelcomeStream(3)">
Manifest<span class="s-label">直接ストリーム</span>
</button>
</div>
<input type="hidden" id="setting-stream" value="1">
</div>
<div class="settings-section">
<div class="settings-section-title">ショートストリーム</div>
<div class="stream-options-grid" id="welcome-short-stream-options">
<button class="stream-option-btn selected" data-val="1" onclick="selectWelcomeShortStream(1)">
Nocookie<span class="s-label">標準</span>
</button>
<button class="stream-option-btn" data-val="2" onclick="selectWelcomeShortStream(2)">
Edu<span class="s-label">教育版</span>
</button>
<button class="stream-option-btn" data-val="3" onclick="selectWelcomeShortStream(3)">
Manifest<span class="s-label">直接再生</span>
</button>
</div>
<input type="hidden" id="setting-short-stream" value="1">
</div>
<div class="settings-section">
<div class="settings-section-title">表示</div>
<div class="setting-item">
<div class="setting-info"><div class="setting-title">ホームにトレンド動画を表示</div></div>
<label class="toggle-switch"><input type="checkbox" id="setting-trend" checked onchange="saveSettings()"><span class="slider"></span></label>
</div>
<div class="setting-item">
<div class="setting-info"><div class="setting-title">ダークモード</div></div>
<label class="toggle-switch"><input type="checkbox" id="setting-theme" onchange="toggleThemeFromSettings()"><span class="slider"></span></label>
</div>
</div>
<div style="text-align: center; margin-top: 20px;">
<button class="start-btn" onclick="finishSetup()">仲良しTubeを始める</button>
</div>
</div>
</div>
</div>

<!-- ホーム -->
<div id="view-home" class="view">
<div id="home-recommend" class="recommend-section hidden">
<div class="recommend-title">
<svg viewBox="0 0 24 24" style="fill:var(--primary-color);stroke:none;"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
あなたへのおすすめ
</div>
<div class="video-grid" id="home-recommend-grid"></div>
</div>
<div id="home-shorts" class="home-shorts-section hidden">
<div class="home-shorts-title">
<svg viewBox="0 0 24 24" style="fill:#ff0000;stroke:none;"><path d="M17.77,10.32l-1.2-.5L18,9.06c1.84-.96,2.53-3.23,1.56-5.06s-3.24-2.53-5.07-1.56L6,6.94c-1.29.68-2.07,2.04-2,3.49.07,1.42.93,2.67,2.22,3.25.03.01,1.2.5,1.2.5L6,14.94c-1.84.96-2.53,3.23-1.56,5.06s3.24,2.53,5.07,1.56l8.5-4.5c1.29-.68,2.06-2.04,1.99-3.49C19.93,12.16,19.08,10.91,17.77,10.32Z"/></svg>
ショート
</div>
<div class="home-shorts-scroll" id="home-shorts-container"></div>
</div>
<div id="home-grid" class="video-grid"></div>
<div id="home-loader" class="loader">動画を読み込み中...</div>
</div>

<!-- 検索 -->
<div id="view-search" class="view">
<div id="search-shorts-section" class="search-shorts-section hidden">
<div class="search-shorts-title">
<svg viewBox="0 0 24 24" style="fill:#ff0000;stroke:none;"><path d="M17.77,10.32l-1.2-.5L18,9.06c1.84-.96,2.53-3.23,1.56-5.06s-3.24-2.53-5.07-1.56L6,6.94c-1.29.68-2.07,2.04-2,3.49.07,1.42.93,2.67,2.22,3.25.03.01,1.2.5,1.2.5L6,14.94c-1.84.96-2.53,3.23-1.56,5.06s3.24,2.53,5.07,1.56l8.5-4.5c1.29-.68,2.06-2.04,1.99-3.49C19.93,12.16,19.08,10.91,17.77,10.32Z"/></svg>
ショート
</div>
<div class="search-shorts-scroll" id="search-shorts-container"></div>
</div>
<div id="search-results-list" class="search-results-list"></div>
<div id="search-loader" class="loader hidden">さらに読み込み中...</div>
</div>

<!-- ショート -->
<div id="view-shorts" class="view" style="padding:0; margin:0;">
<div class="shorts-full-page" id="shorts-full-page">
<div id="shorts-container"></div>
<div id="shorts-loader" class="loader" style="color:#fff;">ショート動画を読み込み中...</div>
</div>
</div>

<!-- 登録チャンネルページ -->
<div id="view-subscriptions" class="view">
<div class="subs-page-header">
<svg viewBox="0 0 24 24" style="width:28px;height:28px;fill:var(--primary-color);stroke:none;"><path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/></svg>
<h2>登録チャンネル</h2>
</div>
<div id="subs-grid"></div>
</div>

<!-- チャンネルページ -->
<div id="view-channel" class="view">
<div class="channel-banner-wrap" id="channel-banner-wrap">
<canvas id="channel-banner-canvas" class="channel-banner-placeholder" style="display:none;"></canvas>
<img id="channel-banner-img" class="channel-banner-img" src="" alt="" style="display:none;"
onerror="this.style.display='none'; document.getElementById('channel-banner-canvas').style.display='block';">
</div>
<div class="channel-header-row">
<div class="channel-header-icon-wrap">
<img src="" class="channel-header-icon" id="channel-page-icon">
</div>
<div class="channel-header-info">
<div class="channel-header-title" id="channel-page-name">チャンネル名</div>
<div class="channel-header-handle" id="channel-page-handle">@channel</div>
<div class="channel-header-meta" id="channel-page-meta">登録者数 -- • 動画 --件</div>
<div class="channel-action-row">
<button class="subscribe-btn" id="channel-subscribe-btn" onclick="toggleSubscribeChannel()">チャンネル登録</button>
<button class="channel-bell-btn" id="channel-bell-btn" style="display:none;">
<svg viewBox="0 0 24 24" style="fill:currentColor;stroke:none;"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>
</button>
<button class="channel-join-btn" id="channel-join-btn" style="display:none;">メンバーになる</button>
</div>
</div>
</div>
<div class="channel-tabs-bar" id="channel-tabs-bar">
<button class="channel-tab active" id="ch-tab-home" onclick="switchChannelTab('home')">ホーム</button>
<button class="channel-tab" id="ch-tab-shorts" onclick="switchChannelTab('shorts')">ショート</button>
<button class="channel-tab" id="ch-tab-videos" onclick="switchChannelTab('videos')">動画</button>
<button class="channel-tab" id="ch-tab-live" onclick="switchChannelTab('live')">ライブ</button>
</div>
<div id="channel-tab-home" class="channel-featured">
<div id="channel-featured-video" style="margin-bottom:16px;"></div>
<h3 style="margin-bottom:12px;">最新動画</h3>
<div id="channel-home-grid" class="video-grid"></div>
</div>
<div id="channel-tab-shorts" style="display:none;">
<div id="channel-shorts-grid" class="channel-shorts-grid"></div>
<div id="channel-shorts-loader" class="loader">読み込み中...</div>
<button id="channel-shorts-more-btn" onclick="loadMoreChannelShorts()" style="display:none;margin:16px auto;padding:10px 24px;border-radius:20px;background:var(--chip-bg);font-size:14px;cursor:pointer;">もっと見る</button>
</div>
<div id="channel-tab-videos" style="display:none;">
<div class="channel-filter-bar">
<button class="channel-filter-chip active" onclick="setChannelFilter(this,'latest')">最新</button>
<button class="channel-filter-chip" onclick="setChannelFilter(this,'popular')">人気</button>
<button class="channel-filter-chip" onclick="setChannelFilter(this,'oldest')">古い順</button>
</div>
<div id="channel-grid" class="video-grid"></div>
<div id="channel-loader" class="loader hidden">読み込み中...</div>
</div>
<div id="channel-tab-live" style="display:none;">
<div id="channel-live-grid" class="video-grid"></div>
<div id="channel-live-loader" class="loader">読み込み中...</div>
</div>
</div>

<!-- ウォッチ -->
<div id="view-watch" class="view">
<div class="watch-layout">
<div class="watch-main">
<div class="player-container" id="player-wrapper">
<iframe id="yt-player" allow="autoplay; fullscreen" allowfullscreen></iframe>
</div>
<h1 class="watch-title" id="watch-title-text">タイトル</h1>
<div class="watch-actions">
<div class="watch-channel-info" id="watch-channel-trigger">
<img src="" class="channel-avatar" id="watch-channel-icon">
<div>
<div class="watch-channel-name" id="watch-channel-name">投稿者</div>
<div class="watch-channel-subs">登録者数 --</div>
</div>
<button class="subscribe-btn" id="watch-subscribe-btn" style="margin-left:12px;" onclick="event.stopPropagation(); toggleSubscribeFromWatch()">登録</button>
</div>
<div class="action-buttons">
<!-- ストリームセレクタ: 3ボタン (m3u8削除) -->
<div class="stream-selector">
<button class="stream-btn active" id="btn-stream-1" onclick="switchStream(1)">Nocookie</button>
<button class="stream-btn" id="btn-stream-2" onclick="switchStream(2)">Edu</button>
<button class="stream-btn" id="btn-stream-3" onclick="switchStream(3)">Manifest</button>
</div>
<div class="quality-wrap" id="quality-wrap" style="display:none;">
<button class="action-btn" onclick="toggleQualityPanel(event)">
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor;stroke:none;"><path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 12.5v-9l6 4.5-6 4.5z"/></svg>
<span id="quality-label">画質</span>
</button>
<div class="quality-panel" id="quality-panel"></div>
</div>
<div class="download-wrap" id="download-wrap-watch">
<button class="action-btn" onclick="toggleDownloadPanel(event, 'watch')" title="ダウンロード">
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor;stroke:none;"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
DL
</button>
<div class="download-panel" id="download-panel-watch"></div>
</div>
<button class="action-btn" onclick="shareCurrentVideo()">
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor;stroke:none;"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/></svg>
共有
</button>
</div>
</div>
<div class="video-description" onclick="toggleDescription()">
<div class="video-stats">
<span id="api-views">---</span> 回視聴 • <span id="api-likes">---</span> 高評価 <span id="api-date"></span>
</div>
<div class="desc-content" id="api-desc">概要欄を読み込んでいます...</div>
<span id="desc-toggle-text">続きを読む</span>
</div>
<div class="comments-section">
<div style="font-weight:bold; margin-bottom:12px;"><span id="comment-count">0</span> 件のコメント</div>
<div id="comments-container"></div>
</div>
</div>
<div class="watch-sidebar">
<div style="font-weight:bold; margin-bottom:16px;">次の動画</div>
<div id="related-videos"></div>
<div id="related-loader" class="loader hidden">関連動画を読み込み中...</div>
</div>
</div>
</div>

<!-- 履歴 -->
<div id="view-history" class="view">
<h2 style="margin-bottom:20px;">視聴履歴</h2>
<div id="history-grid" class="video-grid"></div>
</div>

<!-- 設定 -->
<div id="view-settings" class="view">
<div style="max-width:680px; margin:0 auto; padding-top:20px;">
<h2 style="font-size:22px;font-weight:bold;margin-bottom:24px;">⚙️ 設定</h2>
<div class="settings-panel">
<div class="settings-section">
<div class="settings-section-title">ネットワーク</div>
<div class="setting-item">
<div class="setting-info"><div class="setting-title">CORSプロキシを利用する</div><div class="setting-desc">外部APIへの通信時にプロキシを経由します(推奨)</div></div>
<label class="toggle-switch"><input type="checkbox" id="nav-setting-proxy" onchange="saveNavSettings()"><span class="slider"></span></label>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">動画ストリーム(ウォッチページ)</div>
<div class="stream-options-grid" id="nav-stream-options">
<button class="stream-option-btn" data-val="1" onclick="selectNavStream(1)">Nocookie<span class="s-label">標準・安定</span></button>
<button class="stream-option-btn" data-val="2" onclick="selectNavStream(2)">Edu<span class="s-label">教育版・Premium</span></button>
<button class="stream-option-btn" data-val="3" onclick="selectNavStream(3)">Manifest<span class="s-label">直接ストリーム</span></button>
</div>
<input type="hidden" id="nav-setting-stream" value="1">
</div>
<div class="settings-section">
<div class="settings-section-title">ショートストリーム</div>
<div class="stream-options-grid" id="nav-short-stream-options">
<button class="stream-option-btn" data-val="1" onclick="selectNavShortStream(1)">Nocookie<span class="s-label">標準</span></button>
<button class="stream-option-btn" data-val="2" onclick="selectNavShortStream(2)">Edu<span class="s-label">教育版</span></button>
<button class="stream-option-btn" data-val="3" onclick="selectNavShortStream(3)">Manifest<span class="s-label">直接再生</span></button>
</div>
<input type="hidden" id="nav-setting-short-stream" value="1">
</div>
<div class="settings-section">
<div class="settings-section-title">表示</div>
<div class="setting-item">
<div class="setting-info"><div class="setting-title">ホームにトレンド動画を表示</div></div>
<label class="toggle-switch"><input type="checkbox" id="nav-setting-trend" onchange="saveNavSettings()"><span class="slider"></span></label>
</div>
<div class="setting-item">
<div class="setting-info"><div class="setting-title">ダークモード</div></div>
<label class="toggle-switch"><input type="checkbox" id="nav-setting-theme" onchange="toggleThemeFromNavSettings()"><span class="slider"></span></label>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">登録チャンネル管理</div>
<div id="settings-subs-list"></div>
</div>
</div>
</div>
</div>

</main>

<script>
// =================== ベース設定 ===================
let currentView = 'welcome';
let currentVideoId = null; let currentVideoTitle = '';
let currentChannelName = ''; let currentChannelThumb = '';
let currentChannelId = null;
let searchContext = 'trend'; let isFetching = false;
let lastQuery = ''; let currentPage = 1; let captchaTimer = null;
const seenVideoIds = new Set();
const KAHOOT_KEY_URL = 'https://apis.kahoot.it/media-api/youtube/key';
const CORS_PROXIES = ['https://api.codetabs.com/v1/proxy?quest=', 'https://api.codetabs.com/v1/tmp/?quest='];
let currentChannelTab = 'home';
let shortStreamType = 1;
let currentShortItems = [];
let currentEduKey = null;
let currentGVFormats = null;
let currentGVAllFormats = null;
let selectedQuality = null;
let currentChannelShortsPage = 1;
let currentChannelShortsName = '';

const INVIDIOUS_INSTANCES = [
'https://invidious.f5.si','https://yt.omada.cafe','https://inv.thepixora.com',
'https://yawtu.be','https://inv.nadeko.net','https://iv.duti.dev','https://inv.vern.cc',
'https://invidious.privacyredirect.com','https://inv1.nadeko.net','https://inv2.nadeko.net',
'https://inv3.nadeko.net','https://inv4.nadeko.net','https://inv5.nadeko.net',
'https://yewtu.be','https://invidious.garudalinux.org','https://invidious.tiekoetter.com',
'https://yt.artemislena.eu','https://invidious.privacydev.net','https://invidious.nerdvpn.de',
'https://invidious.lunar.icu','https://invidious.slipfox.xyz','https://iv.ggtyler.dev',
'https://nyc1.iv.ggtyler.dev','https://cal1.iv.ggtyler.dev','https://iv.catgirl.cloud',
'https://invidious.sethforprivacy.com','https://invidious.weblibre.org','https://invidious.pufe.org',
'https://invidious.projectsegfau.lt','https://invidious.fdn.fr','https://invidious.epicsite.xyz',
'https://yt.funami.tech','https://invidious.esmailelbob.xyz','https://invidious.drivet.xyz',
'https://invidious.flokinet.to','https://invidious.rhyshl.live','https://invidious.perennialte.ch',
'https://invidious.private.coffee','https://inv.privacy.com.de','https://iv.nboeck.de'
];

// =================== 設定管理 ===================
function getAppConfig() {
const defaults = { proxy: true, stream: 1, shortStream: 1, trend: true, theme: 'light', isFirstVisit: true };
try { return { ...defaults, ...JSON.parse(localStorage.getItem('study2525_config')) }; } catch(e) { return defaults; }
}
function saveSettings() {
const config = {
proxy: document.getElementById('setting-proxy').checked,
stream: parseInt(document.getElementById('setting-stream').value) || 1,
shortStream: parseInt(document.getElementById('setting-short-stream').value) || 1,
trend: document.getElementById('setting-trend').checked,
theme: document.body.getAttribute('data-theme') || 'light',
isFirstVisit: false
};
localStorage.setItem('study2525_config', JSON.stringify(config)); syncSettings(config);
}
function saveNavSettings() {
const config = {
proxy: document.getElementById('nav-setting-proxy').checked,
stream: parseInt(document.getElementById('nav-setting-stream').value) || 1,
shortStream: parseInt(document.getElementById('nav-setting-short-stream').value) || 1,
trend: document.getElementById('nav-setting-trend').checked,
theme: document.body.getAttribute('data-theme') || 'light',
isFirstVisit: false
};
localStorage.setItem('study2525_config', JSON.stringify(config)); syncSettings(config);
}
function syncSettings(config) {
const p1 = document.getElementById('setting-proxy'), p2 = document.getElementById('nav-setting-proxy');
const t1 = document.getElementById('setting-trend'), t2 = document.getElementById('nav-setting-trend');
const th1 = document.getElementById('setting-theme'), th2 = document.getElementById('nav-setting-theme');
const s1 = document.getElementById('setting-stream'), s2 = document.getElementById('nav-setting-stream');
const ss1 = document.getElementById('setting-short-stream'), ss2 = document.getElementById('nav-setting-short-stream');
if(p1) p1.checked = config.proxy; if(p2) p2.checked = config.proxy;
if(t1) t1.checked = config.trend; if(t2) t2.checked = config.trend;
if(th1) th1.checked = config.theme==='dark'; if(th2) th2.checked = config.theme==='dark';
if(s1) s1.value = config.stream; if(s2) s2.value = config.stream;
if(ss1) ss1.value = config.shortStream; if(ss2) ss2.value = config.shortStream;
['welcome-stream-options','nav-stream-options'].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.querySelectorAll('.stream-option-btn').forEach(b => b.classList.toggle('selected', parseInt(b.dataset.val) === config.stream));
});
['welcome-short-stream-options','nav-short-stream-options'].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.querySelectorAll('.stream-option-btn').forEach(b => b.classList.toggle('selected', parseInt(b.dataset.val) === config.shortStream));
});
}
function selectWelcomeStream(val) {
document.getElementById('setting-stream').value = val;
document.querySelectorAll('#welcome-stream-options .stream-option-btn').forEach(b => b.classList.toggle('selected', parseInt(b.dataset.val)===val));
saveSettings();
}
function selectWelcomeShortStream(val) {
document.getElementById('setting-short-stream').value = val;
document.querySelectorAll('#welcome-short-stream-options .stream-option-btn').forEach(b => b.classList.toggle('selected', parseInt(b.dataset.val)===val));
saveSettings();
}
function selectNavStream(val) {
document.getElementById('nav-setting-stream').value = val;
document.querySelectorAll('#nav-stream-options .stream-option-btn').forEach(b => b.classList.toggle('selected', parseInt(b.dataset.val)===val));
saveNavSettings();
}
function selectNavShortStream(val) {
document.getElementById('nav-setting-short-stream').value = val;
document.querySelectorAll('#nav-short-stream-options .stream-option-btn').forEach(b => b.classList.toggle('selected', parseInt(b.dataset.val)===val));
saveNavSettings();
}
function buildFetchUrl(targetUrl) { return getAppConfig().proxy ? CORS_PROXIES[0] + encodeURIComponent(targetUrl) : targetUrl; }

function initApp() {
const config = getAppConfig();
if (config.theme === 'dark') document.body.setAttribute('data-theme', 'dark');
shortStreamType = config.shortStream || 1;
syncSettings(config);
renderSidebarSubscriptions();
const urlState = parseInitialUrl();
if (config.isFirstVisit) {
navigate('welcome', { noHistory: true });
} else if (urlState && urlState.view === 'watch' && urlState.videoId) {
navigate('home', { noHistory: true });
fetch(`https://inv.nadeko.net/api/v1/videos/${urlState.videoId}?fields=title,author,authorThumbnails`)
.then(r => r.json()).then(d => {
playVideo(urlState.videoId, d.title || urlState.videoId, d.author || '', d.authorThumbnails?.[0]?.url || null, true);
}).catch(() => playVideo(urlState.videoId, urlState.videoId, '', null, true));
} else if (urlState && urlState.view === 'history') {
navigate('history');
} else if (urlState && urlState.view === 'settings') {
navigate('settings');
} else if (urlState && urlState.view === 'subscriptions') {
navigate('subscriptions');
} else {
navigate('home');
}
}

function toggleTheme() {
const isDark = document.body.getAttribute('data-theme') === 'dark';
document.body.setAttribute('data-theme', isDark ? 'light' : 'dark');
const config = getAppConfig(); config.theme = isDark ? 'light' : 'dark';
localStorage.setItem('study2525_config', JSON.stringify(config));
syncSettings(config);
}
function toggleThemeFromSettings() {
const isDark = document.getElementById('setting-theme').checked;
document.body.setAttribute('data-theme', isDark ? 'dark' : 'light'); saveSettings();
}
function toggleThemeFromNavSettings() {
const isDark = document.getElementById('nav-setting-theme').checked;
document.body.setAttribute('data-theme', isDark ? 'dark' : 'light'); saveNavSettings();
}
function finishSetup() { saveSettings(); navigate('home'); if(getAppConfig().trend) loadTrend(); }

// =================== 登録チャンネル ===================
function getSubscriptions() { try { return JSON.parse(localStorage.getItem('subscriptions') || '[]'); } catch(e) { return []; } }
function saveSubscriptions(subs) { localStorage.setItem('subscriptions', JSON.stringify(subs)); }
function isSubscribed(channelName) { return getSubscriptions().some(s => s.name === channelName); }
function toggleSubscribeChannel() {
const name = document.getElementById('channel-page-name').innerText;
const thumb = document.getElementById('channel-page-icon').src;
let subs = getSubscriptions();
if (isSubscribed(name)) { subs = subs.filter(s => s.name !== name); } else { subs.unshift({ name, thumb, channelId: currentChannelId || null, isLive: Math.random() < 0.15 }); }
saveSubscriptions(subs); updateChannelSubscribeUI(name); renderSidebarSubscriptions();
}
function toggleSubscribeFromWatch() {
const name = document.getElementById('watch-channel-name').innerText;
const thumb = document.getElementById('watch-channel-icon').src;
let subs = getSubscriptions();
if (isSubscribed(name)) { subs = subs.filter(s => s.name !== name); } else { subs.unshift({ name, thumb, isLive: Math.random() < 0.15 }); }
saveSubscriptions(subs); updateWatchSubscribeUI(name); renderSidebarSubscriptions();
}
function updateChannelSubscribeUI(name) {
const btn = document.getElementById('channel-subscribe-btn');
const bell = document.getElementById('channel-bell-btn');
const join = document.getElementById('channel-join-btn');
if (!btn) return;
if (isSubscribed(name)) {
btn.innerText = '登録済み'; btn.classList.add('subscribed');
bell.style.display = 'flex'; join.style.display = 'block';
} else {
btn.innerText = 'チャンネル登録'; btn.classList.remove('subscribed');
bell.style.display = 'none'; join.style.display = 'none';
}
}
function updateWatchSubscribeUI(name) {
const btn = document.getElementById('watch-subscribe-btn');
if (!btn) return;
if (isSubscribed(name)) { btn.innerText = '登録済み'; btn.classList.add('subscribed'); }
else { btn.innerText = '登録'; btn.classList.remove('subscribed'); }
}
function renderSidebarSubscriptions() {
const subs = getSubscriptions();
const container = document.getElementById('sidebar-subscriptions');
if (!container) return;
if (subs.length === 0) { container.innerHTML = ''; return; }
container.innerHTML = subs.slice(0, 8).map(s => `
<div class="sidebar-channel-item" onclick="openChannel('${s.name.replace(/'/g,"\\'")}', '${s.thumb}')">
<img src="${s.thumb}" onerror="this.src='https://i.pravatar.cc/40?u=${encodeURIComponent(s.name)}'">
<span class="channel-name-sidebar">${s.name}</span>
${s.isLive ? '<span class="live-badge">LIVE</span>' : ''}
</div>
`).join('');
}
function renderSubscriptionsPage() {
const subs = getSubscriptions();
const grid = document.getElementById('subs-grid');
if (!grid) return;
if (subs.length === 0) {
grid.innerHTML = '<div class="subs-empty" style="padding:60px 0;">チャンネルを登録するとここに表示されます<br><br><button onclick="navigate(\'home\')" style="padding:10px 24px;border-radius:20px;background:var(--primary-color);color:#fff;font-weight:bold;font-size:14px;margin-top:12px;">ホームへ戻る</button></div>';
return;
}
grid.innerHTML = subs.map(s => `
<div class="sub-channel-card" onclick="openChannel('${s.name.replace(/'/g,"\\'")}', '${s.thumb}')">
<img src="${s.thumb}" onerror="this.src='https://i.pravatar.cc/56?u=${encodeURIComponent(s.name)}'">
<div class="sub-channel-info">
<div class="sub-channel-name">${s.name}</div>
<div class="sub-channel-meta">${s.isLive ? '🔴 ライブ配信中' : '登録済みチャンネル'}</div>
</div>
<button class="unsub-btn" onclick="event.stopPropagation(); unsubscribeChannelPage('${s.name.replace(/'/g,"\\'")}', this)">登録解除</button>
</div>
`).join('');
}
function unsubscribeChannelPage(name, btn) {
let subs = getSubscriptions().filter(s => s.name !== name);
saveSubscriptions(subs); renderSubscriptionsPage(); renderSidebarSubscriptions(); renderSettingsSubsList();
}
function renderSettingsSubsList() {
const subs = getSubscriptions();
const el = document.getElementById('settings-subs-list');
if (!el) return;
if (subs.length === 0) { el.innerHTML = '<div style="color:var(--text-secondary);padding:8px 0;">登録チャンネルはありません</div>'; return; }
el.innerHTML = subs.map(s => `
<div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border-color);">
<img src="${s.thumb}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;" onerror="this.src='https://i.pravatar.cc/40?u=${encodeURIComponent(s.name)}'">
<span style="flex:1;font-weight:500;">${s.name}</span>
${s.isLive ? '<span style="background:#ff0000;color:#fff;font-size:11px;font-weight:bold;padding:2px 7px;border-radius:4px;">LIVE</span>' : ''}
<button onclick="unsubscribeChannelSettings('${s.name.replace(/'/g,"\\'")}')" style="padding:6px 14px;border-radius:16px;background:var(--hover-color);font-size:13px;">登録解除</button>
</div>
`).join('');
}
function unsubscribeChannelSettings(name) {
let subs = getSubscriptions().filter(s => s.name !== name);
saveSubscriptions(subs); renderSettingsSubsList(); renderSidebarSubscriptions();
}

// =================== MANIFEST HUNTER (1つ目のHTMLを参考に実装) ===================
// データの全階層からmanifest.を抽出する再帰関数
function deepSearch(obj) {
let found = [];
if (!obj || typeof obj !== 'object') return found;
if (obj.url && String(obj.url).includes('manifest.')) {
const type = String(obj.type || obj.mimeType || '');
if (!type.includes('audio') || type.includes('video')) found.push(obj);
}
for (let k in obj) found = found.concat(deepSearch(obj[k]));
return found;
}

// プロキシを順番に試してmanifestを取得する
async function manifestHunt(videoId) {
const proxies = [
'', // 直通
'https://api.allorigins.win/raw?url=', // Proxy A
'https://corsproxy.io/?' // Proxy B
];
const apiBase = `https://siawaseok.f5.si/api/streams/${videoId}`;

for (const proxy of proxies) {
try {
const targetUrl = proxy ? proxy + encodeURIComponent(apiBase) : apiBase;
const res = await fetch(targetUrl, { signal: AbortSignal.timeout(15000) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();

// 1. deepSearchでmanifest.を含むURL再帰探索
const manifestResults = deepSearch(data);
if (manifestResults.length > 0) {
const unique = Array.from(new Map(manifestResults.map(s => [s.url, s])).values());
const sorted = unique.sort((a, b) => (parseInt(b.qualityLabel) || 0) - (parseInt(a.qualityLabel) || 0));
return { type: 'manifest', streams: sorted };
}

// 2. 通常のformats配列
if (data.formats && data.formats.length > 0) {
const validFormats = data.formats.filter(f => f.url);
if (validFormats.length > 0) return { type: 'formats', streams: validFormats };
}

// 3. 単一url
if (data.url) return { type: 'single', streams: [{ url: data.url, ext: 'mp4', height: 360 }] };

} catch(e) {
console.warn('[ManifestHunter] proxy failed:', proxy || 'direct', e.message);
}
}
return null;
}

// =================== siawaseok API (manifestHuntを内部利用) ===================
async function fetchGVStreamsFromSiawaseok(videoId) {
const result = await manifestHunt(videoId);
if (!result) return null;
if (result.type === 'manifest' || result.type === 'formats') return result.streams;
if (result.type === 'single') return result.streams;
return null;
}

function parseSiawaseokFormats(formats) {
if (!formats) return { mp4Pairs: [], manifestList: [] };
const mp4Formats = formats.filter(f => (f.ext === 'mp4' || f.container === 'mp4') && f.url && f.vcodec && f.vcodec !== 'none' && f.acodec === 'none');
const m4aFormats = formats.filter(f => (f.ext === 'm4a' || f.acodec === 'mp4a.40.2') && f.url && (!f.vcodec || f.vcodec === 'none'));
const combinedMp4 = formats.filter(f => (f.ext === 'mp4') && f.url && f.acodec && f.acodec !== 'none' && f.vcodec && f.vcodec !== 'none');
// manifestリスト (manifest.を含むURL全般)
const manifestList = formats.filter(f => f.url && (f.url.includes('manifest.') || f.url.includes('.m3u8') || f.url.includes('hls_playlist')));
const mp4Pairs = [];
mp4Formats.forEach(vf => {
const res = getResolutionStr(vf);
const af = m4aFormats.find(a => true) || null;
mp4Pairs.push({ videoFmt: vf, audioFmt: af, resolution: res, label: getLabelFromFmt(vf) });
});
combinedMp4.forEach(f => {
const lbl = getLabelFromFmt(f);
if (!mp4Pairs.find(p => p.label === lbl)) mp4Pairs.push({ videoFmt: f, audioFmt: null, resolution: getResolutionStr(f), label: lbl, isCombined: true });
});
const order = ['2160p','1440p','1080p','720p','480p','360p','240p','144p'];
mp4Pairs.sort((a, b) => {
const ia = order.indexOf(a.label), ib = order.indexOf(b.label);
return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
});
return { mp4Pairs, manifestList };
}
function getResolutionStr(fmt) {
if (fmt.resolution) return fmt.resolution;
if (fmt.width && fmt.height) return `${fmt.width}x${fmt.height}`;
if (fmt.height) return `?x${fmt.height}`;
return '';
}
function getLabelFromFmt(fmt) {
const note = (fmt.note || fmt.format_note || fmt.resolution || '').toString();
const h = fmt.height ? `${fmt.height}p` : '';
const priority = ['2160p','1440p','1080p','720p','480p','360p','240p','144p'];
for (const p of priority) { if (note.includes(p.replace('p','')) || h === p) return p; }
return h || note.substring(0, 10) || 'unknown';
}

async function renderSiawaseokPlayer(formats, qualityLabel) {
const wrapper = document.getElementById('player-wrapper');
const { mp4Pairs, manifestList } = parseSiawaseokFormats(formats);

// manifestリストが存在する場合はそちらを優先(Manifest Hunter成果物)
if (mp4Pairs.length === 0 && manifestList.length > 0) {
const mf = manifestList[0];
await renderManifestPlayer(mf.url, getLabelFromFmt(mf) || 'Manifest');
return;
}

if (mp4Pairs.length === 0) {
wrapper.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:#000;color:#aaa;flex-direction:column;gap:8px;font-size:13px;"><span>再生できるフォーマットがありません</span><button onclick="switchStream(1)" style="padding:8px 16px;border-radius:20px;background:#fff;color:#000;font-weight:bold;cursor:pointer;border:none;">Nocookieで再生</button></div>`;
return;
}
const defaultLabels = ['480p','360p','720p'];
let target = mp4Pairs.find(p => p.label === qualityLabel);
if (!target) { for (const dl of defaultLabels) { target = mp4Pairs.find(p => p.label === dl); if (target) break; } }
if (!target) target = mp4Pairs[0];
selectedQuality = target.label;
document.getElementById('quality-label').textContent = selectedQuality;
document.querySelectorAll('.quality-option').forEach(el => el.classList.toggle('active', el.dataset.q === selectedQuality));
const videoUrl = target.videoFmt.url;
const audioUrl = target.audioFmt ? target.audioFmt.url : null;
if (target.isCombined || !audioUrl) {
wrapper.innerHTML = `<video id="gv-video" style="width:100%;height:100%;background:#000;" controls autoplay crossorigin="anonymous"><source src="${videoUrl}" type="video/mp4"></video>`;
document.getElementById('gv-video').onerror = () => { wrapper.innerHTML = failBlock(); };
} else {
wrapper.innerHTML = `<video id="gv-video" style="width:100%;height:100%;background:#000;" controls autoplay crossorigin="anonymous"><source src="${videoUrl}" type="video/mp4"></video><audio id="gv-audio" style="display:none;" crossorigin="anonymous"><source src="${audioUrl}" type="audio/mp4"></audio>`;
const vid = document.getElementById('gv-video'), aud = document.getElementById('gv-audio');
attachAudioVideoSync(vid, aud);
vid.onerror = () => { wrapper.innerHTML = failBlock(); };
}
}

// manifest URL直接再生 (HLS/MPDなど)
async function renderManifestPlayer(manifestUrl, label) {
const wrapper = document.getElementById('player-wrapper');
selectedQuality = label;
const ql = document.getElementById('quality-label'); if (ql) ql.textContent = label;
wrapper.innerHTML = `<video id="gv-video" style="width:100%;height:100%;background:#000;" controls autoplay></video>`;
const vid = document.getElementById('gv-video');

if (manifestUrl.includes('.m3u8') || manifestUrl.includes('hls')) {
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
const hls = new Hls({ enableWorker: true });
hls.loadSource(manifestUrl); hls.attachMedia(vid);
hls.on(Hls.Events.MANIFEST_PARSED, () => { vid.play().catch(() => {}); });
hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) wrapper.innerHTML = failBlock(); });
} else if (vid.canPlayType('application/vnd.apple.mpegurl')) {
vid.src = manifestUrl; vid.play().catch(() => {});
} else {
wrapper.innerHTML = failBlock();
}
} else {
// manifest.mpd等、そのままsrcにセット
vid.src = manifestUrl;
vid.play().catch(() => {});
vid.onerror = () => { wrapper.innerHTML = failBlock(); };
}
}

function failBlock() { return `<div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:#000;color:#aaa;flex-direction:column;gap:8px;font-size:13px;"><span>再生に失敗しました</span><button onclick="switchStream(1)" style="padding:8px 16px;border-radius:20px;background:#fff;color:#000;font-weight:bold;cursor:pointer;border:none;">Nocookieで再生</button></div>`; }

function buildSiawaseokQualityPanel(formats) {
const panel = document.getElementById('quality-panel'), wrap = document.getElementById('quality-wrap');
const { mp4Pairs, manifestList } = parseSiawaseokFormats(formats);

// manifestのみの場合
if (mp4Pairs.length === 0 && manifestList.length > 0) {
wrap.style.display = 'flex';
const defaultOpt = manifestList[0];
selectedQuality = getLabelFromFmt(defaultOpt) || 'Manifest';
document.getElementById('quality-label').textContent = '🎯 ' + selectedQuality;
panel.innerHTML = '<div class="quality-panel-title">📡 Manifest ストリーム</div>' +
manifestList.map(f => {
const lbl = getLabelFromFmt(f) || 'Manifest';
return `<div class="quality-option" data-q="${lbl}" onclick="selectManifestQuality('${encodeURIComponent(f.url)}','${lbl}')">
🎯 ${lbl}
</div>`;
}).join('');
return;
}

if (mp4Pairs.length === 0) { wrap.style.display = 'none'; return; }
wrap.style.display = 'flex';
const defaultLabels = ['480p','360p','720p'];
let defaultOpt = null;
for (const dl of defaultLabels) { defaultOpt = mp4Pairs.find(p => p.label === dl); if (defaultOpt) break; }
if (!defaultOpt) defaultOpt = mp4Pairs[0];
selectedQuality = defaultOpt.label;
document.getElementById('quality-label').textContent = selectedQuality;
const hdLabels = ['1080p','720p','1440p','2160p'];
panel.innerHTML = '<div class="quality-panel-title">画質を選択(MP4+M4A)</div>' +
mp4Pairs.map(o => {
const isHd = hdLabels.includes(o.label), hasAudio = o.audioFmt || o.isCombined;
return `<div class="quality-option ${o.label === selectedQuality ? 'active' : ''}" data-q="${o.label}" onclick="selectSiawaseokQuality('${o.label}')">
${o.label}
${isHd ? '<span class="q-badge">HD</span>' : ''}
${hasAudio ? '<span style="font-size:10px;color:#4caf50;margin-left:4px;">🔊</span>' : ''}
</div>`;
}).join('') +
(manifestList.length > 0 ? '<div class="quality-panel-title" style="margin-top:8px;">📡 Manifest</div>' +
manifestList.map(f => {
const lbl = getLabelFromFmt(f) || 'Manifest';
return `<div class="quality-option" data-q="m_${lbl}" onclick="selectManifestQuality('${encodeURIComponent(f.url)}','${lbl}')">🎯 ${lbl}</div>`;
}).join('') : '');
}

async function selectManifestQuality(encodedUrl, label) {
const url = decodeURIComponent(encodedUrl);
selectedQuality = label;
document.getElementById('quality-label').textContent = '🎯 ' + label;
document.getElementById('quality-panel').classList.remove('open');
await renderManifestPlayer(url, label);
}

async function selectSiawaseokQuality(label) {
if (!currentGVAllFormats) return;
selectedQuality = label;
document.getElementById('quality-label').textContent = label;
document.getElementById('quality-panel').classList.remove('open');
await renderSiawaseokPlayer(currentGVAllFormats, label);
}

async function setupWatchManifest(videoId) {
const wrapper = document.getElementById('player-wrapper');
wrapper.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:#000;color:#fff;flex-direction:column;gap:12px;">
<div style="width:40px;height:40px;border:4px solid rgba(255,255,255,0.3);border-top-color:#0f0;border-radius:50%;animation:spin 0.8s linear infinite;"></div>
<div style="font-size:13px;color:#0f0;font-family:monospace;">🚨 Manifestをスキャン中...</div>
</div><style>@keyframes spin{to{transform:rotate(360deg)}}</style>`;

// manifestHuntで直接取得
const huntResult = await manifestHunt(videoId);

if (huntResult) {
if (huntResult.type === 'manifest') {
// manifest.URLが見つかった!
const streams = huntResult.streams;
currentGVAllFormats = streams; currentGVFormats = null;
buildSiawaseokQualityPanel(streams);
buildDownloadPanelFromSiawaseok(streams, 'watch');
const first = streams[0];
const label = first.qualityLabel || getLabelFromFmt(first) || 'Manifest';
selectedQuality = label;
if (document.getElementById('quality-label')) document.getElementById('quality-label').textContent = '🎯 ' + label;
await renderManifestPlayer(first.url, label);
return;
}
if (huntResult.type === 'formats' && huntResult.streams.length > 0) {
const formats = huntResult.streams;
currentGVAllFormats = formats; currentGVFormats = null;
buildSiawaseokQualityPanel(formats);
buildDownloadPanelFromSiawaseok(formats, 'watch');
const { mp4Pairs } = parseSiawaseokFormats(formats);
const defaultLabels = ['480p','360p','720p'];
let defaultQ = null;
for (const dl of defaultLabels) { defaultQ = mp4Pairs.find(p => p.label === dl); if (defaultQ) break; }
selectedQuality = defaultQ ? defaultQ.label : (mp4Pairs[0] ? mp4Pairs[0].label : null);
if (selectedQuality && document.getElementById('quality-label')) document.getElementById('quality-label').textContent = selectedQuality;
await renderSiawaseokPlayer(formats, selectedQuality);
return;
}
}

// Invidiousフォールバック
const invFormats = await fetchGoogleVideoStreamsInvidious(videoId);
if (invFormats && invFormats.length > 0) {
currentGVFormats = invFormats; currentGVAllFormats = null;
buildInvidiousQualityPanel(invFormats);
buildDownloadPanel(invFormats, 'watch');
const opts = getInvidiousQualityOptions(invFormats);
const defaultQ = opts.find(o => o.label === '480p') || opts.find(o => o.label === '360p') || opts[0];
selectedQuality = defaultQ ? defaultQ.label : null;
if (selectedQuality && document.getElementById('quality-label')) document.getElementById('quality-label').textContent = selectedQuality;
await renderGVPlayerInvidious(invFormats, selectedQuality);
return;
}

wrapper.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:#000;color:#aaa;flex-direction:column;gap:8px;font-size:13px;"><span>ストリームの取得に失敗しました</span><button onclick="switchStream(1)" style="padding:8px 16px;border-radius:20px;background:#fff;color:#000;font-weight:bold;cursor:pointer;border:none;">Nocookieで再生</button></div>`;
}

async function fetchGoogleVideoStreamsInvidious(videoId) {
const priorityInstances = ['https://invidious.f5.si','https://yt.omada.cafe',...INVIDIOUS_INSTANCES.slice(2,10)];
for (const instance of priorityInstances) {
try {
const url = buildFetchUrl(`${instance}/api/v1/videos/${videoId}?fields=adaptiveFormats,formatStreams`);
const res = await fetch(url, { signal: AbortSignal.timeout(6000) });
if (!res.ok) continue;
const data = await res.json();
const formats = [...(data.adaptiveFormats||[]),...(data.formatStreams||[])];
if (formats.length > 0) return formats;
} catch(e) {}
}
return null;
}

function buildInvidiousQualityPanel(formats) {
const panel = document.getElementById('quality-panel'), wrap = document.getElementById('quality-wrap');
const opts = getInvidiousQualityOptions(formats);
if (opts.length === 0) { wrap.style.display = 'none'; return; }
wrap.style.display = 'flex';
const defaultQ = opts.find(o => o.label === '720p') || opts.find(o => o.label === '480p') || opts[0];
selectedQuality = defaultQ.label;
document.getElementById('quality-label').textContent = selectedQuality;
const hdLabels = ['1080p','720p','1440p','2160p'];
panel.innerHTML = '<div class="quality-panel-title">画質を選択</div>' + opts.map(o => `
<div class="quality-option ${o.label === selectedQuality ? 'active' : ''}" data-q="${o.label}" onclick="selectInvidiousQuality('${o.label}')">
${o.label}${hdLabels.includes(o.label) ? '<span class="q-badge">HD</span>' : ''}
</div>
`).join('');
}
function getInvidiousQualityOptions(formats) {
const seen = new Set(), opts = [];
const priority = ['2160p','1440p','1080p','720p','480p','360p','240p','144p'];
for (const label of priority) {
const qNum = label.replace('p','');
const vf = formats.find(f => f.type && f.type.includes('video') && !f.type.includes('audio') && f.qualityLabel && f.qualityLabel.includes(qNum) && f.url);
const cf = formats.find(f => f.type && f.type.includes('video') && f.type.includes('audio') && f.qualityLabel && f.qualityLabel.includes(qNum) && f.url);
if ((vf||cf) && !seen.has(label)) { seen.add(label); opts.push({ label, videoFmt: vf, combinedFmt: cf }); }
}
return opts;
}
async function selectInvidiousQuality(label) {
if (!currentGVFormats) return;
selectedQuality = label;
document.getElementById('quality-label').textContent = label;
document.getElementById('quality-panel').classList.remove('open');
document.querySelectorAll('.quality-option').forEach(o => o.classList.toggle('active', o.dataset.q === label));
await renderGVPlayerInvidious(currentGVFormats, label);
}
async function renderGVPlayerInvidious(formats, quality) {
const wrapper = document.getElementById('player-wrapper');
const result = buildGoogleVideoPlayer(formats, quality);
if (!result) { wrapper.innerHTML = failBlock(); return; }
if (result.type === 'dual') {
wrapper.innerHTML = `<video id="gv-video" style="width:100%;height:100%;" controls autoplay crossorigin="anonymous"><source src="${result.videoUrl}" type="video/mp4"></video><audio id="gv-audio" style="display:none;" crossorigin="anonymous"><source src="${result.audioUrl}" type="audio/mp4"></audio>`;
attachAudioVideoSync(document.getElementById('gv-video'), document.getElementById('gv-audio'));
} else {
wrapper.innerHTML = `<video style="width:100%;height:100%;" controls autoplay crossorigin="anonymous"><source src="${result.url}"></video>`;
}
}
function buildGoogleVideoPlayer(formats, preferQuality = null) {
const audioFmt = formats.find(f => f.type && f.type.includes('audio') && !f.type.includes('video') && f.url) || formats.find(f => f.encoding && (f.encoding==='opus'||f.encoding==='aac') && f.url);
let videoFmt = null;
if (preferQuality) {
const qNum = preferQuality.replace('p','');
videoFmt = formats.find(f => f.type && f.type.includes('video') && !f.type.includes('audio') && f.url && f.qualityLabel && f.qualityLabel.includes(qNum));
}
if (!videoFmt) videoFmt = formats.find(f => f.type && f.type.includes('video') && !f.type.includes('audio') && f.url && f.qualityLabel && (f.qualityLabel.includes('720')||f.qualityLabel.includes('1080'))) || formats.find(f => f.type && f.type.includes('video') && !f.type.includes('audio') && f.url && f.qualityLabel && f.qualityLabel.includes('480')) || formats.find(f => f.type && f.type.includes('video') && !f.type.includes('audio') && f.url);
if (videoFmt && audioFmt) return { type: 'dual', videoUrl: videoFmt.url, audioUrl: audioFmt.url, quality: videoFmt.qualityLabel||'HD' };
let combined = formats.find(f => f.type && f.type.includes('video') && f.type.includes('audio') && f.url && preferQuality && f.qualityLabel && f.qualityLabel.includes(preferQuality.replace('p',''))) || formats.find(f => f.type && f.type.includes('video') && f.type.includes('audio') && f.url) || formats.find(f => f.url && f.qualityLabel);
if (combined) return { type: 'single', url: combined.url, quality: combined.qualityLabel||'SD' };
return null;
}

function toggleQualityPanel(e) { e.stopPropagation(); document.getElementById('quality-panel').classList.toggle('open'); }
document.addEventListener('click', () => {
document.getElementById('quality-panel')?.classList.remove('open');
document.querySelectorAll('.download-panel').forEach(p => p.classList.remove('open'));
});

// =================== ダウンロード ===================
function buildDownloadPanelFromSiawaseok(formats, panelId) {
const panel = document.getElementById(`download-panel-${panelId}`);
if (!panel) return;
const { mp4Pairs, manifestList } = parseSiawaseokFormats(formats);
const m4aFormats = formats.filter(f => (f.ext==='m4a'||f.acodec==='mp4a.40.2') && f.url && (!f.vcodec||f.vcodec==='none'));
let html = '';
if (manifestList.length > 0) {
html += '<div style="padding:8px 16px;font-size:12px;color:var(--text-secondary);font-weight:bold;">🎯 Manifest</div>';
manifestList.slice(0, 3).forEach(f => {
const lbl = getLabelFromFmt(f) || 'Manifest';
html += `<div class="download-option" onclick="window.open('${f.url}','_blank')"><svg viewBox="0 0 24 24"><path d="M15 8H9v3H6l6 6 6-6h-3V8zm-9 9h12v2H6v-2z"/></svg><div class="dl-label"><span class="dl-title">Manifest ${lbl}</span><span class="dl-desc">右クリック→リンクを保存</span></div></div>`;
});
}
if (mp4Pairs.length > 0) {
html += '<div style="padding:8px 16px;font-size:12px;color:var(--text-secondary);font-weight:bold;border-top:1px solid var(--border-color);margin-top:4px;">映像 (MP4)</div>';
mp4Pairs.slice(0, 5).forEach(o => {
const url = o.videoFmt.url;
html += `<div class="download-option" onclick="startDownload('${encodeURIComponent(url)}','${o.label}.mp4','${o.label}')"><svg viewBox="0 0 24 24"><path d="M15 8H9v3H6l6 6 6-6h-3V8zm-9 9h12v2H6v-2z"/></svg><div class="dl-label"><span class="dl-title">${o.label} (MP4)</span><span class="dl-desc">映像のみ</span></div></div>`;
});
}
if (m4aFormats.length > 0) {
html += '<div style="padding:8px 16px 4px;font-size:12px;color:var(--text-secondary);font-weight:bold;border-top:1px solid var(--border-color);margin-top:4px;">音声 (M4A)</div>';
m4aFormats.slice(0, 2).forEach((f, i) => {
html += `<div class="download-option" onclick="startDownload('${encodeURIComponent(f.url)}','audio_${i}.m4a','M4A音声')"><svg viewBox="0 0 24 24"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg><div class="dl-label"><span class="dl-title">音声 (M4A)</span></div></div>`;
});
}
html += `<div style="padding:8px 16px 4px;font-size:12px;color:var(--text-secondary);font-weight:bold;border-top:1px solid var(--border-color);margin-top:4px;">外部ツール</div><div class="download-option" onclick="openYtdl()"><svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg><div class="dl-label"><span class="dl-title">cobaltで開く</span><span class="dl-desc">cobalt.tools(高品質DL)</span></div></div>`;
panel.innerHTML = html;
}
function buildDownloadPanel(formats, panelId) {
const panel = document.getElementById(`download-panel-${panelId}`);
if (!panel) return;
const audioFmt = formats.find(f => f.type && f.type.includes('audio') && !f.type.includes('video') && f.url);
const videoOpts = getInvidiousQualityOptions(formats);
let html = '<div style="padding:8px 16px;font-size:12px;color:var(--text-secondary);font-weight:bold;">動画 (MP4)</div>';
videoOpts.slice(0, 5).forEach(o => {
const url = (o.combinedFmt||o.videoFmt)?.url;
if (!url) return;
html += `<div class="download-option" onclick="startDownload('${encodeURIComponent(url)}','${o.label}.mp4','${o.label}')"><svg viewBox="0 0 24 24"><path d="M15 8H9v3H6l6 6 6-6h-3V8zm-9 9h12v2H6v-2z"/></svg><div class="dl-label"><span class="dl-title">${o.label} (MP4)</span></div></div>`;
});
if (audioFmt) html += `<div style="padding:8px 16px 4px;font-size:12px;color:var(--text-secondary);font-weight:bold;border-top:1px solid var(--border-color);margin-top:4px;">音声</div><div class="download-option" onclick="startDownload('${encodeURIComponent(audioFmt.url)}','audio.m4a','M4A')"><svg viewBox="0 0 24 24"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg><div class="dl-label"><span class="dl-title">音声のみ (M4A)</span></div></div>`;
html += `<div style="padding:8px 16px 4px;font-size:12px;color:var(--text-secondary);font-weight:bold;border-top:1px solid var(--border-color);margin-top:4px;">外部ツール</div><div class="download-option" onclick="openYtdl()"><svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg><div class="dl-label"><span class="dl-title">cobaltで開く</span><span class="dl-desc">cobalt.tools</span></div></div>`;
panel.innerHTML = html;
}
function toggleDownloadPanel(e, panelId) {
e.stopPropagation();
const panel = document.getElementById(`download-panel-${panelId}`);
if (panelId === 'watch' && !currentGVAllFormats && !currentGVFormats) {
panel.innerHTML = `<div style="padding:8px 16px;font-size:12px;color:var(--text-secondary);">Manifestモードでストリームを取得するか外部ツールをご利用ください</div><div class="download-option" onclick="openYtdl()"><svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:currentColor;"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg><div class="dl-label"><span class="dl-title">cobaltで開く</span></div></div>`;
}
panel.classList.toggle('open');
}
function openYtdl() { window.open(`https://cobalt.tools/?url=https://www.youtube.com/watch?v=${currentVideoId}`, '_blank'); }
async function startDownload(encodedUrl, filename, label) {
document.querySelectorAll('.download-panel').forEach(p => p.classList.remove('open'));
const url = decodeURIComponent(encodedUrl);
showDlToast(`${label}をダウンロード中...`, 0);
try {
const res = await fetch(url, { mode: 'cors' });
if (!res.ok) throw new Error('fetch failed');
const total = parseInt(res.headers.get('Content-Length') || '0');
const reader = res.body.getReader();
const chunks = []; let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value); received += value.length;
if (total > 0) updateDlProgress(received / total * 100);
}
updateDlProgress(100);
const blob = new Blob(chunks);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${(currentVideoTitle||'video').replace(/[\\/:*?"<>|]/g,'_')}_${filename}`;
a.click();
setTimeout(() => hideDlToast(), 2000);
} catch(e) {
hideDlToast();
if (confirm('直接ダウンロードに失敗しました。cobalt.toolsで開きますか?')) openYtdl();
}
}
function showDlToast(msg, progress) {
const t = document.getElementById('dl-toast');
document.getElementById('dl-toast-msg').textContent = msg;
document.getElementById('dl-toast-fill').style.width = progress + '%';
t.classList.add('show');
}
function updateDlProgress(p) { document.getElementById('dl-toast-fill').style.width = p + '%'; }
function hideDlToast() { document.getElementById('dl-toast').classList.remove('show'); }

// =================== 共有 ===================
function shareCurrentVideo() {
if (!currentVideoId) return;
openSharePanel(`${location.origin}/watch?v=${currentVideoId}`);
}
function openSharePanel(url) {
document.getElementById('share-url-input').value = url;
document.getElementById('share-panel').classList.add('open');
}
function closeSharePanel() { document.getElementById('share-panel').classList.remove('open'); }
function copyShareUrl() {
const input = document.getElementById('share-url-input');
navigator.clipboard.writeText(input.value).then(() => {
const btn = document.querySelector('.share-copy-btn');
btn.textContent = 'コピー済み!'; btn.style.background = '#4caf50';
setTimeout(() => { btn.textContent = 'コピー'; btn.style.background = ''; }, 2000);
}).catch(() => { input.select(); document.execCommand('copy'); });
}
function shareShort(videoId) { openSharePanel(`${location.origin}/shorts/${videoId}`); }

// =================== 音声同期 ===================
function attachAudioVideoSync(gvV, gvA) {
const syncTime = () => { if (Math.abs(gvA.currentTime - gvV.currentTime) > 0.3) gvA.currentTime = gvV.currentTime; };
gvV.onplay = () => { gvA.play().catch(()=>{}); syncTime(); };
gvV.onpause = () => gvA.pause();
gvV.onwaiting = () => gvA.pause();
gvV.onplaying = () => { gvA.play().catch(()=>{}); syncTime(); };
gvV.onseeked = () => { gvA.currentTime = gvV.currentTime; };
gvV.ontimeupdate = () => { syncTime(); };
gvV.onratechange = () => { gvA.playbackRate = gvV.playbackRate; };
gvV.onvolumechange = () => { gvA.volume = gvV.volume; gvA.muted = gvV.muted; };
}

// =================== ショートObserver ===================
const shortSrcMap = {};
let shortObserver = null;
function initShortObserver() {
if (shortObserver) { shortObserver.disconnect(); shortObserver = null; }
const sfp = document.getElementById('shorts-full-page');
if (!sfp) return;
shortObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const item = entry.target;
const videoId = item.dataset.id;
const iframe = item.querySelector('iframe');
if (!iframe || !videoId) return;
if (entry.isIntersecting) {
const savedSrc = shortSrcMap[videoId];
if (savedSrc && iframe.src !== savedSrc) iframe.src = savedSrc;
} else {
if (iframe.src && iframe.src !== 'about:blank' && iframe.src !== '') {
if (!iframe.src.includes('about:blank')) shortSrcMap[videoId] = iframe.src;
iframe.src = 'about:blank';
}
}
});
}, { root: sfp, threshold: 0.5 });
}
function observeShortItem(el) { if (shortObserver) shortObserver.observe(el); }

// =================== おすすめ ===================
function getWatchHistory() { try { return JSON.parse(localStorage.getItem('history') || '[]'); } catch(e) { return []; } }
function getSearchHistory() { try { return JSON.parse(localStorage.getItem('search_history') || '[]'); } catch(e) { return []; } }
function saveSearchHistory(query) {
let sh = getSearchHistory();
sh = sh.filter(q => q !== query); sh.unshift(query);
localStorage.setItem('search_history', JSON.stringify(sh.slice(0, 30)));
}
async function loadRecommendations() {
const history = getWatchHistory(), searches = getSearchHistory();
const grid = document.getElementById('home-recommend-grid'), section = document.getElementById('home-recommend');
if (!grid || !section) return;
if (history.length < 2 && searches.length < 2) return;
section.classList.remove('hidden');
grid.innerHTML = '<div class="loader" style="grid-column:1/-1;">おすすめを生成中...</div>';
const keywords = [];
history.slice(0, 5).forEach(v => { const words = v.title.replace(/[【】「」『』\[\]【】]/g,' ').split(/\s+/); words.slice(0,2).forEach(w => { if (w.length > 2) keywords.push(w); }); });
searches.slice(0, 3).forEach(s => keywords.push(s));
if (keywords.length === 0) { section.classList.add('hidden'); return; }
const pick = keywords.sort(() => Math.random() - 0.5).slice(0, 2);
const results = [];
for (const kw of pick) {
const data = await fetchFromInvidious(kw, 'trend', 1);
if (data) data.filter(v => v.type==='video' && v.videoId).slice(0,4).forEach(v => {
if (!history.some(h => h.id === v.videoId) && results.length < 8) {
results.push({ id: v.videoId, title: v.title, channel: v.author, isShort: v.lengthSeconds > 0 && v.lengthSeconds <= 61, authorThumb: v.authorThumbnails ? v.authorThumbnails[0].url : `https://i.pravatar.cc/150?u=${v.author}`, duration: v.lengthSeconds||0, published: v.publishedText||'' });
}
});
}
grid.innerHTML = '';
if (results.length === 0) { section.classList.add('hidden'); return; }
results.forEach(v => {
const card = document.createElement('div'); card.className = 'video-card';
const durStr = formatDuration(v.duration);
card.onclick = () => playVideo(v.id, v.title, v.channel, v.authorThumb);
card.innerHTML = `<div class="thumbnail-container"><img src="https://i.ytimg.com/vi/${v.id}/mqdefault.jpg" loading="lazy">${durStr ? `<span class="duration-badge">${durStr}</span>` : ''}</div><div class="video-info"><div class="channel-avatar" onclick="event.stopPropagation(); openChannel('${v.channel.replace(/'/g,"\\'")}', '${v.authorThumb}')"><img src="${v.authorThumb}" loading="lazy"></div><div class="video-details"><div class="video-title">${v.title}</div><div class="video-meta"><span>${v.channel}</span>${v.published ? `<span>${v.published}</span>` : ''}</div></div></div>`;
grid.appendChild(card);
});
}

// =================== APIフェッチ ===================
async function fetchFromInvidious(query, context, page = 1) {
let q = query; if (context === 'shorts') q += ' #shorts';
for (let instance of INVIDIOUS_INSTANCES) {
try {
const url = buildFetchUrl(`${instance}/api/v1/search?q=${encodeURIComponent(q)}&page=${page}`);
const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
if (res.ok) return await res.json();
} catch(e) {}
}
return null;
}

async function fetchChannelInfoFromInvidious(channelName) {
for (let instance of INVIDIOUS_INSTANCES) {
try {
const searchUrl = buildFetchUrl(`${instance}/api/v1/search?q=${encodeURIComponent(channelName)}&type=channel&page=1`);
const sRes = await fetch(searchUrl, { signal: AbortSignal.timeout(8000) });
if (!sRes.ok) continue;
const sData = await sRes.json();
const channel = sData.find(c => c.type === 'channel');
if (!channel || !channel.authorId) continue;
const detailUrl = buildFetchUrl(`${instance}/api/v1/channels/${channel.authorId}`);
try {
const dRes = await fetch(detailUrl, { signal: AbortSignal.timeout(8000) });
if (dRes.ok) { const detail = await dRes.json(); return { ...channel, ...detail, authorId: channel.authorId }; }
} catch(e) {}
return channel;
} catch(e) {}
}
return null;
}

async function fetchChannelLiveVideos(channelName) {
let channelId = null, channelInfo = null;
for (let instance of INVIDIOUS_INSTANCES) {
try {
const searchUrl = buildFetchUrl(`${instance}/api/v1/search?q=${encodeURIComponent(channelName)}&type=channel&page=1`);
const sRes = await fetch(searchUrl, { signal: AbortSignal.timeout(8000) });
if (!sRes.ok) continue;
const sData = await sRes.json();
const channel = sData.find(c => c.type === 'channel');
if (channel) { channelId = channel.authorId; channelInfo = channel; break; }
} catch(e) {}
}
if (!channelId) return { videos: [], channelInfo: null };
const allVideos = [];
for (let instance of INVIDIOUS_INSTANCES) {
try {
const streamsUrl = buildFetchUrl(`${instance}/api/v1/channels/${channelId}/streams`);
const res = await fetch(streamsUrl, { signal: AbortSignal.timeout(8000) });
if (!res.ok) continue;
const data = await res.json();
const videos = data.videos || data || [];
if (Array.isArray(videos) && videos.length > 0) { videos.forEach(v => { if (v.videoId && !allVideos.find(x => x.videoId === v.videoId)) allVideos.push({ ...v, _sourceType: 'stream' }); }); break; }
} catch(e) {}
}
return { videos: allVideos, channelInfo };
}
async function fetchChannelVideos(channelName, page = 1) {
for (let instance of INVIDIOUS_INSTANCES) {
try {
const searchUrl = buildFetchUrl(`${instance}/api/v1/search?q=${encodeURIComponent(channelName)}&type=channel&page=1`);
const sRes = await fetch(searchUrl, { signal: AbortSignal.timeout(8000) });
if (!sRes.ok) continue;
const sData = await sRes.json();
const channel = sData.find(c => c.type === 'channel');
if (!channel) continue;
const videosUrl = buildFetchUrl(`${instance}/api/v1/channels/${channel.authorId}/videos?page=${page}`);
const vRes = await fetch(videosUrl, { signal: AbortSignal.timeout(8000) });
if (!vRes.ok) continue;
const vData = await vRes.json();
return { videos: vData.videos || [], channelInfo: channel };
} catch(e) {}
}
return null;
}
async function fetchChannelShortsMultiPage(channelName, startPage = 1) {
let channelId = null;
for (let instance of INVIDIOUS_INSTANCES) {
try {
const searchUrl = buildFetchUrl(`${instance}/api/v1/search?q=${encodeURIComponent(channelName)}&type=channel&page=1`);
const sRes = await fetch(searchUrl, { signal: AbortSignal.timeout(8000) });
if (!sRes.ok) continue;
const sData = await sRes.json();
const channel = sData.find(c => c.type === 'channel');
if (channel) { channelId = channel.authorId; break; }
} catch(e) {}
}
if (!channelId) return { shorts: [], hasMore: false };
const pagesToFetch = [startPage, startPage+1, startPage+2];
const allVideos = [];
for (const page of pagesToFetch) {
for (let instance of INVIDIOUS_INSTANCES) {
try {
const videosUrl = buildFetchUrl(`${instance}/api/v1/channels/${channelId}/videos?page=${page}`);
const vRes = await fetch(videosUrl, { signal: AbortSignal.timeout(8000) });
if (!vRes.ok) continue;
const vData = await vRes.json();
if (vData.videos && vData.videos.length > 0) { allVideos.push(...vData.videos); break; }
} catch(e) {}
}
}
const shorts = allVideos.filter(v => v.lengthSeconds > 0 && v.lengthSeconds <= 61).map(v => ({ id: v.videoId, title: v.title, channel: v.author || channelName, isShort: true, authorThumb: v.authorThumbnails ? v.authorThumbnails[0].url : `https://i.pravatar.cc/150?u=${encodeURIComponent(v.author||channelName)}`, duration: v.lengthSeconds, published: v.publishedText||'' }));
return { shorts, hasMore: allVideos.length >= 20 };
}

// =================== CSE ===================
window.__gcse = {
parsetags: 'explicit',
initializationCallback: function() {
google.search.cse.element.render({ div: "hidden-cse-container", tag: 'searchresults-only', gname: 'studyCse' });
initApp();
const config = getAppConfig();
if (!config.isFirstVisit && config.trend) loadTrend();
else if (!config.isFirstVisit && !config.trend) document.getElementById('home-loader').classList.add('hidden');
},
searchCallbacks: {
web: {
ready: function(name, q, promos, results) {
isFetching = false; clearTimeout(captchaTimer);
document.getElementById('hidden-cse-container').style.display = 'none';
let videos = [];
if (results && results.length > 0) {
results.forEach(r => {
const urlStr = r.unescapedUrl || r.url || "", titleStr = r.titleNoFormatting || r.title || "";
const isShort = urlStr.includes('/shorts/') || titleStr.toLowerCase().includes('#shorts');
const id = (urlStr.match(/(?:v=|vi\/|youtu\.be\/|embed\/|v%3D|video\/|shorts\/)([a-zA-Z0-9_-]{11})/) || [])[1];
if (searchContext === 'shorts' && !isShort && !urlStr.includes('shorts')) return;
if (id && id.length === 11 && !seenVideoIds.has(id)) {
seenVideoIds.add(id);
const mockChannel = (r.visibleUrl||"YouTube Channel").split('/')[0];
videos.push({ id, title: titleStr, isShort, channel: mockChannel, authorThumb: `https://i.pravatar.cc/150?u=${mockChannel}` });
}
});
}
renderResults(videos, currentPage > 1);
if (searchContext === 'trend' && currentPage <= 1) { fetchShortsForHome(); loadRecommendations(); }
return true;
}
}
}
};

async function triggerSearch(query, context, append = false) {
if (isFetching) return; isFetching = true; searchContext = context; lastQuery = query;
if (!append) { seenVideoIds.clear(); currentPage = 1; } else { currentPage++; }

if (context === 'channel-home' || context === 'channel-videos') {
const result = await fetchChannelVideos(query, currentPage);
isFetching = false;
if (result && result.videos && result.videos.length > 0) {
let videos = result.videos.map(v => ({ id: v.videoId, title: v.title, channel: v.author || query, isShort: false, authorThumb: v.authorThumbnails ? v.authorThumbnails[0].url : `https://i.pravatar.cc/150?u=${encodeURIComponent(v.author||query)}`, duration: v.lengthSeconds||0, published: v.publishedText||'', isLive: v.liveNow||false, isArchived: !v.liveNow && v.lengthSeconds > 0 && (v.title && (v.title.includes('ライブ')||v.title.includes('配信')||v.title.includes('LIVE'))), publishedTimestamp: v.published||0 }));
renderResults(videos, append);
if (result.channelInfo && !append) updateChannelHeaderInfo(result.channelInfo);
return;
}
const invData = await fetchFromInvidious(query, context, currentPage);
if (invData && invData.length > 0) {
let videos = [];
invData.forEach(item => {
if (item.type === 'video' && item.videoId && !seenVideoIds.has(item.videoId)) {
const authorNorm = (item.author||'').toLowerCase().trim(), queryNorm = query.toLowerCase().trim();
if (!authorNorm.includes(queryNorm) && !queryNorm.includes(authorNorm)) return;
seenVideoIds.add(item.videoId);
videos.push({ id: item.videoId, title: item.title, channel: item.author||query, isShort: false, isLive: item.liveNow||false, isArchived: !item.liveNow && (item.title && (item.title.includes('ライブ')||item.title.includes('配信')||item.title.includes('LIVE'))), authorThumb: item.authorThumbnails ? item.authorThumbnails[0].url : `https://i.pravatar.cc/150?u=${encodeURIComponent(item.author||query)}`, duration: item.lengthSeconds||0, published: item.publishedText||'', publishedTimestamp: item.published||0 });
}
});
renderResults(videos, append); return;
}
return;
}
if (context === 'channel-shorts') {
const cleanName = query.replace(/\s*shorts\s*/gi,'').replace(/#/g,'').trim();
currentChannelShortsName = cleanName; isFetching = false;
const { shorts, hasMore } = await fetchChannelShortsMultiPage(cleanName, currentChannelShortsPage);
if (shorts.length > 0) {
renderChannelShorts(shorts, append);
const moreBtn = document.getElementById('channel-shorts-more-btn');
if (moreBtn) moreBtn.style.display = hasMore ? 'block' : 'none';
} else {
const invData = await fetchFromInvidious(cleanName + ' #shorts', 'shorts', currentPage);
if (invData && invData.length > 0) {
let videos = invData.filter(item => item.type==='video' && item.videoId && item.lengthSeconds > 0 && item.lengthSeconds <= 61).map(item => ({ id: item.videoId, title: item.title, channel: item.author||cleanName, isShort: true, authorThumb: item.authorThumbnails ? item.authorThumbnails[0].url : `https://i.pravatar.cc/150?u=${encodeURIComponent(item.author||cleanName)}`, duration: item.lengthSeconds, published: item.publishedText||'' }));
renderChannelShorts(videos, append);
} else {
document.getElementById('channel-shorts-loader')?.classList.add('hidden');
const grid = document.getElementById('channel-shorts-grid');
if (grid && !append) grid.innerHTML = '<div style="padding:24px;color:var(--text-secondary);">ショート動画が見つかりませんでした</div>';
}
}
return;
}
if (context === 'channel-live') {
const chName = query.replace(/ ライブ$/, '').trim(); isFetching = false;
const { videos: liveVideos, channelInfo } = await fetchChannelLiveVideos(chName);
if (liveVideos && liveVideos.length > 0) {
const mapped = liveVideos.map(v => ({ id: v.videoId, title: v.title||'', channel: v.author||chName, isShort: false, isLive: !!(v.liveNow||v.isUpcoming), isUpcoming: !!v.isUpcoming, isArchived: !v.liveNow && !v.isUpcoming, authorThumb: v.authorThumbnails ? v.authorThumbnails[0].url : `https://i.pravatar.cc/150?u=${encodeURIComponent(v.author||chName)}`, duration: v.lengthSeconds||0, published: v.publishedText||'', publishedTimestamp: v.published||0 }));
renderChannelLive(mapped, append);
if (channelInfo && !append) updateChannelHeaderInfo(channelInfo);
} else {
const result = await fetchChannelVideos(chName, 1);
if (result && result.videos) {
const liveFiltered = result.videos.filter(v => v.liveNow || v.isUpcoming || (v.title && v.title.match(/ライブ|配信|LIVE|live|生放送|STREAM/i)));
const mapped = (liveFiltered.length > 0 ? liveFiltered : result.videos.slice(0,12)).map(v => ({ id: v.videoId, title: v.title, channel: v.author||chName, isShort: false, isLive: !!(v.liveNow||v.isUpcoming), isUpcoming: !!v.isUpcoming, isArchived: !v.liveNow && !v.isUpcoming && (v.title && !!v.title.match(/ライブ|配信|LIVE|live|生放送/i)), authorThumb: v.authorThumbnails ? v.authorThumbnails[0].url : `https://i.pravatar.cc/150?u=${encodeURIComponent(v.author||chName)}`, duration: v.lengthSeconds||0, published: v.publishedText||'', publishedTimestamp: v.published||0 }));
renderChannelLive(mapped, append);
} else {
document.getElementById('channel-live-loader')?.classList.add('hidden');
document.getElementById('channel-live-grid').innerHTML = '<div class="subs-empty">ライブ・配信動画が見つかりません</div>';
}
}
return;
}
const invData = await fetchFromInvidious(query, context, currentPage);
if (invData && invData.length > 0) {
isFetching = false; let videos = [];
invData.forEach(item => {
if (item.type === 'video' && item.videoId && !seenVideoIds.has(item.videoId)) {
if (context === 'shorts' && item.lengthSeconds > 61) return;
seenVideoIds.add(item.videoId);
videos.push({ id: item.videoId, title: item.title, channel: item.author, isShort: item.lengthSeconds > 0 && item.lengthSeconds <= 61, isLive: item.liveNow||false, authorThumb: item.authorThumbnails ? item.authorThumbnails[0].url : `https://i.pravatar.cc/150?u=${item.author}`, duration: item.lengthSeconds||0, published: item.publishedText||'' });
}
});
renderResults(videos, append);
if (context === 'search' && !append) fetchShortsForSearch(query);
if (context === 'trend' && !append) { fetchShortsForHome(); loadRecommendations(); }
return;
}
clearTimeout(captchaTimer);
captchaTimer = setTimeout(() => {
if (isFetching) {
const c = document.getElementById('hidden-cse-container');
c.style.display = 'block'; c.style.position = 'fixed'; c.style.top = '100px'; c.style.left = '50%'; c.style.transform = 'translateX(-50%)'; c.style.zIndex = '10000';
alert("ロボット確認が必要です。画面の指示に従ってください。");
}
}, 3000);
const element = google.search.cse.element.getElement('studyCse');
const decor = append ? ["", " pv", " hd"][Math.floor(Math.random()*3)] : "";
element.execute(query + decor + " site:youtube.com");
}

async function loadMoreChannelShorts() {
currentChannelShortsPage += 3;
document.getElementById('channel-shorts-more-btn').style.display = 'none';
document.getElementById('channel-shorts-loader').classList.remove('hidden');
const { shorts, hasMore } = await fetchChannelShortsMultiPage(currentChannelShortsName, currentChannelShortsPage);
renderChannelShorts(shorts, true);
const moreBtn = document.getElementById('channel-shorts-more-btn');
if (moreBtn) moreBtn.style.display = hasMore && shorts.length > 0 ? 'block' : 'none';
}

function updateChannelHeaderInfo(channelInfo) {
if (!channelInfo) return;
const nameEl = document.getElementById('channel-page-name'), handleEl = document.getElementById('channel-page-handle'), metaEl = document.getElementById('channel-page-meta'), iconEl = document.getElementById('channel-page-icon');
if (channelInfo.author) nameEl.innerText = channelInfo.author;
if (channelInfo.authorId) { handleEl.innerText = `@${channelInfo.authorId}`; currentChannelId = channelInfo.authorId; try { history.replaceState({ view: 'channel', channelId: channelInfo.authorId }, '', `/channel/${channelInfo.authorId}`); } catch(e) {} }
if (channelInfo.subCount) metaEl.innerText = `登録者数 ${formatSubCount(channelInfo.subCount)} • 動画 ${channelInfo.videoCount||'--'}件`;
if (channelInfo.authorThumbnails && channelInfo.authorThumbnails.length > 0) iconEl.src = channelInfo.authorThumbnails[channelInfo.authorThumbnails.length-1].url;
if (channelInfo.authorBanners && channelInfo.authorBanners.length > 0) {
const bestBanner = channelInfo.authorBanners.reduce((best, b) => (!best || (b.width || 0) > (best.width || 0)) ? b : best, null);
if (bestBanner && bestBanner.url) drawChannelBanner(bestBanner.url);
} else { drawChannelBanner(null); }
updateChannelSubscribeUI(nameEl.innerText);
}
function formatSubCount(n) {
if (!n) return '--';
if (n >= 1000000) return (n/1000000).toFixed(1)+'万人';
if (n >= 10000) return Math.floor(n/10000)+'万人';
if (n >= 1000) return (n/1000).toFixed(1)+'K人';
return n+'人';
}
function drawChannelBanner(bannerUrl) {
const img = document.getElementById('channel-banner-img'), canvas = document.getElementById('channel-banner-canvas');
if (bannerUrl) { img.src = bannerUrl; img.style.display = 'block'; canvas.style.display = 'none'; }
else { img.style.display = 'none'; canvas.style.display = 'block'; drawFallbackBanner(canvas); }
}
function drawFallbackBanner(canvas) {
canvas.width = canvas.offsetWidth || 1280; canvas.height = 180;
const ctx = canvas.getContext('2d');
const colors = [['#ff0000','#ff6b6b'],['#4285f4','#34a853'],['#ff6d00','#ffab00'],['#7c4dff','#e040fb'],['#00bcd4','#009688'],['#e91e63','#ff5722'],['#1a237e','#283593']];
const c = colors[Math.floor(Math.random()*colors.length)];
const grad = ctx.createLinearGradient(0,0,canvas.width,canvas.height);
grad.addColorStop(0,c[0]); grad.addColorStop(1,c[1]);
ctx.fillStyle = grad; ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.globalAlpha = 0.15; ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(canvas.width*0.8,canvas.height*0.3,120,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(canvas.width*0.2,canvas.height*0.9,80,0,Math.PI*2); ctx.fill();
ctx.globalAlpha = 1;
}

async function fetchShortsForHome() {
const container = document.getElementById('home-shorts-container'), section = document.getElementById('home-shorts');
if (!container || !section) return;
const queries = ["人気 #shorts","#shorts 人気","ショート 人気","shorts popular japan"];
let shorts = [];
for (const q of queries) {
const data = await fetchFromInvidious(q,"shorts",1);
if (data && data.length > 0) { shorts = data.filter(v => v.videoId && v.lengthSeconds >= 0 && v.lengthSeconds <= 65); if (shorts.length === 0) shorts = data.filter(v => v.videoId && (v.lengthSeconds <= 65 || v.isShort)); if (shorts.length > 0) break; }
}
if (shorts.length === 0) return;
container.innerHTML = '';
shorts.slice(0,10).forEach(v => {
const div = document.createElement('div'); div.className = 'home-short-card';
div.onclick = () => navigateToShortPage(v.videoId, v.title, v.author, v.authorThumbnails?.[0]?.url);
div.innerHTML = `<div class="home-short-thumb"><img src="https://i.ytimg.com/vi/${v.videoId}/hqdefault.jpg" loading="lazy" onerror="this.src='https://i.ytimg.com/vi/${v.videoId}/mqdefault.jpg'"></div><div class="home-short-title">${v.title}</div>`;
container.appendChild(div);
});
section.classList.remove('hidden');
}
async function fetchShortsForSearch(query) {
const data = await fetchFromInvidious(query + ' #shorts',"shorts",1);
if (data && data.length > 0) {
const shorts = data.filter(v => v.videoId && v.lengthSeconds > 0 && v.lengthSeconds <= 61).slice(0,8);
if (shorts.length === 0) return;
const container = document.getElementById('search-shorts-container');
container.innerHTML = '';
shorts.forEach(v => {
const div = document.createElement('div'); div.className = 'home-short-card';
div.onclick = () => navigateToShortPage(v.videoId, v.title, v.author, v.authorThumbnails?.[0]?.url);
div.innerHTML = `<div class="home-short-thumb"><img src="https://i.ytimg.com/vi/${v.videoId}/hqdefault.jpg" loading="lazy"></div><div class="home-short-title">${v.title}</div>`;
container.appendChild(div);
});
document.getElementById('search-shorts-section').classList.remove('hidden');
}
}

function navigateToShortPage(videoId, title, channel, thumb, noHistory = false) {
navigate('shorts', { videoId, title, channel, thumb, noHistory });
setTimeout(() => {
const container = document.getElementById('shorts-container');
container.innerHTML = ''; currentShortItems = [];
initShortObserver();
const targetVideo = { id: videoId, title: title||'', channel: channel||'', authorThumb: thumb || `https://i.pravatar.cc/80?u=${videoId}`, isShort: true };
renderShorts([targetVideo], false);
triggerSearch("#shorts 人気",'shorts',true);
}, 100);
}

// =================== ショートストリーム (3タイプ: NC/Edu/Manifest) ===================
function buildShortNocookieSrc(videoId) {
return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=1&loop=1&playlist=${videoId}&rel=0`;
}
function buildShortEduSrc(videoId, key) {
if (!key) return buildShortNocookieSrc(videoId);
const cfg = encodeURIComponent(JSON.stringify({enc: key, hideTitle: true}));
return `https://www.youtubeeducation.com/embed/${videoId}?autoplay=1&origin=https%3A%2F%2Fcreate.kahoot.it&embed_config=${cfg}`;
}

function renderShorts(videos, append = false) {
const container = document.getElementById('shorts-container');
if (!append) { container.innerHTML = ''; currentShortItems = []; initShortObserver(); }
currentShortItems = [...currentShortItems, ...videos];
videos.forEach(v => {
const item = document.createElement('div');
item.className = 'short-snap-item'; item.dataset.id = v.id;
const channelSafe = (v.channel||'').replace(/'/g,"\\'");
const titleSafe = (v.title||'').replace(/</g,'&lt;');
const initialSrc = buildShortNocookieSrc(v.id);
shortSrcMap[v.id] = initialSrc;
const likeCount = Math.floor(Math.random()*50000)+100;
const likeStr = likeCount >= 10000 ? (likeCount/1000).toFixed(0)+'K' : likeCount.toLocaleString();
const subbed = isSubscribed(v.channel||'');
item.innerHTML = `
<div class="short-center">
<div class="short-video-wrap" id="short-wrap-${v.id}">
<iframe src="about:blank" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<div class="short-stream-selector">
<button class="short-stream-btn ${shortStreamType===1?'active':''}" onclick="changeShortStream(1,'${v.id}')">NC</button>
<button class="short-stream-btn ${shortStreamType===2?'active':''}" onclick="changeShortStream(2,'${v.id}')">Edu</button>
<button class="short-stream-btn ${shortStreamType===3?'active':''}" onclick="changeShortStream(3,'${v.id}')">MF</button>
</div>
<div class="short-overlay-bottom">
<div class="short-channel" onclick="openChannel('${channelSafe}','${v.authorThumb||''}')">
<img src="${v.authorThumb||`https://i.pravatar.cc/80?u=${v.channel}`}" onerror="this.src='https://i.pravatar.cc/80?u=${encodeURIComponent(v.channel||'')}'">
<span class="short-channel-name">${v.channel||''}</span>
<button class="short-sub-btn ${subbed?'subscribed':''}" id="short-sub-btn-${v.id}" onclick="event.stopPropagation();toggleSubscribeFromShort('${channelSafe}','${v.authorThumb||''}',this)">${subbed?'登録済み':'登録'}</button>
</div>
<div class="short-title">${titleSafe}</div>
</div>
</div>
<div class="short-side-actions">
<div class="short-action-btn" onclick="openChannel('${channelSafe}','${v.authorThumb||''}')">
<div class="icon short-avatar-btn">
<img src="${v.authorThumb||`https://i.pravatar.cc/80?u=${v.channel}`}" style="width:48px;height:48px;border-radius:50%;object-fit:cover;" onerror="this.src='https://i.pravatar.cc/48?u=${encodeURIComponent(v.channel||'')}'">
</div>
</div>
<div class="short-action-btn" onclick="toggleShortLike(this)">
<div class="icon"><svg viewBox="0 0 24 24" style="fill:currentColor;stroke:none;"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></div>
<span class="short-like-count">${likeStr}</span>
</div>
<div class="short-action-btn" onclick="toggleShortDislike(this)">
<div class="icon"><svg viewBox="0 0 24 24" style="fill:currentColor;stroke:none;"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg></div>
<span>低評価</span>
</div>
<div class="short-action-btn" onclick="openShortComments('${v.id}','${titleSafe}')">
<div class="icon"><svg viewBox="0 0 24 24" style="fill:currentColor;stroke:none;"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"/></svg></div>
<span>コメント</span>
</div>
<div class="short-action-btn" onclick="shareShort('${v.id}')">
<div class="icon"><svg viewBox="0 0 24 24" style="fill:currentColor;stroke:none;"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/></svg></div>
<span>共有</span>
</div>
<div class="short-action-btn" onclick="playVideo('${v.id}','${titleSafe}','${channelSafe}','${v.authorThumb||''}')">
<div class="icon"><svg viewBox="0 0 24 24" style="fill:currentColor;stroke:none;"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg></div>
<span>詳細</span>
</div>
<div class="short-action-btn" onclick="downloadShort('${v.id}')">
<div class="icon"><svg viewBox="0 0 24 24" style="fill:currentColor;stroke:none;"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg></div>
<span>DL</span>
</div>
</div>
</div>
<div class="short-comments-panel" id="short-comments-panel-${v.id}" style="display:none;position:absolute;bottom:0;left:0;right:0;height:65%;background:var(--bg-color);border-radius:16px 16px 0 0;z-index:20;overflow-y:auto;padding:16px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;font-weight:bold;">
<span>コメント</span>
<button onclick="closeShortComments('${v.id}')" style="background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-color);">×</button>
</div>
<div id="short-comments-content-${v.id}" class="loader">読み込み中...</div>
</div>
`;
container.appendChild(item);
observeShortItem(item);
});
document.getElementById('shorts-loader').classList.add('hidden');
}

function downloadShort(videoId) {
window.open(`https://cobalt.tools/?url=https://www.youtube.com/shorts/${videoId}`, '_blank');
}

// changeShortStream: 3タイプのみ (m3u8=type4 削除、3=Manifest Hunter)
async function changeShortStream(type, targetVideoId) {
shortStreamType = type;
const config = getAppConfig(); config.shortStream = type;
localStorage.setItem('study2525_config', JSON.stringify(config));

for (const v of currentShortItems) {
const wrap = document.getElementById(`short-wrap-${v.id}`);
if (!wrap) continue;
wrap.querySelectorAll('.short-stream-btn').forEach((b, i) => b.classList.toggle('active', i+1 === type));
}

if (type === 1) {
// Nocookie
for (const v of currentShortItems) {
const newSrc = buildShortNocookieSrc(v.id);
shortSrcMap[v.id] = newSrc;
const wrap = document.getElementById(`short-wrap-${v.id}`);
if (!wrap) continue;
wrap.querySelectorAll('video, audio, .short-gv-loading').forEach(el => el.remove());
let iframe = wrap.querySelector('iframe');
if (!iframe) { iframe = document.createElement('iframe'); iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'; iframe.allowFullscreen = true; wrap.insertBefore(iframe, wrap.firstChild); }
iframe.style.display = ''; iframe.src = newSrc;
}
} else if (type === 2) {
// Edu
if (!currentEduKey) currentEduKey = await fetchKahootKey();
if (!currentEduKey) { alert("Edu準備中"); shortStreamType = 1; changeShortStream(1, targetVideoId); return; }
for (const v of currentShortItems) {
const newSrc = buildShortEduSrc(v.id, currentEduKey);
shortSrcMap[v.id] = newSrc;
const wrap = document.getElementById(`short-wrap-${v.id}`);
if (!wrap) continue;
wrap.querySelectorAll('video, audio, .short-gv-loading').forEach(el => el.remove());
let iframe = wrap.querySelector('iframe');
if (!iframe) { iframe = document.createElement('iframe'); iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'; iframe.allowFullscreen = true; wrap.insertBefore(iframe, wrap.firstChild); }
iframe.style.display = '';
if (iframe.src && iframe.src !== 'about:blank') iframe.src = newSrc;
}
} else if (type === 3) {
// Manifest Hunter (siawaseok deepSearch)
for (const v of currentShortItems) {
const wrap = document.getElementById(`short-wrap-${v.id}`);
if (!wrap) continue;
const existingIframe = wrap.querySelector('iframe');
if (existingIframe) { existingIframe.src = 'about:blank'; existingIframe.style.display = 'none'; }
wrap.querySelectorAll('video, audio, .short-gv-loading').forEach(el => el.remove());
const loadingDiv = document.createElement('div');
loadingDiv.className = 'short-gv-loading';
loadingDiv.innerHTML = '<div class="spinner"></div><span style="color:#0f0;font-family:monospace;">🚨 Manifest探索中...</span>';
wrap.appendChild(loadingDiv);
}
for (const v of currentShortItems) {
const wrap = document.getElementById(`short-wrap-${v.id}`);
if (!wrap) continue;
(async () => {
const loadingEl = wrap.querySelector('.short-gv-loading');
// manifestHunt使用
const huntResult = await manifestHunt(v.id);
let videoUrl = null, audioUrl = null, isManifest = false, manifestUrl = null;

if (huntResult) {
if (huntResult.type === 'manifest' && huntResult.streams.length > 0) {
manifestUrl = huntResult.streams[0].url;
isManifest = true;
} else if (huntResult.type === 'formats' && huntResult.streams.length > 0) {
const { mp4Pairs } = parseSiawaseokFormats(huntResult.streams);
const m4aFormats = huntResult.streams.filter(f => (f.ext==='m4a'||f.acodec==='mp4a.40.2') && f.url && (!f.vcodec||f.vcodec==='none'));
const target = mp4Pairs.find(p => p.label==='720p') || mp4Pairs.find(p => p.label==='480p') || mp4Pairs.find(p => p.label==='360p') || mp4Pairs[0];
if (target) { videoUrl = target.videoFmt.url; audioUrl = target.audioFmt ? target.audioFmt.url : null; }
}
}

if (!videoUrl && !isManifest) {
const invFormats = await fetchGoogleVideoStreamsInvidious(v.id);
if (invFormats) {
const result = buildGoogleVideoPlayer(invFormats);
if (result && result.type === 'dual') { videoUrl = result.videoUrl; audioUrl = result.audioUrl; }
else if (result && result.type === 'single') videoUrl = result.url;
}
}

if (loadingEl) loadingEl.remove();
const iframe = wrap.querySelector('iframe');

if (isManifest && manifestUrl) {
if (iframe) iframe.style.display = 'none';
const vid = document.createElement('video');
vid.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;';
vid.autoplay = true; vid.loop = true; vid.playsInline = true;
wrap.appendChild(vid);
if (typeof Hls !== 'undefined' && Hls.isSupported() && (manifestUrl.includes('.m3u8') || manifestUrl.includes('hls'))) {
const hls = new Hls(); hls.loadSource(manifestUrl); hls.attachMedia(vid);
hls.on(Hls.Events.MANIFEST_PARSED, () => { vid.play().catch(() => {}); });
hls.on(Hls.Events.ERROR, () => { vid.remove(); if (iframe) { iframe.style.display=''; iframe.src=buildShortNocookieSrc(v.id); } });
} else {
vid.src = manifestUrl; vid.play().catch(() => {});
vid.onerror = () => { vid.remove(); if (iframe) { iframe.style.display=''; iframe.src=buildShortNocookieSrc(v.id); } };
}
} else if (videoUrl) {
if (iframe) iframe.style.display = 'none';
const vid = document.createElement('video');
vid.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;';
vid.autoplay = true; vid.loop = true; vid.muted = !audioUrl; vid.playsInline = true; vid.crossOrigin = 'anonymous';
vid.innerHTML = `<source src="${videoUrl}" type="video/mp4">`;
wrap.appendChild(vid);
if (audioUrl) {
const aud = document.createElement('audio'); aud.style.display='none'; aud.loop=true; aud.crossOrigin='anonymous';
aud.innerHTML=`<source src="${audioUrl}" type="audio/mp4">`; wrap.appendChild(aud); attachAudioVideoSync(vid, aud);
}
vid.play().catch(() => { vid.muted = true; vid.play(); });
} else {
if (iframe) { iframe.style.display=''; iframe.src=buildShortNocookieSrc(v.id); }
}
})();
}
}
}

// =================== レンダリング ===================
function formatDuration(sec) {
if (!sec || sec <= 0) return '';
const h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60), s = sec%60;
if (h > 0) return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
return `${m}:${String(s).padStart(2,'0')}`;
}
function formatTimestamp(ts) {
if (!ts || ts === 0) return '';
const d = new Date(ts*1000);
return `${d.getFullYear()}/${String(d.getMonth()+1).padStart(2,'0')}/${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
}
function renderResults(videos, append = false) {
if (searchContext === 'related') { renderRelatedVideos(videos, append); return; }
if (searchContext === 'shorts') { renderShorts(videos, append); return; }
if (searchContext === 'channel-shorts') { renderChannelShorts(videos, append); return; }
if (searchContext === 'channel-live') { renderChannelLive(videos, append); return; }
if (searchContext === 'search') { renderSearchResults(videos, append); return; }
let containerId = 'home-grid';
if (searchContext === 'channel' || searchContext === 'channel-home') containerId = 'channel-home-grid';
if (searchContext === 'channel-videos') containerId = 'channel-grid';
const container = document.getElementById(containerId);
if (!container) return;
if (!append) container.innerHTML = '';
videos.forEach(v => {
const card = document.createElement('div'); card.className = 'video-card';
const isLive = v.isLive || (v.title && (v.title.includes('ライブ')||v.title.includes('LIVE')||v.title.includes('live')||v.title.includes('配信')));
const isArchived = v.isArchived||false;
const durStr = formatDuration(v.duration), dateStr = v.publishedTimestamp ? formatTimestamp(v.publishedTimestamp) : '';
card.onclick = () => playVideo(v.id, v.title, v.channel, v.authorThumb);
card.innerHTML = `<div class="thumbnail-container"><img src="https://i.ytimg.com/vi/${v.id}/mqdefault.jpg" loading="lazy">${v.isLive ? '<span class="live-thumb-badge">🔴 配信中</span>' : isArchived ? `<span class="archived-badge">📅 配信済み${dateStr ? ' '+dateStr : ''}</span>` : (durStr ? `<span class="duration-badge">${durStr}</span>` : '')}</div><div class="video-info"><div class="channel-avatar" onclick="event.stopPropagation(); openChannel('${v.channel.replace(/'/g,"\\'")}','${v.authorThumb}')"><img src="${v.authorThumb}" loading="lazy"></div><div class="video-details"><div class="video-title">${v.title}</div><div class="video-meta"><span style="cursor:pointer;" onclick="event.stopPropagation(); openChannel('${v.channel.replace(/'/g,"\\'")}','${v.authorThumb}')">${v.channel}</span>${v.isLive ? '<span style="color:#ff0000;font-weight:bold;">● ライブ配信中</span>' : isArchived && dateStr ? `<span>${dateStr} 配信済み</span>` : (v.published ? `<span>${v.published}</span>` : '')}</div></div></div>`;
container.appendChild(card);
});
document.getElementById('home-loader')?.classList.add('hidden');
document.getElementById('channel-loader')?.classList.add('hidden');
if ((searchContext === 'channel' || searchContext === 'channel-home') && videos.length > 0 && !append) renderChannelFeatured(videos[0]);
}

function renderSearchResults(videos, append = false) {
const container = document.getElementById('search-results-list');
if (!container) return;
if (!append) container.innerHTML = '';
videos.forEach(v => {
const item = document.createElement('div'); item.className = 'search-result-item';
const isLive = v.isLive || (v.title && (v.title.includes('ライブ')||v.title.includes('LIVE')||v.title.includes('live')||v.title.includes('配信')));
const isArchived = v.isArchived||false, durStr = formatDuration(v.duration), dateStr = v.publishedTimestamp ? formatTimestamp(v.publishedTimestamp) : '';
const channelSafe = v.channel.replace(/'/g,"\\'");
item.onclick = () => playVideo(v.id, v.title, v.channel, v.authorThumb);
item.innerHTML = `<div class="search-result-thumb"><img src="https://i.ytimg.com/vi/${v.id}/mqdefault.jpg" loading="lazy">${v.isLive ? '<span class="live-thumb-badge">🔴 配信中</span>' : isArchived ? '<span class="archived-badge">📅 配信済み</span>' : (durStr ? `<span class="duration-badge">${durStr}</span>` : '')}</div><div class="search-result-info"><div class="search-result-title">${v.title}</div><div class="search-result-meta">${v.isLive ? '<span style="color:#ff0000;font-weight:bold;">● ライブ配信中</span>' : isArchived && dateStr ? `<span>${dateStr} 配信済み</span>` : (durStr ? `<span>${durStr}</span>` : '')}${!v.isLive && v.published ? `<span>• ${v.published}</span>` : ''}</div><div class="search-result-channel" onclick="event.stopPropagation(); openChannel('${channelSafe}','${v.authorThumb}')"><div class="search-result-channel-icon"><img src="${v.authorThumb}" loading="lazy"></div><span class="search-result-channel-name">${v.channel}</span></div></div>`;
container.appendChild(item);
});
document.getElementById('search-loader')?.classList.add('hidden');
}
function renderChannelFeatured(v) {
const el = document.getElementById('channel-featured-video');
if (!el || !v) return;
el.innerHTML = `<div class="channel-featured-video" onclick="playVideo('${v.id}','${v.title.replace(/'/g,"\\'")}','${v.channel.replace(/'/g,"\\'")}','${v.authorThumb}')"><div class="channel-featured-thumb"><img src="https://i.ytimg.com/vi/${v.id}/hqdefault.jpg"></div><div class="channel-featured-info"><div class="channel-featured-title">${v.title}</div><div style="font-size:13px;color:var(--text-secondary);margin-top:8px;">${v.channel}</div>${v.published ? `<div style="font-size:12px;color:var(--text-secondary);">${v.published}</div>` : ''}</div></div>`;
}
function renderChannelShorts(videos, append = false) {
const grid = document.getElementById('channel-shorts-grid');
if (!grid) return;
if (!append) grid.innerHTML = '';
if (videos.length === 0 && !append) { grid.innerHTML = '<div style="padding:24px;color:var(--text-secondary);">ショート動画が見つかりませんでした</div>'; document.getElementById('channel-shorts-loader')?.classList.add('hidden'); return; }
const seen = new Set(Array.from(grid.querySelectorAll('[data-vid]')).map(el => el.dataset.vid));
videos.forEach(v => {
if (seen.has(v.id)) return; seen.add(v.id);
const card = document.createElement('div'); card.className = 'channel-short-card'; card.dataset.vid = v.id;
card.onclick = () => navigateToShortPage(v.id, v.title, v.channel, v.authorThumb);
card.innerHTML = `<div class="channel-short-thumb"><img src="https://i.ytimg.com/vi/${v.id}/hqdefault.jpg" loading="lazy">${v.duration > 0 ? `<span class="duration-badge">${formatDuration(v.duration)}</span>` : ''}</div><div class="channel-short-title">${v.title}</div>`;
grid.appendChild(card);
});
document.getElementById('channel-shorts-loader')?.classList.add('hidden');
}
function renderChannelLive(videos, append = false) {
const container = document.getElementById('channel-live-grid');
if (!container) return;
if (!append) container.innerHTML = '';
if (videos.length === 0 && !append) { container.innerHTML = '<div class="subs-empty">ライブ・配信動画が見つかりません</div>'; document.getElementById('channel-live-loader')?.classList.add('hidden'); return; }
const sorted = [...videos].sort((a,b) => { const sA=a.isLive&&!a.isUpcoming?2:a.isUpcoming?1:0, sB=b.isLive&&!b.isUpcoming?2:b.isUpcoming?1:0; return sB-sA; });
sorted.forEach(v => {
const card = document.createElement('div'); card.className = 'video-card';
const dateStr = v.publishedTimestamp ? formatTimestamp(v.publishedTimestamp) : '';
card.onclick = () => playVideo(v.id, v.title, v.channel, v.authorThumb);
card.innerHTML = `<div class="thumbnail-container"><img src="https://i.ytimg.com/vi/${v.id}/mqdefault.jpg">${v.isLive&&!v.isUpcoming ? '<span class="live-thumb-badge">🔴 配信中</span>' : v.isUpcoming ? '<span class="live-thumb-badge" style="background:#1a73e8;">🕐 待機中</span>' : `<span class="archived-badge">📅 配信済み${dateStr?' '+dateStr:''}</span>`}</div><div class="video-info"><div class="channel-avatar"><img src="${v.authorThumb||''}" onerror="this.src='https://i.pravatar.cc/40?u=${encodeURIComponent(v.channel)}'"></div><div class="video-details"><div class="video-title">${v.title}</div><div class="video-meta">${v.isLive&&!v.isUpcoming ? '<span style="color:#ff0000;font-weight:bold;">● ライブ配信中</span>' : v.isUpcoming ? '<span style="color:#1a73e8;font-weight:bold;">⏰ 配信予定</span>' : (dateStr ? `<span>${dateStr} 配信済み</span>` : `<span>${v.published||''}</span>`)}</div></div></div>`;
container.appendChild(card);
});
document.getElementById('channel-live-loader')?.classList.add('hidden');
}
let channelFilterMode = 'latest';
function setChannelFilter(btn, mode) {
document.querySelectorAll('.channel-filter-chip').forEach(c => c.classList.remove('active')); btn.classList.add('active'); channelFilterMode = mode;
const grid = document.getElementById('channel-grid'), cards = Array.from(grid.querySelectorAll('.video-card'));
if (mode === 'popular') cards.sort(() => Math.random()-0.5); else if (mode === 'oldest') cards.reverse();
grid.innerHTML = ''; cards.forEach(c => grid.appendChild(c));
}

window.addEventListener('scroll', () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000 && !isFetching) {
if (currentView === 'home' && getAppConfig().trend) { document.getElementById('home-loader').classList.remove('hidden'); triggerSearch(lastQuery||"人気",'trend',true); }
else if (currentView === 'search') { document.getElementById('search-loader').classList.remove('hidden'); triggerSearch(lastQuery,'search',true); }
else if (currentView === 'watch') { document.getElementById('related-loader').classList.remove('hidden'); triggerSearch(currentVideoTitle.substring(0,20),'related',true); }
else if (currentView === 'channel') { if (currentChannelTab === 'videos') { document.getElementById('channel-loader').classList.remove('hidden'); triggerSearch(lastQuery,'channel-videos',true); } }
}
});

document.addEventListener('DOMContentLoaded', () => {
const sfp = document.getElementById('shorts-full-page');
if (sfp) {
sfp.addEventListener('scroll', () => {
if (sfp.scrollTop + sfp.clientHeight >= sfp.scrollHeight - 400 && !isFetching) {
document.getElementById('shorts-loader').classList.remove('hidden');
triggerSearch("#shorts",'shorts',true);
}
});
}
});

function loadTrend() { triggerSearch("人気 日本",'trend'); }
function handleWelcomeSearch(e) {
e.preventDefault(); const q = document.getElementById('welcome-search-input').value;
if(!q) return; document.getElementById('search-input').value = q; saveSettings(); handleSearch(e, q);
}
function handleCategorySearch(cat, btn) {
document.querySelectorAll('.category-chip').forEach(c => c.classList.remove('active')); btn.classList.add('active');
let query = cat === 'すべて' ? '人気' : cat;
document.getElementById('search-input').value = cat === 'すべて' ? '' : cat;
navigate('search');
document.getElementById('search-shorts-section').classList.add('hidden');
document.getElementById('search-shorts-container').innerHTML = '';
document.getElementById('search-results-list').innerHTML = '';
triggerSearch(query,'search');
if (cat !== 'すべて') fetchShortsForSearch(query);
}
function handleSearch(e, externalQuery = null) {
if(e) e.preventDefault();
const q = externalQuery || document.getElementById('search-input').value;
if(!q) return;
saveSearchHistory(q);
lastQuery = q;
document.getElementById('search-shorts-section').classList.add('hidden');
document.getElementById('search-shorts-container').innerHTML = '';
document.getElementById('search-results-list').innerHTML = '';
try { history.pushState({ view: 'search', query: q }, '', `/search?q=${encodeURIComponent(q)}`); } catch(e) {}
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.getElementById('view-search')?.classList.add('active');
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
document.getElementById('categories-bar').style.display = 'flex';
document.getElementById('main-content').style.padding = '24px';
document.getElementById('main-content').style.marginTop = '112px';
currentView = 'search';
document.getElementById('sidebar').classList.remove('open');
document.getElementById('main-content').classList.remove('sidebar-open');
triggerSearch(q,'search');
}

let currentChannelData = { name: '', thumb: '' };

function openChannel(channelName, thumb = null) {
currentChannelData = { name: channelName, thumb: thumb || `https://i.pravatar.cc/150?u=${channelName}` };
currentChannelId = null;
navigate('channel');
document.getElementById('channel-page-name').innerText = channelName;
document.getElementById('channel-page-handle').innerText = `@${channelName.toLowerCase().replace(/\s/g,'')}`;
document.getElementById('channel-page-meta').innerText = `登録者数 -- • 動画 --件`;
const iconEl = document.getElementById('channel-page-icon');
iconEl.src = currentChannelData.thumb;
iconEl.onerror = () => { iconEl.src = `https://i.pravatar.cc/150?u=${encodeURIComponent(channelName)}`; };
const canvas = document.getElementById('channel-banner-canvas'), bannerImg = document.getElementById('channel-banner-img');
bannerImg.style.display = 'none'; canvas.style.display = 'block';
setTimeout(() => drawFallbackBanner(canvas), 50);
updateChannelSubscribeUI(channelName);
currentChannelShortsPage = 1;

fetchChannelInfoFromInvidious(channelName).then(info => {
if (!info) return;
if (info.authorId) {
currentChannelId = info.authorId;
document.getElementById('channel-page-handle').innerText = `@${info.authorId}`;
try { history.replaceState({ view: 'channel', channelId: info.authorId, channelName }, '', `/channel/${info.authorId}`); } catch(e) {}
}
if (info.subCount) document.getElementById('channel-page-meta').innerText = `登録者数 ${formatSubCount(info.subCount)} • 動画 ${info.videoCount||'--'}件`;
if (info.authorThumbnails && info.authorThumbnails.length > 0) {
const bestThumb = info.authorThumbnails[info.authorThumbnails.length-1];
if (bestThumb) iconEl.src = bestThumb.url;
}
if (info.authorBanners && info.authorBanners.length > 0) {
const bestBanner = info.authorBanners.reduce((best, b) => (!best || (b.width||0) > (best.width||0)) ? b : best, null);
if (bestBanner && bestBanner.url) drawChannelBanner(bestBanner.url);
}
}).catch(() => {});

switchChannelTab('home');
}

function switchChannelTab(tab) {
currentChannelTab = tab;
['home','shorts','videos','live'].forEach(t => {
const tabBtn = document.getElementById(`ch-tab-${t}`), tabContent = document.getElementById(`channel-tab-${t}`);
if (tabBtn) tabBtn.classList.toggle('active', t === tab);
if (tabContent) tabContent.style.display = t === tab ? 'block' : 'none';
});
const name = currentChannelData.name; lastQuery = name;
if (tab === 'home') { document.getElementById('channel-home-grid').innerHTML = ''; document.getElementById('channel-featured-video').innerHTML = ''; triggerSearch(name,'channel-home'); }
else if (tab === 'shorts') { document.getElementById('channel-shorts-grid').innerHTML = ''; document.getElementById('channel-shorts-loader').classList.remove('hidden'); currentChannelShortsPage = 1; triggerSearch(name,'channel-shorts'); }
else if (tab === 'videos') { document.getElementById('channel-grid').innerHTML = ''; document.getElementById('channel-loader').classList.remove('hidden'); triggerSearch(name,'channel-videos'); }
else if (tab === 'live') { document.getElementById('channel-live-grid').innerHTML = ''; document.getElementById('channel-live-loader').classList.remove('hidden'); triggerSearch(name,'channel-live'); }
}

// =================== 動画再生 ===================
async function playVideo(id, title, channel = "投稿者", thumb = null) {
currentVideoId = id; currentVideoTitle = title;
currentChannelName = channel; currentChannelThumb = thumb || `https://i.pravatar.cc/150?u=${channel}`;
currentGVFormats = null; currentGVAllFormats = null; selectedQuality = null;
navigate('watch', { videoId: id, title, channel, thumb: thumb||null });
document.getElementById('watch-title-text').innerText = title;
document.getElementById('watch-channel-name').innerText = channel;
const wci = document.getElementById('watch-channel-icon');
wci.src = currentChannelThumb; wci.onerror = () => wci.src = `https://i.pravatar.cc/150?u=${encodeURIComponent(channel)}`;
document.getElementById('watch-channel-trigger').onclick = () => openChannel(channel, currentChannelThumb);
document.getElementById('related-videos').innerHTML = '';
document.getElementById('api-views').innerText = "---"; document.getElementById('api-likes').innerText = "---";
document.getElementById('api-date').innerText = ""; document.getElementById('api-desc').innerHTML = "概要欄を読み込んでいます...";
document.getElementById('quality-wrap').style.display = 'none';
updateWatchSubscribeUI(channel);
setTimeout(() => switchStream(getAppConfig().stream), 50);
saveToLocal('history',{id,title});
triggerSearch(title.substring(0,20),'related'); fetchComments(id); fetchVideoApiDetails(id);
}
function toggleDescription() {
const desc = document.getElementById('api-desc'); desc.classList.toggle('expanded');
document.getElementById('desc-toggle-text').innerText = desc.classList.contains('expanded') ? "一部を表示" : "続きを読む";
}
async function fetchVideoApiDetails(videoId) {
try {
const res = await fetch(buildFetchUrl(`https://api.aijimy.com/get?code=get-youtube-videodata&text=${videoId}`));
const t = await res.text();
const views = (t.match(/再生回数:\s*(\d+)/)||[])[1], likes = (t.match(/高評価数:\s*(\d+)/)||[])[1];
const date = (t.match(/公開日:\s*(.*?)\s*再生回数:/)||[])[1], des = (t.match(/概要欄:\s*([\s\S]*?)\s*公開日:/)||[])[1];
document.getElementById('api-views').innerText = views ? parseInt(views).toLocaleString() : "---";
document.getElementById('api-likes').innerText = likes ? parseInt(likes).toLocaleString() : "---";
if(date) document.getElementById('api-date').innerText = ` • ${date.trim()}`;
document.getElementById('api-desc').innerHTML = des ? des.trim().replace(/\n/g,'<br>') : "概要はありません。";
} catch(e) { document.getElementById('api-desc').innerText = "取得失敗"; }
}
async function fetchKahootKey() {
try { const res = await fetch(buildFetchUrl(KAHOOT_KEY_URL)); if(res.ok) { const data = await res.json(); return data.key||data.enc||data; } } catch(e) {}
if(getAppConfig().proxy) for (const proxy of CORS_PROXIES) try { const res = await fetch(proxy + encodeURIComponent(KAHOOT_KEY_URL)); if(res.ok) { const data = await res.json(); return data.key||data.enc||data; } } catch(e) {}
return null;
}
function buildEduUrl(videoId, enc) {
const cfg = encodeURIComponent(JSON.stringify({enc: enc, hideTitle: true}));
return `https://www.youtubeeducation.com/embed/${videoId}?autoplay=1&origin=https%3A%2F%2Fcreate.kahoot.it&embed_config=${cfg}`;
}

// switchStream: 3タイプのみ (type4=m3u8削除)
async function switchStream(type) {
[1,2,3].forEach(i => { const btn = document.getElementById(`btn-stream-${i}`); if (btn) btn.classList.toggle('active', type===i); });
document.getElementById('quality-wrap').style.display = 'none';
currentGVFormats = null; currentGVAllFormats = null;
const wrapper = document.getElementById('player-wrapper');
if (type === 1) {
wrapper.innerHTML = '<iframe id="yt-player" allow="autoplay; fullscreen" allowfullscreen></iframe>';
await new Promise(r => setTimeout(r, 80));
const iframe = document.getElementById('yt-player');
if (iframe) iframe.src = `https://www.youtube-nocookie.com/embed/${currentVideoId}?autoplay=1&rel=0&start=0`;
} else if (type === 2) {
wrapper.innerHTML = '<iframe id="yt-player" allow="autoplay; fullscreen" allowfullscreen></iframe>';
const key = await fetchKahootKey();
if(key) {
await new Promise(r => setTimeout(r, 80));
const iframe = document.getElementById('yt-player');
if (iframe) iframe.src = buildEduUrl(currentVideoId, key);
} else { alert("Edu準備中"); switchStream(1); }
} else if (type === 3) {
// Manifest Hunter
await setupWatchManifest(currentVideoId);
}
}

function renderRelatedVideos(videos, append = false) {
const container = document.getElementById('related-videos');
const html = videos.map(v => {
const isLive = v.isLive || (v.title && (v.title.includes('ライブ')||v.title.includes('LIVE')));
const isArchived = v.isArchived||false;
return `<div class="related-video" onclick="playVideo('${v.id}','${v.title.replace(/'/g,"\\'")}','${v.channel.replace(/'/g,"\\'")}')"><div class="related-thumb"><img src="https://i.ytimg.com/vi/${v.id}/mqdefault.jpg">${isLive ? '<span class="live-thumb-badge">🔴</span>' : isArchived ? '<span class="archived-badge">配信済</span>' : ''}</div><div class="related-info"><div class="related-title">${v.title}</div><div style="font-size:12px;color:var(--text-secondary);">${v.channel}</div></div></div>`;
}).join('');
if (append) container.insertAdjacentHTML('beforeend', html); else container.innerHTML = html;
document.getElementById('related-loader').classList.add('hidden');
}

// =================== ナビゲーション ===================
function navigate(viewName, opts = {}) {
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.getElementById(`view-${viewName}`)?.classList.add('active');
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
if(document.getElementById(`nav-${viewName}`)) document.getElementById(`nav-${viewName}`).classList.add('active');
const showCats = ['home','search','history','subscriptions'].includes(viewName);
document.getElementById('categories-bar').style.display = showCats ? 'flex' : 'none';
if (viewName === 'shorts') {
document.getElementById('main-content').style.padding = '0';
document.getElementById('main-content').style.marginTop = '56px';
} else {
document.getElementById('main-content').style.padding = '24px';
document.getElementById('main-content').style.marginTop = showCats ? '112px' : '56px';
}
currentView = viewName;
if (viewName !== 'watch') {
const wrapper = document.getElementById('player-wrapper');
if(wrapper) wrapper.innerHTML = '<iframe id="yt-player" allow="autoplay; fullscreen" allowfullscreen></iframe>';
currentGVFormats = null; currentGVAllFormats = null;
}
if (viewName !== 'shorts') {
document.querySelectorAll('#shorts-container .short-snap-item').forEach(item => {
const iframe = item.querySelector('iframe'); if (iframe) iframe.src = 'about:blank';
item.querySelectorAll('video, audio').forEach(el => { el.pause(); el.src = ''; });
});
if (shortObserver) { shortObserver.disconnect(); shortObserver = null; }
}
if (viewName === 'shorts' && document.getElementById('shorts-container').children.length === 0) {
document.getElementById('shorts-loader').classList.remove('hidden');
Object.keys(shortSrcMap).forEach(k => delete shortSrcMap[k]);
shortStreamType = getAppConfig().shortStream || 1;
triggerSearch("#shorts 人気",'shorts');
}
if (viewName === 'history') renderLocalList('history');
if (viewName === 'settings') { renderSettingsSubsList(); syncSettings(getAppConfig()); }
if (viewName === 'subscriptions') renderSubscriptionsPage();
window.scrollTo(0, 0);
document.getElementById('sidebar').classList.remove('open');
document.getElementById('main-content').classList.remove('sidebar-open');

if (!opts.noHistory) {
let url = '/';
if (viewName === 'home') url = '/home';
else if (viewName === 'history') url = '/feed/history';
else if (viewName === 'settings') url = '/setting';
else if (viewName === 'subscriptions') url = '/feed/subscriptions';
else if (viewName === 'search') url = lastQuery ? `/search?q=${encodeURIComponent(lastQuery)}` : '/search';
else if (viewName === 'channel') url = currentChannelId ? `/channel/${currentChannelId}` : '/channel';
else if (viewName === 'welcome') url = '/';
else if (viewName === 'watch' && opts.videoId) url = `/watch?v=${opts.videoId}`;
else if (viewName === 'shorts' && opts.videoId) url = `/shorts/${opts.videoId}`;
else if (viewName === 'shorts') url = '/shorts';
try { history.pushState({ view: viewName, ...opts }, '', url); } catch(e) {}
}
}

window.addEventListener('popstate', (e) => {
if (!e.state) { navigate('home', { noHistory: true }); return; }
const { view, videoId, title, channel, thumb, query, channelId, channelName } = e.state;
if (view === 'watch' && videoId) playVideo(videoId, title||'', channel||'', thumb||null, true);
else if (view === 'shorts' && videoId) navigateToShortPage(videoId, title, channel, thumb, true);
else if (view === 'channel') {
if (channelName) openChannel(channelName, null);
else navigate('channel', { noHistory: true });
}
else if (view === 'search') {
const searchQ = query || new URLSearchParams(location.search).get('q') || '';
if (searchQ) {
document.getElementById('search-input').value = searchQ;
navigate('search', { noHistory: true });
triggerSearch(searchQ, 'search');
} else navigate('search', { noHistory: true });
}
else if (view) navigate(view, { noHistory: true });
});

function parseInitialUrl() {
const path = location.pathname + location.search;
const watchMatch = path.match(/\/watch\?v=([a-zA-Z0-9_-]{11})/);
const shortsMatch = path.match(/\/shorts\/([a-zA-Z0-9_-]{11})/);
const searchMatch = path.match(/\/search\?q=(.+)/);
const channelMatch = path.match(/\/channel\/([a-zA-Z0-9_@-]+)/);
if (watchMatch) return { view: 'watch', videoId: watchMatch[1] };
if (shortsMatch) return { view: 'shorts', videoId: shortsMatch[1] };
if (searchMatch) return { view: 'search', query: decodeURIComponent(searchMatch[1]) };
if (channelMatch) return { view: 'channel', channelId: channelMatch[1] };
if (path === '/feed/history') return { view: 'history' };
if (path === '/setting' || path === '/settings') return { view: 'settings' };
if (path === '/feed/subscriptions') return { view: 'subscriptions' };
if (path === '/home' || path === '/') return { view: 'home' };
return null;
}

function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
document.getElementById('main-content').classList.toggle('sidebar-open');
}
function saveToLocal(key, video) {
let list = JSON.parse(localStorage.getItem(key) || '[]');
list = list.filter(v => v.id !== video.id); list.unshift(video);
localStorage.setItem(key, JSON.stringify(list.slice(0, 50)));
}
function renderLocalList(type) {
document.getElementById(`${type}-grid`).innerHTML = JSON.parse(localStorage.getItem(type)||'[]').map(v => `<div class="video-card" onclick="playVideo('${v.id}','${v.title.replace(/'/g,"\\'")}','YouTube Channel')"><div class="thumbnail-container"><img src="https://i.ytimg.com/vi/${v.id}/mqdefault.jpg"></div><div class="video-details" style="padding:10px;"><div class="video-title">${v.title}</div></div></div>`).join('');
}

async function fetchComments(videoId) {
const container = document.getElementById('comments-container');
container.innerHTML = '<div class="loader">コメントを読み込み中...</div>';
const commentApis = [
async (id) => {
const res = await fetch(`https://inv.nadeko.net/api/v1/comments/${id}`, { signal: AbortSignal.timeout(8000) });
if (!res.ok) throw new Error('failed');
const data = await res.json();
if (!data.comments || data.comments.length === 0) throw new Error('no comments');
return data.comments.map(c => ({ author: c.author||'名無し', content: c.content||c.contentHtml?.replace(/<[^>]+>/g,'')||'', avatar: c.authorThumbnails?.[c.authorThumbnails.length-1]?.url||'', likes: c.likeCount||0 }));
},
async (id) => {
for (const instance of INVIDIOUS_INSTANCES.slice(0,6)) {
try {
const res = await fetch(`${instance}/api/v1/comments/${id}`, { signal: AbortSignal.timeout(6000) });
if (!res.ok) continue;
const data = await res.json();
if (!data.comments || data.comments.length === 0) continue;
return data.comments.map(c => ({ author: c.author||'名無し', content: c.content||c.contentHtml?.replace(/<[^>]+>/g,'')||'', avatar: c.authorThumbnails?.[c.authorThumbnails.length-1]?.url||'', likes: c.likeCount||0 }));
} catch(e) {}
}
throw new Error('all failed');
}
];
let comments = null;
for (const api of commentApis) { try { comments = await api(videoId); if (comments && comments.length > 0) break; } catch(e) {} }
if (!comments || comments.length === 0) {
container.innerHTML = '<div style="color:var(--text-secondary);padding:8px;">コメントを読み込めませんでした。</div>';
document.getElementById('comment-count').innerText = '0'; return;
}
document.getElementById('comment-count').innerText = comments.length;
container.innerHTML = comments.slice(0,20).map(c => `<div class="comment"><img class="comment-avatar" src="${c.avatar}" onerror="this.src='https://i.pravatar.cc/40?u=${encodeURIComponent(c.author)}'"><div class="comment-body"><div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;"><span style="font-weight:bold;font-size:13px;">${c.author}</span>${c.likes > 0 ? `<span style="font-size:12px;color:var(--text-secondary);">👍 ${c.likes.toLocaleString()}</span>` : ''}</div><div style="font-size:14px;line-height:1.5;">${(c.content||'').replace(/\n/g,'<br>')}</div></div></div>`).join('');
}

// =================== ショートUIヘルパー ===================
function toggleShortLike(btn) {
const icon = btn.querySelector('.icon'), span = btn.querySelector('.short-like-count');
const isLiked = btn.dataset.liked === '1'; btn.dataset.liked = isLiked ? '0' : '1';
icon.style.color = isLiked ? '' : '#ff0000';
if (span) {
const cur = parseInt(span.textContent.replace(/[^0-9]/g,''))||0, newVal = isLiked ? Math.max(0,cur-1) : cur+1;
span.textContent = newVal >= 10000 ? (newVal/1000).toFixed(0)+'K' : newVal.toLocaleString();
}
}
function toggleShortDislike(btn) {
const icon = btn.querySelector('.icon'), isDisliked = btn.dataset.disliked === '1';
btn.dataset.disliked = isDisliked ? '0' : '1'; icon.style.color = isDisliked ? '' : '#aaa';
}

async function openShortComments(videoId, title) {
const panel = document.getElementById(`short-comments-panel-${videoId}`);
if (!panel) return;
panel.style.display = 'block';
const content = document.getElementById(`short-comments-content-${videoId}`);
if (content.dataset.loaded === '1') return;
content.innerHTML = '<div class="loader">コメントを読み込み中...</div>'; content.dataset.loaded = '1';
const apis = [`https://inv.nadeko.net/api/v1/comments/${videoId}`, ...INVIDIOUS_INSTANCES.slice(0,4).map(i => `${i}/api/v1/comments/${videoId}`)];
let comments = [];
for (const url of apis) {
try { const res = await fetch(url, { signal: AbortSignal.timeout(6000) }); if (!res.ok) continue; const data = await res.json(); if (data.comments && data.comments.length > 0) { comments = data.comments; break; } } catch(e) {}
}
if (comments.length === 0) { content.innerHTML = '<div style="color:var(--text-secondary);padding:8px;">コメントを読み込めませんでした。</div>'; return; }
content.innerHTML = comments.slice(0,15).map(c => `<div style="display:flex;gap:10px;margin-bottom:14px;"><img src="${c.authorThumbnails?.[0]?.url||''}" style="width:36px;height:36px;border-radius:50%;flex-shrink:0;background:#eee;" onerror="this.src='https://i.pravatar.cc/36?u=${encodeURIComponent(c.author||'')}'"><div><div style="font-weight:bold;font-size:12px;margin-bottom:3px;">${c.author||'名無し'}</div><div style="font-size:13px;line-height:1.4;">${(c.content||'').replace(/\n/g,'<br>')}</div></div></div>`).join('');
}
function closeShortComments(videoId) {
const panel = document.getElementById(`short-comments-panel-${videoId}`); if (panel) panel.style.display = 'none';
}
function toggleSubscribeFromShort(channelName, thumb, btn) {
let subs = getSubscriptions();
if (isSubscribed(channelName)) { subs = subs.filter(s => s.name !== channelName); btn.textContent='登録'; btn.classList.remove('subscribed'); }
else { subs.unshift({ name: channelName, thumb, isLive: false }); btn.textContent='登録済み'; btn.classList.add('subscribed'); }
saveSubscriptions(subs); renderSidebarSubscriptions();
}
</script>

<script async src="https://cse.google.com/cse.js?cx=c04ec9d20c4264b0d"></script>
<script defer data-domain="html.cafe" src="https://milkymouse.com/js/script.js"></script>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v8c78df7c7c0f484497ecbca7046644da1771523124516" integrity="sha512-8DS7rgIrAmghBFwoOTujcf6D9rXvH8xm8JQ1Ja01h9QX8EzXldiszufYa4IFfKdLUKTTrnSFXLDkUEOTrZQ8Qg==" data-cf-beacon='{"version":"2024.11.0","token":"94c79d4d951745d085049dd7f2bbd8f8","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>

<script defer data-domain="html.cafe" src="https://milkymouse.com/js/script.js"></script>