Files
limoka/Den4ikSuperOstryyPer4ik/Astro-modules/YmLive.py
2025-07-10 21:02:34 +03:00

291 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

__version__ = (1, 0, 0)
import json
import random
import string
import logging
import asyncio
import aiohttp
from .. import loader, utils
from yandex_music import ClientAsync
from telethon.tl.types import ChatAdminRights
from telethon.tl.functions.channels import EditAdminRequest
# https://github.com/FozerG/YandexMusicRPC/blob/main/main.py#L133
async def get_current_track(client, token):
device_info = {"app_name": "Chrome","type": 1,}
ws_proto = {
"Ynison-Device-Id": "".join([random.choice(string.ascii_lowercase) for _ in range(16)]),
"Ynison-Device-Info": json.dumps(device_info),
}
timeout = aiohttp.ClientTimeout(total=15, connect=10)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.ws_connect(
url="wss://ynison.music.yandex.ru/redirector.YnisonRedirectService/GetRedirectToYnison",
headers={
"Sec-WebSocket-Protocol": f"Bearer, v2, {json.dumps(ws_proto)}",
"Origin": "http://music.yandex.ru",
"Authorization": f"OAuth {token}",
},
timeout=10,
) as ws:
recv = await ws.receive()
data = json.loads(recv.data)
if "redirect_ticket" not in data or "host" not in data:
print(f"Invalid response structure: {data}")
return {"success": False}
new_ws_proto = ws_proto.copy()
new_ws_proto["Ynison-Redirect-Ticket"] = data["redirect_ticket"]
to_send = {
"update_full_state": {
"player_state": {
"player_queue": {
"current_playable_index": -1,
"entity_id": "",
"entity_type": "VARIOUS",
"playable_list": [],
"options": {"repeat_mode": "NONE"},
"entity_context": "BASED_ON_ENTITY_BY_DEFAULT",
"version": {
"device_id": ws_proto["Ynison-Device-Id"],
"version": 9021243204784341000,
"timestamp_ms": 0,
},
"from_optional": "",
},
"status": {
"duration_ms": 0,
"paused": True,
"playback_speed": 1,
"progress_ms": 0,
"version": {
"device_id": ws_proto["Ynison-Device-Id"],
"version": 8321822175199937000,
"timestamp_ms": 0,
},
},
},
"device": {
"capabilities": {
"can_be_player": True,
"can_be_remote_controller": False,
"volume_granularity": 16,
},
"info": {
"device_id": ws_proto["Ynison-Device-Id"],
"type": "WEB",
"title": "Chrome Browser",
"app_name": "Chrome",
},
"volume_info": {"volume": 0},
"is_shadow": True,
},
"is_currently_active": False,
},
"rid": "ac281c26-a047-4419-ad00-e4fbfda1cba3",
"player_action_timestamp_ms": 0,
"activity_interception_type": "DO_NOT_INTERCEPT_BY_DEFAULT",
}
async with session.ws_connect(
url=f"wss://{data['host']}/ynison_state.YnisonStateService/PutYnisonState",
headers={
"Sec-WebSocket-Protocol": f"Bearer, v2, {json.dumps(new_ws_proto)}",
"Origin": "http://music.yandex.ru",
"Authorization": f"OAuth {token}",
},
timeout=10,
method="GET",
) as ws:
await ws.send_str(json.dumps(to_send))
recv = await asyncio.wait_for(ws.receive(), timeout=10)
ynison = json.loads(recv.data)
track_index = ynison["player_state"]["player_queue"]["current_playable_index"]
if track_index == -1:
print("No track is currently playing.")
return {"success": False}
track = ynison["player_state"]["player_queue"]["playable_list"][track_index]
await session.close()
info = await client.tracks_download_info(track["playable_id"], True)
track = await client.tracks(track["playable_id"])
res = {
"paused": ynison["player_state"]["status"]["paused"],
"duration_ms": ynison["player_state"]["status"]["duration_ms"],
"progress_ms": ynison["player_state"]["status"]["progress_ms"],
"entity_id": ynison["player_state"]["player_queue"]["entity_id"],
"repeat_mode": ynison["player_state"]["player_queue"]["options"]["repeat_mode"],
"entity_type": ynison["player_state"]["player_queue"]["entity_type"],
"track": track,
"info": info,
"success": True,
}
return res
except Exception as e:
print(f"Failed to get current track: {str(e)}")
return {"success": False}
class YmLive(loader.Module):
'''Модуль для демонстрации играющей песни в Яндекс.Музыке'''
strings = {
"name": "YandexMusicLive",
"_text_token": "Токен аккаунта Яндекс Музыки",
"_text_id": "ID канала, который будет использоваться для показа треков...",
"on/off": "YandexMusicLive теперь {}",
'channel_id_error': "В конфиге не указан ID канала. Исправь это!",
"_from_bot_channel_error": (
"Не найден ID канала в конфиге. Пожалуйста исправь это для "
"дальнейшего использования модуля..."
),
'token_from_YmNow': (
"У вас установлен модуль YmNow и в его конфиге я нашел токен. "
"Для вашего удобства токен автоматически выставлен в конфиг. "
"Приятного использования :)"
),
"tutor": (
"🎉 Добро пожаловать в модуль YandexMusicLive!\n"
"Вы успешно загрузили модуль, который позволяет отображать играющую музыку "
"из Яндекс.Музыки прямо в названии вашего канала!\n\n"
"🌟 Чтобы модуль начал работать, выполните следующие шаги:\n"
"1) <b>Создайте канал:</b> Cоздайте новый канал, в котором будет "
"отображаться играющий сейчас трек, и закрепите этот канал в своем профиле.\n\n"
"2) <b>Настройка токена Яндекс.Музыки:</b> Перейдите в <code>{}config YandexMusicLive</code>"
" -> YandexMusicToken и вставьте ваш токен Яндекс.Музыки. <a href='{}'>(Туториал на получение токена)</a>\n\n"
"3) <b>Настройка ID канала:</b> Перейдите в <code>{}config YandexMusicLive</code> -> channel_id"
" и вставьте ID вашего канала. \n Если вы не знаете, как получить ID канала - Напишите в канал"
" сообщение с текстом <code>{}e m.chat.id</code> и вставьте в конфиг то, что вам выдаст ЮзерБот\n\n"
"4) <b>Переустановите модуль</b> После выполнения всех настроек переустановите модуль, чтобы завершить процесс настройки."
)
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"YandexMusicToken",
None,
lambda: self.strings["_text_token"],
validator=loader.validators.Hidden()
),
loader.ConfigValue(
"channel_id",
None,
lambda: self.strings["_text_id"],
validator=loader.validators.TelegramID()
),
)
async def client_ready(self, client, db):
"""Инициализация клиента и базы данных"""
self.client = client
self.db = db
async def on_dlmod(self):
"""Действия при загрузке модуля"""
if self.get("new_") != False:
await self.inline.bot.send_message(
self.client._tg_id,
self.strings("tutor").format(
self.get_prefix(),
"https://github.com/MarshalX/yandex-music-api/discussions/513#discussioncomment-2729781",
self.get_prefix(),
self.get_prefix()
)
)
self.set("new_", False)
if self.config["YandexMusicToken"] and self.config["YandexMusicToken"].startswith("y0_"):
await self.add_bot_to_channel(self.config["channel_id"])
async def add_bot_to_channel(self, channel_id):
"""Добавление бота в канал и выдача прав администратора"""
try:
await self._client(
EditAdminRequest(
channel=int(channel_id),
user_id=self.inline.bot_username,
admin_rights=ChatAdminRights(change_info=True),
rank="YandexMusicLiveBot"
)
)
self.set("ymlive_bot_added", True)
except Exception as e:
logging.error(f"Не удалось выдать боту права в канале: {e}")
async def get_current_track(self, token):
"""Получение информации о текущем треке из Яндекс.Музыки"""
try:
client = ClientAsync(token)
await client.init()
respond = await get_current_track(client, token)
track = respond.get("track")
if not track:
return None
return {
"title": track[0]["title"],
"artists": [artist["name"] for artist in track[0]["artists"]],
"duration_ms": int(track[0]["duration_ms"])
}
except Exception as e:
logging.error(f"Ошибка при получении трека: {e}")
return None
async def update_channel_title(self, channel_id, track_name):
"""Обновление названия канала, если оно отличается от текущего трека"""
try:
channel_info = await self.client.get_fullchannel(channel_id)
current_title = channel_info.chats[0].title.split(' - ')[0]
new_title = track_name.split(' - ')[0]
if current_title != new_title:
await self.inline.bot.set_chat_title(int(f'-100{channel_id}'), track_name)
messages = await self._client.get_messages(channel_id, limit=1)
if messages and messages[0].action:
await messages[0].delete()
except Exception as e:
logging.error(f"Ошибка при изменении названия канала: {e}")
@loader.command(ru_doc="- включить/выключить YaLive")
async def yalive(self, message):
"""Включение или выключение автоматического обновления названия канала"""
if not self.config["channel_id"]:
await utils.answer(message, self.strings["channel_id_error"])
return
if not self.get("ymlive_bot_added"):
await self.add_bot_to_channel(self.config["channel_id"])
autochannel_status = self.get("autochannel", False)
self.set("autochannel", not autochannel_status)
status_text = "enabled" if not autochannel_status else "disabled"
await utils.answer(message, self.strings["on/off"].format(status_text))
@loader.loop(interval=30, autostart=True)
async def autochannel_loop(self):
"""Цикл для автоматического обновления названия канала каждые 30 секунд"""
if not self.get("autochannel"):
return
if not self.config["channel_id"]:
await self.inline.bot.send_message(self.client._tg_id, self.strings["_from_bot_channel_error"])
self.set("autochannel", False)
return
try:
track_info = await self.get_current_track(self.config["YandexMusicToken"])
if track_info:
artists = ", ".join(track_info["artists"])
track_name = f"{track_info['title']} - {utils.escape_html(artists)}"
await self.update_channel_title(self.config["channel_id"], track_name)
except Exception as e:
logging.error(f"Ошибка в autochannel_loop: {e}")