any-env-code / xv /xv.html
izuemon's picture
Create xv.html
0c8ef46 verified
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IzuVidPlayer</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap" rel="stylesheet">
<!-- M3U8ダウンロードスクリプト -->
<script>
function M3U8() {
var _this = this;
this.ie =
navigator.appVersion.toString().indexOf(".NET") > 0;
this.ios =
navigator.platform &&
/iPad|iPhone|iPod/.test(navigator.platform);
this.start = function (m3u8, options) {
if (!options) options = {};
var callbacks = {
progress: null,
finished: null,
error: null,
aborted: null,
};
let recur;
function handleCb(key, payload) {
if (key && callbacks[key]) {
callbacks[key](payload);
}
}
if (_this.ios) {
return handleCb(
"error",
"Downloading on IOS is not supported."
);
}
var startObj = {
on: function (str, cb) {
switch (str) {
case "progress":
callbacks.progress = cb;
break;
case "finished":
callbacks.finished = cb;
break;
case "error":
callbacks.error = cb;
break;
case "aborted":
callbacks.aborted = cb;
break;
}
return startObj;
},
abort: function () {
if (recur) {
recur.aborted = function () {
handleCb("aborted");
};
}
},
};
var download = new Promise(function (resolve, reject) {
var url = new URL(m3u8);
fetch(m3u8)
.then(function (d) {
return d.text();
})
.then(function (d) {
const segmentReg = /^(?!#)(.+)\.(.+)$/gm;
const segments = d.match(segmentReg);
var mapped = map(segments, function (v) {
let tempUrl = new URL(v, url);
return tempUrl.href;
});
if (!mapped.length) {
reject("Invalid m3u8 playlist");
return handleCb(
"error",
"Invalid m3u8 playlist"
);
}
recur = new RecurseDownload(
mapped,
function (data) {
var blob = new Blob(data, {
type: "octet/stream",
});
handleCb("progress", {
status: "Processing...",
});
if (!options.returnBlob) {
if (_this.ie) {
handleCb("progress", {
status:
"Sending video to Internet Explorer...",
});
window.navigator.msSaveBlob(
blob,
options.filename || "video.mp4"
);
} else {
handleCb("progress", {
status: "Sending video to browser...",
});
var a =
document.createElementNS(
"http://www.w3.org/1999/xhtml",
"a"
);
a.href = URL.createObjectURL(blob);
a.download =
options.filename || "video.mp4";
a.style.display = "none";
document.body.appendChild(a);
a.click();
}
handleCb("finished", {
status:
"Successfully downloaded video",
data: blob,
});
resolve(blob);
} else {
handleCb("finished", {
status:
"Successfully downloaded video",
data: blob,
});
resolve(blob);
}
},
0,
[]
);
recur.onprogress = function (obj) {
handleCb("progress", obj);
};
})
.catch(function (err) {
handleCb(
"error",
"Something went wrong when downloading m3u8 playlist: " +
err
);
});
});
return startObj;
};
function RecurseDownload(arr, cb, i, data) {
var _this = this;
this.aborted = false;
recurseDownload(arr, cb, i, data);
function recurseDownload(arr, cb, i, data) {
Promise.all([
fetch(arr[i]),
arr[i + 1] ? fetch(arr[i + 1]) : Promise.resolve(),
])
.then(function (d) {
return map(
filter(d, function (v) {
return v && v.blob;
}),
function (v) {
return v.blob();
}
);
})
.then(function (d) {
return Promise.all(d);
})
.then(function (d) {
var blobs = map(d, function (v, j) {
return new Promise(function (resolve) {
var reader = new FileReader();
reader.readAsArrayBuffer(
new Blob([v], {
type: "octet/stream",
})
);
reader.addEventListener(
"loadend",
function () {
resolve(reader.result);
if (_this.onprogress) {
_this.onprogress({
segment: i + j + 1,
total: arr.length,
percentage: Math.floor(
((i + j + 1) /
arr.length) *
100
),
downloaded: formatNumber(
reduce(
map(data, function (v) {
return v.byteLength;
}),
function (t, c) {
return t + c;
},
0
)
),
status: "Downloading...",
});
}
}
);
});
});
Promise.all(blobs).then(function (d) {
d.forEach(function (buf) {
data.push(buf);
});
var increment = arr[i + 2] ? 2 : 1;
if (_this.aborted) {
data = null;
_this.aborted();
return;
}
if (arr[i + increment]) {
setTimeout(
function () {
recurseDownload(
arr,
cb,
i + increment,
data
);
},
_this.ie ? 500 : 0
);
} else {
cb(data);
}
});
})
.catch(function (err) {
_this.onerror &&
_this.onerror(
"Something went wrong when downloading ts file, nr. " +
i +
": " +
err
);
});
}
}
}
function filter(arr, condition) {
var result = [];
for (var i = 0; i < arr.length; i++) {
if (condition(arr[i], i)) {
result.push(arr[i]);
}
}
return result;
}
function map(arr, condition) {
var result = arr.slice(0);
for (var i = 0; i < arr.length; i++) {
result[i] = condition(arr[i], i);
}
return result;
}
function reduce(arr, condition, start) {
var result = start;
arr.forEach(function (v, i) {
result = condition(result, v, i);
});
return result;
}
function formatNumber(n) {
var ranges = [
{ divider: 1e18, suffix: "EB" },
{ divider: 1e15, suffix: "PB" },
{ divider: 1e12, suffix: "TB" },
{ divider: 1e9, suffix: "GB" },
{ divider: 1e6, suffix: "MB" },
{ divider: 1e3, suffix: "kB" },
];
for (var i = 0; i < ranges.length; i++) {
if (n >= ranges[i].divider) {
return (
Math.floor(n / ranges[i].divider) +
ranges[i].suffix
);
}
}
return n.toString();
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'M PLUS Rounded 1c', 'Segoe UI', Arial, sans-serif;
}
body {
background-color: #0f0f0f;
color: #f1f1f1;
line-height: 1.6;
display: flex;
}
/* サイドバー */
.sidebar {
width: 250px;
background-color: #1a1a1a;
min-height: 100vh;
padding: 20px;
position: sticky;
top: 0;
display: flex;
flex-direction: column;
}
.sidebar-title {
font-size: 24px;
font-weight: bold;
color: #ff4757;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #3d3d3d;
text-align: center;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
}
.nav-item {
padding: 12px 20px;
background-color: #2d2d2d;
border: none;
border-radius: 8px;
color: white;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
text-align: left;
display: flex;
align-items: center;
gap: 10px;
}
.nav-item:hover {
background-color: #3d3d3d;
}
.nav-item.active {
background-color: #ff4757;
}
.nav-icon {
width: 20px;
height: 20px;
}
/* メインコンテンツ */
.main-content {
flex: 1;
padding: 20px;
max-width: calc(100vw - 250px);
}
.container {
max-width: 1400px;
margin: 0 auto;
}
/* ヘッダーと検索フォーム */
header {
background-color: #1a1a1a;
padding: 20px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.search-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-end;
}
.search-box {
flex: 1;
min-width: 300px;
}
.search-box input {
width: 100%;
padding: 14px 20px;
background-color: #2d2d2d;
border: 2px solid #3d3d3d;
border-radius: 8px;
color: white;
font-size: 16px;
transition: border-color 0.3s;
}
.search-box input:focus {
outline: none;
border-color: #ff4757;
}
.search-options {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #3d3d3d;
}
.option-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.option-group label {
font-size: 14px;
color: #aaa;
}
.option-group select {
padding: 10px 15px;
background-color: #2d2d2d;
border: 1px solid #3d3d3d;
border-radius: 6px;
color: white;
font-size: 14px;
}
.buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
button {
padding: 12px 24px;
background-color: #ff4757;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background-color 0.3s;
}
button:hover {
background-color: #ff3742;
}
button.secondary {
background-color: #3d3d3d;
}
button.secondary:hover {
background-color: #4d4d4d;
}
.favorite-btn {
background: none;
border: none;
cursor: pointer;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
}
.favorite-btn svg {
fill: #e3e3e3;
transition: fill 0.3s;
}
.favorite-btn.active svg {
fill: #ffd700;
}
/* 動画グリッド */
.videos-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 25px;
margin-top: 30px;
}
.video-card {
background-color: #1a1a1a;
border-radius: 12px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
position: relative;
}
.video-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5);
}
.thumbnail-container {
position: relative;
width: 100%;
aspect-ratio: 16/9;
overflow: hidden;
}
.thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s;
}
.hover-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.3s;
}
.video-card:hover .thumbnail {
opacity: 0;
}
.video-card:hover .hover-video {
opacity: 1;
}
.quality-badge {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.duration {
position: absolute;
bottom: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.video-info {
padding: 15px;
}
.video-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.video-metadata {
font-size: 14px;
color: #aaa;
display: flex;
justify-content: space-between;
align-items: center;
}
/* 動画プレーヤー画面 */
.video-player-container {
display: none;
margin-top: 30px;
}
.video-wrapper {
position: relative;
width: 100%;
background-color: #000;
border-radius: 12px;
overflow: hidden;
margin-bottom: 30px;
}
#main-video {
width: 100%;
display: block;
max-height: 600px;
}
/* カスタムコントロールバー */
.custom-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
padding: 10px 15px;
display: flex;
flex-direction: column;
gap: 10px;
opacity: 0;
transition: opacity 0.3s;
z-index: 10;
}
.video-wrapper:hover .custom-controls {
opacity: 1;
}
.progress-container {
position: relative;
width: 100%;
height: 5px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
cursor: pointer;
margin-bottom: 5px;
}
.progress-bar {
position: absolute;
height: 100%;
background: #ff4757;
border-radius: 2px;
width: 0%;
}
.progress-buffer {
position: absolute;
height: 100%;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
width: 0%;
}
.progress-time-tooltip {
position: absolute;
top: -30px;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
display: none;
z-index: 100;
}
.controls-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.left-controls,
.right-controls {
display: flex;
align-items: center;
gap: 15px;
}
.control-btn {
background: none;
border: none;
color: #e3e3e3;
cursor: pointer;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.3s;
}
.control-btn:hover {
color: #fff;
}
.control-btn svg {
width: 24px;
height: 24px;
}
.volume-controls {
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
}
.volume-slider {
flex: 1;
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
cursor: pointer;
position: relative;
}
.volume-level {
position: absolute;
height: 100%;
background: #ff4757;
border-radius: 2px;
width: 100%;
}
.speed-controls {
display: flex;
align-items: center;
gap: 8px;
min-width: 100px;
}
.speed-slider {
flex: 1;
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
cursor: pointer;
position: relative;
min-width: 50px
}
.speed-level {
position: absolute;
height: 100%;
background: #4CAF50;
border-radius: 2px;
width: 50%;
}
.speed-value {
font-size: 12px;
color: #e3e3e3;
min-width: 40px;
text-align: center;
}
.time-display {
font-size: 14px;
color: #e3e3e3;
min-width: 110px;
text-align: center;
}
/* 動画読み込み中のアニメーション */
.video-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
display: none;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-left-color: #ff4757;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.video-details {
background-color: #1a1a1a;
padding: 25px;
border-radius: 12px;
margin-bottom: 30px;
position: relative;
}
.video-details h2 {
font-size: 24px;
margin-bottom: 15px;
color: #fff;
padding-right: 40px;
}
.video-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #3d3d3d;
}
.meta-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.meta-label {
font-size: 12px;
color: #aaa;
text-transform: uppercase;
}
.meta-value {
font-size: 16px;
color: #fff;
}
.video-description {
color: #ccc;
line-height: 1.6;
margin-top: 20px;
}
.quality-selector {
margin-top: 20px;
padding: 15px;
background-color: #2d2d2d;
border-radius: 8px;
}
.quality-selector h3 {
margin-bottom: 10px;
font-size: 18px;
}
.quality-options {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.quality-option {
padding: 8px 16px;
background-color: #3d3d3d;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s;
}
.quality-option:hover {
background-color: #4d4d4d;
}
.quality-option.active {
background-color: #ff4757;
}
.download-btn {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.download-btn:hover {
background-color: #45a049;
}
/* ローディング表示 */
.loading {
display: none;
text-align: center;
padding: 40px;
font-size: 18px;
color: #aaa;
}
.loading::after {
content: '';
display: inline-block;
width: 30px;
height: 30px;
border: 3px solid #3d3d3d;
border-top-color: #ff4757;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 15px;
vertical-align: middle;
}
/* エラーメッセージ */
.error-message {
display: none;
background-color: rgba(255, 71, 87, 0.1);
border: 1px solid #ff4757;
color: #ff4757;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
text-align: center;
}
/* 関連動画 */
.related-videos {
margin-top: 40px;
}
.related-videos h2 {
font-size: 22px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #3d3d3d;
}
.back-button {
margin-bottom: 20px;
}
/* 履歴とお気に入り画面 */
.history-favorites-container {
display: none;
margin-top: 30px;
}
.section-title {
font-size: 24px;
margin-bottom: 20px;
color: #fff;
}
.search-history {
margin-bottom: 30px;
}
.search-history-input {
width: 100%;
padding: 12px 20px;
background-color: #2d2d2d;
border: 2px solid #3d3d3d;
border-radius: 8px;
color: white;
font-size: 16px;
margin-bottom: 20px;
}
.history-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.history-item {
padding: 15px;
background-color: #2d2d2d;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
position: relative;
margin-bottom: 10px;
}
.history-info {
flex: 1;
}
.history-title {
font-size: 16px;
margin-bottom: 5px;
}
.history-date {
font-size: 12px;
color: #aaa;
}
/* レスポンシブデザイン */
@media (max-width: 1024px) {
body {
flex-direction: column;
}
.sidebar {
width: 100%;
min-height: auto;
position: relative;
padding: 15px;
}
.sidebar-nav {
flex-direction: row;
flex-wrap: wrap;
}
.nav-item {
flex: 1;
min-width: 150px;
}
.main-content {
max-width: 100%;
padding: 15px;
}
.volume-controls,
.speed-controls {
min-width: 80px;
}
}
@media (max-width: 768px) {
.videos-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.search-container {
flex-direction: column;
align-items: stretch;
}
.search-box {
min-width: 100%;
}
.search-options {
grid-template-columns: 1fr;
}
.video-meta {
flex-direction: column;
gap: 15px;
}
.sidebar-nav {
flex-direction: column;
}
.controls-row {
flex-wrap: wrap;
}
.volume-controls,
.speed-controls {
display: none;
}
}
@media (max-width: 480px) {
.videos-grid {
grid-template-columns: 1fr;
}
.container {
padding: 10px;
}
header {
padding: 15px;
}
.time-display {
font-size: 12px;
min-width: 90px;
}
}
/* 履歴画面のグリッド表示用スタイル */
#history-list.videos-grid {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 25px;
margin-top: 20px;
}
/* 履歴アイテムのカードスタイル */
.history-card {
background-color: #1a1a1a;
border-radius: 12px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
position: relative;
}
.history-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5);
}
/* 履歴サムネイル */
.history-thumbnail {
width: 100%;
aspect-ratio: 16/9;
background-color: #2d2d2d;
position: relative;
overflow: hidden;
}
.history-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 履歴情報 */
.history-content {
padding: 15px;
}
.history-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
height: 44px;
}
.history-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #aaa;
margin-top: 10px;
}
/* 履歴削除ボタン */
.history-delete-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #ff4757;
}
.history-delete-btn:hover {
color: #ff3742;
}
/* 履歴日時 */
.history-date {
font-size: 11px;
color: #888;
}
/* 空の履歴メッセージ */
.history-empty {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
color: #aaa;
font-size: 16px;
}
</style>
</head>
<body>
<!-- サイドバー -->
<div class="sidebar">
<div class="sidebar-title">IzuVidPlayer</div>
<div class="sidebar-nav">
<button class="nav-item active" id="home-nav">
<svg class="nav-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
</svg>
ホーム
</button>
<button class="nav-item" id="history-nav">
<svg class="nav-icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z" />
</svg>
履歴
</button>
<button class="nav-item" id="favorites-nav">
<svg class="nav-icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg>
お気に入り
</button>
<button class="nav-item" id="about-nav">
<svg class="nav-icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z" />
</svg>
このページについて
</button>
</div>
</div>
<!-- メインコンテンツ -->
<div class="main-content">
<div class="container">
<!-- ホーム/検索画面 -->
<div id="home-container">
<!-- ヘッダーと検索フォーム -->
<header>
<div class="search-container">
<div class="search-box">
<input type="text" id="search-input" placeholder="動画を検索...">
</div>
<div class="buttons">
<button id="search-button">検索</button>
<button id="home-button" class="secondary">ホーム</button>
</div>
</div>
<div class="search-options">
<div class="option-group">
<label for="sort-option">並べ替え</label>
<select id="sort-option">
<option value="relevance">関連性</option>
<option value="uploaddate">最新</option>
<option value="rating">評価</option>
<option value="views">視聴回数</option>
<option value="random">ランダム</option>
</select>
</div>
<div class="option-group">
<label for="date-option">作成日時</label>
<select id="date-option">
<option value="all">すべて</option>
<option value="today">ここ3日間</option>
<option value="week">今週</option>
<option value="month">今月</option>
<option value="3month">ここ3ヶ月</option>
<option value="6month">ここ6ヶ月</option>
</select>
</div>
<div class="option-group">
<label for="duration-option">動画の長さ</label>
<select id="duration-option">
<option value="allduration">全て</option>
<option value="1-3min">短い動画 (1-3分)</option>
<option value="3-10min">中程度 (3-10分)</option>
<option value="10min_more">長い動画 (+10分)</option>
<option value="10-20min">長編動画 (10-20分)</option>
<option value="20min_more">長編動画 (20分以上)</option>
</select>
</div>
<div class="option-group">
<label for="quality-option">画質</label>
<select id="quality-option">
<option value="all">全て</option>
<option value="hd">720P以上</option>
<option value="1080P">1080P+</option>
</select>
</div>
<div class="option-group">
<label for="page-option">ページ番号</label>
<input type="number" id="page-option" min="1" value="1" placeholder="1">
</div>
</div>
</header>
<!-- ローディング表示 -->
<div id="loading" class="loading">
読み込み中...
</div>
<!-- エラーメッセージ -->
<div id="error-message" class="error-message">
エラーが発生しました。もう一度お試しください。
</div>
<!-- 動画グリッド(初期画面・検索結果) -->
<div id="videos-grid-container">
<div class="videos-grid" id="videos-grid">
<!-- 動画カードはJavaScriptで生成 -->
</div>
</div>
</div>
<!-- 履歴画面部分(約300行目付近)の変更 -->
<div id="history-container" class="history-favorites-container">
<h2 class="section-title">視聴履歴</h2>
<!-- 履歴設定コントロール -->
<div class="history-controls">
<button id="clear-history-btn" class="secondary">履歴を全て削除</button>
<label class="history-toggle">
<input type="checkbox" id="history-toggle-checkbox" checked>
<span>履歴を記録する</span>
</label>
<button id="delete-selected-btn" class="secondary" style="display:none;">選択項目を削除</button>
</div>
<div class="search-history">
<input type="text" class="search-history-input" id="search-history-input" placeholder="履歴を検索...">
<div class="history-list" id="history-list">
<!-- 履歴項目はJavaScriptで生成 -->
</div>
</div>
</div>
<!-- 履歴画面の下(約310行目付近)に追加 -->
<div id="about-container" class="history-favorites-container">
<h2 class="section-title">このページについて</h2>
<div class="about-content" id="about-content">
<!-- コンテンツはJavaScriptで設定 -->
</div>
</div>
<style>
/* 履歴コントロールの調整 */
.history-controls {
display: flex;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background-color: #2d2d2d;
border-radius: 8px;
flex-wrap: wrap;
align-items: center;
}
.history-toggle {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
color: #f1f1f1;
}
.history-toggle input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.history-item {
padding: 15px;
background-color: #2d2d2d;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
position: relative;
}
.history-item:hover {
background-color: #3d3d3d;
}
.history-item.selected {
background-color: rgba(255, 71, 87, 0.2);
border-left: 4px solid #ff4757;
}
.history-checkbox {
margin-right: 15px;
width: 18px;
height: 18px;
cursor: pointer;
}
#history-container .videos-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 25px;
margin-top: 20px;
}
#search-history-input {
width: 100%;
padding: 12px 20px;
background-color: #2d2d2d;
border: 2px solid #3d3d3d;
border-radius: 8px;
color: white;
font-size: 16px;
margin-bottom: 20px;
}
/* 履歴リストのグリッドレイアウト設定 */
#history-list.videos-grid {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 25px;
margin-top: 20px;
width: 100%;
}
/* 履歴サムネイル */
.history-thumbnail {
width: 120px;
height: 68px;
border-radius: 4px;
overflow: hidden;
flex-shrink: 0;
margin-right: 15px;
background-color: #1a1a1a;
position: relative;
}
.history-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 履歴情報 */
.history-info {
flex: 1;
min-width: 0;
overflow: hidden;
}
.history-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-meta {
display: flex;
gap: 15px;
flex-wrap: wrap;
font-size: 13px;
color: #aaa;
}
/* 履歴アクションボタン */
.history-actions {
display: flex;
gap: 10px;
flex-shrink: 0;
}
/* レスポンシブ対応 */
@media (max-width: 768px) {
.history-item {
flex-direction: column;
align-items: flex-start;
}
.history-thumbnail {
width: 100%;
height: 180px;
margin-right: 0;
margin-bottom: 10px;
}
.history-actions {
width: 100%;
justify-content: flex-end;
margin-top: 10px;
}
.history-info {
width: 100%;
}
}
@media (max-width: 480px) {
.history-thumbnail {
height: 150px;
}
.history-actions {
flex-direction: column;
}
.history-actions button {
width: 100%;
}
}
/* このページについてスタイル */
.about-content {
padding: 20px;
background-color: #2d2d2d;
border-radius: 8px;
line-height: 1.6;
color: #e3e3e3;
}
.about-content h3 {
margin: 20px 0 10px 0;
color: #ff4757;
}
.about-content h3:first-child {
margin-top: 0;
}
.about-content p {
margin-bottom: 15px;
}
.about-content ul {
padding-left: 20px;
margin-bottom: 15px;
}
.about-content li {
margin-bottom: 8px;
}
</style>
<!-- お気に入り画面 -->
<div id="favorites-container" class="history-favorites-container">
<h2 class="section-title">お気に入り動画</h2>
<div class="search-history">
<input type="text" class="search-history-input" id="search-favorites-input"
placeholder="お気に入りを検索...">
<div class="videos-grid" id="favorites-grid">
<!-- お気に入り動画はJavaScriptで生成 -->
</div>
</div>
</div>
<!-- 動画プレーヤー画面 -->
<div id="video-player-container" class="video-player-container">
<div class="back-button">
<button id="back-button" class="secondary">← 戻る</button>
</div>
<div class="video-wrapper">
<video id="main-video" playsinline></video>
<!-- 動画読み込み中のアニメーション -->
<div class="video-loading" id="video-loading">
<div class="spinner"></div>
</div>
<!-- カスタムコントロールバー -->
<div class="custom-controls">
<div class="progress-container" id="progress-container">
<div class="progress-buffer" id="progress-buffer"></div>
<div class="progress-bar" id="progress-bar"></div>
<div class="progress-time-tooltip" id="progress-time-tooltip">00:00</div>
</div>
<div class="controls-row">
<div class="left-controls">
<button class="control-btn" id="play-pause-btn">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960"
width="24px" fill="#e3e3e3">
<path d="M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z" />
</svg>
</button>
<div class="volume-controls">
<button class="control-btn" id="volume-btn">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960"
width="24px" fill="#e3e3e3">
<path
d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM400-606l-86 86H200v80h114l86 86v-252ZM300-480Z" />
</svg>
</button>
<div class="volume-slider" id="volume-slider">
<div class="volume-level" id="volume-level"></div>
</div>
</div>
<div class="time-display" id="time-display">00:00 / 00:00</div>
</div>
<div class="right-controls">
<div class="speed-controls">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960"
width="24px" fill="#e3e3e3">
<path
d="M480-316.5q38-.5 56-27.5l224-336-336 224q-27 18-28.5 55t22.5 61q24 24 62 23.5Zm0-483.5q59 0 113.5 16.5T696-734l-76 48q-33-17-68.5-25.5T480-720q-133 0-226.5 93.5T160-400q0 42 11.5 83t32.5 77h552q23-38 33.5-79t10.5-85q0-36-8.5-70T766-540l48-76q30 47 47.5 100T880-406q1 57-13 109t-41 99q-11 18-30 28t-40 10H204q-21 0-40-10t-30-28q-26-45-40-95.5T80-400q0-83 31.5-155.5t86-127Q252-737 325-768.5T480-800Zm7 313Z" />
</svg>
<div class="speed-slider" id="speed-slider">
<div class="speed-level" id="speed-level"></div>
</div>
<div class="speed-value" id="speed-value">1.0x</div>
</div>
<button class="control-btn" id="pip-btn">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960"
width="24px" fill="#e3e3e3">
<path
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" />
</svg>
</button>
<button class="control-btn" id="fullscreen-btn">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960"
width="24px" fill="#e3e3e3">
<path
d="M120-120v-200h80v120h120v80H120Zm520 0v-80h120v-120h80v200H640ZM120-640v-200h200v80H200v120h-80Zm640 0v-120H640v-80h200v200h-80Z" />
</svg>
</button>
</div>
</div>
</div>
</div>
<div id="video-details" class="video-details">
<!-- 動画詳細はJavaScriptで生成 -->
</div>
<div id="quality-selector" class="quality-selector">
<!-- 画質選択はJavaScriptで生成 -->
</div>
<div id="related-videos-container" class="related-videos">
<h2>関連動画</h2>
<div class="videos-grid" id="related-videos-grid">
<!-- 関連動画はJavaScriptで生成 -->
</div>
</div>
</div>
</div>
</div>
<!-- HLS.js スクリプト -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.10/hls.min.js"></script>
<script>
// CORS Proxy URL(キャッシュ防止用ランダムパラメータ追加)
const getCorsProxyUrl = (url) => {
const randomNum = Math.floor(10000 + Math.random() * 90000);
return `https://izuemon-any-env-code.hf.space/cors-proxy?url=${encodeURIComponent(url)}&uvrqu=${randomNum}`;
};
const BASE_URL = 'https://www.xvideos.com/';
// 状態管理
let currentView = 'home'; // 'home', 'history', 'favorites', 'player'
let currentVideoData = null;
let currentHLSPlaylist = null;
let hlsPlayer = null;
let currentQualityUrl = '';
// ストレージ管理
const storage = {
// 履歴の取得
getHistory: () => {
const history = localStorage.getItem('videoHistory');
return history ? JSON.parse(history) : [];
},
// 履歴の保存
saveHistory: (history) => {
localStorage.setItem('videoHistory', JSON.stringify(history));
},
addToHistory: (video) => {
// 履歴が無効なら追加しない
if (!storage.getHistoryEnabled()) {
return;
}
const history = storage.getHistory();
const existingIndex = history.findIndex(item => item.url === video.url);
// 完全な動画データで上書き
if (existingIndex > -1) {
history.splice(existingIndex, 1);
}
history.unshift({
url: video.url,
pageUrl: video.pageUrl || video.url,
title: video.title || '不明なタイトル',
thumbnail: video.thumbnail || '', // サムネイルURLを確実に保存
hoverVideo: video.hoverVideo || '',
duration: video.duration || '',
quality: video.quality || '',
watchedAt: new Date().toISOString(),
description: video.description || '',
views: video.views || '',
uploadDate: video.uploadDate || '',
hlsUrl: video.hlsUrl || '',
videoUrl: video.videoUrl || video.url
});
if (history.length > 50) {
history.pop();
}
storage.saveHistory(history);
},
updateHistoryItem: (url, updates) => {
const history = storage.getHistory();
const index = history.findIndex(item => item.url === url || item.pageUrl === url);
if (index > -1) {
history[index] = {
...history[index],
...updates,
// 更新時間も更新
updatedAt: new Date().toISOString()
};
storage.saveHistory(history);
}
},
// お気に入りの取得
getFavorites: () => {
const favorites = localStorage.getItem('videoFavorites');
return favorites ? JSON.parse(favorites) : [];
},
// お気に入りの保存
saveFavorites: (favorites) => {
localStorage.setItem('videoFavorites', JSON.stringify(favorites));
},
// お気に入りの追加
addToFavorites: (video) => {
const favorites = storage.getFavorites();
const existingIndex = favorites.findIndex(item => item.url === video.url);
if (existingIndex === -1) {
favorites.push({
...video,
favoritedAt: new Date().toISOString()
});
storage.saveFavorites(favorites);
return true;
}
return false;
},
// お気に入りの削除
removeFromFavorites: (videoUrl) => {
const favorites = storage.getFavorites();
const filtered = favorites.filter(item => item.url !== videoUrl);
storage.saveFavorites(filtered);
return filtered;
},
// お気に入りかどうかの確認
isFavorite: (videoUrl) => {
const favorites = storage.getFavorites();
return favorites.some(item => item.url === videoUrl);
},
// 履歴設定の取得
getHistoryEnabled: () => {
const enabled = localStorage.getItem('historyEnabled');
return enabled === null ? true : JSON.parse(enabled);
},
// 履歴設定の保存
setHistoryEnabled: (enabled) => {
localStorage.setItem('historyEnabled', JSON.stringify(enabled));
}
};
// DOM要素
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
const homeButton = document.getElementById('home-button');
const backButton = document.getElementById('back-button');
const videosGrid = document.getElementById('videos-grid');
const videosGridContainer = document.getElementById('videos-grid-container');
const videoPlayerContainer = document.getElementById('video-player-container');
const mainVideo = document.getElementById('main-video');
const videoDetails = document.getElementById('video-details');
const qualitySelector = document.getElementById('quality-selector');
const relatedVideosGrid = document.getElementById('related-videos-grid');
const relatedVideosContainer = document.getElementById('related-videos-container');
const loadingElement = document.getElementById('loading');
const errorElement = document.getElementById('error-message');
const videoLoadingElement = document.getElementById('video-loading');
// カスタムコントロール要素
const playPauseBtn = document.getElementById('play-pause-btn');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressBuffer = document.getElementById('progress-buffer');
const progressTimeTooltip = document.getElementById('progress-time-tooltip');
const volumeBtn = document.getElementById('volume-btn');
const volumeSlider = document.getElementById('volume-slider');
const volumeLevel = document.getElementById('volume-level');
const speedSlider = document.getElementById('speed-slider');
const speedLevel = document.getElementById('speed-level');
const speedValue = document.getElementById('speed-value');
const timeDisplay = document.getElementById('time-display');
const pipBtn = document.getElementById('pip-btn');
const fullscreenBtn = document.getElementById('fullscreen-btn');
// ナビゲーション要素
const homeNav = document.getElementById('home-nav');
const historyNav = document.getElementById('history-nav');
const favoritesNav = document.getElementById('favorites-nav');
// 画面コンテナ
const homeContainer = document.getElementById('home-container');
const historyContainer = document.getElementById('history-container');
const favoritesContainer = document.getElementById('favorites-container');
// 履歴・お気に入り要素
const historyList = document.getElementById('history-list');
const favoritesGrid = document.getElementById('favorites-grid');
const searchHistoryInput = document.getElementById('search-history-input');
const searchFavoritesInput = document.getElementById('search-favorites-input');
// 検索オプション
const sortOption = document.getElementById('sort-option');
const dateOption = document.getElementById('date-option');
const durationOption = document.getElementById('duration-option');
const qualityOption = document.getElementById('quality-option');
const pageOption = document.getElementById('page-option');
// コントロール状態
let isDraggingProgress = false;
let isDraggingVolume = false;
let isDraggingSpeed = false;
let lastVolume = 1.0;
const aboutNav = document.getElementById('about-nav');
const aboutContainer = document.getElementById('about-container');
const aboutContent = document.getElementById('about-content');
const clearHistoryBtn = document.getElementById('clear-history-btn');
const historyToggleCheckbox = document.getElementById('history-toggle-checkbox');
const deleteSelectedBtn = document.getElementById('delete-selected-btn');
// 3. ナビゲーションイベントに「このページについて」を追加
aboutNav.addEventListener('click', () => {
showView('about');
loadAboutContent();
});
// 4. 履歴コントロールのイベントリスナー
clearHistoryBtn.addEventListener('click', clearAllHistory);
historyToggleCheckbox.addEventListener('change', toggleHistoryRecording);
deleteSelectedBtn.addEventListener('click', deleteSelectedHistoryItems);
// 5. 「このページについて」コンテンツの読み込み
function loadAboutContent() {
const aboutHTML = `<p>このページはテストページです</p>
<h3>機能説明</h3>
<ul>
<li>動画の検索と再生</li>
<li>視聴履歴の記録</li>
<li>お気に入り動画の保存</li>
<li>複数の画質で再生・ダウンロード</li>
</ul>
<h3>履歴設定</h3>
<p>履歴の記録は設定から無効化できます。</p>`;
aboutContent.innerHTML = aboutHTML;
}
// 6. 履歴設定の読み込み
function loadHistorySettings() {
const isEnabled = storage.getHistoryEnabled();
historyToggleCheckbox.checked = isEnabled;
}
// 7. 履歴機能の有効/無効切り替え
function toggleHistoryRecording() {
const isEnabled = historyToggleCheckbox.checked;
storage.setHistoryEnabled(isEnabled);
if (!isEnabled) {
// 履歴を無効にする場合は確認
if (confirm('履歴の記録を無効にすると、新しい視聴履歴が記録されなくなります。よろしいですか?')) {
// 必要に応じて既存の履歴をクリア
// clearAllHistory(); // コメントアウトを外すと履歴もクリアされます
} else {
historyToggleCheckbox.checked = true;
storage.setHistoryEnabled(true);
}
}
}
// 8. 履歴の全削除
function clearAllHistory() {
if (confirm('すべての視聴履歴を削除しますか?この操作は元に戻せません。')) {
storage.saveHistory([]);
loadHistory();
alert('履歴を全て削除しました。');
}
}
// 9. 履歴の選択削除機能の強化
let selectedHistoryItems = [];
// 履歴の表示(タイル形式 - 新しいバージョン)
function displayHistory(history) {
historyList.innerHTML = '';
if (history.length === 0) {
historyList.innerHTML = '<div class="history-empty">視聴履歴はありません</div>';
return;
}
// 履歴リストをグリッドレイアウトに設定
historyList.className = 'videos-grid';
// 履歴を新しい順に表示
history.forEach((item, index) => {
const card = document.createElement('div');
card.className = 'video-card';
card.dataset.url = item.pageUrl || item.url;
// サムネイルコンテナ
const thumbnailContainer = document.createElement('div');
thumbnailContainer.className = 'thumbnail-container';
// サムネイル画像(保存されたサムネイルを使用)
const thumbnail = document.createElement('img');
thumbnail.className = 'thumbnail';
thumbnail.loading = 'lazy';
// サムネイルURLの設定(保存されたものがあれば使用)
if (item.thumbnail && item.thumbnail.trim() !== '') {
thumbnail.src = item.thumbnail;
} else {
// デフォルトのプレースホルダー画像
thumbnail.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjEzNSIgdmlld0JveD0iMCAwIDI0MCAxMzUiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjI0MCIgaGVpZ2h0PSIxMzUiIGZpbGw9IiMxYTFhMWEiLz48cGF0aCBkPSJNOTYgNjcuNUw3MiA3NVY2MEw5NiA2Ny41WiIgZmlsbD0iIzMzMyIvPjxwYXRoIGQ9Ik05NiA2Ny41TDcyIDc1VjYwTDk2IDY3LjVaIiBmaWxsPSIjZmY0NzU3Ii8+PC9zdmc+';
}
thumbnail.alt = item.title || '履歴動画';
// 動画長バッジ
if (item.duration) {
const durationBadge = document.createElement('div');
durationBadge.className = 'duration';
durationBadge.textContent = item.duration;
thumbnailContainer.appendChild(durationBadge);
}
// 視聴日時バッジ
const dateBadge = document.createElement('div');
dateBadge.style.cssText = `
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: normal;
z-index: 2;
`;
const watchedDate = new Date(item.watchedAt);
dateBadge.textContent = `${watchedDate.getMonth() + 1}/${watchedDate.getDate()} ${watchedDate.getHours().toString().padStart(2, '0')}:${watchedDate.getMinutes().toString().padStart(2, '0')}`;
thumbnailContainer.appendChild(dateBadge);
thumbnailContainer.appendChild(thumbnail);
// 動画情報
const videoInfo = document.createElement('div');
videoInfo.className = 'video-info';
const videoTitle = document.createElement('div');
videoTitle.className = 'video-title';
videoTitle.textContent = item.title || `動画 ${index + 1}`;
const videoMetadata = document.createElement('div');
videoMetadata.className = 'video-metadata';
videoMetadata.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
`;
// 視聴回数(あれば)
const viewsContainer = document.createElement('span');
if (item.views) {
try {
const views = parseInt(item.views);
if (!isNaN(views)) {
viewsContainer.textContent = `${views.toLocaleString()} 回視聴`;
}
} catch (e) {
console.log('視聴回数のパースエラー:', e);
}
}
// 削除ボタン(ダウンロードボタンはなし)
const deleteBtn = document.createElement('button');
deleteBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" height="18px" viewBox="0 -960 960 960" width="18px" fill="#ff4757">
<path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/>
</svg>
`;
deleteBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
`;
deleteBtn.title = '履歴から削除';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
deleteSingleHistoryItem(index);
});
videoMetadata.appendChild(viewsContainer);
videoMetadata.appendChild(deleteBtn);
videoInfo.appendChild(videoTitle);
videoInfo.appendChild(videoMetadata);
card.appendChild(thumbnailContainer);
card.appendChild(videoInfo);
// クリックイベント - 動画を再生
card.addEventListener('click', (e) => {
// 削除ボタン以外をクリックした場合
if (!e.target.closest('button')) {
loadVideo(item.pageUrl || item.url);
}
});
// ホバー効果
card.addEventListener('mouseenter', () => {
card.style.transform = 'translateY(-5px)';
card.style.boxShadow = '0 10px 20px rgba(0, 0, 0, 0.5)';
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
card.style.boxShadow = '';
});
historyList.appendChild(card);
});
}
// 10. 履歴選択の切り替え
function toggleHistorySelection(index, isSelected) {
if (isSelected) {
selectedHistoryItems.push(index);
const item = document.querySelector(`.history-item[data-index="${index}"]`);
item.classList.add('selected');
} else {
selectedHistoryItems = selectedHistoryItems.filter(i => i !== index);
const item = document.querySelector(`.history-item[data-index="${index}"]`);
item.classList.remove('selected');
}
deleteSelectedBtn.style.display = selectedHistoryItems.length > 0 ? 'block' : 'none';
}
// 11. 選択した履歴項目の削除
function deleteSelectedHistoryItems() {
if (selectedHistoryItems.length === 0) return;
if (confirm(`選択した${selectedHistoryItems.length}件の履歴を削除しますか?`)) {
const history = storage.getHistory();
// 選択されたインデックスを降順で削除(インデックスがずれないように)
selectedHistoryItems.sort((a, b) => b - a).forEach(index => {
history.splice(index, 1);
});
storage.saveHistory(history);
loadHistory();
selectedHistoryItems = [];
deleteSelectedBtn.style.display = 'none';
}
}
// 12. 単一履歴項目の削除
function deleteSingleHistoryItem(index) {
if (confirm('この履歴を削除しますか?')) {
const history = storage.getHistory();
history.splice(index, 1);
storage.saveHistory(history);
loadHistory();
}
}
// イベントリスナーの設定
document.addEventListener('DOMContentLoaded', () => {
loadHomeVideos();
setupVideoControls();
loadHistorySettings();
});
searchButton.addEventListener('click', () => {
performSearch();
});
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
homeButton.addEventListener('click', () => {
loadHomeVideos();
});
backButton.addEventListener('click', () => {
showVideoGrid();
});
// ナビゲーションイベント
homeNav.addEventListener('click', () => {
showView('home');
loadHomeVideos();
});
historyNav.addEventListener('click', () => {
showView('history');
loadHistory();
});
favoritesNav.addEventListener('click', () => {
showView('favorites');
loadFavorites();
});
// 履歴検索
searchHistoryInput.addEventListener('input', (e) => {
filterHistory(e.target.value);
});
// お気に入り検索
searchFavoritesInput.addEventListener('input', (e) => {
filterFavorites(e.target.value);
});
// ビデオコントロールの設定
function setupVideoControls() {
// 再生/一時停止
playPauseBtn.addEventListener('click', togglePlayPause);
// プログレスバー
progressContainer.addEventListener('mousedown', startProgressDrag);
progressContainer.addEventListener('touchstart', startProgressDrag);
document.addEventListener('mousemove', handleProgressDrag);
document.addEventListener('touchmove', handleProgressDrag);
document.addEventListener('mouseup', stopProgressDrag);
document.addEventListener('touchend', stopProgressDrag);
// プログレスバーホバー時の時間表示
progressContainer.addEventListener('mousemove', updateProgressTooltip);
progressContainer.addEventListener('mouseleave', () => {
progressTimeTooltip.style.display = 'none';
});
// ボリュームコントロール
volumeBtn.addEventListener('click', toggleMute);
volumeSlider.addEventListener('mousedown', startVolumeDrag);
volumeSlider.addEventListener('touchstart', startVolumeDrag);
// 再生速度コントロール
speedSlider.addEventListener('mousedown', startSpeedDrag);
speedSlider.addEventListener('touchstart', startSpeedDrag);
// PIPボタン
pipBtn.addEventListener('click', togglePIP);
// フルスクリーンボタン
fullscreenBtn.addEventListener('click', toggleFullscreen);
// 動画イベント
mainVideo.addEventListener('play', updatePlayPauseIcon);
mainVideo.addEventListener('pause', updatePlayPauseIcon);
mainVideo.addEventListener('timeupdate', updateProgress);
mainVideo.addEventListener('loadedmetadata', updateTimeDisplay);
mainVideo.addEventListener('durationchange', updateTimeDisplay);
mainVideo.addEventListener('volumechange', updateVolumeUI);
mainVideo.addEventListener('waiting', () => {
videoLoadingElement.style.display = 'block';
});
mainVideo.addEventListener('playing', () => {
videoLoadingElement.style.display = 'none';
});
mainVideo.addEventListener('canplay', () => {
videoLoadingElement.style.display = 'none';
});
// フルスクリーン変更検知
document.addEventListener('fullscreenchange', updateFullscreenIcon);
document.addEventListener('webkitfullscreenchange', updateFullscreenIcon);
document.addEventListener('mozfullscreenchange', updateFullscreenIcon);
document.addEventListener('MSFullscreenChange', updateFullscreenIcon);
// PIP変更検知
mainVideo.addEventListener('enterpictureinpicture', updatePIPIcon);
mainVideo.addEventListener('leavepictureinpicture', updatePIPIcon);
// 初期設定
updatePlayPauseIcon();
updateVolumeUI();
updateSpeedUI();
}
// 再生/一時停止の切り替え
function togglePlayPause() {
if (mainVideo.paused) {
mainVideo.play();
} else {
mainVideo.pause();
}
}
// 再生/一時停止アイコンの更新
function updatePlayPauseIcon() {
const icon = playPauseBtn.querySelector('svg');
if (mainVideo.paused) {
icon.innerHTML = '<path d="M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z"/>';
} else {
icon.innerHTML = '<path d="M520-200v-560h240v560H520Zm-320 0v-560h240v560H200Zm400-80h80v-400h-80v400Zm-320 0h80v-400h-80v400Zm0-400v400-400Zm320 0v400-400Z"/>';
}
}
// プログレスバー操作
function startProgressDrag(e) {
e.preventDefault();
isDraggingProgress = true;
updateProgressFromEvent(e);
}
function handleProgressDrag(e) {
if (isDraggingProgress) {
updateProgressFromEvent(e);
}
}
function stopProgressDrag() {
isDraggingProgress = false;
}
function updateProgressFromEvent(e) {
if (!isDraggingProgress && e.type !== 'mousemove') return;
const rect = progressContainer.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
let percent = (clientX - rect.left) / rect.width;
percent = Math.max(0, Math.min(1, percent));
const time = percent * mainVideo.duration;
if (!isNaN(time)) {
mainVideo.currentTime = time;
updateProgress();
}
}
// プログレスバーの更新
function updateProgress() {
if (mainVideo.duration) {
const percent = (mainVideo.currentTime / mainVideo.duration) * 100;
progressBar.style.width = percent + '%';
// バッファリングの表示
if (mainVideo.buffered.length > 0) {
const bufferedEnd = mainVideo.buffered.end(mainVideo.buffered.length - 1);
const bufferedPercent = (bufferedEnd / mainVideo.duration) * 100;
progressBuffer.style.width = bufferedPercent + '%';
}
updateTimeDisplay();
}
}
// プログレスバーツールチップの更新
function updateProgressTooltip(e) {
const rect = progressContainer.getBoundingClientRect();
const clientX = e.clientX;
let percent = (clientX - rect.left) / rect.width;
percent = Math.max(0, Math.min(1, percent));
const time = percent * mainVideo.duration;
const timeStr = formatTime(time);
progressTimeTooltip.textContent = timeStr;
progressTimeTooltip.style.left = (percent * 100) + '%';
progressTimeTooltip.style.display = 'block';
}
// 時間表示の更新
function updateTimeDisplay() {
if (mainVideo.duration) {
const currentTime = formatTime(mainVideo.currentTime);
const duration = formatTime(mainVideo.duration);
timeDisplay.textContent = `${currentTime} / ${duration}`;
}
}
// 時間のフォーマット
function formatTime(seconds) {
if (isNaN(seconds)) return '00:00';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
}
// ミュート切り替え
function toggleMute() {
if (mainVideo.volume > 0) {
lastVolume = mainVideo.volume;
mainVideo.volume = 0;
} else {
mainVideo.volume = lastVolume;
}
updateVolumeUI();
}
// ボリュームUIの更新
function updateVolumeUI() {
const volume = mainVideo.volume;
const isMuted = mainVideo.muted || volume === 0;
volumeLevel.style.width = (volume * 100) + '%';
const icon = volumeBtn.querySelector('svg');
if (isMuted || volume === 0) {
icon.innerHTML = '<path d="M792-56 671-177q-25 16-53 27.5T560-131v-82q14-5 27.5-10t25.5-12L480-368v208L280-360H120v-240h128L56-792l56-56 736 736-56 56Zm-8-232-58-58q17-31 25.5-65t8.5-70q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 53-14.5 102T784-288ZM650-422l-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5T650-422ZM480-592 376-696l104-104v208Zm-80 238v-94l-72-72H200v80h114l86 86Zm-36-130Z"/>';
} else if (volume < 0.33) {
icon.innerHTML = '<path d="M280-360v-240h160l200-200v640L440-360H280Zm80-80h114l86 86v-252l-86 86H360v80Zm100-40Z"/>';
} else if (volume < 0.66) {
icon.innerHTML = '<path d="M200-360v-240h160l200-200v640L360-360H200Zm440 40v-322q45 21 72.5 65t27.5 97q0 53-27.5 96T640-320ZM480-606l-86 86H280v80h114l86 86v-252ZM380-480Z"/>';
} else {
icon.innerHTML = '<path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM400-606l-86 86H200v80h114l86 86v-252ZM300-480Z"/>';
}
}
// ボリュームドラッグ
function startVolumeDrag(e) {
e.preventDefault();
isDraggingVolume = true;
updateVolumeFromEvent(e);
document.addEventListener('mousemove', handleVolumeDrag);
document.addEventListener('touchmove', handleVolumeDrag);
document.addEventListener('mouseup', stopVolumeDrag);
document.addEventListener('touchend', stopVolumeDrag);
}
function handleVolumeDrag(e) {
if (isDraggingVolume) {
updateVolumeFromEvent(e);
}
}
function stopVolumeDrag() {
isDraggingVolume = false;
document.removeEventListener('mousemove', handleVolumeDrag);
document.removeEventListener('touchmove', handleVolumeDrag);
document.removeEventListener('mouseup', stopVolumeDrag);
document.removeEventListener('touchend', stopVolumeDrag);
}
function updateVolumeFromEvent(e) {
const rect = volumeSlider.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
let percent = (clientX - rect.left) / rect.width;
percent = Math.max(0, Math.min(1, percent));
mainVideo.volume = percent;
updateVolumeUI();
}
// 再生速度ドラッグ
function startSpeedDrag(e) {
e.preventDefault();
isDraggingSpeed = true;
updateSpeedFromEvent(e);
document.addEventListener('mousemove', handleSpeedDrag);
document.addEventListener('touchmove', handleSpeedDrag);
document.addEventListener('mouseup', stopSpeedDrag);
document.addEventListener('touchend', stopSpeedDrag);
}
function handleSpeedDrag(e) {
if (isDraggingSpeed) {
updateSpeedFromEvent(e);
}
}
function stopSpeedDrag() {
isDraggingSpeed = false;
document.removeEventListener('mousemove', handleSpeedDrag);
document.removeEventListener('touchmove', handleSpeedDrag);
document.removeEventListener('mouseup', stopSpeedDrag);
document.removeEventListener('touchend', stopSpeedDrag);
}
function updateSpeedFromEvent(e) {
const rect = speedSlider.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
let percent = (clientX - rect.left) / rect.width;
percent = Math.max(0, Math.min(1, percent));
// 0.25x から 3.0x までの範囲
const speed = 0.25 + percent * 2.75;
mainVideo.playbackRate = speed;
updateSpeedUI();
}
// 再生速度UIの更新
function updateSpeedUI() {
const speed = mainVideo.playbackRate;
const percent = (speed - 0.25) / 2.75 * 100;
speedLevel.style.width = percent + '%';
speedValue.textContent = speed.toFixed(1) + 'x';
}
// PIP切り替え
function togglePIP() {
if (document.pictureInPictureElement) {
document.exitPictureInPicture();
} else if (document.pictureInPictureEnabled) {
mainVideo.requestPictureInPicture();
}
}
// PIPアイコンの更新
function updatePIPIcon() {
const icon = pipBtn.querySelector('svg');
if (document.pictureInPictureElement) {
icon.innerHTML = '<path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v280h-80v-280H200v560h280v80H200Zm360 0v-80h144L332-572l56-56 372 371v-143h80v280H560Z"/>';
} else {
icon.innerHTML = '<path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"/>';
}
}
// フルスクリーン切り替え
function toggleFullscreen() {
const videoWrapper = document.querySelector('.video-wrapper');
if (!document.fullscreenElement && !document.webkitFullscreenElement &&
!document.mozFullScreenElement && !document.msFullscreenElement) {
if (videoWrapper.requestFullscreen) {
videoWrapper.requestFullscreen();
} else if (videoWrapper.webkitRequestFullscreen) {
videoWrapper.webkitRequestFullscreen();
} else if (videoWrapper.mozRequestFullScreen) {
videoWrapper.mozRequestFullScreen();
} else if (videoWrapper.msRequestFullscreen) {
videoWrapper.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
}
// フルスクリーンアイコンの更新
function updateFullscreenIcon() {
const icon = fullscreenBtn.querySelector('svg');
if (document.fullscreenElement || document.webkitFullscreenElement ||
document.mozFullScreenElement || document.msFullscreenElement) {
icon.innerHTML = '<path d="M240-120v-120H120v-80h200v200h-80Zm400 0v-200h200v80H720v120h-80ZM120-640v-80h120v-120h80v200H120Zm520 0v-200h80v120h120v80H640Z"/>';
} else {
icon.innerHTML = '<path d="M120-120v-200h80v120h120v80H120Zm520 0v-80h120v-120h80v200H640ZM120-640v-200h200v80H200v120h-80Zm640 0v-120H640v-80h200v200h-80Z"/>';
}
}
function showView(view) {
// すべてのビューを非表示
homeContainer.style.display = 'none';
historyContainer.style.display = 'none';
favoritesContainer.style.display = 'none';
videoPlayerContainer.style.display = 'none';
aboutContainer.style.display = 'none';
// アクティブなナビを更新
homeNav.classList.remove('active');
historyNav.classList.remove('active');
favoritesNav.classList.remove('active');
aboutNav.classList.remove('active');
// 選択したビューを表示
switch (view) {
case 'home':
homeContainer.style.display = 'block';
homeNav.classList.add('active');
break;
case 'history':
historyContainer.style.display = 'block';
historyNav.classList.add('active');
break;
case 'favorites':
favoritesContainer.style.display = 'block';
favoritesNav.classList.add('active');
break;
case 'player':
videoPlayerContainer.style.display = 'block';
break;
case 'about':
aboutContainer.style.display = 'block';
aboutNav.classList.add('active');
break;
}
currentView = view;
}
function addToHistoryWithCheck(video) {
if (!storage.getHistoryEnabled()) {
return; // 履歴が無効なら追加しない
}
const history = storage.getHistory();
const existingIndex = history.findIndex(item => item.url === video.url);
if (existingIndex > -1) {
history.splice(existingIndex, 1);
}
history.unshift({
...video,
watchedAt: new Date().toISOString()
});
if (history.length > 50) {
history.pop();
}
storage.saveHistory(history);
}
function loadHistory() {
const history = storage.getHistory();
loadHistorySettings(); // 設定を読み込む
displayHistory(history);
}
// 履歴の表示
function displayHistory(history) {
historyList.innerHTML = '';
if (history.length === 0) {
historyList.innerHTML = '<div style="text-align:center;padding:40px;color:#aaa;">視聴履歴はありません</div>';
return;
}
history.forEach((item, index) => {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
const historyInfo = document.createElement('div');
historyInfo.className = 'history-info';
const historyTitle = document.createElement('div');
historyTitle.className = 'history-title';
historyTitle.textContent = item.title || `動画 ${index + 1}`;
const historyDate = document.createElement('div');
historyDate.className = 'history-date';
historyDate.textContent = new Date(item.watchedAt).toLocaleString('ja-JP');
historyInfo.appendChild(historyTitle);
historyInfo.appendChild(historyDate);
const downloadBtn = document.createElement('button');
downloadBtn.className = 'download-btn';
downloadBtn.textContent = 'ダウンロード';
downloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
downloadVideo(item);
});
historyItem.appendChild(historyInfo);
historyItem.appendChild(downloadBtn);
historyItem.addEventListener('click', () => {
loadVideo(item.url);
});
historyList.appendChild(historyItem);
});
}
// 履歴のフィルタリング
function filterHistory(query) {
const history = storage.getHistory();
const filtered = history.filter(item =>
item.title.toLowerCase().includes(query.toLowerCase())
);
displayHistory(filtered);
}
// お気に入りの読み込み
function loadFavorites() {
const favorites = storage.getFavorites();
displayFavorites(favorites);
}
// お気に入りの表示
function displayFavorites(favorites) {
favoritesGrid.innerHTML = '';
if (favorites.length === 0) {
favoritesGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;color:#aaa;">お気に入りの動画はありません</div>';
return;
}
favorites.forEach((favorite) => {
createFavoriteCard(favorite);
});
}
// お気に入りカードの作成
function createFavoriteCard(video) {
const card = document.createElement('div');
card.className = 'video-card';
card.dataset.url = video.url;
// サムネイルコンテナ
const thumbnailContainer = document.createElement('div');
thumbnailContainer.className = 'thumbnail-container';
// サムネイル画像
const thumbnail = document.createElement('img');
thumbnail.className = 'thumbnail';
thumbnail.src = video.thumbnail;
thumbnail.alt = video.title;
thumbnail.loading = 'lazy';
// ホバー動画
const hoverVideo = document.createElement('video');
hoverVideo.className = 'hover-video';
hoverVideo.muted = true;
hoverVideo.loop = true;
hoverVideo.preload = 'none';
if (video.hoverVideo) {
const source = document.createElement('source');
source.src = video.hoverVideo;
source.type = 'video/mp4';
hoverVideo.appendChild(source);
}
// 画質バッジ
if (video.quality) {
const qualityBadge = document.createElement('div');
qualityBadge.className = 'quality-badge';
qualityBadge.textContent = video.quality;
thumbnailContainer.appendChild(qualityBadge);
}
// 動画長
if (video.duration) {
const durationBadge = document.createElement('div');
durationBadge.className = 'duration';
durationBadge.textContent = video.duration;
thumbnailContainer.appendChild(durationBadge);
}
thumbnailContainer.appendChild(thumbnail);
thumbnailContainer.appendChild(hoverVideo);
// 動画情報
const videoInfo = document.createElement('div');
videoInfo.className = 'video-info';
const videoTitle = document.createElement('div');
videoTitle.className = 'video-title';
videoTitle.textContent = video.title;
const videoMetadata = document.createElement('div');
videoMetadata.className = 'video-metadata';
const videoMeta = document.createElement('span');
videoMeta.textContent = video.duration || '時間不明';
const removeBtn = document.createElement('button');
removeBtn.className = 'favorite-btn active';
removeBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px">
<path d="m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143ZM233-120l65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Zm247-350Z"/>
</svg>
`;
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
storage.removeFromFavorites(video.url);
card.remove();
if (favoritesGrid.children.length === 0) {
favoritesGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;color:#aaa;">お気に入りの動画はありません</div>';
}
});
videoMetadata.appendChild(removeBtn);
videoInfo.appendChild(videoTitle);
videoInfo.appendChild(videoMetadata);
card.appendChild(thumbnailContainer);
card.appendChild(videoInfo);
card.addEventListener('click', () => {
// クリック時に動画データを保存してから読み込む
const videoData = {
url: video.url,
pageUrl: video.url,
title: video.title,
thumbnail: video.thumbnail,
hoverVideo: video.hoverVideo,
duration: video.duration,
quality: video.quality
};
// 履歴に追加
storage.addToHistory(videoData);
// 動画を読み込み
loadVideo(video.url);
});
// ホバーイベント
card.addEventListener('mouseenter', () => {
if (video.hoverVideo && hoverVideo.paused) {
hoverVideo.play().catch(e => console.log('ホバー動画の再生に失敗:', e));
}
});
card.addEventListener('mouseleave', () => {
if (!hoverVideo.paused) {
hoverVideo.pause();
hoverVideo.currentTime = 0;
}
});
favoritesGrid.appendChild(card);
}
// お気に入りのフィルタリング
function filterFavorites(query) {
const favorites = storage.getFavorites();
const filtered = favorites.filter(item =>
item.title.toLowerCase().includes(query.toLowerCase())
);
favoritesGrid.innerHTML = '';
if (filtered.length === 0) {
favoritesGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;color:#aaa;">一致する動画が見つかりませんでした</div>';
return;
}
filtered.forEach(favorite => {
createFavoriteCard(favorite);
});
}
// ホーム動画の読み込み
async function loadHomeVideos() {
showLoading();
hideError();
currentView = 'home';
showView('home');
try {
const url = BASE_URL;
const proxyUrl = getCorsProxyUrl(url);
const response = await fetch(proxyUrl);
const html = await response.text();
displayVideosFromHTML(html);
} catch (error) {
console.error('ホーム動画の読み込みエラー:', error);
showError('動画の読み込みに失敗しました');
} finally {
hideLoading();
}
}
// 検索の実行(修正:検索画面に移動しないバグを修正)
function performSearch() {
const query = searchInput.value.trim();
if (!query) {
loadHomeVideos();
return;
}
showLoading();
hideError();
currentView = 'home'; // 検索結果もホーム画面で表示
showView('home');
// 検索パラメータの構築
let searchUrl = `${BASE_URL}?k=${encodeURIComponent(query)}`;
// ソートオプション
const sortValue = sortOption.value;
if (sortValue && sortValue !== 'relevance') {
searchUrl += `&sort=${sortValue}`;
}
// 日付フィルター
const dateValue = dateOption.value;
if (dateValue && dateValue !== 'all') {
searchUrl += `&datef=${dateValue}`;
}
// 動画長フィルター
const durationValue = durationOption.value;
if (durationValue && durationValue !== 'allduration') {
searchUrl += `&durf=${durationValue}`;
}
// 画質フィルター
const qualityValue = qualityOption.value;
if (qualityValue && qualityValue !== 'all') {
searchUrl += `&quality=${qualityValue}`;
}
// ページ番号
const pageValue = pageOption.value;
if (pageValue && pageValue > 1) {
searchUrl += `&p=${pageValue}`;
}
// 検索実行
fetchSearchResults(searchUrl);
}
// 検索結果の取得と表示
async function fetchSearchResults(url) {
try {
const proxyUrl = getCorsProxyUrl(url);
const response = await fetch(proxyUrl);
const html = await response.text();
displayVideosFromHTML(html);
} catch (error) {
console.error('検索エラー:', error);
showError('検索に失敗しました');
} finally {
hideLoading();
}
}
// HTMLから動画情報を抽出して表示
function displayVideosFromHTML(html) {
// 動画グリッドをクリア
videosGrid.innerHTML = '';
// HTMLをパースするための一時要素
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// mozaiqueクラスを持つdivを探す
const mozaiqueDivs = doc.querySelectorAll('div[class*="mozaique"]');
if (mozaiqueDivs.length === 0) {
videosGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;color:#aaa;">動画が見つかりませんでした</div>';
return;
}
// 各mozaique divを処理
mozaiqueDivs.forEach((mozaiqueDiv) => {
// mozaique内のdiv要素を取得
const videoDivs = mozaiqueDiv.querySelectorAll('div');
videoDivs.forEach((videoDiv, index) => {
// thumb-insideを探す
const thumbInside = videoDiv.querySelector('div.thumb-inside');
if (!thumbInside) return;
// thumbを探す
const thumb = thumbInside.querySelector('div.thumb');
if (!thumb) return;
// リンクと画像を取得
const link = thumb.querySelector('a');
if (!link) return;
const img = link.querySelector('img');
if (!img) return;
// 動画情報を抽出
const videoUrl = link.getAttribute('href') || '#';
const thumbnailUrl = img.getAttribute('data-src') || img.src;
const hoverVideoUrl = img.getAttribute('data-pvv') || '';
// 画質情報を取得
const qualitySpan = thumbInside.querySelector('div.top-right-tags span.video-hd-mark');
const quality = qualitySpan ? qualitySpan.textContent.trim() : '';
// サムネイル下の情報を取得
const thumbUnder = videoDiv.querySelector('div.thumb-under');
let title = '';
let duration = '';
if (thumbUnder) {
const titleElement = thumbUnder.querySelector('p.title a');
if (titleElement) {
title = titleElement.textContent.replace(/\s*<span class="duration">.*?<\/span>/, '').trim();
const durationElement = titleElement.querySelector('span.duration');
if (durationElement) {
duration = durationElement.textContent.trim();
}
}
// タイトルが空の場合、代替手段を試す
if (!title) {
const metadataDuration = thumbUnder.querySelector('p.metadata span.duration');
if (metadataDuration) {
duration = metadataDuration.textContent.trim();
}
}
}
// タイトルがまだ空の場合、imgのalt属性を試す
if (!title && img.alt) {
title = img.alt;
}
// タイトルがまだ空の場合、デフォルトタイトルを使用
if (!title) {
title = `動画 ${index + 1}`;
}
// 動画カードを作成
createVideoCard({
url: videoUrl,
thumbnail: thumbnailUrl,
hoverVideo: hoverVideoUrl,
title: title,
duration: duration,
quality: quality,
index: index
});
});
});
// 動画が見つからなかった場合
if (videosGrid.children.length === 0) {
videosGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;color:#aaa;">動画が見つかりませんでした</div>';
}
}
// 動画カードの作成
function createVideoCard(video) {
const card = document.createElement('div');
card.className = 'video-card';
card.dataset.url = video.url;
// サムネイルコンテナ
const thumbnailContainer = document.createElement('div');
thumbnailContainer.className = 'thumbnail-container';
// サムネイル画像
const thumbnail = document.createElement('img');
thumbnail.className = 'thumbnail';
thumbnail.src = video.thumbnail;
thumbnail.alt = video.title;
thumbnail.loading = 'lazy';
// ホバー動画
const hoverVideo = document.createElement('video');
hoverVideo.className = 'hover-video';
hoverVideo.muted = true;
hoverVideo.loop = true;
hoverVideo.preload = 'none';
if (video.hoverVideo) {
const source = document.createElement('source');
source.src = video.hoverVideo;
source.type = 'video/mp4';
hoverVideo.appendChild(source);
}
// 画質バッジ
if (video.quality) {
const qualityBadge = document.createElement('div');
qualityBadge.className = 'quality-badge';
qualityBadge.textContent = video.quality;
thumbnailContainer.appendChild(qualityBadge);
}
// 動画長
if (video.duration) {
const durationBadge = document.createElement('div');
durationBadge.className = 'duration';
durationBadge.textContent = video.duration;
thumbnailContainer.appendChild(durationBadge);
}
thumbnailContainer.appendChild(thumbnail);
thumbnailContainer.appendChild(hoverVideo);
// 動画情報
const videoInfo = document.createElement('div');
videoInfo.className = 'video-info';
const videoTitle = document.createElement('div');
videoTitle.className = 'video-title';
videoTitle.textContent = video.title;
const videoMetadata = document.createElement('div');
videoMetadata.className = 'video-metadata';
videoMetadata.textContent = video.duration || '時間不明';
videoInfo.appendChild(videoTitle);
videoInfo.appendChild(videoMetadata);
card.appendChild(thumbnailContainer);
card.appendChild(videoInfo);
// クリックイベント
card.addEventListener('click', () => {
loadVideo(video.url);
});
// ホバーイベント
card.addEventListener('mouseenter', () => {
if (video.hoverVideo && hoverVideo.paused) {
hoverVideo.play().catch(e => console.log('ホバー動画の再生に失敗:', e));
}
});
card.addEventListener('mouseleave', () => {
if (!hoverVideo.paused) {
hoverVideo.pause();
hoverVideo.currentTime = 0;
}
});
videosGrid.appendChild(card);
}
async function loadVideo(url) {
showLoading();
hideError();
videoLoadingElement.style.display = 'block';
currentView = 'player';
showView('player');
// 相対URLを絶対URLに変換
let videoUrl = url;
if (!url.startsWith('http')) {
videoUrl = BASE_URL + (url.startsWith('/') ? url.substring(1) : url);
}
try {
const proxyUrl = getCorsProxyUrl(videoUrl);
const response = await fetch(proxyUrl);
const html = await response.text();
// 動画データを抽出
extractVideoData(html, videoUrl);
} catch (error) {
console.error('動画読み込みエラー:', error);
showError('動画の読み込みに失敗しました');
hideLoading();
videoLoadingElement.style.display = 'none';
}
}
// extractVideoData関数内で履歴保存を追加(約1300行目付近)
function extractVideoData(html, videoUrl) {
// 動画情報を抽出する正規表現
const videoHLSRegex = /html5player\.setVideoHLS\('([^']+)'\)/;
const videoHighRegex = /html5player\.setVideoUrlHigh\('([^']+)'\)/;
const videoLowRegex = /html5player\.setVideoUrlLow\('([^']+)'\)/;
const videoTitleRegex = /html5player\.setVideoTitle\('([^']+)'\)/;
const videoHLSMatch = html.match(videoHLSRegex);
const videoHighMatch = html.match(videoHighRegex);
const videoLowMatch = html.match(videoLowRegex);
const videoTitleMatch = html.match(videoTitleRegex);
// サムネイル画像を抽出
const thumbnailRegex = /meta\s+property="og:image"\s+content="([^"]+)"/;
const thumbnailMatch = html.match(thumbnailRegex);
// JSON-LDデータを抽出
const jsonLdRegex = /<script type="application\/ld\+json">([\s\S]*?)<\/script>/;
const jsonLdMatch = html.match(jsonLdRegex);
let jsonLdData = null;
if (jsonLdMatch) {
try {
jsonLdData = JSON.parse(jsonLdMatch[1]);
} catch (e) {
console.error('JSON-LDの解析エラー:', e);
}
}
// 関連動画を抽出
const relatedRegex = /var video_related\s*=\s*(\[.*?\]);/s;
const relatedMatch = html.match(relatedRegex);
let relatedVideos = [];
if (relatedMatch) {
try {
relatedVideos = JSON.parse(relatedMatch[1]);
} catch (e) {
console.error('関連動画データの解析エラー:', e);
}
}
// サムネイルURLを取得(優先順位: og:image > その他)
let thumbnailUrl = '';
if (thumbnailMatch && thumbnailMatch[1]) {
thumbnailUrl = thumbnailMatch[1];
} else if (jsonLdData && jsonLdData.thumbnailUrl) {
thumbnailUrl = jsonLdData.thumbnailUrl;
}
// タイトルを取得
let title = videoTitleMatch ? videoTitleMatch[1] :
(jsonLdData ? jsonLdData.name : '不明なタイトル');
// 動画データを保存
currentVideoData = {
hlsUrl: videoHLSMatch ? videoHLSMatch[1] : null,
highQualityUrl: videoHighMatch ? videoHighMatch[1] : null,
lowQualityUrl: videoLowMatch ? videoLowMatch[1] : null,
title: title,
thumbnail: thumbnailUrl,
jsonLd: jsonLdData,
relatedVideos: relatedVideos,
pageUrl: videoUrl
};
// 履歴に動画データを保存
const videoDataForHistory = {
url: videoUrl,
pageUrl: videoUrl,
title: title,
thumbnail: thumbnailUrl,
duration: jsonLdData ? jsonLdData.duration?.replace('PT', '').toLowerCase() : '',
description: jsonLdData ? jsonLdData.description : '',
views: jsonLdData?.interactionStatistic?.userInteractionCount || '',
uploadDate: jsonLdData?.uploadDate || '',
hlsUrl: videoHLSMatch ? videoHLSMatch[1] : null,
// 実際の再生URL(最初に利用可能なもの)
videoUrl: videoHLSMatch ? videoHLSMatch[1] :
(videoHighMatch ? videoHighMatch[1] :
(videoLowMatch ? videoLowMatch[1] : videoUrl))
};
storage.addToHistory(videoDataForHistory);
// HLSプレイリストを取得
if (currentVideoData.hlsUrl) {
fetchHLSPlaylist(currentVideoData.hlsUrl);
} else {
displayVideoPlayer();
}
}
// HLSプレイリストの取得
async function fetchHLSPlaylist(hlsUrl) {
try {
const proxyUrl = getCorsProxyUrl(hlsUrl);
const response = await fetch(proxyUrl);
const playlistText = await response.text();
currentHLSPlaylist = parseM3U8Playlist(playlistText, hlsUrl);
displayVideoPlayer();
} catch (error) {
console.error('HLSプレイリストの取得エラー:', error);
currentHLSPlaylist = null;
displayVideoPlayer();
}
}
// M3U8プレイリストの解析
function parseM3U8Playlist(playlistText, baseUrl) {
const lines = playlistText.split('\n');
const variants = [];
let currentVariant = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#EXT-X-STREAM-INF:')) {
currentVariant = {};
// 属性を解析
const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
const nameMatch = line.match(/NAME="([^"]+)"/);
if (bandwidthMatch) currentVariant.bandwidth = parseInt(bandwidthMatch[1]);
if (resolutionMatch) currentVariant.resolution = resolutionMatch[1];
if (nameMatch) currentVariant.name = nameMatch[1];
} else if (line && !line.startsWith('#') && currentVariant) {
// 相対URLを絶対URLに変換
let variantUrl = line;
if (!variantUrl.startsWith('http')) {
const basePath = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);
variantUrl = basePath + variantUrl;
}
currentVariant.url = variantUrl;
variants.push(currentVariant);
currentVariant = null;
}
}
return variants;
}
// 動画プレーヤーの表示
function displayVideoPlayer() {
// 動画詳細情報の表示
displayVideoDetails();
// 画質選択オプションの表示
displayQualityOptions();
// 関連動画の表示
displayRelatedVideos();
// デフォルトの動画ソースを設定
if (currentVideoData.hlsUrl && currentHLSPlaylist && currentHLSPlaylist.length > 0) {
// 最初のHLSバリアントを再生
playHLSVideo(currentHLSPlaylist[0].url);
} else if (currentVideoData.highQualityUrl) {
// 高画質動画を再生
playRegularVideo(currentVideoData.highQualityUrl);
} else if (currentVideoData.lowQualityUrl) {
// 低画質動画を再生
playRegularVideo(currentVideoData.lowQualityUrl);
} else {
showError('再生可能な動画が見つかりませんでした');
}
hideLoading();
videoLoadingElement.style.display = 'none';
}
// 動画詳細情報の表示
function displayVideoDetails() {
const data = currentVideoData;
const jsonLd = data.jsonLd;
let html = '';
if (data.title) {
html += `<h2>${data.title}</h2>`;
}
// お気に入りボタン
const isFavorite = storage.isFavorite(data.pageUrl);
html += `
<button class="favorite-btn ${isFavorite ? 'active' : ''}" id="video-favorite-btn" style="position:absolute;top:25px;right:25px;">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
<path d="m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143ZM233-120l65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Zm247-350Z"/>
</svg>
</button>
`;
html += '<div class="video-meta">';
if (jsonLd) {
if (jsonLd.uploadDate) {
const date = new Date(jsonLd.uploadDate);
const formattedDate = date.toLocaleDateString('ja-JP', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
html += `
<div class="meta-item">
<span class="meta-label">投稿日</span>
<span class="meta-value">${formattedDate}</span>
</div>
`;
}
if (jsonLd.duration) {
const duration = jsonLd.duration.replace('PT', '').toLowerCase();
html += `
<div class="meta-item">
<span class="meta-label">長さ</span>
<span class="meta-value">${duration}</span>
</div>
`;
}
if (jsonLd.interactionStatistic && jsonLd.interactionStatistic.userInteractionCount) {
const views = parseInt(jsonLd.interactionStatistic.userInteractionCount).toLocaleString();
html += `
<div class="meta-item">
<span class="meta-label">視聴回数</span>
<span class="meta-value">${views} 回</span>
</div>
`;
}
}
html += '</div>';
if (jsonLd && jsonLd.description) {
html += `<div class="video-description">${jsonLd.description}</div>`;
}
videoDetails.innerHTML = html;
// お気に入りボタンのイベント
const favoriteBtn = document.getElementById('video-favorite-btn');
favoriteBtn.addEventListener('click', () => {
const isFavorite = storage.isFavorite(data.pageUrl);
if (isFavorite) {
storage.removeFromFavorites(data.pageUrl);
favoriteBtn.classList.remove('active');
} else {
const videoData = {
url: data.pageUrl,
thumbnail: '',
title: data.title,
duration: jsonLd ? jsonLd.duration.replace('PT', '').toLowerCase() : '',
quality: '',
hoverVideo: ''
};
storage.addToFavorites(videoData);
favoriteBtn.classList.add('active');
}
});
}
// 画質選択オプションの表示
function displayQualityOptions() {
let html = '<h3>画質選択</h3><div class="quality-options">';
// HLSオプション
if (currentHLSPlaylist && currentHLSPlaylist.length > 0) {
currentHLSPlaylist.forEach((variant, index) => {
html += `
<div class="quality-option" data-type="hls" data-url="${variant.url}" data-index="${index}">
HLS ${variant.name} (${variant.resolution})
</div>
`;
});
}
// 通常動画オプション
if (currentVideoData.highQualityUrl) {
html += `
<div class="quality-option" data-type="regular" data-url="${currentVideoData.highQualityUrl}">
高画質 (MP4)
</div>
`;
}
if (currentVideoData.lowQualityUrl) {
html += `
<div class="quality-option" data-type="regular" data-url="${currentVideoData.lowQualityUrl}">
低画質 (MP4)
</div>
`;
}
html += '</div>';
// ダウンロードボタン
html += '<button class="download-btn" id="download-quality-btn">この画質でダウンロード</button>';
qualitySelector.innerHTML = html;
// 画質選択イベント
const qualityOptions = qualitySelector.querySelectorAll('.quality-option');
qualityOptions.forEach(option => {
option.addEventListener('click', () => {
// アクティブクラスの切り替え
qualityOptions.forEach(opt => opt.classList.remove('active'));
option.classList.add('active');
const type = option.dataset.type;
const url = option.dataset.url;
currentQualityUrl = url;
if (type === 'hls') {
playHLSVideo(url);
} else {
playRegularVideo(url);
}
});
});
// ダウンロードボタンのイベント
const downloadBtn = document.getElementById('download-quality-btn');
downloadBtn.addEventListener('click', () => {
if (currentQualityUrl) {
downloadVideoFromUrl(currentQualityUrl);
}
});
// 最初のオプションをアクティブにする
if (qualityOptions.length > 0) {
qualityOptions[0].click();
}
}
// 関連動画の表示
function displayRelatedVideos() {
relatedVideosGrid.innerHTML = '';
if (!currentVideoData.relatedVideos || currentVideoData.relatedVideos.length === 0) {
relatedVideosContainer.style.display = 'none';
return;
}
relatedVideosContainer.style.display = 'block';
currentVideoData.relatedVideos.forEach((video, index) => {
// サムネイルURLの取得(優先順位: i > il > if > ip > st1)
let thumbnailUrl = '';
if (video.i) thumbnailUrl = video.i;
else if (video.il) thumbnailUrl = video.il;
else if (video.if) thumbnailUrl = video.if;
else if (video.ip) thumbnailUrl = video.ip;
else if (video.st1) thumbnailUrl = video.st1;
// タイトル
const title = video.t || `関連動画 ${index + 1}`;
// 動画長
const duration = video.d || '';
// 視聴回数
const views = video.n || '';
// ホバー動画
const hoverVideo = video.ipu || '';
// 動画URL(仮 - 実際の構造に合わせて調整が必要)
const videoUrl = `/${video.u || index}`;
// カードを作成
const card = document.createElement('div');
card.className = 'video-card';
card.dataset.url = videoUrl;
const thumbnailContainer = document.createElement('div');
thumbnailContainer.className = 'thumbnail-container';
const thumbnail = document.createElement('img');
thumbnail.className = 'thumbnail';
thumbnail.src = thumbnailUrl;
thumbnail.alt = title;
thumbnail.loading = 'lazy';
const hoverVideoElement = document.createElement('video');
hoverVideoElement.className = 'hover-video';
hoverVideoElement.muted = true;
hoverVideoElement.loop = true;
hoverVideoElement.preload = 'none';
if (hoverVideo) {
const source = document.createElement('source');
source.src = hoverVideo;
source.type = 'video/mp4';
hoverVideoElement.appendChild(source);
}
if (duration) {
const durationBadge = document.createElement('div');
durationBadge.className = 'duration';
durationBadge.textContent = duration;
thumbnailContainer.appendChild(durationBadge);
}
thumbnailContainer.appendChild(thumbnail);
thumbnailContainer.appendChild(hoverVideoElement);
const videoInfo = document.createElement('div');
videoInfo.className = 'video-info';
const videoTitle = document.createElement('div');
videoTitle.className = 'video-title';
videoTitle.textContent = title;
const videoMetadata = document.createElement('div');
videoMetadata.className = 'video-metadata';
let metadataText = duration;
if (views && duration) metadataText += ` • ${views}`;
else if (views) metadataText = views;
videoMetadata.textContent = metadataText || '情報なし';
videoInfo.appendChild(videoTitle);
videoInfo.appendChild(videoMetadata);
card.appendChild(thumbnailContainer);
card.appendChild(videoInfo);
// クリックイベント
card.addEventListener('click', () => {
loadVideo(videoUrl);
});
// ホバーイベント
card.addEventListener('mouseenter', () => {
if (hoverVideo && hoverVideoElement.paused) {
hoverVideoElement.play().catch(e => console.log('ホバー動画の再生に失敗:', e));
}
});
card.addEventListener('mouseleave', () => {
if (!hoverVideoElement.paused) {
hoverVideoElement.pause();
hoverVideoElement.currentTime = 0;
}
});
relatedVideosGrid.appendChild(card);
});
}
let hlsBasePath = '';
function extractHlsBasePath(m3u8Url) {
// https://example.com/path/to/playlist.m3u8
// → https://example.com/path/to/
const match = m3u8Url.match(/^(https?:\/\/[^\/]+\/.*\/)/);
return match ? match[1] : '';
}
// HLSビデオの再生
function playHLSVideo(url) {
hlsBasePath = extractHlsBasePath(url);
// 既存のHLSプレーヤーを破棄
if (hlsPlayer) {
hlsPlayer.destroy();
hlsPlayer = null;
}
// ビデオ要素をリセット
mainVideo.src = '';
mainVideo.load();
// HLSプレーヤーの設定
if (Hls.isSupported()) {
hlsPlayer = new Hls({
debug: false,
enableWorker: true,
xhrSetup: function (xhr) {
const params = 'baseurl23896=' + encodeURIComponent(hlsBasePath);
const originalOpen = xhr.open;
xhr.open = function (method, url, async) {
const newUrl = url.includes('?')
? `${url}&${params}`
: `${url}?${params}`;
return originalOpen.call(xhr, method, newUrl, async);
};
}
});
hlsPlayer.loadSource(getCorsProxyUrl(url));
hlsPlayer.attachMedia(mainVideo);
hlsPlayer.on(Hls.Events.MANIFEST_PARSED, () => {
mainVideo.play().catch(e => console.log('自動再生に失敗:', e));
});
hlsPlayer.on(Hls.Events.ERROR, (event, data) => {
console.error('HLSエラー:', data);
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.error('ネットワークエラー、再試行します');
hlsPlayer.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.error('メディアエラー');
hlsPlayer.recoverMediaError();
break;
default:
console.error('回復不能なエラー');
playFallbackVideo();
break;
}
}
});
} else if (mainVideo.canPlayType('application/vnd.apple.mpegurl')) {
// ネイティブHLSサポート(Safariなど)
mainVideo.src = url;
mainVideo.play().catch(e => console.log('自動再生に失敗:', e));
} else {
console.error('HLSがサポートされていません');
playFallbackVideo();
}
}
// 通常動画の再生
function playRegularVideo(url) {
// HLSプレーヤーを破棄
if (hlsPlayer) {
hlsPlayer.destroy();
hlsPlayer = null;
}
mainVideo.src = getCorsProxyUrl(url);
mainVideo.load();
mainVideo.play().catch(e => console.log('自動再生に失敗:', e));
}
// フォールバック動画の再生
function playFallbackVideo() {
if (currentVideoData.highQualityUrl) {
playRegularVideo(currentVideoData.highQualityUrl);
} else if (currentVideoData.lowQualityUrl) {
playRegularVideo(currentVideoData.lowQualityUrl);
} else {
showError('動画を再生できませんでした');
}
}
// ダウンロード機能
function downloadVideoFromUrl(url) {
// HLSの場合
if (url.includes('.m3u8')) {
const m3u8 = new M3U8();
const download = m3u8.start(url);
download
.on("progress", progress => {
console.log(progress);
showError(`ダウンロード中... ${progress.percentage}%`);
})
.on("finished", finished => {
console.log("完了", finished);
hideError();
})
.on("error", message => {
console.error(message);
showError('ダウンロードに失敗しました: ' + message);
})
.on("aborted", () => {
console.log("Download aborted");
hideError();
});
} else {
// 通常動画の場合
const a = document.createElement('a');
a.href = getCorsProxyUrl(url);
a.download = currentVideoData.title ? `${currentVideoData.title}.mp4` : 'video.mp4';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}
// 履歴からのダウンロード
function downloadVideo(video) {
// 履歴に保存された情報から動画をダウンロード
// 注意: 履歴には動画URLのみが保存されているため、実際のダウンロードは最新の動画URLが必要
// ここでは簡易的に履歴のURLからダウンロードを試みる
downloadVideoFromUrl(video.url);
}
// ビデオグリッドの表示
function showVideoGrid() {
showView('home');
currentView = 'home';
}
// ローディング表示
function showLoading() {
loadingElement.style.display = 'block';
}
function hideLoading() {
loadingElement.style.display = 'none';
}
// エラー表示
function showError(message) {
errorElement.textContent = message;
errorElement.style.display = 'block';
}
function hideError() {
errorElement.style.display = 'none';
}
</script>
</body>
</html>