dea427c8d4
Link support to stream from router via
--streamtplink.php--
<?php
if (!isset($_GET['path'])) {
http_response_code(400);
echo "Missing 'path' parameter.";
exit;
}
$relativePath = ltrim($_GET['path'], '/');
// Validate file extension
if (strtolower(pathinfo($relativePath, PATHINFO_EXTENSION)) !== 'mp4') {
http_response_code(403);
echo "Only .mp4 files are allowed.";
exit;
}
$ftpHost = '192.168.0.1';
$ftpPort = 21;
$ftpUser = 'anonymous';
$ftpPass = ''; // Typically anonymous password can be blank or email
// Connect to FTP
$conn = ftp_connect($ftpHost, $ftpPort, 10);
if (!$conn || !ftp_login($conn, $ftpUser, $ftpPass)) {
http_response_code(500);
echo "FTP connection failed.";
exit;
}
// Get file size
$size = ftp_size($conn, $relativePath);
if ($size === -1) {
ftp_close($conn);
http_response_code(404);
echo "File not found on FTP server.";
exit;
}
$start = 0;
$end = $size - 1;
$length = $size;
if (isset($_SERVER['HTTP_RANGE']) &&
preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches)) {
$start = intval($matches[1]);
$end = ($matches[2] !== '') ? intval($matches[2]) : $end;
if ($start > $end || $start >= $size) {
ftp_close($conn);
http_response_code(416);
header("Content-Range: bytes */$size");
exit;
}
$length = $end - $start + 1;
http_response_code(206);
header("Content-Range: bytes $start-$end/$size");
} else {
http_response_code(200);
}
// Set headers
$filename = basename($relativePath);
header("Content-Type: video/mp4");
header("Content-Length: $length");
header("Accept-Ranges: bytes");
header("Content-Disposition: inline; filename=\"$filename\"");
// Open a temp stream
$tempHandle = fopen('php://temp', 'r+');
if (!ftp_fget($conn, $tempHandle, $relativePath, FTP_BINARY, 0)) {
ftp_close($conn);
http_response_code(500);
echo "Failed to download file from FTP.";
exit;
}
ftp_close($conn);
// Seek to the range offset
rewind($tempHandle);
if ($start > 0) {
fseek($tempHandle, $start);
}
// Stream the requested range
$remaining = $length;
while (!feof($tempHandle) && $remaining > 0) {
$chunkSize = min(8192, $remaining);
$data = fread($tempHandle, $chunkSize);
echo $data;
flush();
$remaining -= strlen($data);
}
fclose($tempHandle);
491 lines
17 KiB
HTML
491 lines
17 KiB
HTML
<a target="_blank" href="https://codeberg.org/alceawisteria/YoutubeArchiveSuite" style=color:lightgray>src</a>
|
|
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Video Archive</title>
|
|
<style>
|
|
body {
|
|
font-family: system-ui, sans-serif;
|
|
background: #f5f7fa;
|
|
margin: 0;
|
|
}
|
|
h1 {
|
|
text-align: center;
|
|
margin: 1rem 0;
|
|
}
|
|
#search-container {
|
|
max-width: 600px;
|
|
margin: 0 auto 1rem auto;
|
|
padding: 0 1rem;
|
|
}
|
|
#search {
|
|
width: 100%;
|
|
padding: 0.5rem 1rem;
|
|
font-size: 1rem;
|
|
border: 1px solid #ccc;
|
|
border-radius: 6px;
|
|
box-sizing: border-box;
|
|
}
|
|
#videos {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 1rem;
|
|
padding: 1rem;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
.card {
|
|
background: #fff;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
video {
|
|
width: 100%;
|
|
border-radius: 4px;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.card h3 {
|
|
margin: 0 0 0.25rem 0;
|
|
font-size: 1rem;
|
|
}
|
|
.card p {
|
|
margin: 0 0 0.5rem 0;
|
|
font-size: 0.875rem;
|
|
color: #555;
|
|
}
|
|
.links a {
|
|
font-size: 0.875rem;
|
|
color: #0066cc;
|
|
margin-right: 0.25rem;
|
|
text-decoration: none;
|
|
}
|
|
.links a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.direct-unreachable {
|
|
color: red !important;
|
|
pointer-events: none;
|
|
cursor: default;
|
|
}
|
|
#error {
|
|
text-align: center;
|
|
color: red;
|
|
font-weight: bold;
|
|
margin-top: 1rem;
|
|
}
|
|
.toggle-src {
|
|
margin-top: 0.5rem;
|
|
padding: 0.4rem;
|
|
font-size: 0.85rem;
|
|
background: #eef;
|
|
border: 1px solid #aac;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.toggle-src:hover {
|
|
background: #dde;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Youtube Video Archive</h1>
|
|
[<a target="_blank" href="backup.php" style=color:blue>⬆️Upload</a>]
|
|
<a target="_blank" href="list.html" style=color:blue>List</a>
|
|
[<a href="javascript:if(!window.location.href.includes('extendedsearch=true')){window.history.pushState({}, '', window.location.href + (window.location.href.includes('?') ? '&' : '?') + 'extendedsearch=true');}">Enable RealtimeSearch</a>]
|
|
<style>
|
|
body{font-family:Arial,sans-serif;margin:20px}.video-links{list-style-type:none;padding:0}.video-links li{margin:10px 0}.video-links a{text-decoration:none;color:#007bff;font-size:16px}.video-links a:hover{text-decoration:underline}
|
|
</style>
|
|
<ul class="video-links" id="video-list"></ul>
|
|
<script>
|
|
async function fetchVideos() {
|
|
try {
|
|
const response = await fetch('videos.json?' + new Date().getTime());
|
|
const videos = await response.json();
|
|
const latestVideos = videos.slice(-3);
|
|
const videoList = document.getElementById('video-list');
|
|
|
|
latestVideos.forEach(video => {
|
|
const index = videos.indexOf(video); // Get original index
|
|
const listItem = document.createElement('li');
|
|
const link = document.createElement('a');
|
|
link.href = `?limit=8&extendedsearch=true&videoid=${video.id}`;
|
|
link.textContent = `${index + 1}. ${video.title}`; // Show 1-based index
|
|
listItem.appendChild(link);
|
|
videoList.appendChild(listItem);
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching videos:", error);
|
|
}
|
|
}
|
|
fetchVideos();
|
|
</script>
|
|
|
|
<div id="search-container">
|
|
<input type="text" id="search" placeholder="Search videos by title, uploader, or ID..." />
|
|
</div>
|
|
|
|
<div style="text-align:center; margin: 1rem;">
|
|
<button id="check-links">🔍 Check Direct Links</button>
|
|
</div>
|
|
|
|
<div id="videos"></div>
|
|
<div id="error"></div>
|
|
<script>
|
|
// Ultra-robust JSON parser with multiple fallbacks
|
|
function superParseJSON(jsonString) {
|
|
// First try standard parsing
|
|
try {
|
|
return JSON.parse(jsonString);
|
|
} catch (e1) {
|
|
console.warn("Standard parse failed, attempting cleanup:", e1);
|
|
|
|
// Try removing BOM if present
|
|
try {
|
|
if (jsonString.charCodeAt(0) === 0xFEFF) {
|
|
return JSON.parse(jsonString.substring(1));
|
|
}
|
|
} catch (e2) {}
|
|
|
|
// Try fixing common JSON issues
|
|
try {
|
|
// Fix escaped quotes
|
|
let fixed = jsonString.replace(/\\'/g, "'")
|
|
.replace(/\\"/g, '"')
|
|
.replace(/\\\//g, '/');
|
|
return JSON.parse(fixed);
|
|
} catch (e3) {
|
|
console.warn("Quote fixing failed, trying last resort:", e3);
|
|
|
|
// Last resort - try eval (with safety check)
|
|
try {
|
|
if (jsonString.trim().startsWith("{") || jsonString.trim().startsWith("[")) {
|
|
return (new Function('return ' + jsonString))();
|
|
}
|
|
} catch (e4) {
|
|
throw new Error(`All JSON parsing attempts failed. Errors: ${e1.message}, ${e3.message}, ${e4.message}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
const errorEl = document.getElementById('error');
|
|
const videosEl = document.getElementById('videos');
|
|
const searchInput = document.getElementById('search');
|
|
const checkLinksButton = document.getElementById('check-links');
|
|
|
|
// Function to check for escaped slashes and show warnings
|
|
function checkAndWarnForEscapedSlashes(url, videoId) {
|
|
if (url && url.includes('\\/')) {
|
|
console.warn(`Escaped slashes detected in URL for video ${videoId}:`, url);
|
|
|
|
// Create warning banner if not already present
|
|
if (!document.getElementById('escaped-slashes-warning')) {
|
|
const warningBanner = document.createElement('div');
|
|
warningBanner.id = 'escaped-slashes-warning';
|
|
warningBanner.style.cssText = `
|
|
background-color: #fff3cd;
|
|
color: #856404;
|
|
padding: 10px;
|
|
margin-bottom: 15px;
|
|
border-radius: 4px;
|
|
border-left: 4px solid #ffeeba;
|
|
`;
|
|
warningBanner.innerHTML = `
|
|
⚠ Warning: Some video URLs contain escaped slashes (\\/).
|
|
This may cause playback issues in some browsers.
|
|
`;
|
|
videosEl.parentNode.insertBefore(warningBanner, videosEl);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function isReachable(url) {
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
const res = await fetch(url, { method: 'HEAD', signal: controller.signal });
|
|
clearTimeout(timeoutId);
|
|
return res.ok || (res.status >= 300 && res.status < 400);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
let allVideos = []; // Stores ALL videos (unfiltered, unlimited)
|
|
let displayedVideos = []; // Stores currently displayed videos (may be limited/filtered)
|
|
|
|
function renderVideos(list) {
|
|
videosEl.innerHTML = '';
|
|
let hasEscapedSlashWarning = false;
|
|
displayedVideos = list;
|
|
|
|
for (const v of list) {
|
|
// Check for escaped slashes in the waybackurl
|
|
const hasEscapedSlashes = checkAndWarnForEscapedSlashes(v.waybackurl, v.id);
|
|
if (hasEscapedSlashes) hasEscapedSlashWarning = true;
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
const videoId = `video-${v.id}`;
|
|
const unreachableClass = v.directOk === false ? 'direct-unreachable' : '';
|
|
|
|
card.innerHTML = `
|
|
<div class="video-container">
|
|
<video id="${videoId}" src="${v.waybackurl}" controls preload="metadata"></video>
|
|
${hasEscapedSlashes ? '<div class="url-warning" style="color:red;font-size:12px;">⚠ URL contains escaped slashes</div>' : ''}
|
|
</div>
|
|
<h3>${v.title || 'Untitled'}</h3>
|
|
<p>Uploader: ${v.uploader || 'Unknown'}</p>
|
|
<p>ID: ${v.id ?? 'N/A'}</p>
|
|
<p>Uploaded: ${v.uploaded ? new Date(v.uploaded).toLocaleString() : 'Unknown date'}</p>
|
|
<div class="links">
|
|
<a href="${v.url || '#'}" target="_blank" class="direct-link ${unreachableClass}">Direct</a>
|
|
<a href="${v.waybackurl || '#'}" target="_blank">Wayback</a><a target="_blank" href="waybackurlcheck.html?waybackurl=${v.waybackurl || ''}" style="color:green">(?)</a>
|
|
<a href="javascript:void(0);" class="youtube-link" data-id="${v.id || ''}">YouTube</a>
|
|
${v.id ? `<a href="https://web.archive.org/web/2oe_/http://wayback-fakeurl.archive.org/yt/${v.id}" target="_blank"><img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT61taAw7gmjapOgSz2nugoCrazkCg42il25A&s" width="20px"></a>` : ''}
|
|
${v.id ? `<a href="https://preservetube.com/watch?v=${v.id}" target="_blank"><img src="https://i.ibb.co/SXjxP6v6/Screenshot-20250625-224044-Brave.png" width="20px"></a>` : ''}
|
|
${v.id ? `<a href="https://alcea-wisteria.de/hidrive/hidrive.php?path=/public/ytarchive/${v.id}.mp4" target="_blank"><img src="https://media.technologycounter.com/vendors/software-products/software-logo/hidrive.jpg" width="20px"></a>` : ''}
|
|
${v.id ? `<a href="http://127.0.0.1:8080/2025-06-28-FileServerUpload/2025-06-28-MultiUpload(SFTP-FTP)/streamtplink.php?path=/(YT-To-Upload)/ytarchive/${v.id}.mp4" target="_blank"><img src="https://images.seeklogo.com/logo-png/43/1/tp-link-logo-png_seeklogo-434726.png" width="20px"></a>` : ''}
|
|
</div>
|
|
<div class="current-link"></div>
|
|
<button class="toggle-src" data-id="#">🔁 Switch</button>
|
|
`;
|
|
|
|
videosEl.appendChild(card);
|
|
|
|
const toggleBtn = card.querySelector('.toggle-src');
|
|
const videoEl = card.querySelector('video');
|
|
const currentLinkContainer = card.querySelector('.current-link');
|
|
const sources = [
|
|
{ url: v.id ? `fin/${v.id}.mp4` : '', label: '🔁 Switch' },
|
|
{ url: v.id ? `https://alcea-wisteria.de/hidrive/hidrive.php?path=/public/ytarchive/${v.id}.mp4` : '', label: '🔁 Switch' },
|
|
{ url: v.waybackurl || '', label: '🔁 Switch' },
|
|
].filter(source => source.url); // Filter out empty URLs
|
|
|
|
if (sources.length > 0) {
|
|
let currentIndex = 0;
|
|
toggleBtn.addEventListener('click', () => {
|
|
currentIndex = (currentIndex + 1) % sources.length;
|
|
videoEl.src = sources[currentIndex].url;
|
|
toggleBtn.textContent = sources[currentIndex].label;
|
|
let currentLink = currentLinkContainer.querySelector('a');
|
|
if (!currentLink) {
|
|
currentLink = document.createElement('a');
|
|
currentLink.target = '_blank';
|
|
currentLink.textContent = '🔗 Current Link';
|
|
currentLinkContainer.appendChild(currentLink);
|
|
}
|
|
currentLink.href = sources[currentIndex].url;
|
|
if (sources[currentIndex].url.includes("fin")) {
|
|
currentLink.textContent = '🔗 Direct Link';
|
|
} else if (sources[currentIndex].url.includes("hidrive")) {
|
|
currentLink.textContent = '🔗 Hidrive Link';
|
|
} else if (sources[currentIndex].url.includes("web.archive")) {
|
|
currentLink.textContent = '🔗 Archive Link';
|
|
}
|
|
});
|
|
} else {
|
|
toggleBtn.disabled = true;
|
|
toggleBtn.textContent = 'No sources';
|
|
}
|
|
}
|
|
|
|
// Update search placeholder with TOTAL count (not limited count)
|
|
searchInput.placeholder = `Search ${allVideos.length} videos...`;
|
|
|
|
// Hide warning banner if no escaped slashes were found
|
|
if (!hasEscapedSlashWarning) {
|
|
const warningBanner = document.getElementById('escaped-slashes-warning');
|
|
if (warningBanner) warningBanner.remove();
|
|
}
|
|
}
|
|
|
|
async function loadVideos() {
|
|
const sources = [
|
|
'videos.json',
|
|
'./videos.json',
|
|
'https://ry3yr.github.io/videos.json',
|
|
];
|
|
|
|
for (const source of sources) {
|
|
try {
|
|
const url = new URL(source, window.location.href);
|
|
url.searchParams.set('_', Date.now());
|
|
const res = await fetch(url.toString(), { cache: 'no-store' });
|
|
|
|
if (res.ok) {
|
|
const text = await res.text();
|
|
try {
|
|
allVideos = superParseJSON(text);
|
|
if (Array.isArray(allVideos)) {
|
|
console.log(`Successfully loaded ${allVideos.length} videos from ${source}`);
|
|
return true;
|
|
} else {
|
|
console.warn(`Data from ${source} is not an array`);
|
|
}
|
|
} catch (parseError) {
|
|
console.warn(`Failed to parse JSON from ${source}:`, parseError);
|
|
}
|
|
}
|
|
} catch (fetchError) {
|
|
console.warn(`Failed to fetch from ${source}:`, fetchError);
|
|
}
|
|
}
|
|
|
|
errorEl.textContent = 'Failed to load videos from all available sources.';
|
|
return false;
|
|
}
|
|
|
|
const success = await loadVideos();
|
|
if (!success) return;
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const limit = parseInt(params.get('limit')) || 0;
|
|
const videoId = params.get('videoid');
|
|
|
|
// Ensure allVideos is always an array
|
|
if (!Array.isArray(allVideos)) allVideos = [];
|
|
|
|
// Initialize with all videos (limit will be applied in render)
|
|
displayedVideos = [...allVideos];
|
|
|
|
// Apply limit if specified (but keep search count showing total)
|
|
const initialVideos = limit > 0 ? allVideos.slice(0, limit) : [...allVideos];
|
|
|
|
renderVideos(initialVideos);
|
|
|
|
searchInput.addEventListener('input', () => {
|
|
const query = searchInput.value.trim().toLowerCase();
|
|
let filtered = [...allVideos]; // Always search through ALL videos
|
|
|
|
if (query) {
|
|
filtered = allVideos.filter(v =>
|
|
(v.title?.toLowerCase().includes(query)) ||
|
|
(v.uploader?.toLowerCase().includes(query)) ||
|
|
(v.id?.toString().toLowerCase().includes(query))
|
|
);
|
|
}
|
|
|
|
// Apply limit to filtered results (but search count remains total)
|
|
if (limit > 0) filtered = filtered.slice(0, limit);
|
|
|
|
renderVideos(filtered);
|
|
});
|
|
|
|
if (videoId) {
|
|
searchInput.value = videoId;
|
|
searchInput.dispatchEvent(new Event('input'));
|
|
}
|
|
|
|
checkLinksButton.addEventListener('click', async () => {
|
|
checkLinksButton.disabled = true;
|
|
checkLinksButton.textContent = '⏳ Checking...';
|
|
|
|
// Process in batches to avoid overwhelming the browser
|
|
const batchSize = 5;
|
|
for (let i = 0; i < allVideos.length; i += batchSize) {
|
|
const batch = allVideos.slice(i, i + batchSize);
|
|
await Promise.all(batch.map(async (v) => {
|
|
v.directOk = await isReachable(v.url);
|
|
}));
|
|
|
|
// Update UI after each batch
|
|
const currentFilter = searchInput.value.trim().toLowerCase();
|
|
let filtered = [...allVideos];
|
|
if (currentFilter) {
|
|
filtered = allVideos.filter(v =>
|
|
(v.title?.toLowerCase().includes(currentFilter)) ||
|
|
(v.uploader?.toLowerCase().includes(currentFilter)) ||
|
|
(v.id?.toString().toLowerCase().includes(currentFilter))
|
|
);
|
|
}
|
|
if (limit > 0) filtered = filtered.slice(0, limit);
|
|
renderVideos(filtered);
|
|
}
|
|
|
|
checkLinksButton.disabled = false;
|
|
checkLinksButton.textContent = '🔍 Check Direct Links';
|
|
});
|
|
|
|
const isExtendedSearchActive = params.has('extendedsearch') || localStorage.getItem('extendedsearch') === 'true';
|
|
if (isExtendedSearchActive) {
|
|
localStorage.setItem('extendedsearch', 'true');
|
|
const suggestionList = document.createElement('ul');
|
|
Object.assign(suggestionList.style, {
|
|
position: 'absolute',
|
|
maxHeight: '300px',
|
|
overflowY: 'auto',
|
|
width: '100%',
|
|
backgroundColor: '#fff',
|
|
border: '1px solid #ccc',
|
|
borderRadius: '6px',
|
|
zIndex: '10',
|
|
listStyle: 'none',
|
|
padding: '0',
|
|
margin: '0'
|
|
});
|
|
searchInput.parentElement.appendChild(suggestionList);
|
|
|
|
searchInput.addEventListener('input', () => {
|
|
const query = searchInput.value.trim().toLowerCase();
|
|
suggestionList.innerHTML = '';
|
|
if (query) {
|
|
const filtered = allVideos.filter(v => v.title?.toLowerCase().includes(query));
|
|
filtered.forEach(v => {
|
|
const item = document.createElement('li');
|
|
item.style.padding = '0.5rem';
|
|
item.style.cursor = 'pointer';
|
|
item.textContent = v.title;
|
|
item.addEventListener('click', () => {
|
|
searchInput.value = v.title;
|
|
suggestionList.innerHTML = '';
|
|
searchInput.dispatchEvent(new Event('input'));
|
|
});
|
|
suggestionList.appendChild(item);
|
|
});
|
|
}
|
|
});
|
|
|
|
searchInput.addEventListener('blur', () => {
|
|
setTimeout(() => suggestionList.innerHTML = '', 200);
|
|
});
|
|
}
|
|
})();
|
|
|
|
document.addEventListener('click', function(event) {
|
|
if (event.target && event.target.classList.contains('youtube-link')) {
|
|
const videoId = event.target.getAttribute('data-id');
|
|
if (!videoId) return;
|
|
|
|
const card = event.target.closest('.card');
|
|
if (!card) return;
|
|
|
|
const videoContainer = card.querySelector('.video-container');
|
|
if (!videoContainer) return;
|
|
|
|
const iframe = document.createElement('iframe');
|
|
iframe.width = '100%';
|
|
iframe.height = '315';
|
|
iframe.src = `https://www.youtube.com/embed/${videoId}`;
|
|
iframe.frameBorder = '0';
|
|
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
|
|
iframe.allowFullscreen = true;
|
|
videoContainer.innerHTML = '';
|
|
videoContainer.appendChild(iframe);
|
|
event.target.textContent = 'YouTube (Playing)';
|
|
event.target.classList.add('playing');
|
|
}
|
|
});
|
|
</script>
|