Spaces:
Running
Running
| <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 ; | |
| 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 ; | |
| 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> |