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 CHANGED
@@ -11,4 +11,4 @@ StartTime = time()
11
  USE_DEFAULT_ID: str = None
12
  db = Database()
13
 
14
- __version__ = "1.4.4"
 
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, quality: str, _: bool = Depends(require_auth)):
104
- return await delete_movie_quality_api(tmdb_id, db_index, quality)
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, quality: str, _: bool = Depends(require_auth)):
108
- return await delete_tv_quality_api(tmdb_id, db_index, season, episode, quality)
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, quality: str):
123
  try:
124
- result = await db.delete_movie_quality(tmdb_id, db_index, quality)
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, quality: str
134
  ):
135
  try:
136
- result = await db.delete_tv_quality(tmdb_id, db_index, season, episode, quality)
137
  if result:
138
- return {"message": "Quality deleted successfully"}
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 }} - Telegram Stremio{% 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,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.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.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 quality from movie
387
- async function deleteQuality(quality) {
388
- if (!confirm(`Are you sure you want to delete the ${quality} quality?`)) return;
389
 
390
  try {
391
- const response = await fetch(`/api/media/delete-quality?tmdb_id=${tmdbId}&db_index=${dbIndex}&quality=${encodeURIComponent(quality)}`, {
392
  method: 'DELETE'
393
  });
394
 
395
  if (response.ok) {
396
- showSuccessMessage(`${quality} quality deleted successfully`);
397
  setTimeout(() => location.reload(), 1000);
398
  } else {
399
  const error = await response.json();
400
- showErrorMessage(`Error deleting quality: ${error.detail || 'Unknown error'}`);
401
  }
402
  } catch (error) {
403
- console.error('Error deleting quality:', error);
404
- showErrorMessage('Error deleting quality. Please try again.');
405
  }
406
  }
407
 
408
  // Delete TV quality
409
- async function deleteTVQuality(season, episode, quality) {
410
- if (!confirm(`Are you sure you want to delete the ${quality} quality 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}&quality=${encodeURIComponent(quality)}`, {
414
  method: 'DELETE'
415
  });
416
 
417
  if (response.ok) {
418
- showSuccessMessage(`${quality} quality deleted successfully`);
419
  setTimeout(() => location.reload(), 1000);
420
  } else {
421
  const error = await response.json();
422
- showErrorMessage(`Error deleting quality: ${error.detail || 'Unknown error'}`);
423
  }
424
  } catch (error) {
425
- console.error('Error deleting quality:', error);
426
- showErrorMessage('Error deleting quality. Please try again.');
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
- total_storage_dbs = len(self.dbs) - 1
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
- matching_quality.update(quality_to_update)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"] if s["season_number"] == season["season_number"]), None
 
 
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
- if old_id:
399
- decoded_data = await decode_string(old_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- existing_season["episodes"].append(episode)
412
- else:
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, quality: str) -> bool:
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("quality") == quality:
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("quality") != quality]
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, quality: str) -> bool:
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("quality") == quality:
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("quality") != quality]
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.4"
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.3"
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" },