Skip to content

Commit fe217be

Browse files
committed
add ability to export multiple chats simultaneously
1 parent bd5f5b7 commit fe217be

File tree

9 files changed

+70
-56
lines changed

9 files changed

+70
-56
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.idea
22
__pycache__
3-
dist
3+
dist
4+
/telegram_export

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "t-export"
3-
version = "0.1.3b4"
3+
version = "0.1.3b5"
44
description = "Telegram chats export tool."
55
authors = ["RuslanUC <dev_ruslan_uc@protonmail.com>"]
66
readme = "README.md"

texport/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .export_config import ExportConfig
2+
from .progress_print import ProgressPrint
3+
from .media_downloader import MediaExporter
4+
from .messages_preloader import Preloader
5+
from .messages_saver import MessagesSaver
6+
from .exporter import Exporter

texport/export_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
@dataclass
2020
class ExportConfig:
21-
chat_id: Union[str, int] = "me"
21+
chat_ids: list[Union[str, int]] = field(default_factory=lambda: ["me"])
2222
output_dir: Path = Path("./telegram_export")
2323
export_photos: bool = True
2424
export_videos: bool = True

texport/exporter.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@
66
from pyrogram.types import Message as PyroMessage
77
from pyrogram.utils import zero_datetime
88

9-
from .export_config import ExportConfig
9+
from . import ExportConfig, MediaExporter, Preloader, MessagesSaver, ProgressPrint
1010
from .media import MEDIA_TYPES
11-
from .media_downloader import MediaExporter
12-
from .messages_preloader import Preloader
13-
from .messages_saver import MessagesSaver
14-
from .progress_print import ProgressPrint
1511

1612

1713
class Exporter:
@@ -35,50 +31,54 @@ async def _export_media(self, message: PyroMessage) -> None:
3531
return
3632

3733
if m.downloadable:
38-
self._media_downloader.add(media.file_id, f"{self._config.output_dir.absolute()}/{m.dir_name}/", message.id)
34+
chat_output_dir = (self._config.output_dir / f"{message.chat.id}").absolute()
3935

36+
self._media_downloader.add(media.file_id, f"{chat_output_dir}/{m.dir_name}/", message.id)
4037
if hasattr(media, "thumbs") and media.thumbs:
41-
self._media_downloader.add(media.thumbs[0].file_id, f"{self._config.output_dir.absolute()}/thumbs/",
42-
f"{message.id}_thumb")
38+
self._media_downloader.add(media.thumbs[0].file_id, f"{chat_output_dir}/thumbs/", f"{message.id}_thumb")
4339

4440
async def _write(self, wait_media: list[int]) -> None:
4541
self.progress.status = "Waiting for all media to be downloaded..."
4642
await self._media_downloader.wait(wait_media)
4743
self.progress.status = "Writing messages to file..."
4844
await self._saver.save()
4945

50-
async def _export(self, chat_id: Union[int, str]):
46+
async def _export(self):
5147
await self._media_downloader.run()
5248

5349
offset_date = zero_datetime() if self._config.to_date.date() >= date.today() else self._config.to_date
5450
loaded = 0
5551
medias = []
56-
self.progress.approx_messages_count = await self._client.get_chat_history_count(chat_id)
57-
messages_iter = Preloader(self._client, self.progress, self._export_media) \
52+
for chat_id in self._config.chat_ids:
53+
self.progress.approx_messages_count += await self._client.get_chat_history_count(chat_id)
54+
messages_iter = Preloader(self._client, self.progress, self._config.chat_ids, self._export_media) \
5855
if self._config.preload else self._client.get_chat_history
59-
async for message in messages_iter(chat_id, offset_date=offset_date):
60-
if message.date < self._config.from_date:
61-
break
6256

63-
loaded += 1
64-
with self.progress.update():
65-
self.progress.status = "Exporting messages..."
66-
self.progress.messages_exported = loaded
57+
for chat_id in self._config.chat_ids:
58+
async for message in messages_iter(chat_id, offset_date=offset_date):
59+
if message.date < self._config.from_date:
60+
break
6761

68-
if message.media:
69-
medias.append(message.id)
70-
medias.append(f"{message.id}_thumb")
71-
await self._export_media(message)
62+
loaded += 1
63+
with self.progress.update():
64+
self.progress.status = "Exporting messages..."
65+
self.progress.messages_exported = loaded
7266

73-
if not message.text and not message.caption and message.media not in MEDIA_TYPES:
74-
continue
67+
if message.media:
68+
medias.append(message.id)
69+
medias.append(f"{message.id}_thumb")
70+
await self._export_media(message)
7571

76-
self._messages.append(message)
77-
if len(self._messages) > 1000:
72+
if not message.text and not message.caption and message.media not in MEDIA_TYPES:
73+
continue
74+
75+
self._messages.append(message)
76+
if len(self._messages) > 1000:
77+
await self._write(medias)
78+
79+
if self._messages:
7880
await self._write(medias)
7981

80-
if self._messages:
81-
await self._write(medias)
8282
self._task = None
8383

8484
self.progress.status = "Stopping media downloader..."
@@ -88,7 +88,7 @@ async def _export(self, chat_id: Union[int, str]):
8888
async def export(self, block: bool=True) -> None:
8989
if self._task is not None:
9090
return
91-
coro = self._export(self._config.chat_id)
91+
coro = self._export()
9292
if block:
9393
await coro
9494
else:

texport/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async def _main(session_name: str, api_id: int, api_hash: str, config: ExportCon
3333
help="Telegram api hash. Saved in ~/.texport/config.json file.")
3434
@click.option("--session-name", "-s", type=click.STRING, default="main",
3535
help="Pyrogram session name or path to existing file. Saved in ~/.texport/<session_name>.session file.")
36-
@click.option("--chat-id", "-c", type=click.STRING, default="me",
36+
@click.option("--chat-id", "-c", type=click.STRING, default=["me"], multiple=True,
3737
help="Chat id or username or phone number. \"me\" or \"self\" to export saved messages.")
3838
@click.option("--output", "-o", type=click.STRING, default="./telegram_export",
3939
help="Output directory.")
@@ -55,7 +55,7 @@ async def _main(session_name: str, api_id: int, api_hash: str, config: ExportCon
5555
@click.option("--max-concurrent-downloads", "-d", type=click.INT, default=4,
5656
help="Number of concurrent media downloads.")
5757
def main(
58-
session_name: str, api_id: int, api_hash: str, chat_id: str, output: str, size_limit: int, from_date: str,
58+
session_name: str, api_id: int, api_hash: str, chat_id: list[str], output: str, size_limit: int, from_date: str,
5959
to_date: str, photos: bool, videos: bool, voice: bool, video_notes: bool, stickers: bool, gifs: bool,
6060
documents: bool, quiet: bool, no_preload: bool, max_concurrent_downloads: int,
6161
) -> None:
@@ -65,7 +65,7 @@ def main(
6565
makedirs(output, exist_ok=True)
6666

6767
config = ExportConfig(
68-
chat_id=chat_id,
68+
chat_ids=chat_id,
6969
output_dir=Path(output),
7070
size_limit=size_limit,
7171
from_date=datetime.strptime(from_date, "%d.%m.%Y"),

texport/media_downloader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
from asyncio import sleep, Task, Semaphore, create_task
33
from os.path import relpath
4+
from pathlib import Path
45
from typing import Union, Optional
56

67
from pyrogram import Client
@@ -42,8 +43,7 @@ async def _download(self, file_id: str, download_dir: str, out_id: Union[str, in
4243
self._downloading.pop(out_id, None)
4344
self.ids.discard(out_id)
4445

45-
path = relpath(path, self.config.output_dir.absolute())
46-
self.output[out_id] = path
46+
self.output[out_id] = relpath(path, Path(download_dir).parent.absolute())
4747

4848
def _status(self, status: str=None) -> None:
4949
with self.progress.update():

texport/messages_preloader.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
from asyncio import sleep, get_event_loop
22

33
from pyrogram import Client
4-
from pyrogram.types import Message as PyroMessage
4+
from pyrogram.types import Message
55

66
from .progress_print import ProgressPrint
77

88

99
class Preloader:
10-
def __init__(self, client: Client, progress: ProgressPrint, media_cb):
10+
def __init__(self, client: Client, progress: ProgressPrint, chat_ids: list, media_cb):
1111
self.client = client
1212
self.progress = progress
13-
self.finished = False
14-
self.messages: list[PyroMessage] = []
13+
self.finished = {chat_id: False for chat_id in chat_ids}
14+
self.messages: dict[..., list[Message]] = {chat_id: [] for chat_id in chat_ids}
1515
self.messages_loaded = 0
1616
self.media_cb = media_cb
17+
self._chat_ids = chat_ids
1718

1819
self._task = None
1920
self._pyro_args = ()
2021
self._pyro_kwargs = {}
22+
self._current_chat_id = None
2123

22-
def __call__(self, *pyrogram_args, **pyrogram_kwargs):
24+
def __call__(self, chat_id, *pyrogram_args, **pyrogram_kwargs):
25+
self._current_chat_id = chat_id
2326
self._pyro_args = pyrogram_args
2427
self._pyro_kwargs = pyrogram_kwargs
2528
return self
@@ -28,27 +31,28 @@ def __aiter__(self):
2831
return self
2932

3033
async def _preload(self) -> None:
31-
async for message in self.client.get_chat_history(*self._pyro_args, **self._pyro_kwargs):
32-
self.messages.append(message)
33-
self.messages_loaded += 1
34+
for chat_id in self._chat_ids:
35+
async for message in self.client.get_chat_history(chat_id, *self._pyro_args, **self._pyro_kwargs):
36+
self.messages[chat_id].append(message)
37+
self.messages_loaded += 1
3438

35-
if message.media and self.media_cb:
36-
await self.media_cb(message)
39+
if message.media and self.media_cb:
40+
await self.media_cb(message)
3741

38-
with self.progress.update():
39-
self.progress.status = "Preloading messages and media..."
40-
self.progress.messages_loaded = self.messages_loaded
42+
with self.progress.update():
43+
self.progress.status = "Preloading messages and media..."
44+
self.progress.messages_loaded = self.messages_loaded
4145

42-
self.finished = True
46+
self.finished[chat_id] = True
4347

44-
async def __anext__(self) -> PyroMessage:
48+
async def __anext__(self) -> Message:
4549
if self._task is None: self._task = get_event_loop().create_task(self._preload())
4650

47-
while not self.finished and not self.messages:
51+
while not self.finished[self._current_chat_id] and not self.messages[self._current_chat_id]:
4852
await sleep(.01)
4953

50-
if self.finished and not self.messages:
54+
if self.finished[self._current_chat_id] and not self.messages[self._current_chat_id]:
5155
raise StopAsyncIteration
5256

53-
return self.messages.pop(0)
57+
return self.messages[self._current_chat_id].pop(0)
5458

texport/messages_saver.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ def __init__(self, messages: list[PyroMessage], media: dict[Union[int, str], str
1919

2020
def _save(self) -> None:
2121
out_dir = self.config.output_dir
22-
if not exists(out_dir / "js") or exists(out_dir / "images") or exists(out_dir / "css"):
22+
if self.messages:
23+
out_dir = out_dir / str(self.messages[0].chat.id)
24+
25+
if not exists(out_dir / "js") or not exists(out_dir / "images") or not exists(out_dir / "css"):
2326
unpack_to(out_dir)
2427

2528
output = ""

0 commit comments

Comments
 (0)