initial commit
This commit is contained in:
commit
3b1178e7d7
|
@ -0,0 +1,4 @@
|
||||||
|
*.session
|
||||||
|
.env
|
||||||
|
__pycache__/
|
||||||
|
venv/
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .main import main
|
||||||
|
|
||||||
|
main()
|
|
@ -0,0 +1,33 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
port = int(os.environ.get("PORT", "8080"))
|
||||||
|
except ValueError:
|
||||||
|
port = -1
|
||||||
|
if not 1 <= port <= 65535:
|
||||||
|
print("Please make sure the PORT environment variable is an integer between 1 and 65535")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
api_id = int(os.environ["API_ID"])
|
||||||
|
api_hash = os.environ["API_HASH"]
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
print("Please set the API_ID and API_HASH environment variables correctly")
|
||||||
|
print("You can get your own API keys at https://my.telegram.org/apps")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
chat_id = int(os.environ["CHAT_ID"])
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
print("Please set the CHAT_ID environment variable correctly")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
session_string = os.environ["SESSION_STRING"]
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
print("Please set the SESSION_STRING environment variable correctly")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
host = os.environ.get("HOST", "0.0.0.0")
|
|
@ -0,0 +1,42 @@
|
||||||
|
import asyncio
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import aiohttp_jinja2
|
||||||
|
import jinja2
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from .telegram import Client
|
||||||
|
from .routes import setup_routes
|
||||||
|
from .views import Views
|
||||||
|
from .config import host, port, session_string, api_id, api_hash
|
||||||
|
|
||||||
|
TEMPLATES_ROOT = pathlib.Path(__file__).parent / 'templates'
|
||||||
|
client = Client(session_string, api_id, api_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_jinja(app):
|
||||||
|
loader = jinja2.FileSystemLoader(str(TEMPLATES_ROOT))
|
||||||
|
aiohttp_jinja2.setup(app, loader=loader)
|
||||||
|
|
||||||
|
|
||||||
|
async def start():
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
|
||||||
|
async def stop(app):
|
||||||
|
await client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
async def init():
|
||||||
|
server = web.Application()
|
||||||
|
await start()
|
||||||
|
setup_routes(server, Views(client))
|
||||||
|
setup_jinja(server)
|
||||||
|
server.on_cleanup.append(stop)
|
||||||
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
app = loop.run_until_complete(init())
|
||||||
|
web.run_app(app, host=host, port=port)
|
|
@ -0,0 +1,15 @@
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
|
||||||
|
def setup_routes(app, handler):
|
||||||
|
h = handler
|
||||||
|
app.add_routes(
|
||||||
|
[
|
||||||
|
web.get('/', h.index, name='index'),
|
||||||
|
web.get(r"/{id:\d+}/view", h.info, name='info'),
|
||||||
|
web.get(r"/{id:\d+}/download", h.download_get),
|
||||||
|
web.head(r"/{id:\d+}/download", h.download_head),
|
||||||
|
web.get(r"/{id:\d+}/thumbnail", h.thumbnail_get),
|
||||||
|
web.head(r"/{id:\d+}/thumbnail", h.thumbnail_head),
|
||||||
|
]
|
||||||
|
)
|
|
@ -0,0 +1,37 @@
|
||||||
|
import math
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from telethon import TelegramClient
|
||||||
|
from telethon.sessions import StringSession
|
||||||
|
|
||||||
|
class Client(TelegramClient):
|
||||||
|
|
||||||
|
def __init__(self, session_string, *args, **kwargs):
|
||||||
|
super().__init__(StringSession(session_string), *args, **kwargs)
|
||||||
|
self.log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def download(self, file, file_size, offset, limit):
|
||||||
|
part_size = 1024 * 1024
|
||||||
|
first_part_cut = offset % part_size
|
||||||
|
first_part = math.floor(offset / part_size)
|
||||||
|
last_part_cut = part_size - (limit % part_size)
|
||||||
|
last_part = math.ceil(limit / part_size)
|
||||||
|
part_count = math.ceil(file_size / part_size)
|
||||||
|
part = first_part
|
||||||
|
try:
|
||||||
|
async for chunk in self.iter_download(file, offset=first_part * part_size, file_size=file_size, limit=part_size):
|
||||||
|
if part == first_part:
|
||||||
|
yield chunk[first_part_cut:]
|
||||||
|
elif part == last_part:
|
||||||
|
yield chunk[:last_part_cut]
|
||||||
|
else:
|
||||||
|
yield chunk
|
||||||
|
self.log.debug(f"Part {part}/{last_part} (total {part_count}) downloaded")
|
||||||
|
part += 1
|
||||||
|
self.log.debug("download finished")
|
||||||
|
except (GeneratorExit, StopAsyncIteration, asyncio.CancelledError):
|
||||||
|
self.log.debug("download interrupted")
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
self.log.debug("download errored", exc_info=True)
|
|
@ -0,0 +1,8 @@
|
||||||
|
<footer class="text-center text-black w-full my-4 py-4 bg-gray-300 xl:text-xl">
|
||||||
|
<a href="https://github.com/odysseusmax" target="_blank"> @odysseusmax </a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
||||||
|
<script src="https://cdn.fluidplayer.com/v3/current/fluidplayer.min.js"></script>
|
||||||
|
|
||||||
|
<title>
|
||||||
|
{{title}}
|
||||||
|
</title>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="m-auto w-full xl:max-w-screen-xl">
|
||||||
|
|
||||||
|
<header class="bg-red-600 text-white w-full mb-2 p-4">
|
||||||
|
<h1 class="w-full md:w-1/4 text-left text-xl lg:text-2xl xl:text-3xl"> Telegram Index </h1>
|
||||||
|
</header>
|
|
@ -0,0 +1,46 @@
|
||||||
|
{% include 'header.html' %}
|
||||||
|
<form class="p-4" action="/">
|
||||||
|
<input class="border rounded p-2 border-indigo-500 placeholder-indigo-500 text-indigo-500" type="text" name="search" value="{% if search %}{{search}}{% endif %}" placeholder="search...">
|
||||||
|
<button class="border rounded p-2 border-indigo-500 bg-indigo-500 text-white hover:text-indigo-500 hover:bg-white" type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mx-auto my-2 w-full overflow-x-auto px-4 text-xs">
|
||||||
|
|
||||||
|
<table class="m-auto rounded text-left shadow-md w-full">
|
||||||
|
<tr class="border-t border-b bg-gray-200 border-gray-300 my-2 p-4">
|
||||||
|
<th class="my-2 p-2">FileId</th>
|
||||||
|
<th class="my-2 p-2">Media</th>
|
||||||
|
<th class="my-2 p-2">Type</th>
|
||||||
|
<th class="my-2 p-2">Description</th>
|
||||||
|
<th class="my-2 p-2">Date</th>
|
||||||
|
<th class="my-2 p-2">Size</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% for item in item_list %}
|
||||||
|
<tr class="border-t border-b border-gray-300 my-2 p-4 rounded hover:bg-blue-100">
|
||||||
|
<th class="my-2 p-2">{{item.file_id}}</th>
|
||||||
|
<th class="my-2 p-2">{{item.media}}</th>
|
||||||
|
<th class="my-2 p-2">{{item.mime_type}}</th>
|
||||||
|
<th class="my-2 p-2 rounded hover:text-blue-600"><a href="/{{item.file_id}}/view">{{item.insight}}</a></th>
|
||||||
|
<th class="my-2 p-2">{{item.date}}</th>
|
||||||
|
<th class="my-2 p-2">{{item.size}}</th>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mx-auto my-2 text-center flex text-white content-center justify-center">
|
||||||
|
{% if prev_page %}
|
||||||
|
<a class="mx-2 p-2 border rounded bg-green-500 hover:border-green-500 hover:text-green-500 hover:bg-white" href="{{prev_page.url}}">Page {{prev_page.no}}</a>
|
||||||
|
{% endif %}
|
||||||
|
<p class="mx-2 p-2 border rounded border-green-500 text-green-500 hover:border-green-500 hover:text-green-500 hover:bg-white">Page {{cur_page}}</p>
|
||||||
|
{% if next_page %}
|
||||||
|
<a class="mx-2 p-2 border rounded bg-green-500 hover:border-green-500 hover:text-green-500 hover:bg-white" href="{{next_page.url}}">Page {{next_page.no}}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include 'footer.html' %}
|
|
@ -0,0 +1,91 @@
|
||||||
|
{% include 'header.html' %}
|
||||||
|
|
||||||
|
<div class="p-4 space-y-8">
|
||||||
|
{% if found %}
|
||||||
|
{% if media %}
|
||||||
|
<h1 class="text-blue-500 text-center text-lg md:text-xl lg:text-2xl xl:text-3xl w-full break-all">
|
||||||
|
Download {{name}}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="mx-auto w-full md:w-3/4 lg:w-2/5 p-2">
|
||||||
|
{% if media.image %}
|
||||||
|
<img class="mx-auto w-2/3 md:w-3/5 lg:w-2/5 rounded" src="/{{id}}/thumbnail" alt="{{name}}">
|
||||||
|
{% elif media.video %}
|
||||||
|
<video id="my-video-player" class="mx-auto rounded" controls poster="/{{id}}/thumbnail">
|
||||||
|
<source src="/{{id}}/download" type="{{ media.type }}" />
|
||||||
|
</video>
|
||||||
|
{% elif media.audio %}
|
||||||
|
<audio class="mx-auto" controls autoplay muted src="/{{id}}/download" type="{{ media.type }}"></audio>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if caption %}
|
||||||
|
|
||||||
|
<div class="flex justify-center mt-1 text-gray-300 font-mono text-xs lg:text-sm xl:text-lg break-all">
|
||||||
|
<p class="text-left rounded p-4 bg-gray-900 w-auto inline-block shadow-lg"> {{ caption|safe }} </p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center text-white text-sm md:text-base xl:text-2xl">
|
||||||
|
|
||||||
|
<a class="rounded p-3 bg-indigo-500 hover:bg-indigo-700 shadow-lg" href="/{{id}}/download">Download Now! ({{ size }})</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="mx-auto flex justify-center w-full md:w-3/4 mt-1 text-gray-300 font-mono text-xs lg:text-sm xl:text-lg break-all">
|
||||||
|
<p class="text-left rounded p-4 bg-gray-900 w-auto inline-block shadow-lg"> {{ text|safe }} </p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<h1 class="text-blue-500 text-center text-2xl md:text-3xl lg:text-4xl xl:text-5xl">Ooops...</h1>
|
||||||
|
|
||||||
|
<p class="text-center text-sm md:text-base lg:text-xl xl:text-2xl font-semibold">
|
||||||
|
{{ reason }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var myFP = fluidPlayer(
|
||||||
|
'my-video-player',{
|
||||||
|
"layoutControls": {
|
||||||
|
"controlBar": {
|
||||||
|
"autoHideTimeout": 3,
|
||||||
|
"animated": true,
|
||||||
|
"autoHide": true
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"imageUrl": null,
|
||||||
|
"position": "top left",
|
||||||
|
"clickUrl": null,
|
||||||
|
"opacity": 1
|
||||||
|
},
|
||||||
|
"htmlOnPauseBlock": {
|
||||||
|
"html": null,
|
||||||
|
"height": null,
|
||||||
|
"width": null
|
||||||
|
},
|
||||||
|
"autoPlay": false,
|
||||||
|
"mute": true,
|
||||||
|
"allowTheatre": true,
|
||||||
|
"playPauseAnimation": true,
|
||||||
|
"playbackRateEnabled": true,
|
||||||
|
"allowDownload": false,
|
||||||
|
"playButtonShowing": true,
|
||||||
|
"fillToContainer": true,
|
||||||
|
"posterImage": ""
|
||||||
|
},
|
||||||
|
"vastOptions": {
|
||||||
|
"adList": [],
|
||||||
|
"adCTAText": false,
|
||||||
|
"adCTATextPosition": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% include 'footer.html' %}
|
|
@ -0,0 +1,14 @@
|
||||||
|
def get_file_name(message):
|
||||||
|
if message.file.name:
|
||||||
|
return message.file.name.replace('\n', ' ')
|
||||||
|
ext = message.file.ext or ""
|
||||||
|
return f"{message.date.strftime('%Y-%m-%d_%H:%M:%S')}{ext}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_human_size(num):
|
||||||
|
base = 1024.0
|
||||||
|
sufix_list = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB', 'YiB']
|
||||||
|
for unit in sufix_list:
|
||||||
|
if abs(num) < base:
|
||||||
|
return f"{round(num, 2)} {unit}"
|
||||||
|
num /= base
|
|
@ -0,0 +1,216 @@
|
||||||
|
from aiohttp import web
|
||||||
|
import aiohttp_jinja2
|
||||||
|
from jinja2 import Markup
|
||||||
|
from telethon.tl import types
|
||||||
|
from telethon.tl.custom import Message
|
||||||
|
|
||||||
|
from .util import get_file_name, get_human_size
|
||||||
|
from .config import chat_id
|
||||||
|
|
||||||
|
|
||||||
|
class Views:
|
||||||
|
|
||||||
|
def __init__(self, client):
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
|
||||||
|
@aiohttp_jinja2.template('index.html')
|
||||||
|
async def index(self, req):
|
||||||
|
try:
|
||||||
|
offset_val = int(req.query.get('page', '1'))
|
||||||
|
except:
|
||||||
|
offset_val = 1
|
||||||
|
try:
|
||||||
|
search_query = req.query.get('search', '')
|
||||||
|
except:
|
||||||
|
search_query = ''
|
||||||
|
offset_val = 0 if offset_val <=1 else offset_val-1
|
||||||
|
try:
|
||||||
|
if search_query:
|
||||||
|
messages = (await self.client.get_messages(chat_id, search=search_query, limit=20, add_offset=20*offset_val)) or []
|
||||||
|
else:
|
||||||
|
messages = (await self.client.get_messages(chat_id, limit=20, add_offset=20*offset_val)) or []
|
||||||
|
except:
|
||||||
|
messages = []
|
||||||
|
results = []
|
||||||
|
for m in messages:
|
||||||
|
if m.file and not isinstance(m.media, types.MessageMediaWebPage):
|
||||||
|
entry = dict(
|
||||||
|
file_id=m.id,
|
||||||
|
media=True,
|
||||||
|
mime_type=m.file.mime_type,
|
||||||
|
insight = get_file_name(m)[:55],
|
||||||
|
date = m.date.isoformat(),
|
||||||
|
size=get_human_size(m.file.size)
|
||||||
|
)
|
||||||
|
elif m.message:
|
||||||
|
entry = dict(
|
||||||
|
file_id=m.id,
|
||||||
|
media=False,
|
||||||
|
mime_type='text/plain',
|
||||||
|
insight = m.raw_text[:55],
|
||||||
|
date = m.date.isoformat(),
|
||||||
|
size=get_human_size(len(m.raw_text))
|
||||||
|
)
|
||||||
|
results.append(entry)
|
||||||
|
prev_page = False
|
||||||
|
next_page = False
|
||||||
|
if offset_val:
|
||||||
|
query = {'page':offset_val}
|
||||||
|
if search_query:
|
||||||
|
query.update({'search':search_query})
|
||||||
|
prev_page = {
|
||||||
|
'url': req.rel_url.with_query(query),
|
||||||
|
'no': offset_val
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(messages)==20:
|
||||||
|
query = {'page':offset_val+2}
|
||||||
|
if search_query:
|
||||||
|
query.update({'search':search_query})
|
||||||
|
next_page = {
|
||||||
|
'url': req.rel_url.with_query(query),
|
||||||
|
'no': offset_val+2
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'item_list':results,
|
||||||
|
'prev_page': prev_page,
|
||||||
|
'cur_page' : offset_val+1,
|
||||||
|
'next_page': next_page,
|
||||||
|
'search': search_query,
|
||||||
|
'title': "Telegram Index"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@aiohttp_jinja2.template('info.html')
|
||||||
|
async def info(self, req):
|
||||||
|
file_id = int(req.match_info["id"])
|
||||||
|
message = await self.client.get_messages(entity=chat_id, ids=file_id)
|
||||||
|
if not message or not isinstance(message, Message):
|
||||||
|
print(type(message))
|
||||||
|
return {
|
||||||
|
'found':False,
|
||||||
|
'reason' : "Entry you are looking for cannot be retrived!",
|
||||||
|
'title': "Telegram Index"
|
||||||
|
}
|
||||||
|
if message.file and not isinstance(message.media, types.MessageMediaWebPage):
|
||||||
|
file_name = get_file_name(message)
|
||||||
|
file_size = get_human_size(message.file.size)
|
||||||
|
media = {
|
||||||
|
'type':message.file.mime_type
|
||||||
|
}
|
||||||
|
if 'video/' in message.file.mime_type:
|
||||||
|
media.update({
|
||||||
|
'video' : True
|
||||||
|
})
|
||||||
|
elif 'audio/' in message.file.mime_type:
|
||||||
|
media['audio'] = True
|
||||||
|
elif 'image/' in message.file.mime_type:
|
||||||
|
media['image'] = True
|
||||||
|
|
||||||
|
if message.text:
|
||||||
|
caption = Markup.escape(message.raw_text).__str__().replace('\n', '<br>')
|
||||||
|
|
||||||
|
else:
|
||||||
|
caption = False
|
||||||
|
return {
|
||||||
|
'found': True,
|
||||||
|
'name': file_name,
|
||||||
|
'id': file_id,
|
||||||
|
'size': file_size,
|
||||||
|
'media': media,
|
||||||
|
'caption': caption,
|
||||||
|
'title': f"Download | {file_name} | {file_size}"
|
||||||
|
}
|
||||||
|
elif message.message:
|
||||||
|
text = Markup.escape(message.raw_text).__str__().replace('\n', '<br>')
|
||||||
|
return {
|
||||||
|
'found': True,
|
||||||
|
'media': False,
|
||||||
|
'text': text,
|
||||||
|
'title': "Telegram Index"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'found':False,
|
||||||
|
'reason' : "Some kind of entry that I cannot display",
|
||||||
|
'title': "Telegram Index"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def download_get(self, req):
|
||||||
|
return await self.handle_request(req)
|
||||||
|
|
||||||
|
|
||||||
|
async def download_head(self, req):
|
||||||
|
return await self.handle_request(req, head=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def thumbnail_get(self, req):
|
||||||
|
return await self.handle_request(req, thumb=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def thumbnail_head(self, req):
|
||||||
|
return await self.handle_request(req, head=True, thumb=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_request(self, req, head=False, thumb=False):
|
||||||
|
file_id = int(req.match_info["id"])
|
||||||
|
|
||||||
|
message = await self.client.get_messages(entity=chat_id, ids=file_id)
|
||||||
|
if not message or not message.file:
|
||||||
|
return web.Response(status=410, text="410: Gone. Access to the target resource is no longer available!")
|
||||||
|
|
||||||
|
if thumb and message.document:
|
||||||
|
thumbnail = message.document.thumbs
|
||||||
|
if not thumbnail:
|
||||||
|
return web.Response(status=404, text="404: Not Found")
|
||||||
|
thumbnail = thumbnail[-1]
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
size = thumbnail.size
|
||||||
|
file_name = f"{file_id}_thumbnail.jpg"
|
||||||
|
media = types.InputDocumentFileLocation(
|
||||||
|
id=message.document.id,
|
||||||
|
access_hash=message.document.access_hash,
|
||||||
|
file_reference=message.document.file_reference,
|
||||||
|
thumb_size=thumbnail.type
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
media = message.media
|
||||||
|
size = message.file.size
|
||||||
|
file_name = get_file_name(message)
|
||||||
|
mime_type = message.file.mime_type
|
||||||
|
|
||||||
|
try:
|
||||||
|
offset = req.http_range.start or 0
|
||||||
|
limit = req.http_range.stop or size
|
||||||
|
if (limit > size) or (offset < 0) or (limit < offset):
|
||||||
|
raise ValueError("range not in acceptable format")
|
||||||
|
except ValueError:
|
||||||
|
return web.Response(
|
||||||
|
status=416,
|
||||||
|
text="416: Range Not Satisfiable",
|
||||||
|
headers = {
|
||||||
|
"Content-Range": f"bytes */{size}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not head:
|
||||||
|
body = self.client.download(media, size, offset, limit)
|
||||||
|
else:
|
||||||
|
body = None
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": mime_type,
|
||||||
|
"Content-Range": f"bytes {offset}-{limit}/{size}",
|
||||||
|
"Content-Length": str(limit - offset),
|
||||||
|
"Accept-Ranges": "bytes",
|
||||||
|
"Content-Disposition": f'attachment; filename="{file_name}"'
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.Response(
|
||||||
|
status=206 if offset else 200,
|
||||||
|
body=body,
|
||||||
|
headers=headers
|
||||||
|
)
|
|
@ -0,0 +1,4 @@
|
||||||
|
aiohttp
|
||||||
|
aiohttp-jinja2
|
||||||
|
telethon
|
||||||
|
cryptg
|
|
@ -0,0 +1 @@
|
||||||
|
python-3.8.5
|
Loading…
Reference in New Issue