diff --git a/.gitignore b/.gitignore index 7e70cb5..c1fc56a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +*.pyc # PyInstaller *.manifest @@ -34,6 +35,7 @@ note.txt list_proxy.txt cmd.txt downloaded_files +key.t # Cache __pycache__/ @@ -42,4 +44,4 @@ __pycache__/ /client/dashboard/node_modules # Ignore build directory in the client dashboard to avoid committing build artifacts -/client/dashboard/build \ No newline at end of file +/client/dashboard/build diff --git a/README.md b/README.md index 3924c15..19b85a6 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ Per testare 1. Installare requirements -2. Inserire url per mongodb e creare database -3. Runnare server.py +1. Installare node js: https://nodejs.org/en/download/package-manager +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 -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 -- Bottone download intera stagione -- Messaggio con richiesta se scaricare le nuove stagione quando si fa il check in watchlist -- Migliore player in case watch con bottone +- Migliore player quando si clicca sul button watch. - Coda di download con bottone aggiungere alla coda (complessa) +- Progresso di download (complesso) ... \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/streamingcommunity/api.py b/StreamingCommunity/Api/Site/streamingcommunity/api.py index d829833..e84295b 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/api.py +++ b/StreamingCommunity/Api/Site/streamingcommunity/api.py @@ -87,14 +87,6 @@ def get_infoSelectTitle(url_title: str, domain: str, version: str): if response.status_code == 200: 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 = {} 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')}" @@ -106,8 +98,6 @@ def get_infoSelectTitle(url_title: str, domain: str, version: str): 'plot': json_response['title']['plot'], 'type': json_response['title']['type'], 'season_count': json_response['title']['seasons_count'], - 'generes': generes, - 'trailer': trailer, 'image': images } diff --git a/client/dashboard/src/components/ApiUrl.js b/client/dashboard/src/components/ApiUrl.js new file mode 100644 index 0000000..b6d927b --- /dev/null +++ b/client/dashboard/src/components/ApiUrl.js @@ -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`; \ No newline at end of file diff --git a/client/dashboard/src/components/Dashboard.js b/client/dashboard/src/components/Dashboard.js index cb0fa55..c5c6739 100644 --- a/client/dashboard/src/components/Dashboard.js +++ b/client/dashboard/src/components/Dashboard.js @@ -3,8 +3,7 @@ import axios from 'axios'; import { Container, Button, Form, InputGroup } from 'react-bootstrap'; import SearchBar from './SearchBar.js'; - -const API_BASE_URL = "http://127.0.0.1:1234"; +import { API_BASE_URL } from './ApiUrl.js'; const Dashboard = () => { const [items, setItems] = useState([]); diff --git a/client/dashboard/src/components/Downloads.js b/client/dashboard/src/components/Downloads.js index 54d98bc..c50ddad 100644 --- a/client/dashboard/src/components/Downloads.js +++ b/client/dashboard/src/components/Downloads.js @@ -1,10 +1,9 @@ import React, { useState, useEffect } from 'react'; 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 { 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, setDownloads] = useState([]); @@ -15,7 +14,7 @@ const Downloads = () => { // Fetch all downloads const fetchDownloads = async () => { try { - const response = await axios.get(`${API_BASE_URL}/downloads`); + const response = await axios.get(`${SERVER_PATH_URL}/getAll`); setDownloads(response.data); setLoading(false); } catch (error) { @@ -27,8 +26,8 @@ const Downloads = () => { // Delete a TV episode const handleDeleteEpisode = async (id, season, episode) => { try { - await axios.delete(`${API_BASE_URL}/deleteEpisode`, { - params: { id, season, episode } + await axios.delete(`${SERVER_DELETE_URL}/episode`, { + params: { series_id: id, season_number: season, episode_number: episode } }); fetchDownloads(); // Refresh the list } catch (error) { @@ -39,8 +38,8 @@ const Downloads = () => { // Delete a movie const handleDeleteMovie = async (id) => { try { - await axios.delete(`${API_BASE_URL}/deleteMovie`, { - params: { id } + await axios.delete(`${SERVER_DELETE_URL}/movie`, { + params: { movie_id: id } }); fetchDownloads(); // Refresh the list } catch (error) { @@ -111,13 +110,6 @@ const Downloads = () => { > Watch - - View Details - @@ -160,13 +152,6 @@ const Downloads = () => { > Watch - - View Details - @@ -181,7 +166,7 @@ const Downloads = () => { setShowPlayer(false)} size="lg" centered> { const [results, setResults] = useState([]); @@ -20,7 +19,7 @@ const SearchResults = () => { const fetchSearchResults = async () => { try { setLoading(true); - const response = await axios.get(`${API_BASE_URL}/api/search`, { + const response = await axios.get(`${API_URL}/search`, { params: { q: query } }); setResults(response.data); diff --git a/client/dashboard/src/components/TitleDetail.js b/client/dashboard/src/components/TitleDetail.js index 8dd814c..65e1372 100644 --- a/client/dashboard/src/components/TitleDetail.js +++ b/client/dashboard/src/components/TitleDetail.js @@ -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 SearchBar from './SearchBar.js'; - -const API_BASE_URL = "http://127.0.0.1:1234"; +import { API_BASE_URL, API_URL, SERVER_DELETE_URL, SERVER_PATH_URL, SERVER_WATCHLIST_URL} from './ApiUrl.js'; const TitleDetail = () => { const [titleDetails, setTitleDetails] = useState(null); @@ -16,6 +15,7 @@ const TitleDetail = () => { const [hoveredEpisode, setHoveredEpisode] = useState(null); const [isInWatchlist, setIsInWatchlist] = useState(false); const [downloadStatus, setDownloadStatus] = useState({}); + const [downloadingAllSeason, setDownloadingAllSeason] = useState(false); const [showPlayer, setShowPlayer] = useState(false); const [currentVideo, setCurrentVideo] = useState(""); const location = useLocation(); @@ -27,7 +27,7 @@ const TitleDetail = () => { const titleUrl = location.state?.url || location.pathname.split('/title/')[1]; // Fetch title information - const response = await axios.get(`${API_BASE_URL}/api/getInfo`, { + const response = await axios.get(`${API_URL}/getInfo`, { params: { url: titleUrl } }); @@ -70,7 +70,7 @@ const TitleDetail = () => { } }); } 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( download => download.type === 'tv' && download.slug === titleData.slug ); @@ -92,7 +92,7 @@ const TitleDetail = () => { // Check watchlist status const checkWatchlistStatus = async (slug) => { 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); setIsInWatchlist(inWatchlist); } catch (error) { @@ -104,7 +104,7 @@ const TitleDetail = () => { if (titleDetails.type === 'tv') { try { setLoading(true); - const seasonResponse = await axios.get(`${API_BASE_URL}/api/getInfoSeason`, { + const seasonResponse = await axios.get(`${API_URL}/getInfoSeason`, { params: { url: location.state?.url, n: seasonNumber @@ -123,7 +123,7 @@ const TitleDetail = () => { const handleDownloadFilm = async () => { try { - const response = await axios.get(`${API_BASE_URL}/downloadFilm`, { + const response = await axios.get(`${API_URL}/download/film`, { params: { id: titleDetails.id, slug: titleDetails.slug @@ -146,7 +146,7 @@ const TitleDetail = () => { const handleDownloadEpisode = async (seasonNumber, episodeNumber) => { try { - const response = await axios.get(`${API_BASE_URL}/downloadEpisode`, { + const response = await axios.get(`${API_URL}/download/episode`, { params: { n_s: seasonNumber, 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) => { if (!videoPath) { // If no path provided, attempt to get path from downloads try { let path; 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 } }); path = response.data.path; @@ -198,7 +237,7 @@ const TitleDetail = () => { const handleAddToWatchlist = async () => { try { - await axios.post(`${API_BASE_URL}/api/addWatchlist`, { + await axios.post(`${SERVER_WATCHLIST_URL}/add`, { name: titleDetails.slug, url: location.state?.url || location.pathname.split('/title/')[1], season: titleDetails.season_count @@ -212,7 +251,7 @@ const TitleDetail = () => { const handleRemoveFromWatchlist = async () => { try { - await axios.post(`${API_BASE_URL}/api/removeWatchlist`, { + await axios.post(`${SERVER_WATCHLIST_URL}/remove`, { name: titleDetails.slug }); setIsInWatchlist(false); @@ -318,22 +357,34 @@ const TitleDetail = () => { <> - - - Season {selectedSeason} - + + {/* Dropdown per la selezione della stagione */} + + + Season {selectedSeason} + + + {[...Array(titleDetails.season_count)].map((_, index) => ( + handleSeasonSelect(index + 1)} + > + Season {index + 1} + + ))} + + - - {[...Array(titleDetails.season_count)].map((_, index) => ( - handleSeasonSelect(index + 1)} - > - Season {index + 1} - - ))} - - + {/* Bottone per il download con margine a sinistra */} + + {downloadingAllSeason ? 'Downloading...' : `Download Season ${selectedSeason}`} + + @@ -393,7 +444,7 @@ const TitleDetail = () => { setShowPlayer(false)} size="lg" centered> { const [watchlistItems, setWatchlistItems] = useState([]); @@ -12,10 +12,19 @@ const Watchlist = () => { const [loading, setLoading] = useState(true); 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 const fetchWatchlistData = async () => { try { - const watchlistResponse = await axios.get(`${API_BASE_URL}/api/getWatchlist`); + const watchlistResponse = await axios.get(`${SERVER_WATCHLIST_URL}/get`); setWatchlistItems(watchlistResponse.data); setLoading(false); } catch (error) { @@ -27,27 +36,22 @@ const Watchlist = () => { // Funzione per controllare se ci sono nuove stagioni (attivata dal bottone) const checkNewSeasons = async () => { 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); + setSeasonsToDownload(newSeasonsResponse.data); + setShowDownloadModal(true); - // Crea un messaggio per i titoli con nuove stagioni const titlesWithNewSeasons = newSeasonsResponse.data.map(season => season.name); - if (titlesWithNewSeasons.length > 0) { - setNewSeasonsMessage(`Nuove stagioni disponibili per: ${titlesWithNewSeasons.join(", ")}`); - - // Dopo aver mostrato il messaggio, aggiorniamo i titoli con le nuove stagioni - updateTitlesWithNewSeasons(newSeasonsResponse.data); - } else { - setNewSeasonsMessage("Nessuna nuova stagione disponibile."); - } + setNewSeasonsMessage(`Nuove stagioni disponibili per: ${titlesWithNewSeasons.join(", ")}`); } else { - setNewSeasons([]); // In caso contrario, non ci sono nuove stagioni + setNewSeasons([]); setNewSeasonsMessage("Nessuna nuova stagione disponibile."); } } catch (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 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, 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 const handleRemoveFromWatchlist = async (serieName) => { 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 setWatchlistItems((prev) => prev.filter((item) => item.name !== serieName)); @@ -156,6 +226,57 @@ const Watchlist = () => { )} + + + setShowDownloadModal(false)}> + + Nuove Stagioni Disponibili + + + Sono disponibili nuove stagioni per i seguenti titoli: + + {seasonsToDownload.map((season) => ( + + {season.name.replace(/-/g, ' ')} - Stagione {season.nNewSeason} + + ))} + + + {downloadProgress.status !== 'idle' && ( + + Progresso download: + + + {downloadProgress.current}/{downloadProgress.total} Stagioni + + + + )} + + + setShowDownloadModal(false)}> + Annulla + + + + Scarica Nuove Stagioni + + + ); }; diff --git a/config.json b/config.json index 0e4f749..f5989e2 100644 --- a/config.json +++ b/config.json @@ -1,22 +1,12 @@ { "DEFAULT": { "debug": false, - "log_file": "app.log", - "log_to_file": true, "show_message": true, - "clean_console": true, "root_path": "Video", "movie_folder_name": "Movie", "serie_folder_name": "TV", "map_episode_name": "%(tv_name)_S%(season)E%(episode)_%(episode_name)", - "config_qbit_tor": { - "host": "192.168.1.59", - "port": "8080", - "user": "admin", - "pass": "adminadmin" - }, - "not_close": false, - "show_trending": false + "clean_console": true }, "REQUESTS": { "timeout": 20, @@ -26,9 +16,6 @@ "proxy_start_max": 0.5, "user-agent": "" }, - "BROWSER": { - "headless": false - }, "M3U8_DOWNLOAD": { "tqdm_delay": 0.01, "tqdm_use_large_bar": true, diff --git a/requirements.txt b/requirements.txt index 760434a..4f19709 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,4 @@ qbittorrent-api python-qbittorrent googlesearch-python pymongo -flask \ No newline at end of file +fastapi \ No newline at end of file diff --git a/server.py b/server.py index e8d0738..e9a22c4 100644 --- a/server.py +++ b/server.py @@ -1,17 +1,23 @@ import os import logging -import shutil import datetime from urllib.parse import urlparse from pymongo import MongoClient from urllib.parse import unquote -from flask_cors import CORS -from flask import Flask, jsonify, request -from flask import send_from_directory +from pydantic import BaseModel +from typing import List, Optional + +# Fast api +from fastapi import FastAPI, HTTPException +from fastapi.responses import JSONResponse, FileResponse +from fastapi.middleware.cors import CORSMiddleware +import uvicorn + # Util from StreamingCommunity.Util._jsonConfig import config_manager + # Internal 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 @@ -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.util.ScrapeSerie import ScrapeSerie + # Player 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() season_name = None scrape_serie = ScrapeSerie("streamingcommunity") @@ -33,6 +49,7 @@ video_source = VideoSource("streamingcommunity", True) DOWNLOAD_DIRECTORY = os.getcwd() +# Mongo client = MongoClient(config_manager.get("EXTRA", "mongodb")) db = client[config_manager.get("EXTRA", "database")] 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 ------------ -@app.route('/') -def index(): +@app.get("/", summary="Health Check") +async def index(): """ Health check endpoint to confirm server is operational. @@ -50,51 +78,56 @@ def index(): str: Operational status message """ logging.info("Health check endpoint accessed") - return 'Server is operational' + return {"status": "Server is operational"} -@app.route('/api/search', methods=['GET']) -def get_list_search(): +@app.get("/api/search", summary="Search Titles") +async def get_list_search(q): """ Search for titles based on query parameter. + Args: + q (str, optional): Search query parameter + Returns: - JSON response with search results or error message + JSON response with search results """ + if not q: + logging.warning("Search request without query parameter") + raise HTTPException(status_code=400, detail="Missing query parameter") + try: - query = request.args.get('q') - - if not query: - logging.warning("Search request without query parameter") - return jsonify({'error': 'Missing query parameter'}), 400 - - result = search_titles(query, domain) - logging.info(f"Search performed for query: {query}") - return jsonify(result), 200 + result = search_titles(q, domain) + logging.info(f"Search performed for query: {q}") + return result except Exception as e: 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']) -def get_info_title(): +@app.get("/api/getInfo", summary="Get Title Information") +async def get_info_title(url): """ Retrieve information for a specific title. + Args: + url (str, optional): Title URL parameter + Returns: - JSON response with title information or error message + JSON response with title information """ + if not url: + logging.warning("GetInfo request without URL parameter") + raise HTTPException(status_code=400, detail="Missing URL parameter") + try: - title_url = request.args.get('url') - - if not title_url: - logging.warning("GetInfo request without URL parameter") - return jsonify({'error': 'Missing URL parameter'}), 400 - - result = get_infoSelectTitle(title_url, domain, version) + result = get_infoSelectTitle(update_domain(url), domain, version) + # Global state management for TV series if result.get('type') == "tv": global season_name, scrape_serie, video_source season_name = result.get('slug') + + # Setup for TV series (adjust based on your actual implementation) scrape_serie.setup( version=version, media_id=int(result.get('id')), @@ -104,38 +137,39 @@ def get_info_title(): logging.info(f"TV series info retrieved: {season_name}") - return jsonify(result), 200 + return result except Exception as e: 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']) -def get_info_season(): +@app.get("/api/getInfoSeason", summary="Get Season Information") +async def get_info_season(url, n): """ Retrieve season information for a specific title. + Args: + url (str, optional): Title URL parameter + n (str, optional): Season number + Returns: - JSON response with season information or error message + JSON response with season information """ + if not url or not n: + logging.warning("GetInfoSeason request with missing parameters") + raise HTTPException(status_code=400, detail="Missing URL or season number") + try: - 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") - return jsonify({'error': 'Missing URL or season number'}), 400 - - result = get_infoSelectSeason(title_url, number_season, domain, version) - logging.info(f"Season info retrieved for season {number_season}") - return jsonify(result), 200 + result = get_infoSelectSeason(update_domain(url), n, domain, version) + logging.info(f"Season info retrieved for season {n}") + return result except Exception as e: 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']) -def get_domain(): +@app.get("/api/getDomain", summary="Get Current Domain") +async def get_domain(): """ Retrieve current domain and version. @@ -146,37 +180,38 @@ def get_domain(): global version, domain version, domain = get_version_and_domain() logging.info(f"Domain retrieved: {domain}, Version: {version}") - return jsonify({'domain': domain, 'version': version}), 200 + return {"domain": domain, "version": version} except Exception as e: 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 ------------ -@app.route('/downloadFilm', methods=['GET']) -def call_download_film(): +# ---------- CALL DOWNLOAD API ------------ +@app.get("/api/download/film", summary="Download Film") +async def call_download_film(id, slug): """ Download a film by its ID and slug. + Args: + id (str, optional): Film ID + slug (str, optional): Film slug + Returns: JSON response with download path or error message """ + if not id or not slug: + logging.warning("Download film request with missing parameters") + raise HTTPException(status_code=400, detail="Missing film ID or slug") + try: - 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") - return jsonify({'error': 'Missing film ID or slug'}), 400 - - item_media = MediaItem(**{'id': film_id, 'slug': slug}) + item_media = MediaItem(**{'id': id, 'slug': slug}) path_download = download_film(item_media) download_data = { 'type': 'movie', - 'id': film_id, + 'id': id, 'slug': slug, 'path': path_download, 'timestamp': datetime.datetime.now(datetime.timezone.utc) @@ -184,44 +219,58 @@ def call_download_film(): downloads_collection.insert_one(download_data) logging.info(f"Film downloaded: {slug}") - return jsonify({'path': path_download}), 200 + return {"path": path_download} except Exception as e: 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']) -def call_download_episode(): +@app.get("/api/download/episode", summary="Download TV 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. + 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: JSON response with download path or error message """ + if not n_s or not n_ep: + logging.warning("Download episode request with missing parameters") + raise HTTPException(status_code=400, detail="Missing season or episode number") + try: - season_number = request.args.get('n_s') - episode_number = request.args.get('n_ep') + season_number = int(n_s) + episode_number = int(n_ep) - if not season_number or not episode_number: - logging.warning("Download episode request with missing parameters") - return jsonify({'error': 'Missing season or episode number'}), 400 + # 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) - season_number = int(season_number) - episode_number = int(episode_number) - - scrape_serie.collect_title_season(season_number) + # Scaricare il video path_download = download_video( - season_name, + scrape_serie.series_name, season_number, episode_number, scrape_serie, video_source ) + # Salvare i dati del download download_data = { 'type': 'tv', - 'id': scrape_serie.media_id, - 'slug': scrape_serie.series_name, + 'id': media_id if media_id else scrape_serie.media_id, + 'slug': series_name if series_name else scrape_serie.series_name, 'n_s': season_number, 'n_ep': episode_number, 'path': path_download, @@ -230,21 +279,24 @@ def call_download_episode(): downloads_collection.insert_one(download_data) logging.info(f"Episode downloaded: S{season_number}E{episode_number}") - return jsonify({'path': path_download}), 200 + return {"path": path_download} except ValueError: 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: 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/', methods=['GET']) -def serve_downloaded_file(filename): +@app.get("/api/downloaded/{filename:path}", summary="Serve Downloaded Files") +async def serve_downloaded_file(filename: str): """ Serve downloaded files with proper URL decoding and error handling. + Args: + filename (str): Encoded filename path + Returns: Downloaded file or error message """ @@ -260,132 +312,204 @@ def serve_downloaded_file(filename): # Verify file exists if not os.path.isfile(file_path): 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 - 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: 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 ------------ -@app.route('/api/addWatchlist', methods=['POST']) -def add_to_watchlist(): - title_name = request.json.get('name') - title_url = request.json.get('url') - season = request.json.get('season') +class WatchlistItem(BaseModel): + name: str + url: str + season: int - if title_url and season: +class UpdateWatchlistItem(BaseModel): + url: str + season: int - existing_item = watchlist_collection.find_one({'name': title_name, 'url': title_url, 'season': season}) +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 + }) + 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({ - 'name': title_name, - 'title_url': title_url, - 'season': season, + 'name': item.name, + 'title_url': item.url, + 'season': item.season, 'added_on': datetime.datetime.utcnow() }) - return jsonify({'message': 'Titolo aggiunto alla watchlist'}), 200 - else: - return jsonify({'message': 'Missing title_url or season'}), 400 + + return {"message": "Titolo aggiunto alla watchlist"} + + except HTTPException: + raise + except Exception as e: + logging.error(f"Error adding to watchlist: {str(e)}") + raise HTTPException(status_code=500, detail="Errore interno del server") -@app.route('/api/updateTitleWatchlist', methods=['POST']) -def update_title_watchlist(): - print(request.json) - - title_url = request.json.get('url') - new_season = request.json.get('season') - - 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( - {'title_url': title_url}, - {'$set': {'season': new_season}} + {'title_url': item.url}, + {'$set': {'season': item.season}} ) - + 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: - return jsonify({'message': 'La stagione non è cambiata'}), 200 - - return jsonify({'message': 'Stagione aggiornata con successo'}), 200 + return {"message": "La stagione non è cambiata"} + + return {"message": "Stagione aggiornata con successo"} - else: - return jsonify({'message': 'Missing title_url or season'}), 400 + except HTTPException: + raise + except Exception as e: + logging.error(f"Error updating watchlist: {str(e)}") + raise HTTPException(status_code=500, detail="Errore interno del server") + +@app.post("/server/watchlist/remove", summary="Remove Item from Watchlist") +async def remove_from_watchlist(item: RemoveWatchlistItem): + """ + Remove an item from the watchlist. -@app.route('/api/removeWatchlist', methods=['POST']) -def remove_from_watchlist(): - title_name = request.json.get('name') - - if title_name: - result = watchlist_collection.delete_one({'name': title_name}) - + Args: + 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: - return jsonify({'message': 'Titolo rimosso dalla watchlist'}), 200 - else: - return jsonify({'message': 'Titolo non trovato nella watchlist'}), 404 - else: - return jsonify({'message': 'Missing title_url or season'}), 400 + return {"message": "Titolo rimosso dalla watchlist"} + + raise HTTPException(status_code=404, detail="Titolo non trovato nella watchlist") -@app.route('/api/getWatchlist', methods=['GET']) -def get_watchlist(): - watchlist_items = list(watchlist_collection.find({}, {'_id': 0})) + except HTTPException: + raise + except Exception as e: + logging.error(f"Error removing from watchlist: {str(e)}") + raise HTTPException(status_code=500, detail="Errore interno del server") - if watchlist_items: - return jsonify(watchlist_items), 200 - else: - return jsonify({'message': 'La watchlist è vuota'}), 200 +@app.get("/server/watchlist/get", summary="Get Watchlist Items") +async def get_watchlist(): + """ + Retrieve all items in the watchlist. -@app.route('/api/checkWatchlist', methods=['GET']) -def get_newSeason(): - title_newSeasons = [] - watchlist_items = list(watchlist_collection.find({}, {'_id': 0})) + Returns: + List of watchlist items or empty list message + """ + try: + watchlist_items = list(watchlist_collection.find({}, {'_id': 0})) + + if not watchlist_items: + 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") - if not watchlist_items: - return jsonify({'message': 'La watchlist è vuota'}), 200 - - for item in watchlist_items: - title_url = item.get('title_url') - if not title_url: - continue - - try: - parsed_url = urlparse(title_url) - hostname = parsed_url.hostname - domain_part = hostname.split('.')[1] - new_url = title_url.replace(domain_part, domain) - - result = get_infoSelectTitle(new_url, domain, version) - - if not result or 'season_count' not in result: - continue - - number_season = result.get("season_count") - - if number_season > item.get("season"): - title_newSeasons.append({ - 'title_url': item.get('title_url'), - 'name': item.get('name'), - 'season': int(number_season), - 'nNewSeason': int(number_season) - int(item.get("season")) - }) - - except Exception as e: - print(f"Errore nel recuperare informazioni per {item.get('title_url')}: {e}") - - if title_newSeasons: - return jsonify(title_newSeasons), 200 - else: - return jsonify({'message': 'Nessuna nuova stagione disponibile'}), 200 +@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: + title_url = item.get('title_url') + if not title_url: + continue + + try: + new_url = update_domain(title_url) + + # Fetch title info + result = get_infoSelectTitle(new_url, domain, version) + + if not result or 'season_count' not in result: + continue + + number_season = result.get("season_count") + + # Check for new seasons + if number_season > item.get("season"): + title_new_seasons.append({ + 'title_url': item.get('title_url'), + 'name': item.get('name'), + 'season': int(number_season), + 'nNewSeason': int(number_season) - int(item.get("season")) + }) + + except Exception as e: + logging.error(f"Error checking seasons for {item.get('title_url')}: {str(e)}") + + if title_new_seasons: + return title_new_seasons + + 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): """ Ensures that the required collections exist in the database. @@ -405,40 +529,36 @@ def ensure_collections_exist(db): else: logging.info(f"Collection already exists: {collection_name}") -@app.route('/downloads', methods=['GET']) -def fetch_all_downloads(): +class Episode(BaseModel): + 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. """ try: downloads = list(downloads_collection.find({}, {'_id': 0})) - return jsonify(downloads), 200 + return downloads except Exception as 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']) -def remove_episode(): +# Remove a specific episode and its file +@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. """ 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 - - # Trova il percorso del file + # Find the episode in the database episode = downloads_collection.find_one({ 'type': 'tv', 'id': series_id, @@ -446,12 +566,15 @@ def remove_episode(): 'n_ep': episode_number }, {'_id': 0, 'path': 1}) + logging.info("FIND => ", 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'] + logging.info("PATH => ", file_path) - # Elimina il file fisico + # Delete the file try: if os.path.exists(file_path): os.remove(file_path) @@ -461,7 +584,7 @@ def remove_episode(): except Exception as 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({ 'type': 'tv', 'id': series_id, @@ -470,35 +593,31 @@ def remove_episode(): }) if result.deleted_count > 0: - return jsonify({'success': True}), 200 + return JSONResponse(status_code=200, content={'success': True}) 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: 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']) -def remove_movie(): +# Remove a specific movie, its file, and its parent folder if empty +@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. """ try: - movie_id = request.args.get('id') - - 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}) + # Find the movie in the database + movie = downloads_collection.find_one({'type': 'movie', 'id': str(movie_id)}, {'_id': 0, 'path': 1}) 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'] parent_folder = os.path.dirname(file_path) - # Elimina il file fisico + # Delete the movie file try: if os.path.exists(file_path): os.remove(file_path) @@ -508,7 +627,7 @@ def remove_movie(): except Exception as e: logging.error(f"Error deleting movie file: {str(e)}") - # Elimina la cartella superiore se vuota + # Delete the parent folder if empty try: if os.path.exists(parent_folder) and not os.listdir(parent_folder): os.rmdir(parent_folder) @@ -516,60 +635,43 @@ def remove_movie(): except Exception as e: logging.error(f"Error deleting parent folder: {str(e)}") - # Rimuovi il film dal database - result = downloads_collection.delete_one({'type': 'movie', 'id': movie_id}) + # Remove the movie from the database + result = downloads_collection.delete_one({'type': 'movie', 'id': str(movie_id)}) if result.deleted_count > 0: - return jsonify({'success': True}), 200 + return JSONResponse(status_code=200, content={'success': True}) 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: 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']) -def fetch_movie_path(): +# Fetch the path of a specific movie +@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. """ 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}) if movie and 'path' in movie: - return jsonify({'path': movie['path']}), 200 + return {"path": movie['path']} else: - return jsonify({'error': 'Movie not found'}), 404 + raise HTTPException(status_code=404, detail="Movie not found") except Exception as 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']) -def fetch_episode_path(): +# Fetch the path of a specific episode +@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. """ 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({ 'type': 'tv', 'id': series_id, @@ -578,17 +680,17 @@ def fetch_episode_path(): }, {'_id': 0, 'path': 1}) if episode and 'path' in episode: - return jsonify({'path': episode['path']}), 200 + return {"path": episode['path']} else: - return jsonify({'error': 'Episode not found'}), 404 + raise HTTPException(status_code=404, detail="Episode not found") except Exception as 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__': - ensure_collections_exist(db) - app.run(debug=True, port=1234, threaded=True) \ No newline at end of file + uvicorn.run(app, host="127.0.0.1", port=1234, loop="asyncio")
Sono disponibili nuove stagioni per i seguenti titoli:
Progresso download: