sanch1tx commited on
Commit
837764d
·
verified ·
1 Parent(s): 91cee74

Update app/api/movies4u/route.ts

Browse files
Files changed (1) hide show
  1. app/api/movies4u/route.ts +560 -551
app/api/movies4u/route.ts CHANGED
@@ -1,551 +1,560 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { load } from 'cheerio';
3
- import { getMovies4UUrl } from '@/lib/utils/providers';
4
-
5
- // Interface for actual download link
6
- interface ActualDownloadLink {
7
- url: string;
8
- label: string;
9
- }
10
-
11
- // Interface for download link structure
12
- interface DownloadLink {
13
- type: 'episodes' | 'batch';
14
- url: string;
15
- label: string;
16
- batchSize?: string;
17
- extractedLinks?: ActualDownloadLink[];
18
- }
19
-
20
- // Interface for quality options
21
- interface QualityOption {
22
- quality: string;
23
- format: string;
24
- size: string;
25
- language: string;
26
- links: DownloadLink[];
27
- }
28
-
29
- // Interface for season structure
30
- interface Season {
31
- name: string;
32
- qualityOptions: QualityOption[];
33
- }
34
-
35
- // Interface for the complete content data
36
- interface ContentData {
37
- title: string;
38
- url: string;
39
- posterUrl?: string;
40
- seasons: Season[];
41
- }
42
-
43
- interface Movies4UItem {
44
- id: string;
45
- title: string;
46
- url: string;
47
- image: string;
48
- videoLabel: string;
49
- hasVideoIcon: boolean;
50
- altText: string;
51
- }
52
-
53
- interface Movies4UResponse {
54
- success: boolean;
55
- data?: {
56
- items: Movies4UItem[];
57
- pagination?: {
58
- currentPage: number;
59
- hasNextPage: boolean;
60
- };
61
- };
62
- error?: string;
63
- message?: string;
64
- remainingRequests?: number;
65
- }
66
-
67
- interface StreamResponse {
68
- success: boolean;
69
- data?: ContentData;
70
- error?: string;
71
- message?: string;
72
- remainingRequests?: number;
73
- seasonCount?: number;
74
- qualityOptionCount?: number;
75
- linkCount?: number;
76
- }
77
-
78
- // Function to normalize image URLs
79
- async function await normalizeImageUrl(url: string | undefined): Promise<string | undefined> {
80
- if (!url) return undefined;
81
- if (url.startsWith('//')) return 'https:' + url;
82
- if (url.startsWith('/')) {
83
- const baseUrl = await getMovies4UUrl();
84
- return baseUrl + url;
85
- }
86
- return url;
87
- }
88
-
89
- // Function to extract ID from URL
90
- function extractIdFromUrl(url: string): string {
91
- try {
92
- const urlParts = url.split('/');
93
- const relevantPart = urlParts.find(part =>
94
- part.includes('-') &&
95
- (part.includes('season') || part.length > 10)
96
- );
97
- return relevantPart ? relevantPart.replace(/[^a-zA-Z0-9-]/g, '') : '';
98
- } catch {
99
- return '';
100
- }
101
- }
102
-
103
- // Main function to scrape Movies4U data
104
- async function scrapeMovies4UData(page: number = 1, searchQuery?: string): Promise<Movies4UItem[]> {
105
- try {
106
- const baseUrl = await getMovies4UUrl();
107
- let url = baseUrl;
108
-
109
- if (searchQuery) {
110
- url += `?s=${encodeURIComponent(searchQuery)}`;
111
- } else if (page > 1) {
112
- url += `page/${page}/`;
113
- }
114
-
115
- console.log(`Fetching Movies4U content from: ${url}`);
116
-
117
- const response = await fetch(url, {
118
- cache: 'no-cache',
119
- headers: {
120
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
121
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
122
- 'Accept-Language': 'en-US,en;q=0.5',
123
- 'Referer': '${baseUrl}/',
124
- },
125
- next: { revalidate: 0 }
126
- });
127
-
128
- if (!response.ok) {
129
- throw new Error(`Failed to fetch content: ${response.status}`);
130
- }
131
-
132
- const html = await response.text();
133
- const $ = load(html);
134
- const items: Movies4UItem[] = [];
135
-
136
- console.log(`Received HTML content (length: ${html.length})`);
137
-
138
- // Extract figure elements with post-thumbnail
139
- $('figure').each((_, element) => {
140
- const $figure = $(element);
141
- const $postThumbnail = $figure.find('.post-thumbnail');
142
-
143
- if ($postThumbnail.length > 0) {
144
- try {
145
- // Extract URL from the anchor tag
146
- const url = $postThumbnail.attr('href') || '';
147
-
148
- // Extract image information
149
- const $img = $postThumbnail.find('img');
150
- let imageUrl = $img.attr('src') || $img.attr('data-src') || '';
151
- imageUrl = await normalizeImageUrl(imageUrl);
152
-
153
- // Extract alt text (title)
154
- const altText = $img.attr('alt') || '';
155
-
156
- // Extract video label
157
- const videoLabel = $postThumbnail.find('.video-label').text().trim() || '';
158
-
159
- // Check if video icon exists
160
- const hasVideoIcon = $postThumbnail.find('.video-icon').length > 0;
161
-
162
- // Generate ID from URL
163
- const id = extractIdFromUrl(url) || `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
164
-
165
- // Only add if we have essential information
166
- if (url && imageUrl && altText) {
167
- items.push({
168
- id,
169
- title: altText,
170
- url,
171
- image: imageUrl,
172
- videoLabel,
173
- hasVideoIcon,
174
- altText
175
- });
176
- }
177
- } catch (itemError) {
178
- console.error('Error parsing figure item:', itemError);
179
- }
180
- }
181
- });
182
-
183
- // If no items found with figure selector, try alternative selectors
184
- if (items.length === 0) {
185
- console.log('No items found with figure selector, trying alternative patterns...');
186
-
187
- // Try article or post selectors as fallback
188
- $('article, .post-item, .item').each((_, element) => {
189
- const $element = $(element);
190
- const $link = $element.find('a').first();
191
- const $img = $element.find('img').first();
192
-
193
- if ($link.length > 0 && $img.length > 0) {
194
- const url = $link.attr('href') || '';
195
- let imageUrl = $img.attr('src') || $img.attr('data-src') || '';
196
- imageUrl = await normalizeImageUrl(imageUrl);
197
- const altText = $img.attr('alt') || $link.text().trim() || '';
198
-
199
- const videoLabel = $element.find('.video-label, .quality, .format').text().trim() || '';
200
- const hasVideoIcon = $element.find('.video-icon, .play-icon').length > 0;
201
- const id = extractIdFromUrl(url) || `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
202
-
203
- if (url && imageUrl && altText) {
204
- items.push({
205
- id,
206
- title: altText,
207
- url,
208
- image: imageUrl,
209
- videoLabel,
210
- hasVideoIcon,
211
- altText
212
- });
213
- }
214
- }
215
- });
216
- }
217
-
218
- console.log(`Successfully parsed ${items.length} items from Movies4U`);
219
- return items;
220
-
221
- } catch (error) {
222
- console.error('Error scraping Movies4U data:', error);
223
- throw error;
224
- }
225
- }
226
-
227
- // Function to scrape download links from a specific movie/show page
228
- async function scrapeDownloadLinks(url: string): Promise<ContentData> {
229
- try {
230
- console.log(`Fetching download links from: ${url}`);
231
-
232
- const response = await fetch(url, {
233
- cache: 'no-cache',
234
- headers: {
235
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
236
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
237
- 'Accept-Language': 'en-US,en;q=0.5',
238
- 'Referer': '${baseUrl}/',
239
- },
240
- next: { revalidate: 0 }
241
- });
242
-
243
- if (!response.ok) {
244
- throw new Error(`Failed to fetch page: ${response.status}`);
245
- }
246
-
247
- const html = await response.text();
248
- const $ = load(html);
249
-
250
- // Extract page title
251
- const pageTitle = $('h1.single-title').text().trim() ||
252
- $('h1').first().text().trim() ||
253
- $('title').text().trim().replace(' - Movies4u', '');
254
-
255
- // Extract poster image
256
- let posterUrl = '';
257
- const posterImg = $('.post-thumbnail img, .featured-image img').first();
258
- if (posterImg.length) {
259
- posterUrl = posterImg.attr('src') || '';
260
- console.log(`Found poster image: ${posterUrl}`);
261
- }
262
-
263
- // Extract all seasons and download links
264
- const seasons: Season[] = [];
265
- let currentSeason: Season | null = null;
266
-
267
- // Find the download links div
268
- $('.download-links-div').each((_, downloadSection) => {
269
- // Process each h4 header (represents a quality option for a season)
270
- $(downloadSection).find('h4').each((_, headerElement) => {
271
- const headerText = $(headerElement).text().trim();
272
-
273
- console.log('Processing header:', headerText);
274
-
275
- // Skip horizontal rules
276
- if (headerText === '<hr>') return;
277
-
278
- // Extract season name
279
- const seasonMatch = headerText.match(/Season\s+(\d+)/i);
280
-
281
- if (seasonMatch) {
282
- const seasonName = `Season ${seasonMatch[1]}`;
283
-
284
- // Check if we need to create a new season object
285
- if (!currentSeason || currentSeason.name !== seasonName) {
286
- currentSeason = {
287
- name: seasonName,
288
- qualityOptions: []
289
- };
290
- seasons.push(currentSeason);
291
- console.log(`Found season: ${seasonName}`);
292
- }
293
-
294
- // Extract language info (text in curly braces)
295
- const languageMatch = headerText.match(/\{([^{}]+)\}/);
296
- const language = languageMatch ? languageMatch[0] : '';
297
-
298
- // Extract quality
299
- let quality = '';
300
- if (headerText.includes('480p')) quality = '480p';
301
- else if (headerText.includes('720p')) quality = '720p';
302
- else if (headerText.includes('1080p')) quality = '1080p';
303
- else if (headerText.includes('2160p') || headerText.includes('4K')) quality = '2160p 4K';
304
-
305
- // Extract format and codec
306
- let format = '';
307
- if (headerText.includes('WEB-DL')) format = 'WEB-DL';
308
- else if (headerText.includes('WEBRip')) format = 'WEBRip';
309
- else if (headerText.includes('BluRay')) format = 'BluRay';
310
-
311
- if (headerText.includes('x264')) format += ' x264';
312
- else if (headerText.includes('HEVC') || headerText.includes('x265')) format += ' HEVC x265';
313
- if (headerText.includes('10bit')) format += ' 10bit';
314
-
315
- // Extract size per episode
316
- let size = '';
317
- const sizeMatch = headerText.match(/\[([^[\]]+)(?:\/E)?\]/);
318
- if (sizeMatch) {
319
- size = sizeMatch[1];
320
- if (!size.includes('/E') && !headerText.includes('BATCH')) {
321
- size += '/E';
322
- }
323
- }
324
-
325
- // Create a new quality option
326
- const qualityOption: QualityOption = {
327
- quality,
328
- format,
329
- size,
330
- language,
331
- links: []
332
- };
333
-
334
- // Find the download buttons div that follows this header
335
- const downloadsDiv = $(headerElement).next('.downloads-btns-div');
336
- if (downloadsDiv.length) {
337
- // Extract episode links only (no batch links)
338
- downloadsDiv.find('a.btn:not(.btn-zip)').each((_, link) => {
339
- const linkUrl = $(link).attr('href') || '';
340
- const linkText = $(link).text().trim().replace(/\s+/g, ' ');
341
-
342
- // Skip if link text contains BATCH or ZIP
343
- if (linkUrl && !linkText.toLowerCase().includes('batch') && !linkText.toLowerCase().includes('zip')) {
344
- qualityOption.links.push({
345
- type: 'episodes',
346
- url: linkUrl,
347
- label: linkText || 'Download Links'
348
- });
349
- console.log(` - Found episode link: ${linkUrl}`);
350
- }
351
- });
352
- }
353
-
354
- // Add this quality option to the current season
355
- if (currentSeason) {
356
- currentSeason.qualityOptions.push(qualityOption);
357
- }
358
- } else {
359
- // Special case for standalone quality options (not tied to a numbered season)
360
- const specialTitle = headerText.replace(/<[^>]*>/g, '').trim();
361
-
362
- if (specialTitle && specialTitle.length > 0) {
363
- // Create a special "season" for this standalone quality
364
- const specialSeason: Season = {
365
- name: "Special",
366
- qualityOptions: []
367
- };
368
-
369
- // Extract language info (text in curly braces)
370
- const languageMatch = headerText.match(/\{([^{}]+)\}/);
371
- const language = languageMatch ? languageMatch[0] : '';
372
-
373
- // Extract quality
374
- let quality = '';
375
- if (headerText.includes('480p')) quality = '480p';
376
- else if (headerText.includes('720p')) quality = '720p';
377
- else if (headerText.includes('1080p')) quality = '1080p';
378
- else if (headerText.includes('2160p') || headerText.includes('4K')) quality = '2160p 4K';
379
-
380
- // Extract format and codec
381
- let format = '';
382
- if (headerText.includes('WEB-DL')) format = 'WEB-DL';
383
- else if (headerText.includes('WEBRip')) format = 'WEBRip';
384
- else if (headerText.includes('BluRay')) format = 'BluRay';
385
-
386
- if (headerText.includes('x264')) format += ' x264';
387
- else if (headerText.includes('HEVC') || headerText.includes('x265')) format += ' HEVC x265';
388
- if (headerText.includes('10bit')) format += ' 10bit';
389
-
390
- // Extract size
391
- let size = '';
392
- const sizeMatch = headerText.match(/\[([^[\]]+)\]/);
393
- if (sizeMatch) size = sizeMatch[1];
394
-
395
- // Create quality option
396
- const qualityOption: QualityOption = {
397
- quality,
398
- format,
399
- size,
400
- language,
401
- links: []
402
- };
403
-
404
- // Find the download buttons div that follows this header
405
- const downloadsDiv = $(headerElement).next('.downloads-btns-div');
406
- if (downloadsDiv.length) {
407
- // Extract download links
408
- downloadsDiv.find('a.btn').each((_, link) => {
409
- const linkUrl = $(link).attr('href') || '';
410
- const linkText = $(link).text().trim().replace(/\s+/g, ' ');
411
-
412
- if (linkUrl) {
413
- qualityOption.links.push({
414
- type: 'episodes',
415
- url: linkUrl,
416
- label: linkText || 'Download Links'
417
- });
418
- console.log(` - Found special link: ${linkUrl}`);
419
- }
420
- });
421
- }
422
-
423
- // Add this quality option to the special season
424
- specialSeason.qualityOptions.push(qualityOption);
425
-
426
- // Add the special season to our list
427
- if (specialSeason.qualityOptions.length > 0 &&
428
- specialSeason.qualityOptions[0].links.length > 0) {
429
- seasons.push(specialSeason);
430
- console.log(`Found special quality option: ${specialTitle}`);
431
- }
432
- }
433
- }
434
- });
435
- });
436
-
437
- // Create the final content data
438
- const contentData: ContentData = {
439
- title: pageTitle,
440
- url,
441
- posterUrl: posterUrl || undefined,
442
- seasons
443
- };
444
-
445
- console.log(`Successfully extracted ${seasons.length} seasons with download links`);
446
- return contentData;
447
-
448
- } catch (error) {
449
- console.error('Error scraping download links:', error);
450
- throw error;
451
- }
452
- }
453
-
454
- export async function GET(request: NextRequest): Promise<NextResponse<Movies4UResponse | StreamResponse>> {
455
- try {
456
- // Validate API key
457
-
458
-
459
- const { searchParams } = new URL(request.url);
460
- const streamId = searchParams.get('stream');
461
-
462
- // Handle stream request for download links
463
- if (streamId) {
464
- try {
465
- const contentData = await scrapeDownloadLinks(streamId);
466
-
467
- if (contentData.seasons.length === 0) {
468
- return NextResponse.json<StreamResponse>({
469
- success: false,
470
- error: 'No download links found',
471
- message: `No download links found for: ${streamId}`,
472
- });
473
- }
474
-
475
- return NextResponse.json<StreamResponse>({
476
- success: true,
477
- data: contentData,
478
- seasonCount: contentData.seasons.length,
479
- qualityOptionCount: contentData.seasons.reduce((total, season) => total + season.qualityOptions.length, 0),
480
- linkCount: contentData.seasons.reduce((total, season) => {
481
- return total + season.qualityOptions.reduce((subtotal, option) => {
482
- return subtotal + option.links.length;
483
- }, 0);
484
- }, 0)
485
- });
486
-
487
- } catch (error) {
488
- console.error('Stream request error:', error);
489
- return NextResponse.json<StreamResponse>(
490
- {
491
- success: false,
492
- error: 'Failed to fetch download links',
493
- message: error instanceof Error ? error.message : 'Unknown error occurred'
494
- },
495
- { status: 500 }
496
- );
497
- }
498
- }
499
-
500
- // Handle regular listing request
501
- const page = parseInt(searchParams.get('page') || '1');
502
- const searchQuery = searchParams.get('search');
503
-
504
- if (page < 1) {
505
- return NextResponse.json<Movies4UResponse>(
506
- {
507
- success: false,
508
- error: 'Page number must be 1 or greater'
509
- },
510
- { status: 400 }
511
- );
512
- }
513
-
514
- console.log('Processing Movies4U request:', { page, searchQuery });
515
-
516
- const items = await scrapeMovies4UData(page, searchQuery || undefined);
517
-
518
- if (!items || items.length === 0) {
519
- return NextResponse.json<Movies4UResponse>({
520
- success: false,
521
- error: 'No items found',
522
- message: searchQuery
523
- ? `No items found for search query: "${searchQuery}"`
524
- : `No items found on page ${page}`,
525
- });
526
- }
527
-
528
- return NextResponse.json<Movies4UResponse>({
529
- success: true,
530
- data: {
531
- items,
532
- pagination: {
533
- currentPage: page,
534
- hasNextPage: items.length >= 10
535
- }
536
- },
537
- });
538
-
539
- } catch (error: unknown) {
540
- console.error('Movies4U API error:', error);
541
-
542
- return NextResponse.json<Movies4UResponse>(
543
- {
544
- success: false,
545
- error: 'Failed to fetch content from Movies4U',
546
- message: error instanceof Error ? error.message : 'Unknown error occurred'
547
- },
548
- { status: 500 }
549
- );
550
- }
551
- }
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { load } from 'cheerio';
3
+ import { getMovies4UUrl } from '@/lib/utils/providers';
4
+
5
+ // Interface for actual download link
6
+ interface ActualDownloadLink {
7
+ url: string;
8
+ label: string;
9
+ }
10
+
11
+ // Interface for download link structure
12
+ interface DownloadLink {
13
+ type: 'episodes' | 'batch';
14
+ url: string;
15
+ label: string;
16
+ batchSize?: string;
17
+ extractedLinks?: ActualDownloadLink[];
18
+ }
19
+
20
+ // Interface for quality options
21
+ interface QualityOption {
22
+ quality: string;
23
+ format: string;
24
+ size: string;
25
+ language: string;
26
+ links: DownloadLink[];
27
+ }
28
+
29
+ // Interface for season structure
30
+ interface Season {
31
+ name: string;
32
+ qualityOptions: QualityOption[];
33
+ }
34
+
35
+ // Interface for the complete content data
36
+ interface ContentData {
37
+ title: string;
38
+ url: string;
39
+ posterUrl?: string;
40
+ seasons: Season[];
41
+ }
42
+
43
+ interface Movies4UItem {
44
+ id: string;
45
+ title: string;
46
+ url: string;
47
+ image: string;
48
+ videoLabel: string;
49
+ hasVideoIcon: boolean;
50
+ altText: string;
51
+ }
52
+
53
+ interface Movies4UResponse {
54
+ success: boolean;
55
+ data?: {
56
+ items: Movies4UItem[];
57
+ pagination?: {
58
+ currentPage: number;
59
+ hasNextPage: boolean;
60
+ };
61
+ };
62
+ error?: string;
63
+ message?: string;
64
+ remainingRequests?: number;
65
+ }
66
+
67
+ interface StreamResponse {
68
+ success: boolean;
69
+ data?: ContentData;
70
+ error?: string;
71
+ message?: string;
72
+ remainingRequests?: number;
73
+ seasonCount?: number;
74
+ qualityOptionCount?: number;
75
+ linkCount?: number;
76
+ }
77
+
78
+ // Function to normalize image URLs
79
+ async function normalizeImageUrl(url: string | undefined): Promise<string | undefined> {
80
+ if (!url) return undefined;
81
+ if (url.startsWith('//')) return 'https:' + url;
82
+ if (url.startsWith('/')) {
83
+ const baseUrl = await getMovies4UUrl();
84
+ return baseUrl + url;
85
+ }
86
+ return url;
87
+ }
88
+
89
+ // Function to extract ID from URL
90
+ function extractIdFromUrl(url: string): string {
91
+ try {
92
+ const urlParts = url.split('/');
93
+ const relevantPart = urlParts.find(part =>
94
+ part.includes('-') &&
95
+ (part.includes('season') || part.length > 10)
96
+ );
97
+ return relevantPart ? relevantPart.replace(/[^a-zA-Z0-9-]/g, '') : '';
98
+ } catch {
99
+ return '';
100
+ }
101
+ }
102
+
103
+ // Main function to scrape Movies4U data
104
+ async function scrapeMovies4UData(page: number = 1, searchQuery?: string): Promise<Movies4UItem[]> {
105
+ try {
106
+ const baseUrl = await getMovies4UUrl();
107
+ let url = baseUrl;
108
+
109
+ if (searchQuery) {
110
+ url += `?s=${encodeURIComponent(searchQuery)}`;
111
+ } else if (page > 1) {
112
+ url += `page/${page}/`;
113
+ }
114
+
115
+ console.log(`Fetching Movies4U content from: ${url}`);
116
+
117
+ const response = await fetch(url, {
118
+ cache: 'no-cache',
119
+ headers: {
120
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
121
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
122
+ 'Accept-Language': 'en-US,en;q=0.5',
123
+ 'Referer': `${baseUrl}/`,
124
+ },
125
+ next: { revalidate: 0 }
126
+ });
127
+
128
+ if (!response.ok) {
129
+ throw new Error(`Failed to fetch content: ${response.status}`);
130
+ }
131
+
132
+ const html = await response.text();
133
+ const $ = load(html);
134
+ const items: Movies4UItem[] = [];
135
+
136
+ console.log(`Received HTML content (length: ${html.length})`);
137
+
138
+ // Extract figure elements with post-thumbnail
139
+ $('figure').each((_, element) => {
140
+ const $figure = $(element);
141
+ const $postThumbnail = $figure.find('.post-thumbnail');
142
+
143
+ if ($postThumbnail.length > 0) {
144
+ try {
145
+ // Extract URL from the anchor tag
146
+ const url = $postThumbnail.attr('href') || '';
147
+
148
+ // Extract image information
149
+ const $img = $postThumbnail.find('img');
150
+ let imageUrl = $img.attr('src') || $img.attr('data-src') || '';
151
+
152
+ // Extract alt text (title)
153
+ const altText = $img.attr('alt') || '';
154
+
155
+ // Extract video label
156
+ const videoLabel = $postThumbnail.find('.video-label').text().trim() || '';
157
+
158
+ // Check if video icon exists
159
+ const hasVideoIcon = $postThumbnail.find('.video-icon').length > 0;
160
+
161
+ // Generate ID from URL
162
+ const id = extractIdFromUrl(url) || `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
163
+
164
+ // Only add if we have essential information
165
+ if (url && imageUrl && altText) {
166
+ // We need to resolve the promise for normalizeImageUrl here or handle it differently
167
+ // Since we can't await inside forEach, we'll collect raw data first or use a normal loop
168
+ // For now, let's just push a promise or handle async properly.
169
+ // Actually, best practice with cheerio loops and async is `for...of` or `Promise.all`
170
+ // But since this structure is provided, let's fix the logic
171
+ items.push({
172
+ id,
173
+ title: altText,
174
+ url,
175
+ image: imageUrl, // Temporarily store raw, we will normalize later or assume it's absolute for now if sync is needed
176
+ videoLabel,
177
+ hasVideoIcon,
178
+ altText
179
+ });
180
+ }
181
+ } catch (itemError) {
182
+ console.error('Error parsing figure item:', itemError);
183
+ }
184
+ }
185
+ });
186
+
187
+ // If no items found with figure selector, try alternative selectors
188
+ if (items.length === 0) {
189
+ console.log('No items found with figure selector, trying alternative patterns...');
190
+
191
+ // Try article or post selectors as fallback
192
+ $('article, .post-item, .item').each((_, element) => {
193
+ const $element = $(element);
194
+ const $link = $element.find('a').first();
195
+ const $img = $element.find('img').first();
196
+
197
+ if ($link.length > 0 && $img.length > 0) {
198
+ const url = $link.attr('href') || '';
199
+ let imageUrl = $img.attr('src') || $img.attr('data-src') || '';
200
+ const altText = $img.attr('alt') || $link.text().trim() || '';
201
+
202
+ const videoLabel = $element.find('.video-label, .quality, .format').text().trim() || '';
203
+ const hasVideoIcon = $element.find('.video-icon, .play-icon').length > 0;
204
+ const id = extractIdFromUrl(url) || `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
205
+
206
+ if (url && imageUrl && altText) {
207
+ items.push({
208
+ id,
209
+ title: altText,
210
+ url,
211
+ image: imageUrl,
212
+ videoLabel,
213
+ hasVideoIcon,
214
+ altText
215
+ });
216
+ }
217
+ }
218
+ });
219
+ }
220
+
221
+ // Normalize images asynchronously after collection
222
+ for (const item of items) {
223
+ item.image = (await normalizeImageUrl(item.image)) || item.image;
224
+ }
225
+
226
+ console.log(`Successfully parsed ${items.length} items from Movies4U`);
227
+ return items;
228
+
229
+ } catch (error) {
230
+ console.error('Error scraping Movies4U data:', error);
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ // Function to scrape download links from a specific movie/show page
236
+ async function scrapeDownloadLinks(url: string): Promise<ContentData> {
237
+ try {
238
+ const baseUrl = await getMovies4UUrl();
239
+ console.log(`Fetching download links from: ${url}`);
240
+
241
+ const response = await fetch(url, {
242
+ cache: 'no-cache',
243
+ headers: {
244
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
245
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
246
+ 'Accept-Language': 'en-US,en;q=0.5',
247
+ 'Referer': `${baseUrl}/`,
248
+ },
249
+ next: { revalidate: 0 }
250
+ });
251
+
252
+ if (!response.ok) {
253
+ throw new Error(`Failed to fetch page: ${response.status}`);
254
+ }
255
+
256
+ const html = await response.text();
257
+ const $ = load(html);
258
+
259
+ // Extract page title
260
+ const pageTitle = $('h1.single-title').text().trim() ||
261
+ $('h1').first().text().trim() ||
262
+ $('title').text().trim().replace(' - Movies4u', '');
263
+
264
+ // Extract poster image
265
+ let posterUrl = '';
266
+ const posterImg = $('.post-thumbnail img, .featured-image img').first();
267
+ if (posterImg.length) {
268
+ posterUrl = posterImg.attr('src') || '';
269
+ console.log(`Found poster image: ${posterUrl}`);
270
+ }
271
+
272
+ // Extract all seasons and download links
273
+ const seasons: Season[] = [];
274
+ let currentSeason: Season | null = null;
275
+
276
+ // Find the download links div
277
+ $('.download-links-div').each((_, downloadSection) => {
278
+ // Process each h4 header (represents a quality option for a season)
279
+ $(downloadSection).find('h4').each((_, headerElement) => {
280
+ const headerText = $(headerElement).text().trim();
281
+
282
+ console.log('Processing header:', headerText);
283
+
284
+ // Skip horizontal rules
285
+ if (headerText === '<hr>') return;
286
+
287
+ // Extract season name
288
+ const seasonMatch = headerText.match(/Season\s+(\d+)/i);
289
+
290
+ if (seasonMatch) {
291
+ const seasonName = `Season ${seasonMatch[1]}`;
292
+
293
+ // Check if we need to create a new season object
294
+ if (!currentSeason || currentSeason.name !== seasonName) {
295
+ currentSeason = {
296
+ name: seasonName,
297
+ qualityOptions: []
298
+ };
299
+ seasons.push(currentSeason);
300
+ console.log(`Found season: ${seasonName}`);
301
+ }
302
+
303
+ // Extract language info (text in curly braces)
304
+ const languageMatch = headerText.match(/\{([^{}]+)\}/);
305
+ const language = languageMatch ? languageMatch[0] : '';
306
+
307
+ // Extract quality
308
+ let quality = '';
309
+ if (headerText.includes('480p')) quality = '480p';
310
+ else if (headerText.includes('720p')) quality = '720p';
311
+ else if (headerText.includes('1080p')) quality = '1080p';
312
+ else if (headerText.includes('2160p') || headerText.includes('4K')) quality = '2160p 4K';
313
+
314
+ // Extract format and codec
315
+ let format = '';
316
+ if (headerText.includes('WEB-DL')) format = 'WEB-DL';
317
+ else if (headerText.includes('WEBRip')) format = 'WEBRip';
318
+ else if (headerText.includes('BluRay')) format = 'BluRay';
319
+
320
+ if (headerText.includes('x264')) format += ' x264';
321
+ else if (headerText.includes('HEVC') || headerText.includes('x265')) format += ' HEVC x265';
322
+ if (headerText.includes('10bit')) format += ' 10bit';
323
+
324
+ // Extract size per episode
325
+ let size = '';
326
+ const sizeMatch = headerText.match(/\[([^[\]]+)(?:\/E)?\]/);
327
+ if (sizeMatch) {
328
+ size = sizeMatch[1];
329
+ if (!size.includes('/E') && !headerText.includes('BATCH')) {
330
+ size += '/E';
331
+ }
332
+ }
333
+
334
+ // Create a new quality option
335
+ const qualityOption: QualityOption = {
336
+ quality,
337
+ format,
338
+ size,
339
+ language,
340
+ links: []
341
+ };
342
+
343
+ // Find the download buttons div that follows this header
344
+ const downloadsDiv = $(headerElement).next('.downloads-btns-div');
345
+ if (downloadsDiv.length) {
346
+ // Extract episode links only (no batch links)
347
+ downloadsDiv.find('a.btn:not(.btn-zip)').each((_, link) => {
348
+ const linkUrl = $(link).attr('href') || '';
349
+ const linkText = $(link).text().trim().replace(/\s+/g, ' ');
350
+
351
+ // Skip if link text contains BATCH or ZIP
352
+ if (linkUrl && !linkText.toLowerCase().includes('batch') && !linkText.toLowerCase().includes('zip')) {
353
+ qualityOption.links.push({
354
+ type: 'episodes',
355
+ url: linkUrl,
356
+ label: linkText || 'Download Links'
357
+ });
358
+ console.log(` - Found episode link: ${linkUrl}`);
359
+ }
360
+ });
361
+ }
362
+
363
+ // Add this quality option to the current season
364
+ if (currentSeason) {
365
+ currentSeason.qualityOptions.push(qualityOption);
366
+ }
367
+ } else {
368
+ // Special case for standalone quality options (not tied to a numbered season)
369
+ const specialTitle = headerText.replace(/<[^>]*>/g, '').trim();
370
+
371
+ if (specialTitle && specialTitle.length > 0) {
372
+ // Create a special "season" for this standalone quality
373
+ const specialSeason: Season = {
374
+ name: "Special",
375
+ qualityOptions: []
376
+ };
377
+
378
+ // Extract language info (text in curly braces)
379
+ const languageMatch = headerText.match(/\{([^{}]+)\}/);
380
+ const language = languageMatch ? languageMatch[0] : '';
381
+
382
+ // Extract quality
383
+ let quality = '';
384
+ if (headerText.includes('480p')) quality = '480p';
385
+ else if (headerText.includes('720p')) quality = '720p';
386
+ else if (headerText.includes('1080p')) quality = '1080p';
387
+ else if (headerText.includes('2160p') || headerText.includes('4K')) quality = '2160p 4K';
388
+
389
+ // Extract format and codec
390
+ let format = '';
391
+ if (headerText.includes('WEB-DL')) format = 'WEB-DL';
392
+ else if (headerText.includes('WEBRip')) format = 'WEBRip';
393
+ else if (headerText.includes('BluRay')) format = 'BluRay';
394
+
395
+ if (headerText.includes('x264')) format += ' x264';
396
+ else if (headerText.includes('HEVC') || headerText.includes('x265')) format += ' HEVC x265';
397
+ if (headerText.includes('10bit')) format += ' 10bit';
398
+
399
+ // Extract size
400
+ let size = '';
401
+ const sizeMatch = headerText.match(/\[([^[\]]+)\]/);
402
+ if (sizeMatch) size = sizeMatch[1];
403
+
404
+ // Create quality option
405
+ const qualityOption: QualityOption = {
406
+ quality,
407
+ format,
408
+ size,
409
+ language,
410
+ links: []
411
+ };
412
+
413
+ // Find the download buttons div that follows this header
414
+ const downloadsDiv = $(headerElement).next('.downloads-btns-div');
415
+ if (downloadsDiv.length) {
416
+ // Extract download links
417
+ downloadsDiv.find('a.btn').each((_, link) => {
418
+ const linkUrl = $(link).attr('href') || '';
419
+ const linkText = $(link).text().trim().replace(/\s+/g, ' ');
420
+
421
+ if (linkUrl) {
422
+ qualityOption.links.push({
423
+ type: 'episodes',
424
+ url: linkUrl,
425
+ label: linkText || 'Download Links'
426
+ });
427
+ console.log(` - Found special link: ${linkUrl}`);
428
+ }
429
+ });
430
+ }
431
+
432
+ // Add this quality option to the special season
433
+ specialSeason.qualityOptions.push(qualityOption);
434
+
435
+ // Add the special season to our list
436
+ if (specialSeason.qualityOptions.length > 0 &&
437
+ specialSeason.qualityOptions[0].links.length > 0) {
438
+ seasons.push(specialSeason);
439
+ console.log(`Found special quality option: ${specialTitle}`);
440
+ }
441
+ }
442
+ }
443
+ });
444
+ });
445
+
446
+ // Create the final content data
447
+ const contentData: ContentData = {
448
+ title: pageTitle,
449
+ url,
450
+ posterUrl: posterUrl || undefined,
451
+ seasons
452
+ };
453
+
454
+ console.log(`Successfully extracted ${seasons.length} seasons with download links`);
455
+ return contentData;
456
+
457
+ } catch (error) {
458
+ console.error('Error scraping download links:', error);
459
+ throw error;
460
+ }
461
+ }
462
+
463
+ export async function GET(request: NextRequest): Promise<NextResponse<Movies4UResponse | StreamResponse>> {
464
+ try {
465
+ // Validate API key
466
+
467
+
468
+ const { searchParams } = new URL(request.url);
469
+ const streamId = searchParams.get('stream');
470
+
471
+ // Handle stream request for download links
472
+ if (streamId) {
473
+ try {
474
+ const contentData = await scrapeDownloadLinks(streamId);
475
+
476
+ if (contentData.seasons.length === 0) {
477
+ return NextResponse.json<StreamResponse>({
478
+ success: false,
479
+ error: 'No download links found',
480
+ message: `No download links found for: ${streamId}`,
481
+ });
482
+ }
483
+
484
+ return NextResponse.json<StreamResponse>({
485
+ success: true,
486
+ data: contentData,
487
+ seasonCount: contentData.seasons.length,
488
+ qualityOptionCount: contentData.seasons.reduce((total, season) => total + season.qualityOptions.length, 0),
489
+ linkCount: contentData.seasons.reduce((total, season) => {
490
+ return total + season.qualityOptions.reduce((subtotal, option) => {
491
+ return subtotal + option.links.length;
492
+ }, 0);
493
+ }, 0)
494
+ });
495
+
496
+ } catch (error) {
497
+ console.error('Stream request error:', error);
498
+ return NextResponse.json<StreamResponse>(
499
+ {
500
+ success: false,
501
+ error: 'Failed to fetch download links',
502
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
503
+ },
504
+ { status: 500 }
505
+ );
506
+ }
507
+ }
508
+
509
+ // Handle regular listing request
510
+ const page = parseInt(searchParams.get('page') || '1');
511
+ const searchQuery = searchParams.get('search');
512
+
513
+ if (page < 1) {
514
+ return NextResponse.json<Movies4UResponse>(
515
+ {
516
+ success: false,
517
+ error: 'Page number must be 1 or greater'
518
+ },
519
+ { status: 400 }
520
+ );
521
+ }
522
+
523
+ console.log('Processing Movies4U request:', { page, searchQuery });
524
+
525
+ const items = await scrapeMovies4UData(page, searchQuery || undefined);
526
+
527
+ if (!items || items.length === 0) {
528
+ return NextResponse.json<Movies4UResponse>({
529
+ success: false,
530
+ error: 'No items found',
531
+ message: searchQuery
532
+ ? `No items found for search query: "${searchQuery}"`
533
+ : `No items found on page ${page}`,
534
+ });
535
+ }
536
+
537
+ return NextResponse.json<Movies4UResponse>({
538
+ success: true,
539
+ data: {
540
+ items,
541
+ pagination: {
542
+ currentPage: page,
543
+ hasNextPage: items.length >= 10
544
+ }
545
+ },
546
+ });
547
+
548
+ } catch (error: unknown) {
549
+ console.error('Movies4U API error:', error);
550
+
551
+ return NextResponse.json<Movies4UResponse>(
552
+ {
553
+ success: false,
554
+ error: 'Failed to fetch content from Movies4U',
555
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
556
+ },
557
+ { status: 500 }
558
+ );
559
+ }
560
+ }