weebzone commited on
Commit ·
14ad4e9
1
Parent(s): 5645d3c
`🚀 V1.5.0 Release`
Browse files### New Features & Improvements:
📂 **Flexible Quality Storage:** Supports storing multiple files of the same quality when replace mode is disabled.
### Fixes:
🐛 **Quality Deletion Fix:** Deleting a quality now removes only the selected file instead of all files with the same quality.
📖 **Documentation Fix:** Corrected and clarified `REPLACE_MODE` explanation in README.
### ⚙️ Configuration:
```env
REPLACE_MODE=true # Replace old files of same quality (false = allow duplicates)
```
- Backend/__init__.py +1 -1
- Backend/config.py +1 -0
- Backend/fastapi/main.py +9 -12
- Backend/fastapi/routes/api_routes.py +5 -5
- Backend/fastapi/templates/media_edit.html +18 -18
- Backend/helper/database.py +95 -58
- README.md +1 -0
- pyproject.toml +1 -1
- uv.lock +1 -1
Backend/__init__.py
CHANGED
|
@@ -11,4 +11,4 @@ StartTime = time()
|
|
| 11 |
USE_DEFAULT_ID: str = None
|
| 12 |
db = Database()
|
| 13 |
|
| 14 |
-
__version__ = "1.
|
|
|
|
| 11 |
USE_DEFAULT_ID: str = None
|
| 12 |
db = Database()
|
| 13 |
|
| 14 |
+
__version__ = "1.5.0"
|
Backend/config.py
CHANGED
|
@@ -21,6 +21,7 @@ class Telegram:
|
|
| 21 |
UPSTREAM_BRANCH = getenv("UPSTREAM_BRANCH", "")
|
| 22 |
|
| 23 |
OWNER_ID = int(getenv("OWNER_ID", "5422223708"))
|
|
|
|
| 24 |
|
| 25 |
ADMIN_USERNAME = getenv("ADMIN_USERNAME", "fyvio")
|
| 26 |
ADMIN_PASSWORD = getenv("ADMIN_PASSWORD", "fyvio")
|
|
|
|
| 21 |
UPSTREAM_BRANCH = getenv("UPSTREAM_BRANCH", "")
|
| 22 |
|
| 23 |
OWNER_ID = int(getenv("OWNER_ID", "5422223708"))
|
| 24 |
+
REPLACE_MODE = getenv("REPLACE_MODE", "true").lower() == "true"
|
| 25 |
|
| 26 |
ADMIN_USERNAME = getenv("ADMIN_USERNAME", "fyvio")
|
| 27 |
ADMIN_PASSWORD = getenv("ADMIN_PASSWORD", "fyvio")
|
Backend/fastapi/main.py
CHANGED
|
@@ -4,7 +4,6 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 4 |
from fastapi.staticfiles import StaticFiles
|
| 5 |
from starlette.middleware.sessions import SessionMiddleware
|
| 6 |
from Backend import __version__
|
| 7 |
-
|
| 8 |
from Backend.fastapi.security.credentials import require_auth
|
| 9 |
from Backend.fastapi.routes.stream_routes import router as stream_router
|
| 10 |
from Backend.fastapi.routes.stremio_routes import router as stremio_router
|
|
@@ -37,7 +36,7 @@ app.add_middleware(
|
|
| 37 |
try:
|
| 38 |
app.mount("/static", StaticFiles(directory="Backend/fastapi/static"), name="static")
|
| 39 |
except Exception:
|
| 40 |
-
pass
|
| 41 |
|
| 42 |
# --- Include existing API routers ---
|
| 43 |
app.include_router(stream_router)
|
|
@@ -83,9 +82,9 @@ async def edit_media(request: Request, tmdb_id: int, db_index: int, media_type:
|
|
| 83 |
|
| 84 |
@app.get("/api/media/list")
|
| 85 |
async def list_media(
|
| 86 |
-
media_type: str = Query("movie", regex="^(movie|tv)$"),
|
| 87 |
-
page: int = Query(1, ge=1),
|
| 88 |
-
page_size: int = Query(24, ge=1, le=100),
|
| 89 |
search: str = Query("", max_length=100),
|
| 90 |
_: bool = Depends(require_auth)
|
| 91 |
):
|
|
@@ -100,12 +99,12 @@ async def update_media(request: Request, tmdb_id: int, db_index: int, media_type
|
|
| 100 |
return await update_media_api(request, tmdb_id, db_index, media_type)
|
| 101 |
|
| 102 |
@app.delete("/api/media/delete-quality")
|
| 103 |
-
async def delete_movie_quality(tmdb_id: int, db_index: int,
|
| 104 |
-
return await delete_movie_quality_api(tmdb_id, db_index,
|
| 105 |
|
| 106 |
@app.delete("/api/media/delete-tv-quality")
|
| 107 |
-
async def delete_tv_quality(tmdb_id: int, db_index: int, season: int, episode: int,
|
| 108 |
-
return await delete_tv_quality_api(tmdb_id, db_index, season, episode,
|
| 109 |
|
| 110 |
@app.delete("/api/media/delete-tv-episode")
|
| 111 |
async def delete_tv_episode(tmdb_id: int, db_index: int, season: int, episode: int, _: bool = Depends(require_auth)):
|
|
@@ -115,7 +114,6 @@ async def delete_tv_episode(tmdb_id: int, db_index: int, season: int, episode: i
|
|
| 115 |
async def delete_tv_season(tmdb_id: int, db_index: int, season: int, _: bool = Depends(require_auth)):
|
| 116 |
return await delete_tv_season_api(tmdb_id, db_index, season)
|
| 117 |
|
| 118 |
-
|
| 119 |
@app.get("/api/system/workloads")
|
| 120 |
async def get_workloads(_: bool = Depends(require_auth)):
|
| 121 |
try:
|
|
@@ -131,7 +129,6 @@ async def get_workloads(_: bool = Depends(require_auth)):
|
|
| 131 |
except Exception as e:
|
| 132 |
return {"loads": {}}
|
| 133 |
|
| 134 |
-
|
| 135 |
@app.exception_handler(401)
|
| 136 |
async def auth_exception_handler(request: Request, exc):
|
| 137 |
-
return RedirectResponse(url="/login", status_code=302)
|
|
|
|
| 4 |
from fastapi.staticfiles import StaticFiles
|
| 5 |
from starlette.middleware.sessions import SessionMiddleware
|
| 6 |
from Backend import __version__
|
|
|
|
| 7 |
from Backend.fastapi.security.credentials import require_auth
|
| 8 |
from Backend.fastapi.routes.stream_routes import router as stream_router
|
| 9 |
from Backend.fastapi.routes.stremio_routes import router as stremio_router
|
|
|
|
| 36 |
try:
|
| 37 |
app.mount("/static", StaticFiles(directory="Backend/fastapi/static"), name="static")
|
| 38 |
except Exception:
|
| 39 |
+
pass
|
| 40 |
|
| 41 |
# --- Include existing API routers ---
|
| 42 |
app.include_router(stream_router)
|
|
|
|
| 82 |
|
| 83 |
@app.get("/api/media/list")
|
| 84 |
async def list_media(
|
| 85 |
+
media_type: str = Query("movie", regex="^(movie|tv)$"),
|
| 86 |
+
page: int = Query(1, ge=1),
|
| 87 |
+
page_size: int = Query(24, ge=1, le=100),
|
| 88 |
search: str = Query("", max_length=100),
|
| 89 |
_: bool = Depends(require_auth)
|
| 90 |
):
|
|
|
|
| 99 |
return await update_media_api(request, tmdb_id, db_index, media_type)
|
| 100 |
|
| 101 |
@app.delete("/api/media/delete-quality")
|
| 102 |
+
async def delete_movie_quality(tmdb_id: int, db_index: int, id: str, _: bool = Depends(require_auth)):
|
| 103 |
+
return await delete_movie_quality_api(tmdb_id, db_index, id)
|
| 104 |
|
| 105 |
@app.delete("/api/media/delete-tv-quality")
|
| 106 |
+
async def delete_tv_quality(tmdb_id: int, db_index: int, season: int, episode: int, id: str, _: bool = Depends(require_auth)):
|
| 107 |
+
return await delete_tv_quality_api(tmdb_id, db_index, season, episode, id)
|
| 108 |
|
| 109 |
@app.delete("/api/media/delete-tv-episode")
|
| 110 |
async def delete_tv_episode(tmdb_id: int, db_index: int, season: int, episode: int, _: bool = Depends(require_auth)):
|
|
|
|
| 114 |
async def delete_tv_season(tmdb_id: int, db_index: int, season: int, _: bool = Depends(require_auth)):
|
| 115 |
return await delete_tv_season_api(tmdb_id, db_index, season)
|
| 116 |
|
|
|
|
| 117 |
@app.get("/api/system/workloads")
|
| 118 |
async def get_workloads(_: bool = Depends(require_auth)):
|
| 119 |
try:
|
|
|
|
| 129 |
except Exception as e:
|
| 130 |
return {"loads": {}}
|
| 131 |
|
|
|
|
| 132 |
@app.exception_handler(401)
|
| 133 |
async def auth_exception_handler(request: Request, exc):
|
| 134 |
+
return RedirectResponse(url="/login", status_code=302)
|
Backend/fastapi/routes/api_routes.py
CHANGED
|
@@ -119,9 +119,9 @@ async def get_media_details_api(
|
|
| 119 |
except Exception as e:
|
| 120 |
raise HTTPException(status_code=500, detail=str(e))
|
| 121 |
|
| 122 |
-
async def delete_movie_quality_api(tmdb_id: int, db_index: int,
|
| 123 |
try:
|
| 124 |
-
result = await db.delete_movie_quality(tmdb_id, db_index,
|
| 125 |
if result:
|
| 126 |
return {"message": "Quality deleted successfully"}
|
| 127 |
else:
|
|
@@ -130,12 +130,12 @@ async def delete_movie_quality_api(tmdb_id: int, db_index: int, quality: str):
|
|
| 130 |
raise HTTPException(status_code=500, detail=str(e))
|
| 131 |
|
| 132 |
async def delete_tv_quality_api(
|
| 133 |
-
tmdb_id: int, db_index: int, season: int, episode: int,
|
| 134 |
):
|
| 135 |
try:
|
| 136 |
-
result = await db.delete_tv_quality(tmdb_id, db_index, season, episode,
|
| 137 |
if result:
|
| 138 |
-
return {"message": "
|
| 139 |
else:
|
| 140 |
raise HTTPException(status_code=404, detail="Quality not found")
|
| 141 |
except Exception as e:
|
|
|
|
| 119 |
except Exception as e:
|
| 120 |
raise HTTPException(status_code=500, detail=str(e))
|
| 121 |
|
| 122 |
+
async def delete_movie_quality_api(tmdb_id: int, db_index: int, id: str):
|
| 123 |
try:
|
| 124 |
+
result = await db.delete_movie_quality(tmdb_id, db_index, id)
|
| 125 |
if result:
|
| 126 |
return {"message": "Quality deleted successfully"}
|
| 127 |
else:
|
|
|
|
| 130 |
raise HTTPException(status_code=500, detail=str(e))
|
| 131 |
|
| 132 |
async def delete_tv_quality_api(
|
| 133 |
+
tmdb_id: int, db_index: int, season: int, episode: int, id: str
|
| 134 |
):
|
| 135 |
try:
|
| 136 |
+
result = await db.delete_tv_quality(tmdb_id, db_index, season, episode, id)
|
| 137 |
if result:
|
| 138 |
+
return {"message": "deleted successfully"}
|
| 139 |
else:
|
| 140 |
raise HTTPException(status_code=404, detail="Quality not found")
|
| 141 |
except Exception as e:
|
Backend/fastapi/templates/media_edit.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{% extends "base.html" %}
|
| 2 |
|
| 3 |
-
{% block title %}Edit {{ media_type|title }} -
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-8">
|
|
@@ -220,7 +220,7 @@
|
|
| 220 |
</div>
|
| 221 |
<p class="text-sm text-gray-500 mt-2 sm:mt-1 break-all">{{ quality.name }}</p>
|
| 222 |
</div>
|
| 223 |
-
<button onclick="deleteQuality('{{ quality.
|
| 224 |
class="self-start sm:self-center text-red-600 hover:text-red-800 hover:bg-red-50 px-3 py-1 rounded transition-colors text-sm">
|
| 225 |
Delete
|
| 226 |
</button>
|
|
@@ -277,7 +277,7 @@
|
|
| 277 |
<span class="text-xs text-gray-500">{{ quality.size }}</span>
|
| 278 |
<span class="text-xs text-gray-400 break-all">{{ quality.name }}</span>
|
| 279 |
</div>
|
| 280 |
-
<button onclick="deleteTVQuality({{ season.season_number }}, {{ episode.episode_number }}, '{{ quality.
|
| 281 |
class="self-start sm:self-center text-red-600 hover:text-red-800 hover:bg-red-50 px-2 py-1 rounded transition-colors text-xs">
|
| 282 |
Delete
|
| 283 |
</button>
|
|
@@ -383,47 +383,47 @@ async function updateMedia(event) {
|
|
| 383 |
}
|
| 384 |
}
|
| 385 |
|
| 386 |
-
// Delete
|
| 387 |
-
async function deleteQuality(
|
| 388 |
-
if (!confirm(`Are you sure you want to delete the ${
|
| 389 |
|
| 390 |
try {
|
| 391 |
-
const response = await fetch(`/api/media/delete-quality?tmdb_id=${tmdbId}&db_index=${dbIndex}&
|
| 392 |
method: 'DELETE'
|
| 393 |
});
|
| 394 |
|
| 395 |
if (response.ok) {
|
| 396 |
-
showSuccessMessage(`${
|
| 397 |
setTimeout(() => location.reload(), 1000);
|
| 398 |
} else {
|
| 399 |
const error = await response.json();
|
| 400 |
-
showErrorMessage(`Error deleting
|
| 401 |
}
|
| 402 |
} catch (error) {
|
| 403 |
-
console.error('Error deleting
|
| 404 |
-
showErrorMessage('Error deleting
|
| 405 |
}
|
| 406 |
}
|
| 407 |
|
| 408 |
// Delete TV quality
|
| 409 |
-
async function deleteTVQuality(season, episode,
|
| 410 |
-
if (!confirm(`Are you sure you want to delete the ${
|
| 411 |
|
| 412 |
try {
|
| 413 |
-
const response = await fetch(`/api/media/delete-tv-quality?tmdb_id=${tmdbId}&db_index=${dbIndex}&season=${season}&episode=${episode}&
|
| 414 |
method: 'DELETE'
|
| 415 |
});
|
| 416 |
|
| 417 |
if (response.ok) {
|
| 418 |
-
showSuccessMessage(`${
|
| 419 |
setTimeout(() => location.reload(), 1000);
|
| 420 |
} else {
|
| 421 |
const error = await response.json();
|
| 422 |
-
showErrorMessage(`Error deleting
|
| 423 |
}
|
| 424 |
} catch (error) {
|
| 425 |
-
console.error('Error deleting
|
| 426 |
-
showErrorMessage('Error deleting
|
| 427 |
}
|
| 428 |
}
|
| 429 |
|
|
|
|
| 1 |
{% extends "base.html" %}
|
| 2 |
|
| 3 |
+
{% block title %}Edit {{ media_type|title }} - Fyvio Backend{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-8">
|
|
|
|
| 220 |
</div>
|
| 221 |
<p class="text-sm text-gray-500 mt-2 sm:mt-1 break-all">{{ quality.name }}</p>
|
| 222 |
</div>
|
| 223 |
+
<button onclick="deleteQuality('{{ quality.id }}')"
|
| 224 |
class="self-start sm:self-center text-red-600 hover:text-red-800 hover:bg-red-50 px-3 py-1 rounded transition-colors text-sm">
|
| 225 |
Delete
|
| 226 |
</button>
|
|
|
|
| 277 |
<span class="text-xs text-gray-500">{{ quality.size }}</span>
|
| 278 |
<span class="text-xs text-gray-400 break-all">{{ quality.name }}</span>
|
| 279 |
</div>
|
| 280 |
+
<button onclick="deleteTVQuality({{ season.season_number }}, {{ episode.episode_number }}, '{{ quality.id }}')"
|
| 281 |
class="self-start sm:self-center text-red-600 hover:text-red-800 hover:bg-red-50 px-2 py-1 rounded transition-colors text-xs">
|
| 282 |
Delete
|
| 283 |
</button>
|
|
|
|
| 383 |
}
|
| 384 |
}
|
| 385 |
|
| 386 |
+
// Delete name from movie
|
| 387 |
+
async function deleteQuality(id) {
|
| 388 |
+
if (!confirm(`Are you sure you want to delete the ${id} ?`)) return;
|
| 389 |
|
| 390 |
try {
|
| 391 |
+
const response = await fetch(`/api/media/delete-quality?tmdb_id=${tmdbId}&db_index=${dbIndex}&id=${encodeURIComponent(id)}`, {
|
| 392 |
method: 'DELETE'
|
| 393 |
});
|
| 394 |
|
| 395 |
if (response.ok) {
|
| 396 |
+
showSuccessMessage(`${id} deleted successfully`);
|
| 397 |
setTimeout(() => location.reload(), 1000);
|
| 398 |
} else {
|
| 399 |
const error = await response.json();
|
| 400 |
+
showErrorMessage(`Error deleting : ${error.detail || 'Unknown error'}`);
|
| 401 |
}
|
| 402 |
} catch (error) {
|
| 403 |
+
console.error('Error deleting :', error);
|
| 404 |
+
showErrorMessage('Error deleting . Please try again.');
|
| 405 |
}
|
| 406 |
}
|
| 407 |
|
| 408 |
// Delete TV quality
|
| 409 |
+
async function deleteTVQuality(season, episode, id) {
|
| 410 |
+
if (!confirm(`Are you sure you want to delete the ${id} from Season ${season} Episode ${episode}?`)) return;
|
| 411 |
|
| 412 |
try {
|
| 413 |
+
const response = await fetch(`/api/media/delete-tv-quality?tmdb_id=${tmdbId}&db_index=${dbIndex}&season=${season}&episode=${episode}&id=${encodeURIComponent(id)}`, {
|
| 414 |
method: 'DELETE'
|
| 415 |
});
|
| 416 |
|
| 417 |
if (response.ok) {
|
| 418 |
+
showSuccessMessage(`${id} deleted successfully`);
|
| 419 |
setTimeout(() => location.reload(), 1000);
|
| 420 |
} else {
|
| 421 |
const error = await response.json();
|
| 422 |
+
showErrorMessage(`Error deleting : ${error.detail || 'Unknown error'}`);
|
| 423 |
}
|
| 424 |
} catch (error) {
|
| 425 |
+
console.error('Error deleting :', error);
|
| 426 |
+
showErrorMessage('Error deleting. Please try again.');
|
| 427 |
}
|
| 428 |
}
|
| 429 |
|
Backend/helper/database.py
CHANGED
|
@@ -249,34 +249,38 @@ class Database:
|
|
| 249 |
tmdb_id = movie_dict["tmdb_id"]
|
| 250 |
title = movie_dict["title"]
|
| 251 |
release_year = movie_dict["release_year"]
|
|
|
|
| 252 |
quality_to_update = movie_dict["telegram"][0]
|
| 253 |
target_quality = quality_to_update["quality"]
|
|
|
|
| 254 |
current_db_key = f"storage_{self.current_db_index}"
|
|
|
|
| 255 |
|
| 256 |
-
|
| 257 |
existing_db_key = None
|
| 258 |
existing_db_index = None
|
| 259 |
-
existing_movie = None
|
| 260 |
|
| 261 |
-
|
| 262 |
for db_index in range(1, total_storage_dbs + 1):
|
| 263 |
db_key = f"storage_{db_index}"
|
| 264 |
movie = None
|
|
|
|
| 265 |
if imdb_id:
|
| 266 |
movie = await self.dbs[db_key]["movie"].find_one({"imdb_id": imdb_id})
|
| 267 |
if not movie and tmdb_id:
|
| 268 |
movie = await self.dbs[db_key]["movie"].find_one({"tmdb_id": tmdb_id})
|
| 269 |
if not movie and title and release_year:
|
| 270 |
movie = await self.dbs[db_key]["movie"].find_one({
|
| 271 |
-
"title": title,
|
| 272 |
"release_year": release_year
|
| 273 |
})
|
|
|
|
| 274 |
if movie:
|
|
|
|
| 275 |
existing_db_key = db_key
|
| 276 |
existing_db_index = db_index
|
| 277 |
-
existing_movie = movie
|
| 278 |
break
|
| 279 |
|
|
|
|
| 280 |
if not existing_movie:
|
| 281 |
try:
|
| 282 |
movie_dict["db_index"] = self.current_db_index
|
|
@@ -288,24 +292,34 @@ class Database:
|
|
| 288 |
return await self._handle_storage_error(self.update_movie, movie_data, total_storage_dbs=total_storage_dbs)
|
| 289 |
return None
|
| 290 |
|
|
|
|
| 291 |
movie_id = existing_movie["_id"]
|
| 292 |
existing_qualities = existing_movie.get("telegram", [])
|
| 293 |
-
matching_quality = next((q for q in existing_qualities if q["quality"] == target_quality), None)
|
| 294 |
-
if matching_quality:
|
| 295 |
-
|
| 296 |
-
try:
|
| 297 |
-
old_id = matching_quality.get("id")
|
| 298 |
-
if old_id:
|
| 299 |
-
decoded_data = await decode_string(old_id)
|
| 300 |
-
chat_id = int(f"-100{decoded_data['chat_id']}")
|
| 301 |
-
msg_id = int(decoded_data['msg_id'])
|
| 302 |
-
create_task(delete_message(chat_id, msg_id))
|
| 303 |
-
except Exception as e:
|
| 304 |
-
LOGGER.error(f"Failed to queue old quality file for deletion: {e}")
|
| 305 |
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
else:
|
|
|
|
| 308 |
existing_qualities.append(quality_to_update)
|
|
|
|
| 309 |
existing_movie["telegram"] = existing_qualities
|
| 310 |
existing_movie["updated_on"] = datetime.utcnow()
|
| 311 |
|
|
@@ -332,21 +346,23 @@ class Database:
|
|
| 332 |
except ValidationError as e:
|
| 333 |
LOGGER.error(f"Validation error: {e}")
|
| 334 |
return None
|
| 335 |
-
|
| 336 |
imdb_id = tv_show_dict.get("imdb_id")
|
| 337 |
tmdb_id = tv_show_dict.get("tmdb_id")
|
| 338 |
title = tv_show_dict["title"]
|
| 339 |
release_year = tv_show_dict["release_year"]
|
|
|
|
| 340 |
current_db_key = f"storage_{self.current_db_index}"
|
| 341 |
total_storage_dbs = len(self.dbs) - 1
|
| 342 |
|
|
|
|
| 343 |
existing_db_key = None
|
| 344 |
existing_db_index = None
|
| 345 |
-
existing_tv = None
|
| 346 |
|
| 347 |
for db_index in range(1, total_storage_dbs + 1):
|
| 348 |
db_key = f"storage_{db_index}"
|
| 349 |
tv = None
|
|
|
|
| 350 |
if imdb_id:
|
| 351 |
tv = await self.dbs[db_key]["tv"].find_one({"imdb_id": imdb_id})
|
| 352 |
if not tv and tmdb_id:
|
|
@@ -356,12 +372,14 @@ class Database:
|
|
| 356 |
"title": title,
|
| 357 |
"release_year": release_year
|
| 358 |
})
|
|
|
|
| 359 |
if tv:
|
|
|
|
| 360 |
existing_db_key = db_key
|
| 361 |
existing_db_index = db_index
|
| 362 |
-
existing_tv = tv
|
| 363 |
break
|
| 364 |
|
|
|
|
| 365 |
if not existing_tv:
|
| 366 |
try:
|
| 367 |
tv_show_dict["db_index"] = self.current_db_index
|
|
@@ -373,46 +391,65 @@ class Database:
|
|
| 373 |
return await self._handle_storage_error(self.update_tv_show, tv_show_data, total_storage_dbs=total_storage_dbs)
|
| 374 |
return None
|
| 375 |
|
|
|
|
| 376 |
tv_id = existing_tv["_id"]
|
|
|
|
| 377 |
for season in tv_show_dict["seasons"]:
|
| 378 |
existing_season = next(
|
| 379 |
-
(s for s in existing_tv["seasons"]
|
|
|
|
|
|
|
| 380 |
)
|
| 381 |
-
if existing_season:
|
| 382 |
-
for episode in season["episodes"]:
|
| 383 |
-
existing_episode = next(
|
| 384 |
-
(e for e in existing_season["episodes"] if e["episode_number"] == episode["episode_number"]), None
|
| 385 |
-
)
|
| 386 |
-
if existing_episode:
|
| 387 |
-
existing_episode.setdefault("telegram", [])
|
| 388 |
-
for quality in episode["telegram"]:
|
| 389 |
-
existing_quality = next(
|
| 390 |
-
(q for q in existing_episode["telegram"]
|
| 391 |
-
if q.get("quality") == quality.get("quality")),
|
| 392 |
-
None
|
| 393 |
-
)
|
| 394 |
-
if existing_quality:
|
| 395 |
-
try:
|
| 396 |
-
old_id = existing_quality.get("id")
|
| 397 |
|
| 398 |
-
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
|
| 401 |
-
chat_id = int(f"-100{decoded_data['chat_id']}")
|
| 402 |
-
msg_id = int(decoded_data['msg_id'])
|
| 403 |
-
create_task(delete_message(chat_id, msg_id))
|
| 404 |
-
|
| 405 |
-
except Exception as e:
|
| 406 |
-
LOGGER.error(f"Failed to queue old quality file for deletion: {e}")
|
| 407 |
-
existing_quality.update(quality)
|
| 408 |
-
else:
|
| 409 |
-
existing_episode["telegram"].append(quality)
|
| 410 |
else:
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
existing_tv["seasons"].append(season)
|
| 414 |
existing_tv["updated_on"] = datetime.utcnow()
|
| 415 |
|
|
|
|
| 416 |
if existing_db_index != self.current_db_index:
|
| 417 |
try:
|
| 418 |
if await self._move_document("tv", existing_tv, existing_db_index):
|
|
@@ -714,7 +751,7 @@ class Database:
|
|
| 714 |
LOGGER.info(f"No document found with tmdb_id {tmdb_id}.")
|
| 715 |
return False
|
| 716 |
|
| 717 |
-
async def delete_movie_quality(self, tmdb_id: int, db_index: int,
|
| 718 |
db_key = f"storage_{db_index}"
|
| 719 |
movie = await self.dbs[db_key]["movie"].find_one({"tmdb_id": tmdb_id})
|
| 720 |
|
|
@@ -722,7 +759,7 @@ class Database:
|
|
| 722 |
return False
|
| 723 |
|
| 724 |
for q in movie["telegram"]:
|
| 725 |
-
if q.get("
|
| 726 |
try:
|
| 727 |
old_id = q.get("id")
|
| 728 |
if old_id:
|
|
@@ -735,7 +772,7 @@ class Database:
|
|
| 735 |
break
|
| 736 |
|
| 737 |
original_len = len(movie["telegram"])
|
| 738 |
-
movie["telegram"] = [q for q in movie["telegram"] if q.get("
|
| 739 |
|
| 740 |
if len(movie["telegram"]) == original_len:
|
| 741 |
return False
|
|
@@ -812,7 +849,7 @@ class Database:
|
|
| 812 |
result = await self.dbs[db_key]["tv"].replace_one({"tmdb_id": tmdb_id}, tv)
|
| 813 |
return result.modified_count > 0
|
| 814 |
|
| 815 |
-
async def delete_tv_quality(self, tmdb_id: int, db_index: int, season_number: int, episode_number: int,
|
| 816 |
db_key = f"storage_{db_index}"
|
| 817 |
tv = await self.dbs[db_key]["tv"].find_one({"tmdb_id": tmdb_id})
|
| 818 |
|
|
@@ -825,7 +862,7 @@ class Database:
|
|
| 825 |
for episode in season["episodes"]:
|
| 826 |
if episode.get("episode_number") == episode_number and "telegram" in episode:
|
| 827 |
for q in episode["telegram"]:
|
| 828 |
-
if q.get("
|
| 829 |
try:
|
| 830 |
old_id = q.get("id")
|
| 831 |
if old_id:
|
|
@@ -838,7 +875,7 @@ class Database:
|
|
| 838 |
break
|
| 839 |
|
| 840 |
original_len = len(episode["telegram"])
|
| 841 |
-
episode["telegram"] = [q for q in episode["telegram"] if q.get("
|
| 842 |
found = original_len > len(episode["telegram"])
|
| 843 |
break
|
| 844 |
|
|
|
|
| 249 |
tmdb_id = movie_dict["tmdb_id"]
|
| 250 |
title = movie_dict["title"]
|
| 251 |
release_year = movie_dict["release_year"]
|
| 252 |
+
|
| 253 |
quality_to_update = movie_dict["telegram"][0]
|
| 254 |
target_quality = quality_to_update["quality"]
|
| 255 |
+
|
| 256 |
current_db_key = f"storage_{self.current_db_index}"
|
| 257 |
+
total_storage_dbs = len(self.dbs) - 1
|
| 258 |
|
| 259 |
+
existing_movie = None
|
| 260 |
existing_db_key = None
|
| 261 |
existing_db_index = None
|
|
|
|
| 262 |
|
|
|
|
| 263 |
for db_index in range(1, total_storage_dbs + 1):
|
| 264 |
db_key = f"storage_{db_index}"
|
| 265 |
movie = None
|
| 266 |
+
|
| 267 |
if imdb_id:
|
| 268 |
movie = await self.dbs[db_key]["movie"].find_one({"imdb_id": imdb_id})
|
| 269 |
if not movie and tmdb_id:
|
| 270 |
movie = await self.dbs[db_key]["movie"].find_one({"tmdb_id": tmdb_id})
|
| 271 |
if not movie and title and release_year:
|
| 272 |
movie = await self.dbs[db_key]["movie"].find_one({
|
| 273 |
+
"title": title,
|
| 274 |
"release_year": release_year
|
| 275 |
})
|
| 276 |
+
|
| 277 |
if movie:
|
| 278 |
+
existing_movie = movie
|
| 279 |
existing_db_key = db_key
|
| 280 |
existing_db_index = db_index
|
|
|
|
| 281 |
break
|
| 282 |
|
| 283 |
+
# ---------------- INSERT NEW MOVIE ----------------
|
| 284 |
if not existing_movie:
|
| 285 |
try:
|
| 286 |
movie_dict["db_index"] = self.current_db_index
|
|
|
|
| 292 |
return await self._handle_storage_error(self.update_movie, movie_data, total_storage_dbs=total_storage_dbs)
|
| 293 |
return None
|
| 294 |
|
| 295 |
+
# ---------------- UPDATE MOVIE ----------------
|
| 296 |
movie_id = existing_movie["_id"]
|
| 297 |
existing_qualities = existing_movie.get("telegram", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
|
| 299 |
+
if Telegram.REPLACE_MODE:
|
| 300 |
+
# delete all same-quality entries
|
| 301 |
+
to_delete = [q for q in existing_qualities if q.get("quality") == target_quality]
|
| 302 |
+
|
| 303 |
+
for q in to_delete:
|
| 304 |
+
try:
|
| 305 |
+
old_id = q.get("id")
|
| 306 |
+
if old_id:
|
| 307 |
+
decoded = await decode_string(old_id)
|
| 308 |
+
chat_id = int(f"-100{decoded['chat_id']}")
|
| 309 |
+
msg_id = int(decoded['msg_id'])
|
| 310 |
+
create_task(delete_message(chat_id, msg_id))
|
| 311 |
+
except Exception as e:
|
| 312 |
+
LOGGER.error(f"Failed to delete old quality: {e}")
|
| 313 |
+
|
| 314 |
+
existing_qualities = [
|
| 315 |
+
q for q in existing_qualities if q.get("quality") != target_quality
|
| 316 |
+
]
|
| 317 |
+
existing_qualities.append(quality_to_update)
|
| 318 |
+
|
| 319 |
else:
|
| 320 |
+
# allow duplicate qualities
|
| 321 |
existing_qualities.append(quality_to_update)
|
| 322 |
+
|
| 323 |
existing_movie["telegram"] = existing_qualities
|
| 324 |
existing_movie["updated_on"] = datetime.utcnow()
|
| 325 |
|
|
|
|
| 346 |
except ValidationError as e:
|
| 347 |
LOGGER.error(f"Validation error: {e}")
|
| 348 |
return None
|
| 349 |
+
|
| 350 |
imdb_id = tv_show_dict.get("imdb_id")
|
| 351 |
tmdb_id = tv_show_dict.get("tmdb_id")
|
| 352 |
title = tv_show_dict["title"]
|
| 353 |
release_year = tv_show_dict["release_year"]
|
| 354 |
+
|
| 355 |
current_db_key = f"storage_{self.current_db_index}"
|
| 356 |
total_storage_dbs = len(self.dbs) - 1
|
| 357 |
|
| 358 |
+
existing_tv = None
|
| 359 |
existing_db_key = None
|
| 360 |
existing_db_index = None
|
|
|
|
| 361 |
|
| 362 |
for db_index in range(1, total_storage_dbs + 1):
|
| 363 |
db_key = f"storage_{db_index}"
|
| 364 |
tv = None
|
| 365 |
+
|
| 366 |
if imdb_id:
|
| 367 |
tv = await self.dbs[db_key]["tv"].find_one({"imdb_id": imdb_id})
|
| 368 |
if not tv and tmdb_id:
|
|
|
|
| 372 |
"title": title,
|
| 373 |
"release_year": release_year
|
| 374 |
})
|
| 375 |
+
|
| 376 |
if tv:
|
| 377 |
+
existing_tv = tv
|
| 378 |
existing_db_key = db_key
|
| 379 |
existing_db_index = db_index
|
|
|
|
| 380 |
break
|
| 381 |
|
| 382 |
+
# ---------------- INSERT NEW TV ----------------
|
| 383 |
if not existing_tv:
|
| 384 |
try:
|
| 385 |
tv_show_dict["db_index"] = self.current_db_index
|
|
|
|
| 391 |
return await self._handle_storage_error(self.update_tv_show, tv_show_data, total_storage_dbs=total_storage_dbs)
|
| 392 |
return None
|
| 393 |
|
| 394 |
+
# ---------------- UPDATE TV ----------------
|
| 395 |
tv_id = existing_tv["_id"]
|
| 396 |
+
|
| 397 |
for season in tv_show_dict["seasons"]:
|
| 398 |
existing_season = next(
|
| 399 |
+
(s for s in existing_tv["seasons"]
|
| 400 |
+
if s["season_number"] == season["season_number"]),
|
| 401 |
+
None
|
| 402 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
+
if not existing_season:
|
| 405 |
+
existing_tv["seasons"].append(season)
|
| 406 |
+
continue
|
| 407 |
+
|
| 408 |
+
for episode in season["episodes"]:
|
| 409 |
+
existing_episode = next(
|
| 410 |
+
(e for e in existing_season["episodes"]
|
| 411 |
+
if e["episode_number"] == episode["episode_number"]),
|
| 412 |
+
None
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
if not existing_episode:
|
| 416 |
+
existing_season["episodes"].append(episode)
|
| 417 |
+
continue
|
| 418 |
+
|
| 419 |
+
existing_episode.setdefault("telegram", [])
|
| 420 |
+
|
| 421 |
+
for quality in episode["telegram"]:
|
| 422 |
+
target_quality = quality.get("quality")
|
| 423 |
+
|
| 424 |
+
if Telegram.REPLACE_MODE:
|
| 425 |
+
to_delete = [
|
| 426 |
+
q for q in existing_episode["telegram"]
|
| 427 |
+
if q.get("quality") == target_quality
|
| 428 |
+
]
|
| 429 |
+
|
| 430 |
+
for q in to_delete:
|
| 431 |
+
try:
|
| 432 |
+
old_id = q.get("id")
|
| 433 |
+
if old_id:
|
| 434 |
+
decoded = await decode_string(old_id)
|
| 435 |
+
chat_id = int(f"-100{decoded['chat_id']}")
|
| 436 |
+
msg_id = int(decoded['msg_id'])
|
| 437 |
+
create_task(delete_message(chat_id, msg_id))
|
| 438 |
+
except Exception as e:
|
| 439 |
+
LOGGER.error(f"Failed to delete old quality: {e}")
|
| 440 |
+
|
| 441 |
+
existing_episode["telegram"] = [
|
| 442 |
+
q for q in existing_episode["telegram"]
|
| 443 |
+
if q.get("quality") != target_quality
|
| 444 |
+
]
|
| 445 |
+
existing_episode["telegram"].append(quality)
|
| 446 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
else:
|
| 448 |
+
existing_episode["telegram"].append(quality)
|
| 449 |
+
|
|
|
|
| 450 |
existing_tv["updated_on"] = datetime.utcnow()
|
| 451 |
|
| 452 |
+
# ---------------- MOVE DB IF NEEDED ----------------
|
| 453 |
if existing_db_index != self.current_db_index:
|
| 454 |
try:
|
| 455 |
if await self._move_document("tv", existing_tv, existing_db_index):
|
|
|
|
| 751 |
LOGGER.info(f"No document found with tmdb_id {tmdb_id}.")
|
| 752 |
return False
|
| 753 |
|
| 754 |
+
async def delete_movie_quality(self, tmdb_id: int, db_index: int, id: str) -> bool:
|
| 755 |
db_key = f"storage_{db_index}"
|
| 756 |
movie = await self.dbs[db_key]["movie"].find_one({"tmdb_id": tmdb_id})
|
| 757 |
|
|
|
|
| 759 |
return False
|
| 760 |
|
| 761 |
for q in movie["telegram"]:
|
| 762 |
+
if q.get("id") == id:
|
| 763 |
try:
|
| 764 |
old_id = q.get("id")
|
| 765 |
if old_id:
|
|
|
|
| 772 |
break
|
| 773 |
|
| 774 |
original_len = len(movie["telegram"])
|
| 775 |
+
movie["telegram"] = [q for q in movie["telegram"] if q.get("id") != id]
|
| 776 |
|
| 777 |
if len(movie["telegram"]) == original_len:
|
| 778 |
return False
|
|
|
|
| 849 |
result = await self.dbs[db_key]["tv"].replace_one({"tmdb_id": tmdb_id}, tv)
|
| 850 |
return result.modified_count > 0
|
| 851 |
|
| 852 |
+
async def delete_tv_quality(self, tmdb_id: int, db_index: int, season_number: int, episode_number: int, id: str) -> bool:
|
| 853 |
db_key = f"storage_{db_index}"
|
| 854 |
tv = await self.dbs[db_key]["tv"].find_one({"tmdb_id": tmdb_id})
|
| 855 |
|
|
|
|
| 862 |
for episode in season["episodes"]:
|
| 863 |
if episode.get("episode_number") == episode_number and "telegram" in episode:
|
| 864 |
for q in episode["telegram"]:
|
| 865 |
+
if q.get("id") == id:
|
| 866 |
try:
|
| 867 |
old_id = q.get("id")
|
| 868 |
if old_id:
|
|
|
|
| 875 |
break
|
| 876 |
|
| 877 |
original_len = len(episode["telegram"])
|
| 878 |
+
episode["telegram"] = [q for q in episode["telegram"] if q.get("id") != id]
|
| 879 |
found = original_len > len(episode["telegram"])
|
| 880 |
break
|
| 881 |
|
README.md
CHANGED
|
@@ -217,6 +217,7 @@ All environment variables for this project are defined in the `config.env` file.
|
|
| 217 |
| **`BOT_TOKEN`** | The main bot’s **access token** from [@BotFather](https://t.me/BotFather). Handles user requests and media fetching. |
|
| 218 |
| **`HELPER_BOT_TOKEN`** | **Secondary bot token** used to assist the main bot with tasks like deleting, editing, or managing. |
|
| 219 |
| **`OWNER_ID`** | Your **Telegram user ID**. This ID has full administrative access. |
|
|
|
|
| 220 |
|
| 221 |
### 🗄️ Storage
|
| 222 |
|
|
|
|
| 217 |
| **`BOT_TOKEN`** | The main bot’s **access token** from [@BotFather](https://t.me/BotFather). Handles user requests and media fetching. |
|
| 218 |
| **`HELPER_BOT_TOKEN`** | **Secondary bot token** used to assist the main bot with tasks like deleting, editing, or managing. |
|
| 219 |
| **`OWNER_ID`** | Your **Telegram user ID**. This ID has full administrative access. |
|
| 220 |
+
| **`REPLACE_MODE`** | When `true`, new files replace existing files of the same quality. When `false`, multiple files of the same quality are allowed. |
|
| 221 |
|
| 222 |
### 🗄️ Storage
|
| 223 |
|
pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "Telegram-Stremio"
|
| 3 |
-
version = "1.
|
| 4 |
description = "A powerful, self-hosted Telegram Stremio Media Server built with FastAPI, MongoDB, and PyroFork seamlessly integrated with Stremio for automated media streaming and discovery."
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.11"
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "Telegram-Stremio"
|
| 3 |
+
version = "1.5.0"
|
| 4 |
description = "A powerful, self-hosted Telegram Stremio Media Server built with FastAPI, MongoDB, and PyroFork seamlessly integrated with Stremio for automated media streaming and discovery."
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.11"
|
uv.lock
CHANGED
|
@@ -901,7 +901,7 @@ wheels = [
|
|
| 901 |
|
| 902 |
[[package]]
|
| 903 |
name = "telegram-stremio"
|
| 904 |
-
version = "1.4.
|
| 905 |
source = { virtual = "." }
|
| 906 |
dependencies = [
|
| 907 |
{ name = "aiofiles" },
|
|
|
|
| 901 |
|
| 902 |
[[package]]
|
| 903 |
name = "telegram-stremio"
|
| 904 |
+
version = "1.4.4"
|
| 905 |
source = { virtual = "." }
|
| 906 |
dependencies = [
|
| 907 |
{ name = "aiofiles" },
|