A story about a beginner making a VTuber notification bot from scratch in Python

Introduction

Self-introduction

Nice to meet you. My name is Yourein. Since it is a self-introduction, I will post various things below.

Birthplace: Hokkaido (still resident) Who are you ?: I'm an active industrial high school student What is your programming experience?: I learned the basics of C language in class. I used to teach myself C ++, but recently I'm using Python.

I have some experience with programs, but until now I have been doing very small-scale development for myself that I would never post to sites such as Qiita. The main reason I decided to write this article this time is "I wrote a level of code that I have never experienced before." "I stumbled in various places like a beginner, so I wanted to put together a memorandum for myself."

I decided to pull some code from the program I wrote and post it, but since I am a beginner, I would be grateful if you could overlook it even if you wrote it unsightly.

What I wanted to do

Although it is labeled as VTuber notification bot, it is actually more correct this time to use holobox notification bot. The goal is to make a bot that behaves like Hololive Notice Server ** by yourself **. Screenshot (479).png This is not a live broadcast notification, but you can think of it as an image. The functional requirements are as follows:

--Notify the latest video or the latest live broadcast of the specified river. --Get some of the latest videos of the specified river. --List and notify the live broadcasts currently being delivered by the box rivers. --Several other features

What I couldn't do

Actually, I used to create a Python application that uses the Spotify API to get album information and song information, but I couldn't understand the mechanism of Auth at all. I was thinking of using the Youtube Data API this time as well, but it seemed necessary to create several projects due to my weakness in Auth and the number of rivers, so this time I will collect information etc. without using the Youtube Data API. I became a bot.

Services used, development environment, etc.

Development/execution environment

OS : Windows10 Pro(1909 English(System Locale:Japan)) Editor: ** Visual Studio Code ** Execution environment: ** python ** (The created .py file is opened and used as it is.)/** Discord **

Service used

The functions of HoloTools are now available as APIs. See the HoloFans API link for more information

-** Youtube (RSS feed) **

https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID

 You can get the xml of the video page of the Youtube channel by doing>. Scraping is prohibited on Youtube, but getting RSS feeds is cool, so I'm eager to make tea muddy.

- **Discord.py**

 > Needless to say, this is a well-known library for Discord. I think 50% of the code I wrote this time depends on this.

# Preparation stage
 There was the word ** "specified river" ** in the functional requirements mentioned earlier.
 The bot created this time does not notify everyone for the time being, but to a bot that has a function such as ** If you specify "I want notification of this river!", It will start notification of that river ** Become.
 Prepare a database in JSON with specifiers that specify the river and other information.

 It's a good idea to fork the database from Git Hub [because it's in the HoloFans API](https://github.com/holofans/holoapi/blob/develop/database/seeders/json/hololive.json) with more information (packed), but I haven't written JSON myself. I decided to write it after studying.


#### **`members.json`**
```json

{
    "counts": 56,
    "channels":[
        {
            "id": 1,
            "yt_id": "UCp6993wxpyDPHUpavwDFqgg",
            "icon": "https://yt3.ggpht.com/ytc/AAUvwngZmr_qbKhGIvHaHwLRmKhKxdeFfM7ZbK316vFNSw=s800-c-k-c0x00ffffff-no-rj",
            "tw_id": "tokino_sora",
            "ch_name": "SoraCh.Tokino Sora Channel",
            "name": "Tokino Sora",
            "specifier": "Sora",
            "part": "hololive",
            "color": "0x4D84E8"
        },
        {
            "id": 2,
            "yt_id": "UCDqI2jOz0weumE8s7paEk6g",
            "icon": "https://yt3.ggpht.com/ytc/AAUvwnjiwtRNYHIo1zl37gOIiEeOh-2s7HUdhv6WB8M1vQ=s800-c-k-c0x00ffffff-no-rj",
            "tw_id": "robocosan",
            "ch_name": "Roboco Ch. -Roboco",
            "name": "Roboco-san",
            "specifier": "Roboco",
            "part": "hololive",
            "color": "0xEA00EB"
        },
        {
            "id": 3,
            "yt_id": "UC5CwaMl1eIgY8h02uZw7u8A",
            "icon": "https://yt3.ggpht.com/ytc/AAUvwnjdAl5rn3IjWzl55_0-skvKced7znPZRuPC5xLB=s800-c-k-c0x00ffffff-no-rj",
            "tw_id": "suisei_hosimati",
            "ch_name": "Suisei Channel",
            "name": "Suisei Hoshimachi",
            "specifier": "Suisei",
            "part": "hololive",
            "color": "0x4D84E8"
        },
...
        {
            "id": 54,
            "yt_id": "UCWsfcksUUpoEvhia0_ut0bA",
            "icon": "https://yt3.ggpht.com/ytc/AAUvwnjwFEptYg7ed7Ze1nWT7Bj4bbXiOoNwzeM9-4g=s800-c-k-c0x00ffffff-no-rj",
            "tw_id": "holostarstv",
            "ch_name": "Holosters Official",
            "name": "Holosters",
            "specifier": "Holostars",
            "part":"Holostars 1st-gen",
            "color":"0x7979EF"
        },
        {
            "id": 55,
            "yt_id": "UCfrWoRGlawPQDQxxeIDRP0Q",
            "icon": "https://yt3.ggpht.com/ytc/AAUvwnh523hdzQe8vPD2Du77mqxianT1HHR1McSLHXK4=s800-c-k-c0x00ffffff-no-rj",
            "tw_id": "hololive_Id",
            "ch_name": "hololive Indonesia",
            "name": "hololive Indonesia",
            "specifier": "Hololiveid",
            "part":"hololiveID 1st-gen",
            "color":"0xFF9800"
        },
        {
            "id": 56,
            "yt_id": "UCotXwY6s8pWmuWd_snKYjhg",
            "icon": "https://yt3.ggpht.com/ytc/AAUvwnieM4gqtwmRtapt0va5VTi7BiKHhsYMxOu9qYRR=s800-c-k-c0x00ffffff-no-rj",
            "tw_id": "hololive_En",
            "ch_name": "hololive English",
            "name": "hololive English",
            "specifier": "Hololiveen",
            "part":"HololiveEN 1st-gen",
            "color":"0xFF0000"
        }
    ]
}

Since JSON is defined in this way, if you rewrite the "counts" that was in the beginning and then add data to JSON, this database can be modified according to future member subscriptions. (** However, since there is code that depends on the database on the HoloFans API side this time, there are functions that can not be used in this program when the service of the API ends. ** The HoloFans API can be deployed even in the local environment for the time being. If you are deploying locally, you can rewrite the API database by yourself, so you can continue to use it even after the service ends. ** This time it is troublesome, but ... ** )

Preparation of variables

variables


clientid = "CLIENTID"
client = commands.Bot(command_prefix = '.')
thumbnail_endpoint_1 = "https://i.ytimg.com/vi/"
thumbnail_endpoint_2 = "/maxresdefault.jpg "
stream_endpoint = "https://www.youtube.com/watch?v=" #Youtube video page
dischannel = CHANNELID(int) #Discord's channel id
members = json.load(open("members.json", mode = 'r', encoding = "utf-8")) #load hololive members dic
xml_endpoint = "https://www.youtube.com/feeds/videos.xml?channel_id=" #Youtube video feed xml

I put variables that I used many times while writing a program and command_prefix that is necessary for execution in the first place.

Addition of functions

1. List and notify broadcasts delivered by box rivers

Screenshot (493).png In the end, it looks like this. You can get this list with **. Listlive **.

listlive


@client.command()
async def listlive(ctx): #make list of nowlive and return it.
    """Make a list of online-stream hosted by hololive & stars members"""
    templist = Holo.apilistlive() #requesting live list to Holoapi
    tempnum = len(templist) #getting range of the list
    embed_tosend = discord.Embed(title = f"{tempnum}streams on live!")

    #sending process
    for i in range(tempnum):
        embed_tosend.add_field(name = f"{templist[i][1]}'s stream", value = f"[{templist[i][0]}]({stream_endpoint}{templist[i][2]})")
    await ctx.send(embed = embed_tosend)

apilistlive


class holoapi(object):
    holoapi_endpoint = "https://api.holotools.app/v1/"

    def apilivenum(self): #get the number of nowlive
        ...

    def apilistlive(self): #make a list of nowlive
        lookup_url = f"{self.holoapi_endpoint}live"
        result = requests.get(lookup_url)
        live_dic = result.json()["live"]
        
        livelist = []
        for i in range(len(live_dic)):
            #get each stream information except desc. and some stats.
            temp = [live_dic[i]["title"], live_dic[i]["channel"]["name"], live_dic[i]["yt_video_key"]]
            livelist.append(temp)

        return livelist
https://api.holotools.app/v1/live

You can get the frame information currently being distributed among the rivers registered in the HoloFans API with. If you hit the API, it will come out as JSON, so convert it to a dictionary type, process it, make it a two-dimensional list, and then return the list to the function executed when the command **. Listlive ** is called with return ..

In the for statement, add a field to Embed for the frame currently being delivered, and describe ** "who is delivering", "what is the delivery title", "delivery URL" **, and describe everything. I'm sending it when I'm done. The reason for inline display is ** I didn't know how to disable inline display when I wrote this code **, but when I tried inline disabling after that ** 5 or more was delivered Because the readability at that time was clearly low **. This is probably less readable from a smartphone, but it's okay because I'm only planning to see it on a PC in the first place. (Because it may be open to someone ...)

2. Get some of the latest videos of the specified river

Next is the function for viewing the archive exclusively. This time, we will get the latest videos of the specified rivers and 5 live distributions. ** This is a type of function that does not depend on API unlike the previous one, so if you update * members.json * (database prepared this time), you can get it by other than the rivers in the holo box. ** ** You don't have to be a river. Screenshot (495).png

This is like this. It would have been nice to send the videos one by one to the text channel with thumbnails, but since it's usually hard to see and disturbing, I obediently put them together in one Embed with inline invalidation.

The command is

.recent specifier(specifier)

is.

recent


@client.command()
async def recent(ctx, spec = "Default"):
    """Make the list of their recent 5 vids or streams and back"""
    profile_base = getprofile_withspec(spec)
    if profile_base == 0:
        embed_tosend = discord.Embed(title="Please input specifier!", descprition = "Check specifier with [.spechelp] command!")
        embed_tosend.set_image(url = "https://i.ytimg.com/vi/7tQgpXgv570/maxresdefault.jpg ")
        await ctx.send(embed = embed_tosend)
    elif profile_base == 1:
        embed_tosend = discord.Embed(title = "Wrong specifier!", description = "Check specifier with [.spechelp] command!")
        embed_tosend.set_image(url = "https://i.ytimg.com/vi/7tQgpXgv570/maxresdefault.jpg ")
        await ctx.send(embed = embed_tosend)
    else:
        youtube_name = profile_base["ch_name"]
        youtube_id = profile_base["yt_id"]
        channel_videopage = f"https://www.youtube.com/channel/{youtube_id}/videos"
        channel_xml = feedparser.parse(f"{xml_endpoint}{youtube_id}")
        embedcolor = profile_base["color"]
        if embedcolor != "undefined":
            embedcolor = int(embedcolor, 16)
        else:
            embedcolor = 0x19FFFF
        embed_tosend = discord.Embed(title = f"Recent video of {youtube_name}", url = channel_videopage, color = embedcolor)

        for i in range (5):
            title = channel_xml["entries"][i]["title"]
            title = f"**{title}**"
            videoid = channel_xml["entries"][0]["id"]
            videoid = videoid.lstrip("yt:video:")
            video_page = f"{stream_endpoint}{videoid}"
            embed_tosend.add_field(name = title, value = f"Click [here]({video_page})towatch!",inline=False)

        await ctx.send(embed = embed_tosend)

getprofile_withspec


def getprofile_withspec(temp):
    temp = temp.capitalize()
    if temp == "Default":
        return 0
    else:
        for i in range(members["counts"]):
            if temp == members["channels"][i]["specifier"]:
                return members["channels"][i]
        return 1

It may be said that I couldn't devise the return of the function getprofile, but I have only this ability. Please understand. In order to make the specifier case insensitive, we define only the first letter in JSON as uppercase and the others as lowercase and capitalize it in getprofile.

return 0 and 1 are both returns when an error occurs, 0 is returned when no specifier is entered (when the default argument is used), and it looks like it is collapsed. An error message is displayed. ![Screenshot (482).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/16d54d96-5b62-1caf-91a6-e95ef7d56d2f.png) 1 is returned when the entered specifier cannot be found in the JSON file and the following error message is displayed. ![Screenshot (483).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/5a72a869-9e6e-d60c-60ca-d122d4579a72.png)

If the specifier is found in JSON, the part where the information of that river is stored will be returned as it is. This time

.recent aqua

Since it was specified in, ** specifier * "aqua" * = Minato Aqua's information should be returned. ** **

aqua


        {
            "id": 11,
            "yt_id": "UC1opHUrw8rvnsadT-iGp7Cg",
            "icon": "https://yt3.ggpht.com/ytc/AAUvwngM9Jmc29dvbOY43w7RWFbOZLU4tGtOkEwtt-g7PA=s800-c-k-c0x00ffffff-no-rj",
            "tw_id": "minatoaqua",
            "ch_name": "Aqua Ch.Minato Aqua",
            "name": "Minato Aqua",
            "specifier": "Aqua",
            "part":"2nd-gen",
            "color": "0xFA99E0"
        },

The channel ID of Youtube is extracted from the returned information, and the xml of the video feed of the channel is acquired by feedparser. I will omit the explanation of xml, but ** Since each video is registered as an entry, data will be retrieved from xml in a dictionary type. ** ** Since we will get 5 videos this time, we will process the information obtained by turning the loop 5 times, add it to Embed by inline invalidation, and then send it.

Also, if the color information used by the river during live performance is registered ("color" = hexnum), the embed will be qualified with that color and sent. If not, all will be sent in light blue. This time, ** 0xFA99E0 ** is registered in the ** key "color" **, so Embed is qualified with a color close to Light Pink. The theme color is published on the unofficial Wiki for Nijisanji and so on, so you can copy and paste from there, but since there is no such thing with hololive, at the time of the last 2nd fes. Beyond the Stage I used the color picker function of Power Toys to get it from the published image.

** From here on, I rely on the power of the f-string. It can't be helped. .. .. It's convenient. .. .. ** **

3. Notify the latest video or latest live broadcast of the specified river

** To be honest, I was stepping on that it seemed to be the easiest, but I was really stuck here. ** ** Since it does not use the YoutubeDataAPI, it does not go to real-time update, but by updating the xml feed every few minutes, guerrilla delivery etc. can also be obtained. In the first place, I don't know how often the xml feed is updated, so I don't know until I actually operate it.

Since the functional requirement was to ** deliver only the notifications of the rivers that requested the notifications **, we will start by implementing the function to manage the rivers that notify the notifications. ** **

editclist


@client.command()
async def editclist(ctx, mode = "add", spec = "Default"):
    """Add or delete members on checklist"""
    check = 0
    if mode == "add":
        check = RCh.addlist(spec)
        if check == '0':
            embed_tosend = discord.Embed(title="Please input specifier!", descprition = "Check specifier with [.spechelp] command!")
            embed_tosend.set_image(url = "https://i.ytimg.com/vi/7tQgpXgv570/maxresdefault.jpg ")
            await ctx.send(embed = embed_tosend)
        elif check == '1':
            embed_tosend = discord.Embed(title = "Wrong specifier!", description = "Check specifier with [.spechelp] command!")
            embed_tosend.set_image(url = "https://i.ytimg.com/vi/7tQgpXgv570/maxresdefault.jpg ")
            await ctx.send(embed = embed_tosend)
        elif check == '2':
            embed_tosend = discord.Embed(title="Success!", description="Member you chose was successfully added to checklist!")
            await ctx.send(embed = embed_tosend)
    elif mode == "delete":
        check = RCh.dellist(spec)
        if check == '0':
            embed_tosend = discord.Embed(title="Please input specifier!", descprition = "Check specifier with [.spechelp] command!")
            embed_tosend.set_image(url = "https://i.ytimg.com/vi/7tQgpXgv570/maxresdefault.jpg ")
            await ctx.send(embed = embed_tosend)
        elif check == '1':
            embed_tosend = discord.Embed(title = "Wrong specifier!", description = "Check specifier with [.spechelp] command!")
            embed_tosend.set_image(url = "https://i.ytimg.com/vi/7tQgpXgv570/maxresdefault.jpg ")
            await ctx.send(embed = embed_tosend)
        elif check == '2':
            embed_tosend = discord.Embed(title="Success!", description="Member you chose was successfully deleted from checklist!")
            await ctx.send(embed = embed_tosend)
    elif mode == "check":
        RCh.checklist_debug()
        await ctx.send("**Check the console!**")

recentchecking1


class recentchecking(object):
    check_list = []
    def addlist(self, temp):
        temp = temp.capitalize()
        if temp == "Default":
            return '0'
        else:
            for i in range(members["counts"]):
                if temp == members["channels"][i]["specifier"]:
                    self.check_list.append([members["channels"][i]["name"], members["channels"][i]["yt_id"]])
                    return '2'
            return '1'

    def dellist(self, temp):
        temp = temp.capitalize()
        if temp == "Default":
            return "0"
        else:
            for i in range(members["counts"]):
                if temp == members["channels"][i]["specifier"]:
                    self.check_list.remove([members["channels"][i]["name"], members["channels"][i]["yt_id"]])
                    return "2"
            return "1"

    def checklist_debug(self):
        print(self.check_list)

This is an on-parade code for nesting if statements, but here we manage notification requests.

The description ** RCh.function name ** appears several times, but the variable RCh refers to class recent checking. Regarding the ** return returned from the class, it is the same as before, 0, 1 is an error, 2 is a normal end. ** **

.editclist mode specifier

You can check ** "Add notification request river", "Delete", "Who is registered now" **. Screenshot (484).png When a river is added to the list, a notification like the one above will fly to Discord. If you check the contents of the current list here,

checklist


[["Minato Aqua", "UC1opHUrw8rvnsadT-iGp7Cg"]]

It will be. We will use this data to obtain information.

recentchecking2


class recentchecking(object):
    check_list = []
    recent_list = []
    old_recent_list = []
    
    def getmostrecent(self):
        self.recent_list.clear() #Reset recent_list for running
        for i in range(len(RCh.check_list)): #Add one's recent video to recent_list
            youtube_id = self.check_list[i][1]
            channel_xml = feedparser.parse(f"{xml_endpoint}{youtube_id}")
            recent_video_title = channel_xml["entries"][0]["title"]
            recent_video_id = channel_xml["entries"][0]["yt_videoid"]
            
            #check video or stream status
            returned = Holo.checkstatus(recent_video_id)
            status = returned[0]
            scheduled_time = returned[1]
            
            #embed_color
            embedcolor = getprofile_withytid(youtube_id)
            embedcolor = embedcolor["color"]
            if embedcolor != "undefined":
                embedcolor = int(embedcolor, 16)
            else:
                embedcolor = 0x19FFFF
            
            #appends latest stream information
            self.recent_list.append([recent_video_title, recent_video_id, status, scheduled_time, embedcolor, False])

    def compareon(self):
        if len(self.old_recent_list) < len(self.recent_list):
            for i in range(len(self.old_recent_list), len(self.recent_list)):
                self.recent_list[i][5] = True
        
        for i in range(len(self.old_recent_list)):
            if (self.old_recent_list[i][1] != self.recent_list[i][1]) or (self.old_recent_list[i][2] != self.recent_list[i][2]):
                self.recent_list[i][5] = True
                    
    def checking(self):
        self.getmostrecent()
        self.compareon()
        self.old_recent_list = copy.deepcopy(self.recent_list)

Since the code block itself is long, I separated it separately, but in reality, the checklist addition (deletion) part and the above code block belong to the same class. What you are doing with the getmostrecent function is not much different from fetching the previous 5 videos. ** Get yt_id stored in checklist and get xml. ** ** From there, I get only the latest videos as ** xml ["entries"] [0] **. The acquired ** information is processed and stored in recent_list. ** **

The contents of recent_list are as follows, and the data sent to Discord is determined based on the contents of this list. Also, since I am updating recent_list with ** append, recent_list must be empty every time it is executed **, so ** self.recent_list.clear () ** should be the first in getmostrecent. I'm writing and resetting recent_list.

recent_list


[["Video title", "Video ID(For URL molding that jumps to the viewing page)", "Video status(live, past, upcoming)", "Delivery start time(UTC+09)"(Stored only when the state is live or upcoming),"Bool flag"(Flag to send to Discord. Send if True)]]

Also, I'm hitting the Holofans API to get the status of the video, and at that time I also list the delivery start time and have it returned to the check function Holo.checkstatus.

checkstatus


def checkstatus(self, temp):
        lookup_url = f"{self.holoapi_endpoint}{self.holoapi_video_endpoint}{temp}"
        result = requests.get(lookup_url)
        video_dic = result.json()
        if video_dic["status"] == "live":
            st = video_dic["live_schedule"]
            st = st.replace('T', ' ')
            st = st.replace('Z', '')
            st = st.replace('.000', '')
            starttime = replace_JST(st)

            return_list = [video_dic["status"], starttime]
            print(return_list)
            return return_list
        elif video_dic["status"] == "past":
            return_list = [video_dic["status"], ""]
            print(return_list)
            return return_list
        elif video_dic["status"] == "upcoming":
            st = video_dic["live_schedule"]
            st = st.replace('T', ' ')
            st = st.replace('Z', '')
            st = st.replace('.000', '')
            starttime = replace_JST(st)

            return_list = [video_dic["status"], starttime]
            print(return_list)
            return return_list

replace_JST


#k0gane_p(@k0gane_p)I am using the function created by.
#The processing here is quite@k0gane_I have linked the article to the end of this section because there is a part that referred to Mr. p's article.
def replace_JST(s):
    a = s.split("-")
    u = a[2].split(" ")
    t = u[1].split(":")
    time = [int(a[0]), int(a[1]), int(u[0]), int(t[0]), int(t[1]), int(t[2])]
    if(time[3] >= 15):
      time[2] += 1
      time[3] = time[3] + 9 - 24
    else:
      time[3] += 9
    return (str(time[0]) + "/" + str(time[1]).zfill(2) + "/" + str(time[2]).zfill(2) + " " + str(time[3]).zfill(2) + ":" + str(time[4]).zfill(2))

** The function compareon selects the video to actually send to Discord. ** ** Compare the old_recent_list (I don't know the name ...) that stores the data acquired the previous time with the newly acquired recent_list, and operate the Bool flag that determines whether or not to send.

if len(self.old_recent_list) < len(self.recent_list):
    if len(self.old_recent_list) < len(self.recent_list):
            for i in range(len(self.old_recent_list), len(self.recent_list)):
                self.recent_list[i][5] = True
        
        for i in range(len(self.old_recent_list)):
            if (self.old_recent_list[i][1] != self.recent_list[i][1]) or (self.old_recent_list[i][2] != self.recent_list[i][2]):
                self.recent_list[i][5] = True

Here, ** when content that is not in old_recent_list is added to recent_list ** (when a new notification request river is added or when the bot is launched and executed for the first time), ** all the content that is not in old_recent_list It is set to send. ** **

In the next for statement, the old_recent_list and recent_list are simply compared, and when a new frame or video is found, the contents are sent to Discord. Also, even in the same frame, notifications will be sent three times before distribution, after the start, and after the end (after the archive is released).

So, since it is necessary to execute the process regularly, we adopted the fixed time loop in the extension function of Discord.py. ** (There is also a reference article here, so I will link it together at the end of the section.) **

loop


@tasks.loop(seconds=35)
async def loop():
    now = datetime.now().strftime('%M')
    if (int(now) % 30) == 0: #Do this every %n minutes
        print("Doing")
        channel = client.get_channel(dischannel)
        
        
        if RCh.check_list != []:
            RCh.checking()
            print(RCh.recent_list)
            for i in range(len(RCh.recent_list)):
                if RCh.recent_list[i][5] == True:
                    if RCh.recent_list[i][2] == "live":
                        video_page = f"{stream_endpoint}{RCh.recent_list[i][1]}"
                        video_name = RCh.recent_list[i][0]
                        embed_tosend = discord.Embed(title=f"{RCh.check_list[i][0]} now streaming!", description = f"[**{video_name}**]({video_page})",color=RCh.recent_list[i][4])
                        embed_tosend.add_field(name = "Start Time", value = RCh.recent_list[i][3])
                        embed_tosend.add_field(name = "Video ID", value = RCh.recent_list[i][1])
                        embed_tosend.set_image(url = f"{thumbnail_endpoint_1}{RCh.recent_list[i][1]}{thumbnail_endpoint_2}")
                    elif RCh.recent_list[i][2] == "past":
                        video_page = f"{stream_endpoint}{RCh.recent_list[i][1]}"
                        video_name = RCh.recent_list[i][0]
                        embed_tosend = discord.Embed(title=f"{RCh.check_list[i][0]}'s new video!", description = f"[**{video_name}**]({video_page})",color=RCh.recent_list[i][4])
                        embed_tosend.add_field(name = "Video ID", value = RCh.recent_list[i][1]) 
                        embed_tosend.set_image(url = f"{thumbnail_endpoint_1}{RCh.recent_list[i][1]}{thumbnail_endpoint_2}")
                    elif RCh.recent_list[i][2] == "upcoming":
                        video_page = f"{stream_endpoint}{RCh.recent_list[i][1]}"
                        video_name = RCh.recent_list[i][0]
                        embed_tosend = discord.Embed(title=f"{RCh.check_list[i][0]}'s future stream!", description = f"[**{video_name}**]({video_page})",color=RCh.recent_list[i][4])
                        embed_tosend.add_field(name = "Estimated Start Time", value = RCh.recent_list[i][3])
                        embed_tosend.add_field(name = "Video ID", value = RCh.recent_list[i][1])
                        embed_tosend.set_image(url = f"{thumbnail_endpoint_1}{RCh.recent_list[i][1]}{thumbnail_endpoint_2}")
                    await channel.send(embed = embed_tosend)

** At this point, the data that has been molded so far is finally transmitted. ** ** ** Get the current time (minutes only) every 35 seconds, request xml when that minute is divisible by 30, and start the process so far. ** The point is that the process is executed every 30 minutes.

By the way, if you check the time every 35 seconds, when the bot starts executing at Nh hours Nm minutes 00 seconds, if it is every 30 seconds, the same process will be repeated twice a minute. It's usually useless, so I set it to 35 seconds to prevent it. (If so, I will answer "I want information as soon as possible" to the opinion that 60 is okay.)

I prepared some functions so far, but the ** function that is actually called ** is a completely different function called ** RCh.Recent_list (). ** ** Insanely simple

RCh.recent_list


def checking(self):
     self.getmostrecent()
     self.compareon()
     self.old_recent_list = copy.deepcopy(self.recent_list)

** That's all, but here I got stuck for 5 hours **

The reason is that I didn't know the reference copy and the actual copy of python, but at first I wrote as follows.

Wrongcode


self.old_recent_list = self.recent_list

I thought that I could copy the contents of the list with this, but apparently ** old_recent_list was assigned something like a pointer of recent_list and it seemed to be copied in a pseudo manner, but in reality both of them have the same entity. It is a punch line that was represented **.

** In C ++, even structures can be copied with =, so I was completely interested in it. ** **

** I wish I had noticed it sooner, but sooner or later I was grateful that I would have been fighting a fundamental mistake that I couldn't understand for a longer time without searching the Web. ** **

If executed successfully, the following notification will be sent. Screenshot (494).png

The problem is that a notification is always sent at the first execution after registering a river, but since it is intended to be deployed on a mackerel machine and then executed all the time, there is no big problem. I think. My house often trips, so I'm likely to end up seeing the same notification several times.

Reference article

Python: Making a Discord Bot (Rewrite / v1.x)

I made a Discord bot that gets the distribution schedule of Hololive members and notifies them at the distribution start time
A story I was addicted to by copying the list
[Python] BOT that lets you speak at a specified time on Discord
What you can do with Embed in Discord.py (memo)
Display link on Discord BOT small story Embed

Many other sites, videos, etc. All of them were very helpful. Thank you very much.

Future roadmap

--Code optimization

I think it's a poor code myself. So I'm thinking of deleting unnecessary variables or changing the syntax.

--Addition of functions

Since it is still a Ver1.2 bot, I feel that it is necessary to add functions in the future. It's just a notification bot, so you don't have to be so enthusiastic.

--Rewrite to code that does not depend on HoloTools

This is the ultimate goal of the roadmap for the future. To achieve this, you need to understand how to use the Youtube Data API, and it is expected that the level will rise dramatically from this time. However, I would like to take on the challenge in consideration of the future.

If this can be done, not only hololive but also information such as the rivers of the outer box can be acquired, so the degree of freedom of the bot as a whole will increase. Currently, I am running the name of this bot as "Holo checker", but I am aiming to be able to run it as "Vcheker" someday.

Afterword

Bot Ver1.0 was completed in about 1.5 weeks by concentrating my poor programming ability.

Currently, the number of lines at the time of writing this article is 500 (1100 including the JSON I made), so it is considerably shorter than a general program, but in the first place it is a condition that it is on that scale and will continue to be used. It was a very fresh experience as I had never developed it below.

It's the same for both programs and studies, but I have to do it, so I can't remember it, so by building this program this time, I became able to see Python, and I feel that I've finally become able to handle the language Python in earnest . ( I agree with the opinion, "Isn't it practical?" **)

It's a bot I made myself, so I'll keep going with it for a long time. Perhaps the contents will change completely in a few months or years ...

** Finally, it was a very long article, but thank you for reading this far. ** ** I hope this article is helpful for you.

Recommended Posts

A story about a beginner making a VTuber notification bot from scratch in Python
Automatic Zakuzaku, Bitcoin. A story about a Python beginner making a coin check 1-minute chart
A story about making 3D space recognition with Python
A story about making Hanon-like sheet music with Python
A story about a Linux beginner passing LPIC101 in a week
A story about how to specify a relative path in python.
A story about an amateur making a breakout with python (kivy) ②
A story about an amateur making a breakout with python (kivy) ①
A story about trying to implement a private variable in Python.
A story about a python beginner stuck with No module named'http.server'
A story about a beginner participating in a project by Django from team building to product release in 6 weeks
A story about everything from data collection to AI development and Web application release in Python (3. AI development)
[Google Photo & Slack Photo Bot] A story about making a bot that acquires photos in Google Photos and sends them to Slack.
A story about Python pop and append
Generate a class from a string in Python
The story of making a university 100 yen breakfast LINE bot with Python
A story about operating a GCP instance from Discord
A memo about writing merge sort in Python
Escape from Python's virtual environment ~ A story about being trapped in a virtual environment I created ~
Data analysis in Python: A note about line_profiler
A story about running Python on PHP on Heroku
Think about building a Python 3 environment in a Mac environment
Call a Python script from Embedded Python in C ++ / C ++
Create a datetime object from a string in Python (Python 3.3)
In Python, I made a LINE Bot that sends pollen information from location information.
A story about modifying Python and adding functions
[Python] A story about making a LINE Bot with a practical manned function on its own without using Salesforce [Messaging API]
A template that I often use when making Discord BOT in Python (memorial note)
A story about a Python beginner trying to get Google search results using the API
A story about trying to introduce Linter in the middle of a Python (Flask) project
Create a data collection bot in Python using Selenium
Receive dictionary data from a Python program in AppleScript
A story about trying a (Golang +) Python monorepo with Bazel
A story about reflecting Discord activity in Slack Status
Task registration or Notification in Python in Outlook (from VIM)
A reminder about the implementation of recommendations in Python
A story about a Linux beginner putting Linux on a Windows tablet
About __all__ in python
A story about a Python beginner who was about to be crushed by ModuleNotFoundError: No module named'tweepy'
[Note] A story about trying to override a class method with two underscores in Python 3 series.
Machine learning A story about people who are not familiar with GBDT using GBDT in Python
[Python / GAS] A story about creating a personal Web API that allows you to read all about becoming a novelist in vertical writing, and then making it a LINE bot.
How to slice a block multiple array from a multiple array in Python
From a book that programmers can learn ... (Python): About sorting
A story about competing with a friend in Othello AI Preparation
I want to send a message from Python to LINE Bot
A story about making a tanka by chance with Sudachi Py
A story about a tragedy happening by exchanging commands in chat
The story of making a question box bot with discord.py
A story about a GCP beginner building a Minecraft server on GCE
[Deep Learning from scratch] About the layers required to implement backpropagation processing in a neural network
The story of creating a bot that displays active members in a specific channel of slack with python
Take a screenshot in Python
Create a function in Python
Create a dictionary in Python
OCR from PDF in Python
Think about architecture in python
Python beginner launches Discord Bot
A memorandum about correlation [Python]
Make a bookmarklet in Python
A memorandum about Python mock