__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) Создайте канал: Cоздайте новый канал, в котором будет "
"отображаться играющий сейчас трек, и закрепите этот канал в своем профиле.\n\n"
"2) Настройка токена Яндекс.Музыки: Перейдите в {}config YandexMusicLive"
" -> YandexMusicToken и вставьте ваш токен Яндекс.Музыки. (Туториал на получение токена)\n\n"
"3) Настройка ID канала: Перейдите в {}config YandexMusicLive -> channel_id"
" и вставьте ID вашего канала. \n Если вы не знаете, как получить ID канала - Напишите в канал"
" сообщение с текстом {}e m.chat.id и вставьте в конфиг то, что вам выдаст ЮзерБот\n\n"
"4) Переустановите модуль После выполнения всех настроек переустановите модуль, чтобы завершить процесс настройки."
)
}
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}")