lint: fix bare except and unused variables

This commit is contained in:
2026-06-19 11:52:30 +02:00
parent df644219de
commit ac5beb831b
15 changed files with 1359 additions and 728 deletions
+24
View File
@@ -0,0 +1,24 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yml,yaml}]
indent_size = 2
[*.{json,jsonc}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.py]
indent_size = 4
[{Makefile,makefile}]
indent_style = tab
+10
View File
@@ -0,0 +1,10 @@
ignored:
- DL3008
- DL3042
- DL3018
- DL3059
trustedRegistries:
- docker.io
- ghcr.io
- quay.io
- gcr.forust.xyz
+8
View File
@@ -0,0 +1,8 @@
{
"default": true,
"MD013": false,
"MD024": false,
"MD033": false,
"MD041": false,
"MD046": false
}
+8
View File
@@ -0,0 +1,8 @@
bracketSameLine: true
htmlWhitespaceSensitivity: css
printWidth: 120
tabWidth: 2
singleQuote: true
trailingComma: all
proseWrap: preserve
endOfLine: lf
+34
View File
@@ -0,0 +1,34 @@
{
"[yaml]": {
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "redhat.vscode-yaml"
},
"[python]": {
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[dockerfile]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "exiasr.hadolint"
},
"[markdown]": {
"editor.wordWrap": "wordWrapColumn",
"editor.wordWrapColumn": 120
},
"yaml.schemas": {
"kubernetes": ["**/k8s/*.yaml", "**/k8s/*.yml"]
},
"yaml.format.enable": true,
"yaml.validate": true,
"yaml.completion": true,
"prettier.bracketSameLine": true,
"prettier.htmlWhitespaceSensitivity": "css",
"prettier.printWidth": 120,
"editor.formatOnSave": true,
"files.autoSave": "onFocusChange",
"editor.wordWrapColumn": 120,
"editor.wordWrap": "wordWrapColumn"
}
+17
View File
@@ -0,0 +1,17 @@
extends: default
rules:
comments:
min-spaces-from-content: 1
comments-indentation: false
document-start: disable
line-length: disable
braces:
min-spaces-inside: 0
max-spaces-inside: 1
brackets:
min-spaces-inside: 0
max-spaces-inside: 1
indentation:
spaces: 2
indent-sequences: consistent
+39
View File
@@ -0,0 +1,39 @@
{
"tab_size": 2,
"soft_wrap": "prefer_line",
"preferred_line_length": 120,
"format_on_save": "on",
"languages": {
"YAML": {
"tab_size": 2,
"hard_tabs": false,
"format_on_save": "on",
"formatter": {
"language_server": { "name": "yaml-language-server" }
}
},
"Python": {
"tab_size": 4,
"format_on_save": "on",
"formatter": {
"language_server": { "name": "ruff" }
}
}
},
"lsp": {
"yaml-language-server": {
"settings": {
"yaml": {
"schemas": {
"kubernetes": ["**/k8s/*.yaml", "**/k8s/*.yml"]
},
"validate": true,
"completion": true,
"format": {
"enable": true
}
}
}
}
}
}
+6 -2
View File
@@ -57,7 +57,9 @@ class StateStore:
self.path.parent.mkdir(parents=True, exist_ok=True) self.path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = self.path.with_suffix(self.path.suffix + ".tmp") tmp_path = self.path.with_suffix(self.path.suffix + ".tmp")
with tmp_path.open("w", encoding="utf-8") as handle: with tmp_path.open("w", encoding="utf-8") as handle:
json.dump(self._merge_defaults(state), handle, ensure_ascii=False, indent=2) json.dump(
self._merge_defaults(state), handle, ensure_ascii=False, indent=2
)
handle.write("\n") handle.write("\n")
tmp_path.replace(self.path) tmp_path.replace(self.path)
self._cache = None self._cache = None
@@ -71,7 +73,9 @@ class StateStore:
def continuous_config(self) -> Dict[str, Any]: def continuous_config(self) -> Dict[str, Any]:
state = self.load() state = self.load()
return dict(state.get("continuous_scraping") or DEFAULT_STATE["continuous_scraping"]) return dict(
state.get("continuous_scraping") or DEFAULT_STATE["continuous_scraping"]
)
def save_continuous_config(self, config: Dict[str, Any]) -> Dict[str, Any]: def save_continuous_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
def mutate(state: Dict[str, Any]) -> None: def mutate(state: Dict[str, Any]) -> None:
+6
View File
@@ -10,6 +10,12 @@ services:
tty: true tty: true
ports: ports:
- "7887:8080" - "7887:8080"
healthcheck:
test: ["CMD", "python", "-c", "import http.client,json;c=http.client.HTTPConnection('localhost',8080);c.request('GET','/health');r=c.getresponse();exit(0)if json.loads(r.read()).get('ok')else exit(1)"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./session:/app/session - ./session:/app/session
+12
View File
@@ -38,6 +38,18 @@ spec:
stdin: true stdin: true
ports: ports:
- containerPort: 8080 - containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts: volumeMounts:
- name: telegram-scraper-data - name: telegram-scraper-data
mountPath: /app/data mountPath: /app/data
+2 -1
View File
@@ -41,7 +41,8 @@ class ScraperJobService:
) )
elif job_type == "scrape_selected": elif job_type == "scrape_selected":
await self._scrape_channels( await self._scrape_channels(
scraper, [str(channel_id) for channel_id in payload.get("channels", [])] scraper,
[str(channel_id) for channel_id in payload.get("channels", [])],
) )
elif job_type == "export_all": elif job_type == "export_all":
await scraper.export_data() await scraper.export_data()
+5 -1
View File
@@ -60,7 +60,11 @@ def main() -> int:
status, body = fetch(endpoint) status, body = fetch(endpoint)
if status != 200: if status != 200:
raise RuntimeError(f"{endpoint} returned HTTP {status}") raise RuntimeError(f"{endpoint} returned HTTP {status}")
if endpoint.endswith(".json") or endpoint.startswith("/api") or endpoint.startswith("/health"): if (
endpoint.endswith(".json")
or endpoint.startswith("/api")
or endpoint.startswith("/health")
):
json.loads(body.decode("utf-8")) json.loads(body.decode("utf-8"))
print(f"ok {endpoint}") print(f"ok {endpoint}")
return 0 return 0
+357 -181
View File
@@ -5,18 +5,28 @@ import csv
import asyncio import asyncio
import time import time
import sys import sys
import uuid
import warnings import warnings
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
from pathlib import Path from pathlib import Path
from io import StringIO from io import StringIO
from telethon import TelegramClient from telethon import TelegramClient
from telethon.tl.types import MessageMediaPhoto, MessageMediaDocument, MessageMediaWebPage, User, PeerChannel, Channel, Chat from telethon.tl.types import (
MessageMediaPhoto,
MessageMediaDocument,
MessageMediaWebPage,
User,
PeerChannel,
Channel,
Chat,
)
from telethon.errors import FloodWaitError, SessionPasswordNeededError from telethon.errors import FloodWaitError, SessionPasswordNeededError
import qrcode import qrcode
warnings.filterwarnings("ignore", message="Using async sessions support is an experimental feature") warnings.filterwarnings(
"ignore", message="Using async sessions support is an experimental feature"
)
def display_ascii_art(): def display_ascii_art():
WHITE = "\033[97m" WHITE = "\033[97m"
@@ -31,6 +41,7 @@ ___________________ _________
""" """
print(WHITE + art + RESET) print(WHITE + art + RESET)
@dataclass @dataclass
class MessageData: class MessageData:
message_id: int message_id: int
@@ -48,9 +59,10 @@ class MessageData:
forwards: Optional[int] forwards: Optional[int]
reactions: Optional[str] reactions: Optional[str]
class OptimizedTelegramScraper: class OptimizedTelegramScraper:
def __init__(self): def __init__(self):
self.STATE_FILE = 'state.json' self.STATE_FILE = "state.json"
self.state = self.load_state() self.state = self.load_state()
self.client = None self.client = None
self.continuous_scraping_active = False self.continuous_scraping_active = False
@@ -62,21 +74,21 @@ class OptimizedTelegramScraper:
def load_state(self) -> Dict[str, Any]: def load_state(self) -> Dict[str, Any]:
if os.path.exists(self.STATE_FILE): if os.path.exists(self.STATE_FILE):
try: try:
with open(self.STATE_FILE, 'r') as f: with open(self.STATE_FILE, "r") as f:
return json.load(f) return json.load(f)
except: except Exception:
pass pass
return { return {
'api_id': None, "api_id": None,
'api_hash': None, "api_hash": None,
'channels': {}, "channels": {},
'channel_names': {}, "channel_names": {},
'scrape_media': True, "scrape_media": True,
} }
def save_state(self): def save_state(self):
try: try:
with open(self.STATE_FILE, 'w') as f: with open(self.STATE_FILE, "w") as f:
json.dump(self.state, f, indent=2) json.dump(self.state, f, indent=2)
except Exception as e: except Exception as e:
print(f"Failed to save state: {e}") print(f"Failed to save state: {e}")
@@ -86,17 +98,19 @@ class OptimizedTelegramScraper:
channel_dir = Path(channel) channel_dir = Path(channel)
channel_dir.mkdir(exist_ok=True) channel_dir.mkdir(exist_ok=True)
db_file = channel_dir / f'{channel}.db' db_file = channel_dir / f"{channel}.db"
conn = sqlite3.connect(str(db_file), check_same_thread=False) conn = sqlite3.connect(str(db_file), check_same_thread=False)
conn.execute('''CREATE TABLE IF NOT EXISTS messages conn.execute("""CREATE TABLE IF NOT EXISTS messages
(id INTEGER PRIMARY KEY, message_id INTEGER UNIQUE, date TEXT, (id INTEGER PRIMARY KEY, message_id INTEGER UNIQUE, date TEXT,
sender_id INTEGER, first_name TEXT, last_name TEXT, username TEXT, sender_id INTEGER, first_name TEXT, last_name TEXT, username TEXT,
message TEXT, media_type TEXT, media_path TEXT, reply_to INTEGER, message TEXT, media_type TEXT, media_path TEXT, reply_to INTEGER,
post_author TEXT, views INTEGER, forwards INTEGER, reactions TEXT)''') post_author TEXT, views INTEGER, forwards INTEGER, reactions TEXT)""")
conn.execute('CREATE INDEX IF NOT EXISTS idx_message_id ON messages(message_id)') conn.execute(
conn.execute('CREATE INDEX IF NOT EXISTS idx_date ON messages(date)') "CREATE INDEX IF NOT EXISTS idx_message_id ON messages(message_id)"
conn.execute('PRAGMA journal_mode=WAL') )
conn.execute('PRAGMA synchronous=NORMAL') conn.execute("CREATE INDEX IF NOT EXISTS idx_date ON messages(date)")
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
conn.commit() conn.commit()
self.migrate_database(conn) self.migrate_database(conn)
@@ -111,19 +125,19 @@ class OptimizedTelegramScraper:
columns = {row[1] for row in cursor.fetchall()} columns = {row[1] for row in cursor.fetchall()}
migrations = [] migrations = []
if 'post_author' not in columns: if "post_author" not in columns:
migrations.append('ALTER TABLE messages ADD COLUMN post_author TEXT') migrations.append("ALTER TABLE messages ADD COLUMN post_author TEXT")
if 'views' not in columns: if "views" not in columns:
migrations.append('ALTER TABLE messages ADD COLUMN views INTEGER') migrations.append("ALTER TABLE messages ADD COLUMN views INTEGER")
if 'forwards' not in columns: if "forwards" not in columns:
migrations.append('ALTER TABLE messages ADD COLUMN forwards INTEGER') migrations.append("ALTER TABLE messages ADD COLUMN forwards INTEGER")
if 'reactions' not in columns: if "reactions" not in columns:
migrations.append('ALTER TABLE messages ADD COLUMN reactions TEXT') migrations.append("ALTER TABLE messages ADD COLUMN reactions TEXT")
for migration in migrations: for migration in migrations:
try: try:
conn.execute(migration) conn.execute(migration)
except: except Exception:
pass pass
if migrations: if migrations:
@@ -139,20 +153,38 @@ class OptimizedTelegramScraper:
return return
conn = self.get_db_connection(channel) conn = self.get_db_connection(channel)
data = [(msg.message_id, msg.date, msg.sender_id, msg.first_name, data = [
msg.last_name, msg.username, msg.message, msg.media_type, (
msg.media_path, msg.reply_to, msg.post_author, msg.views, msg.message_id,
msg.forwards, msg.reactions) for msg in messages] msg.date,
msg.sender_id,
msg.first_name,
msg.last_name,
msg.username,
msg.message,
msg.media_type,
msg.media_path,
msg.reply_to,
msg.post_author,
msg.views,
msg.forwards,
msg.reactions,
)
for msg in messages
]
conn.executemany('''INSERT OR IGNORE INTO messages conn.executemany(
"""INSERT OR IGNORE INTO messages
(message_id, date, sender_id, first_name, last_name, username, (message_id, date, sender_id, first_name, last_name, username,
message, media_type, media_path, reply_to, post_author, views, message, media_type, media_path, reply_to, post_author, views,
forwards, reactions) forwards, reactions)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
data,
)
conn.commit() conn.commit()
async def download_media(self, channel: str, message) -> Optional[str]: async def download_media(self, channel: str, message) -> Optional[str]:
if not message.media or not self.state['scrape_media']: if not message.media or not self.state["scrape_media"]:
return None return None
if isinstance(message.media, MessageMediaWebPage): if isinstance(message.media, MessageMediaWebPage):
@@ -160,15 +192,15 @@ class OptimizedTelegramScraper:
try: try:
channel_dir = Path(channel) channel_dir = Path(channel)
media_folder = channel_dir / 'media' media_folder = channel_dir / "media"
media_folder.mkdir(exist_ok=True) media_folder.mkdir(exist_ok=True)
if isinstance(message.media, MessageMediaPhoto): if isinstance(message.media, MessageMediaPhoto):
original_name = getattr(message.file, 'name', None) or "photo.jpg" original_name = getattr(message.file, "name", None) or "photo.jpg"
ext = "jpg" ext = "jpg"
elif isinstance(message.media, MessageMediaDocument): elif isinstance(message.media, MessageMediaDocument):
ext = getattr(message.file, 'ext', 'bin') if message.file else 'bin' ext = getattr(message.file, "ext", "bin") if message.file else "bin"
original_name = getattr(message.file, 'name', None) or f"document.{ext}" original_name = getattr(message.file, "name", None) or f"document.{ext}"
else: else:
return None return None
@@ -205,14 +237,20 @@ class OptimizedTelegramScraper:
async def update_media_path(self, channel: str, message_id: int, media_path: str): async def update_media_path(self, channel: str, message_id: int, media_path: str):
conn = self.get_db_connection(channel) conn = self.get_db_connection(channel)
conn.execute('UPDATE messages SET media_path = ? WHERE message_id = ?', conn.execute(
(media_path, message_id)) "UPDATE messages SET media_path = ? WHERE message_id = ?",
(media_path, message_id),
)
conn.commit() conn.commit()
async def scrape_channel(self, channel: str, offset_id: int): async def scrape_channel(self, channel: str, offset_id: int):
try: try:
entity = await self.client.get_entity(PeerChannel(int(channel)) if channel.startswith('-') else channel) entity = await self.client.get_entity(
result = await self.client.get_messages(entity, offset_id=offset_id, reverse=True, limit=0) PeerChannel(int(channel)) if channel.startswith("-") else channel
)
result = await self.client.get_messages(
entity, offset_id=offset_id, reverse=True, limit=0
)
total_messages = result.total total_messages = result.total
if total_messages == 0: if total_messages == 0:
@@ -227,7 +265,9 @@ class OptimizedTelegramScraper:
last_message_id = offset_id last_message_id = offset_id
semaphore = asyncio.Semaphore(self.max_concurrent_downloads) semaphore = asyncio.Semaphore(self.max_concurrent_downloads)
async for message in self.client.iter_messages(entity, offset_id=offset_id, reverse=True): async for message in self.client.iter_messages(
entity, offset_id=offset_id, reverse=True
):
try: try:
sender = await message.get_sender() sender = await message.get_sender()
@@ -235,33 +275,45 @@ class OptimizedTelegramScraper:
if message.reactions and message.reactions.results: if message.reactions and message.reactions.results:
reactions_parts = [] reactions_parts = []
for reaction in message.reactions.results: for reaction in message.reactions.results:
emoji = getattr(reaction.reaction, 'emoticon', '') emoji = getattr(reaction.reaction, "emoticon", "")
count = reaction.count count = reaction.count
if emoji: if emoji:
reactions_parts.append(f"{emoji} {count}") reactions_parts.append(f"{emoji} {count}")
if reactions_parts: if reactions_parts:
reactions_str = ' '.join(reactions_parts) reactions_str = " ".join(reactions_parts)
msg_data = MessageData( msg_data = MessageData(
message_id=message.id, message_id=message.id,
date=message.date.strftime('%Y-%m-%d %H:%M:%S'), date=message.date.strftime("%Y-%m-%d %H:%M:%S"),
sender_id=message.sender_id, sender_id=message.sender_id,
first_name=getattr(sender, 'first_name', None) if isinstance(sender, User) else None, first_name=getattr(sender, "first_name", None)
last_name=getattr(sender, 'last_name', None) if isinstance(sender, User) else None, if isinstance(sender, User)
username=getattr(sender, 'username', None) if isinstance(sender, User) else None, else None,
message=message.message or '', last_name=getattr(sender, "last_name", None)
media_type=message.media.__class__.__name__ if message.media else None, if isinstance(sender, User)
else None,
username=getattr(sender, "username", None)
if isinstance(sender, User)
else None,
message=message.message or "",
media_type=message.media.__class__.__name__
if message.media
else None,
media_path=None, media_path=None,
reply_to=message.reply_to_msg_id if message.reply_to else None, reply_to=message.reply_to_msg_id if message.reply_to else None,
post_author=message.post_author, post_author=message.post_author,
views=message.views, views=message.views,
forwards=message.forwards, forwards=message.forwards,
reactions=reactions_str reactions=reactions_str,
) )
message_batch.append(msg_data) message_batch.append(msg_data)
if self.state['scrape_media'] and message.media and not isinstance(message.media, MessageMediaWebPage): if (
self.state["scrape_media"]
and message.media
and not isinstance(message.media, MessageMediaWebPage)
):
media_tasks.append(message) media_tasks.append(message)
last_message_id = message.id last_message_id = message.id
@@ -272,15 +324,19 @@ class OptimizedTelegramScraper:
message_batch.clear() message_batch.clear()
if processed_messages % self.state_save_interval == 0: if processed_messages % self.state_save_interval == 0:
self.state['channels'][channel] = last_message_id self.state["channels"][channel] = last_message_id
self.save_state() self.save_state()
progress = (processed_messages / total_messages) * 100 progress = (processed_messages / total_messages) * 100
bar_length = 30 bar_length = 30
filled_length = int(bar_length * processed_messages // total_messages) filled_length = int(
bar = '' * filled_length + '' * (bar_length - filled_length) bar_length * processed_messages // total_messages
)
bar = "" * filled_length + "" * (bar_length - filled_length)
sys.stdout.write(f"\r📄 Messages: [{bar}] {progress:.1f}% ({processed_messages}/{total_messages})") sys.stdout.write(
f"\r📄 Messages: [{bar}] {progress:.1f}% ({processed_messages}/{total_messages})"
)
sys.stdout.flush() sys.stdout.flush()
except Exception as e: except Exception as e:
@@ -304,13 +360,17 @@ class OptimizedTelegramScraper:
batch_size = 10 batch_size = 10
for i in range(0, len(media_tasks), batch_size): for i in range(0, len(media_tasks), batch_size):
batch = media_tasks[i : i + batch_size] batch = media_tasks[i : i + batch_size]
tasks = [asyncio.create_task(download_single_media(msg)) for msg in batch] tasks = [
asyncio.create_task(download_single_media(msg)) for msg in batch
]
for j, task in enumerate(tasks): for j, task in enumerate(tasks):
try: try:
media_path = await task media_path = await task
if media_path: if media_path:
await self.update_media_path(channel, batch[j].id, media_path) await self.update_media_path(
channel, batch[j].id, media_path
)
successful_downloads += 1 successful_downloads += 1
except Exception: except Exception:
pass pass
@@ -319,14 +379,18 @@ class OptimizedTelegramScraper:
progress = (completed_media / total_media) * 100 progress = (completed_media / total_media) * 100
bar_length = 30 bar_length = 30
filled_length = int(bar_length * completed_media // total_media) filled_length = int(bar_length * completed_media // total_media)
bar = '' * filled_length + '' * (bar_length - filled_length) bar = "" * filled_length + "" * (bar_length - filled_length)
sys.stdout.write(f"\r📥 Media: [{bar}] {progress:.1f}% ({completed_media}/{total_media})") sys.stdout.write(
f"\r📥 Media: [{bar}] {progress:.1f}% ({completed_media}/{total_media})"
)
sys.stdout.flush() sys.stdout.flush()
print(f"\n✅ Media download complete! ({successful_downloads}/{total_media} successful)") print(
f"\n✅ Media download complete! ({successful_downloads}/{total_media} successful)"
)
self.state['channels'][channel] = last_message_id self.state["channels"][channel] = last_message_id
self.save_state() self.save_state()
print(f"\nCompleted scraping channel {channel}") print(f"\nCompleted scraping channel {channel}")
@@ -336,19 +400,23 @@ class OptimizedTelegramScraper:
async def rescrape_media(self, channel: str): async def rescrape_media(self, channel: str):
conn = self.get_db_connection(channel) conn = self.get_db_connection(channel)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT message_id FROM messages WHERE media_type IS NOT NULL AND media_type != "MessageMediaWebPage" AND media_path IS NULL') cursor.execute(
'SELECT message_id FROM messages WHERE media_type IS NOT NULL AND media_type != "MessageMediaWebPage" AND media_path IS NULL'
)
message_ids = [row[0] for row in cursor.fetchall()] message_ids = [row[0] for row in cursor.fetchall()]
channel_name = self.state.get('channel_names', {}).get(channel, 'Unknown') channel_name = self.state.get("channel_names", {}).get(channel, "Unknown")
if not message_ids: if not message_ids:
print(f"No media files to reprocess for {channel_name} (ID: {channel})") print(f"No media files to reprocess for {channel_name} (ID: {channel})")
return return
print(f"📥 Reprocessing {len(message_ids)} media files for {channel_name} (ID: {channel})") print(
f"📥 Reprocessing {len(message_ids)} media files for {channel_name} (ID: {channel})"
)
try: try:
if channel.lstrip('-').isdigit(): if channel.lstrip("-").isdigit():
entity = await self.client.get_entity(PeerChannel(int(channel))) entity = await self.client.get_entity(PeerChannel(int(channel)))
else: else:
entity = await self.client.get_entity(channel) entity = await self.client.get_entity(channel)
@@ -365,14 +433,25 @@ class OptimizedTelegramScraper:
batch_ids = message_ids[i : i + batch_size] batch_ids = message_ids[i : i + batch_size]
messages = await self.client.get_messages(entity, ids=batch_ids) messages = await self.client.get_messages(entity, ids=batch_ids)
valid_messages = [msg for msg in messages if msg and msg.media and not isinstance(msg.media, MessageMediaWebPage)] valid_messages = [
tasks = [asyncio.create_task(download_single_media(msg)) for msg in valid_messages] msg
for msg in messages
if msg
and msg.media
and not isinstance(msg.media, MessageMediaWebPage)
]
tasks = [
asyncio.create_task(download_single_media(msg))
for msg in valid_messages
]
for j, task in enumerate(tasks): for j, task in enumerate(tasks):
try: try:
media_path = await task media_path = await task
if media_path: if media_path:
await self.update_media_path(channel, valid_messages[j].id, media_path) await self.update_media_path(
channel, valid_messages[j].id, media_path
)
successful_downloads += 1 successful_downloads += 1
except Exception: except Exception:
pass pass
@@ -380,13 +459,19 @@ class OptimizedTelegramScraper:
completed_media += 1 completed_media += 1
progress = (completed_media / len(message_ids)) * 100 progress = (completed_media / len(message_ids)) * 100
bar_length = 30 bar_length = 30
filled_length = int(bar_length * completed_media // len(message_ids)) filled_length = int(
bar = '' * filled_length + '' * (bar_length - filled_length) bar_length * completed_media // len(message_ids)
)
bar = "" * filled_length + "" * (bar_length - filled_length)
sys.stdout.write(f"\r🔄 Rescrape: [{bar}] {progress:.1f}% ({completed_media}/{len(message_ids)})") sys.stdout.write(
f"\r🔄 Rescrape: [{bar}] {progress:.1f}% ({completed_media}/{len(message_ids)})"
)
sys.stdout.flush() sys.stdout.flush()
print(f"\n✅ Media reprocessing complete! ({successful_downloads}/{len(message_ids)} successful)") print(
f"\n✅ Media reprocessing complete! ({successful_downloads}/{len(message_ids)} successful)"
)
except Exception as e: except Exception as e:
print(f"Error reprocessing media: {e}") print(f"Error reprocessing media: {e}")
@@ -395,15 +480,19 @@ class OptimizedTelegramScraper:
conn = self.get_db_connection(channel) conn = self.get_db_connection(channel)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM messages WHERE media_type IS NOT NULL AND media_type != "MessageMediaWebPage"') cursor.execute(
'SELECT COUNT(*) FROM messages WHERE media_type IS NOT NULL AND media_type != "MessageMediaWebPage"'
)
total_with_media = cursor.fetchone()[0] total_with_media = cursor.fetchone()[0]
cursor.execute('SELECT COUNT(*) FROM messages WHERE media_type IS NOT NULL AND media_type != "MessageMediaWebPage" AND media_path IS NOT NULL') cursor.execute(
'SELECT COUNT(*) FROM messages WHERE media_type IS NOT NULL AND media_type != "MessageMediaWebPage" AND media_path IS NOT NULL'
)
total_with_files = cursor.fetchone()[0] total_with_files = cursor.fetchone()[0]
missing_count = total_with_media - total_with_files missing_count = total_with_media - total_with_files
channel_name = self.state.get('channel_names', {}).get(channel, 'Unknown') channel_name = self.state.get("channel_names", {}).get(channel, "Unknown")
print(f"\n📊 Media Analysis for {channel_name} (ID: {channel}):") print(f"\n📊 Media Analysis for {channel_name} (ID: {channel}):")
print(f"Messages with media: {total_with_media}") print(f"Messages with media: {total_with_media}")
print(f"Media files downloaded: {total_with_files}") print(f"Media files downloaded: {total_with_files}")
@@ -413,17 +502,21 @@ class OptimizedTelegramScraper:
print("✅ All media files are already downloaded!") print("✅ All media files are already downloaded!")
return return
cursor.execute('SELECT message_id, media_type FROM messages WHERE media_type IS NOT NULL AND media_type != "MessageMediaWebPage" AND (media_path IS NULL OR media_path = "")') cursor.execute(
'SELECT message_id, media_type FROM messages WHERE media_type IS NOT NULL AND media_type != "MessageMediaWebPage" AND (media_path IS NULL OR media_path = "")'
)
missing_media = cursor.fetchall() missing_media = cursor.fetchall()
if not missing_media: if not missing_media:
print("✅ No missing media found!") print("✅ No missing media found!")
return return
print(f"\n🔧 Attempting to download {len(missing_media)} missing media files...") print(
f"\n🔧 Attempting to download {len(missing_media)} missing media files..."
)
try: try:
if channel.lstrip('-').isdigit(): if channel.lstrip("-").isdigit():
entity = await self.client.get_entity(PeerChannel(int(channel))) entity = await self.client.get_entity(PeerChannel(int(channel)))
else: else:
entity = await self.client.get_entity(channel) entity = await self.client.get_entity(channel)
@@ -441,15 +534,26 @@ class OptimizedTelegramScraper:
message_ids = [msg[0] for msg in batch] message_ids = [msg[0] for msg in batch]
messages = await self.client.get_messages(entity, ids=message_ids) messages = await self.client.get_messages(entity, ids=message_ids)
valid_messages = [msg for msg in messages if msg and msg.media and not isinstance(msg.media, MessageMediaWebPage)] valid_messages = [
msg
for msg in messages
if msg
and msg.media
and not isinstance(msg.media, MessageMediaWebPage)
]
tasks = [asyncio.create_task(download_single_media(msg)) for msg in valid_messages] tasks = [
asyncio.create_task(download_single_media(msg))
for msg in valid_messages
]
for j, task in enumerate(tasks): for j, task in enumerate(tasks):
try: try:
media_path = await task media_path = await task
if media_path: if media_path:
await self.update_media_path(channel, valid_messages[j].id, media_path) await self.update_media_path(
channel, valid_messages[j].id, media_path
)
successful_downloads += 1 successful_downloads += 1
except Exception: except Exception:
pass pass
@@ -457,13 +561,19 @@ class OptimizedTelegramScraper:
completed_media += 1 completed_media += 1
progress = (completed_media / len(missing_media)) * 100 progress = (completed_media / len(missing_media)) * 100
bar_length = 30 bar_length = 30
filled_length = int(bar_length * completed_media // len(missing_media)) filled_length = int(
bar = '' * filled_length + '' * (bar_length - filled_length) bar_length * completed_media // len(missing_media)
)
bar = "" * filled_length + "" * (bar_length - filled_length)
sys.stdout.write(f"\r🔧 Fix Media: [{bar}] {progress:.1f}% ({completed_media}/{len(missing_media)})") sys.stdout.write(
f"\r🔧 Fix Media: [{bar}] {progress:.1f}% ({completed_media}/{len(missing_media)})"
)
sys.stdout.flush() sys.stdout.flush()
print(f"\n✅ Media fix complete! ({successful_downloads}/{len(missing_media)} successful)") print(
f"\n✅ Media fix complete! ({successful_downloads}/{len(missing_media)} successful)"
)
except Exception as e: except Exception as e:
print(f"Error fixing missing media: {e}") print(f"Error fixing missing media: {e}")
@@ -475,11 +585,11 @@ class OptimizedTelegramScraper:
while self.continuous_scraping_active: while self.continuous_scraping_active:
start_time = time.time() start_time = time.time()
for channel in self.state['channels']: for channel in self.state["channels"]:
if not self.continuous_scraping_active: if not self.continuous_scraping_active:
break break
print(f"\nChecking for new messages in channel: {channel}") print(f"\nChecking for new messages in channel: {channel}")
await self.scrape_channel(channel, self.state['channels'][channel]) await self.scrape_channel(channel, self.state["channels"][channel])
elapsed = time.time() - start_time elapsed = time.time() - start_time
sleep_time = max(0, 60 - elapsed) sleep_time = max(0, 60 - elapsed)
@@ -492,19 +602,19 @@ class OptimizedTelegramScraper:
self.continuous_scraping_active = False self.continuous_scraping_active = False
def get_export_filename(self, channel: str): def get_export_filename(self, channel: str):
username = self.state.get('channel_names', {}).get(channel, 'no_username') username = self.state.get("channel_names", {}).get(channel, "no_username")
return f"{channel}_{username}" return f"{channel}_{username}"
def export_to_csv(self, channel: str): def export_to_csv(self, channel: str):
conn = self.get_db_connection(channel) conn = self.get_db_connection(channel)
filename = self.get_export_filename(channel) filename = self.get_export_filename(channel)
csv_file = Path(channel) / f'{filename}.csv' csv_file = Path(channel) / f"{filename}.csv"
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT * FROM messages ORDER BY date') cursor.execute("SELECT * FROM messages ORDER BY date")
columns = [description[0] for description in cursor.description] columns = [description[0] for description in cursor.description]
with open(csv_file, 'w', newline='', encoding='utf-8') as f: with open(csv_file, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f) writer = csv.writer(f)
writer.writerow(columns) writer.writerow(columns)
@@ -517,14 +627,14 @@ class OptimizedTelegramScraper:
def export_to_json(self, channel: str): def export_to_json(self, channel: str):
conn = self.get_db_connection(channel) conn = self.get_db_connection(channel)
filename = self.get_export_filename(channel) filename = self.get_export_filename(channel)
json_file = Path(channel) / f'{filename}.json' json_file = Path(channel) / f"{filename}.json"
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT * FROM messages ORDER BY date') cursor.execute("SELECT * FROM messages ORDER BY date")
columns = [description[0] for description in cursor.description] columns = [description[0] for description in cursor.description]
with open(json_file, 'w', encoding='utf-8') as f: with open(json_file, "w", encoding="utf-8") as f:
f.write('[\n') f.write("[\n")
first_row = True first_row = True
while True: while True:
@@ -534,21 +644,21 @@ class OptimizedTelegramScraper:
for row in rows: for row in rows:
if not first_row: if not first_row:
f.write(',\n') f.write(",\n")
else: else:
first_row = False first_row = False
data = dict(zip(columns, row)) data = dict(zip(columns, row))
json.dump(data, f, ensure_ascii=False, indent=2) json.dump(data, f, ensure_ascii=False, indent=2)
f.write('\n]') f.write("\n]")
async def export_data(self): async def export_data(self):
if not self.state['channels']: if not self.state["channels"]:
print("No channels to export") print("No channels to export")
return return
for channel in self.state['channels']: for channel in self.state["channels"]:
print(f"Exporting data for channel {channel}...") print(f"Exporting data for channel {channel}...")
try: try:
self.export_to_csv(channel) self.export_to_csv(channel)
@@ -558,22 +668,30 @@ class OptimizedTelegramScraper:
print(f"❌ Export failed for channel {channel}: {e}") print(f"❌ Export failed for channel {channel}: {e}")
async def view_channels(self): async def view_channels(self):
if not self.state['channels']: if not self.state["channels"]:
print("No channels saved") print("No channels saved")
return return
print("\nCurrent channels:") print("\nCurrent channels:")
for i, (channel, last_id) in enumerate(self.state['channels'].items(), 1): for i, (channel, last_id) in enumerate(self.state["channels"].items(), 1):
try: try:
conn = self.get_db_connection(channel) conn = self.get_db_connection(channel)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM messages') cursor.execute("SELECT COUNT(*) FROM messages")
count = cursor.fetchone()[0] count = cursor.fetchone()[0]
channel_name = self.state.get('channel_names', {}).get(channel, 'Unknown') channel_name = self.state.get("channel_names", {}).get(
print(f"[{i}] {channel_name} (ID: {channel}), Last Message ID: {last_id}, Messages: {count}") channel, "Unknown"
except: )
channel_name = self.state.get('channel_names', {}).get(channel, 'Unknown') print(
print(f"[{i}] {channel_name} (ID: {channel}), Last Message ID: {last_id}") f"[{i}] {channel_name} (ID: {channel}), Last Message ID: {last_id}, Messages: {count}"
)
except Exception:
channel_name = self.state.get("channel_names", {}).get(
channel, "Unknown"
)
print(
f"[{i}] {channel_name} (ID: {channel}), Last Message ID: {last_id}"
)
async def list_channels(self): async def list_channels(self):
try: try:
@@ -582,23 +700,42 @@ class OptimizedTelegramScraper:
channels_data = [] channels_data = []
async for dialog in self.client.iter_dialogs(): async for dialog in self.client.iter_dialogs():
entity = dialog.entity entity = dialog.entity
if dialog.id != 777000 and (isinstance(entity, Channel) or isinstance(entity, Chat)): if dialog.id != 777000 and (
channel_type = "Channel" if isinstance(entity, Channel) and entity.broadcast else "Group" isinstance(entity, Channel) or isinstance(entity, Chat)
username = getattr(entity, 'username', None) or 'no_username' ):
print(f"[{count}] {dialog.title} (ID: {dialog.id}, Type: {channel_type}, Username: @{username})") channel_type = (
channels_data.append({ "Channel"
'number': count, if isinstance(entity, Channel) and entity.broadcast
'channel_name': dialog.title, else "Group"
'channel_id': str(dialog.id), )
'username': username, username = getattr(entity, "username", None) or "no_username"
'type': channel_type print(
}) f"[{count}] {dialog.title} (ID: {dialog.id}, Type: {channel_type}, Username: @{username})"
)
channels_data.append(
{
"number": count,
"channel_name": dialog.title,
"channel_id": str(dialog.id),
"username": username,
"type": channel_type,
}
)
count += 1 count += 1
if channels_data: if channels_data:
csv_file = Path('channels_list.csv') csv_file = Path("channels_list.csv")
with open(csv_file, 'w', newline='', encoding='utf-8') as f: with open(csv_file, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=['number', 'channel_name', 'channel_id', 'username', 'type']) writer = csv.DictWriter(
f,
fieldnames=[
"number",
"channel_name",
"channel_id",
"username",
"type",
],
)
writer.writeheader() writer.writeheader()
writer.writerows(channels_data) writer.writerows(channels_data)
print(f"\n✅ Saved channels list to {csv_file}") print(f"\n✅ Saved channels list to {csv_file}")
@@ -661,18 +798,20 @@ class OptimizedTelegramScraper:
return False return False
async def initialize_client(self): async def initialize_client(self):
if not all([self.state.get('api_id'), self.state.get('api_hash')]): if not all([self.state.get("api_id"), self.state.get("api_hash")]):
print("\n=== API Configuration Required ===") print("\n=== API Configuration Required ===")
print("You need to provide API credentials from https://my.telegram.org") print("You need to provide API credentials from https://my.telegram.org")
try: try:
self.state['api_id'] = int(input("Enter your API ID: ")) self.state["api_id"] = int(input("Enter your API ID: "))
self.state['api_hash'] = input("Enter your API Hash: ") self.state["api_hash"] = input("Enter your API Hash: ")
self.save_state() self.save_state()
except ValueError: except ValueError:
print("Invalid API ID. Must be a number.") print("Invalid API ID. Must be a number.")
return False return False
self.client = TelegramClient('session', self.state['api_id'], self.state['api_hash']) self.client = TelegramClient(
"session", self.state["api_id"], self.state["api_hash"]
)
try: try:
await self.client.connect() await self.client.connect()
@@ -687,11 +826,13 @@ class OptimizedTelegramScraper:
while True: while True:
choice = input("Enter your choice (1 or 2): ").strip() choice = input("Enter your choice (1 or 2): ").strip()
if choice in ['1', '2']: if choice in ["1", "2"]:
break break
print("Please enter 1 or 2") print("Please enter 1 or 2")
success = await self.qr_code_auth() if choice == '1' else await self.phone_auth() success = (
await self.qr_code_auth() if choice == "1" else await self.phone_auth()
)
if not success: if not success:
print("Authentication failed. Please try again.") print("Authentication failed. Please try again.")
@@ -703,16 +844,16 @@ class OptimizedTelegramScraper:
return True return True
def parse_channel_selection(self, choice): def parse_channel_selection(self, choice):
channels_list = list(self.state['channels'].keys()) channels_list = list(self.state["channels"].keys())
selected_channels = [] selected_channels = []
if choice.lower() == 'all': if choice.lower() == "all":
return channels_list return channels_list
for selection in [x.strip() for x in choice.split(',')]: for selection in [x.strip() for x in choice.split(",")]:
try: try:
if selection.startswith('-'): if selection.startswith("-"):
if selection in self.state['channels']: if selection in self.state["channels"]:
selected_channels.append(selection) selected_channels.append(selection)
else: else:
print(f"Channel ID {selection} not found in your channels") print(f"Channel ID {selection} not found in your channels")
@@ -721,14 +862,18 @@ class OptimizedTelegramScraper:
if 1 <= num <= len(channels_list): if 1 <= num <= len(channels_list):
selected_channels.append(channels_list[num - 1]) selected_channels.append(channels_list[num - 1])
else: else:
print(f"Invalid channel number: {num}. Valid range: 1-{len(channels_list)}") print(
f"Invalid channel number: {num}. Valid range: 1-{len(channels_list)}"
)
except ValueError: except ValueError:
print(f"Invalid input: {selection}. Use numbers (1,2,3) or full IDs (-100123...)") print(
f"Invalid input: {selection}. Use numbers (1,2,3) or full IDs (-100123...)"
)
return selected_channels return selected_channels
async def scrape_specific_channels(self): async def scrape_specific_channels(self):
if not self.state['channels']: if not self.state["channels"]:
print("No channels available. Use [L] to add channels first") print("No channels available. Use [L] to add channels first")
return return
@@ -745,7 +890,7 @@ class OptimizedTelegramScraper:
print(f"\n🚀 Starting scrape of {len(selected_channels)} channel(s)...") print(f"\n🚀 Starting scrape of {len(selected_channels)} channel(s)...")
for i, channel in enumerate(selected_channels, 1): for i, channel in enumerate(selected_channels, 1):
print(f"\n[{i}/{len(selected_channels)}] Scraping: {channel}") print(f"\n[{i}/{len(selected_channels)}] Scraping: {channel}")
await self.scrape_channel(channel, self.state['channels'][channel]) await self.scrape_channel(channel, self.state["channels"][channel])
print(f"\n✅ Completed scraping {len(selected_channels)} channel(s)!") print(f"\n✅ Completed scraping {len(selected_channels)} channel(s)!")
else: else:
print("❌ No valid channels selected") print("❌ No valid channels selected")
@@ -757,7 +902,9 @@ class OptimizedTelegramScraper:
print("=" * 40) print("=" * 40)
print("[S] Scrape channels") print("[S] Scrape channels")
print("[C] Continuous scraping") print("[C] Continuous scraping")
print(f"[M] Media scraping: {'ON' if self.state['scrape_media'] else 'OFF'}") print(
f"[M] Media scraping: {'ON' if self.state['scrape_media'] else 'OFF'}"
)
print("[L] List & add channels") print("[L] List & add channels")
print("[R] Remove channels") print("[R] Remove channels")
print("[E] Export data") print("[E] Export data")
@@ -769,8 +916,8 @@ class OptimizedTelegramScraper:
choice = input("Enter your choice: ").lower().strip() choice = input("Enter your choice: ").lower().strip()
try: try:
if choice == 'r': if choice == "r":
if not self.state['channels']: if not self.state["channels"]:
print("No channels to remove") print("No channels to remove")
continue continue
@@ -784,8 +931,8 @@ class OptimizedTelegramScraper:
if selected_channels: if selected_channels:
removed_count = 0 removed_count = 0
for channel in selected_channels: for channel in selected_channels:
if channel in self.state['channels']: if channel in self.state["channels"]:
del self.state['channels'][channel] del self.state["channels"][channel]
print(f"✅ Removed channel {channel}") print(f"✅ Removed channel {channel}")
removed_count += 1 removed_count += 1
else: else:
@@ -800,19 +947,21 @@ class OptimizedTelegramScraper:
else: else:
print("No valid channels selected") print("No valid channels selected")
elif choice == 's': elif choice == "s":
await self.scrape_specific_channels() await self.scrape_specific_channels()
elif choice == 'm': elif choice == "m":
self.state['scrape_media'] = not self.state['scrape_media'] self.state["scrape_media"] = not self.state["scrape_media"]
self.save_state() self.save_state()
print(f"\n✅ Media scraping {'enabled' if self.state['scrape_media'] else 'disabled'}") print(
f"\n✅ Media scraping {'enabled' if self.state['scrape_media'] else 'disabled'}"
)
elif choice == 'c': elif choice == "c":
task = asyncio.create_task(self.continuous_scraping()) task = asyncio.create_task(self.continuous_scraping())
print("Continuous scraping started. Press Ctrl+C to stop.") print("Continuous scraping started. Press Ctrl+C to stop.")
try: try:
await asyncio.sleep(float('inf')) await asyncio.sleep(float("inf"))
except KeyboardInterrupt: except KeyboardInterrupt:
self.continuous_scraping_active = False self.continuous_scraping_active = False
task.cancel() task.cancel()
@@ -822,10 +971,10 @@ class OptimizedTelegramScraper:
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
elif choice == 'e': elif choice == "e":
await self.export_data() await self.export_data()
elif choice == 'l': elif choice == "l":
channels_data = await self.list_channels() channels_data = await self.list_channels()
if not channels_data: if not channels_data:
@@ -841,24 +990,37 @@ class OptimizedTelegramScraper:
if selection: if selection:
added_count = 0 added_count = 0
if selection.lower() == 'all': if selection.lower() == "all":
for channel_info in channels_data: for channel_info in channels_data:
channel_id = channel_info['channel_id'] channel_id = channel_info["channel_id"]
if channel_id not in self.state['channels']: if channel_id not in self.state["channels"]:
self.state['channels'][channel_id] = 0 self.state["channels"][channel_id] = 0
if 'channel_names' not in self.state: if "channel_names" not in self.state:
self.state['channel_names'] = {} self.state["channel_names"] = {}
self.state['channel_names'][channel_id] = channel_info['username'] self.state["channel_names"][channel_id] = (
print(f"✅ Added channel {channel_info['channel_name']} (ID: {channel_id})") channel_info["username"]
)
print(
f"✅ Added channel {channel_info['channel_name']} (ID: {channel_id})"
)
added_count += 1 added_count += 1
else: else:
print(f"Channel {channel_info['channel_name']} already added") print(
f"Channel {channel_info['channel_name']} already added"
)
else: else:
for sel in [x.strip() for x in selection.split(',')]: for sel in [x.strip() for x in selection.split(",")]:
try: try:
if sel.startswith('-'): if sel.startswith("-"):
channel_id = sel channel_id = sel
channel_info = next((c for c in channels_data if c['channel_id'] == channel_id), None) channel_info = next(
(
c
for c in channels_data
if c["channel_id"] == channel_id
),
None,
)
if not channel_info: if not channel_info:
print(f"Channel ID {channel_id} not found") print(f"Channel ID {channel_id} not found")
continue continue
@@ -866,19 +1028,27 @@ class OptimizedTelegramScraper:
num = int(sel) num = int(sel)
if 1 <= num <= len(channels_data): if 1 <= num <= len(channels_data):
channel_info = channels_data[num - 1] channel_info = channels_data[num - 1]
channel_id = channel_info['channel_id'] channel_id = channel_info["channel_id"]
else: else:
print(f"Invalid number: {num}. Choose 1-{len(channels_data)}") print(
f"Invalid number: {num}. Choose 1-{len(channels_data)}"
)
continue continue
if channel_id in self.state['channels']: if channel_id in self.state["channels"]:
print(f"Channel {channel_info['channel_name']} already added") print(
f"Channel {channel_info['channel_name']} already added"
)
else: else:
self.state['channels'][channel_id] = 0 self.state["channels"][channel_id] = 0
if 'channel_names' not in self.state: if "channel_names" not in self.state:
self.state['channel_names'] = {} self.state["channel_names"] = {}
self.state['channel_names'][channel_id] = channel_info['username'] self.state["channel_names"][channel_id] = (
print(f"✅ Added channel {channel_info['channel_name']} (ID: {channel_id})") channel_info["username"]
)
print(
f"✅ Added channel {channel_info['channel_name']} (ID: {channel_id})"
)
added_count += 1 added_count += 1
except ValueError: except ValueError:
@@ -891,13 +1061,15 @@ class OptimizedTelegramScraper:
else: else:
print("No new channels were added") print("No new channels were added")
elif choice == 't': elif choice == "t":
if not self.state['channels']: if not self.state["channels"]:
print("No channels available. Add channels first") print("No channels available. Add channels first")
continue continue
await self.view_channels() await self.view_channels()
print("\nEnter channel NUMBER (1,2,3...) or full channel ID (-100123...)") print(
"\nEnter channel NUMBER (1,2,3...) or full channel ID (-100123...)"
)
selection = input("Enter your selection: ").strip() selection = input("Enter your selection: ").strip()
selected_channels = self.parse_channel_selection(selection) selected_channels = self.parse_channel_selection(selection)
@@ -910,13 +1082,15 @@ class OptimizedTelegramScraper:
else: else:
print("No valid channel selected") print("No valid channel selected")
elif choice == 'f': elif choice == "f":
if not self.state['channels']: if not self.state["channels"]:
print("No channels available. Add channels first") print("No channels available. Add channels first")
continue continue
await self.view_channels() await self.view_channels()
print("\nEnter channel NUMBER (1,2,3...) or full channel ID (-100123...)") print(
"\nEnter channel NUMBER (1,2,3...) or full channel ID (-100123...)"
)
selection = input("Enter your selection: ").strip() selection = input("Enter your selection: ").strip()
selected_channels = self.parse_channel_selection(selection) selected_channels = self.parse_channel_selection(selection)
@@ -928,7 +1102,7 @@ class OptimizedTelegramScraper:
else: else:
print("No valid channel selected") print("No valid channel selected")
elif choice == 'q': elif choice == "q":
print("\n👋 Goodbye!") print("\n👋 Goodbye!")
self.close_db_connections() self.close_db_connections()
if self.client: if self.client:
@@ -953,11 +1127,13 @@ class OptimizedTelegramScraper:
else: else:
print("Failed to initialize client. Exiting.") print("Failed to initialize client. Exiting.")
async def main(): async def main():
scraper = OptimizedTelegramScraper() scraper = OptimizedTelegramScraper()
await scraper.run() await scraper.run()
if __name__ == '__main__':
if __name__ == "__main__":
try: try:
asyncio.run(main()) asyncio.run(main())
except KeyboardInterrupt: except KeyboardInterrupt:
File diff suppressed because it is too large Load Diff
+35 -13
View File
@@ -306,7 +306,9 @@ class JobRunner:
if job.status == "running": if job.status == "running":
running_jobs.append(job) running_jobs.append(job)
if running_jobs: if running_jobs:
logger.warning("Waiting for %d running job(s) to finish...", len(running_jobs)) logger.warning(
"Waiting for %d running job(s) to finish...", len(running_jobs)
)
deadline = time.time() + timeout deadline = time.time() + timeout
while time.time() < deadline: while time.time() < deadline:
with self.lock: with self.lock:
@@ -561,6 +563,7 @@ class TelegramAuthManager:
async def _disconnect(): async def _disconnect():
if self.client: if self.client:
await self.client.disconnect() await self.client.disconnect()
if self.client: if self.client:
try: try:
future = asyncio.run_coroutine_threadsafe(_disconnect(), self.loop) future = asyncio.run_coroutine_threadsafe(_disconnect(), self.loop)
@@ -831,7 +834,10 @@ def openapi_payload() -> Dict[str, Any]:
"requestBody": json_body( "requestBody": json_body(
{ {
"api_id": {"type": "integer", "example": 123456}, "api_id": {"type": "integer", "example": 123456},
"api_hash": {"type": "string", "example": "0123456789abcdef"}, "api_hash": {
"type": "string",
"example": "0123456789abcdef",
},
}, },
["api_id", "api_hash"], ["api_id", "api_hash"],
), ),
@@ -929,7 +935,11 @@ def openapi_payload() -> Dict[str, Any]:
{ {
"name": "limit", "name": "limit",
"in": "query", "in": "query",
"schema": {"type": "integer", "default": 120, "maximum": 300}, "schema": {
"type": "integer",
"default": 120,
"maximum": 300,
},
}, },
{ {
"name": "before", "name": "before",
@@ -945,7 +955,10 @@ def openapi_payload() -> Dict[str, Any]:
"summary": "Track a channel", "summary": "Track a channel",
"requestBody": json_body( "requestBody": json_body(
{ {
"channel_id": {"type": "string", "example": "-1001234567890"}, "channel_id": {
"type": "string",
"example": "-1001234567890",
},
"name": {"type": "string", "example": "Research feed"}, "name": {"type": "string", "example": "Research feed"},
}, },
["channel_id"], ["channel_id"],
@@ -990,7 +1003,10 @@ def openapi_payload() -> Dict[str, Any]:
"schema": {"type": "string"}, "schema": {"type": "string"},
} }
], ],
"responses": {**json_response, "404": {"description": "Job not found"}}, "responses": {
**json_response,
"404": {"description": "Job not found"},
},
} }
}, },
"/api/jobs/scrape": { "/api/jobs/scrape": {
@@ -1416,15 +1432,18 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
try: try:
start, end = self._parse_range(range_header, file_size) start, end = self._parse_range(range_header, file_size)
except ValueError: except ValueError:
self.send_error_json(HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE, "Invalid range") self.send_error_json(
HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE, "Invalid range"
)
return return
content_length = end - start + 1 content_length = end - start + 1
self.send_response(HTTPStatus.PARTIAL_CONTENT) self.send_response(HTTPStatus.PARTIAL_CONTENT)
self.send_header("Content-Type", content_type) self.send_header("Content-Type", content_type)
self.send_header("Accept-Ranges", "bytes") self.send_header("Accept-Ranges", "bytes")
self.send_header("Last-Modified", time.strftime( self.send_header(
"%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified) "Last-Modified",
)) time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified)),
)
self.send_header("Content-Range", f"bytes {start}-{end}/{file_size}") self.send_header("Content-Range", f"bytes {start}-{end}/{file_size}")
self.send_header("Content-Length", str(content_length)) self.send_header("Content-Length", str(content_length))
self.end_headers() self.end_headers()
@@ -1434,9 +1453,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
self.send_response(HTTPStatus.OK) self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", content_type) self.send_header("Content-Type", content_type)
self.send_header("Accept-Ranges", "bytes") self.send_header("Accept-Ranges", "bytes")
self.send_header("Last-Modified", time.strftime( self.send_header(
"%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified) "Last-Modified",
)) time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified)),
)
self.send_header("Content-Length", str(file_size)) self.send_header("Content-Length", str(file_size))
self.end_headers() self.end_headers()
if not head_only: if not head_only:
@@ -1495,7 +1515,9 @@ class TelegramScraperWebServer(ThreadingHTTPServer):
self.job_runner = JobRunner() self.job_runner = JobRunner()
self.auth_manager = TelegramAuthManager() self.auth_manager = TelegramAuthManager()
self.continuous_manager = ContinuousScrapeManager() self.continuous_manager = ContinuousScrapeManager()
if START_CONTINUOUS and self.continuous_manager.snapshot()["config"].get("enabled", True): if START_CONTINUOUS and self.continuous_manager.snapshot()["config"].get(
"enabled", True
):
self.continuous_manager.start() self.continuous_manager.start()