iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
💫

Building a LINE Video Echo Bot using YouTube

に公開

Introduction

This article is the same as the one I posted on Qiita.
Hello. I'm Maguro from STECH.

This is an article for the STECH and Aichi Institute of Technology Systems Engineering Research Group Joint Advent Calendar.

In this article, I will try uploading a video to YouTube using the LINE API.

Why I Built It

When I previously wrote an article about linking Discord and LINE, images and videos had to be sent in HTTP format.
Discord uses Cloudflare for file storage, so files can be referenced via HTTP.

However, on the LINE side, files are saved as binary data and are reportedly deleted after a certain period.
Setting images aside, I thought sharing videos would be difficult due to the large capacity and load involved.

Nevertheless, I kept looking and discovered the YouTube Data API.
I found that it is possible to upload videos using Python.

By incorporating this into a LINE Bot and returning a URL when it's uploaded, sharing becomes possible.
This time, I will apply that to create a Bot that returns a YouTube video link when a video is uploaded to LINE.

In short, it will be a video version of a LINE echo bot.

Environment

  • Python 3.8.6
  • flask
  • youtube-data-api
  • google-api-python-client
  • ngrok

Preparation

Create credentials from Google Cloud Platform (GCP).
Please proceed with registration by referring to the following article.

https://qiita.com/ny7760/items/5a728fd9e7b40588237c

About Registration

Since the application method seems to have changed slightly from the article above, I will write about the registration method here.
If you have already done this, please skip this section.

Creating a New Project

Access GCP and click "Select a project".

https://console.cloud.google.com/

image.png

Click "New Project".

image.png

Set the Project name (any name you like) and Organization (leave as default).
Once configured, click "Create".

image.png

Once created, you will be redirected to a screen like this.

スクリーンショット 2022-11-01 105309.png

Enabling the YouTube API

Select "Enabled APIs & services" from "APIs & Services".
image.png

You will be redirected to the search screen, so search for "YouTube data api v3".

スクリーンショット 2022-11-01 105345.png

Click on the "YouTube Data API v3" that appears.

スクリーンショット 2022-11-01 105358.png

Click "Enable".

スクリーンショット 2022-11-01 105415.png

You will then be prompted to register credentials, so click "Create Credentials".

image.png

Click "Create Credentials" at the top.
image.png

Select "OAuth client ID".
image.png

You will be prompted to create a consent screen, so click create.

スクリーンショット 2022-11-01 105753.png

Select "External" for User Type and click "Create". Enter the information for the consent screen.

スクリーンショット 2022-11-01 105817.png

Fill in the App name, User support email, and Developer contact information fields.
The logo is optional.
Once filled in, click "Save and Continue".

image.png
image.png

Now, create an OAuth client ID.
Set the Application type to "Desktop app".
The name can be anything you like.

スクリーンショット 2022-11-01 110032.png
スクリーンショット 2022-11-01 110054.png

When you select "Create" here...
スクリーンショット 2022-11-01 110124.png
The OAuth client will be created.
Download the JSON and try uploading a video following the article mentioned earlier.

If the upload completes successfully and upload_video.py-oauth2.json is generated, the preparation is complete.

Design

The overall flow is shown in the diagram below.
image.png

Host the bot by starting a server from your PC using ngrok.

When a video is sent to LINE, the bot saves the video.
Once the saving is finished, it starts uploading to YouTube.
After the upload is complete, an ID is output, which is then used to generate the video URL.

The sequence of events concludes by replying with that URL.

Implementation

Directory Structure

$ tree
.
├── movies
│   └── sample.mp4  # Video to be uploaded
├── client_secrets.json
├── upload_video.py-oauth2.json
├── main.py  
├── line_api.py  # lineapi class
└── upload_video.py  # For YouTube upload

main.py

main.py
from flask import Flask, request, abort,Response
import json
from line_api import LineMessageAPI


app = Flask(__name__)

ACCESSTOKEN = ''
GROUPID = ''

line_bot_api = LineMessageAPI(line_bot_token=ACCESSTOKEN,line_group_id=GROUPID)

@app.route("/", methods=['POST'])
async def callback():
    # Parse the incoming request
    requests = request.get_json()
    
    # If the events content is empty (connection verification)
    if len(requests['events']) == 0:
        return abort(Response("ok"))

    # Parse the contents of events
    data = requests['events'][0]
    
    if data['message']['type'] == 'video':
        # Retrieve video data by class using message_id
        message_content = await line_bot_api.movie_upload(message_id=data['message']['id'],display_name=await line_bot_api.get_proflie(user_id=data['source']['userId']))
        await line_bot_api.reply(reply_token=data['replyToken'],message=message_content)
        return abort(Response("ok"))


if __name__ == "__main__":
    app.run("0.0.0.0", port=8080)

line_api.py

line_api.py
import subprocess
import requests

import asyncio

from functools import partial

class LineMessageAPI:
    def __init__(self, line_bot_token: str, line_group_id: str) -> None:
        self.line_group_id = line_group_id
        self.line_bot_token = line_bot_token

    # Get name from LINE user profile
    async def get_proflie(self, user_id: str):
        # Determine if Group ID is valid
        try:
            return await linereq(
                f"https://api.line.me/v2/bot/group/{self.line_group_id}/member/{user_id}",
                self.line_bot_token,
                "displayName"
            )
        # If Group ID is invalid, determine from friends
        except KeyError:
            return await linereq(
                f"https://api.line.me/v2/bot/profile/{user_id}",
                self.line_bot_token,
                "displayName"
            )

    # Save the video received from LINE and upload it to YouTube as unlisted
    async def movie_upload(self, message_id: int, display_name: str):
        # Get video binary data
        movies_bytes = requests.get(
            f'https://api-data.line.me/v2/bot/message/{message_id}/content',
            headers={
                'Authorization': 'Bearer ' + self.line_bot_token
            }
        ).iter_content()

        # Save as mp4
        with open("./movies/sample.mp4", 'wb') as fd:
            for chunk in movies_bytes:
                fd.write(chunk)

        # Execute upload_video.py using subprocess; the video will be uploaded to YouTube as unlisted
        youtube_id = subprocess.run(
            ['python', 'upload_video.py','--file=./movies/sample.mp4', f'--title="{display_name}\'s Video"', '--description="Video from LINE"','--privacyStatus=unlisted'],
            capture_output=True)

        # Use the outputted ID to create a YouTube unlisted link
        return f"https://youtu.be/{youtube_id.stdout.decode()}"

    async def reply(self, reply_token:str,message:str):
        body = {
            'replyToken': reply_token,
            'messages': message
        }
        return await requests.post(url="https://api.line.me/v2/bot/message/reply",headers={'Authorization': 'Bearer ' + self.line_bot_token},data=body)

# Send a GET request and return the specified data column.
async def linereq(url: str, token: str, jsonkey: str):
    r = requests.get(url=url,headers={'Authorization': 'Bearer ' + token})
    return r.json()[jsonkey]

upload_video.py

upload_video.py
import http.client  # httplib has moved to http.client in Python 3
import httplib2
import os
import random
import sys
import time

from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload
from oauth2client.client import flow_from_clientsecrets
from oauth2client.file import Storage
from oauth2client.tools import argparser, run_flow


httplib2.RETRIES = 1
MAX_RETRIES = 10
RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error,
                        IOError,
                        http.client.NotConnected,
                        http.client.IncompleteRead,
                        http.client.ImproperConnectionState,
                        http.client.CannotSendRequest,
                        http.client.CannotSendHeader,
                        http.client.ResponseNotReady,
                        http.client.BadStatusLine)
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
CLIENT_SECRETS_FILE = "client_secret.json"       # Rewrite to the generated JSON filename
MISSING_CLIENT_SECRETS_MESSAGE = """
WARNING: Please configure OAuth 2.0

To make this sample run you will need to populate the client_secrets.json file
found at:

   %s

with information from the API Console
https://console.developers.google.com/

For more information about the client_secrets.json file format, please visit:
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
""" % os.path.abspath(os.path.join(os.path.dirname(__file__),
                                   CLIENT_SECRETS_FILE))

YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload"
YOUTUBE_API_SERVICE_NAME = "youtube"
YOUTUBE_API_VERSION = "v3"


VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")

#  python upload_video.py --file="./movies/sample.mp4" --title="Sample Movie" --description="This is a sample movie." --category="22" --privacyStatus="unlisted"

def get_authenticated_service(args):
    flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
                                   scope=YOUTUBE_UPLOAD_SCOPE,
                                   message=MISSING_CLIENT_SECRETS_MESSAGE)

    storage = Storage("%s-oauth2.json" % sys.argv[0])
    credentials = storage.get()

    if credentials is None or credentials.invalid:
        credentials = run_flow(flow, storage, args)

    return build(YOUTUBE_API_SERVICE_NAME,
                 YOUTUBE_API_VERSION,
                 http=credentials.authorize(httplib2.Http()))


def initialize_upload(youtube, options):
    tags = None
    if options.keywords:
        tags = options.keywords.split(",")

    body = dict(
        snippet=dict(
            title=options.title,
            description=options.description,
            tags=tags,
            categoryId=options.category
        ),
        status=dict(
            privacyStatus=options.privacyStatus
        )
    )

    insert_request = youtube.videos().insert(
        part=",".join(body.keys()),
        body=body,
        media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
    )

    resumable_upload(insert_request)


def resumable_upload(insert_request):
    response = None
    error = None
    retry = 0
    while response is None:
        try:
            # print("Uploading file...")  # print statement
            status, response = insert_request.next_chunk()
            if response is not None:
                if 'id' in response:
                    print(response['id'])     # Video ID; use this to generate the video URL.
                else:
                    exit("The upload failed with an unexpected response: %s" % response)
        except HttpError as e:
            if e.resp.status in RETRIABLE_STATUS_CODES:
                error = "A retriable HTTP error %d occurred:\n%s" % \
                        (e.resp.status, e.content)
            else:
                raise
        except RETRIABLE_EXCEPTIONS as e:
            error = "A retriable error occurred: %s" % e
        if error is not None:
            print(error)
            retry += 1
            if retry > MAX_RETRIES:
              exit("No longer attempting to retry.")
            max_sleep = 2 ** retry
            sleep_seconds = random.random() * max_sleep
            print("Sleeping %f seconds and then retrying..." % sleep_seconds)
            time.sleep(sleep_seconds)


if __name__ == '__main__':
    # Set the video to upload, title, visibility, etc., from command line arguments (mostly defaults are defined)
    argparser.add_argument("--file", help="Video file to upload",default="./movies/sample.mp4")
    argparser.add_argument("--title", help="Video title", default="Test Title")
    argparser.add_argument("--description",
                           help="Video description",
                           default="Test Description")
    argparser.add_argument("--category", default="22",
                           help="Numeric video category. " +
                                "See https://developers.google.com/youtube/v3/docs/videoCategories/list")
    argparser.add_argument("--keywords", help="Video keywords, comma separated",
                           default="")
    argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
                           default=VALID_PRIVACY_STATUSES[0],
                           help="Video privacy status.")
    args = argparser.parse_args()

    if not os.path.exists(args.file):
        exit("Please specify a valid file using the --file= parameter.")

    youtube = get_authenticated_service(args)
    try:
        initialize_upload(youtube, args)
    except HttpError as e:
        print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content))

Starting ngrok

Start it on port 8080.

ngrok http 8080

Once started, don't forget to set the displayed https://*********.ngrok.io as the Webhook URL for your LINE Bot.
Next, start the LINE Bot.

python main.py

If the connection test is successful, you're all set.

Sending

Let's try sending it.
image.png
image.png

It's complete if a URL is returned and you can watch the video.
It may take some time to process, so it might not be viewable immediately.

Conclusion

Great job.
Thank you for reading this far.

Regarding the Discord integration mentioned earlier, I'm currently rebuilding it to support Notify.
I'm currently busy, so the completion date is undecided, but I'll write an article once it's finished.

See you in the next article.

Discussion