Without Streaming

Telegram-канал как радиостанция

6 заметок · 9 мин

Without Streaming: Telegram-канал как радиостанция: иллюстрация 1

Оглавление

  1. Радио из Telegram в один клик1 мин
  2. Две ручки настроения: стиль и энергия2 мин
  3. Как это работает внутри: индексатор → плейлист → стримвидео1 мин
  4. Инженерная реальность: плеер, фон и PWAвидео1 мин
  5. Проект развивается медленнофото1 мин
  6. Without Streaming перестал быть плеером и стал радиостанциейвидео3 мин

Радио из Telegram в один клик

У меня есть Telegram-канал Without Streaming, куда я фиксирую свою личную музыкальную медиатеку, а так же короткие видео-вырезки: из своих проектов @arsenynn и workflow@arsenynn. Для меня это не просто подборка, а слепок вкуса и времени.

Изначально все началось прагматично: прийти в компанию, на тусовку или сесть в машину и не подключаться к колонке или проигрывателю, а просто открыть свой канал на телефоне того, кто уже подключён, и включить музыку в один шаг.

Музыка и GIF здесь работают как компактная запись состояния: вместе они сохраняют настроение момента быстрее и точнее, чем любые пояснения.

Telegram как CMS. Новый пост в канале автоматически становится частью эфира, без отдельной админки и медиатеки.

Визуалы как «телеканал». Один визуал повторяется три раза, затем переключается на следующий; стартовый набор каждый раз перемешан.

Аудио стримится «на лету» из Telegram без сохранения на диск. Это осознанный выбор: минимум инфраструктуры и минимум «следов».

Техническая проекция простая: FastAPI + Telethon → JSON-плейлист → chunked-стрим

В следующем посте расскажу, как две ручки (Jazz ⇄ Hip-hop и Drifting ⇄ Driving) управляют настроением эфира через простые теги в Telegram.

К оглавлению

Две ручки настроения: стиль и энергия

В Without Streaming я сделал управление как на пульте:
одна ручка смещает эфир Jazz ⇄ Hip-hop, вторая меняет энергию Drifting ⇄ Driving.

Основа намеренно простая и прозрачная: я помечаю треки тегами в Telegram (#J / #H / #JxH и #drift / #drive / #dd). Индексатор собирает это в playlist.json, а сервер по запросу /playlist?mix=&energy= пересобирает порядок треков. #JxH и #dd это треки, которые я сам считаю смешанными по стилю (Jazz×Hip-hop) и треки со смешанной энергией (между drift и drive).

Дальше код делает простую вещь: превращает положение ползунков в коэффициенты “сколько внимания дать каждой стороне”, и считает итоговую оценку трека score. Чем выше score, тем выше трек поднимается в выдаче /playlist.

jazz_bias   = 1.0 - hiphop_bias

drive_bias  = energy / 100.0
drift_bias  = 1.0 - drive_bias

score = jazz_bias * w_jazz + hiphop_bias * w_hiphop \
      + drift_bias * w_drift + drive_bias * w_drive
  • mix и energy превращаются в коэффициенты *_bias: это “сколько влияния” дать каждой стороне шкалы.
  • w_jazz / w_hiphop / w_drift / w_drive это веса трека, которые получаются из тегов при индексации.
  • score это итоговая оценка: чем она выше, тем раньше трек окажется в эфире при текущих настройках.

Дальше включается важная инженерная часть: чтобы эфир не зацикливался на узкой категории, сервер сначала отдаёт “профильные” треки (и мосты), а затем подмешивает остальные.

Почему пока не идеально: теги субъективны, две оси создают много комбинаций, и “музыкально правильно” не всегда совпадает с “самой высокой оценкой”.

Авто-режимы (Auto Flow / Auto Energy) двигают ручки плавно и не ломают текущий трек, а эффект проявляется на следующем.
Кстати, авто-режим можно включить прямо с ползунка: нажмите на “точку” (ползунок) и он начнёт двигаться автоматически.

В следующем посте разберу внутренний конвейер Without Streaming: как индексатор собирает playlist.json, как /playlist формирует эфир под ручки и как /audio стримит медиа из Telegram chunked без сохранения на диск.

К оглавлению

Как это работает внутри: индексатор → плейлист → стрим

Проект устроен как конвейер: Telegram-канал → каталог → эфир.

1) Индексатор пробегает канал, находит аудио и GIF/MP4, читает теги и формирует playlist.json.

2) /playlist?mix=&energy= отдаёт JSON с треками и визуалами уже в нужном порядке под текущие ручки.

3) /audio/{id} и /gif/{id} запрашивают медиа в Telegram и отдают его в браузер.

Ключевой принцип: аудио не сохраняется на диск. Сервер отдаёт данные потоково (chunked) по мере получения.

Визуалы кэшируются на диск, потому что они короткие и повторяются, а кэш заметно ускоряет переключения.

Самая важная часть (то, что превращает канал в эфир):

@app.get("/audio/{message_id}")
async def audio(message_id: int):
    async def gen():
        async for chunk in telethon_stream_bytes(message_id):
            yield chunk  

    return StreamingResponse(
        gen(),
        media_type="audio/mpeg",
        headers={"Cache-Control": "no-store", "Accept-Ranges": "none"},
    )

— @app.get(“/audio/{message_id}”)
Маршрут: GET /audio/12345 попадает сюда, {message_id} это ID сообщения с треком в Telegram.

— async def audio(message_id: int):
Асинхронный обработчик, чтобы сервер не блокировался во время загрузки/отдачи. int фиксирует тип параметра.

— async def gen(): … yield chunk
Асинхронный генератор: читает трек из Telegram порциями (async for) и сразу отдаёт каждый кусок (yield).

— StreamingResponse(gen(), …)
Оборачивает поток в корректный HTTP-стрим: воспроизведение может начаться до полной загрузки файла.

— media_type=“audio/mpeg”
Явно задаёт Content-Type, чтобы браузер правильно воспроизводил аудио.

— headers
no-store — не кэшировать аудио. Accept-Ranges: none — без Range-запросов (перемотка/дозагрузка “с середины” не предусмотрены).

К оглавлению

Инженерная реальность: плеер, фон и PWA

Сделать “плейлист в браузере” легко. Сделать радио-ощущение сложнее: нужно бесшовное звучание, устойчивый UI и поведение, которое не ломается при смене условий (сеть, устройство, фон).

1) Плеер как движок

Эффект “радио” держится на двух вещах: кроссфейд и состояния. Плеер работает как state machine (idle → buffering → playing), чтобы не было “двойного звука” и гонок при переключениях. Параллельно идёт префетч следующего трека, чтобы реакции на Next/Prev ощущались быстрыми.

2) Зачем нужна кнопка «Слушать в фоне»

На iOS/WebView “богатый” режим (WebAudio, видео-лупы, таймеры, префетч) часто агрессивно режется при сворачивании. Поэтому кнопка «Слушать в фоне» переводит радио в облегчённый профиль: отключает тяжёлые части, оставляет стабильное воспроизведение через простой <audio> и даёт быстрый возврат в основной режим без потери контекста.

3) PWA

PWA здесь не про “нативность”. Оно даёт более предсказуемый запуск интерфейса и повышает шанс, что сессия проживёт дольше. Но я не обещаю невозможного: фон в вебе нельзя гарантировать, поэтому и нужен отдельный “фон-режим”.

4) Gesture gate: Telegram Mini App / Safari WebView

Есть ещё один “взрослый” слой надёжности: когда платформа запрещает autoplay (особенно в WebView), проект не выглядит сломанным. Визуалы переводятся в режим ожидания до первого пользовательского жеста, а после тапа аккуратно стартуют заново. Это редкая, но важная деталь: большинство веб-плееров в такой ситуации просто молча “не играет” и выглядит как баг, а здесь это осмысленная деградация под реальные ограничения.

Telegram-контент превращается в медиа-объект, а не в ленту ссылок, и при этом система честно учитывает ограничения платформ.

Запуск из браузера 🎵

К оглавлению

Проект развивается медленно

Проект развивается медленно: иллюстрация 1

Проект развивается медленно, в темпе моего текущего интереса к музыке. Но библиотека, собранная за жизнь, настолько обширна и сильна, что Without Streaming для меня это долгий формат: он будет пополняться и постепенно модернизироваться.

Live mix 👈

К оглавлению

Without Streaming перестал быть плеером и стал радиостанцией

Когда я писал первые посты про Without Streaming, слово «радио» было скорее формой описания.

Тогда каждый человек фактически запускал свой собственный плеер. Браузер получал плейлист из Telegram, сам выбирал последовательность треков, управлял переходами, а две ручки Jazz ⇄ Hip-hop и Drifting ⇄ Driving позволяли каждому собирать собственную версию потока.

Это работало, но у настоящего радио есть одно фундаментальное свойство: в каждый момент времени существует один эфир для всех.

Именно это я в итоге полностью перестроил.

Теперь человек, который открывает Without Streaming, не получает собственную очередь и не начинает трек сначала. Он подключается к тому месту, которое прямо сейчас звучит в эфире.

Внутри архитектура теперь выглядит примерно так:

Telegram
локальная медиатека
Python
Liquidsoap
AAC-LC
HLS
Caddy
один общий live-эфир

Если совсем простыми словами:

Liquidsoap — это радиодвижок. Он непрерывно воспроизводит выбранные треки, делает переходы между ними и формирует тот самый сигнал, который в этот момент действительно находится в эфире.

AAC-LC — аудиокодек: он сжимает этот сигнал в формат, который можно достаточно экономно передавать слушателям через интернет.

HLS — способ доставки live-потока. Вместо одного бесконечного соединения эфир постоянно нарезается на короткие сегменты, а браузер последовательно забирает их и поэтому может подключиться к текущему моменту трансляции.

Caddy — веб-сервер на выходе этой цепочки. Он раздаёт слушателям HLS, сайт и остальные публичные файлы.

Самое важное изменение здесь связано даже не с Liquidsoap или HLS, а с разделением ответственности.

Python решает, что должно прозвучать дальше.

Liquidsoap является фактической точкой эфира.

Браузер больше ничего не программирует, он только слушает.

Раньше трек во время Play запрашивался непосредственно из Telegram и потоково отдавался браузеру. Сейчас вся пригодная медиатека зеркалируется локально и может продолжать играть независимо от доступности Telegram.

То есть Telegram постепенно превратился из:

CMS + файлового сервера + runtime-зависимости

в:

CMS + источник публикации и происхождения материала.

Изменился и сам клиент.

Вместо браузерного аудиодвижка сейчас используется один постоянный <audio>, подключённый к HLS.

Play теперь после паузы возвращает слушателя не к старой позиции, а обратно в текущий эфир.

Визуалы существуют отдельно от музыкальной программы. Они локально подготовлены, раздаются как статические MP4 и вращаются независимо у каждого слушателя.

Старые Jazz / Hip-hop и Drifting / Driving ручки поэтому исчезли из интерфейса не случайно. Их идея не отброшена, но управление музыкальной логикой постепенно переезжает туда, где ему теперь правильнее находиться: в серверное программирование общего эфира, а не в персональный плеер каждого слушателя.

Параллельно у радио появился отдельный operational layer. Теперь я вижу, сколько людей находится на сайте, сколько реально слушает HLS, дневное количество listening sessions и peak.

Сейчас следующий этап для меня уже не про новые кнопки. Я довожу систему до состояния, где она самостоятельно восстанавливается после падений процессов, потери сети и reboot, затем измерю реальную пропускную способность домашнего сервера и только после этого продолжу строить Programming Engine.

И дальше начинается, пожалуй, самая интересная часть развития Without Streaming. Музыкальная программа постепенно станет намного сложнее, а в самом эфире появятся вещи, которых раньше в проекте вообще не было. Но про это уже отдельно.

Получился немного парадоксальный результат: чем сложнее Without Streaming становится внутри, тем меньше действий остаётся у слушателя снаружи :)

😎 Запустить Without Streaming 👈👈

😎 Without Streaming Live mix

К оглавлению

Следующая серияMac Automation Stack