weebzone commited on
Commit
ed5fc32
·
1 Parent(s): f1ea5be

`Release v1.3.0`

Browse files

### Features
✨ Add 10 new attractive themes to admin panel
- Includes Crimson Elegance, Coral Bliss, Cyber Neon, Forest Earth, Lavender Dream
- Includes Golden Luxury, Arctic Frost, Cherry Cola, Emerald Tech, Midnight Carbon
- Enhanced theme customization options for better UI/UX

✨ Add genre filter support across all platforms
- Mobile app genre filtering
- Web interface genre filtering
- TV application genre filtering

### Bug Fixes
🐛 Fix missing IMDb ID in TMDb fallback code
- Properly handle IMDb ID extraction when TMDb search fails
- Improved metadata retrieval reliability

🐛 Fix start command behavior
- Resolved minor issues with bot initialization

### Enhancements
♻️ Implement automatic file cleanup on deletion
- Delete Telegram channel files when content is removed from admin panel
- Prevents orphaned files and saves storage space
- Added file deletion for movies, TV shows, episodes, seasons, and quality variants

### Breaking Changes
None

### Dependencies
No new dependencies added

Backend/__init__.py CHANGED
@@ -11,4 +11,4 @@ StartTime = time()
11
  USE_DEFAULT_ID: str = None
12
  db = Database()
13
 
14
- __version__ = "1.2.1"
 
11
  USE_DEFAULT_ID: str = None
12
  db = Database()
13
 
14
+ __version__ = "1.3.0"
Backend/fastapi/themes.py CHANGED
@@ -64,11 +64,143 @@ THEMES = {
64
  "text_secondary": "#CBD5E1"
65
  },
66
  "css_classes": "theme-dark-professional"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
  }
69
 
 
70
  def get_theme(theme_name: str = "purple_gradient"):
71
  return THEMES.get(theme_name, THEMES["purple_gradient"])
72
 
 
73
  def get_all_themes():
74
  return THEMES
 
64
  "text_secondary": "#CBD5E1"
65
  },
66
  "css_classes": "theme-dark-professional"
67
+ },
68
+ "crimson_elegance": {
69
+ "name": "Crimson Elegance",
70
+ "colors": {
71
+ "primary": "#DC2626",
72
+ "secondary": "#991B1B",
73
+ "accent": "#F87171",
74
+ "background": "#FEF2F2",
75
+ "card": "#FFFFFF",
76
+ "text": "#1F2937",
77
+ "text_secondary": "#6B7280"
78
+ },
79
+ "css_classes": "theme-crimson-elegance"
80
+ },
81
+ "coral_bliss": {
82
+ "name": "Coral Bliss",
83
+ "colors": {
84
+ "primary": "#FF6B6B",
85
+ "secondary": "#EE5A6F",
86
+ "accent": "#FFB347",
87
+ "background": "#FFF5F5",
88
+ "card": "#FFFFFF",
89
+ "text": "#2D3748",
90
+ "text_secondary": "#718096"
91
+ },
92
+ "css_classes": "theme-coral-bliss"
93
+ },
94
+ "cyber_neon": {
95
+ "name": "Cyber Neon",
96
+ "colors": {
97
+ "primary": "#00FFF0",
98
+ "secondary": "#FF00FF",
99
+ "accent": "#00FF88",
100
+ "background": "#0A0E27",
101
+ "card": "#1A1F3A",
102
+ "text": "#FFFFFF",
103
+ "text_secondary": "#A0AEC0"
104
+ },
105
+ "css_classes": "theme-cyber-neon"
106
+ },
107
+ "forest_earth": {
108
+ "name": "Forest Earth",
109
+ "colors": {
110
+ "primary": "#2D5016",
111
+ "secondary": "#50623A",
112
+ "accent": "#86A789",
113
+ "background": "#F5F5DC",
114
+ "card": "#FFFFFF",
115
+ "text": "#1C1C1C",
116
+ "text_secondary": "#5C5C5C"
117
+ },
118
+ "css_classes": "theme-forest-earth"
119
+ },
120
+ "lavender_dream": {
121
+ "name": "Lavender Dream",
122
+ "colors": {
123
+ "primary": "#B08BBB",
124
+ "secondary": "#9370DB",
125
+ "accent": "#DDA0DD",
126
+ "background": "#FAF5FF",
127
+ "card": "#FFFFFF",
128
+ "text": "#2D3748",
129
+ "text_secondary": "#718096"
130
+ },
131
+ "css_classes": "theme-lavender-dream"
132
+ },
133
+ "golden_luxury": {
134
+ "name": "Golden Luxury",
135
+ "colors": {
136
+ "primary": "#D4AF37",
137
+ "secondary": "#B8860B",
138
+ "accent": "#FFD700",
139
+ "background": "#FFFEF7",
140
+ "card": "#FFFFFF",
141
+ "text": "#1A202C",
142
+ "text_secondary": "#4A5568"
143
+ },
144
+ "css_classes": "theme-golden-luxury"
145
+ },
146
+ "arctic_frost": {
147
+ "name": "Arctic Frost",
148
+ "colors": {
149
+ "primary": "#B0E0E6",
150
+ "secondary": "#4682B4",
151
+ "accent": "#87CEEB",
152
+ "background": "#F0F8FF",
153
+ "card": "#FFFFFF",
154
+ "text": "#1E3A5F",
155
+ "text_secondary": "#5B7C99"
156
+ },
157
+ "css_classes": "theme-arctic-frost"
158
+ },
159
+ "cherry_cola": {
160
+ "name": "Cherry Cola",
161
+ "colors": {
162
+ "primary": "#BF1922",
163
+ "secondary": "#8B0000",
164
+ "accent": "#DC143C",
165
+ "background": "#FFF0F0",
166
+ "card": "#FFFFFF",
167
+ "text": "#1F2937",
168
+ "text_secondary": "#6B7280"
169
+ },
170
+ "css_classes": "theme-cherry-cola"
171
+ },
172
+ "emerald_tech": {
173
+ "name": "Emerald Tech",
174
+ "colors": {
175
+ "primary": "#047857",
176
+ "secondary": "#065F46",
177
+ "accent": "#10B981",
178
+ "background": "#ECFDF5",
179
+ "card": "#FFFFFF",
180
+ "text": "#1F2937",
181
+ "text_secondary": "#4B5563"
182
+ },
183
+ "css_classes": "theme-emerald-tech"
184
+ },
185
+ "midnight_carbon": {
186
+ "name": "Midnight Carbon",
187
+ "colors": {
188
+ "primary": "#3B82F6",
189
+ "secondary": "#1E40AF",
190
+ "accent": "#60A5FA",
191
+ "background": "#111827",
192
+ "card": "#1F2937",
193
+ "text": "#F9FAFB",
194
+ "text_secondary": "#D1D5DB"
195
+ },
196
+ "css_classes": "theme-midnight-carbon"
197
  }
198
  }
199
 
200
+
201
  def get_theme(theme_name: str = "purple_gradient"):
202
  return THEMES.get(theme_name, THEMES["purple_gradient"])
203
 
204
+
205
  def get_all_themes():
206
  return THEMES
Backend/helper/database.py CHANGED
@@ -654,10 +654,40 @@ class Database:
654
  # Delete a Movie or Tvshow completely
655
  async def delete_document(self, media_type: str, tmdb_id: int, db_index: int) -> bool:
656
  db_key = f"storage_{db_index}"
 
657
  if media_type == "Movie":
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  result = await self.dbs[db_key]["movie"].delete_one({"tmdb_id": tmdb_id})
659
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  result = await self.dbs[db_key]["tv"].delete_one({"tmdb_id": tmdb_id})
 
661
  if result.deleted_count > 0:
662
  LOGGER.info(f"{media_type} with tmdb_id {tmdb_id} deleted successfully.")
663
  return True
@@ -669,12 +699,29 @@ class Database:
669
  async def delete_movie_quality(self, tmdb_id: int, db_index: int, quality: str) -> bool:
670
  db_key = f"storage_{db_index}"
671
  movie = await self.dbs[db_key]["movie"].find_one({"tmdb_id": tmdb_id})
 
672
  if not movie or "telegram" not in movie:
673
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
674
  original_len = len(movie["telegram"])
675
  movie["telegram"] = [q for q in movie["telegram"] if q.get("quality") != quality]
 
676
  if len(movie["telegram"]) == original_len:
677
- return False
 
678
  movie['updated_on'] = datetime.utcnow()
679
  result = await self.dbs[db_key]["movie"].replace_one({"tmdb_id": tmdb_id}, movie)
680
  return result.modified_count > 0
@@ -683,17 +730,35 @@ class Database:
683
  async def delete_tv_episode(self, tmdb_id: int, db_index: int, season_number: int, episode_number: int) -> bool:
684
  db_key = f"storage_{db_index}"
685
  tv = await self.dbs[db_key]["tv"].find_one({"tmdb_id": tmdb_id})
 
686
  if not tv or "seasons" not in tv:
687
  return False
 
688
  found = False
689
  for season in tv["seasons"]:
690
  if season.get("season_number") == season_number:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  original_len = len(season["episodes"])
692
  season["episodes"] = [ep for ep in season["episodes"] if ep.get("episode_number") != episode_number]
693
  found = original_len > len(season["episodes"])
694
  break
 
695
  if not found:
696
  return False
 
697
  tv['updated_on'] = datetime.utcnow()
698
  result = await self.dbs[db_key]["tv"].replace_one({"tmdb_id": tmdb_id}, tv)
699
  return result.modified_count > 0
@@ -702,12 +767,31 @@ class Database:
702
  async def delete_tv_season(self, tmdb_id: int, db_index: int, season_number: int) -> bool:
703
  db_key = f"storage_{db_index}"
704
  tv = await self.dbs[db_key]["tv"].find_one({"tmdb_id": tmdb_id})
 
705
  if not tv or "seasons" not in tv:
706
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
  original_len = len(tv["seasons"])
708
  tv["seasons"] = [s for s in tv["seasons"] if s.get("season_number") != season_number]
 
709
  if len(tv["seasons"]) == original_len:
710
- return False
 
711
  tv['updated_on'] = datetime.utcnow()
712
  result = await self.dbs[db_key]["tv"].replace_one({"tmdb_id": tmdb_id}, tv)
713
  return result.modified_count > 0
@@ -716,23 +800,40 @@ class Database:
716
  async def delete_tv_quality(self, tmdb_id: int, db_index: int, season_number: int, episode_number: int, quality: str) -> bool:
717
  db_key = f"storage_{db_index}"
718
  tv = await self.dbs[db_key]["tv"].find_one({"tmdb_id": tmdb_id})
 
719
  if not tv or "seasons" not in tv:
720
  return False
 
721
  found = False
722
  for season in tv["seasons"]:
723
  if season.get("season_number") == season_number:
724
  for episode in season["episodes"]:
725
  if episode.get("episode_number") == episode_number and "telegram" in episode:
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  original_len = len(episode["telegram"])
727
  episode["telegram"] = [q for q in episode["telegram"] if q.get("quality") != quality]
728
  found = original_len > len(episode["telegram"])
729
  break
 
730
  if not found:
731
  return False
732
  tv['updated_on'] = datetime.utcnow()
733
  result = await self.dbs[db_key]["tv"].replace_one({"tmdb_id": tmdb_id}, tv)
734
  return result.modified_count > 0
735
 
 
736
  # Get per-DB statistics (movies, tv shows, used size, etc.)
737
  async def get_database_stats(self):
738
  stats = []
 
654
  # Delete a Movie or Tvshow completely
655
  async def delete_document(self, media_type: str, tmdb_id: int, db_index: int) -> bool:
656
  db_key = f"storage_{db_index}"
657
+
658
  if media_type == "Movie":
659
+ doc = await self.dbs[db_key]["movie"].find_one({"tmdb_id": tmdb_id})
660
+ if doc and "telegram" in doc:
661
+ for quality in doc["telegram"]:
662
+ try:
663
+ old_id = quality.get("id")
664
+ if old_id:
665
+ decoded_data = await decode_string(old_id)
666
+ chat_id = int(f"-100{decoded_data['chat_id']}")
667
+ msg_id = int(decoded_data['msg_id'])
668
+ create_task(delete_message(chat_id, msg_id))
669
+ except Exception as e:
670
+ LOGGER.error(f"Failed to queue file for deletion: {e}")
671
+
672
  result = await self.dbs[db_key]["movie"].delete_one({"tmdb_id": tmdb_id})
673
  else:
674
+ doc = await self.dbs[db_key]["tv"].find_one({"tmdb_id": tmdb_id})
675
+ if doc and "seasons" in doc:
676
+ for season in doc["seasons"]:
677
+ for episode in season.get("episodes", []):
678
+ for quality in episode.get("telegram", []):
679
+ try:
680
+ old_id = quality.get("id")
681
+ if old_id:
682
+ decoded_data = await decode_string(old_id)
683
+ chat_id = int(f"-100{decoded_data['chat_id']}")
684
+ msg_id = int(decoded_data['msg_id'])
685
+ create_task(delete_message(chat_id, msg_id))
686
+ except Exception as e:
687
+ LOGGER.error(f"Failed to queue file for deletion: {e}")
688
+
689
  result = await self.dbs[db_key]["tv"].delete_one({"tmdb_id": tmdb_id})
690
+
691
  if result.deleted_count > 0:
692
  LOGGER.info(f"{media_type} with tmdb_id {tmdb_id} deleted successfully.")
693
  return True
 
699
  async def delete_movie_quality(self, tmdb_id: int, db_index: int, quality: str) -> bool:
700
  db_key = f"storage_{db_index}"
701
  movie = await self.dbs[db_key]["movie"].find_one({"tmdb_id": tmdb_id})
702
+
703
  if not movie or "telegram" not in movie:
704
  return False
705
+
706
+ for q in movie["telegram"]:
707
+ if q.get("quality") == quality:
708
+ try:
709
+ old_id = q.get("id")
710
+ if old_id:
711
+ decoded_data = await decode_string(old_id)
712
+ chat_id = int(f"-100{decoded_data['chat_id']}")
713
+ msg_id = int(decoded_data['msg_id'])
714
+ create_task(delete_message(chat_id, msg_id))
715
+ except Exception as e:
716
+ LOGGER.error(f"Failed to queue file for deletion: {e}")
717
+ break
718
+
719
  original_len = len(movie["telegram"])
720
  movie["telegram"] = [q for q in movie["telegram"] if q.get("quality") != quality]
721
+
722
  if len(movie["telegram"]) == original_len:
723
+ return False
724
+
725
  movie['updated_on'] = datetime.utcnow()
726
  result = await self.dbs[db_key]["movie"].replace_one({"tmdb_id": tmdb_id}, movie)
727
  return result.modified_count > 0
 
730
  async def delete_tv_episode(self, tmdb_id: int, db_index: int, season_number: int, episode_number: int) -> bool:
731
  db_key = f"storage_{db_index}"
732
  tv = await self.dbs[db_key]["tv"].find_one({"tmdb_id": tmdb_id})
733
+
734
  if not tv or "seasons" not in tv:
735
  return False
736
+
737
  found = False
738
  for season in tv["seasons"]:
739
  if season.get("season_number") == season_number:
740
+ for ep in season["episodes"]:
741
+ if ep.get("episode_number") == episode_number:
742
+ for quality in ep.get("telegram", []):
743
+ try:
744
+ old_id = quality.get("id")
745
+ if old_id:
746
+ decoded_data = await decode_string(old_id)
747
+ chat_id = int(f"-100{decoded_data['chat_id']}")
748
+ msg_id = int(decoded_data['msg_id'])
749
+ create_task(delete_message(chat_id, msg_id))
750
+ except Exception as e:
751
+ LOGGER.error(f"Failed to queue file for deletion: {e}")
752
+ break
753
+
754
  original_len = len(season["episodes"])
755
  season["episodes"] = [ep for ep in season["episodes"] if ep.get("episode_number") != episode_number]
756
  found = original_len > len(season["episodes"])
757
  break
758
+
759
  if not found:
760
  return False
761
+
762
  tv['updated_on'] = datetime.utcnow()
763
  result = await self.dbs[db_key]["tv"].replace_one({"tmdb_id": tmdb_id}, tv)
764
  return result.modified_count > 0
 
767
  async def delete_tv_season(self, tmdb_id: int, db_index: int, season_number: int) -> bool:
768
  db_key = f"storage_{db_index}"
769
  tv = await self.dbs[db_key]["tv"].find_one({"tmdb_id": tmdb_id})
770
+
771
  if not tv or "seasons" not in tv:
772
  return False
773
+
774
+ for season in tv["seasons"]:
775
+ if season.get("season_number") == season_number:
776
+ for episode in season.get("episodes", []):
777
+ for quality in episode.get("telegram", []):
778
+ try:
779
+ old_id = quality.get("id")
780
+ if old_id:
781
+ decoded_data = await decode_string(old_id)
782
+ chat_id = int(f"-100{decoded_data['chat_id']}")
783
+ msg_id = int(decoded_data['msg_id'])
784
+ create_task(delete_message(chat_id, msg_id))
785
+ except Exception as e:
786
+ LOGGER.error(f"Failed to queue file for deletion: {e}")
787
+ break
788
+
789
  original_len = len(tv["seasons"])
790
  tv["seasons"] = [s for s in tv["seasons"] if s.get("season_number") != season_number]
791
+
792
  if len(tv["seasons"]) == original_len:
793
+ return False
794
+
795
  tv['updated_on'] = datetime.utcnow()
796
  result = await self.dbs[db_key]["tv"].replace_one({"tmdb_id": tmdb_id}, tv)
797
  return result.modified_count > 0
 
800
  async def delete_tv_quality(self, tmdb_id: int, db_index: int, season_number: int, episode_number: int, quality: str) -> bool:
801
  db_key = f"storage_{db_index}"
802
  tv = await self.dbs[db_key]["tv"].find_one({"tmdb_id": tmdb_id})
803
+
804
  if not tv or "seasons" not in tv:
805
  return False
806
+
807
  found = False
808
  for season in tv["seasons"]:
809
  if season.get("season_number") == season_number:
810
  for episode in season["episodes"]:
811
  if episode.get("episode_number") == episode_number and "telegram" in episode:
812
+ for q in episode["telegram"]:
813
+ if q.get("quality") == quality:
814
+ try:
815
+ old_id = q.get("id")
816
+ if old_id:
817
+ decoded_data = await decode_string(old_id)
818
+ chat_id = int(f"-100{decoded_data['chat_id']}")
819
+ msg_id = int(decoded_data['msg_id'])
820
+ create_task(delete_message(chat_id, msg_id))
821
+ except Exception as e:
822
+ LOGGER.error(f"Failed to queue file for deletion: {e}")
823
+ break
824
+
825
  original_len = len(episode["telegram"])
826
  episode["telegram"] = [q for q in episode["telegram"] if q.get("quality") != quality]
827
  found = original_len > len(episode["telegram"])
828
  break
829
+
830
  if not found:
831
  return False
832
  tv['updated_on'] = datetime.utcnow()
833
  result = await self.dbs[db_key]["tv"].replace_one({"tmdb_id": tmdb_id}, tv)
834
  return result.modified_count > 0
835
 
836
+
837
  # Get per-DB statistics (movies, tv shows, used size, etc.)
838
  async def get_database_stats(self):
839
  stats = []
Backend/helper/metadata.py CHANGED
@@ -158,7 +158,7 @@ async def fetch_tv_metadata(title, season, episode, encoded_string, year=None, q
158
  if use_tmdb and tv_details:
159
  return {
160
  "tmdb_id": tv_details.id,
161
- "imdb_id": "",
162
  "title": tv_details.name,
163
  "year": getattr(tv_details.first_air_date, "year", 0),
164
  "rate": getattr(tv_details, "vote_average", 0) or 0,
@@ -234,7 +234,7 @@ async def fetch_movie_metadata(title, encoded_string, year=None, quality=None, d
234
  if use_tmdb and movie_details:
235
  return {
236
  "tmdb_id": movie_details.id,
237
- "imdb_id": "",
238
  "title": movie_details.title,
239
  "year": getattr(movie_details.release_date, "year", 0),
240
  "rate": getattr(movie_details, "vote_average", 0) or 0,
 
158
  if use_tmdb and tv_details:
159
  return {
160
  "tmdb_id": tv_details.id,
161
+ "imdb_id": tv_details.external_ids.imdb_id,
162
  "title": tv_details.name,
163
  "year": getattr(tv_details.first_air_date, "year", 0),
164
  "rate": getattr(tv_details, "vote_average", 0) or 0,
 
234
  if use_tmdb and movie_details:
235
  return {
236
  "tmdb_id": movie_details.id,
237
+ "imdb_id": movie_details.external_ids.imdb_id,
238
  "title": movie_details.title,
239
  "year": getattr(movie_details.release_date, "year", 0),
240
  "rate": getattr(movie_details, "vote_average", 0) or 0,
Backend/pyrofork/plugins/start.py CHANGED
@@ -2,17 +2,11 @@ from pyrogram import filters, Client, enums
2
  from Backend.helper.custom_filter import CustomFilters
3
  from pyrogram.types import Message
4
  from Backend.config import Telegram
5
- from urllib.parse import urlparse
6
 
7
  @Client.on_message(filters.command('start') & filters.private & CustomFilters.owner, group=10)
8
  async def send_start_message(client: Client, message: Message):
9
  try:
10
- parsed_url = urlparse(Telegram.BASE_URL)
11
  base_url = Telegram.BASE_URL
12
-
13
- if parsed_url.hostname.replace('.', '').isdigit() and Telegram.PORT:
14
- base_url = f"{parsed_url.scheme}://{parsed_url.hostname}:{Telegram.PORT}"
15
-
16
  addon_url = f"{base_url}/stremio/manifest.json"
17
 
18
  await message.reply_text(
 
2
  from Backend.helper.custom_filter import CustomFilters
3
  from pyrogram.types import Message
4
  from Backend.config import Telegram
 
5
 
6
  @Client.on_message(filters.command('start') & filters.private & CustomFilters.owner, group=10)
7
  async def send_start_message(client: Client, message: Message):
8
  try:
 
9
  base_url = Telegram.BASE_URL
 
 
 
 
10
  addon_url = f"{base_url}/stremio/manifest.json"
11
 
12
  await message.reply_text(
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
  [project]
2
  name = "Telegram-Stremio"
3
- version = "1.2.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.3.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.2.0"
905
  source = { virtual = "." }
906
  dependencies = [
907
  { name = "aiofiles" },
 
901
 
902
  [[package]]
903
  name = "telegram-stremio"
904
+ version = "1.2.1"
905
  source = { virtual = "." }
906
  dependencies = [
907
  { name = "aiofiles" },