Streamixph05 commited on
Commit
8964916
·
verified ·
1 Parent(s): 8c13919

Upload 7 files

Browse files
Files changed (7) hide show
  1. .gitignore +8 -0
  2. Dockerfile +6 -0
  3. app.py +372 -0
  4. config.py +34 -0
  5. database.py +42 -0
  6. requirements.txt +6 -0
  7. webserver.py +195 -0
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # .gitignore
2
+ .env
3
+ *.session
4
+ *.session-journal
5
+ __pycache__/
6
+ *.pyc
7
+ downloads/
8
+ sessions/ # <-- Add this line
Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ CMD ["python3", "app.py"]
app.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py (THE REAL, FINAL, CLEAN, EASY-TO-READ FULL CODE)
2
+
3
+ import os
4
+ import asyncio
5
+ import secrets
6
+ import traceback
7
+ import uvicorn
8
+ import re
9
+ import logging
10
+ from contextlib import asynccontextmanager
11
+
12
+ from pyrogram import Client, filters, enums
13
+ from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, ChatMemberUpdated
14
+ from pyrogram.errors import FloodWait, UserNotParticipant
15
+ from fastapi import FastAPI, Request, HTTPException
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.responses import JSONResponse, StreamingResponse
18
+ from pyrogram.file_id import FileId
19
+ from pyrogram import raw
20
+ from pyrogram.session import Session, Auth
21
+ from fastapi.responses import HTMLResponse
22
+ from fastapi.templating import Jinja2Templates
23
+ import math
24
+
25
+ # Project ki dusri files se important cheezein import karo
26
+ from config import Config
27
+ from database import db
28
+
29
+ # =====================================================================================
30
+ # --- SETUP: BOT, WEB SERVER, AUR LOGGING ---
31
+ # =====================================================================================
32
+
33
+ @asynccontextmanager
34
+ async def lifespan(app: FastAPI):
35
+ """
36
+ Yeh function bot ko web server ke saath start aur stop karta hai.
37
+ """
38
+ print("--- Lifespan: Server chalu ho raha hai... ---")
39
+
40
+ await db.connect()
41
+
42
+ try:
43
+ print("Starting main Pyrogram bot...")
44
+ await bot.start()
45
+
46
+ me = await bot.get_me()
47
+ Config.BOT_USERNAME = me.username
48
+ print(f"✅ Main Bot [@{Config.BOT_USERNAME}] safaltapoorvak start ho gaya.")
49
+
50
+ # --- MULTI-CLIENT STARTUP ---
51
+ multi_clients[0] = bot
52
+ work_loads[0] = 0
53
+ await initialize_clients()
54
+
55
+ print(f"Verifying storage channel ({Config.STORAGE_CHANNEL})...")
56
+ await bot.get_chat(Config.STORAGE_CHANNEL)
57
+ print("✅ Storage channel accessible hai.")
58
+
59
+ if Config.FORCE_SUB_CHANNEL:
60
+ try:
61
+ print(f"Verifying force sub channel ({Config.FORCE_SUB_CHANNEL})...")
62
+ await bot.get_chat(Config.FORCE_SUB_CHANNEL)
63
+ print("✅ Force Sub channel accessible hai.")
64
+ except Exception as e:
65
+ print(f"!!! WARNING: Bot, Force Sub channel mein admin nahi hai. Error: {e}")
66
+
67
+ try:
68
+ await cleanup_channel(bot)
69
+ except Exception as e:
70
+ print(f"Warning: Channel cleanup fail ho gaya. Error: {e}")
71
+
72
+ print("--- Lifespan: Startup safaltapoorvak poora hua. ---")
73
+
74
+ except Exception as e:
75
+ print(f"!!! FATAL ERROR: Bot startup ke dauraan error aa gaya: {traceback.format_exc()}")
76
+
77
+ yield
78
+
79
+ print("--- Lifespan: Server band ho raha hai... ---")
80
+ if bot.is_initialized:
81
+ await bot.stop()
82
+ print("--- Lifespan: Shutdown poora hua. ---")
83
+
84
+ app = FastAPI(lifespan=lifespan)
85
+ templates = Jinja2Templates(directory="templates")
86
+ app.add_middleware(
87
+ CORSMiddleware,
88
+ allow_origins=["*"],
89
+ allow_credentials=True,
90
+ allow_methods=["*"],
91
+ allow_headers=["*"],
92
+ )
93
+
94
+ # --- LOG FILTER: YEH SIRF /dl/ WALE LOGS KO CHUPAYEGA ---
95
+ class HideDLFilter(logging.Filter):
96
+ def filter(self, record: logging.LogRecord) -> bool:
97
+ # Agar log message mein "GET /dl/" hai, toh usse mat dikhao
98
+ return "GET /dl/" not in record.getMessage()
99
+
100
+ # Uvicorn ke 'access' logger par filter lagao
101
+ logging.getLogger("uvicorn.access").addFilter(HideDLFilter())
102
+ # --- FIX KHATAM ---
103
+
104
+ bot = Client("SimpleStreamBot", api_id=Config.API_ID, api_hash=Config.API_HASH, bot_token=Config.BOT_TOKEN, in_memory=True)
105
+ multi_clients = {}; work_loads = {}; class_cache = {}
106
+
107
+ # =====================================================================================
108
+ # --- MULTI-CLIENT LOGIC ---
109
+ # =====================================================================================
110
+
111
+ class TokenParser:
112
+ """ Environment variables se MULTI_TOKENs ko parse karta hai. """
113
+ @staticmethod
114
+ def parse_from_env():
115
+ return {
116
+ c + 1: t
117
+ for c, (_, t) in enumerate(
118
+ filter(lambda n: n[0].startswith("MULTI_TOKEN"), sorted(os.environ.items()))
119
+ )
120
+ }
121
+
122
+ async def start_client(client_id, bot_token):
123
+ """ Ek naye client bot ko start karta hai. """
124
+ try:
125
+ print(f"Attempting to start Client: {client_id}")
126
+ client = await Client(
127
+ name=str(client_id),
128
+ api_id=Config.API_ID,
129
+ api_hash=Config.API_HASH,
130
+ bot_token=bot_token,
131
+ no_updates=True,
132
+ in_memory=True
133
+ ).start()
134
+ work_loads[client_id] = 0
135
+ multi_clients[client_id] = client
136
+ print(f"✅ Client {client_id} started successfully.")
137
+ except Exception as e:
138
+ print(f"!!! CRITICAL ERROR: Failed to start Client {client_id} - Error: {e}")
139
+
140
+ async def initialize_clients():
141
+ """ Saare additional clients ko initialize karta hai. """
142
+ all_tokens = TokenParser.parse_from_env()
143
+ if not all_tokens:
144
+ print("No additional clients found. Using default bot only.")
145
+ return
146
+
147
+ print(f"Found {len(all_tokens)} extra clients. Starting them...")
148
+ tasks = [start_client(i, token) for i, token in all_tokens.items()]
149
+ await asyncio.gather(*tasks)
150
+
151
+ if len(multi_clients) > 1:
152
+ print(f"✅ Multi-Client Mode Enabled. Total Clients: {len(multi_clients)}")
153
+
154
+ # =====================================================================================
155
+ # --- HELPER FUNCTIONS ---
156
+ # =====================================================================================
157
+
158
+ def get_readable_file_size(size_in_bytes):
159
+ if not size_in_bytes:
160
+ return '0B'
161
+ power = 1024
162
+ n = 0
163
+ power_labels = {0: 'B', 1: 'KB', 2: 'MB', 3: 'GB'}
164
+ while size_in_bytes >= power and n < len(power_labels) - 1:
165
+ size_in_bytes /= power
166
+ n += 1
167
+ return f"{size_in_bytes:.2f} {power_labels[n]}"
168
+
169
+ def mask_filename(name: str):
170
+ if not name:
171
+ return "Protected File"
172
+ base, ext = os.path.splitext(name)
173
+ metadata_pattern = re.compile(
174
+ r'((19|20)\d{2}|4k|2160p|1080p|720p|480p|360p|HEVC|x265|BluRay|WEB-DL|HDRip)',
175
+ re.IGNORECASE
176
+ )
177
+ match = metadata_pattern.search(base)
178
+ if match:
179
+ title_part = base[:match.start()].strip(' .-_')
180
+ metadata_part = base[match.start():]
181
+ else:
182
+ title_part = base
183
+ metadata_part = ""
184
+ masked_title = ''.join(c if (i % 3 == 0 and c.isalnum()) else ('*' if c.isalnum() else c) for i, c in enumerate(title_part))
185
+ return f"{masked_title} {metadata_part}{ext}".strip()
186
+
187
+ # =====================================================================================
188
+ # --- PYROGRAM BOT HANDLERS ---
189
+ # =====================================================================================
190
+
191
+ @bot.on_message(filters.command("start") & filters.private)
192
+ async def start_command(client: Client, message: Message):
193
+ user_id = message.from_user.id
194
+ user_name = message.from_user.first_name
195
+
196
+ if len(message.command) > 1 and message.command[1].startswith("verify_"):
197
+ unique_id = message.command[1].split("_", 1)[1]
198
+
199
+ if Config.FORCE_SUB_CHANNEL:
200
+ try:
201
+ await client.get_chat_member(Config.FORCE_SUB_CHANNEL, user_id)
202
+ except UserNotParticipant:
203
+ channel_username = str(Config.FORCE_SUB_CHANNEL).replace('@', '')
204
+ channel_link = f"https://t.me/{channel_username}"
205
+ join_button = InlineKeyboardButton("📢 Join Channel", url=channel_link)
206
+ retry_button = InlineKeyboardButton("✅ Joined", url=f"https://t.me/{Config.BOT_USERNAME}?start={message.command[1]}")
207
+ keyboard = InlineKeyboardMarkup([[join_button], [retry_button]])
208
+ await message.reply_text(
209
+ "**You Must Join Our Channel To Get The Link!**\n\n"
210
+ "__Join Channel & Click '✅ Joined'.__",
211
+ reply_markup=keyboard, quote=True
212
+ )
213
+ return
214
+
215
+ final_link = f"{Config.BASE_URL}/show/{unique_id}"
216
+ reply_text = f"__✅ Verification Successful!\n\nCopy Link:__ `{final_link}`"
217
+ button = InlineKeyboardMarkup([[InlineKeyboardButton("Open Link", url=final_link)]])
218
+ await message.reply_text(reply_text, reply_markup=button, quote=True, disable_web_page_preview=True)
219
+
220
+ else:
221
+ reply_text = f"""
222
+ 👋 **Hello, {user_name}!**
223
+
224
+ __Welcome To Sharing Box Bot. I Can Help You Create Permanent, Shareable Links For Your Files.__
225
+
226
+ **How To Use Me:**
227
+
228
+ __Just Send Or Forward Any File To Me And I will instantly give you a special link that you can share with anyone!__
229
+ """
230
+ await message.reply_text(reply_text)
231
+
232
+ async def handle_file_upload(message: Message, user_id: int):
233
+ try:
234
+ sent_message = await message.copy(chat_id=Config.STORAGE_CHANNEL)
235
+ unique_id = secrets.token_urlsafe(8)
236
+ await db.save_link(unique_id, sent_message.id)
237
+
238
+ verify_link = f"https://t.me/{Config.BOT_USERNAME}?start=verify_{unique_id}"
239
+ button = InlineKeyboardMarkup([[InlineKeyboardButton("Get Link Now", url=verify_link)]])
240
+
241
+ await message.reply_text("__✅ File Uploaded!__", reply_markup=button, quote=True)
242
+ except Exception as e:
243
+ print(f"!!! ERROR: {traceback.format_exc()}"); await message.reply_text("Sorry, something went wrong.")
244
+
245
+ @bot.on_message(filters.private & (filters.document | filters.video | filters.audio))
246
+ async def file_handler(_, message: Message):
247
+ await handle_file_upload(message, message.from_user.id)
248
+
249
+ @bot.on_chat_member_updated(filters.chat(Config.STORAGE_CHANNEL))
250
+ async def simple_gatekeeper(c: Client, m_update: ChatMemberUpdated):
251
+ try:
252
+ if(m_update.new_chat_member and m_update.new_chat_member.status==enums.ChatMemberStatus.MEMBER):
253
+ u=m_update.new_chat_member.user
254
+ if u.id==Config.OWNER_ID or u.is_self: return
255
+ print(f"Gatekeeper: Kicking {u.id}"); await c.ban_chat_member(Config.STORAGE_CHANNEL,u.id); await c.unban_chat_member(Config.STORAGE_CHANNEL,u.id)
256
+ except Exception as e: print(f"Gatekeeper Error: {e}")
257
+
258
+ async def cleanup_channel(c: Client):
259
+ print("Gatekeeper: Running cleanup..."); allowed={Config.OWNER_ID,c.me.id}
260
+ try:
261
+ async for m in c.get_chat_members(Config.STORAGE_CHANNEL):
262
+ if m.user.id in allowed: continue
263
+ if m.status in [enums.ChatMemberStatus.ADMINISTRATOR,enums.ChatMemberStatus.OWNER]: continue
264
+ try: print(f"Cleanup: Kicking {m.user.id}"); await c.ban_chat_member(Config.STORAGE_CHANNEL,m.user.id); await asyncio.sleep(1)
265
+ except FloodWait as e: await asyncio.sleep(e.value)
266
+ except Exception as e: print(f"Cleanup Error: {e}")
267
+ except Exception as e: print(f"Cleanup Error: {e}")
268
+
269
+ # =====================================================================================
270
+ # --- FASTAPI WEB SERVER ---
271
+ # =====================================================================================
272
+
273
+ @app.get("/")
274
+ async def health_check():
275
+ """
276
+ This route provides a 200 OK response for uptime monitors.
277
+ """
278
+ return {"status": "ok", "message": "Server is healthy and running!"}
279
+
280
+ @app.get("/show/{unique_id}", response_class=HTMLResponse)
281
+ async def show_page(request: Request, unique_id: str):
282
+ return templates.TemplateResponse(
283
+ "show.html",
284
+ {"request": request}
285
+ )
286
+
287
+ @app.get("/api/file/{unique_id}", response_class=JSONResponse)
288
+ async def get_file_details_api(request: Request, unique_id: str):
289
+ message_id = await db.get_link(unique_id)
290
+ if not message_id:
291
+ raise HTTPException(status_code=404, detail="Link expired or invalid.")
292
+ main_bot = multi_clients.get(0)
293
+ if not main_bot:
294
+ raise HTTPException(status_code=503, detail="Bot is not ready.")
295
+ try:
296
+ message = await main_bot.get_messages(Config.STORAGE_CHANNEL, message_id)
297
+ except Exception:
298
+ raise HTTPException(status_code=404, detail="File not found on Telegram.")
299
+ media = message.document or message.video or message.audio
300
+ if not media:
301
+ raise HTTPException(status_code=404, detail="Media not found in the message.")
302
+ file_name = media.file_name or "file"
303
+ safe_file_name = "".join(c for c in file_name if c.isalnum() or c in (' ', '.', '_', '-')).rstrip()
304
+ mime_type = media.mime_type or "application/octet-stream"
305
+ response_data = {
306
+ "file_name": mask_filename(file_name),
307
+ "file_size": get_readable_file_size(media.file_size),
308
+ "is_media": mime_type.startswith(("video", "audio")),
309
+ "direct_dl_link": f"{Config.BASE_URL}/dl/{message_id}/{safe_file_name}",
310
+ "mx_player_link": f"intent:{Config.BASE_URL}/dl/{message_id}/{safe_file_name}#Intent;action=android.intent.action.VIEW;type={mime_type};end",
311
+ "vlc_player_link": f"intent:{Config.BASE_URL}/dl/{message_id}/{safe_file_name}#Intent;action=android.intent.action.VIEW;type={mime_type};package=org.videolan.vlc;end"
312
+ }
313
+ return response_data
314
+
315
+ class ByteStreamer:
316
+ def __init__(self,c:Client):self.client=c
317
+ @staticmethod
318
+ async def get_location(f:FileId): return raw.types.InputDocumentFileLocation(id=f.media_id,access_hash=f.access_hash,file_reference=f.file_reference,thumb_size=f.thumbnail_size)
319
+ async def yield_file(self,f:FileId,i:int,o:int,fc:int,lc:int,pc:int,cs:int):
320
+ c=self.client;work_loads[i]+=1;ms=c.media_sessions.get(f.dc_id)
321
+ if ms is None:
322
+ if f.dc_id!=await c.storage.dc_id():
323
+ ak=await Auth(c,f.dc_id,await c.storage.test_mode()).create();ms=Session(c,f.dc_id,ak,await c.storage.test_mode(),is_media=True);await ms.start();ea=await c.invoke(raw.functions.auth.ExportAuthorization(dc_id=f.dc_id));await ms.invoke(raw.functions.auth.ImportAuthorization(id=ea.id,bytes=ea.bytes))
324
+ else:ms=c.session
325
+ c.media_sessions[f.dc_id]=ms
326
+ loc=await self.get_location(f);cp=1
327
+ try:
328
+ while cp<=pc:
329
+ r=await ms.invoke(raw.functions.upload.GetFile(location=loc,offset=o,limit=cs),retries=0)
330
+ if isinstance(r,raw.types.upload.File):
331
+ chk=r.bytes
332
+ if not chk:break
333
+ if pc==1:yield chk[fc:lc]
334
+ elif cp==1:yield chk[fc:]
335
+ elif cp==pc:yield chk[:lc]
336
+ else:yield chk
337
+ cp+=1;o+=cs
338
+ else:break
339
+ finally:work_loads[i]-=1
340
+
341
+ @app.get("/dl/{mid}/{fname}")
342
+ async def stream_media(r:Request,mid:int,fname:str):
343
+ if not work_loads: raise HTTPException(503)
344
+ client_id = min(work_loads, key=work_loads.get)
345
+ c = multi_clients.get(client_id)
346
+ if not c: raise HTTPException(503)
347
+
348
+ tc=class_cache.get(c) or ByteStreamer(c);class_cache[c]=tc
349
+ try:
350
+ msg=await c.get_messages(Config.STORAGE_CHANNEL,mid);m=msg.document or msg.video or msg.audio
351
+ if not m or msg.empty:raise FileNotFoundError
352
+ fid=FileId.decode(m.file_id);fsize=m.file_size;rh=r.headers.get("Range","");fb,ub=0,fsize-1
353
+ if rh:
354
+ rps=rh.replace("bytes=","").split("-");fb=int(rps[0])
355
+ if len(rps)>1 and rps[1]:ub=int(rps[1])
356
+ if(ub>=fsize)or(fb<0):raise HTTPException(416)
357
+ rl=ub-fb+1;cs=1024*1024;off=(fb//cs)*cs;fc=fb-off;lc=(ub%cs)+1;pc=math.ceil(rl/cs)
358
+ body=tc.yield_file(fid,client_id,off,fc,lc,pc,cs);sc=206 if rh else 200
359
+ hdrs={"Content-Type":m.mime_type or "application/octet-stream","Accept-Ranges":"bytes","Content-Disposition":f'inline; filename="{m.file_name}"',"Content-Length":str(rl)}
360
+ if rh:hdrs["Content-Range"]=f"bytes {fb}-{ub}/{fsize}"
361
+ return StreamingResponse(body,status_code=sc,headers=hdrs)
362
+ except FileNotFoundError:raise HTTPException(404)
363
+ except Exception:print(traceback.format_exc());raise HTTPException(500)
364
+
365
+ # =====================================================================================
366
+ # --- MAIN EXECUTION BLOCK ---
367
+ # =====================================================================================
368
+
369
+ if __name__ == "__main__":
370
+ port = int(os.environ.get("PORT", 8000))
371
+ # Log level ko "info" rakho taaki hamara filter kaam kar sake
372
+ uvicorn.run("app:app", host="0.0.0.0", port=port, log_level="info")
config.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py (UPDATED)
2
+
3
+ import os
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv(".env")
7
+
8
+ class Config:
9
+ API_ID = int(os.environ.get("API_ID", 0))
10
+ API_HASH = os.environ.get("API_HASH", "")
11
+ BOT_TOKEN = os.environ.get("BOT_TOKEN", "")
12
+ OWNER_ID = int(os.environ.get("OWNER_ID", 0))
13
+
14
+ _storage_channel_str = os.environ.get("STORAGE_CHANNEL")
15
+ if _storage_channel_str:
16
+ try: STORAGE_CHANNEL = int(_storage_channel_str)
17
+ except ValueError: STORAGE_CHANNEL = _storage_channel_str
18
+ else: STORAGE_CHANNEL = 0
19
+
20
+ BASE_URL = os.environ.get("BASE_URL", "").rstrip('/')
21
+ DATABASE_URL = os.environ.get("DATABASE_URL", "")
22
+ REDIRECT_BLOGGER_URL = os.environ.get("REDIRECT_BLOGGER_URL", "")
23
+ BLOGGER_PAGE_URL = os.environ.get("BLOGGER_PAGE_URL", "")
24
+
25
+ # --- YAHAN BADLAV KIYA GAYA HAI ---
26
+ # Force Subscribe ke liye channel ID/username
27
+ _fsub_channel_str = os.environ.get("FORCE_SUB_CHANNEL")
28
+ if _fsub_channel_str:
29
+ try: FORCE_SUB_CHANNEL = int(_fsub_channel_str)
30
+ except ValueError: FORCE_SUB_CHANNEL = _fsub_channel_str
31
+ else: FORCE_SUB_CHANNEL = 0
32
+
33
+ # Yeh bot ka username store karega (code isse automatic set karega)
34
+ BOT_USERNAME = ""
database.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # database.py (UPDATED VERSION)
2
+
3
+ import motor.motor_asyncio
4
+ from config import Config
5
+
6
+ class Database:
7
+ def __init__(self):
8
+ self._client = None
9
+ self.db = None
10
+ self.collection = None
11
+ if not Config.DATABASE_URL:
12
+ print("WARNING: DATABASE_URL not set. Links will not be permanent.")
13
+
14
+ async def connect(self):
15
+ """Database se connection banata hai."""
16
+ if Config.DATABASE_URL:
17
+ print("Connecting to the database...")
18
+ self._client = motor.motor_asyncio.AsyncIOMotorClient(Config.DATABASE_URL)
19
+ self.db = self._client["StreamLinksDB"]
20
+ self.collection = self.db["links"]
21
+ print("✅ Database connection established.")
22
+ else:
23
+ self.db = None
24
+ self.collection = None
25
+
26
+ async def disconnect(self):
27
+ """Database connection ko band karta hai."""
28
+ if self._client:
29
+ self._client.close()
30
+ print("Database connection closed.")
31
+
32
+ async def save_link(self, unique_id, message_id):
33
+ if self.collection is not None:
34
+ await self.collection.insert_one({'_id': unique_id, 'message_id': message_id})
35
+
36
+ async def get_link(self, unique_id):
37
+ if self.collection is not None:
38
+ doc = await self.collection.find_one({'_id': unique_id})
39
+ return doc.get('message_id') if doc else None
40
+ return None
41
+
42
+ db = Database()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi[standard]
2
+ pyrogram
3
+ tgcrypto
4
+ python-dotenv
5
+ python-magic
6
+ motor
webserver.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # webserver.py (FULL, COMPLETE CODE for the main.py structure)
2
+
3
+ import math
4
+ import traceback
5
+ import os
6
+ from fastapi import FastAPI, Request, HTTPException
7
+ from fastapi.responses import HTMLResponse, StreamingResponse
8
+ from fastapi.templating import Jinja2Templates
9
+ from pyrogram.file_id import FileId
10
+ from pyrogram import raw, Client
11
+ from pyrogram.session import Session, Auth
12
+
13
+ # Local imports from your project
14
+ from config import Config
15
+ from bot import multi_clients, work_loads, get_readable_file_size
16
+ from database import db
17
+
18
+ # FastAPI app instance, started by main.py
19
+ app = FastAPI()
20
+ templates = Jinja2Templates(directory="templates")
21
+
22
+ # A cache to store ByteStreamer instances to avoid re-creating them
23
+ class_cache = {}
24
+
25
+ @app.api_route("/", methods=["GET", "HEAD"])
26
+ async def root():
27
+ """A simple health check route."""
28
+ return {"status": "ok", "message": "Web server is healthy!"}
29
+
30
+ def mask_filename(name: str) -> str:
31
+ """Obfuscates the filename to hide it in the URL/page."""
32
+ if not name: return "Protected File"
33
+ resolutions = ["216_p", "480p", "720p", "1080p", "2160p"]
34
+ res_part = ""
35
+ for res in resolutions:
36
+ if res in name:
37
+ res_part = f" {res}"
38
+ name = name.replace(res, "")
39
+ break
40
+ base, ext = os.path.splitext(name)
41
+ masked_base = ''.join(c if (i % 3 == 0 and c.isalnum()) else '*' for i, c in enumerate(base))
42
+ return f"{masked_base}{res_part}{ext}"
43
+
44
+ class ByteStreamer:
45
+ """Handles the low-level logic of fetching file parts from Telegram."""
46
+ def __init__(self, client: Client):
47
+ self.client = client
48
+
49
+ @staticmethod
50
+ async def get_location(file_id: FileId):
51
+ return raw.types.InputDocumentFileLocation(
52
+ id=file_id.media_id,
53
+ access_hash=file_id.access_hash,
54
+ file_reference=file_id.file_reference,
55
+ thumb_size=file_id.thumbnail_size
56
+ )
57
+
58
+ async def yield_file(self, file_id: FileId, index: int, offset: int, first_part_cut: int, last_part_cut: int, part_count: int, chunk_size: int):
59
+ client = self.client
60
+ work_loads[index] += 1
61
+
62
+ media_session = client.media_sessions.get(file_id.dc_id)
63
+ if media_session is None:
64
+ if file_id.dc_id != await client.storage.dc_id():
65
+ auth_key = await Auth(client, file_id.dc_id, await client.storage.test_mode()).create()
66
+ media_session = Session(client, file_id.dc_id, auth_key, await client.storage.test_mode(), is_media=True)
67
+ await media_session.start()
68
+ exported_auth = await client.invoke(raw.functions.auth.ExportAuthorization(dc_id=file_id.dc_id))
69
+ await media_session.invoke(raw.functions.auth.ImportAuthorization(id=exported_auth.id, bytes=exported_auth.bytes))
70
+ else:
71
+ media_session = client.session
72
+ client.media_sessions[file_id.dc_id] = media_session
73
+
74
+ location = await self.get_location(file_id)
75
+ current_part = 1
76
+ try:
77
+ while current_part <= part_count:
78
+ r = await media_session.invoke(
79
+ raw.functions.upload.GetFile(location=location, offset=offset, limit=chunk_size),
80
+ retries=0
81
+ )
82
+ if isinstance(r, raw.types.upload.File):
83
+ chunk = r.bytes
84
+ if not chunk: break
85
+
86
+ if part_count == 1: yield chunk[first_part_cut:last_part_cut]
87
+ elif current_part == 1: yield chunk[first_part_cut:]
88
+ elif current_part == part_count: yield chunk[:last_part_cut]
89
+ else: yield chunk
90
+
91
+ current_part += 1
92
+ offset += chunk_size
93
+ else:
94
+ break
95
+ finally:
96
+ work_loads[index] -= 1
97
+
98
+ @app.get("/show/{unique_id}", response_class=HTMLResponse)
99
+ async def show_file_page(request: Request, unique_id: str):
100
+ """The route that displays the download page to the user."""
101
+ try:
102
+ storage_msg_id = await db.get_link(unique_id)
103
+ if not storage_msg_id:
104
+ raise HTTPException(status_code=404, detail="Link expired or invalid.")
105
+
106
+ # Use the main bot (client 0) to get message details
107
+ main_bot = multi_clients.get(0)
108
+ if not main_bot:
109
+ raise HTTPException(status_code=503, detail="Bot is not ready yet. Please try again in a moment.")
110
+
111
+ file_msg = await main_bot.get_messages(Config.STORAGE_CHANNEL, storage_msg_id)
112
+ media = file_msg.document or file_msg.video or file_msg.audio
113
+ if not media:
114
+ raise HTTPException(status_code=404, detail="File not found in the message.")
115
+
116
+ original_file_name = media.file_name or "file"
117
+ safe_file_name = "".join(c for c in original_file_name if c.isalnum() or c in (' ', '.', '_', '-')).rstrip()
118
+
119
+ context = {
120
+ "request": request,
121
+ "file_name": mask_filename(original_file_name),
122
+ "file_size": get_readable_file_size(media.file_size),
123
+ "is_media": (media.mime_type or "").startswith(("video/", "audio/")),
124
+ "direct_dl_link": f"{Config.BASE_URL}/dl/{storage_msg_id}/{safe_file_name}",
125
+ "mx_player_link": f"intent:{Config.BASE_URL}/dl/{storage_msg_id}/{safe_file_name}#Intent;action=android.intent.action.VIEW;type={media.mime_type};end",
126
+ "vlc_player_link": f"vlc://{Config.BASE_URL}/dl/{storage_msg_id}/{safe_file_name}"
127
+ }
128
+ return templates.TemplateResponse("show.html", context)
129
+
130
+ except HTTPException:
131
+ raise
132
+ except Exception as e:
133
+ print(f"Error in /show route: {traceback.format_exc()}")
134
+ raise HTTPException(status_code=500, detail="Internal server error.")
135
+
136
+ @app.get("/dl/{msg_id}/{file_name}")
137
+ async def stream_handler(request: Request, msg_id: int, file_name: str):
138
+ """The route that handles the actual file streaming and download."""
139
+ try:
140
+ # Choose the client with the least workload
141
+ index = min(work_loads, key=work_loads.get, default=0)
142
+ client = multi_clients.get(index)
143
+ if not client:
144
+ raise HTTPException(status_code=503, detail="No available clients to handle the request.")
145
+
146
+ tg_connect = class_cache.get(client)
147
+ if not tg_connect:
148
+ tg_connect = ByteStreamer(client)
149
+ class_cache[client] = tg_connect
150
+
151
+ message = await client.get_messages(Config.STORAGE_CHANNEL, msg_id)
152
+ media = message.document or message.video or message.audio
153
+ if not media or message.empty:
154
+ raise FileNotFoundError
155
+
156
+ file_id = FileId.decode(media.file_id)
157
+ file_size = media.file_size
158
+
159
+ range_header = request.headers.get("Range", 0)
160
+ from_bytes, until_bytes = 0, file_size - 1
161
+ if range_header:
162
+ from_bytes_str, until_bytes_str = range_header.replace("bytes=", "").split("-")
163
+ from_bytes = int(from_bytes_str)
164
+ if until_bytes_str:
165
+ until_bytes = int(until_bytes_str)
166
+
167
+ if (until_bytes >= file_size) or (from_bytes < 0):
168
+ raise HTTPException(status_code=416, detail="Requested range not satisfiable")
169
+
170
+ req_length = until_bytes - from_bytes + 1
171
+ chunk_size = 1024 * 1024 # 1 MB
172
+ offset = (from_bytes // chunk_size) * chunk_size
173
+ first_part_cut = from_bytes - offset
174
+ last_part_cut = (until_bytes % chunk_size) + 1
175
+ part_count = math.ceil(req_length / chunk_size)
176
+
177
+ body = tg_connect.yield_file(file_id, index, offset, first_part_cut, last_part_cut, part_count, chunk_size)
178
+
179
+ status_code = 206 if range_header else 200
180
+ headers = {
181
+ "Content-Type": media.mime_type or "application/octet-stream",
182
+ "Accept-Ranges": "bytes",
183
+ "Content-Disposition": f'inline; filename="{media.file_name}"',
184
+ "Content-Length": str(req_length)
185
+ }
186
+ if range_header:
187
+ headers["Content-Range"] = f"bytes {from_bytes}-{until_bytes}/{file_size}"
188
+
189
+ return StreamingResponse(content=body, status_code=status_code, headers=headers)
190
+
191
+ except FileNotFoundError:
192
+ raise HTTPException(status_code=404, detail="File not found on Telegram.")
193
+ except Exception as e:
194
+ print(f"Error in /dl route: {traceback.format_exc()}")
195
+ raise HTTPException(status_code=500, detail="Internal streaming error.")