Files
Alcea dea427c8d4 Add files via upload
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);
2025-08-23 01:55:16 +02:00

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>