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

4
.gitignore vendored
View File

@ -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
/client/dashboard/build

View File

@ -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)
...

View File

@ -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
}

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 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([]);

View File

@ -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 = () => {
>
<FaPlay className="me-2" /> Watch
</Button>
<Link
to={`/title/${movie.slug}`}
state={{ url: movie.slug }}
className="btn btn-secondary btn-sm ms-2"
>
View Details
</Link>
</Card.Body>
</Card>
</Col>
@ -160,13 +152,6 @@ const Downloads = () => {
>
<FaPlay className="me-2" /> Watch
</Button>
<Link
to={`/title/${slug}`}
state={{ url: slug }}
className="btn btn-secondary btn-sm ms-2"
>
View Details
</Link>
</Card.Body>
</Card>
</Col>
@ -181,7 +166,7 @@ const Downloads = () => {
<Modal show={showPlayer} onHide={() => setShowPlayer(false)} size="lg" centered>
<Modal.Body>
<video
src={`http://127.0.0.1:1234/downloaded/${currentVideo}`}
src={`${API_URL}/downloaded/${currentVideo}`}
controls
autoPlay
style={{ width: '100%' }}

View File

@ -4,8 +4,7 @@ import axios from 'axios';
import { Container, Row, Col, Card, Spinner } from 'react-bootstrap';
import SearchBar from './SearchBar.js';
const API_BASE_URL = "http://127.0.0.1:1234";
import { API_URL } from './ApiUrl.js';
const SearchResults = () => {
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);

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 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 = () => {
<>
<Row className="mb-3">
<Col>
<Dropdown>
<Dropdown.Toggle variant="secondary">
Season {selectedSeason}
</Dropdown.Toggle>
<div className="d-flex align-items-center">
{/* Dropdown per la selezione della stagione */}
<Dropdown>
<Dropdown.Toggle variant="secondary">
Season {selectedSeason}
</Dropdown.Toggle>
<Dropdown.Menu>
{[...Array(titleDetails.season_count)].map((_, index) => (
<Dropdown.Item
key={index + 1}
onClick={() => handleSeasonSelect(index + 1)}
>
Season {index + 1}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<Dropdown.Menu>
{[...Array(titleDetails.season_count)].map((_, index) => (
<Dropdown.Item
key={index + 1}
onClick={() => handleSeasonSelect(index + 1)}
>
Season {index + 1}
</Dropdown.Item>
))}
</Dropdown.Menu>
</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>
</Row>
@ -393,7 +444,7 @@ const TitleDetail = () => {
<Modal show={showPlayer} onHide={() => setShowPlayer(false)} size="lg" centered>
<Modal.Body>
<video
src={`http://127.0.0.1:1234/downloaded/${currentVideo}`}
src={`${API_URL}/downloaded/${currentVideo}`}
controls
autoPlay
style={{ width: '100%' }}

View File

@ -1,10 +1,10 @@
import React, { useState, useEffect } from 'react';
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 { 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 [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 = () => {
</Row>
)}
</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>
);
};

View File

@ -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,

View File

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

618
server.py
View File

@ -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/<path:filename>', 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)
uvicorn.run(app, host="127.0.0.1", port=1234, loop="asyncio")