Repository navigation
Expand file tree
/
Copy pathapp.js
More file actions
153 lines (134 loc) · 4.72 KB
/
Copy pathapp.js
File metadata and controls
153 lines (134 loc) · 4.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// app.js
// Create cookies from Railway environment variable
import fs from 'fs';
if (process.env.YTDLP_COOKIES) {
fs.writeFileSync('./cookies.txt', process.env.YTDLP_COOKIES, 'utf-8');
// Never log the contents: they are a logged-in YouTube session.
console.log(`Cookies file written (${process.env.YTDLP_COOKIES.length} bytes).`);
} else {
console.warn('⚠️ YTDLP_COOKIES env var is not set! YouTube downloads may fail.');
}
const cookieOptions = fs.existsSync('./cookies.txt') ? { cookies: './cookies.txt' } : {};
// Newer yt-dlp needs a JavaScript runtime to solve YouTube's challenges; node is already here.
const runtimeOptions = { jsRuntimes: 'node', ...cookieOptions };
import express from 'express';
//import ytdlp from 'yt-dlp-exec'; // old way just to try compare
import { create } from 'yt-dlp-exec'; // This one is local copy and newer
const ytdlp = create('./yt-dlp'); // Use the local binary from prestart
const app = express();
app.disable('x-powered-by');
const PORT = 3000; // hard‐code 3000 so Railway’s edge proxy lines up
const YOUTUBE_HOSTS = ['youtube.com', 'youtu.be'];
const X_HOSTS = ['x.com', 'twitter.com'];
const hostMatches = (host, domains) =>
domains.some((d) => host === d || host.endsWith(`.${d}`));
// Accept only a real http(s) URL on a supported host. The normalized href always
// starts with "http", so yt-dlp can never read it as a command-line option.
function parseVideoUrl(raw) {
if (typeof raw !== 'string' || raw.length > 2048) return null;
let url;
try {
url = new URL(raw);
} catch {
return null;
}
if (url.protocol !== 'https:' && url.protocol !== 'http:') return null;
if (url.username || url.password) return null;
const host = url.hostname.toLowerCase();
if (hostMatches(host, YOUTUBE_HOSTS)) return { href: url.href, platform: 'youtube' };
if (hostMatches(host, X_HOSTS)) return { href: url.href, platform: 'x' };
return null;
}
const escapeHtml = (s) =>
String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
// Health check
app.get('/', (_req, res) => {
res.send('🎬 YouTube-proxy service is running.');
});
app.get('/formats', async (req, res) => {
if (!req.query.url) {
return res.status(400).send('❌ Missing required query parameter: ?url=');
}
const video = parseVideoUrl(req.query.url);
if (!video) {
return res.status(400).send('❌ Unsupported URL.');
}
try {
const stdout = await ytdlp(video.href, {
...runtimeOptions,
listFormats: true,
addHeader: [
'User-Agent:Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36',
'X-YouTube-Client-Name:55',
'X-YouTube-Client-Version:1.0'
],
noCheckCertificates: true,
noWarnings: true,
preferInsecure: true,
addHeaders: {
'Accept': '*/*',
'Origin': 'https://www.youtube.com',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'cors'
}
});
res.send(`<pre>${escapeHtml(stdout)}</pre>`);
} catch (err) {
console.error('Format list error:', err);
res.status(500).send('❌ Failed to list formats.');
}
});
app.get('/stream', async (req, res) => {
if (!req.query.url) {
return res.status(400).send('❌ Missing required query parameter: ?url=');
}
const video = parseVideoUrl(req.query.url);
if (!video) {
return res.status(400).send('❌ Unsupported platform.');
}
// Detect platform
let format, headers;
if (video.platform === 'youtube') {
format = '18';
headers = {
'User-Agent': 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36',
'X-YouTube-Client-Name': '55',
'X-YouTube-Client-Version': '1.0',
'Accept': '*/*',
'Origin': 'https://www.youtube.com'
};
} else {
format = 'best[protocol=m3u8]'; // auto-select best HLS stream for X/Twitter
headers = {
'User-Agent': 'Mozilla/5.0',
'Accept': '*/*',
'Origin': 'https://x.com'
};
}
try {
const stdout = await ytdlp(video.href, {
format,
getUrl: true,
...runtimeOptions,
noCheckCertificates: true,
noWarnings: true,
preferInsecure: true,
addHeader: Object.entries(headers).map(([k, v]) => `${k}:${v}`)
});
const directUrl = stdout.trim();
res.json({
url: directUrl,
headers
});
} catch (err) {
console.error('Stream error:', err);
res.status(500).send('❌ Failed to fetch video.');
}
});
app.use((err, _req, res, _next) => {
console.error('Unhandled error:', err);
res.status(500).send('❌ Internal error.');
});
app.listen(PORT, () => {
console.log(`🚀 Listening on port ${PORT}`);
});