Skip to content

Commit db700fb

Browse files
committed
chore: remove Vercel configuration and restore standalone server
1 parent cee5a1e commit db700fb

4 files changed

Lines changed: 135 additions & 157 deletions

File tree

‎api/index.js‎

Lines changed: 0 additions & 141 deletions
This file was deleted.

‎server.js‎

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,140 @@
1-
import app from './api/index.js';
1+
import express from 'express';
2+
import cors from 'cors';
3+
import axios from 'axios';
4+
import { YoutubeTranscript } from 'youtube-transcript';
25

6+
const app = express();
37
const PORT = process.env.PORT || 3001;
48

9+
app.use(cors());
10+
app.use(express.json());
11+
12+
// Helper to extract video ID from YouTube URL (supports watch, shorts, live, youtu.be, and raw ID)
13+
function extractVideoId(url) {
14+
if (!url) return null;
15+
const trimmed = url.trim();
16+
if (/^[a-zA-Z0-9_-]{11}$/.test(trimmed)) return trimmed;
17+
const regExp = /(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/|live\/))([\w-]{11})/;
18+
const match = trimmed.match(regExp);
19+
return match ? match[1] : null;
20+
}
21+
22+
// Health check endpoint
23+
app.get('/api/health', (req, res) => {
24+
res.json({ status: 'ok', time: new Date().toISOString() });
25+
});
26+
27+
// Video info & transcript endpoint
28+
app.get('/api/video-info', async (req, res) => {
29+
const videoUrl = req.query.url;
30+
if (!videoUrl) {
31+
return res.status(400).json({ error: 'Missing url parameter' });
32+
}
33+
34+
const videoId = extractVideoId(videoUrl);
35+
if (!videoId) {
36+
return res.status(400).json({ error: 'Invalid YouTube URL or Video ID' });
37+
}
38+
39+
try {
40+
let title = 'YouTube Video';
41+
let channel = 'YouTube Channel';
42+
let lengthSeconds = 0;
43+
let thumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
44+
45+
// 1. Attempt watch page parsing for rich metadata (Duration, precise title)
46+
try {
47+
const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
48+
const response = await axios.get(watchUrl, {
49+
headers: {
50+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
51+
'Accept-Language': 'en-US,en;q=0.9'
52+
},
53+
timeout: 5000
54+
});
55+
56+
const html = response.data;
57+
const jsonRegex = /ytInitialPlayerResponse\s*=\s*({.+?});/;
58+
const match = html.match(jsonRegex);
59+
if (match) {
60+
const playerResponse = JSON.parse(match[1]);
61+
const videoDetails = playerResponse.videoDetails || {};
62+
if (videoDetails.title) title = videoDetails.title;
63+
if (videoDetails.author) channel = videoDetails.author;
64+
if (videoDetails.lengthSeconds) lengthSeconds = parseInt(videoDetails.lengthSeconds, 10);
65+
}
66+
} catch (scrapeErr) {
67+
console.warn('[Server] Watch page scraping skipped/failed:', scrapeErr.message);
68+
}
69+
70+
// 2. Fallback to Official YouTube oEmbed API if title is still default
71+
if (title === 'YouTube Video') {
72+
try {
73+
const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`;
74+
const oembedRes = await axios.get(oembedUrl, { timeout: 4000 });
75+
if (oembedRes.data) {
76+
title = oembedRes.data.title || title;
77+
channel = oembedRes.data.author_name || channel;
78+
if (oembedRes.data.thumbnail_url) thumbnailUrl = oembedRes.data.thumbnail_url;
79+
}
80+
} catch (oembedErr) {
81+
console.warn('[Server] oEmbed fallback error:', oembedErr.message);
82+
}
83+
}
84+
85+
const metadata = {
86+
title,
87+
channel,
88+
duration: lengthSeconds,
89+
videoId,
90+
thumbnailUrl
91+
};
92+
93+
// 3. Fetch Transcript using youtube-transcript library
94+
console.log(`[Server] Fetching transcript for video ID: ${videoId}...`);
95+
let transcript = [];
96+
let captionWarning = null;
97+
98+
try {
99+
const rawTranscript = await YoutubeTranscript.fetchTranscript(videoId);
100+
transcript = rawTranscript.map(item => ({
101+
text: item.text,
102+
start: item.offset / 1000,
103+
duration: item.duration / 1000
104+
}));
105+
console.log(`[Server] Successfully parsed ${transcript.length} transcript lines.`);
106+
} catch (transcriptError) {
107+
console.warn('[Server] Captions not directly accessible via default language:', transcriptError.message);
108+
try {
109+
const rawTranscript = await YoutubeTranscript.fetchTranscript(videoId, { lang: 'en' });
110+
transcript = rawTranscript.map(item => ({
111+
text: item.text,
112+
start: item.offset / 1000,
113+
duration: item.duration / 1000
114+
}));
115+
console.log(`[Server] Successfully parsed ${transcript.length} lines via fallback language.`);
116+
} catch (fallbackErr) {
117+
console.warn('[Server] Fallback transcript attempt also failed:', fallbackErr.message);
118+
captionWarning = 'No automated captions were found for this video. You can write custom notes or paste a transcript manually.';
119+
}
120+
}
121+
122+
if (!metadata.duration && transcript.length > 0) {
123+
metadata.duration = Math.ceil(transcript[transcript.length - 1].start + (transcript[transcript.length - 1].duration || 10));
124+
}
125+
126+
return res.json({
127+
metadata,
128+
transcript,
129+
warning: captionWarning
130+
});
131+
132+
} catch (error) {
133+
console.error('Error fetching YouTube info:', error.message);
134+
return res.status(500).json({ error: `Server error: ${error.message}` });
135+
}
136+
});
137+
5138
app.listen(PORT, () => {
6139
console.log(`[Server] YouTube-to-PDF backend running on http://localhost:${PORT}`);
7140
});

‎src/App.jsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,11 @@ export default function App() {
110110
return parsed;
111111
};
112112

113-
// Safe fetch helper that handles CORS, Vercel Serverless, and non-JSON HTML error pages gracefully
113+
// Safe fetch helper that handles CORS, proxies, and non-JSON HTML error pages gracefully
114114
const fetchVideoData = async (url) => {
115115
const isLocal = typeof window !== 'undefined' &&
116116
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
117117

118-
// On Vercel production, avoid insecure HTTP mixed-content calls to localhost:3001
119118
const endpoints = isLocal
120119
? [
121120
`http://localhost:3001/api/video-info?url=${encodeURIComponent(url)}`,

‎vercel.json‎

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)