to fix youtube 0 byte error

This commit is contained in:
celebrateyang 2025-06-08 22:58:00 +08:00
parent 996a7426f6
commit 7369bc3687
3 changed files with 117 additions and 29 deletions

View File

@ -13,22 +13,45 @@ async function* readChunks(streamInfo, size) {
throw new Error("controller aborted"); throw new Error("controller aborted");
} }
const chunk = await request(streamInfo.url, { let retries = 3;
headers: { let chunk;
...getHeaders('youtube'),
Range: `bytes=${read}-${read + CHUNK_SIZE}`
},
dispatcher: streamInfo.dispatcher,
signal: streamInfo.controller.signal,
maxRedirections: 4
});
if (chunk.statusCode === 403 && chunksSinceTransplant >= 3 && streamInfo.transplant) { // Retry mechanism for failed chunks
chunksSinceTransplant = 0; while (retries > 0) {
try { try {
await streamInfo.transplant(streamInfo.dispatcher); chunk = await request(streamInfo.url, {
continue; headers: {
} catch {} ...getHeaders('youtube'),
Range: `bytes=${read}-${read + CHUNK_SIZE}`
},
dispatcher: streamInfo.dispatcher,
signal: streamInfo.controller.signal,
maxRedirections: 4
});
// Check for valid response before processing
if (chunk.statusCode === 206 || chunk.statusCode === 200) {
break; // Success, exit retry loop
} else if (chunk.statusCode === 403 && chunksSinceTransplant >= 3 && streamInfo.transplant) {
chunksSinceTransplant = 0;
try {
await streamInfo.transplant(streamInfo.dispatcher);
continue; // Retry with transplanted connection
} catch {}
}
// For other status codes, retry
throw new Error(`HTTP ${chunk.statusCode} response`);
} catch (error) {
retries--;
if (retries === 0) {
throw new Error(`Failed to fetch chunk after 3 attempts: ${error.message}`);
}
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, (4 - retries) * 1000));
}
} }
chunksSinceTransplant++; chunksSinceTransplant++;
@ -36,8 +59,9 @@ async function* readChunks(streamInfo, size) {
const expected = min(CHUNK_SIZE, size - read); const expected = min(CHUNK_SIZE, size - read);
const received = BigInt(chunk.headers['content-length']); const received = BigInt(chunk.headers['content-length']);
if (received < expected / 2n) { // Validate we received some data
closeRequest(streamInfo.controller); if (received === 0n && expected > 0n) {
throw new Error("Received empty chunk when data was expected");
} }
for await (const data of chunk.body) { for await (const data of chunk.body) {
@ -50,7 +74,13 @@ async function* readChunks(streamInfo, size) {
async function handleYoutubeStream(streamInfo, res) { async function handleYoutubeStream(streamInfo, res) {
const { signal } = streamInfo.controller; const { signal } = streamInfo.controller;
const cleanup = () => (res.end(), closeRequest(streamInfo.controller)); const cleanup = () => {
// Only end response if headers haven't been sent to prevent 0-byte files
if (!res.headersSent) {
res.end();
}
closeRequest(streamInfo.controller);
};
try { try {
let req, attempts = 3; let req, attempts = 3;
@ -78,6 +108,21 @@ async function handleYoutubeStream(streamInfo, res) {
return cleanup(); return cleanup();
} }
// Check if client sent a Range header for partial content request
const rangeHeader = streamInfo.headers?.get('range');
if (rangeHeader && req.headers.get('accept-ranges') === 'bytes') {
// Handle range request - delegate to handleGenericStream for proper range handling
return await handleGenericStream(streamInfo, res);
}
// Set headers before starting stream to ensure they're sent
for (const headerName of ['content-type', 'content-length', 'accept-ranges']) {
const headerValue = req.headers.get(headerName);
if (headerValue) res.setHeader(headerName, headerValue);
}
// Full file request - use chunked download approach
const generator = readChunks(streamInfo, size); const generator = readChunks(streamInfo, size);
const abortGenerator = () => { const abortGenerator = () => {
@ -89,13 +134,17 @@ async function handleYoutubeStream(streamInfo, res) {
const stream = Readable.from(generator); const stream = Readable.from(generator);
for (const headerName of ['content-type', 'content-length']) { // Add error handling to prevent 0-byte files
const headerValue = req.headers.get(headerName); stream.on('error', (error) => {
if (headerValue) res.setHeader(headerName, headerValue); console.error('YouTube stream error:', error);
} if (!res.headersSent) {
res.status(500).end();
}
});
pipe(stream, res, cleanup); pipe(stream, res, cleanup);
} catch { } catch (error) {
console.error('YouTube stream handling error:', error);
cleanup(); cleanup();
} }
} }

View File

@ -41,11 +41,43 @@ export function getHeaders(service) {
} }
export function pipe(from, to, done) { export function pipe(from, to, done) {
from.on('error', done) let finished = false;
.on('close', done); let bytesWritten = 0;
to.on('error', done) const cleanup = (error) => {
.on('close', done); if (finished) return;
finished = true;
// Log if we got 0 bytes to help debug production issues
if (bytesWritten === 0 && !error) {
console.warn('Stream completed with 0 bytes written - potential issue');
}
done(error);
};
from.on('error', (error) => {
console.error('Stream source error:', error);
cleanup(error);
})
.on('close', () => {
if (!finished) {
cleanup();
}
})
.on('data', (chunk) => {
bytesWritten += chunk.length;
});
to.on('error', (error) => {
console.error('Stream destination error:', error);
cleanup(error);
})
.on('close', () => {
if (!finished) {
cleanup();
}
});
from.pipe(to); from.pipe(to);
} }

View File

@ -54,7 +54,14 @@
<div class="notification" role="alert"> <div class="notification" role="alert">
<div class="notification-content"> <div class="notification-content">
<span class="notification-icon">🎉</span> <span class="notification-icon">🎉</span>
<span class="notification-text">最新功能=>【传输】: 你可以方便的在你的设备之间传输文件和文本。采用端到端加密技术,保护隐私性!</span> <button <span class="notification-text">
通知:
<br>
1. 上线最新功能=>【传输】: 此功能方便在你的设备之间传输文件和文本。采用端到端加密技术,保护隐私性!
<br>
2. youtube下载遇到了些问题正在修复中...
</span>
<button
class="notification-close" class="notification-close"
aria-label="关闭通知" aria-label="关闭通知"
on:click={closeNotification} on:click={closeNotification}