Migrate to fastapi, add button download serie

This commit is contained in:
Lovi 2024-12-10 15:30:03 +01:00
parent cff39796fc
commit 5969fb2f00
12 changed files with 606 additions and 364 deletions

2
.gitignore vendored
View File

@ -23,6 +23,7 @@ share/python-wheels/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST MANIFEST
*.pyc
# PyInstaller # PyInstaller
*.manifest *.manifest
@ -34,6 +35,7 @@ note.txt
list_proxy.txt list_proxy.txt
cmd.txt cmd.txt
downloaded_files downloaded_files
key.t
# Cache # Cache
__pycache__/ __pycache__/

View File

@ -1,16 +1,16 @@
Per testare Per testare
1. Installare requirements 1. Installare requirements
2. Inserire url per mongodb e creare database 1. Installare node js: https://nodejs.org/en/download/package-manager
3. Runnare server.py 2. Inserire url per mongodb. Pagina login: https://account.mongodb.com/account/login
3. Eseguire server.py: python server.py
4. Spostarsi su client\dashboard 4. Spostarsi su client\dashboard
5. Eseguire npm install, npm run build, npm install -g serve 5. Eseguire npm install, npm run build, npm install -g serve, serve -s build
Cosa da fare Cosa da fare:
- Aggiungere documentazione - Aggiungere documentazione
- Bottone download intera stagione - Migliore player quando si clicca sul button watch.
- Messaggio con richiesta se scaricare le nuove stagione quando si fa il check in watchlist
- Migliore player in case watch con bottone
- Coda di download con bottone aggiungere alla coda (complessa) - Coda di download con bottone aggiungere alla coda (complessa)
- Progresso di download (complesso)
... ...

View File

@ -87,14 +87,6 @@ def get_infoSelectTitle(url_title: str, domain: str, version: str):
if response.status_code == 200: if response.status_code == 200:
json_response = response.json()['props'] json_response = response.json()['props']
generes = []
for g in json_response["genres"]:
generes.append(g["name"])
trailer = None
if len(json_response['title']['trailers']) > 0:
trailer = f"https://www.youtube.com/watch?v={json_response['title']['trailers'][0]['youtube_id']}"
images = {} images = {}
for dict_image in json_response['title'].get('images', []): for dict_image in json_response['title'].get('images', []):
images[dict_image.get('type')] = f"https://cdn.{SITE_NAME}.{domain}/images/{dict_image.get('filename')}" images[dict_image.get('type')] = f"https://cdn.{SITE_NAME}.{domain}/images/{dict_image.get('filename')}"
@ -106,8 +98,6 @@ def get_infoSelectTitle(url_title: str, domain: str, version: str):
'plot': json_response['title']['plot'], 'plot': json_response['title']['plot'],
'type': json_response['title']['type'], 'type': json_response['title']['type'],
'season_count': json_response['title']['seasons_count'], 'season_count': json_response['title']['seasons_count'],
'generes': generes,
'trailer': trailer,
'image': images 'image': images
} }

View File

@ -0,0 +1,6 @@
export const API_BASE_URL = "http://127.0.0.1:1234";
export const API_URL = `${API_BASE_URL}/api`;
export const SERVER_WATCHLIST_URL = `${API_BASE_URL}/server/watchlist`;
export const SERVER_PATH_URL = `${API_BASE_URL}/server/path`;
export const SERVER_DELETE_URL = `${API_BASE_URL}/server/delete`;

View File

@ -3,8 +3,7 @@ import axios from 'axios';
import { Container, Button, Form, InputGroup } from 'react-bootstrap'; import { Container, Button, Form, InputGroup } from 'react-bootstrap';
import SearchBar from './SearchBar.js'; import SearchBar from './SearchBar.js';
import { API_BASE_URL } from './ApiUrl.js';
const API_BASE_URL = "http://127.0.0.1:1234";
const Dashboard = () => { const Dashboard = () => {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);

View File

@ -1,10 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import { Container, Row, Col, Card, Button, Badge, Modal } from 'react-bootstrap'; import { Container, Row, Col, Card, Button, Modal } from 'react-bootstrap'; // Aggiungi Modal qui
import { FaTrash, FaPlay } from 'react-icons/fa'; import { FaTrash, FaPlay } from 'react-icons/fa';
import { Link } from 'react-router-dom';
const API_BASE_URL = "http://127.0.0.1:1234"; import { SERVER_PATH_URL, SERVER_DELETE_URL, API_URL } from './ApiUrl';
const Downloads = () => { const Downloads = () => {
const [downloads, setDownloads] = useState([]); const [downloads, setDownloads] = useState([]);
@ -15,7 +14,7 @@ const Downloads = () => {
// Fetch all downloads // Fetch all downloads
const fetchDownloads = async () => { const fetchDownloads = async () => {
try { try {
const response = await axios.get(`${API_BASE_URL}/downloads`); const response = await axios.get(`${SERVER_PATH_URL}/getAll`);
setDownloads(response.data); setDownloads(response.data);
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
@ -27,8 +26,8 @@ const Downloads = () => {
// Delete a TV episode // Delete a TV episode
const handleDeleteEpisode = async (id, season, episode) => { const handleDeleteEpisode = async (id, season, episode) => {
try { try {
await axios.delete(`${API_BASE_URL}/deleteEpisode`, { await axios.delete(`${SERVER_DELETE_URL}/episode`, {
params: { id, season, episode } params: { series_id: id, season_number: season, episode_number: episode }
}); });
fetchDownloads(); // Refresh the list fetchDownloads(); // Refresh the list
} catch (error) { } catch (error) {
@ -39,8 +38,8 @@ const Downloads = () => {
// Delete a movie // Delete a movie
const handleDeleteMovie = async (id) => { const handleDeleteMovie = async (id) => {
try { try {
await axios.delete(`${API_BASE_URL}/deleteMovie`, { await axios.delete(`${SERVER_DELETE_URL}/movie`, {
params: { id } params: { movie_id: id }
}); });
fetchDownloads(); // Refresh the list fetchDownloads(); // Refresh the list
} catch (error) { } catch (error) {
@ -111,13 +110,6 @@ const Downloads = () => {
> >
<FaPlay className="me-2" /> Watch <FaPlay className="me-2" /> Watch
</Button> </Button>
<Link
to={`/title/${movie.slug}`}
state={{ url: movie.slug }}
className="btn btn-secondary btn-sm ms-2"
>
View Details
</Link>
</Card.Body> </Card.Body>
</Card> </Card>
</Col> </Col>
@ -160,13 +152,6 @@ const Downloads = () => {
> >
<FaPlay className="me-2" /> Watch <FaPlay className="me-2" /> Watch
</Button> </Button>
<Link
to={`/title/${slug}`}
state={{ url: slug }}
className="btn btn-secondary btn-sm ms-2"
>
View Details
</Link>
</Card.Body> </Card.Body>
</Card> </Card>
</Col> </Col>
@ -181,7 +166,7 @@ const Downloads = () => {
<Modal show={showPlayer} onHide={() => setShowPlayer(false)} size="lg" centered> <Modal show={showPlayer} onHide={() => setShowPlayer(false)} size="lg" centered>
<Modal.Body> <Modal.Body>
<video <video
src={`http://127.0.0.1:1234/downloaded/${currentVideo}`} src={`${API_URL}/downloaded/${currentVideo}`}
controls controls
autoPlay autoPlay
style={{ width: '100%' }} style={{ width: '100%' }}

View File

@ -4,8 +4,7 @@ import axios from 'axios';
import { Container, Row, Col, Card, Spinner } from 'react-bootstrap'; import { Container, Row, Col, Card, Spinner } from 'react-bootstrap';
import SearchBar from './SearchBar.js'; import SearchBar from './SearchBar.js';
import { API_URL } from './ApiUrl.js';
const API_BASE_URL = "http://127.0.0.1:1234";
const SearchResults = () => { const SearchResults = () => {
const [results, setResults] = useState([]); const [results, setResults] = useState([]);
@ -20,7 +19,7 @@ const SearchResults = () => {
const fetchSearchResults = async () => { const fetchSearchResults = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await axios.get(`${API_BASE_URL}/api/search`, { const response = await axios.get(`${API_URL}/search`, {
params: { q: query } params: { q: query }
}); });
setResults(response.data); setResults(response.data);

View File

@ -5,8 +5,7 @@ import { Container, Row, Col, Image, Button, Dropdown, Modal, Alert } from 'reac
import { FaDownload, FaPlay, FaPlus, FaTrash } from 'react-icons/fa'; import { FaDownload, FaPlay, FaPlus, FaTrash } from 'react-icons/fa';
import SearchBar from './SearchBar.js'; import SearchBar from './SearchBar.js';
import { API_BASE_URL, API_URL, SERVER_DELETE_URL, SERVER_PATH_URL, SERVER_WATCHLIST_URL} from './ApiUrl.js';
const API_BASE_URL = "http://127.0.0.1:1234";
const TitleDetail = () => { const TitleDetail = () => {
const [titleDetails, setTitleDetails] = useState(null); const [titleDetails, setTitleDetails] = useState(null);
@ -16,6 +15,7 @@ const TitleDetail = () => {
const [hoveredEpisode, setHoveredEpisode] = useState(null); const [hoveredEpisode, setHoveredEpisode] = useState(null);
const [isInWatchlist, setIsInWatchlist] = useState(false); const [isInWatchlist, setIsInWatchlist] = useState(false);
const [downloadStatus, setDownloadStatus] = useState({}); const [downloadStatus, setDownloadStatus] = useState({});
const [downloadingAllSeason, setDownloadingAllSeason] = useState(false);
const [showPlayer, setShowPlayer] = useState(false); const [showPlayer, setShowPlayer] = useState(false);
const [currentVideo, setCurrentVideo] = useState(""); const [currentVideo, setCurrentVideo] = useState("");
const location = useLocation(); const location = useLocation();
@ -27,7 +27,7 @@ const TitleDetail = () => {
const titleUrl = location.state?.url || location.pathname.split('/title/')[1]; const titleUrl = location.state?.url || location.pathname.split('/title/')[1];
// Fetch title information // Fetch title information
const response = await axios.get(`${API_BASE_URL}/api/getInfo`, { const response = await axios.get(`${API_URL}/getInfo`, {
params: { url: titleUrl } params: { url: titleUrl }
}); });
@ -70,7 +70,7 @@ const TitleDetail = () => {
} }
}); });
} else if (titleData.type === 'tv') { } else if (titleData.type === 'tv') {
const response = await axios.get(`${API_BASE_URL}/downloads`); const response = await axios.get(`${SERVER_PATH_URL}/getAll`);
const downloadedEpisodes = response.data.filter( const downloadedEpisodes = response.data.filter(
download => download.type === 'tv' && download.slug === titleData.slug download => download.type === 'tv' && download.slug === titleData.slug
); );
@ -92,7 +92,7 @@ const TitleDetail = () => {
// Check watchlist status // Check watchlist status
const checkWatchlistStatus = async (slug) => { const checkWatchlistStatus = async (slug) => {
try { try {
const response = await axios.get(`${API_BASE_URL}/api/getWatchlist`); const response = await axios.get(`${SERVER_WATCHLIST_URL}/get`);
const inWatchlist = response.data.some(item => item.name === slug); const inWatchlist = response.data.some(item => item.name === slug);
setIsInWatchlist(inWatchlist); setIsInWatchlist(inWatchlist);
} catch (error) { } catch (error) {
@ -104,7 +104,7 @@ const TitleDetail = () => {
if (titleDetails.type === 'tv') { if (titleDetails.type === 'tv') {
try { try {
setLoading(true); setLoading(true);
const seasonResponse = await axios.get(`${API_BASE_URL}/api/getInfoSeason`, { const seasonResponse = await axios.get(`${API_URL}/getInfoSeason`, {
params: { params: {
url: location.state?.url, url: location.state?.url,
n: seasonNumber n: seasonNumber
@ -123,7 +123,7 @@ const TitleDetail = () => {
const handleDownloadFilm = async () => { const handleDownloadFilm = async () => {
try { try {
const response = await axios.get(`${API_BASE_URL}/downloadFilm`, { const response = await axios.get(`${API_URL}/download/film`, {
params: { params: {
id: titleDetails.id, id: titleDetails.id,
slug: titleDetails.slug slug: titleDetails.slug
@ -146,7 +146,7 @@ const TitleDetail = () => {
const handleDownloadEpisode = async (seasonNumber, episodeNumber) => { const handleDownloadEpisode = async (seasonNumber, episodeNumber) => {
try { try {
const response = await axios.get(`${API_BASE_URL}/downloadEpisode`, { const response = await axios.get(`${API_URL}/download/episode`, {
params: { params: {
n_s: seasonNumber, n_s: seasonNumber,
n_ep: episodeNumber n_ep: episodeNumber
@ -170,13 +170,52 @@ const TitleDetail = () => {
} }
}; };
const handleDownloadAllSeason = async () => {
if (!titleDetails || titleDetails.type !== 'tv') return;
try {
setDownloadingAllSeason(true);
// Download all episodes in the current season
for (const episode of episodes) {
try {
const response = await axios.get(`${API_URL}/download/episode`, {
params: {
n_s: selectedSeason,
n_ep: episode.number
}
});
const videoPath = response.data.path;
// Update download status for this specific episode
setDownloadStatus(prev => ({
tv: {
...prev.tv,
[`S${selectedSeason}E${episode.number}`]: {
downloaded: true,
path: videoPath
}
}
}));
} catch (error) {
console.error(`Error downloading episode ${episode.number}:`, error);
}
}
} catch (error) {
console.error("Error downloading all season episodes:", error);
alert("Error downloading season. Some episodes might not have downloaded.");
} finally {
setDownloadingAllSeason(false);
}
};
const handleWatchVideo = async (videoPath) => { const handleWatchVideo = async (videoPath) => {
if (!videoPath) { if (!videoPath) {
// If no path provided, attempt to get path from downloads // If no path provided, attempt to get path from downloads
try { try {
let path; let path;
if (titleDetails.type === 'movie') { if (titleDetails.type === 'movie') {
const response = await axios.get(`${API_BASE_URL}/moviePath`, { const response = await axios.get(`${SERVER_PATH_URL}/movie`, {
params: { id: titleDetails.id } params: { id: titleDetails.id }
}); });
path = response.data.path; path = response.data.path;
@ -198,7 +237,7 @@ const TitleDetail = () => {
const handleAddToWatchlist = async () => { const handleAddToWatchlist = async () => {
try { try {
await axios.post(`${API_BASE_URL}/api/addWatchlist`, { await axios.post(`${SERVER_WATCHLIST_URL}/add`, {
name: titleDetails.slug, name: titleDetails.slug,
url: location.state?.url || location.pathname.split('/title/')[1], url: location.state?.url || location.pathname.split('/title/')[1],
season: titleDetails.season_count season: titleDetails.season_count
@ -212,7 +251,7 @@ const TitleDetail = () => {
const handleRemoveFromWatchlist = async () => { const handleRemoveFromWatchlist = async () => {
try { try {
await axios.post(`${API_BASE_URL}/api/removeWatchlist`, { await axios.post(`${SERVER_WATCHLIST_URL}/remove`, {
name: titleDetails.slug name: titleDetails.slug
}); });
setIsInWatchlist(false); setIsInWatchlist(false);
@ -318,11 +357,12 @@ const TitleDetail = () => {
<> <>
<Row className="mb-3"> <Row className="mb-3">
<Col> <Col>
<div className="d-flex align-items-center">
{/* Dropdown per la selezione della stagione */}
<Dropdown> <Dropdown>
<Dropdown.Toggle variant="secondary"> <Dropdown.Toggle variant="secondary">
Season {selectedSeason} Season {selectedSeason}
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu> <Dropdown.Menu>
{[...Array(titleDetails.season_count)].map((_, index) => ( {[...Array(titleDetails.season_count)].map((_, index) => (
<Dropdown.Item <Dropdown.Item
@ -334,6 +374,17 @@ const TitleDetail = () => {
))} ))}
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
{/* Bottone per il download con margine a sinistra */}
<Button
variant="secondary"
className="ms-3"
onClick={handleDownloadAllSeason}
disabled={downloadingAllSeason}
>
{downloadingAllSeason ? 'Downloading...' : `Download Season ${selectedSeason}`}
</Button>
</div>
</Col> </Col>
</Row> </Row>
@ -393,7 +444,7 @@ const TitleDetail = () => {
<Modal show={showPlayer} onHide={() => setShowPlayer(false)} size="lg" centered> <Modal show={showPlayer} onHide={() => setShowPlayer(false)} size="lg" centered>
<Modal.Body> <Modal.Body>
<video <video
src={`http://127.0.0.1:1234/downloaded/${currentVideo}`} src={`${API_URL}/downloaded/${currentVideo}`}
controls controls
autoPlay autoPlay
style={{ width: '100%' }} style={{ width: '100%' }}

View File

@ -1,10 +1,10 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import { Container, Row, Col, Card, Button, Badge, Alert } from 'react-bootstrap'; import { Container, Row, Col, Card, Button, Badge, Alert, Modal } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { FaTrash } from 'react-icons/fa'; import { FaTrash, FaDownload } from 'react-icons/fa';
const API_BASE_URL = "http://127.0.0.1:1234"; import { SERVER_WATCHLIST_URL, API_URL } from './ApiUrl';
const Watchlist = () => { const Watchlist = () => {
const [watchlistItems, setWatchlistItems] = useState([]); const [watchlistItems, setWatchlistItems] = useState([]);
@ -12,10 +12,19 @@ const Watchlist = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [newSeasonsMessage, setNewSeasonsMessage] = useState(""); // Stato per il messaggio delle nuove stagioni const [newSeasonsMessage, setNewSeasonsMessage] = useState(""); // Stato per il messaggio delle nuove stagioni
// Nuovo stato per la gestione del modal di download
const [showDownloadModal, setShowDownloadModal] = useState(false);
const [seasonsToDownload, setSeasonsToDownload] = useState([]);
const [downloadProgress, setDownloadProgress] = useState({
status: 'idle', // 'idle', 'downloading', 'completed', 'error'
current: 0,
total: 0
});
// Funzione per recuperare i dati della watchlist // Funzione per recuperare i dati della watchlist
const fetchWatchlistData = async () => { const fetchWatchlistData = async () => {
try { try {
const watchlistResponse = await axios.get(`${API_BASE_URL}/api/getWatchlist`); const watchlistResponse = await axios.get(`${SERVER_WATCHLIST_URL}/get`);
setWatchlistItems(watchlistResponse.data); setWatchlistItems(watchlistResponse.data);
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
@ -27,27 +36,22 @@ const Watchlist = () => {
// Funzione per controllare se ci sono nuove stagioni (attivata dal bottone) // Funzione per controllare se ci sono nuove stagioni (attivata dal bottone)
const checkNewSeasons = async () => { const checkNewSeasons = async () => {
try { try {
const newSeasonsResponse = await axios.get(`${API_BASE_URL}/api/checkWatchlist`); const newSeasonsResponse = await axios.get(`${SERVER_WATCHLIST_URL}/checkNewSeason`);
if (Array.isArray(newSeasonsResponse.data)) { if (Array.isArray(newSeasonsResponse.data) && newSeasonsResponse.data.length > 0) {
setNewSeasons(newSeasonsResponse.data); setNewSeasons(newSeasonsResponse.data);
setSeasonsToDownload(newSeasonsResponse.data);
setShowDownloadModal(true);
// Crea un messaggio per i titoli con nuove stagioni
const titlesWithNewSeasons = newSeasonsResponse.data.map(season => season.name); const titlesWithNewSeasons = newSeasonsResponse.data.map(season => season.name);
if (titlesWithNewSeasons.length > 0) {
setNewSeasonsMessage(`Nuove stagioni disponibili per: ${titlesWithNewSeasons.join(", ")}`); setNewSeasonsMessage(`Nuove stagioni disponibili per: ${titlesWithNewSeasons.join(", ")}`);
// Dopo aver mostrato il messaggio, aggiorniamo i titoli con le nuove stagioni
updateTitlesWithNewSeasons(newSeasonsResponse.data);
} else { } else {
setNewSeasonsMessage("Nessuna nuova stagione disponibile."); setNewSeasons([]);
}
} else {
setNewSeasons([]); // In caso contrario, non ci sono nuove stagioni
setNewSeasonsMessage("Nessuna nuova stagione disponibile."); setNewSeasonsMessage("Nessuna nuova stagione disponibile.");
} }
} catch (error) { } catch (error) {
console.error("Error fetching new seasons:", error); console.error("Error fetching new seasons:", error);
setNewSeasonsMessage("Errore nel recuperare le nuove stagioni.");
} }
}; };
@ -58,7 +62,7 @@ const Watchlist = () => {
// Manda una richiesta POST per ogni titolo con nuove stagioni // Manda una richiesta POST per ogni titolo con nuove stagioni
console.log(`Updated watchlist for ${season.name} with new season ${season.nNewSeason}, url: ${season.title_url}`); console.log(`Updated watchlist for ${season.name} with new season ${season.nNewSeason}, url: ${season.title_url}`);
await axios.post(`${API_BASE_URL}/api/updateTitleWatchlist`, { await axios.post(`${SERVER_WATCHLIST_URL}/update`, {
url: season.title_url, url: season.title_url,
season: season.season season: season.season
}); });
@ -69,10 +73,76 @@ const Watchlist = () => {
} }
}; };
const downloadNewSeasons = async () => {
try {
setDownloadProgress({
status: 'downloading',
current: 0,
total: seasonsToDownload.length
});
for (const [index, season] of seasonsToDownload.entries()) {
try {
// Request complete series information
const seriesInfoResponse = await axios.get(`${API_URL}/getInfo`, {
params: { url: season.title_url }
});
// Download entire season
const seasonResponse = await axios.get(`${API_URL}/getInfoSeason`, {
params: {
url: season.title_url,
n: season.nNewSeason
}
});
// Download each episode of the season
for (const episode of seasonResponse.data) {
await axios.get(`${API_URL}/download/episode`, {
params: {
n_s: season.nNewSeason,
n_ep: episode.number,
media_id: seriesInfoResponse.data.id,
series_name: seriesInfoResponse.data.slug
}
});
}
// Update watchlist with new season
await axios.post(`${SERVER_WATCHLIST_URL}/update`, {
url: season.title_url,
season: season.season
});
// Update progress
setDownloadProgress(prev => ({
...prev,
current: index + 1,
status: index + 1 === seasonsToDownload.length ? 'completed' : 'downloading'
}));
} catch (error) {
console.error(`Errore durante lo scaricamento della stagione ${season.name}:`, error);
}
}
// Close modal after completion
setShowDownloadModal(false);
// Reload watchlist to show updates
fetchWatchlistData();
} catch (error) {
console.error("Errore durante lo scaricamento delle nuove stagioni:", error);
setDownloadProgress({
status: 'error',
current: 0,
total: 0
});
}
};
// Funzione per rimuovere un elemento dalla watchlist // Funzione per rimuovere un elemento dalla watchlist
const handleRemoveFromWatchlist = async (serieName) => { const handleRemoveFromWatchlist = async (serieName) => {
try { try {
await axios.post(`${API_BASE_URL}/api/removeWatchlist`, { name: serieName }); await axios.post(`${SERVER_WATCHLIST_URL}/remove`, { name: serieName });
// Aggiorna lo stato locale per rimuovere l'elemento dalla watchlist // Aggiorna lo stato locale per rimuovere l'elemento dalla watchlist
setWatchlistItems((prev) => prev.filter((item) => item.name !== serieName)); setWatchlistItems((prev) => prev.filter((item) => item.name !== serieName));
@ -156,6 +226,57 @@ const Watchlist = () => {
</Row> </Row>
)} )}
</Container> </Container>
<Modal show={showDownloadModal} onHide={() => setShowDownloadModal(false)}>
<Modal.Header closeButton>
<Modal.Title>Nuove Stagioni Disponibili</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Sono disponibili nuove stagioni per i seguenti titoli:</p>
<ul>
{seasonsToDownload.map((season) => (
<li key={season.name}>
{season.name.replace(/-/g, ' ')} - Stagione {season.nNewSeason}
</li>
))}
</ul>
{downloadProgress.status !== 'idle' && (
<div className="mt-3">
<p>Progresso download:</p>
<div className="progress">
<div
className={`progress-bar ${
downloadProgress.status === 'completed' ? 'bg-success' :
downloadProgress.status === 'error' ? 'bg-danger' : 'bg-primary'
}`}
role="progressbar"
style={{width: `${(downloadProgress.current / downloadProgress.total) * 100}%`}}
aria-valuenow={(downloadProgress.current / downloadProgress.total) * 100}
aria-valuemin="0"
aria-valuemax="100"
>
{downloadProgress.current}/{downloadProgress.total} Stagioni
</div>
</div>
</div>
)}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => setShowDownloadModal(false)}>
Annulla
</Button>
<Button
variant="primary"
onClick={downloadNewSeasons}
disabled={downloadProgress.status === 'downloading'}
>
<FaDownload className="me-2" />
Scarica Nuove Stagioni
</Button>
</Modal.Footer>
</Modal>
</Container> </Container>
); );
}; };

View File

@ -1,22 +1,12 @@
{ {
"DEFAULT": { "DEFAULT": {
"debug": false, "debug": false,
"log_file": "app.log",
"log_to_file": true,
"show_message": true, "show_message": true,
"clean_console": true,
"root_path": "Video", "root_path": "Video",
"movie_folder_name": "Movie", "movie_folder_name": "Movie",
"serie_folder_name": "TV", "serie_folder_name": "TV",
"map_episode_name": "%(tv_name)_S%(season)E%(episode)_%(episode_name)", "map_episode_name": "%(tv_name)_S%(season)E%(episode)_%(episode_name)",
"config_qbit_tor": { "clean_console": true
"host": "192.168.1.59",
"port": "8080",
"user": "admin",
"pass": "adminadmin"
},
"not_close": false,
"show_trending": false
}, },
"REQUESTS": { "REQUESTS": {
"timeout": 20, "timeout": 20,
@ -26,9 +16,6 @@
"proxy_start_max": 0.5, "proxy_start_max": 0.5,
"user-agent": "" "user-agent": ""
}, },
"BROWSER": {
"headless": false
},
"M3U8_DOWNLOAD": { "M3U8_DOWNLOAD": {
"tqdm_delay": 0.01, "tqdm_delay": 0.01,
"tqdm_use_large_bar": true, "tqdm_use_large_bar": true,

View File

@ -13,4 +13,4 @@ qbittorrent-api
python-qbittorrent python-qbittorrent
googlesearch-python googlesearch-python
pymongo pymongo
flask fastapi

538
server.py
View File

@ -1,17 +1,23 @@
import os import os
import logging import logging
import shutil
import datetime import datetime
from urllib.parse import urlparse from urllib.parse import urlparse
from pymongo import MongoClient from pymongo import MongoClient
from urllib.parse import unquote from urllib.parse import unquote
from flask_cors import CORS from pydantic import BaseModel
from flask import Flask, jsonify, request from typing import List, Optional
from flask import send_from_directory
# Fast api
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
# Util # Util
from StreamingCommunity.Util._jsonConfig import config_manager from StreamingCommunity.Util._jsonConfig import config_manager
# Internal # Internal
from StreamingCommunity.Api.Template.Class.SearchType import MediaItem from StreamingCommunity.Api.Template.Class.SearchType import MediaItem
from StreamingCommunity.Api.Site.streamingcommunity.api import get_version_and_domain, search_titles, get_infoSelectTitle, get_infoSelectSeason from StreamingCommunity.Api.Site.streamingcommunity.api import get_version_and_domain, search_titles, get_infoSelectTitle, get_infoSelectSeason
@ -19,13 +25,23 @@ from StreamingCommunity.Api.Site.streamingcommunity.film import download_film
from StreamingCommunity.Api.Site.streamingcommunity.series import download_video from StreamingCommunity.Api.Site.streamingcommunity.series import download_video
from StreamingCommunity.Api.Site.streamingcommunity.util.ScrapeSerie import ScrapeSerie from StreamingCommunity.Api.Site.streamingcommunity.util.ScrapeSerie import ScrapeSerie
# Player # Player
from StreamingCommunity.Api.Player.vixcloud import VideoSource from StreamingCommunity.Api.Player.vixcloud import VideoSource
# Variable
app = Flask(__name__)
CORS(app)
# Variable
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Site variable
version, domain = get_version_and_domain() version, domain = get_version_and_domain()
season_name = None season_name = None
scrape_serie = ScrapeSerie("streamingcommunity") scrape_serie = ScrapeSerie("streamingcommunity")
@ -33,6 +49,7 @@ video_source = VideoSource("streamingcommunity", True)
DOWNLOAD_DIRECTORY = os.getcwd() DOWNLOAD_DIRECTORY = os.getcwd()
# Mongo
client = MongoClient(config_manager.get("EXTRA", "mongodb")) client = MongoClient(config_manager.get("EXTRA", "mongodb"))
db = client[config_manager.get("EXTRA", "database")] db = client[config_manager.get("EXTRA", "database")]
watchlist_collection = db['watchlist'] watchlist_collection = db['watchlist']
@ -40,9 +57,20 @@ downloads_collection = db['downloads']
def update_domain(url: str):
parsed_url = urlparse(url)
hostname = parsed_url.hostname
domain_part = hostname.split('.')[1]
new_url = url.replace(domain_part, domain)
return new_url
# ---------- SITE API ------------ # ---------- SITE API ------------
@app.route('/') @app.get("/", summary="Health Check")
def index(): async def index():
""" """
Health check endpoint to confirm server is operational. Health check endpoint to confirm server is operational.
@ -50,51 +78,56 @@ def index():
str: Operational status message str: Operational status message
""" """
logging.info("Health check endpoint accessed") logging.info("Health check endpoint accessed")
return 'Server is operational' return {"status": "Server is operational"}
@app.route('/api/search', methods=['GET']) @app.get("/api/search", summary="Search Titles")
def get_list_search(): async def get_list_search(q):
""" """
Search for titles based on query parameter. Search for titles based on query parameter.
Args:
q (str, optional): Search query parameter
Returns: Returns:
JSON response with search results or error message JSON response with search results
""" """
try: if not q:
query = request.args.get('q')
if not query:
logging.warning("Search request without query parameter") logging.warning("Search request without query parameter")
return jsonify({'error': 'Missing query parameter'}), 400 raise HTTPException(status_code=400, detail="Missing query parameter")
result = search_titles(query, domain) try:
logging.info(f"Search performed for query: {query}") result = search_titles(q, domain)
return jsonify(result), 200 logging.info(f"Search performed for query: {q}")
return result
except Exception as e: except Exception as e:
logging.error(f"Error in search: {str(e)}", exc_info=True) logging.error(f"Error in search: {str(e)}", exc_info=True)
return jsonify({'error': 'Internal server error'}), 500 raise HTTPException(status_code=500, detail="Internal server error")
@app.route('/api/getInfo', methods=['GET']) @app.get("/api/getInfo", summary="Get Title Information")
def get_info_title(): async def get_info_title(url):
""" """
Retrieve information for a specific title. Retrieve information for a specific title.
Args:
url (str, optional): Title URL parameter
Returns: Returns:
JSON response with title information or error message JSON response with title information
""" """
try: if not url:
title_url = request.args.get('url')
if not title_url:
logging.warning("GetInfo request without URL parameter") logging.warning("GetInfo request without URL parameter")
return jsonify({'error': 'Missing URL parameter'}), 400 raise HTTPException(status_code=400, detail="Missing URL parameter")
result = get_infoSelectTitle(title_url, domain, version) try:
result = get_infoSelectTitle(update_domain(url), domain, version)
# Global state management for TV series
if result.get('type') == "tv": if result.get('type') == "tv":
global season_name, scrape_serie, video_source global season_name, scrape_serie, video_source
season_name = result.get('slug') season_name = result.get('slug')
# Setup for TV series (adjust based on your actual implementation)
scrape_serie.setup( scrape_serie.setup(
version=version, version=version,
media_id=int(result.get('id')), media_id=int(result.get('id')),
@ -104,38 +137,39 @@ def get_info_title():
logging.info(f"TV series info retrieved: {season_name}") logging.info(f"TV series info retrieved: {season_name}")
return jsonify(result), 200 return result
except Exception as e: except Exception as e:
logging.error(f"Error retrieving title info: {str(e)}", exc_info=True) logging.error(f"Error retrieving title info: {str(e)}", exc_info=True)
return jsonify({'error': 'Failed to retrieve title information'}), 500 raise HTTPException(status_code=500, detail="Failed to retrieve title information")
@app.route('/api/getInfoSeason', methods=['GET']) @app.get("/api/getInfoSeason", summary="Get Season Information")
def get_info_season(): async def get_info_season(url, n):
""" """
Retrieve season information for a specific title. Retrieve season information for a specific title.
Args:
url (str, optional): Title URL parameter
n (str, optional): Season number
Returns: Returns:
JSON response with season information or error message JSON response with season information
""" """
try: if not url or not n:
title_url = request.args.get('url')
number_season = request.args.get('n')
if not title_url or not number_season:
logging.warning("GetInfoSeason request with missing parameters") logging.warning("GetInfoSeason request with missing parameters")
return jsonify({'error': 'Missing URL or season number'}), 400 raise HTTPException(status_code=400, detail="Missing URL or season number")
result = get_infoSelectSeason(title_url, number_season, domain, version) try:
logging.info(f"Season info retrieved for season {number_season}") result = get_infoSelectSeason(update_domain(url), n, domain, version)
return jsonify(result), 200 logging.info(f"Season info retrieved for season {n}")
return result
except Exception as e: except Exception as e:
logging.error(f"Error retrieving season info: {str(e)}", exc_info=True) logging.error(f"Error retrieving season info: {str(e)}", exc_info=True)
return jsonify({'error': 'Failed to retrieve season information'}), 500 raise HTTPException(status_code=500, detail="Failed to retrieve season information")
@app.route('/api/getdomain', methods=['GET']) @app.get("/api/getDomain", summary="Get Current Domain")
def get_domain(): async def get_domain():
""" """
Retrieve current domain and version. Retrieve current domain and version.
@ -146,37 +180,38 @@ def get_domain():
global version, domain global version, domain
version, domain = get_version_and_domain() version, domain = get_version_and_domain()
logging.info(f"Domain retrieved: {domain}, Version: {version}") logging.info(f"Domain retrieved: {domain}, Version: {version}")
return jsonify({'domain': domain, 'version': version}), 200 return {"domain": domain, "version": version}
except Exception as e: except Exception as e:
logging.error(f"Error retrieving domain: {str(e)}", exc_info=True) logging.error(f"Error retrieving domain: {str(e)}", exc_info=True)
return jsonify({'error': 'Failed to retrieve domain information'}), 500 raise HTTPException(status_code=500, detail="Failed to retrieve domain information")
# ---------- DOWNLOAD API ------------ # ---------- CALL DOWNLOAD API ------------
@app.route('/downloadFilm', methods=['GET']) @app.get("/api/download/film", summary="Download Film")
def call_download_film(): async def call_download_film(id, slug):
""" """
Download a film by its ID and slug. Download a film by its ID and slug.
Args:
id (str, optional): Film ID
slug (str, optional): Film slug
Returns: Returns:
JSON response with download path or error message JSON response with download path or error message
""" """
try: if not id or not slug:
film_id = request.args.get('id')
slug = request.args.get('slug')
if not film_id or not slug:
logging.warning("Download film request with missing parameters") logging.warning("Download film request with missing parameters")
return jsonify({'error': 'Missing film ID or slug'}), 400 raise HTTPException(status_code=400, detail="Missing film ID or slug")
item_media = MediaItem(**{'id': film_id, 'slug': slug}) try:
item_media = MediaItem(**{'id': id, 'slug': slug})
path_download = download_film(item_media) path_download = download_film(item_media)
download_data = { download_data = {
'type': 'movie', 'type': 'movie',
'id': film_id, 'id': id,
'slug': slug, 'slug': slug,
'path': path_download, 'path': path_download,
'timestamp': datetime.datetime.now(datetime.timezone.utc) 'timestamp': datetime.datetime.now(datetime.timezone.utc)
@ -184,44 +219,58 @@ def call_download_film():
downloads_collection.insert_one(download_data) downloads_collection.insert_one(download_data)
logging.info(f"Film downloaded: {slug}") logging.info(f"Film downloaded: {slug}")
return jsonify({'path': path_download}), 200 return {"path": path_download}
except Exception as e: except Exception as e:
logging.error(f"Error downloading film: {str(e)}", exc_info=True) logging.error(f"Error downloading film: {str(e)}", exc_info=True)
return jsonify({'error': 'Failed to download film'}), 500 raise HTTPException(status_code=500, detail="Failed to download film")
@app.route('/downloadEpisode', methods=['GET']) @app.get("/api/download/episode", summary="Download TV Episode")
def call_download_episode(): async def call_download_episode(n_s: str, n_ep: str, media_id: Optional[int] = None, series_name: Optional[str] = None):
""" """
Download a specific TV series episode. Download a specific TV series episode.
Args:
n_s (str, optional): Season number
n_ep (str, optional): Episode number
media_id (int, optional): Media ID of the series
series_name (str, optional): Series slug
Returns: Returns:
JSON response with download path or error message JSON response with download path or error message
""" """
try: if not n_s or not n_ep:
season_number = request.args.get('n_s')
episode_number = request.args.get('n_ep')
if not season_number or not episode_number:
logging.warning("Download episode request with missing parameters") logging.warning("Download episode request with missing parameters")
return jsonify({'error': 'Missing season or episode number'}), 400 raise HTTPException(status_code=400, detail="Missing season or episode number")
season_number = int(season_number) try:
episode_number = int(episode_number) season_number = int(n_s)
episode_number = int(n_ep)
# Se i parametri opzionali sono presenti, impostare la serie con setup
if media_id is not None and series_name is not None:
scrape_serie.setup(
version=version,
media_id=media_id,
series_name=series_name
)
else:
scrape_serie.collect_title_season(season_number) scrape_serie.collect_title_season(season_number)
# Scaricare il video
path_download = download_video( path_download = download_video(
season_name, scrape_serie.series_name,
season_number, season_number,
episode_number, episode_number,
scrape_serie, scrape_serie,
video_source video_source
) )
# Salvare i dati del download
download_data = { download_data = {
'type': 'tv', 'type': 'tv',
'id': scrape_serie.media_id, 'id': media_id if media_id else scrape_serie.media_id,
'slug': scrape_serie.series_name, 'slug': series_name if series_name else scrape_serie.series_name,
'n_s': season_number, 'n_s': season_number,
'n_ep': episode_number, 'n_ep': episode_number,
'path': path_download, 'path': path_download,
@ -230,21 +279,24 @@ def call_download_episode():
downloads_collection.insert_one(download_data) downloads_collection.insert_one(download_data)
logging.info(f"Episode downloaded: S{season_number}E{episode_number}") logging.info(f"Episode downloaded: S{season_number}E{episode_number}")
return jsonify({'path': path_download}), 200 return {"path": path_download}
except ValueError: except ValueError:
logging.error("Invalid season or episode number format") logging.error("Invalid season or episode number format")
return jsonify({'error': 'Invalid season or episode number'}), 400 raise HTTPException(status_code=400, detail="Invalid season or episode number")
except Exception as e: except Exception as e:
logging.error(f"Error downloading episode: {str(e)}", exc_info=True) logging.error(f"Error downloading episode: {str(e)}", exc_info=True)
return jsonify({'error': 'Failed to download episode'}), 500 raise HTTPException(status_code=500, detail="Failed to download episode")
@app.route('/downloaded/<path:filename>', methods=['GET']) @app.get("/api/downloaded/{filename:path}", summary="Serve Downloaded Files")
def serve_downloaded_file(filename): async def serve_downloaded_file(filename: str):
""" """
Serve downloaded files with proper URL decoding and error handling. Serve downloaded files with proper URL decoding and error handling.
Args:
filename (str): Encoded filename path
Returns: Returns:
Downloaded file or error message Downloaded file or error message
""" """
@ -260,94 +312,163 @@ def serve_downloaded_file(filename):
# Verify file exists # Verify file exists
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
logging.warning(f"File not found: {decoded_filename}") logging.warning(f"File not found: {decoded_filename}")
return jsonify({'error': 'File not found'}), 404 HTTPException(status_code=404, detail="File not found")
# Serve the file # Serve the file
return send_from_directory(DOWNLOAD_DIRECTORY, decoded_filename, as_attachment=False) return FileResponse(
path=file_path,
filename=decoded_filename,
media_type='application/octet-stream'
)
except Exception as e: except Exception as e:
logging.error(f"Error serving file: {str(e)}", exc_info=True) logging.error(f"Error serving file: {str(e)}", exc_info=True)
return jsonify({'error': 'Internal server error'}), 500 raise HTTPException(status_code=500, detail="Internal server error")
# ---------- WATCHLIST MONGO ------------ # ---------- WATCHLIST MONGO ------------
@app.route('/api/addWatchlist', methods=['POST']) class WatchlistItem(BaseModel):
def add_to_watchlist(): name: str
title_name = request.json.get('name') url: str
title_url = request.json.get('url') season: int
season = request.json.get('season')
if title_url and season: class UpdateWatchlistItem(BaseModel):
url: str
season: int
class RemoveWatchlistItem(BaseModel):
name: str
@app.post("/server/watchlist/add", summary="Add Item to Watchlist")
async def add_to_watchlist(item: WatchlistItem):
"""
Add a new item to the watchlist.
Args:
item (WatchlistItem): Details of the item to add
Returns:
JSON response with success or error message
"""
try:
# Check if item already exists
existing_item = watchlist_collection.find_one({
'name': item.name,
'title_url': item.url,
'season': item.season
})
existing_item = watchlist_collection.find_one({'name': title_name, 'url': title_url, 'season': season})
if existing_item: if existing_item:
return jsonify({'message': 'Il titolo è già nella watchlist'}), 400 raise HTTPException(status_code=400, detail="Il titolo è già nella watchlist")
# Insert new item
watchlist_collection.insert_one({ watchlist_collection.insert_one({
'name': title_name, 'name': item.name,
'title_url': title_url, 'title_url': item.url,
'season': season, 'season': item.season,
'added_on': datetime.datetime.utcnow() 'added_on': datetime.datetime.utcnow()
}) })
return jsonify({'message': 'Titolo aggiunto alla watchlist'}), 200
else:
return jsonify({'message': 'Missing title_url or season'}), 400
@app.route('/api/updateTitleWatchlist', methods=['POST']) return {"message": "Titolo aggiunto alla watchlist"}
def update_title_watchlist():
print(request.json)
title_url = request.json.get('url') except HTTPException:
new_season = request.json.get('season') raise
except Exception as e:
logging.error(f"Error adding to watchlist: {str(e)}")
raise HTTPException(status_code=500, detail="Errore interno del server")
if title_url is not None and new_season is not None: @app.post("/server/watchlist/update", summary="Update Watchlist Item")
async def update_title_watchlist(item: UpdateWatchlistItem):
"""
Update the season for an existing watchlist item.
Args:
item (UpdateWatchlistItem): Details of the item to update
Returns:
JSON response with update status
"""
try:
result = watchlist_collection.update_one( result = watchlist_collection.update_one(
{'title_url': title_url}, {'title_url': item.url},
{'$set': {'season': new_season}} {'$set': {'season': item.season}}
) )
if result.matched_count == 0: if result.matched_count == 0:
return jsonify({'message': 'Titolo non trovato nella watchlist'}), 404 raise HTTPException(status_code=404, detail="Titolo non trovato nella watchlist")
if result.modified_count == 0: if result.modified_count == 0:
return jsonify({'message': 'La stagione non è cambiata'}), 200 return {"message": "La stagione non è cambiata"}
return jsonify({'message': 'Stagione aggiornata con successo'}), 200 return {"message": "Stagione aggiornata con successo"}
else: except HTTPException:
return jsonify({'message': 'Missing title_url or season'}), 400 raise
except Exception as e:
logging.error(f"Error updating watchlist: {str(e)}")
raise HTTPException(status_code=500, detail="Errore interno del server")
@app.route('/api/removeWatchlist', methods=['POST']) @app.post("/server/watchlist/remove", summary="Remove Item from Watchlist")
def remove_from_watchlist(): async def remove_from_watchlist(item: RemoveWatchlistItem):
title_name = request.json.get('name') """
Remove an item from the watchlist.
if title_name: Args:
result = watchlist_collection.delete_one({'name': title_name}) item (RemoveWatchlistItem): Details of the item to remove
Returns:
JSON response with removal status
"""
try:
result = watchlist_collection.delete_one({'name': item.name})
if result.deleted_count == 1: if result.deleted_count == 1:
return jsonify({'message': 'Titolo rimosso dalla watchlist'}), 200 return {"message": "Titolo rimosso dalla watchlist"}
else:
return jsonify({'message': 'Titolo non trovato nella watchlist'}), 404
else:
return jsonify({'message': 'Missing title_url or season'}), 400
@app.route('/api/getWatchlist', methods=['GET']) raise HTTPException(status_code=404, detail="Titolo non trovato nella watchlist")
def get_watchlist():
watchlist_items = list(watchlist_collection.find({}, {'_id': 0}))
if watchlist_items: except HTTPException:
return jsonify(watchlist_items), 200 raise
else: except Exception as e:
return jsonify({'message': 'La watchlist è vuota'}), 200 logging.error(f"Error removing from watchlist: {str(e)}")
raise HTTPException(status_code=500, detail="Errore interno del server")
@app.route('/api/checkWatchlist', methods=['GET']) @app.get("/server/watchlist/get", summary="Get Watchlist Items")
def get_newSeason(): async def get_watchlist():
title_newSeasons = [] """
Retrieve all items in the watchlist.
Returns:
List of watchlist items or empty list message
"""
try:
watchlist_items = list(watchlist_collection.find({}, {'_id': 0})) watchlist_items = list(watchlist_collection.find({}, {'_id': 0}))
if not watchlist_items: if not watchlist_items:
return jsonify({'message': 'La watchlist è vuota'}), 200 return {"message": "La watchlist è vuota"}
return watchlist_items
except Exception as e:
logging.error(f"Error retrieving watchlist: {str(e)}")
raise HTTPException(status_code=500, detail="Errore interno del server")
@app.get("/server/watchlist/checkNewSeason", summary="Check for New Seasons")
async def get_new_seasons():
"""
Check for new seasons of watchlist items.
Returns:
List of items with new seasons or message
"""
try:
watchlist_items = list(watchlist_collection.find({}, {'_id': 0}))
if not watchlist_items:
return {"message": "La watchlist è vuota"}
title_new_seasons = []
for item in watchlist_items: for item in watchlist_items:
title_url = item.get('title_url') title_url = item.get('title_url')
@ -355,11 +476,9 @@ def get_newSeason():
continue continue
try: try:
parsed_url = urlparse(title_url) new_url = update_domain(title_url)
hostname = parsed_url.hostname
domain_part = hostname.split('.')[1]
new_url = title_url.replace(domain_part, domain)
# Fetch title info
result = get_infoSelectTitle(new_url, domain, version) result = get_infoSelectTitle(new_url, domain, version)
if not result or 'season_count' not in result: if not result or 'season_count' not in result:
@ -367,8 +486,9 @@ def get_newSeason():
number_season = result.get("season_count") number_season = result.get("season_count")
# Check for new seasons
if number_season > item.get("season"): if number_season > item.get("season"):
title_newSeasons.append({ title_new_seasons.append({
'title_url': item.get('title_url'), 'title_url': item.get('title_url'),
'name': item.get('name'), 'name': item.get('name'),
'season': int(number_season), 'season': int(number_season),
@ -376,16 +496,20 @@ def get_newSeason():
}) })
except Exception as e: except Exception as e:
print(f"Errore nel recuperare informazioni per {item.get('title_url')}: {e}") logging.error(f"Error checking seasons for {item.get('title_url')}: {str(e)}")
if title_newSeasons: if title_new_seasons:
return jsonify(title_newSeasons), 200 return title_new_seasons
else:
return jsonify({'message': 'Nessuna nuova stagione disponibile'}), 200 return {"message": "Nessuna nuova stagione disponibile"}
except Exception as e:
logging.error(f"Error in check watchlist: {str(e)}")
raise HTTPException(status_code=500, detail="Errore interno del server")
# ---------- DOWNLOAD MONGO ------------ # ---------- REMOVE DOWNLOAD FILE WITH MONGO ------------
def ensure_collections_exist(db): def ensure_collections_exist(db):
""" """
Ensures that the required collections exist in the database. Ensures that the required collections exist in the database.
@ -405,40 +529,36 @@ def ensure_collections_exist(db):
else: else:
logging.info(f"Collection already exists: {collection_name}") logging.info(f"Collection already exists: {collection_name}")
@app.route('/downloads', methods=['GET']) class Episode(BaseModel):
def fetch_all_downloads(): id: int
season: int
episode: int
class Movie(BaseModel):
id: int
# Fetch all downloads
@app.get("/server/path/getAll", response_model=List[dict], summary="Get all download from disk")
async def fetch_all_downloads():
""" """
Endpoint to fetch all downloads. Endpoint to fetch all downloads.
""" """
try: try:
downloads = list(downloads_collection.find({}, {'_id': 0})) downloads = list(downloads_collection.find({}, {'_id': 0}))
return jsonify(downloads), 200 return downloads
except Exception as e: except Exception as e:
logging.error(f"Error fetching all downloads: {str(e)}") logging.error(f"Error fetching all downloads: {str(e)}")
return [] raise HTTPException(status_code=500, detail="Error fetching all downloads")
@app.route('/deleteEpisode', methods=['DELETE']) # Remove a specific episode and its file
def remove_episode(): @app.delete("/server/delete/episode", summary="Remove episode from disk")
async def remove_episode(series_id: int, season_number: int, episode_number: int):
""" """
Endpoint to delete a specific episode and its file. Endpoint to delete a specific episode and its file.
""" """
try: try:
series_id = request.args.get('id') # Find the episode in the database
season_number = request.args.get('season')
episode_number = request.args.get('episode')
if not series_id or not season_number or not episode_number:
return jsonify({'error': 'Missing parameters (id, season, episode)'}), 400
try:
series_id = int(series_id)
season_number = int(season_number)
episode_number = int(episode_number)
except ValueError:
return jsonify({'error': 'Invalid season or episode number'}), 400
# Trova il percorso del file
episode = downloads_collection.find_one({ episode = downloads_collection.find_one({
'type': 'tv', 'type': 'tv',
'id': series_id, 'id': series_id,
@ -446,12 +566,15 @@ def remove_episode():
'n_ep': episode_number 'n_ep': episode_number
}, {'_id': 0, 'path': 1}) }, {'_id': 0, 'path': 1})
logging.info("FIND => ", episode)
if not episode or 'path' not in episode: if not episode or 'path' not in episode:
return jsonify({'error': 'Episode not found'}), 404 raise HTTPException(status_code=404, detail="Episode not found")
file_path = episode['path'] file_path = episode['path']
logging.info("PATH => ", file_path)
# Elimina il file fisico # Delete the file
try: try:
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
@ -461,7 +584,7 @@ def remove_episode():
except Exception as e: except Exception as e:
logging.error(f"Error deleting episode file: {str(e)}") logging.error(f"Error deleting episode file: {str(e)}")
# Rimuovi l'episodio dal database # Remove the episode from the database
result = downloads_collection.delete_one({ result = downloads_collection.delete_one({
'type': 'tv', 'type': 'tv',
'id': series_id, 'id': series_id,
@ -470,35 +593,31 @@ def remove_episode():
}) })
if result.deleted_count > 0: if result.deleted_count > 0:
return jsonify({'success': True}), 200 return JSONResponse(status_code=200, content={'success': True})
else: else:
return jsonify({'error': 'Failed to delete episode from database'}), 500 raise HTTPException(status_code=500, detail="Failed to delete episode from database")
except Exception as e: except Exception as e:
logging.error(f"Error deleting episode: {str(e)}") logging.error(f"Error deleting episode: {str(e)}")
return jsonify({'error': 'Failed to delete episode'}), 500 raise HTTPException(status_code=500, detail="Failed to delete episode")
@app.route('/deleteMovie', methods=['DELETE']) # Remove a specific movie, its file, and its parent folder if empty
def remove_movie(): @app.delete("/server/delete/movie", summary="Remove a movie from disk")
async def remove_movie(movie_id: int):
""" """
Endpoint to delete a specific movie, its file, and its parent folder if empty. Endpoint to delete a specific movie, its file, and its parent folder if empty.
""" """
try: try:
movie_id = request.args.get('id') # Find the movie in the database
movie = downloads_collection.find_one({'type': 'movie', 'id': str(movie_id)}, {'_id': 0, 'path': 1})
if not movie_id:
return jsonify({'error': 'Missing movie ID'}), 400
# Trova il percorso del file
movie = downloads_collection.find_one({'type': 'movie', 'id': movie_id}, {'_id': 0, 'path': 1})
if not movie or 'path' not in movie: if not movie or 'path' not in movie:
return jsonify({'error': 'Movie not found'}), 404 raise HTTPException(status_code=404, detail="Movie not found")
file_path = movie['path'] file_path = movie['path']
parent_folder = os.path.dirname(file_path) parent_folder = os.path.dirname(file_path)
# Elimina il file fisico # Delete the movie file
try: try:
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
@ -508,7 +627,7 @@ def remove_movie():
except Exception as e: except Exception as e:
logging.error(f"Error deleting movie file: {str(e)}") logging.error(f"Error deleting movie file: {str(e)}")
# Elimina la cartella superiore se vuota # Delete the parent folder if empty
try: try:
if os.path.exists(parent_folder) and not os.listdir(parent_folder): if os.path.exists(parent_folder) and not os.listdir(parent_folder):
os.rmdir(parent_folder) os.rmdir(parent_folder)
@ -516,60 +635,43 @@ def remove_movie():
except Exception as e: except Exception as e:
logging.error(f"Error deleting parent folder: {str(e)}") logging.error(f"Error deleting parent folder: {str(e)}")
# Rimuovi il film dal database # Remove the movie from the database
result = downloads_collection.delete_one({'type': 'movie', 'id': movie_id}) result = downloads_collection.delete_one({'type': 'movie', 'id': str(movie_id)})
if result.deleted_count > 0: if result.deleted_count > 0:
return jsonify({'success': True}), 200 return JSONResponse(status_code=200, content={'success': True})
else: else:
return jsonify({'error': 'Failed to delete movie from database'}), 500 raise HTTPException(status_code=500, detail="Failed to delete movie from database")
except Exception as e: except Exception as e:
logging.error(f"Error deleting movie: {str(e)}") logging.error(f"Error deleting movie: {str(e)}")
return jsonify({'error': 'Failed to delete movie'}), 500 raise HTTPException(status_code=500, detail="Failed to delete movie")
@app.route('/moviePath', methods=['GET']) # Fetch the path of a specific movie
def fetch_movie_path(): @app.get("/server/path/movie", response_model=dict, summary="Get movie download path on disk")
async def fetch_movie_path(movie_id: int):
""" """
Endpoint to fetch the path of a specific movie. Endpoint to fetch the path of a specific movie.
""" """
try: try:
movie_id = int(request.args.get('id'))
if not movie_id:
return jsonify({'error': 'Missing movie ID'}), 400
movie = downloads_collection.find_one({'type': 'movie', 'id': movie_id}, {'_id': 0, 'path': 1}) movie = downloads_collection.find_one({'type': 'movie', 'id': movie_id}, {'_id': 0, 'path': 1})
if movie and 'path' in movie: if movie and 'path' in movie:
return jsonify({'path': movie['path']}), 200 return {"path": movie['path']}
else: else:
return jsonify({'error': 'Movie not found'}), 404 raise HTTPException(status_code=404, detail="Movie not found")
except Exception as e: except Exception as e:
logging.error(f"Error fetching movie path: {str(e)}") logging.error(f"Error fetching movie path: {str(e)}")
return jsonify({'error': 'Failed to fetch movie path'}), 500 raise HTTPException(status_code=500, detail="Failed to fetch movie path")
@app.route('/episodePath', methods=['GET']) # Fetch the path of a specific episode
def fetch_episode_path(): @app.get("/server/path/episode", response_model=dict, summary="Get episode download path on disk")
async def fetch_episode_path(series_id: int, season_number: int, episode_number: int):
""" """
Endpoint to fetch the path of a specific episode. Endpoint to fetch the path of a specific episode.
""" """
try: try:
series_id = request.args.get('id')
season_number = request.args.get('season')
episode_number = request.args.get('episode')
if not series_id or not season_number or not episode_number:
return jsonify({'error': 'Missing parameters (id, season, episode)'}), 400
try:
series_id = int(series_id)
season_number = int(season_number)
episode_number = int(episode_number)
except ValueError:
return jsonify({'error': 'Invalid season or episode number'}), 400
episode = downloads_collection.find_one({ episode = downloads_collection.find_one({
'type': 'tv', 'type': 'tv',
'id': series_id, 'id': series_id,
@ -578,17 +680,17 @@ def fetch_episode_path():
}, {'_id': 0, 'path': 1}) }, {'_id': 0, 'path': 1})
if episode and 'path' in episode: if episode and 'path' in episode:
return jsonify({'path': episode['path']}), 200 return {"path": episode['path']}
else: else:
return jsonify({'error': 'Episode not found'}), 404 raise HTTPException(status_code=404, detail="Episode not found")
except Exception as e: except Exception as e:
logging.error(f"Error fetching episode path: {str(e)}") logging.error(f"Error fetching episode path: {str(e)}")
return jsonify({'error': 'Failed to fetch episode path'}), 500 raise HTTPException(status_code=500, detail="Failed to fetch episode path")
# ---------- MAIN ------------
if __name__ == '__main__': if __name__ == '__main__':
ensure_collections_exist(db) uvicorn.run(app, host="127.0.0.1", port=1234, loop="asyncio")
app.run(debug=True, port=1234, threaded=True)