본문 바로가기

Programming/RaspberryPi

[Docker] qbittorrent-telegrambot 개발 과정

반응형

 

 

[개요]

qbittorrent를 이용하여 다운로드가 완료된 경우 telegrambot을 통해 메시지를 보내는 기능을 기존에 사용하고 있었다.

python으로 간단하게 py 스크립트를 만들어서 보내고 있었는데 여기서 문제가 발생한다

 

[문제발생]

기존 라즈베리 파이에서 사용하고 있던 서비스들을 모두 docker 컨테이너로 띄워서 docker-compose.yml만 실행하면 한번에 서비스를 이전할 수 있도록 구성하고 있었는데, qbittorrent 컨테이너에서 기본적으로 있는 python을 사용하고 싶어도 해당 컨테이너에 pip 패키지를 설치하기 어려웠던 것이 문제였다. (컨테이너를 껐다 다시 켜면 패키지가 모두 사라진다던가 하는 문제 발생)

 

[해결과정]

그러다 보니 다음과 같은 과정을 거치게 됐는데

 

우선, 기존 파이썬 패키지가 저장된 경로를 볼륨 설정으로 만들어서 사용하고자 했다.

최신 qbittorrent 이미지에서는 Python 3.12를 사용하고 있었는데, 이것을 OS에 있는 파이썬 디렉토리와 일치시켜서 OS에 설치된 패키지를 컨테이너에서도 사용할 수 있는 것이다.

 

이를 통해 임시방편으로나마 python-telegram-bot 패키지를 qbittorrent 컨테이너에서도 사용할 수 있었다.

하지만, 다른 라즈베리 파이나, 파이썬 환경이 다른 곳에서는 해당 구성을 사용할 수 없어서 다른 방법을 찾아나섰다.

 

며칠을 고민한 끝에, flask 서버를 띄우고 웹 서버에서 GET 요청을 받으면 telegram으로 메시지를 보내줄 수 있도록 하고자 했다.

 

우선 기존 코드는 다음과 같다.

 

import sys
import telegram     # pip3 install python-telegram-bot
import asyncio

from datetime import datetime

telgm_token = '(텔레그램봇 TOKEN)'
telgm_chatid = '(텔레그램봇 CHATID)'
bot = telegram.Bot(token = telgm_token)

time = datetime.today().strftime('%Y-%m-%d %H:%M:%S')

arg_1 = sys.argv[1]     # 이름
arg_2 = sys.argv[2]     # 크기
arg_3 = sys.argv[3]     # 저장경로

if len(sys.argv) != 4:
    print("Insufficient arguments")
    sys.exit()

data = int(arg_2)

if data >= 1024:
    # 1024보다 큰 경우 KByte 단위로 변환
    data = round(data / 1024, 2)
    if data >= 1024:
        # 1024보다 큰 경우 MByte 단위로 변환
        data = round(data / 1024, 2)
        if data >= 1024:
            # 1024보다 큰 경우 GByte 단위로 변환
            data = round(data / 1024, 2)
            data = "{} GBytes".format(data)
        else:
            data = "{} MBytes".format(data)
    else:
        data = "{} KBytes".format(data)
else:
    data = "{} Bytes".format(data)

content = '<b>[Qbittorrent]</b> 다운로드 완료\n[SERVER] RaspberryPi 4B\n\n이름: {}\n크기: {}\n저장경로 {}\n완료시간: {}'.format(arg_1, data, arg_3, time)
asyncio.run(bot.sendMessage(chat_id=telgm_chatid, text=content, parse_mode='html'))

 

 

qbittorrent의 외부 프로그램 실행 기능을 통해 파이썬을 실행하고, 이름, 크기, 저장경로에 대한 정보를 인자값으로 받아 텔레그램으로 메시지를 보내는 간단한 파이썬 스크립트이다.

 

 

해당 코드를 Flask를 통해 웹 서버로 만들고, docker 컨테이너로 띄우기 편하게 코드를 수정해보았다.

 

https://github.com/rigizer/qbittorrent-telegrambot

 

GitHub - rigizer/qbittorrent-telegrambot: qbittorrent 다운로드 완료 시 telegram 메시지를 보내주는 서비스

qbittorrent 다운로드 완료 시 telegram 메시지를 보내주는 서비스. Contribute to rigizer/qbittorrent-telegrambot development by creating an account on GitHub.

github.com

from flask import Flask     # pip install flask
from flask import request
from datetime import datetime

import os
import telegram             # pip install python-telegram-bot
import asyncio
import platform

if platform.system() == 'Windows':
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

SERVER_NAME = os.environ['SERVER_NAME']
TELEGRAM_TOKEN = os.environ['TELEGRAM_TOKEN']
TELEGRAM_CHATID = os.environ['TELEGRAM_CHATID']

bot = telegram.Bot(token = TELEGRAM_TOKEN)
app = Flask(__name__)

@app.route('/')
def send_message():
    time = datetime.today().strftime('%Y-%m-%d %H:%M:%S')

    name: str = request.args.get('name')
    size: int = size_convert(int(request.args.get('size')))
    path: str = request.args.get('path')

    content = '<b>[Qbittorrent]</b> 다운로드 완료\n[SERVER] {}\n\n이름: {}\n크기: {}\n저장경로 {}\n완료시간: {}'.format(SERVER_NAME, name, size, path, time)
    asyncio.run(bot.sendMessage(chat_id=TELEGRAM_CHATID, text=content, parse_mode='html'))

    return 'success'

def size_convert(size):
    if size >= 1024:
        # 1024보다 큰 경우 KBytes 단위로 변환
        size = round(size / 1024, 2)
        if size >= 1024:
            # 1024보다 큰 경우 MBytes 단위로 변환
            size = round(size / 1024, 2)
            if size >= 1024:
                # 1024보다 큰 경우 GBytes 단위로 변환
                size = round(size / 1024, 2)
                size = "{} GBytes".format(size)
            else:
                size = "{} MBytes".format(size)
        else:
            size = "{} KBytes".format(size)
    else:
        size = "{} Bytes".format(size)
    
    return size

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

 

 

개발하기 위한 과정은 다음과 같다.

 

1. 가상 환경(venv)를 통해 의존성 패키지 별도 관리 (OS에서 다른 프로젝트에서 사용되는 패키지와 구분하기 위함. 이후 freeze를 통해 필요한 의존성만 설치해야하기 때문.)

- 이때, 윈도우 환경이라면 .venv/Scripts/activate 명령어를 통해 venv 환경을 활성화시킨다 (.bat는 사용하지 않음)

https://velog.io/@tls0506/Windows-Python-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-venv-%EA%B0%80%EC%83%81-%ED%99%98%EA%B2%BD-formattor-linter

 

Windows Python 환경 설정 (venv 가상 환경, formattor, linter)

windows에서 파이썬 가상환경을 설정하고, vscode에서 formattor와 linter로 개발 환경 구축하기!

velog.io

 

2. flask 패키지를 설치하여 flask 웹 서버를 구성한다

https://jsikim1.tistory.com/329

 

Flask 프로젝트 생성 및 시작하는 방법 (직접 시작하는 방법과 PyCharm으로 하는 방법)

Flask 프로젝트 생성 및 시작하는 방법 (직접 시작하는 방법과 PyCharm으로 하는 방법) Flask는 Python 프레임워크로 Django 보다 가벼운 프레임워크로서 사용하기에 좋은 프레임워크입니다. Flask 프로젝

jsikim1.tistory.com

https://aplab.tistory.com/entry/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%ED%94%8C%EB%9D%BC%EC%8A%A4%ED%81%ACflask-URL-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%EB%A1%9C-%EA%B0%92-%EC%9E%85%EB%A0%A5%EB%B0%9B%EA%B8%B0

 

파이썬 플라스크(flask), URL 파라미터 값 입력 받는 방법

파이썬 플라스크는 웹 프레임워크 중에서 가장 이해하기 쉬운 구조를 가지고 있습니다. 간단한 웹페이지는 쉽고 빠르게 만들 수 있지요. 웹서핑을 하다 보면 특정 링크나 버튼을 눌렀을 때, url

aplab.tistory.com

 

- 이 때, 윈도우 환경에서 개발하다보니 asyncio 관련 오류가 발생했다.

https://velog.io/@hyesukim1/%EC%98%A4%EB%A5%98RuntimeError-Event-loop-is-closed

 

[오류]RuntimeError: Event loop is closed

asyncio를 사용한 비동기 프로그래밍 모듈을 사용할 때 자꾸 오류가 나서 찾아보고 해결했다.python 3.8이상부터 운영체제 windows에서 asyncio를 사용할 경우 정상적으로 작동 되었음에도 위와같은 오

velog.io

 

- 윈도우 환경에서만 발생하는 오류이기 때문에, 파이썬에서 OS 환경을 체크하는 기능도 함께 확인했다.

https://computer-science-student.tistory.com/373

 

[파이썬, Python] 운영체제(os) 확인하기

Python 운영체제(os) 확인하기 파이썬 코드를 통해 운영체제를 확인하고 싶다면 platform 모듈을 사용해서 정보를 얻을 수 있다. 나의 경우 os만 확인하면 되는 것이라 아래 코드로 충분하지만 혹시 pl

computer-science-student.tistory.com

 

- 기본 5000번 포트 대신 다른 포트번호 사용 및 로컬 호스트가 아닌 전역 IP에 대응하여 접속하기 위한 방법도 함께 확인했다.

https://webisfree.com/2018-01-19/python-%EC%8B%A4%ED%96%89%EC%8B%9C-5000-%ED%8F%AC%ED%8A%B8%EB%A5%BC-80-%ED%8F%AC%ED%8A%B8-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0

 

Python 실행시 5000 포트를 80 포트 변경하기

Python에서 앱을 만들고 동작하면 기본 포트는 5000으로 사용됩니다. 만약 5000이 아닌 다른 포트로 사용하려면 어떻게 할까요?

webisfree.com

 

하지만 이러한 과정을 모두 거쳤음에도, 다음과 같은 비동기 오류가 발생했다.

RuntimeError: Install Flask with the 'async' extra in order to use async views.

 

 

때문에, 비동기 함수를 지원하지 않는 Flask 대신, Quart라는 비동기를 지원하는 프레임워크를 사용하는 것으로 변경하였다.

Quart는 Flask와 호환되는 프레임워크여서 변경하기 수월했다.

from quart import Quart, request  # pip install quart
from datetime import datetime

import os
import telegram  # pip install python-telegram-bot
import asyncio
import platform

if platform.system() == 'Windows':
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

SERVER_NAME = os.environ['SERVER_NAME']
TELEGRAM_TOKEN = os.environ['TELEGRAM_TOKEN']
TELEGRAM_CHATID = os.environ['TELEGRAM_CHATID']

bot = telegram.Bot(token=TELEGRAM_TOKEN)
app = Quart(__name__)

@app.route('/')
async def send_message():
    time = datetime.today().strftime('%Y-%m-%d %H:%M:%S')

    name: str = request.args.get('name')
    size: int = size_convert(int(request.args.get('size')))
    path: str = request.args.get('path')

    content = '[Qbittorrent] 다운로드 완료\n[SERVER] {}\n\n이름: {}\n크기: {}\n저장경로 {}\n완료시간: {}'.format(SERVER_NAME, name, size, path, time)
    
    await bot.send_message(chat_id=TELEGRAM_CHATID, text=content)
    
    return 'success'

def size_convert(size):
    if size >= 1024:
        size = round(size / 1024, 2)
        if size >= 1024:
            size = round(size / 1024, 2)
            if size >= 1024:
                size = round(size / 1024, 2)
                size = "{} GBytes".format(size)
            else:
                size = "{} MBytes".format(size)
        else:
            size = "{} KBytes".format(size)
    else:
        size = "{} Bytes".format(size)
    
    return size

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

 

 

3. 외부 환경변수를 통해 필요한 정보(서버주소, 텔레그램봇 토큰 및 ID)를 동적으로 관리 (Docker 컨테이너 실행 시 환경변수 주입)

https://ti.bqbro.com/30

 

[Python] 파이썬에서 환경변수 값 얻어오기

파이썬에서 OS의 환경변수를 읽어오는 방법.import osos.environ['환경변수명'] 도커를 빌드할때 환경변수를 지정해주고간편하게 사양자가 입력값을 입력하도록 유도 할 수 있다. Dorkerfile 에서 환경변

ti.bqbro.com

 

4. 개발이 완료된 프로젝트에 대해 의존성 패키지를 requirements.txt로 관리

pip freeze > requirements.txt

https://computer-science-student.tistory.com/221

 

[파이썬, Python] 설치된 패키지 목록 requirements.txt 생성(pip freeze)과 requirements.txt 속 패키지 설치

설치된 패키지 목록 requirements.txt 생성 가상 환경(venv) 혹은 현재 python에 pip로 설치된 패키지 목록에 대한 정보를 만들기 위해서는 freeze라는 명령어를 사용하면 된다. freeze 명령어를 통해 나온 출

computer-science-student.tistory.com

 

5. docker 이미지 빌드

- Dockerfile 생성

FROM python:3.12

COPY . /app
WORKDIR /app

RUN pip install -r requirements.txt

EXPOSE 8080

CMD ["python", "/app/app.py"]

 

다만, 이 과정에서도 Dockerfile를 개선하여 이미지 빌드를 개선할 수 있는 방법이 있는데, 이 부분은 별도의 블로그 글을 작성하였다.

https://codespeed.tistory.com/53

 

[Docker] Docker 이미지 빌드속도 향상

https://tech.cloudmt.co.kr/2022/11/08/container-imagesize-diet/ 컨테이너 이미지 용량 줄이기문서 작성의 이유 이제는 필수가 된 Dockerfile을 잘 작성하는 것 만으로도 Container의 이미지 사이즈를 줄일 수 있다.

codespeed.tistory.com

 

- docker 이미지 빌드

기존에는 다음 명령어를 이용하여 이미지를 빌드했다.

docker build -t rigizer/qbittorrent-telegrambot:latest .

 

하지만 이렇게 하면 내가 빌드하고 있는 환경(AMD64) 기준으로 이미지가 빌드되기 때문에, buildx를 이용하여 멀티 플랫폼에 대응하는 이미지를 빌드하였다 (라즈베리 파이 환경은 ARM64이기 때문)

 

- buildx 환경 세팅 및 빌드

docker buildx create --name builder --driver docker-container --platform linux/amd64,linux/arm64/v8 --use
docker buildx build --platform linux/amd64,linux/arm64/v8 -t rigizer/qbittorrent-telegrambot:latest --push .

https://80000coding.oopy.io/54dc871d-30c9-46cb-b609-2e8831541b5e

 

Docker buildx로 멀티 아키텍처 플랫폼 image 만들기(2)

Docker Buildx

80000coding.oopy.io

 

마지막 명령어의 push를 하면 인증정보가 없다고 하면서 오류가 발생하는데, dockerkhub에 대한 로그인을 수행하지 않아서 그렇다.

 

- dockerhub 로그인 (CLI 환경)

docker login --username=(dockerhub 사용자 이름)

https://ok-lab.tistory.com/123

 

[Docker] 이미지(Image)를 Docker hub에 Push하기

도커를 한 개의 서버에서 작업을 하고, 본인이 만든 도커 이미지를 다른 서버에서 활용하고 싶을때 도커 허브(Docker hub)에 Push한 후 다른 서버에서 pull을 통해 다른 서버에서 가져갈 수 있을 것이

ok-lab.tistory.com

 

 

이 과정을 모두 거치면 dockerhub에서 다음과 같은 이미지가 푸시된 것을 확인할 수 있다.

 

 

이제 이 컨테이너를 실행하기 위해서는 다음 명령어를 사용하면 된다.

- TZ 환경변수를 통해 시간대를 설정하는 것을 권장한다. (미설정시 UTC-0 기준으로 메시지의 시간이 기재된다)

docker run -d -p (접속하고자 하는 외부 포트번호):8080 -e SERVER_NAME=(서버 이름)-e TELEGRAM_TOKEN=(텔레그램봇 TOKEN) -e TELEGRAM_CHATID=(텔레그램봇 CHATID) -e TZ=Asia/Seoul --name=qbt-tbot rigizer/qbittorrent-telegrambot:latest

 

 

모든 설정이 완료되었다면, 다음과 같이 컨테이너가 올라가있는 것을 확인할 수 있다.

 

 

이제 qbittorrent의 다운로드 완료 시 설정을 curl을 통해 메시지를 보낼 수 있도록 GET 요청을 보내게 설정한다.

curl -X GET -G --data-urlencode "name=%N" --data-urlencode "size=%Z" --data-urlencode "path=%D" "http://qbt-tbot:8080/"

 

모든 설정이 마무리 되면, 다음과 같이 다운로드 완료 메시지를 telegram에서 확인할 수 있을 것이다.

 

 

이미 만들어놓은 docker 이미지를 사용하고 싶다면 다음 레포지토리를 이용하면 된다.

docker pull rigizer/qbittorrent-telegrambot:latest

https://hub.docker.com/repository/docker/rigizer/qbittorrent-telegrambot

 

https://hub.docker.com/repository/docker/rigizer/qbittorrent-telegrambot

 

hub.docker.com

 

 

[개선여지]

쉘 스크립트를 통해 메시지를 전달하고, 쉘 스크립트는 도커 볼륨 설정을 통해 컨테이너마다 불러올 수 있게 되면 훨씬 리소스를 적게먹으면서도 구조가 단순해질 것 같다.

그럼에도 불구하고, 일단 당장 할 수 있는 방법을 수행하여 원하던 목적을 달성하여 기쁘다.

이 과정에서 배운 것도 참 많았던 것 같다.

반응형