├── .github └── workflows │ ├── codeql-analysis.yml │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config.py_example ├── instagram.txt_example ├── requirements.txt ├── scraper.py ├── tests ├── __init__.py └── test_bot.py └── version.py /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '33 14 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: '3.9' 23 | 24 | - name: Install dependencies 25 | run: | 26 | python3 -m pip install --upgrade pip 27 | if [ -f requirements.txt ]; then pip3 install -r requirements.txt; fi 28 | 29 | - run: mv config.py_example config.py 30 | - name: Run tests with pytest 31 | run: | 32 | pytest 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | __pycache__/ 3 | mp4/ 4 | mp3/ 5 | .vscode/ 6 | bot.log 7 | /Logs 8 | .idea 9 | config.pyc 10 | scraper.py 11 | instagram.txt 12 | 13 | /venv 14 | /.idea 15 | /Logs 16 | config.py 17 | 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | pip-wheel-metadata/ 41 | share/python-wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .nox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *.cover 67 | *.py,cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | 71 | # Translations 72 | *.mo 73 | *.pot 74 | 75 | # Django stuff: 76 | *.log 77 | local_settings.py 78 | db.sqlite3 79 | db.sqlite3-journal 80 | 81 | # Flask stuff: 82 | instance/ 83 | .webassets-cache 84 | 85 | # Scrapy stuff: 86 | .scrapy 87 | 88 | # Sphinx documentation 89 | docs/_build/ 90 | 91 | # PyBuilder 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | .python-version 103 | 104 | # pipenv 105 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 106 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 107 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 108 | # install all needed dependencies. 109 | #Pipfile.lock 110 | 111 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 112 | __pypackages__/ 113 | 114 | # Celery stuff 115 | celerybeat-schedule 116 | celerybeat.pid 117 | 118 | # SageMath parsed files 119 | *.sage.py 120 | 121 | # Environments 122 | .env 123 | .venv 124 | env/ 125 | venv/ 126 | ENV/ 127 | env.bak/ 128 | venv.bak/ 129 | 130 | # Spyder project settings 131 | .spyderproject 132 | .spyproject 133 | 134 | # Rope project settings 135 | .ropeproject 136 | 137 | # mkdocs documentation 138 | /site 139 | 140 | # mypy 141 | .mypy_cache/ 142 | .dmypy.json 143 | dmypy.json 144 | 145 | # Pyre type checker 146 | .pyre/ 147 | .DS_Store 148 | twitter.txt 149 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.0] - 2024-10-19 4 | 5 | ### Added 6 | 7 | - Force download option added. 8 | - Added testing 9 | - Refactor code, used wrappers, and removed duplications 10 | - Remove unnecessary code 11 | - Added the ability to use twitter cookies 12 | - Upgrade packages 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mohammad Aless 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Scraper Bot 2 | 3 | A telegram bot that uses the command line utility [yt-dlp](https://github.com/yt-dlp/yt-dlp) to download videos from YouTube and other video sites. 4 | 5 | Check the [change log](https://github.com/mmaless/VideoScraperBot/blob/main/CHANGELOG.md) for the latest updates. 6 | 7 | ## Requirements 8 | 9 | - Python 3+ 10 | - [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) 11 | - Install via pip: 12 | ``` 13 | pip3 install python-telegram-bot --upgrade 14 | ``` 15 | - [yt-dlp](https://github.com/yt-dlp/yt-dlp) 16 | - Install via pip: 17 | ``` 18 | pip3 install --upgrade yt-dlp 19 | ``` 20 | - ffmpeg 21 | - Ubuntu: 22 | ``` 23 | sudo apt-get install ffmpeg 24 | ``` 25 | - Instagram or Twitter cookies 26 | - Install cookies.txt chrome extension, login to instagram and export your cookies to 'instagram.txt' file, place the file in the code directory 27 | 28 | ## Config 29 | 30 | - authorized_ids: users that are allowed to use the bot 31 | - video_path: location of the downloaded video files 32 | - audio_path: location of the downloaded audio files 33 | 34 | ## Commands 35 | 36 | - Simply just send a link to download the video 37 | - \mp4 followed by a link to download a video 38 | - \mp3 followed by a link to download an audio 39 | - \test check if the bot is working 40 | - \id get your telegram ID 41 | 42 | ## Running the bot 43 | 44 | - Install requirements 45 | 46 | ``` 47 | pip3 install -r requirements.txt 48 | ``` 49 | 50 | - Run the bot 51 | 52 | ``` 53 | python3 scraper.py 54 | ``` 55 | 56 | ## Running the tests 57 | 58 | ``` 59 | pytest -v 60 | ``` 61 | -------------------------------------------------------------------------------- /config.py_example: -------------------------------------------------------------------------------- 1 | telegram_token = '' 2 | authorized_ids = [123456789] 3 | video_path = './mp4/' 4 | audio_path = './mp3/' 5 | -------------------------------------------------------------------------------- /instagram.txt_example: -------------------------------------------------------------------------------- 1 | # Netscape HTTP Cookie File 2 | # This file is generated by youtube-dl. Do not edit. 3 | 4 | .instagram.com TRUE / TRUE 123456 csrftoken ABCDEFG 5 | .instagram.com TRUE / TRUE 123456 ds_user_id 123456 6 | .instagram.com TRUE / TRUE 123456 ig_did 123456-ABCDEFG-ABCDEFG-ABCDEFG-123456 7 | .instagram.com TRUE / TRUE 123456 ig_nrcb 1 8 | .instagram.com TRUE / TRUE 123456 mid ABCDEFG 9 | .instagram.com TRUE / TRUE 0 rur PRN 10 | .instagram.com TRUE / TRUE 123456 sessionid 123456%3ABCDEFG 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==4.9.0 2 | Brotli==1.1.0 3 | certifi==2025.4.26 4 | charset-normalizer==3.4.2 5 | exceptiongroup==1.3.0 6 | h11==0.16.0 7 | httpcore==1.0.9 8 | httpx==0.28.1 9 | idna==3.10 10 | iniconfig==2.1.0 11 | mutagen==1.47.0 12 | packaging==25.0 13 | pluggy==1.6.0 14 | pycryptodomex==3.23.0 15 | Pygments==2.19.1 16 | pytest==8.4.0 17 | pytest-asyncio==1.0.0 18 | python-telegram-bot==22.1 19 | requests==2.32.3 20 | sniffio==1.3.1 21 | typing_extensions==4.14.0 22 | urllib3==2.4.0 23 | websockets==15.0.1 24 | yt-dlp==2025.5.22 25 | -------------------------------------------------------------------------------- /scraper.py: -------------------------------------------------------------------------------- 1 | import re 2 | import logging 3 | from functools import wraps 4 | from config import telegram_token, authorized_ids, video_path, audio_path 5 | from version import __version__ 6 | from telegram import Update 7 | from telegram.ext import ApplicationBuilder, MessageHandler, CommandHandler, filters, CallbackContext 8 | from datetime import datetime 9 | from yt_dlp import YoutubeDL 10 | from typing import Callable, Awaitable, Dict 11 | from pathlib import Path 12 | import requests 13 | 14 | MAX_FILESIZE_MB = 50 15 | LINK_REGEX = r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)" 16 | 17 | 18 | def create_folders_if_not_exist(folder_paths): 19 | """Checks if the specified folders exist and creates them if they do not.""" 20 | for folder in folder_paths: 21 | path = Path(folder) 22 | path.mkdir(parents=True, exist_ok=True) 23 | 24 | 25 | create_folders_if_not_exist(['logs', video_path, audio_path]) 26 | 27 | logging.basicConfig(filename=f'logs/{datetime.now():%Y-%m-%d}.log', 28 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 29 | level=logging.ERROR) 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | def url_search(message: str) -> str: 35 | """Extracts the first URL found in a message using regex.""" 36 | link = re.search(LINK_REGEX, message) 37 | return link.group(0) if link else "" 38 | 39 | 40 | async def start(update: Update, _: CallbackContext) -> None: 41 | """Sends a welcome message explaining how to use the bot.""" 42 | await update.message.reply_text( 43 | "\n".join( 44 | ["Hi!", 45 | "This bot uses the command line utility [yt-dlp](https://github.com/yt-dlp/yt-dlp)", 46 | "to download videos or audios from YouTube.com and other video sites.", 47 | "To download a video just send the link or use the command \\mp4 and for audio use \\mp3 followed by the link", 48 | "Example: `\\mp4 https://www.youtube.com/watch?v=dQw4w9WgXcQ`", 49 | "This is an open-source [project](https://github.com/mmaless/VideoScraperBot)", 50 | f"You are using version: {__version__}"]), 51 | parse_mode='Markdown', 52 | disable_web_page_preview=True 53 | ) 54 | 55 | 56 | def get_opts(video_url: str, opts_type: str) -> Dict[str, object]: 57 | """Generates download options for video or audio.""" 58 | date = f'{datetime.now():%Y-%m-%d}' 59 | opts = { 60 | 'format': 'best' if opts_type == 'video' else 'bestaudio/best', 61 | 'quiet': True, 62 | 'noplaylist': True, 63 | 'outtmpl': f"{video_path if opts_type == 'video' else audio_path}{date}_%(id)s.%(ext)s", 64 | } 65 | if opts_type == 'audio': 66 | opts['postprocessors'] = [{ 67 | 'key': 'FFmpegExtractAudio', 68 | 'preferredcodec': 'mp3', 69 | 'preferredquality': '192', 70 | }] 71 | if 'instagram.com' in video_url: 72 | opts['cookiefile'] = 'instagram.txt' 73 | elif 'twitter.com' in video_url or 'x.com' in video_url: 74 | opts['cookiefile'] = 'twitter.txt' 75 | return opts 76 | 77 | 78 | def get_video_info(video_url: str, ydl_opts: Dict[str, object]) -> float: 79 | """Retrieves video file size, without downloading.""" 80 | if video_url.endswith(('.mp4', '.mp3')): 81 | response = requests.head(video_url) 82 | if 'Content-Length' in response.headers: 83 | file_size = int(response.headers['Content-Length']) 84 | file_size_mb = file_size / (1024 * 1024) 85 | return file_size_mb 86 | with YoutubeDL(ydl_opts) as ydl: 87 | info = ydl.extract_info(video_url, download=False) 88 | filesize = info.get('filesize') or info.get('filesize_approx') 89 | return filesize / (1024 ** 2) if filesize else 0 90 | 91 | 92 | def authorize(func: Callable[[Update, CallbackContext], Awaitable[None]]) -> Callable[[Update, CallbackContext], Awaitable[None]]: 93 | """Decorator to authorize users.""" 94 | @wraps(func) 95 | async def wrapper(update: Update, context: CallbackContext, *args, **kwargs) -> None: 96 | user_id = update.effective_user.id 97 | if user_id not in authorized_ids: 98 | await update.message.reply_text("You are not authorized to use this bot.") 99 | return 100 | await func(update, context, *args, **kwargs) 101 | return wrapper 102 | 103 | 104 | def validate_url(func: Callable[[Update, CallbackContext], Awaitable[None]]) -> Callable[[Update, CallbackContext], Awaitable[None]]: 105 | """Decorator to validate URL from message.""" 106 | @wraps(func) 107 | async def wrapper(update: Update, context: CallbackContext, *args, **kwargs) -> None: 108 | video_url = url_search(update.message.text) 109 | if not video_url: 110 | await update.message.reply_text('That URL looks invalid.') 111 | return 112 | await func(update, context, *args, **kwargs) 113 | return wrapper 114 | 115 | 116 | @authorize 117 | async def test(update: Update, _: CallbackContext) -> None: 118 | """Test command to check bot authorization.""" 119 | await update.message.reply_text('Works!') 120 | 121 | 122 | @authorize 123 | async def getId(update: Update, _: CallbackContext) -> None: 124 | """Returns the user's chat ID.""" 125 | await update.message.reply_text(f'Your ID is: {update.effective_user.id}') 126 | 127 | 128 | @authorize 129 | @validate_url 130 | async def mp4(update: Update, context: CallbackContext, force: bool = False) -> None: 131 | """Downloads video from a provided URL.""" 132 | video_url = url_search(update.message.text) 133 | ydl_opts = get_opts(video_url, 'video') 134 | if not force: 135 | video_size = get_video_info(video_url, ydl_opts) 136 | if video_size > MAX_FILESIZE_MB: 137 | await update.message.reply_text( 138 | f'The file size ({video_size:.2f} MB) is too big and ' 139 | f'cannot be sent using Telegram.' 140 | ) 141 | return 142 | date = f'{datetime.now():%Y-%m-%d}' 143 | with YoutubeDL(ydl_opts) as ydl: 144 | info_dict = ydl.extract_info(video_url, download=False) 145 | video_id = info_dict.get("id") 146 | video_ext = info_dict.get("ext") 147 | ydl.download([video_url]) 148 | video = f"{date}_{video_id}.{video_ext}" 149 | if force: 150 | await update.message.reply_text(f'You chose to force download a video. It will not be sent via Telegram.') 151 | return 152 | await context.bot.send_video(chat_id=update.effective_user.id, video=open(f"{video_path}{video}", 'rb')) 153 | 154 | 155 | @authorize 156 | async def force(update: Update, context: CallbackContext) -> None: 157 | """Forces downloading a video even if the file size exceeds the limit.""" 158 | await mp4(update, context, force=True) 159 | 160 | 161 | @authorize 162 | @validate_url 163 | async def mp3(update: Update, context: CallbackContext) -> None: 164 | """Downloads audio from a provided URL.""" 165 | video_url = url_search(update.message.text) 166 | ydl_opts = get_opts(video_url, 'audio') 167 | 168 | audio_size = get_video_info(video_url, ydl_opts) 169 | if audio_size > MAX_FILESIZE_MB: 170 | await update.message.reply_text( 171 | f'The file size ({audio_size:.2f} MB) is too big and ' 172 | f'cannot be sent using Telegram.' 173 | ) 174 | return 175 | date = f'{datetime.now():%Y-%m-%d}' 176 | with YoutubeDL(ydl_opts) as ydl: 177 | info_dict = ydl.extract_info(video_url, download=False) 178 | audio_id = info_dict.get("id") 179 | ydl.download([video_url]) 180 | audio = f"{date}_{audio_id}.mp3" 181 | await context.bot.send_audio(chat_id=update.effective_user.id, audio=open(f"{audio_path}{audio}", 'rb')) 182 | 183 | 184 | async def error(update: Update, context: CallbackContext) -> None: 185 | """Handles errors during command execution.""" 186 | await update.message.reply_text('An error occurred! You have to check the logs.') 187 | logger.error('Update "%s" caused error "%s"', update, context.error) 188 | 189 | 190 | def main() -> None: 191 | """Starts the bot and sets up handlers.""" 192 | app = ApplicationBuilder().token(telegram_token).build() 193 | app.add_handler(CommandHandler('start', start)) 194 | app.add_handler(CommandHandler('test', test)) 195 | app.add_handler(CommandHandler('mp4', mp4)) 196 | app.add_handler(CommandHandler('mp3', mp3)) 197 | app.add_handler(CommandHandler('id', getId)) 198 | app.add_handler(CommandHandler('force', force)) 199 | app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, mp4)) 200 | app.add_error_handler(error) 201 | app.run_polling() 202 | 203 | 204 | if __name__ == '__main__': 205 | main() 206 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmaless/VideoScraperBot/ec6fdaa6b6c44e369caf5bac0b4977235a335461/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_bot.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import patch 3 | from scraper import getId, mp4, url_search, get_opts, get_video_info 4 | 5 | video_url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4" 6 | long_video_url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" 7 | 8 | 9 | class TestBotFunctions: 10 | 11 | def test_url_search_valid_url(self): 12 | """Test url_search with a valid URL.""" 13 | message = f"Check this out {video_url}" 14 | result = url_search(message) 15 | assert result == video_url 16 | 17 | def test_url_search_invalid_url(self): 18 | """Test url_search with no URL.""" 19 | message = "There is no URL in this message" 20 | result = url_search(message) 21 | assert result == "" 22 | 23 | def test_get_short_video_info(self): 24 | """Test get_video_info using real URL of a short video""" 25 | result = get_video_info(video_url, get_opts(video_url, 'video')) 26 | assert result < 50 27 | 28 | def test_get_long_video_info(self): 29 | """Test get_video_info using real URL of a long video""" 30 | result = get_video_info( 31 | long_video_url, get_opts(long_video_url, 'video')) 32 | assert result > 50 33 | 34 | @pytest.mark.asyncio 35 | @patch('scraper.authorized_ids', [123456]) 36 | async def test_get_user_id(self): 37 | """Test id command returning user's ID""" 38 | class MockMessage: 39 | text = "" 40 | 41 | async def reply_text(self, text): 42 | assert "Your ID is: 123456" in text 43 | 44 | class MockUser: 45 | id = 123456 46 | 47 | class MockUpdate: 48 | message = MockMessage() 49 | effective_user = MockUser() 50 | 51 | class MockContext: 52 | bot = None 53 | mock_update = MockUpdate() 54 | mock_context = MockContext() 55 | await getId(mock_update, mock_context) 56 | 57 | @pytest.mark.asyncio 58 | async def test_get_user_id_unauthorized_access(self): 59 | """Test id command rejecting unauthorized access""" 60 | class MockMessage: 61 | text = "" 62 | 63 | async def reply_text(self, text): 64 | assert "You are not authorized to use this bot." in text 65 | 66 | class MockUser: 67 | id = 123456 68 | 69 | class MockUpdate: 70 | message = MockMessage() 71 | effective_user = MockUser() 72 | 73 | class MockContext: 74 | bot = None 75 | mock_update = MockUpdate() 76 | mock_context = MockContext() 77 | await getId(mock_update, mock_context) 78 | 79 | @pytest.mark.asyncio 80 | @patch('scraper.authorized_ids', [123456]) 81 | async def test_mp4_download_too_large(self): 82 | """Test mp4 command rejecting large files using a real URL.""" 83 | class MockMessage: 84 | text = long_video_url 85 | 86 | async def reply_text(self, text): 87 | assert "is too big and cannot be sent using Telegram" in text 88 | 89 | class MockUser: 90 | id = 123456 91 | 92 | class MockUpdate: 93 | message = MockMessage() 94 | effective_user = MockUser() 95 | 96 | class MockContext: 97 | bot = None 98 | mock_update = MockUpdate() 99 | mock_context = MockContext() 100 | await mp4(mock_update, mock_context) 101 | 102 | @pytest.mark.asyncio 103 | @patch('scraper.authorized_ids', [123456]) 104 | async def test_mp4_download(self): 105 | """Test mp4 command using a real URL.""" 106 | class MockMessage: 107 | text = video_url 108 | 109 | class MockUser: 110 | id = 123456 111 | 112 | class MockUpdate: 113 | message = MockMessage() 114 | effective_user = MockUser() 115 | 116 | class MockBot: 117 | async def send_video(self, chat_id, video): 118 | assert chat_id == 123456 119 | assert video is not None 120 | 121 | class MockContext: 122 | bot = MockBot() 123 | 124 | mock_update = MockUpdate() 125 | mock_context = MockContext() 126 | await mp4(mock_update, mock_context) 127 | 128 | 129 | if __name__ == '__main__': 130 | pytest.main() 131 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.1" 2 | --------------------------------------------------------------------------------