mirror of
https://github.com/avitoras/telegram-tui.git
synced 2025-07-27 19:26:10 +00:00
UNSTABLE | Cache
This commit is contained in:
parent
3592da4503
commit
db3ea38ad4
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,7 +13,7 @@ venv/
|
|||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
pics*
|
pics*
|
||||||
|
cache*
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
@ -19,6 +19,8 @@ from PIL import Image
|
|||||||
import io
|
import io
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
# Разрешаем вложенные event loops
|
# Разрешаем вложенные event loops
|
||||||
nest_asyncio.apply()
|
nest_asyncio.apply()
|
||||||
@ -160,6 +162,123 @@ class AsciiArtCache:
|
|||||||
|
|
||||||
return ascii_art
|
return ascii_art
|
||||||
|
|
||||||
|
class MediaCache:
|
||||||
|
"""Класс для кэширования медиафайлов"""
|
||||||
|
def __init__(self, cache_dir='cache', max_size_mb=1000):
|
||||||
|
self.cache_dir = Path(cache_dir)
|
||||||
|
self.files_dir = self.cache_dir / 'files'
|
||||||
|
self.index_file = self.cache_dir / 'index.json'
|
||||||
|
self.max_size = max_size_mb * 1024 * 1024 # Конвертируем в байты
|
||||||
|
self.current_size = 0
|
||||||
|
|
||||||
|
# Создаем директории
|
||||||
|
self.files_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Загружаем индекс
|
||||||
|
self.load_index()
|
||||||
|
|
||||||
|
def load_index(self):
|
||||||
|
"""Загружает индекс кэшированных файлов"""
|
||||||
|
try:
|
||||||
|
with open(self.index_file, 'r') as f:
|
||||||
|
self.index = json.load(f)
|
||||||
|
# Подсчитываем текущий размер кэша
|
||||||
|
self.current_size = sum(item['size'] for item in self.index.values())
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
self.index = {}
|
||||||
|
self.current_size = 0
|
||||||
|
self.save_index()
|
||||||
|
|
||||||
|
def save_index(self):
|
||||||
|
"""Сохраняет индекс кэшированных файлов"""
|
||||||
|
with open(self.index_file, 'w') as f:
|
||||||
|
json.dump(self.index, f)
|
||||||
|
|
||||||
|
def get_cache_key(self, data):
|
||||||
|
"""Генерирует ключ кэша"""
|
||||||
|
return hashlib.md5(data).hexdigest()
|
||||||
|
|
||||||
|
def cleanup(self, needed_space=0):
|
||||||
|
"""Очищает старые файлы для освобождения места"""
|
||||||
|
if self.current_size + needed_space <= self.max_size:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Сортируем файлы по времени последнего доступа
|
||||||
|
files = [(k, v) for k, v in self.index.items()]
|
||||||
|
files.sort(key=lambda x: x[1]['last_access'])
|
||||||
|
|
||||||
|
# Удаляем старые файлы, пока не освободится достаточно места
|
||||||
|
for key, info in files:
|
||||||
|
file_path = self.files_dir / f"{key}{info['ext']}"
|
||||||
|
try:
|
||||||
|
file_path.unlink()
|
||||||
|
self.current_size -= info['size']
|
||||||
|
del self.index[key]
|
||||||
|
if self.current_size + needed_space <= self.max_size:
|
||||||
|
self.save_index()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при удалении файла {file_path}: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_cached_file(self, file_data, file_type):
|
||||||
|
"""Получает файл из кэша или сохраняет новый"""
|
||||||
|
cache_key = self.get_cache_key(file_data)
|
||||||
|
|
||||||
|
# Проверяем наличие в кэше
|
||||||
|
if cache_key in self.index:
|
||||||
|
info = self.index[cache_key]
|
||||||
|
file_path = self.files_dir / f"{cache_key}{info['ext']}"
|
||||||
|
if file_path.exists():
|
||||||
|
# Обновляем время последнего доступа
|
||||||
|
info['last_access'] = datetime.datetime.now().isoformat()
|
||||||
|
self.save_index()
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
# Определяем расширение файла
|
||||||
|
ext = self.get_extension_for_type(file_type)
|
||||||
|
|
||||||
|
# Проверяем и освобождаем место если нужно
|
||||||
|
if not self.cleanup(len(file_data)):
|
||||||
|
print("Недостаточно места в кэше")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Сохраняем файл
|
||||||
|
file_path = self.files_dir / f"{cache_key}{ext}"
|
||||||
|
try:
|
||||||
|
with open(file_path, 'wb') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
|
||||||
|
# Обновляем индекс
|
||||||
|
self.index[cache_key] = {
|
||||||
|
'type': file_type,
|
||||||
|
'ext': ext,
|
||||||
|
'size': len(file_data),
|
||||||
|
'created_at': datetime.datetime.now().isoformat(),
|
||||||
|
'last_access': datetime.datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
self.current_size += len(file_data)
|
||||||
|
self.save_index()
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка сохранения файла: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_extension_for_type(self, file_type):
|
||||||
|
"""Возвращает расширение файла для типа медиа"""
|
||||||
|
extensions = {
|
||||||
|
'photo': '.jpg',
|
||||||
|
'video': '.mp4',
|
||||||
|
'audio': '.ogg',
|
||||||
|
'voice': '.ogg',
|
||||||
|
'document': '', # Будет использовано оригинальное расширение
|
||||||
|
'sticker': '.webp',
|
||||||
|
'gif': '.gif'
|
||||||
|
}
|
||||||
|
return extensions.get(file_type, '')
|
||||||
|
|
||||||
class ChatWidget(urwid.WidgetWrap):
|
class ChatWidget(urwid.WidgetWrap):
|
||||||
"""Виджет чата"""
|
"""Виджет чата"""
|
||||||
|
|
||||||
@ -318,20 +437,144 @@ class MessageWidget(urwid.WidgetWrap):
|
|||||||
return key
|
return key
|
||||||
|
|
||||||
class SearchEdit(urwid.Edit):
|
class SearchEdit(urwid.Edit):
|
||||||
|
"""Виджет поиска с отложенным обновлением"""
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.search_callback = kwargs.pop('search_callback', None)
|
self.search_callback = kwargs.pop('search_callback', None)
|
||||||
|
self.search_delay = 0.5 # Задержка поиска в секундах
|
||||||
|
self.last_search = 0
|
||||||
|
self.pending_search = None
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
if key in ('up', 'down', 'esc', 'enter'):
|
if key in ('up', 'down', 'esc', 'enter', 'tab'):
|
||||||
return key
|
return key
|
||||||
|
|
||||||
result = super().keypress(size, key)
|
result = super().keypress(size, key)
|
||||||
# Вызываем поиск при каждом изменении текста
|
|
||||||
|
# Отменяем предыдущий отложенный поиск
|
||||||
|
if self.pending_search:
|
||||||
|
self.pending_search.cancel()
|
||||||
|
|
||||||
|
# Создаем новый отложенный поиск
|
||||||
if self.search_callback and result is None:
|
if self.search_callback and result is None:
|
||||||
asyncio.create_task(self.search_callback())
|
async def delayed_search():
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(self.search_delay)
|
||||||
|
await self.search_callback()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка отложенного поиска: {e}")
|
||||||
|
|
||||||
|
self.pending_search = asyncio.create_task(delayed_search())
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def update_chat_list(self):
|
||||||
|
"""Обновляет список чатов"""
|
||||||
|
try:
|
||||||
|
# Сохраняем текущий фокус и ID выбранного чата
|
||||||
|
current_focus = self.chat_list.focus_position if self.chat_walker else 0
|
||||||
|
current_chat_id = self.current_chat_id
|
||||||
|
|
||||||
|
# Получаем диалоги
|
||||||
|
try:
|
||||||
|
dialogs = await self.telegram_client.get_dialogs(
|
||||||
|
limit=50,
|
||||||
|
folder=self.current_folder
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка получения диалогов: {e}")
|
||||||
|
dialogs = []
|
||||||
|
|
||||||
|
# Фильтруем по поисковому запросу
|
||||||
|
search_query = normalize_text(self.search_edit.get_edit_text().lower())
|
||||||
|
if search_query:
|
||||||
|
filtered_dialogs = []
|
||||||
|
for dialog in dialogs:
|
||||||
|
try:
|
||||||
|
# Получаем имя
|
||||||
|
name = ""
|
||||||
|
if hasattr(dialog.entity, 'title') and dialog.entity.title:
|
||||||
|
name = dialog.entity.title
|
||||||
|
elif hasattr(dialog.entity, 'first_name'):
|
||||||
|
name = dialog.entity.first_name
|
||||||
|
if hasattr(dialog.entity, 'last_name') and dialog.entity.last_name:
|
||||||
|
name += f" {dialog.entity.last_name}"
|
||||||
|
name = normalize_text(name).lower()
|
||||||
|
|
||||||
|
# Получаем последнее сообщение
|
||||||
|
last_message = ""
|
||||||
|
if dialog.message and hasattr(dialog.message, 'message'):
|
||||||
|
last_message = normalize_text(dialog.message.message).lower()
|
||||||
|
|
||||||
|
# Проверяем совпадение
|
||||||
|
if search_query in name or search_query in last_message:
|
||||||
|
filtered_dialogs.append(dialog)
|
||||||
|
|
||||||
|
# Ограничиваем количество результатов для производительности
|
||||||
|
if len(filtered_dialogs) >= 20:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка фильтрации диалога: {e}")
|
||||||
|
|
||||||
|
dialogs = filtered_dialogs
|
||||||
|
|
||||||
|
# Очищаем список
|
||||||
|
self.chat_walker[:] = []
|
||||||
|
|
||||||
|
# Добавляем чаты
|
||||||
|
restored_focus = False
|
||||||
|
for i, dialog in enumerate(dialogs):
|
||||||
|
try:
|
||||||
|
# Получаем имя чата
|
||||||
|
name = ""
|
||||||
|
if hasattr(dialog.entity, 'title') and dialog.entity.title:
|
||||||
|
name = dialog.entity.title
|
||||||
|
elif hasattr(dialog.entity, 'first_name'):
|
||||||
|
name = dialog.entity.first_name
|
||||||
|
if hasattr(dialog.entity, 'last_name') and dialog.entity.last_name:
|
||||||
|
name += f" {dialog.entity.last_name}"
|
||||||
|
else:
|
||||||
|
name = "Без названия"
|
||||||
|
|
||||||
|
# Получаем последнее сообщение
|
||||||
|
message = ""
|
||||||
|
if dialog.message and hasattr(dialog.message, 'message'):
|
||||||
|
message = dialog.message.message
|
||||||
|
|
||||||
|
# Создаем виджет чата
|
||||||
|
chat = ChatWidget(
|
||||||
|
chat_id=dialog.id,
|
||||||
|
name=name,
|
||||||
|
message=message,
|
||||||
|
is_selected=(dialog.id == current_chat_id),
|
||||||
|
folder=1 if self.current_folder else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.chat_walker.append(chat)
|
||||||
|
|
||||||
|
# Восстанавливаем фокус если это текущий чат
|
||||||
|
if dialog.id == current_chat_id and not restored_focus:
|
||||||
|
current_focus = i
|
||||||
|
restored_focus = True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка создания виджета чата: {e}")
|
||||||
|
|
||||||
|
# Восстанавливаем фокус
|
||||||
|
if self.chat_walker:
|
||||||
|
if current_focus >= len(self.chat_walker):
|
||||||
|
current_focus = len(self.chat_walker) - 1
|
||||||
|
self.chat_list.set_focus(max(0, current_focus))
|
||||||
|
self.selected_chat_index = current_focus
|
||||||
|
self.update_selected_chat()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка обновления чатов: {e}")
|
||||||
|
# В случае ошибки очищаем список
|
||||||
|
self.chat_walker[:] = []
|
||||||
|
|
||||||
class InputEdit(urwid.Edit):
|
class InputEdit(urwid.Edit):
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
if key in ('esc', 'up', 'down'):
|
if key in ('esc', 'up', 'down'):
|
||||||
@ -478,6 +721,7 @@ class TelegramTUI:
|
|||||||
self.can_send_messages = False
|
self.can_send_messages = False
|
||||||
|
|
||||||
self.ascii_cache = AsciiArtCache()
|
self.ascii_cache = AsciiArtCache()
|
||||||
|
self.media_cache = MediaCache(max_size_mb=1000) # 1GB по умолчанию
|
||||||
|
|
||||||
def switch_screen(self, screen_name: str):
|
def switch_screen(self, screen_name: str):
|
||||||
"""Переключение между экранами"""
|
"""Переключение между экранами"""
|
||||||
@ -648,6 +892,48 @@ class TelegramTUI:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка обновления выделения: {e}")
|
print(f"Ошибка обновления выделения: {e}")
|
||||||
|
|
||||||
|
async def process_media(self, message):
|
||||||
|
"""Обрабатывает медиа в сообщении"""
|
||||||
|
try:
|
||||||
|
media_type = None
|
||||||
|
media_data = None
|
||||||
|
|
||||||
|
if message.photo:
|
||||||
|
media_type = 'photo'
|
||||||
|
elif message.video:
|
||||||
|
media_type = 'video'
|
||||||
|
elif message.audio:
|
||||||
|
media_type = 'audio'
|
||||||
|
elif message.voice:
|
||||||
|
media_type = 'voice'
|
||||||
|
elif message.document:
|
||||||
|
media_type = 'document'
|
||||||
|
elif message.sticker:
|
||||||
|
media_type = 'sticker'
|
||||||
|
elif getattr(message, 'gif', None):
|
||||||
|
media_type = 'gif'
|
||||||
|
|
||||||
|
if media_type:
|
||||||
|
# Загружаем медиа
|
||||||
|
media_data = await self.telegram_client.download_media(message.media, bytes)
|
||||||
|
if media_data:
|
||||||
|
# Сохраняем в кэш
|
||||||
|
cached_path = self.media_cache.get_cached_file(media_data, media_type)
|
||||||
|
if cached_path:
|
||||||
|
if media_type == 'photo':
|
||||||
|
# Для фото создаем ASCII-арт
|
||||||
|
with open(cached_path, 'rb') as f:
|
||||||
|
return self.ascii_cache.get_cached_art(f.read())
|
||||||
|
else:
|
||||||
|
# Для других типов возвращаем описание
|
||||||
|
size_mb = len(media_data) / (1024 * 1024)
|
||||||
|
return f"[{media_type.upper()}: {size_mb:.1f}MB - {cached_path.name}]"
|
||||||
|
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка обработки медиа: {e}")
|
||||||
|
return f"[Ошибка обработки {media_type if media_type else 'медиа'}]"
|
||||||
|
|
||||||
async def update_message_list(self, chat_id):
|
async def update_message_list(self, chat_id):
|
||||||
"""Обновляет список сообщений"""
|
"""Обновляет список сообщений"""
|
||||||
try:
|
try:
|
||||||
@ -697,16 +983,13 @@ class TelegramTUI:
|
|||||||
text = msg.message if hasattr(msg, 'message') else ""
|
text = msg.message if hasattr(msg, 'message') else ""
|
||||||
media_data = None
|
media_data = None
|
||||||
|
|
||||||
if hasattr(msg, 'photo') and msg.photo:
|
# Обрабатываем все типы медиа
|
||||||
try:
|
media_data = await self.process_media(msg)
|
||||||
photo_data = await self.telegram_client.download_media(msg.photo, bytes)
|
if media_data:
|
||||||
if photo_data:
|
if not text:
|
||||||
media_data = self.ascii_cache.get_cached_art(photo_data)
|
text = media_data
|
||||||
if not text:
|
else:
|
||||||
text = "[Фото]"
|
text = media_data + "\n" + text
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка загрузки фото: {e}")
|
|
||||||
text = "[Ошибка загрузки фото]"
|
|
||||||
|
|
||||||
username = ""
|
username = ""
|
||||||
if hasattr(msg, 'sender') and msg.sender:
|
if hasattr(msg, 'sender') and msg.sender:
|
||||||
@ -762,12 +1045,12 @@ class TelegramTUI:
|
|||||||
widget = self.pending_messages[message.id]
|
widget = self.pending_messages[message.id]
|
||||||
# Определяем статус
|
# Определяем статус
|
||||||
if getattr(message, 'from_id', None):
|
if getattr(message, 'from_id', None):
|
||||||
widget.status = "✓✓" # Доставлено
|
widget.status = "vv" # Доставлено
|
||||||
else:
|
else:
|
||||||
widget.status = "✓" # Отправлено
|
widget.status = "v" # Отправлено
|
||||||
widget.update_widget()
|
widget.update_widget()
|
||||||
# Если сообщение доставлено, удаляем из отслеживания
|
# Если сообщение доставлено, удаляем из отслеживания
|
||||||
if widget.status == "✓✓":
|
if widget.status == "vv":
|
||||||
del self.pending_messages[message.id]
|
del self.pending_messages[message.id]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка обновления статуса: {e}")
|
print(f"Ошибка обновления статуса: {e}")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user