Spaces:
Build error
Build error
Commit ·
508e2bf
1
Parent(s): 39624f5
0.0.3 V Alpha UI update
Browse files- frontend/package-lock.json +37 -0
- frontend/package.json +1 -0
- frontend/src/App.css +3 -3
- frontend/src/api/LoadBalancer.js +126 -95
- frontend/src/components/film/modals/playModal.css +129 -0
- frontend/src/components/film/modals/playModal.js +99 -0
- frontend/src/config.js +1 -1
- frontend/src/pages/filmDetailsPage.css +1 -1
- frontend/src/pages/filmDetailsPage.js +54 -17
- nginx.conf +12 -0
frontend/package-lock.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
| 18 |
"http-proxy-middleware": "^2.0.6",
|
| 19 |
"react": "^18.2.0",
|
| 20 |
"react-dom": "^18.2.0",
|
|
|
|
| 21 |
"react-router-dom": "^6.25.1",
|
| 22 |
"react-scripts": "5.0.1",
|
| 23 |
"serve": "^14.2.3",
|
|
@@ -8390,6 +8391,11 @@
|
|
| 8390 |
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
| 8391 |
}
|
| 8392 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8393 |
"node_modules/exit": {
|
| 8394 |
"version": "0.1.2",
|
| 8395 |
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
|
@@ -15141,6 +15147,29 @@
|
|
| 15141 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
| 15142 |
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
| 15143 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15144 |
"node_modules/react-refresh": {
|
| 15145 |
"version": "0.11.0",
|
| 15146 |
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
|
@@ -17558,6 +17587,14 @@
|
|
| 17558 |
"makeerror": "1.0.12"
|
| 17559 |
}
|
| 17560 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17561 |
"node_modules/watchpack": {
|
| 17562 |
"version": "2.4.0",
|
| 17563 |
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
|
|
|
| 18 |
"http-proxy-middleware": "^2.0.6",
|
| 19 |
"react": "^18.2.0",
|
| 20 |
"react-dom": "^18.2.0",
|
| 21 |
+
"react-modal": "^3.16.1",
|
| 22 |
"react-router-dom": "^6.25.1",
|
| 23 |
"react-scripts": "5.0.1",
|
| 24 |
"serve": "^14.2.3",
|
|
|
|
| 8391 |
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
| 8392 |
}
|
| 8393 |
},
|
| 8394 |
+
"node_modules/exenv": {
|
| 8395 |
+
"version": "1.2.2",
|
| 8396 |
+
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
|
| 8397 |
+
"integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="
|
| 8398 |
+
},
|
| 8399 |
"node_modules/exit": {
|
| 8400 |
"version": "0.1.2",
|
| 8401 |
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
|
|
|
| 15147 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
| 15148 |
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
| 15149 |
},
|
| 15150 |
+
"node_modules/react-lifecycles-compat": {
|
| 15151 |
+
"version": "3.0.4",
|
| 15152 |
+
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
| 15153 |
+
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
| 15154 |
+
},
|
| 15155 |
+
"node_modules/react-modal": {
|
| 15156 |
+
"version": "3.16.1",
|
| 15157 |
+
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz",
|
| 15158 |
+
"integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==",
|
| 15159 |
+
"dependencies": {
|
| 15160 |
+
"exenv": "^1.2.0",
|
| 15161 |
+
"prop-types": "^15.7.2",
|
| 15162 |
+
"react-lifecycles-compat": "^3.0.0",
|
| 15163 |
+
"warning": "^4.0.3"
|
| 15164 |
+
},
|
| 15165 |
+
"engines": {
|
| 15166 |
+
"node": ">=8"
|
| 15167 |
+
},
|
| 15168 |
+
"peerDependencies": {
|
| 15169 |
+
"react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18",
|
| 15170 |
+
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18"
|
| 15171 |
+
}
|
| 15172 |
+
},
|
| 15173 |
"node_modules/react-refresh": {
|
| 15174 |
"version": "0.11.0",
|
| 15175 |
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
|
|
|
| 17587 |
"makeerror": "1.0.12"
|
| 17588 |
}
|
| 17589 |
},
|
| 17590 |
+
"node_modules/warning": {
|
| 17591 |
+
"version": "4.0.3",
|
| 17592 |
+
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
| 17593 |
+
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
| 17594 |
+
"dependencies": {
|
| 17595 |
+
"loose-envify": "^1.0.0"
|
| 17596 |
+
}
|
| 17597 |
+
},
|
| 17598 |
"node_modules/watchpack": {
|
| 17599 |
"version": "2.4.0",
|
| 17600 |
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
frontend/package.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
| 13 |
"http-proxy-middleware": "^2.0.6",
|
| 14 |
"react": "^18.2.0",
|
| 15 |
"react-dom": "^18.2.0",
|
|
|
|
| 16 |
"react-router-dom": "^6.25.1",
|
| 17 |
"react-scripts": "5.0.1",
|
| 18 |
"serve": "^14.2.3",
|
|
|
|
| 13 |
"http-proxy-middleware": "^2.0.6",
|
| 14 |
"react": "^18.2.0",
|
| 15 |
"react-dom": "^18.2.0",
|
| 16 |
+
"react-modal": "^3.16.1",
|
| 17 |
"react-router-dom": "^6.25.1",
|
| 18 |
"react-scripts": "5.0.1",
|
| 19 |
"serve": "^14.2.3",
|
frontend/src/App.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
body {
|
| 2 |
-
background
|
| 3 |
color: #f5f5f5; /* Light gray text for contrast */
|
| 4 |
font-family: "Signika", sans-serif;
|
| 5 |
font-optical-sizing: auto;
|
|
@@ -15,8 +15,8 @@ body {
|
|
| 15 |
.App-header {
|
| 16 |
background: linear-gradient(
|
| 17 |
to right,
|
| 18 |
-
#
|
| 19 |
-
#
|
| 20 |
); /* Dark gradient for a sophisticated header */
|
| 21 |
padding: 10px 0; /* Padding for spacing */
|
| 22 |
color: #f5f5f5; /* Light text color for readability */
|
|
|
|
| 1 |
body {
|
| 2 |
+
background: linear-gradient(to top, #1e2634 30%, #181e28);
|
| 3 |
color: #f5f5f5; /* Light gray text for contrast */
|
| 4 |
font-family: "Signika", sans-serif;
|
| 5 |
font-optical-sizing: auto;
|
|
|
|
| 15 |
.App-header {
|
| 16 |
background: linear-gradient(
|
| 17 |
to right,
|
| 18 |
+
#171717,
|
| 19 |
+
#31506e
|
| 20 |
); /* Dark gradient for a sophisticated header */
|
| 21 |
padding: 10px 0; /* Padding for spacing */
|
| 22 |
color: #f5f5f5; /* Light text color for readability */
|
frontend/src/api/LoadBalancer.js
CHANGED
|
@@ -1,101 +1,132 @@
|
|
| 1 |
class LoadBalancerAPI {
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
}
|
| 75 |
}
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
body: JSON.stringify(body),
|
| 83 |
-
});
|
| 84 |
-
return await this._handleResponse(response);
|
| 85 |
-
} catch (error) {
|
| 86 |
-
console.error(`Error during POST request to ${endpoint}:`, error);
|
| 87 |
-
throw error;
|
| 88 |
-
}
|
| 89 |
}
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
}
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
}
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
| 1 |
class LoadBalancerAPI {
|
| 2 |
+
constructor(baseURL) {
|
| 3 |
+
this.baseURL = baseURL;
|
| 4 |
+
this.filmCache = null; // Cache for film store
|
| 5 |
+
this.tvCache = null; // Cache for TV store
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
async getInstances() {
|
| 9 |
+
return this._getRequest('/api/instances');
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
async getInstancesHealth() {
|
| 13 |
+
return this._getRequest('/api/instances/health');
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
async getMovieByTitle(title) {
|
| 17 |
+
return this._getRequest(`/api/film/${encodeURIComponent(title)}`);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
async getTVEpisode(title, season, episode) {
|
| 21 |
+
return this._getRequest(`/api/tv/${encodeURIComponent(title)}/${season}/${episode}`);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
async getFilmIDByTitle(title) {
|
| 25 |
+
return this._getRequest(`/api/filmid/${encodeURIComponent(title)}`);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
async getEpisodeIDByTitleSeasonEpisode(title, season, episode) {
|
| 29 |
+
return this._getRequest(`/api/episodeid/${encodeURIComponent(title)}/${season}/${episode}`);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
async getCacheSize() {
|
| 33 |
+
return this._getRequest('/api/cache/size');
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
async clearCache() {
|
| 37 |
+
return this._postRequest('/api/cache/clear');
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
async getTVStore() {
|
| 41 |
+
const response = await this._getRequest('/api/tv/store');
|
| 42 |
+
|
| 43 |
+
if (response && Object.keys(response).length > 0) {
|
| 44 |
+
this.tvCache = response; // Update cache if response is not empty
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return this.tvCache || {}; // Return cache if response is empty
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
async getFilmStore() {
|
| 51 |
+
const response = await this._getRequest('/api/film/store');
|
| 52 |
+
|
| 53 |
+
if (response && Object.keys(response).length > 0) {
|
| 54 |
+
this.filmCache = response; // Update cache if response is not empty
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return this.filmCache || {}; // Return cache if response is empty
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
async getFilmMetadataByTitle(title) {
|
| 61 |
+
return this._getRequest(`/api/film/metadata/${encodeURIComponent(title)}`);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
async getTVMetadataByTitle(title) {
|
| 65 |
+
return this._getRequest(`/api/tv/metadata/${encodeURIComponent(title)}`);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
async getAllFilms() {
|
| 69 |
+
return this._getRequest('/api/film/all');
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
async getAllTVShows() {
|
| 73 |
+
return this._getRequest('/api/tv/all');
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
async getDownloadProgress(url) {
|
| 77 |
+
return this._getRequestNoBase(url);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Helper methods for GET and POST requests
|
| 81 |
+
async _getRequest(endpoint) {
|
| 82 |
+
try {
|
| 83 |
+
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
| 84 |
+
method: 'GET',
|
| 85 |
+
headers: { 'Content-Type': 'application/json' },
|
| 86 |
+
});
|
| 87 |
+
console.log(`api endpoint: ${this.baseURL}${endpoint}`);
|
| 88 |
+
return await this._handleResponse(response);
|
| 89 |
+
} catch (error) {
|
| 90 |
+
console.error(`Error during GET request to ${endpoint}:`, error);
|
| 91 |
+
throw error;
|
| 92 |
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
async _postRequest(endpoint, body) {
|
| 96 |
+
try {
|
| 97 |
+
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
| 98 |
+
method: 'POST',
|
| 99 |
+
headers: { 'Content-Type': 'application/json' },
|
| 100 |
+
body: JSON.stringify(body),
|
| 101 |
+
});
|
| 102 |
+
return await this._handleResponse(response);
|
| 103 |
+
} catch (error) {
|
| 104 |
+
console.error(`Error during POST request to ${endpoint}:`, error);
|
| 105 |
+
throw error;
|
|
|
|
| 106 |
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
async _handleResponse(response) {
|
| 110 |
+
if (!response.ok) {
|
| 111 |
+
const errorDetails = await response.text();
|
| 112 |
+
throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
+
return response.json();
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
async _getRequestNoBase(url) {
|
| 118 |
+
try {
|
| 119 |
+
const response = await fetch(`${url}`, {
|
| 120 |
+
method: 'GET',
|
| 121 |
+
headers: { 'Content-Type': 'application/json' },
|
| 122 |
+
});
|
| 123 |
+
console.log(`api endpoint: ${url}`);
|
| 124 |
+
return await this._handleResponse(response);
|
| 125 |
+
} catch (error) {
|
| 126 |
+
console.error(`Error during GET request to ${url}:`, error);
|
| 127 |
+
throw error;
|
| 128 |
}
|
| 129 |
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
export { LoadBalancerAPI };
|
frontend/src/components/film/modals/playModal.css
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.play-modal {
|
| 2 |
+
position: absolute;
|
| 3 |
+
top: 50%;
|
| 4 |
+
left: 50%;
|
| 5 |
+
transform: translate(-50%, -50%);
|
| 6 |
+
background: linear-gradient(135deg, #171717, #31506e); /* Deep blue gradient */
|
| 7 |
+
padding: 30px;
|
| 8 |
+
border-radius: 20px;
|
| 9 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
|
| 10 |
+
color: #f0f0f0; /* Light gray text color */
|
| 11 |
+
text-align: center;
|
| 12 |
+
width: 90%;
|
| 13 |
+
max-width: 600px;
|
| 14 |
+
z-index: 1001;
|
| 15 |
+
animation: fadeIn 0.3s ease-out;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
.play-modal-overlay {
|
| 19 |
+
background-color: rgba(0, 0, 0, 0.8);
|
| 20 |
+
z-index: 1000;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.modal-content {
|
| 24 |
+
display: flex;
|
| 25 |
+
flex-direction: column;
|
| 26 |
+
align-items: center;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.modal-title {
|
| 30 |
+
font-size: 2.5rem;
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
color: #ffffff; /* White text color for the title */
|
| 33 |
+
font-family: 'Roboto', sans-serif;
|
| 34 |
+
text-transform: uppercase;
|
| 35 |
+
letter-spacing: 2px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.progress-container {
|
| 39 |
+
width: 100%;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.progress-bar {
|
| 43 |
+
width: 100%;
|
| 44 |
+
background: #003366; /* Darker blue for the progress bar background */
|
| 45 |
+
border-radius: 10px;
|
| 46 |
+
overflow: hidden;
|
| 47 |
+
margin-bottom: 10px;
|
| 48 |
+
height: 15px;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.progress {
|
| 52 |
+
height: 100%;
|
| 53 |
+
background: #1e90ff; /* Bright blue for the progress */
|
| 54 |
+
transition: width 0.3s ease-in-out;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.progress-info {
|
| 58 |
+
display: flex;
|
| 59 |
+
justify-content: space-between;
|
| 60 |
+
width: 100%;
|
| 61 |
+
padding: 0 10px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.progress-text {
|
| 65 |
+
font-size: 1.1rem;
|
| 66 |
+
color: #d3d3d3; /* Light gray text color for progress info */
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.modal-actions {
|
| 70 |
+
display: flex;
|
| 71 |
+
justify-content: center;
|
| 72 |
+
gap: 15px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.confirm-button {
|
| 76 |
+
background: #3f75d2; /* Soft blue for the confirm button */
|
| 77 |
+
color: #fff;
|
| 78 |
+
border: none;
|
| 79 |
+
padding: 12px 25px;
|
| 80 |
+
border-radius: 8px;
|
| 81 |
+
cursor: pointer;
|
| 82 |
+
font-size: 1.1rem;
|
| 83 |
+
transition: background 0.3s ease;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.confirm-button:hover {
|
| 87 |
+
background: #4244b3; /* Slightly darker blue on hover */
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.cancel-button {
|
| 91 |
+
background: #b0bec5; /* Light silver-blue for the cancel button */
|
| 92 |
+
color: #000; /* Dark text color for contrast */
|
| 93 |
+
border: none;
|
| 94 |
+
padding: 12px 25px;
|
| 95 |
+
border-radius: 8px;
|
| 96 |
+
cursor: pointer;
|
| 97 |
+
font-size: 1.1rem;
|
| 98 |
+
transition: background 0.3s ease;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.cancel-button:hover {
|
| 102 |
+
background: #90a4ae; /* Slightly darker silver-blue on hover */
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
@keyframes fadeIn {
|
| 106 |
+
from {
|
| 107 |
+
opacity: 0;
|
| 108 |
+
}
|
| 109 |
+
to {
|
| 110 |
+
opacity: 1;
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
@media (max-width: 768px) {
|
| 115 |
+
.play-modal {
|
| 116 |
+
width: 95%;
|
| 117 |
+
padding: 20px;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.modal-title {
|
| 121 |
+
font-size: 2rem;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.confirm-button, .cancel-button {
|
| 125 |
+
padding: 10px 20px;
|
| 126 |
+
font-size: 1rem;
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
frontend/src/components/film/modals/playModal.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from "react";
|
| 2 |
+
import Modal from "react-modal";
|
| 3 |
+
import apiClient from "../../../api/apiClient";
|
| 4 |
+
import "./playModal.css";
|
| 5 |
+
|
| 6 |
+
Modal.setAppElement("#root");
|
| 7 |
+
|
| 8 |
+
const PlayModal = ({ isOpen, onRequestClose, videoUrl, setVideoUrl, downloadProgress, title }) => {
|
| 9 |
+
const [progress, setProgress] = useState(null);
|
| 10 |
+
const [completed, setCompleted] = useState(false);
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
let interval;
|
| 14 |
+
if (downloadProgress) {
|
| 15 |
+
interval = setInterval(async () => {
|
| 16 |
+
try {
|
| 17 |
+
const response = await apiClient.getDownloadProgress(downloadProgress);
|
| 18 |
+
setProgress(response.progress);
|
| 19 |
+
if (response.progress.status === "Completed") {
|
| 20 |
+
setCompleted(true);
|
| 21 |
+
clearInterval(interval);
|
| 22 |
+
}
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.error("Failed to fetch download progress:", error);
|
| 25 |
+
}
|
| 26 |
+
}, 5000);
|
| 27 |
+
}
|
| 28 |
+
return () => clearInterval(interval);
|
| 29 |
+
}, [downloadProgress]);
|
| 30 |
+
|
| 31 |
+
useEffect(() => {
|
| 32 |
+
const fetchVideoUrl = async () => {
|
| 33 |
+
try {
|
| 34 |
+
const response = await apiClient.getMovieByTitle(title);
|
| 35 |
+
if (response.url) {
|
| 36 |
+
setVideoUrl(response.url);
|
| 37 |
+
}
|
| 38 |
+
} catch (error) {
|
| 39 |
+
console.error("Failed to fetch video URL:", error);
|
| 40 |
+
}
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
if (completed) {
|
| 44 |
+
const timer = setTimeout(() => {
|
| 45 |
+
fetchVideoUrl();
|
| 46 |
+
}, 5000);
|
| 47 |
+
return () => clearTimeout(timer);
|
| 48 |
+
}
|
| 49 |
+
}, [completed, title, setVideoUrl]);
|
| 50 |
+
|
| 51 |
+
const handleConfirm = () => {
|
| 52 |
+
if (videoUrl) {
|
| 53 |
+
window.location.href = videoUrl;
|
| 54 |
+
}
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
const handleCancel = () => {
|
| 58 |
+
onRequestClose();
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
return (
|
| 62 |
+
<Modal
|
| 63 |
+
isOpen={isOpen}
|
| 64 |
+
onRequestClose={onRequestClose}
|
| 65 |
+
contentLabel="Play Modal"
|
| 66 |
+
className="play-modal"
|
| 67 |
+
overlayClassName="play-modal-overlay"
|
| 68 |
+
>
|
| 69 |
+
<div className="modal-content">
|
| 70 |
+
{videoUrl ? (
|
| 71 |
+
<>
|
| 72 |
+
<h2 className="modal-title">Ready to Play</h2>
|
| 73 |
+
<div className="modal-actions">
|
| 74 |
+
<button className="confirm-button" onClick={handleConfirm}>Play</button>
|
| 75 |
+
<button className="cancel-button" onClick={handleCancel}>No</button>
|
| 76 |
+
</div>
|
| 77 |
+
</>
|
| 78 |
+
) : progress ? (
|
| 79 |
+
<>
|
| 80 |
+
<h2 className="modal-title">Downloading...</h2>
|
| 81 |
+
<div className="progress-container">
|
| 82 |
+
<div className="progress-bar">
|
| 83 |
+
<div className="progress" style={{ width: `${progress.progress}%` }}></div>
|
| 84 |
+
</div>
|
| 85 |
+
<div className="progress-info">
|
| 86 |
+
<span className="progress-text">Progress: {progress.progress.toFixed(2)}%</span>
|
| 87 |
+
<span className="progress-text">ETA: {progress.eta.toFixed(2)} seconds</span>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
</>
|
| 91 |
+
) : (
|
| 92 |
+
<h2 className="modal-title">Initializing download...</h2>
|
| 93 |
+
)}
|
| 94 |
+
</div>
|
| 95 |
+
</Modal>
|
| 96 |
+
);
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
export default PlayModal;
|
frontend/src/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
// src/config.js
|
| 2 |
const config = {
|
| 3 |
apiBaseUrl: 'https://unicone-studio-load-balancer.hf.space',
|
| 4 |
-
version: "0.0.
|
| 5 |
};
|
| 6 |
|
| 7 |
export default config;
|
|
|
|
| 1 |
// src/config.js
|
| 2 |
const config = {
|
| 3 |
apiBaseUrl: 'https://unicone-studio-load-balancer.hf.space',
|
| 4 |
+
version: "0.0.3 V Alpha",
|
| 5 |
};
|
| 6 |
|
| 7 |
export default config;
|
frontend/src/pages/filmDetailsPage.css
CHANGED
|
@@ -40,7 +40,7 @@
|
|
| 40 |
left: 0;
|
| 41 |
right: 0;
|
| 42 |
bottom: 0;
|
| 43 |
-
background: linear-gradient(to top,
|
| 44 |
z-index: 1;
|
| 45 |
}
|
| 46 |
|
|
|
|
| 40 |
left: 0;
|
| 41 |
right: 0;
|
| 42 |
bottom: 0;
|
| 43 |
+
background: linear-gradient(to top, #181e28 30%, transparent);
|
| 44 |
z-index: 1;
|
| 45 |
}
|
| 46 |
|
frontend/src/pages/filmDetailsPage.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
-
import React, { useEffect, useState } from "react";
|
| 2 |
import { useParams } from "react-router-dom";
|
| 3 |
import apiClient from "../api/apiClient";
|
| 4 |
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
| 5 |
import { faPlay } from "@fortawesome/free-solid-svg-icons";
|
|
|
|
| 6 |
import "./filmDetailsPage.css";
|
| 7 |
|
| 8 |
function FilmDetailsPage() {
|
|
@@ -10,28 +11,56 @@ function FilmDetailsPage() {
|
|
| 10 |
const [filmDetails, setFilmDetails] = useState(null);
|
| 11 |
const [loading, setLoading] = useState(true);
|
| 12 |
const [error, setError] = useState(null);
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
setError("No film details found.");
|
| 22 |
-
}
|
| 23 |
-
} catch (err) {
|
| 24 |
-
setError("Failed to fetch film details.");
|
| 25 |
-
} finally {
|
| 26 |
-
setLoading(false);
|
| 27 |
}
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}, [title]);
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
const handlePlay = () => {
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
};
|
| 36 |
|
| 37 |
if (loading) {
|
|
@@ -147,6 +176,14 @@ function FilmDetailsPage() {
|
|
| 147 |
</div>
|
| 148 |
)}
|
| 149 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
</div>
|
| 151 |
);
|
| 152 |
}
|
|
|
|
| 1 |
+
import React, { useEffect, useState, useCallback } from "react";
|
| 2 |
import { useParams } from "react-router-dom";
|
| 3 |
import apiClient from "../api/apiClient";
|
| 4 |
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
| 5 |
import { faPlay } from "@fortawesome/free-solid-svg-icons";
|
| 6 |
+
import PlayModal from "../components/film/modals/playModal";
|
| 7 |
import "./filmDetailsPage.css";
|
| 8 |
|
| 9 |
function FilmDetailsPage() {
|
|
|
|
| 11 |
const [filmDetails, setFilmDetails] = useState(null);
|
| 12 |
const [loading, setLoading] = useState(true);
|
| 13 |
const [error, setError] = useState(null);
|
| 14 |
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
| 15 |
+
const [videoUrl, setVideoUrl] = useState(null);
|
| 16 |
+
const [downloadProgress, setDownloadProgress] = useState(null);
|
| 17 |
|
| 18 |
+
const fetchFilmDetails = useCallback(async () => {
|
| 19 |
+
try {
|
| 20 |
+
const response = await apiClient.getFilmMetadataByTitle(title);
|
| 21 |
+
if (response.data) {
|
| 22 |
+
setFilmDetails(response.data);
|
| 23 |
+
} else {
|
| 24 |
+
setError("No film details found.");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
+
} catch (err) {
|
| 27 |
+
setError("Failed to fetch film details.");
|
| 28 |
+
} finally {
|
| 29 |
+
setLoading(false);
|
| 30 |
+
}
|
| 31 |
+
}, [title]);
|
| 32 |
|
| 33 |
+
const fetchVideoUrl = useCallback(async () => {
|
| 34 |
+
try {
|
| 35 |
+
const response = await apiClient.getMovieByTitle(title);
|
| 36 |
+
console.log(response);
|
| 37 |
+
if (response.url) {
|
| 38 |
+
setVideoUrl(response.url);
|
| 39 |
+
} else if (response.progress_url) {
|
| 40 |
+
setDownloadProgress(response.progress_url);
|
| 41 |
+
}
|
| 42 |
+
} catch (err) {
|
| 43 |
+
console.log(err);
|
| 44 |
+
setError("Failed to fetch video URL.");
|
| 45 |
+
}
|
| 46 |
}, [title]);
|
| 47 |
|
| 48 |
+
useEffect(() => {
|
| 49 |
+
fetchFilmDetails();
|
| 50 |
+
}, [fetchFilmDetails]);
|
| 51 |
+
|
| 52 |
+
useEffect(() => {
|
| 53 |
+
if (isModalOpen && !videoUrl) {
|
| 54 |
+
fetchVideoUrl();
|
| 55 |
+
}
|
| 56 |
+
}, [isModalOpen, videoUrl, fetchVideoUrl]);
|
| 57 |
+
|
| 58 |
const handlePlay = () => {
|
| 59 |
+
setIsModalOpen(true);
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const handleModalClose = () => {
|
| 63 |
+
setIsModalOpen(false);
|
| 64 |
};
|
| 65 |
|
| 66 |
if (loading) {
|
|
|
|
| 176 |
</div>
|
| 177 |
)}
|
| 178 |
</div>
|
| 179 |
+
<PlayModal
|
| 180 |
+
isOpen={isModalOpen}
|
| 181 |
+
onRequestClose={handleModalClose}
|
| 182 |
+
videoUrl={videoUrl}
|
| 183 |
+
setVideoUrl={setVideoUrl}
|
| 184 |
+
downloadProgress={downloadProgress}
|
| 185 |
+
title={title}
|
| 186 |
+
/>
|
| 187 |
</div>
|
| 188 |
);
|
| 189 |
}
|
nginx.conf
CHANGED
|
@@ -14,6 +14,18 @@ http {
|
|
| 14 |
tcp_nopush on;
|
| 15 |
tcp_nodelay on;
|
| 16 |
keepalive_timeout 65;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# Improve performance and security
|
| 19 |
gzip on;
|
|
|
|
| 14 |
tcp_nopush on;
|
| 15 |
tcp_nodelay on;
|
| 16 |
keepalive_timeout 65;
|
| 17 |
+
types {
|
| 18 |
+
text/html html;
|
| 19 |
+
text/css css;
|
| 20 |
+
text/xml xml;
|
| 21 |
+
image/gif gif;
|
| 22 |
+
image/jpeg jpeg jpg;
|
| 23 |
+
application/javascript js;
|
| 24 |
+
application/json json;
|
| 25 |
+
application/xml xml;
|
| 26 |
+
application/rss+xml rss;
|
| 27 |
+
text/plain txt;
|
| 28 |
+
}
|
| 29 |
|
| 30 |
# Improve performance and security
|
| 31 |
gzip on;
|