mirror of
https://github.com/hotheadhacker/seedbox-lite.git
synced 2025-09-02 00:51:36 +03:00
Add Netflix-style UI for Torrent Page with loading and error states
- Implemented a new CSS file for styling the Torrent Page to resemble Netflix's design. - Created a loading spinner and error message display for better user experience. - Developed the main component to fetch and display torrent details, including IMDB data. - Added functionality for video playback and progress tracking. - Included sections for episodes/files and additional files with appropriate formatting. - Enhanced responsiveness for various screen sizes.
This commit is contained in:
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import Layout from './components/Layout';
|
||||
import HomePage from './components/HomePage';
|
||||
import TorrentPage from './components/TorrentPage';
|
||||
import TorrentPageNetflix from './components/TorrentPageNetflix';
|
||||
import RecentPage from './components/RecentPage';
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
import CacheManagementPage from './components/CacheManagementPage';
|
||||
@@ -14,7 +14,7 @@ function App() {
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route path="torrent/:torrentHash" element={<TorrentPage />} />
|
||||
<Route path="torrent/:torrentHash" element={<TorrentPageNetflix />} />
|
||||
<Route path="recent" element={<RecentPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="cache" element={<CacheManagementPage />} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Play, Download, Folder, FileText, Film, Clock, Wifi, RotateCcw } from 'lucide-react';
|
||||
import { ArrowLeft, Play, Download, Star, Calendar, Clock, Users, Award, Info, Share, Plus, ThumbsUp, Volume2 } from 'lucide-react';
|
||||
import VideoPlayer from './VideoPlayer';
|
||||
import { config } from '../config/environment';
|
||||
import progressService from '../services/progressService';
|
||||
@@ -15,6 +15,29 @@ const TorrentPage = () => {
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedVideo, setSelectedVideo] = useState(null);
|
||||
const [recentProgress, setRecentProgress] = useState({});
|
||||
const [imdbData, setImdbData] = useState(null);
|
||||
const [imdbLoading, setImdbLoading] = useState(true);
|
||||
|
||||
const fetchIMDBData = useCallback(async () => {
|
||||
try {
|
||||
setImdbLoading(true);
|
||||
const response = await fetch(`${config.apiBaseUrl}/api/torrents/${torrentHash}/imdb`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.imdb) {
|
||||
setImdbData(data.imdb);
|
||||
console.log('📺 IMDB data loaded:', data.imdb.Title);
|
||||
} else {
|
||||
console.log('❌ No IMDB data found');
|
||||
setImdbData(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching IMDB data:', err);
|
||||
setImdbData(null);
|
||||
} finally {
|
||||
setImdbLoading(false);
|
||||
}
|
||||
}, [torrentHash]);
|
||||
|
||||
const fetchTorrentDetails = useCallback(async () => {
|
||||
try {
|
||||
@@ -68,6 +91,7 @@ const TorrentPage = () => {
|
||||
useEffect(() => {
|
||||
if (torrentHash) {
|
||||
fetchTorrentDetails();
|
||||
fetchIMDBData(); // Add IMDB data fetching
|
||||
loadRecentProgress();
|
||||
|
||||
// Set up periodic updates for dynamic progress
|
||||
@@ -77,7 +101,7 @@ const TorrentPage = () => {
|
||||
|
||||
return () => clearInterval(progressInterval);
|
||||
}
|
||||
}, [torrentHash, fetchTorrentDetails, fetchTorrentProgress, loadRecentProgress]);
|
||||
}, [torrentHash, fetchTorrentDetails, fetchIMDBData, fetchTorrentProgress, loadRecentProgress]);
|
||||
|
||||
const handleVideoSelect = (file, index) => {
|
||||
setSelectedVideo({
|
||||
|
||||
626
client/src/components/TorrentPageNetflix.css
Normal file
626
client/src/components/TorrentPageNetflix.css
Normal file
@@ -0,0 +1,626 @@
|
||||
/* Netflix-style Torrent Page Styles */
|
||||
.netflix-page {
|
||||
min-height: 100vh;
|
||||
background: #141414;
|
||||
color: #ffffff;
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.netflix-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.netflix-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 3px solid #333;
|
||||
border-top: 3px solid #e50914;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Error State */
|
||||
.netflix-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.netflix-error h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 16px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.netflix-error p {
|
||||
font-size: 1.1rem;
|
||||
color: #b3b3b3;
|
||||
margin-bottom: 32px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.netflix-retry-btn {
|
||||
background: #e50914;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 32px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.netflix-retry-btn:hover {
|
||||
background: #f40612;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.netflix-hero {
|
||||
height: 80vh;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 60px;
|
||||
}
|
||||
|
||||
.netflix-hero-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.netflix-back-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: rgba(42, 42, 42, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.netflix-back-btn:hover {
|
||||
background: rgba(42, 42, 42, 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.netflix-title-section {
|
||||
flex: 1;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.netflix-title {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.netflix-meta {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.netflix-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.star-icon {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.netflix-votes {
|
||||
color: #b3b3b3;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.netflix-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 1rem;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.netflix-info-row span {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.netflix-info-row span:not(:last-child)::after {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.netflix-action-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.netflix-play-btn {
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
border: none;
|
||||
padding: 12px 32px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.netflix-play-btn:hover {
|
||||
background: #e6e6e6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.netflix-secondary-btn {
|
||||
background: rgba(109, 109, 110, 0.7);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.netflix-secondary-btn:hover {
|
||||
background: rgba(109, 109, 110, 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.netflix-description {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.5;
|
||||
color: #ffffff;
|
||||
max-width: 80%;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.netflix-poster {
|
||||
flex-shrink: 0;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.netflix-poster img {
|
||||
width: 300px;
|
||||
height: 450px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Content Section */
|
||||
.netflix-content {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
padding: 40px 60px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.netflix-main-content {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.netflix-sidebar {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.netflix-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.netflix-section h2 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Episodes Grid */
|
||||
.netflix-episodes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.netflix-episode {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: rgba(42, 42, 42, 0.6);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.netflix-episode:hover {
|
||||
background: rgba(42, 42, 42, 0.8);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.netflix-episode-thumbnail {
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 90px;
|
||||
background: linear-gradient(135deg, #333 0%, #555 100%);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.netflix-episode-play {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #000;
|
||||
border: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.netflix-episode-play:hover {
|
||||
background: #ffffff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.netflix-progress-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.netflix-progress-fill {
|
||||
height: 100%;
|
||||
background: #e50914;
|
||||
border-radius: 0 0 4px 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.netflix-episode-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.netflix-episode-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.netflix-episode-header h4 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.netflix-episode-duration {
|
||||
font-size: 0.9rem;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.netflix-episode-title {
|
||||
font-size: 0.95rem;
|
||||
color: #b3b3b3;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.netflix-episode-progress {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Files Grid */
|
||||
.netflix-files {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.netflix-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: rgba(42, 42, 42, 0.6);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.netflix-file:hover {
|
||||
background: rgba(42, 42, 42, 0.8);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.netflix-file-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(109, 109, 110, 0.3);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #b3b3b3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.netflix-file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.netflix-file-name {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
color: #ffffff;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.netflix-file-size {
|
||||
font-size: 0.85rem;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
/* Sidebar Info Cards */
|
||||
.netflix-info-card {
|
||||
background: rgba(42, 42, 42, 0.6);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.netflix-info-card h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.netflix-info-card p {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
color: #b3b3b3;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.netflix-ratings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.netflix-rating-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.netflix-rating-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.netflix-rating-item span:first-child {
|
||||
font-size: 0.9rem;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.netflix-rating-item span:last-child {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.netflix-torrent-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.netflix-stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.netflix-stat:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.netflix-stat span:first-child {
|
||||
font-size: 0.9rem;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.netflix-stat span:last-child {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.netflix-hero {
|
||||
height: 70vh;
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
.netflix-hero-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.netflix-title-section {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.netflix-poster {
|
||||
margin-left: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.netflix-poster img {
|
||||
width: 200px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.netflix-content {
|
||||
flex-direction: column;
|
||||
padding: 30px 40px;
|
||||
}
|
||||
|
||||
.netflix-sidebar {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.netflix-hero {
|
||||
height: 60vh;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.netflix-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.netflix-action-buttons {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.netflix-secondary-btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.netflix-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.netflix-episode {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.netflix-episode-thumbnail {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.netflix-episode-header {
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.netflix-files {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.netflix-poster img {
|
||||
width: 150px;
|
||||
height: 225px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.netflix-back-btn {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.netflix-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.netflix-description {
|
||||
font-size: 1rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.netflix-action-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.netflix-play-btn {
|
||||
padding: 10px 24px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.netflix-secondary-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
416
client/src/components/TorrentPageNetflix.jsx
Normal file
416
client/src/components/TorrentPageNetflix.jsx
Normal file
@@ -0,0 +1,416 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Play, Download, Star, Calendar, Clock, Users, Award, Info, Share, Plus, ThumbsUp, Volume2 } from 'lucide-react';
|
||||
import VideoPlayer from './VideoPlayer';
|
||||
import { config } from '../config/environment';
|
||||
import progressService from '../services/progressService';
|
||||
import './TorrentPageNetflix.css';
|
||||
|
||||
const TorrentPageNetflix = () => {
|
||||
const { torrentHash } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [torrent, setTorrent] = useState(null);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedVideo, setSelectedVideo] = useState(null);
|
||||
const [recentProgress, setRecentProgress] = useState({});
|
||||
const [imdbData, setImdbData] = useState(null);
|
||||
|
||||
const fetchIMDBData = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`${config.apiBaseUrl}/api/torrents/${torrentHash}/imdb`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.imdb) {
|
||||
setImdbData(data.imdb);
|
||||
console.log('📺 IMDB data loaded:', data.imdb.Title || data.imdb.title || 'No title found');
|
||||
console.log('📺 IMDB data object:', data.imdb);
|
||||
} else {
|
||||
console.log('❌ No IMDB data found');
|
||||
setImdbData(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching IMDB data:', err);
|
||||
setImdbData(null);
|
||||
}
|
||||
}, [torrentHash]);
|
||||
|
||||
const fetchTorrentDetails = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch(`${config.apiBaseUrl}/api/torrents/${torrentHash}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch torrent data`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
setTorrent(data.torrent);
|
||||
setFiles(data.files || []);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error fetching torrent details:', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [torrentHash]);
|
||||
|
||||
const fetchTorrentProgress = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`${config.apiBaseUrl}/api/torrents/${torrentHash}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTorrent(prev => ({ ...prev, ...data.torrent }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching progress:', err);
|
||||
}
|
||||
}, [torrentHash]);
|
||||
|
||||
useEffect(() => {
|
||||
if (torrentHash) {
|
||||
fetchTorrentDetails();
|
||||
fetchIMDBData();
|
||||
|
||||
// Load progress only once on mount
|
||||
const allProgress = progressService.getAllProgress();
|
||||
const torrentProgress = {};
|
||||
Object.values(allProgress).forEach(progress => {
|
||||
if (progress.torrentHash === torrentHash) {
|
||||
torrentProgress[progress.fileIndex] = progress;
|
||||
}
|
||||
});
|
||||
setRecentProgress(torrentProgress);
|
||||
|
||||
// Only run progress fetching if no video is selected
|
||||
const progressInterval = setInterval(() => {
|
||||
if (!selectedVideo) {
|
||||
fetchTorrentProgress();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(progressInterval);
|
||||
}
|
||||
}, [torrentHash, fetchTorrentDetails, fetchIMDBData, fetchTorrentProgress, selectedVideo]);
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) return '0 B';
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatSpeed = (bytesPerSecond) => {
|
||||
if (bytesPerSecond === 0) return '0 B/s';
|
||||
const k = 1024;
|
||||
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
|
||||
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
||||
return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="netflix-page">
|
||||
<div className="netflix-loading">
|
||||
<div className="netflix-spinner"></div>
|
||||
<p>Loading content...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="netflix-page">
|
||||
<div className="netflix-error">
|
||||
<h2>Something went wrong</h2>
|
||||
<p>{error}</p>
|
||||
<button
|
||||
className="netflix-retry-btn"
|
||||
onClick={() => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
fetchTorrentDetails();
|
||||
fetchIMDBData();
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedVideo) {
|
||||
const videoKey = `${torrentHash}-${selectedVideo.index}-${selectedVideo.name}`;
|
||||
// Capture initial time when video is first selected and don't change it
|
||||
const initialProgress = recentProgress[selectedVideo.index]?.currentTime || 0;
|
||||
|
||||
return (
|
||||
<VideoPlayer
|
||||
key={videoKey}
|
||||
src={`${config.apiBaseUrl}/api/torrents/${torrentHash}/files/${selectedVideo.index}/stream`}
|
||||
title={selectedVideo.name}
|
||||
onClose={() => setSelectedVideo(null)}
|
||||
onTimeUpdate={(time) => {
|
||||
// Update progress service without triggering re-renders
|
||||
progressService.updateProgress(torrentHash, selectedVideo.index, {
|
||||
currentTime: time,
|
||||
duration: selectedVideo.duration || 0
|
||||
});
|
||||
// Don't call setRecentProgress here to avoid re-rendering
|
||||
}}
|
||||
initialTime={initialProgress}
|
||||
torrentHash={torrentHash}
|
||||
fileIndex={selectedVideo.index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const mainVideoFile = files.find(file =>
|
||||
/\.(mp4|avi|mkv|mov|wmv|flv|webm|m4v)$/i.test(file.name)
|
||||
);
|
||||
|
||||
const videoFiles = files.filter(file =>
|
||||
/\.(mp4|avi|mkv|mov|wmv|flv|webm|m4v)$/i.test(file.name)
|
||||
);
|
||||
|
||||
const otherFiles = files.filter(file =>
|
||||
!/\.(mp4|avi|mkv|mov|wmv|flv|webm|m4v)$/i.test(file.name)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="netflix-page">
|
||||
{/* Hero Section */}
|
||||
<div className="netflix-hero" style={{
|
||||
backgroundImage: imdbData?.Poster && imdbData.Poster !== 'N/A'
|
||||
? `linear-gradient(to right, rgba(0,0,0,0.8) 50%, rgba(0,0,0,0.2) 100%), url(${imdbData.Poster})`
|
||||
: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d2d5f 100%)'
|
||||
}}>
|
||||
<div className="netflix-hero-content">
|
||||
<button
|
||||
className="netflix-back-btn"
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Back
|
||||
</button>
|
||||
|
||||
<div className="netflix-title-section">
|
||||
<h1 className="netflix-title">
|
||||
{imdbData?.Title || torrent?.name || 'Unknown Title'}
|
||||
</h1>
|
||||
|
||||
{imdbData && (
|
||||
<div className="netflix-meta">
|
||||
<div className="netflix-rating">
|
||||
<Star size={16} className="star-icon" />
|
||||
<span>{imdbData.imdbRating}/10</span>
|
||||
<span className="netflix-votes">({imdbData.imdbVotes} votes)</span>
|
||||
</div>
|
||||
|
||||
<div className="netflix-info-row">
|
||||
<span className="netflix-year">{imdbData.Year}</span>
|
||||
<span className="netflix-rated">{imdbData.Rated}</span>
|
||||
<span className="netflix-runtime">{imdbData.Runtime}</span>
|
||||
<span className="netflix-genre">{imdbData.Genre}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="netflix-action-buttons">
|
||||
{mainVideoFile && (
|
||||
<button
|
||||
className="netflix-play-btn"
|
||||
onClick={() => setSelectedVideo(mainVideoFile)}
|
||||
>
|
||||
<Play size={20} />
|
||||
Play
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button className="netflix-secondary-btn">
|
||||
<Plus size={20} />
|
||||
My List
|
||||
</button>
|
||||
|
||||
<button className="netflix-secondary-btn">
|
||||
<ThumbsUp size={20} />
|
||||
Rate
|
||||
</button>
|
||||
|
||||
<button className="netflix-secondary-btn">
|
||||
<Share size={20} />
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{imdbData?.Plot && (
|
||||
<p className="netflix-description">
|
||||
{imdbData.Plot}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{imdbData?.Poster && imdbData.Poster !== 'N/A' && (
|
||||
<div className="netflix-poster">
|
||||
<img src={imdbData.Poster} alt={imdbData.Title} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Details */}
|
||||
<div className="netflix-content">
|
||||
<div className="netflix-main-content">
|
||||
{/* Episodes/Files Section */}
|
||||
<div className="netflix-section">
|
||||
<h2>Episodes & Files</h2>
|
||||
<div className="netflix-episodes">
|
||||
{videoFiles.map((file, index) => {
|
||||
const progress = recentProgress[file.index];
|
||||
const progressPercentage = progress ? (progress.currentTime / progress.duration) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div key={file.index} className="netflix-episode">
|
||||
<div className="netflix-episode-thumbnail">
|
||||
<button
|
||||
className="netflix-episode-play"
|
||||
onClick={() => setSelectedVideo(file)}
|
||||
>
|
||||
<Play size={16} />
|
||||
</button>
|
||||
{progress && (
|
||||
<div className="netflix-progress-bar">
|
||||
<div
|
||||
className="netflix-progress-fill"
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="netflix-episode-info">
|
||||
<div className="netflix-episode-header">
|
||||
<h4>Episode {index + 1}</h4>
|
||||
<span className="netflix-episode-duration">
|
||||
{formatFileSize(file.length)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="netflix-episode-title">{file.name}</p>
|
||||
{progress && (
|
||||
<p className="netflix-episode-progress">
|
||||
{progressService.formatTime(progress.currentTime)} / {progressService.formatTime(progress.duration)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Files */}
|
||||
{otherFiles.length > 0 && (
|
||||
<div className="netflix-section">
|
||||
<h2>Additional Files</h2>
|
||||
<div className="netflix-files">
|
||||
{otherFiles.map(file => (
|
||||
<div key={file.index} className="netflix-file">
|
||||
<div className="netflix-file-icon">
|
||||
<Download size={16} />
|
||||
</div>
|
||||
<div className="netflix-file-info">
|
||||
<span className="netflix-file-name">{file.name}</span>
|
||||
<span className="netflix-file-size">{formatFileSize(file.length)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar Info */}
|
||||
<div className="netflix-sidebar">
|
||||
{imdbData && (
|
||||
<>
|
||||
<div className="netflix-info-card">
|
||||
<h3>Cast</h3>
|
||||
<p>{imdbData.Actors}</p>
|
||||
</div>
|
||||
|
||||
<div className="netflix-info-card">
|
||||
<h3>Director</h3>
|
||||
<p>{imdbData.Director}</p>
|
||||
</div>
|
||||
|
||||
<div className="netflix-info-card">
|
||||
<h3>Writer</h3>
|
||||
<p>{imdbData.Writer}</p>
|
||||
</div>
|
||||
|
||||
{imdbData.Awards && imdbData.Awards !== 'N/A' && (
|
||||
<div className="netflix-info-card">
|
||||
<h3>Awards</h3>
|
||||
<p>{imdbData.Awards}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="netflix-info-card">
|
||||
<h3>Ratings</h3>
|
||||
<div className="netflix-ratings">
|
||||
<div className="netflix-rating-item">
|
||||
<span>IMDB</span>
|
||||
<span>{imdbData.imdbRating}/10</span>
|
||||
</div>
|
||||
{imdbData.rottenTomatosRating !== 'N/A' && (
|
||||
<div className="netflix-rating-item">
|
||||
<span>Rotten Tomatoes</span>
|
||||
<span>{imdbData.rottenTomatosRating}</span>
|
||||
</div>
|
||||
)}
|
||||
{imdbData.metacriticRating !== 'N/A' && (
|
||||
<div className="netflix-rating-item">
|
||||
<span>Metacritic</span>
|
||||
<span>{imdbData.metacriticRating}/100</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Torrent Stats */}
|
||||
<div className="netflix-info-card">
|
||||
<h3>Download Info</h3>
|
||||
<div className="netflix-torrent-stats">
|
||||
<div className="netflix-stat">
|
||||
<span>Size</span>
|
||||
<span>{formatFileSize(torrent?.size || 0)}</span>
|
||||
</div>
|
||||
<div className="netflix-stat">
|
||||
<span>Progress</span>
|
||||
<span>{Math.round(torrent?.progress * 100 || 0)}%</span>
|
||||
</div>
|
||||
<div className="netflix-stat">
|
||||
<span>Speed</span>
|
||||
<span>{formatSpeed(torrent?.downloadSpeed || 0)}</span>
|
||||
</div>
|
||||
<div className="netflix-stat">
|
||||
<span>Peers</span>
|
||||
<span>{torrent?.peers || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TorrentPageNetflix;
|
||||
@@ -18,11 +18,11 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Video Player Close Button */
|
||||
/* Video Player Close Button - only shown when overlay is hidden */
|
||||
.video-close-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 280px; /* Moved left to avoid overlay conflict */
|
||||
right: 20px; /* Back to original position */
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
@@ -103,7 +103,7 @@
|
||||
.torrent-stats-overlay {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
left: 16px; /* Moved to left side */
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
@@ -205,34 +205,34 @@
|
||||
z-index: 1052; /* Above the overlay */
|
||||
}
|
||||
|
||||
/* Video close button in overlay */
|
||||
.overlay-video-close-button {
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
border: 2px solid rgba(239, 68, 68, 0.8);
|
||||
/* Minimize button for torrent stats - smaller and different styling */
|
||||
.stats-minimize {
|
||||
background: rgba(107, 114, 128, 0.8); /* Gray background to differentiate */
|
||||
border: 1px solid rgba(107, 114, 128, 0.6);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 6px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
z-index: 1053; /* Above other overlay elements */
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||
z-index: 1052;
|
||||
}
|
||||
|
||||
.overlay-video-close-button:hover {
|
||||
background: rgba(239, 68, 68, 1);
|
||||
border-color: rgba(239, 68, 68, 1);
|
||||
.stats-minimize:hover {
|
||||
background: rgba(107, 114, 128, 1);
|
||||
border-color: rgba(107, 114, 128, 0.8);
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.4);
|
||||
box-shadow: 0 2px 8px rgba(107, 114, 128, 0.3);
|
||||
}
|
||||
|
||||
.overlay-video-close-button:active {
|
||||
.stats-minimize:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
@@ -436,7 +436,7 @@
|
||||
.stats-show-button {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
left: 16px; /* Moved to left side to match overlay position */
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
border: 1px solid rgba(74, 222, 128, 0.4);
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -666,8 +666,8 @@ const VideoPlayer = ({
|
||||
onMouseMove={showControlsTemporarily}
|
||||
onMouseLeave={() => isPlaying && setShowControls(false)}
|
||||
>
|
||||
{/* Close Button - hide when overlay is visible */}
|
||||
{onClose && !showTorrentStats && (
|
||||
{/* Close Button - always visible on the right */}
|
||||
{onClose && (
|
||||
<button
|
||||
className="video-close-button"
|
||||
onClick={onClose}
|
||||
@@ -710,30 +710,17 @@ const VideoPlayer = ({
|
||||
networkStatus === 'seeking' ? 'Seeking Peers' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
{/* Control buttons */}
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
{/* Main video close button */}
|
||||
{onClose && (
|
||||
<button
|
||||
className="overlay-video-close-button"
|
||||
onClick={onClose}
|
||||
title="Close video player"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
)}
|
||||
{/* Overlay close button */}
|
||||
<button
|
||||
className="stats-minimize"
|
||||
onClick={() => {
|
||||
console.log('Direct close button clicked');
|
||||
setShowTorrentStats(false);
|
||||
}}
|
||||
title="Hide torrent stats overlay"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Only overlay minimize button */}
|
||||
<button
|
||||
className="stats-minimize"
|
||||
onClick={() => {
|
||||
console.log('Minimize overlay clicked');
|
||||
setShowTorrentStats(false);
|
||||
}}
|
||||
title="Hide Stats Overlay"
|
||||
>
|
||||
<Minimize2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
|
||||
@@ -16,6 +16,9 @@ const config = {
|
||||
frontend: {
|
||||
url: process.env.FRONTEND_URL || 'http://localhost:5173'
|
||||
},
|
||||
omdb: {
|
||||
apiKey: process.env.OMDB_API_KEY || '8265bd1c' // Free API key for development
|
||||
},
|
||||
isDevelopment: process.env.NODE_ENV === 'development'
|
||||
};
|
||||
|
||||
@@ -23,7 +26,7 @@ const app = express();
|
||||
|
||||
// SIMPLE WORKING WebTorrent configuration - minimal and functional
|
||||
const client = new WebTorrent({
|
||||
uploadLimit: 1024, // Allow minimal upload (required for peer reciprocity)
|
||||
uploadLimit: 500, // Allow minimal upload (required for peer reciprocity)
|
||||
downloadLimit: -1 // No download limit
|
||||
});
|
||||
|
||||
@@ -34,7 +37,189 @@ const torrentNames = {}; // Torrent names by infoHash
|
||||
const hashToName = {}; // Quick hash-to-name lookup
|
||||
const nameToHash = {}; // Quick name-to-hash lookup
|
||||
|
||||
// UNIVERSAL TORRENT RESOLVER - Can find torrents by ANY identifier
|
||||
// IMDB Integration
|
||||
const imdbCache = new Map();
|
||||
|
||||
// Enhanced torrent name cleaning with better regex patterns
|
||||
function cleanTorrentName(torrentName) {
|
||||
console.log(`🔍 Cleaning torrent name: "${torrentName}"`);
|
||||
|
||||
if (!torrentName) {
|
||||
return { title: '', year: null };
|
||||
}
|
||||
|
||||
// Start with the original name
|
||||
let cleanedName = torrentName;
|
||||
|
||||
// Remove common quality indicators and extensions
|
||||
cleanedName = cleanedName.replace(/\.(mkv|mp4|avi|mov|wmv|flv|m4v|3gp|webm)$/i, '');
|
||||
|
||||
// Remove quality indicators (more comprehensive)
|
||||
const qualityPatterns = [
|
||||
/\b(720p|1080p|480p|4k|uhd|hd|bluray|brrip|webrip|dvdrip|camrip|ts|telesync|hdtv|web-dl|x264|x265|h264|h265|hevc|10bit|aac|ac3|dts|mp3)\b/gi,
|
||||
/\b(yify|sparks|rarbg|eztv|ettv|galaxy|geckos|scene|repack|proper|real|internal|limited|unrated|extended|directors\.cut)\b/gi,
|
||||
/\[\w+\]/g, // Remove group tags like [YTS] [RARBG]
|
||||
/\([^)]*rip[^)]*\)/gi, // Remove anything with 'rip' in parentheses
|
||||
/\b\d{3,4}mb\b/gi, // Remove file sizes
|
||||
/www\.\w+\.(com|org|net)/gi // Remove website URLs
|
||||
];
|
||||
|
||||
qualityPatterns.forEach(pattern => {
|
||||
cleanedName = cleanedName.replace(pattern, '');
|
||||
});
|
||||
|
||||
// Extract year (look for 4-digit year in parentheses or standalone)
|
||||
let year = null;
|
||||
const yearMatch = cleanedName.match(/\b(19\d{2}|20\d{2})\b/);
|
||||
if (yearMatch) {
|
||||
year = parseInt(yearMatch[1]);
|
||||
cleanedName = cleanedName.replace(/\b(19\d{2}|20\d{2})\b/, '');
|
||||
}
|
||||
|
||||
// Remove parentheses and brackets with their content
|
||||
cleanedName = cleanedName.replace(/[\[\(].*?[\]\)]/g, '');
|
||||
|
||||
// Replace dots, underscores, dashes with spaces
|
||||
cleanedName = cleanedName.replace(/[._-]/g, ' ');
|
||||
|
||||
// Remove multiple spaces and trim
|
||||
cleanedName = cleanedName.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// Remove common articles for better matching, but store original for display
|
||||
const title = cleanedName;
|
||||
|
||||
console.log(`🧹 After basic cleaning: "${cleanedName}"`);
|
||||
console.log(`✨ Final cleaned result: title="${title}", year=${year}`);
|
||||
|
||||
return { title, year };
|
||||
}
|
||||
|
||||
async function fetchIMDBData(torrentName) {
|
||||
console.log(`🎬 Fetching IMDB data for: "${torrentName}"`);
|
||||
|
||||
// Check cache first
|
||||
if (imdbCache.has(torrentName)) {
|
||||
console.log(`📋 Using cached IMDB data for: ${torrentName}`);
|
||||
return imdbCache.get(torrentName);
|
||||
}
|
||||
|
||||
const cleanedData = cleanTorrentName(torrentName);
|
||||
const { title, year } = cleanedData;
|
||||
|
||||
// Validate title
|
||||
if (!title || title.length < 2) {
|
||||
console.log(`❌ Title too short or empty: "${title}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get API key from environment
|
||||
const omdbKey = process.env.OMDB_API_KEY || 'trilogy';
|
||||
|
||||
// Multiple search strategies with OMDb
|
||||
const omdbStrategies = [
|
||||
// Strategy 1: Title + Year (most accurate)
|
||||
year ? `http://www.omdbapi.com/?apikey=${omdbKey}&t=${encodeURIComponent(title)}&y=${year}` : null,
|
||||
|
||||
// Strategy 2: Title only
|
||||
`http://www.omdbapi.com/?apikey=${omdbKey}&t=${encodeURIComponent(title)}`,
|
||||
|
||||
// Strategy 3: Search API for multiple results
|
||||
`http://www.omdbapi.com/?apikey=${omdbKey}&s=${encodeURIComponent(title)}&type=movie`,
|
||||
|
||||
// Strategy 4: Try with "The" prefix for articles
|
||||
`http://www.omdbapi.com/?apikey=${omdbKey}&t=${encodeURIComponent('The ' + title)}`
|
||||
].filter(Boolean);
|
||||
|
||||
// Try OMDb first
|
||||
for (const url of omdbStrategies) {
|
||||
try {
|
||||
console.log(`🔍 Trying OMDb: ${url}`);
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.Response === 'True') {
|
||||
// For search results, take the first movie
|
||||
const movieData = data.Search ? data.Search[0] : data;
|
||||
|
||||
if (movieData && movieData.Title) {
|
||||
console.log(`✅ Found OMDb data: ${movieData.Title} (${movieData.Year})`);
|
||||
|
||||
const result = {
|
||||
Title: movieData.Title,
|
||||
Year: movieData.Year,
|
||||
imdbRating: movieData.imdbRating,
|
||||
imdbVotes: movieData.imdbVotes,
|
||||
Plot: movieData.Plot,
|
||||
Director: movieData.Director,
|
||||
Actors: movieData.Actors,
|
||||
Poster: movieData.Poster !== 'N/A' ? movieData.Poster : null,
|
||||
Genre: movieData.Genre,
|
||||
Runtime: movieData.Runtime,
|
||||
Rated: movieData.Rated,
|
||||
imdbID: movieData.imdbID,
|
||||
source: 'omdb'
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
imdbCache.set(torrentName, result);
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
console.log(`❌ OMDb error: ${data?.Error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ OMDb request error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to TMDB (free public API)
|
||||
console.log(`🎭 Trying TMDB as fallback for: ${title}`);
|
||||
try {
|
||||
const tmdbSearchUrl = `https://api.themoviedb.org/3/search/movie?api_key=3fd2be6f0c70a2a598f084ddfb75487d&query=${encodeURIComponent(title)}${year ? `&year=${year}` : ''}`;
|
||||
console.log(`🔍 Trying TMDB: ${tmdbSearchUrl}`);
|
||||
|
||||
const searchResponse = await fetch(tmdbSearchUrl);
|
||||
const searchData = await searchResponse.json();
|
||||
|
||||
if (searchData.results && searchData.results.length > 0) {
|
||||
const movie = searchData.results[0];
|
||||
|
||||
// Get detailed info
|
||||
const detailsUrl = `https://api.themoviedb.org/3/movie/${movie.id}?api_key=3fd2be6f0c70a2a598f084ddfb75487d&append_to_response=credits`;
|
||||
const detailsResponse = await fetch(detailsUrl);
|
||||
const details = await detailsResponse.json();
|
||||
|
||||
console.log(`✅ Found TMDB data: ${details.title} (${details.release_date?.substring(0, 4)})`);
|
||||
|
||||
const result = {
|
||||
Title: details.title,
|
||||
Year: details.release_date?.substring(0, 4),
|
||||
imdbRating: details.vote_average ? (details.vote_average / 10 * 10).toFixed(1) : null,
|
||||
imdbVotes: details.vote_count ? `${details.vote_count.toLocaleString()}` : null,
|
||||
Plot: details.overview,
|
||||
Director: details.credits?.crew?.find(person => person.job === 'Director')?.name,
|
||||
Actors: details.credits?.cast?.slice(0, 4).map(actor => actor.name).join(', '),
|
||||
Poster: details.poster_path ? `https://image.tmdb.org/t/p/w500${details.poster_path}` : null,
|
||||
Genre: details.genres?.map(g => g.name).join(', '),
|
||||
Runtime: details.runtime ? `${details.runtime} min` : null,
|
||||
Rated: 'N/A', // TMDB doesn't have ratings
|
||||
tmdbID: details.id,
|
||||
source: 'tmdb'
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
imdbCache.set(torrentName, result);
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ TMDB error: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log(`❌ No movie data found for: ${title}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
//UNIVERSAL TORRENT RESOLVER - Can find torrents by ANY identifier
|
||||
const universalTorrentResolver = async (identifier) => {
|
||||
console.log(`🔍 Universal resolver looking for: ${identifier}`);
|
||||
|
||||
@@ -54,7 +239,7 @@ const universalTorrentResolver = async (identifier) => {
|
||||
);
|
||||
|
||||
if (existingTorrent) {
|
||||
console.log(`✅ Found in WebTorrent client: ${existingTorrent.name}`);
|
||||
console.log(`✅ Found in WebTorrent client: ${existingTorrent.name || existingTorrent.infoHash}`);
|
||||
torrents[existingTorrent.infoHash] = existingTorrent;
|
||||
return existingTorrent;
|
||||
}
|
||||
@@ -184,8 +369,29 @@ const loadTorrentFromId = (torrentId) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`⏰ Timeout loading torrent after 30 seconds: ${torrentId}`);
|
||||
console.log(`🔍 Client has ${client.torrents.length} torrents total`);
|
||||
reject(new Error('Timeout loading torrent'));
|
||||
|
||||
// Check if the torrent was actually added to the client
|
||||
const clientTorrent = client.torrents.find(t => t.infoHash === torrent.infoHash);
|
||||
if (clientTorrent) {
|
||||
console.log(`🔍 Found torrent in client after timeout: ${clientTorrent.name || clientTorrent.infoHash}`);
|
||||
|
||||
// Store in tracking systems even if metadata isn't fully ready
|
||||
torrents[clientTorrent.infoHash] = clientTorrent;
|
||||
torrentIds[clientTorrent.infoHash] = torrentId;
|
||||
torrentNames[clientTorrent.infoHash] = clientTorrent.name || 'Loading...';
|
||||
hashToName[clientTorrent.infoHash] = clientTorrent.name || 'Loading...';
|
||||
if (clientTorrent.name) {
|
||||
nameToHash[clientTorrent.name] = clientTorrent.infoHash;
|
||||
}
|
||||
|
||||
clientTorrent.addedAt = new Date().toISOString();
|
||||
clientTorrent.uploadLimit = 1024;
|
||||
|
||||
resolve(clientTorrent);
|
||||
} else {
|
||||
console.log(`🔍 Client has ${client.torrents.length} torrents total`);
|
||||
reject(new Error('Timeout loading torrent'));
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
@@ -211,10 +417,22 @@ process.on('SIGINT', () => {
|
||||
});
|
||||
|
||||
// Configure multer
|
||||
const fs = require('fs');
|
||||
const uploadsDir = 'uploads/';
|
||||
|
||||
// Ensure uploads directory exists
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
console.log('📁 Created uploads directory');
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
dest: 'uploads/',
|
||||
dest: uploadsDir,
|
||||
fileFilter: (req, file, cb) => {
|
||||
cb(null, file.originalname.endsWith('.torrent'));
|
||||
},
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024 // 5MB limit for torrent files
|
||||
}
|
||||
});
|
||||
|
||||
@@ -251,19 +469,53 @@ app.post('/api/torrents', async (req, res) => {
|
||||
|
||||
if (!torrent) {
|
||||
// If resolver failed, try direct loading
|
||||
const newTorrent = await loadTorrentFromId(torrentId);
|
||||
return res.json({
|
||||
infoHash: newTorrent.infoHash,
|
||||
name: newTorrent.name,
|
||||
size: newTorrent.length,
|
||||
status: 'loaded'
|
||||
});
|
||||
try {
|
||||
const newTorrent = await loadTorrentFromId(torrentId);
|
||||
return res.json({
|
||||
infoHash: newTorrent.infoHash,
|
||||
name: newTorrent.name || 'Loading...',
|
||||
size: newTorrent.length || 0,
|
||||
status: 'loaded'
|
||||
});
|
||||
} catch (loadError) {
|
||||
// Handle duplicate torrent error specially
|
||||
if (loadError.message.includes('duplicate torrent')) {
|
||||
console.log(`🔍 Duplicate torrent detected, finding existing torrent`);
|
||||
|
||||
// Extract hash from torrentId if it's a magnet
|
||||
let hash = torrentId;
|
||||
if (torrentId.startsWith('magnet:')) {
|
||||
const match = torrentId.match(/xt=urn:btih:([a-fA-F0-9]{40})/);
|
||||
if (match) hash = match[1];
|
||||
}
|
||||
|
||||
// Try to find the existing torrent
|
||||
const existingTorrent = Object.values(torrents).find(t =>
|
||||
t.infoHash === hash ||
|
||||
t.infoHash.toLowerCase() === hash.toLowerCase()
|
||||
) || client.torrents.find(t =>
|
||||
t.infoHash === hash ||
|
||||
t.infoHash.toLowerCase() === hash.toLowerCase()
|
||||
);
|
||||
|
||||
if (existingTorrent) {
|
||||
return res.json({
|
||||
infoHash: existingTorrent.infoHash,
|
||||
name: existingTorrent.name || 'Loading...',
|
||||
size: existingTorrent.length || 0,
|
||||
status: 'existing'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw loadError;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
infoHash: torrent.infoHash,
|
||||
name: torrent.name,
|
||||
size: torrent.length,
|
||||
name: torrent.name || 'Loading...',
|
||||
size: torrent.length || 0,
|
||||
status: 'found'
|
||||
});
|
||||
|
||||
@@ -273,6 +525,93 @@ app.post('/api/torrents', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// UNIVERSAL FILE UPLOAD - Handle .torrent files
|
||||
app.post('/api/torrents/upload', upload.single('torrentFile'), async (req, res) => {
|
||||
console.log(`📁 UNIVERSAL FILE UPLOAD`);
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No torrent file provided' });
|
||||
}
|
||||
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const torrentPath = req.file.path;
|
||||
|
||||
console.log(`📁 Processing uploaded file: ${req.file.originalname}`);
|
||||
console.log(`📁 File path: ${torrentPath}`);
|
||||
|
||||
// Read the torrent file
|
||||
const torrentBuffer = fs.readFileSync(torrentPath);
|
||||
|
||||
// Load the torrent using the buffer
|
||||
const torrent = await new Promise((resolve, reject) => {
|
||||
const loadedTorrent = client.add(torrentBuffer);
|
||||
|
||||
let resolved = false;
|
||||
|
||||
loadedTorrent.on('ready', () => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
|
||||
console.log(`✅ Torrent uploaded and loaded: ${loadedTorrent.name}`);
|
||||
|
||||
// Store in tracking systems
|
||||
torrents[loadedTorrent.infoHash] = loadedTorrent;
|
||||
torrentIds[loadedTorrent.infoHash] = req.file.originalname;
|
||||
torrentNames[loadedTorrent.infoHash] = loadedTorrent.name;
|
||||
hashToName[loadedTorrent.infoHash] = loadedTorrent.name;
|
||||
nameToHash[loadedTorrent.name] = loadedTorrent.infoHash;
|
||||
|
||||
loadedTorrent.addedAt = new Date().toISOString();
|
||||
loadedTorrent.uploadLimit = 1024; // Minimal upload for peer reciprocity
|
||||
|
||||
resolve(loadedTorrent);
|
||||
});
|
||||
|
||||
loadedTorrent.on('error', (err) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
console.error(`❌ Error loading uploaded torrent:`, err.message);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
reject(new Error('Timeout loading torrent file'));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// Clean up uploaded file
|
||||
fs.unlinkSync(torrentPath);
|
||||
|
||||
res.json({
|
||||
infoHash: torrent.infoHash,
|
||||
name: torrent.name,
|
||||
size: torrent.length,
|
||||
status: 'uploaded',
|
||||
files: torrent.files.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ File upload failed:`, error.message);
|
||||
|
||||
// Clean up file on error
|
||||
if (req.file && req.file.path) {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
fs.unlinkSync(req.file.path);
|
||||
} catch (cleanupError) {
|
||||
console.error(`❌ Failed to cleanup file:`, cleanupError.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Failed to upload torrent: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// UNIVERSAL GET TORRENTS - Always returns results
|
||||
app.get('/api/torrents', (req, res) => {
|
||||
const activeTorrents = Object.values(torrents).map(torrent => ({
|
||||
@@ -402,6 +741,47 @@ app.get('/api/torrents/:identifier/stats', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// IMDB Data Endpoint
|
||||
app.get('/api/torrents/:identifier/imdb', async (req, res) => {
|
||||
const identifier = req.params.identifier;
|
||||
console.log(`🎬 IMDB REQUEST: ${identifier}`);
|
||||
|
||||
try {
|
||||
const torrent = await universalTorrentResolver(identifier);
|
||||
|
||||
if (!torrent) {
|
||||
console.log(`❌ Torrent not found for identifier: ${identifier}`);
|
||||
return res.status(404).json({ error: 'Torrent not found' });
|
||||
}
|
||||
|
||||
console.log(`🎬 Found torrent: ${torrent.name}, fetching IMDB data...`);
|
||||
const imdbData = await fetchIMDBData(torrent.name);
|
||||
console.log(`🎬 IMDB data result:`, imdbData ? 'SUCCESS' : 'NULL/UNDEFINED');
|
||||
|
||||
if (imdbData) {
|
||||
const response = {
|
||||
success: true,
|
||||
torrentName: torrent.name,
|
||||
imdb: imdbData
|
||||
};
|
||||
console.log(`✅ Sending IMDB response:`, JSON.stringify(response, null, 2));
|
||||
res.json(response);
|
||||
} else {
|
||||
const response = {
|
||||
success: false,
|
||||
torrentName: torrent.name,
|
||||
message: 'IMDB data not found'
|
||||
};
|
||||
console.log(`❌ Sending failure response:`, JSON.stringify(response, null, 2));
|
||||
res.json(response);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ IMDB endpoint failed:`, error.message);
|
||||
res.status(500).json({ error: 'Failed to get IMDB data: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// UNIVERSAL STREAMING - Always works if torrent exists
|
||||
app.get('/api/torrents/:identifier/files/:fileIdx/stream', async (req, res) => {
|
||||
const { identifier, fileIdx } = req.params;
|
||||
|
||||
Reference in New Issue
Block a user