Skip to content

Commit

Permalink
2024-10-27 12:59:58+04:00
Browse files Browse the repository at this point in the history
  • Loading branch information
nett00n committed Oct 27, 2024
1 parent 0454f41 commit 26944ee
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 18 deletions.
33 changes: 30 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,69 @@
repos:
- hooks:
- id: trailing-whitespace
always_run: true
- id: end-of-file-fixer
always_run: true
- id: check-yaml
always_run: true
- id: check-added-large-files
always_run: true
- id: check-ast
- files: bot/requirements.txt
id: file-contents-sorter
always_run: true
- id: check-builtin-literals
always_run: true
- id: check-case-conflict
always_run: true
- id: check-docstring-first
always_run: true
- id: check-executables-have-shebangs
always_run: true
- id: check-json
always_run: true
- id: check-merge-conflict
always_run: true
- id: check-shebang-scripts-are-executable
always_run: true
- id: check-symlinks
always_run: true
- id: detect-private-key
always_run: true
- id: end-of-file-fixer
always_run: true
- id: fix-byte-order-marker
- id: fix-encoding-pragma
always_run: true
- id: mixed-line-ending
always_run: true
- id: name-tests-test
always_run: true
- id: pretty-format-json
always_run: true
- id: trailing-whitespace
always_run: true
- files: docker-compose.yml
id: sort-simple-yaml
always_run: true
- id: no-commit-to-branch
args: ['--branch', 'main']
always_run: true
repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
- hooks:
- id: black
args: ["."]
always_run: true
repo: https://github.com/psf/black
rev: 24.10.0
- hooks:
- id: isort
args: ["."]
always_run: true
repo: https://github.com/PyCQA/isort
rev: 5.13.2
- hooks:
- id: flake8
always_run: true
exclude: lib/python3.11/site-packages
repo: https://github.com/PyCQA/flake8
rev: 7.1.1
- repo: local
Expand All @@ -50,3 +76,4 @@ repos:
entry: poetry run pytest
language: system
pass_filenames: false
always_run: true
7 changes: 6 additions & 1 deletion app/api.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
# -*- coding: utf-8 -*-
# api.py

from fastapi import APIRouter, HTTPException, Body
from fastapi import APIRouter, Body, HTTPException
from pydantic import BaseModel

from .url_processing import process_url_request


# Define a request model to parse JSON body
class URLRequest(BaseModel):
url: str


api_router = APIRouter()


@api_router.post("/process_url/")
async def process_url(request: URLRequest = Body(...)):
try:
Expand Down
8 changes: 7 additions & 1 deletion app/bot.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
# -*- coding: utf-8 -*-
# bot.py

import asyncio
import logging

from aiogram import Bot, Dispatcher, types
from aiogram.utils import executor

from app.config import settings

from .url_processing import process_url_request

bot = Bot(token=settings.BOT_TOKEN)
dp = Dispatcher(bot)
logger = logging.getLogger(__name__)


async def start(message: types.Message):
await message.reply("Hello! Send me a URL to process!")


@dp.message_handler(content_types=types.ContentType.TEXT)
async def handle_message(message: types.Message):
url = message.text.strip()
result = await process_url_request(url)
await message.reply(result)


def start_bot():
dp.register_message_handler(start, commands="start")
executor.start_polling(dp, skip_updates=True)
4 changes: 4 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
# -*- coding: utf-8 -*-
# config.py

import os

from dotenv import load_dotenv
from pydantic_settings import BaseSettings # Updated import

load_dotenv()


class Settings(BaseSettings):
BASE_URL: str = os.getenv("BASE_URL", "")
BOT_TOKEN: str = os.getenv("BOT_TOKEN", "")
CACHE_DIR: str = os.getenv("CACHE_DIR", "/tmp/url-fairy-bot-cache/")
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO").upper()


settings = Settings()
7 changes: 6 additions & 1 deletion app/download.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# -*- coding: utf-8 -*-
# download.py

import os
import logging
import os

import yt_dlp

from app.config import settings

logger = logging.getLogger(__name__)


async def yt_dlp_download(url: str) -> str:
video_path = os.path.join(settings.CACHE_DIR, f"{sanitize_subfolder_name(url)}.mp4")
if os.path.exists(video_path):
Expand All @@ -21,5 +25,6 @@ async def yt_dlp_download(url: str) -> str:
logger.error(f"Download failed for URL: {url}, error: {e}")
return None


def sanitize_subfolder_name(url: str) -> str:
return "".join(c if c.isalnum() else "_" for c in url)
9 changes: 8 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
# main.py

import logging
import asyncio
import logging

from fastapi import FastAPI

from app.config import settings

from .api import api_router
from .bot import dp # Import the dispatcher directly

Expand All @@ -17,15 +21,18 @@
logging.basicConfig(level=settings.LOG_LEVEL)
logger = logging.getLogger(__name__)


@app.on_event("startup")
async def on_startup():
# Start the bot's polling in a background task
asyncio.create_task(dp.start_polling())


@app.on_event("shutdown")
async def on_shutdown():
# Shutdown the dispatcher when FastAPI stops
await dp.storage.close()
await dp.storage.wait_closed()


logger.info("✨ URL Fairy bot initialized with FastAPI")
8 changes: 6 additions & 2 deletions app/url_processing.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# -*- coding: utf-8 -*-
# url_processing.py

import os
import logging
from urllib.parse import urlparse, urlunparse

import requests
from app.config import settings

from .download import yt_dlp_download

logger = logging.getLogger(__name__)


async def process_url_request(url: str) -> str:
try:
final_url = follow_redirects(url)
Expand All @@ -24,6 +26,7 @@ async def process_url_request(url: str) -> str:
logger.error(f"Error processing URL: {e}")
return str(e)


def follow_redirects(url: str, timeout=10) -> str:
try:
response = requests.head(url, allow_redirects=True, timeout=timeout)
Expand All @@ -32,6 +35,7 @@ def follow_redirects(url: str, timeout=10) -> str:
logger.warning(f"Timeout for URL: {url}")
return url


def transform_tiktok_url(url: str) -> str:
parsed_url = urlparse(url)
return f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}"
4 changes: 3 additions & 1 deletion tests/test_api.py → tests/api_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# test_api.py
# api_test.py

import pytest
from httpx import AsyncClient

from app.main import app


@pytest.mark.asyncio
async def test_process_url():
async with AsyncClient(app=app, base_url="http://test") as ac:
Expand Down
6 changes: 4 additions & 2 deletions tests/test_bot.py → tests/bot_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# test_bot.py
# bot_test.py

import pytest
from aiogram.types import Message

from app.bot import handle_message


@pytest.mark.asyncio
async def test_handle_message():
class MockMessage:
text = "https://example.com"

async def reply(self, text):
assert "Unsupported URL https://example.com/" in text or "success" in text

Expand Down
21 changes: 15 additions & 6 deletions tests/test_url_processing.py → tests/url_processing_test.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
# test_url_processing.py
# url_processing_test.py

from unittest.mock import patch

import os
import pytest
from unittest.mock import patch, AsyncMock

from app.url_processing import process_url_request, transform_tiktok_url


@pytest.mark.asyncio
async def test_tiktok_url_processing():
test_url = "https://vt.tiktok.com/ZSYFmgDeS/"
expected_file_path = "/tmp/url-fairy-bot-cache/https___www_tiktok_com___video_7371578973689482539.mp4"
transformed_url = transform_tiktok_url("https://www.tiktok.com/video/7371578973689482539")
transformed_url = transform_tiktok_url(
"https://www.tiktok.com/video/7371578973689482539"
)

# Mock follow_redirects to return the final TikTok URL format
with patch("app.url_processing.follow_redirects", return_value="https://www.tiktok.com/video/7371578973689482539"):
with patch(
"app.url_processing.follow_redirects",
return_value="https://www.tiktok.com/video/7371578973689482539",
):
# Mock yt_dlp_download to simulate successful download
with patch("app.url_processing.yt_dlp_download", return_value=expected_file_path) as mock_download:
with patch(
"app.url_processing.yt_dlp_download", return_value=expected_file_path
) as mock_download:
result = await process_url_request(test_url)

# Assertions
Expand Down

0 comments on commit 26944ee

Please sign in to comment.