fix(cache): канонизация ключа канала (Durov/durov/@name -> один ключ) + one-shot миграция

Один канал жил под ключами Durov/durov/@name в трёх слоях (tgcache-файлы,
data/cache/ каталоги, SQLite-строки) -> дубли кеша, двойные походы в Telegram,
осиротевшие деревья. ID<->username НЕ унифицируем (сознательно).

- channel_key.py: canonical_channel_key (strip @, -100... числовые verbatim,
  иначе lowercase; безопасно и для API-вызовов).
- tg_cache._cache_file_path прогоняет ключ через canonical.
- post_parser.get_channel_username -> lowercase (обе ветки; канонизирует
  SQLite/media-URL/t.me). Только эти 2 строки.
- api_server.get_media: fs_channel = canonical_channel_key(channel) ПОСЛЕ проверки
  дайджеста (дайджест — по ИСХОДНОМУ url, иначе ломаются выданные ссылки); fs_channel
  во всех путях/ключах/download ниже.
- migrate_channel_keys_sync в lifespan (после init_db_sync, до client.start, через
  asyncio.to_thread): канал целиком FS-потом-SQL. Обязательный os.path.samefile
  guard ДО merge/rmtree — на case-insensitive FS Durov/durov один inode, иначе
  снесли бы кеш. FS-сбой -> SQL пропуск (строки старорегистровые, дерево видно
  свиперу, нет вечных orphan). SQL-merge PK-safe (twin -> merge added=max/mime
  non-NULL + DELETE, иначе plain UPDATE). Идемпотентно.

closes #24

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 03:40:51 +03:00
parent d5a38c3f13
commit 11bfd2b3ee
6 changed files with 498 additions and 17 deletions
+2 -2
View File
@@ -1416,8 +1416,8 @@ class PostParser:
if hasattr(chat, 'usernames') and chat.usernames: # Check many usernames
active_usernames = [u.username for u in chat.usernames if u.active]
if active_usernames:
return active_usernames[0]
if hasattr(chat, 'username') and chat.username: return chat.username # Check single username
return active_usernames[0].lower() # Canonicalize: usernames are case-insensitive
if hasattr(chat, 'username') and chat.username: return chat.username.lower() # Check single username (canonicalized)
# Return numeric ID only if no username found
if isinstance(chat.id, int) and str(chat.id).startswith('-100'): return str(chat.id)