File size: 8,609 Bytes
8964916
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# webserver.py (FULL, COMPLETE CODE for the main.py structure)

import math
import traceback
import os
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from pyrogram.file_id import FileId
from pyrogram import raw, Client
from pyrogram.session import Session, Auth

# Local imports from your project
from config import Config
from bot import multi_clients, work_loads, get_readable_file_size
from database import db

# FastAPI app instance, started by main.py
app = FastAPI()
templates = Jinja2Templates(directory="templates")

# A cache to store ByteStreamer instances to avoid re-creating them
class_cache = {}

@app.api_route("/", methods=["GET", "HEAD"])
async def root():
    """A simple health check route."""
    return {"status": "ok", "message": "Web server is healthy!"}

def mask_filename(name: str) -> str:
    """Obfuscates the filename to hide it in the URL/page."""
    if not name: return "Protected File"
    resolutions = ["216_p", "480p", "720p", "1080p", "2160p"]
    res_part = ""
    for res in resolutions:
        if res in name:
            res_part = f" {res}"
            name = name.replace(res, "")
            break
    base, ext = os.path.splitext(name)
    masked_base = ''.join(c if (i % 3 == 0 and c.isalnum()) else '*' for i, c in enumerate(base))
    return f"{masked_base}{res_part}{ext}"

class ByteStreamer:
    """Handles the low-level logic of fetching file parts from Telegram."""
    def __init__(self, client: Client):
        self.client = client

    @staticmethod
    async def get_location(file_id: FileId):
        return raw.types.InputDocumentFileLocation(
            id=file_id.media_id,
            access_hash=file_id.access_hash,
            file_reference=file_id.file_reference,
            thumb_size=file_id.thumbnail_size
        )

    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):
        client = self.client
        work_loads[index] += 1
        
        media_session = client.media_sessions.get(file_id.dc_id)
        if media_session is None:
            if file_id.dc_id != await client.storage.dc_id():
                auth_key = await Auth(client, file_id.dc_id, await client.storage.test_mode()).create()
                media_session = Session(client, file_id.dc_id, auth_key, await client.storage.test_mode(), is_media=True)
                await media_session.start()
                exported_auth = await client.invoke(raw.functions.auth.ExportAuthorization(dc_id=file_id.dc_id))
                await media_session.invoke(raw.functions.auth.ImportAuthorization(id=exported_auth.id, bytes=exported_auth.bytes))
            else:
                media_session = client.session
            client.media_sessions[file_id.dc_id] = media_session
        
        location = await self.get_location(file_id)
        current_part = 1
        try:
            while current_part <= part_count:
                r = await media_session.invoke(
                    raw.functions.upload.GetFile(location=location, offset=offset, limit=chunk_size),
                    retries=0
                )
                if isinstance(r, raw.types.upload.File):
                    chunk = r.bytes
                    if not chunk: break
                    
                    if part_count == 1: yield chunk[first_part_cut:last_part_cut]
                    elif current_part == 1: yield chunk[first_part_cut:]
                    elif current_part == part_count: yield chunk[:last_part_cut]
                    else: yield chunk
                    
                    current_part += 1
                    offset += chunk_size
                else:
                    break
        finally:
            work_loads[index] -= 1

@app.get("/show/{unique_id}", response_class=HTMLResponse)
async def show_file_page(request: Request, unique_id: str):
    """The route that displays the download page to the user."""
    try:
        storage_msg_id = await db.get_link(unique_id)
        if not storage_msg_id:
            raise HTTPException(status_code=404, detail="Link expired or invalid.")
        
        # Use the main bot (client 0) to get message details
        main_bot = multi_clients.get(0)
        if not main_bot:
            raise HTTPException(status_code=503, detail="Bot is not ready yet. Please try again in a moment.")
        
        file_msg = await main_bot.get_messages(Config.STORAGE_CHANNEL, storage_msg_id)
        media = file_msg.document or file_msg.video or file_msg.audio
        if not media:
            raise HTTPException(status_code=404, detail="File not found in the message.")
        
        original_file_name = media.file_name or "file"
        safe_file_name = "".join(c for c in original_file_name if c.isalnum() or c in (' ', '.', '_', '-')).rstrip()

        context = {
            "request": request,
            "file_name": mask_filename(original_file_name),
            "file_size": get_readable_file_size(media.file_size),
            "is_media": (media.mime_type or "").startswith(("video/", "audio/")),
            "direct_dl_link": f"{Config.BASE_URL}/dl/{storage_msg_id}/{safe_file_name}",
            "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",
            "vlc_player_link": f"vlc://{Config.BASE_URL}/dl/{storage_msg_id}/{safe_file_name}"
        }
        return templates.TemplateResponse("show.html", context)

    except HTTPException:
        raise
    except Exception as e:
        print(f"Error in /show route: {traceback.format_exc()}")
        raise HTTPException(status_code=500, detail="Internal server error.")

@app.get("/dl/{msg_id}/{file_name}")
async def stream_handler(request: Request, msg_id: int, file_name: str):
    """The route that handles the actual file streaming and download."""
    try:
        # Choose the client with the least workload
        index = min(work_loads, key=work_loads.get, default=0)
        client = multi_clients.get(index)
        if not client:
            raise HTTPException(status_code=503, detail="No available clients to handle the request.")
        
        tg_connect = class_cache.get(client)
        if not tg_connect:
            tg_connect = ByteStreamer(client)
            class_cache[client] = tg_connect
            
        message = await client.get_messages(Config.STORAGE_CHANNEL, msg_id)
        media = message.document or message.video or message.audio
        if not media or message.empty:
            raise FileNotFoundError

        file_id = FileId.decode(media.file_id)
        file_size = media.file_size
        
        range_header = request.headers.get("Range", 0)
        from_bytes, until_bytes = 0, file_size - 1
        if range_header:
            from_bytes_str, until_bytes_str = range_header.replace("bytes=", "").split("-")
            from_bytes = int(from_bytes_str)
            if until_bytes_str:
                until_bytes = int(until_bytes_str)
        
        if (until_bytes >= file_size) or (from_bytes < 0):
            raise HTTPException(status_code=416, detail="Requested range not satisfiable")
        
        req_length = until_bytes - from_bytes + 1
        chunk_size = 1024 * 1024  # 1 MB
        offset = (from_bytes // chunk_size) * chunk_size
        first_part_cut = from_bytes - offset
        last_part_cut = (until_bytes % chunk_size) + 1
        part_count = math.ceil(req_length / chunk_size)
        
        body = tg_connect.yield_file(file_id, index, offset, first_part_cut, last_part_cut, part_count, chunk_size)
        
        status_code = 206 if range_header else 200
        headers = {
            "Content-Type": media.mime_type or "application/octet-stream",
            "Accept-Ranges": "bytes",
            "Content-Disposition": f'inline; filename="{media.file_name}"',
            "Content-Length": str(req_length)
        }
        if range_header:
            headers["Content-Range"] = f"bytes {from_bytes}-{until_bytes}/{file_size}"
        
        return StreamingResponse(content=body, status_code=status_code, headers=headers)
        
    except FileNotFoundError:
        raise HTTPException(status_code=404, detail="File not found on Telegram.")
    except Exception as e:
        print(f"Error in /dl route: {traceback.format_exc()}")
        raise HTTPException(status_code=500, detail="Internal streaming error.")