Python with Yandex music API. Or index of your death

Preface

Hmm… Some people may remember me from my publications on the StopGame website.

I may not be the smartest programmer, but I can whip something up, so don't take my work as a standard, I just described what I know from experience. If you want to correct me or point out mistakes, I'll be glad to learn from you!

Well, now the fish!

Start | Schizophrenia is a bad companion

Well, I decided to make a discord bot to play music, and I wrote a Typical bot in python in a couple of hours, and also improved the internet radio game function by adding a similar list there. And then I remembered: I was able to get my Yandex Music token not long ago! Why not make a Yandex music playback function then?

We write | There is no such thing as too much tea

It all starts with the designation of the client: the main class of our “player”

from yandex_music import ClientAsync
import json
import asyncio

SETTINGS = json.load(open("config.json", "r"))
YCLIENT = asyncio.run(ClientAsync(SETTINGS["YToken"]).init())

The first three lines, I hope there is no need to explain, these are just imports of everything needed
I use the 5th one for downloading token from a separate file (Yes, I know that there is env, but I don't give a damn).
The 6th line is the most interesting, this is actually our client, since it is asynchronous, it must be launched exclusively through asyncio.run (Otherwise, it will give errors during any operations)

Next we create the necessary… “environment” for our bot

import discord
from discord import FFmpegOpusAudio
from discord.ext import commands
from yandex_music import ClientAsync
import json
import asyncio

SETTINGS = json.load(open("config.json", "r"))
YCLIENT = asyncio.run(ClientAsync(SETTINGS["YToken"]).init())

intents = discord.Intents.all()
bot = commands.Bot(command_prefix="^", intents = intents)

@bot.command()
async def play(ctx, *desc):
  ctx.send("Hello World!")
  
bot.run(SETTINGS["APIKey"])

Not that much…
Here we added the bot itself and the currently test “play” command.

Next we write the command “play”

global YCLIENT

desc = list(desc)

url = None
if len(desc) == 0:
  await ctx.message.add_reaction("❌")
  return
else:
  url = desc[0]

What is desc? Some will ask, and I will answer: It is a discord benefactor
Discord has ahu… cool code for getting arguments, so that to get all the arguments of a command it is enough to write *descand then transform it into list.
Actually, here we just check if there is at least one argument and if there is, we accept it as if it were a url

YandexMusicAPI | The beginning of SHIZY

Now we are in good health and start writing Yandex

Let's start with the 1st one, I have SO FAR identified 4 types of URLs in Yandex music (Actually 5, but we will ignore radio).
This:

  1. track – Single song

  2. album – A collection of songs from one album

  3. artist – A set of albums by one artist

  4. playlists – A collection of songs from various artists and albums

And what's the problem? You ask. And I'll answer: THEY, DUDE, ARE ABSOLUTELY DIFFERENT!

Let's start by sections:

track

if "track" in command:
  idm = command.split("/")[-1]
  track = (await YCLIENT.tracks(idm))[0]

The easiest method to determine what type of link it is is to check if there is any “piece of text” in order, from track to playlist (As written above)

idm is the track id. You can get the ID of any of the “objects” by the last element of the list, which we get if we split the link by the “/” sign.

And then the horror, you can’t get a track by ID, you will definitely get list tracks.
So, take the very first element and don’t suffer.

album

if "album" in command:
  album = (await YCLIENT.albums_with_tracks(int(command.split("/")[-1])))
  volumes = album.volumes[0]
  track = volumes[0]

Here we get an album with all the “whole” tracks, and then we select all the tracks and throw them into a separate array. You may ask: why do we take only the first index? It's very simple!
When we get an array of tracks we get it differently:

[
  track,
  track,
  track,
  track,
  track,
  ...
]

And like this:

[
  [
    track,
    track,
    track,
    track,
    track,
    ...
  ]
]

Why? Ask the creator!

artist

The fun begins!

elif "artist" in command:
  ida = command.split("/")[-1]
  artist = (await YCLIENT.artists(ida))
  artist = artist[0]
  albums = (await artist.get_albums_async())
  albumList = [await YCLIENT.albums_with_tracks(album.id) for album in albums]
  volumes = []
  for x in albumList:
      volumes = volumes + x.volumes[0]
  track = volumes[0]

First we get the performer's ID, after which we get LIST artists, and take the first element from it. Then we get all the albums of this artist, after which we create an array with album objects with tracks.

On lines 7-9 we get all the tracks from all the albums and put them into a separate array to get “single array” all tracks

playlists

elif "playlists" in command:
  idu = command.split("/")[-3]
  album = (await YCLIENT.users_playlists(kind=command.split("/")[-1], user_id=idu))
  album = album.tracks
  alubmid = [x.id for x in album]
  album = (await YCLIENT.tracks(alubmid))
  track = album[0]

It is worth mentioning here that playlists, for Yandex.Music, are a type of data that store not Tracks but ShortTracks (See the explanation of whole tracks), so that at the beginning we get a playlist object, and then we get tracks, from the tracks we get the id, and from the id, we get the tracks again.

And yes, in .tracks as in other commands you can insert, and after and get lists

Loading and playing

if ctx.author.voice is not None:
  player = await (ctx.author.voice).channel.connect()
  trackSource = await track.download_bytes_async()
  audio_data = AudioSegment.from_file(BytesIO(trackSource), format="mp3")
  normalized_audio = audio_data.normalize()
  player.play(FFmpegOpusAudio(normalized_audio.export(format="wav"), pipe=True))
else:
  await ctx.send("Пользователь не в голосовом канале. Попробуйте перезайти в него, если это не так")

And here we will play our songs 🙂

But first we check if the user is in the voice channel and if so, we create a player object specifying the voice channel. We download the song in bytecode and convert it using AudioSegment to perform normalization, and then give it to Discord not as an .mp3, but as a .wav file.

We use BytesIO so that we don’t have to save files on the hard drive.

Well, now let's add some “charm~”

embed = discord.Embed(title=f"**{track.title}**", description=f"{track.albums[0]["title"]} • {track.albums[0]["year"]}", color=discord.Color.from_rgb(random.randint(125, 255), random.randint(125, 255), random.randint(125, 255)))
embed.set_footer(text=track.artists[0]["name"], icon_url=track.albums[0].artists[0].cover.get_url())
embed.set_thumbnail(url=track.get_cover_url())
ctx.send(embed=embed)

This is where we show how to get “additional” data for a track. If you want to get a FULL list, you can simply write print(track) . Fortunately, a good majority of classes from the library are converted to JSON.

However! Please note that in most cases, to get a URL, such as album covers, you need to execute a function like .get_url()

Well, now…

Maybe we can add a little more Numpy?

url = track.get_cover_url()
thumbnail_image = np.array(Image.open(BytesIO(requests.get(url).content)))
rl, gl, bl = (thumbnail_image[:,:,0])[:,0].tolist(), (thumbnail_image[:,:,1])[:,0].tolist(), (thumbnail_image[:,:,2])[:,0].tolist()
r, g, b = int(sum(rl) / len(rl)), int(sum(gl) / len(gl)), int(sum(bl) / len(bl))

Looks… Hellish….

Well, according to the classics, in order:

  1. track.get_cover_url() – get the album cover url

  2. ...requests.get(url).content))) – we get an image

  3. ...BytesIO(requ... – save the image in RAM memory

  4. ...Image.open(Byte... – open it as a Pillow object

  5. np.array(Imag... – translate Pillow into numpy.array

  6. thumbnail_image[:,:,0] – we get all 0 indices in each of the sub-arrays, sub-arrays (Numpy has data in the form of a matrix where each element is a matrix with RGB colors, so that we get all the red values ​​divided into “rows”). We do the same with the 1st and 2nd indices

  7. (thumbnail_image[:,:,0])[:,0] – we get all 0 indices of subarrays (As I said, the colors are also separated by rows, but since we don't need that, we get them from there)

  8. .tolist() – we use it to convert numpy.array into a classic Python list

  9. And in the 4th line we calculate the arithmetic mean for all colors

    Hoba! We have a “medium color” from the album cover that we can safely insert as an embed color

color=discord.Color.from_rgb(r, g, b))

The End | FINALLY

And this is what we got as a result:

import discord
from discord import FFmpegOpusAudio
from discord.ext import commands
from yandex_music import ClientAsync
import json
import asyncio
from io import BytesIO
from pydub import AudioSegment
from PIL import Image
import numpy as np
import requests

SETTINGS = json.load(open("config.json", "r"))
YCLIENT = asyncio.run(ClientAsync(SETTINGS["YToken"]).init())

intents = discord.Intents.all()
bot = commands.Bot(command_prefix="^", intents = intents)

@bot.command()
async def play(ctx, *desc):
  global YCLIENT

  desc = list(desc)
  
  command = None # Да, простите, я на пол пути url в command переименовал :3
  if len(desc) == 0:
    await ctx.message.add_reaction("❌")
    return
  else:
    command = desc[0]

  if "track" in command:
    idm = command.split("/")[-1]
    track = (await YCLIENT.tracks(idm))[0]
  if "album" in command:
    album = (await YCLIENT.albums_with_tracks(int(command.split("/")[-1])))
    volumes = album.volumes[0]
    track = volumes[0]
  elif "artist" in command:
    ida = command.split("/")[-1]
    artist = (await YCLIENT.artists(ida))
    artist = artist[0]
    albums = (await artist.get_albums_async())
    albumList = [await YCLIENT.albums_with_tracks(album.id) for album in albums]
    volumes = []
    for x in albumList:
        volumes = volumes + x.volumes[0]
    track = volumes[0]
  elif "playlists" in command:
    idu = command.split("/")[-3]
    album = (await YCLIENT.users_playlists(kind=command.split("/")[-1], user_id=idu))
    album = album.tracks
    alubmid = [x.id for x in album]
    album = (await YCLIENT.tracks(alubmid))
    track = album[0]
  if ctx.author.voice is not None:
    player = await (ctx.author.voice).channel.connect()
    trackSource = await track.download_bytes_async()
    audio_data = AudioSegment.from_file(BytesIO(trackSource), format="mp3")
    normalized_audio = audio_data.normalize()
    player.play(FFmpegOpusAudio(normalized_audio.export(format="wav"), pipe=True))
    
    url = track.get_cover_url()
    thumbnail_image = np.array(Image.open(BytesIO(requests.get(url).content)))
    rl, gl, bl = (thumbnail_image[:,:,0])[:,0].tolist(), (thumbnail_image[:,:,1])[:,0].tolist(), (thumbnail_image[:,:,2])[:,0].tolist()
    r, g, b = int(sum(rl) / len(rl)), int(sum(gl) / len(gl)), int(sum(bl) / len(bl))  
    
    embed = discord.Embed(title=f"**{track.title}**", description=f"{track.albums[0]["title"]} • {track.albums[0]["year"]}", color=discord.Color.from_rgb(r, g, b)))
    embed.set_footer(text=track.artists[0]["name"], icon_url=track.albums[0].artists[0].cover.get_url())
    embed.set_thumbnail(url=url)
    ctx.send(embed=embed)
  else:
    await ctx.send("Пользователь не в голосовом канале. Попробуйте перезайти в него, если это не так")
  
bot.run(SETTINGS["APIKey"])

And this is the result we got…

There are many things that can be corrected here, starting with determining the average color (This can also be done if you use Pillow to reduce the image to 1×1 pixel, look at this one StackOverFlow) and finishing adding playlists…

But we'll leave this either for others or for later~

Because I wrote this entire article using already existing code, which had support for playlists and pretty buttons, and support for more than one server, but there was so much code and hacks that I don't have the strength or time to sort it out, so… here…

I think we can say goodbye here! Bye!

  Take your coat and let's go home! Take your coat and let's go home!

Take your coat and let's go home.
Take your coat and let's go home!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *