Merge pull request #277 from Dineshkarthik/v2.0.0-stable

Migrate to Pyrogram v2
This commit is contained in:
DK 2022-07-19 12:23:12 +02:00 committed by GitHub
commit 07cd9aec25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 154 additions and 105 deletions

24
.github/workflows/code-checks.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Code Quality
on:
pull_request:
branches: [ master ]
paths-ignore:
- 'README.md'
push:
branches: [ master ]
paths-ignore:
- 'README.md'
jobs:
pre-commit:
name: Linting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: make dev_install
- uses: pre-commit/action@v3.0.0

View File

@ -3,8 +3,12 @@ name: Unittest
on: on:
push: push:
branches: [ master ] branches: [ master ]
paths-ignore:
- 'README.md'
pull_request: pull_request:
branches: [ master ] branches: [ master ]
paths-ignore:
- 'README.md'
jobs: jobs:
build: build:
@ -13,13 +17,13 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [ '3.6', '3.7', '3.8', '3.9' ] python-version: ['3.7', '3.8', '3.9', '3.10', '3.11.0-beta.4' ]
name: Test - Python ${{ matrix.python-version }} on ${{ matrix.os }} name: Test - Python ${{ matrix.python-version }} on ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Get setuptools Unix - name: Get setuptools Unix
@ -30,10 +34,6 @@ jobs:
run: pip install --upgrade --user pip setuptools codecov run: pip install --upgrade --user pip setuptools codecov
- name: Install dependencies - name: Install dependencies
run: make dev_install run: make dev_install
- name: Code Check - Pylint
run: make pylint
- name: Static Type Check - Mypy
run: make static_type_check
- name: Test with pytest - name: Test with pytest
run: | run: |
make -e test make -e test

48
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,48 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
name: black
entry: black
types: [python]
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: isort
entry: isort
types: [python]
args: ["--profile", "black", "--filter-files"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.961
hooks:
- id: mypy
name: mypy
entry: mypy
types: [python]
args: [--ignore-missing-imports]
files: utils/|media_downloader.py
exclude: tests/
- repo: https://github.com/pycqa/pylint
rev: v2.14.5
hooks:
- id: pylint
name: pylint
entry: pylint
language: system
types: [python]
args: [
"-rn", # Only display messages
"-sn", # Don't display the score
"--rcfile=pylintrc" # Link to your config file
]
files: utils/|media_downloader.py
exclude: tests/

View File

@ -179,10 +179,10 @@ Must be one of the following:
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc) - **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc)
- **test**: Additions/updates to tests - **test**: Additions/updates to tests
- **type**: Type annotations - **type**: Type annotations
#### Subject: #### Subject:
Please reference the relevant GitHub issues in your commit message using #1234. Please reference the relevant GitHub issues in your commit message using #1234.
- a subject line with `< 80` chars. - a subject line with `< 80` chars.
- summary in present tense. - summary in present tense.
- not capitalized. - not capitalized.
@ -196,4 +196,4 @@ Explain the motivation for the change in the commit message body. This commit me
### Code of Conduct ### Code of Conduct
As a contributor, you can help us keep the community open and inclusive. Please read and follow our [Code of Conduct](https://github.com/Dineshkarthik/telegram_media_downloader/blob/master/CODE_OF_CONDUCT.md). As a contributor, you can help us keep the community open and inclusive. Please read and follow our [Code of Conduct](https://github.com/Dineshkarthik/telegram_media_downloader/blob/master/CODE_OF_CONDUCT.md).

View File

@ -4,7 +4,7 @@ TEST_ARTIFACTS ?= /tmp/coverage
install: install:
python3 -m pip install --upgrade pip setuptools python3 -m pip install --upgrade pip setuptools
python3 -m pip install -r requirements.txt python3 -m pip install -r requirements.txt
dev_install: install dev_install: install
python3 -m pip install -r dev-requirements.txt python3 -m pip install -r dev-requirements.txt
@ -23,4 +23,4 @@ test:
--cov-report term-missing \ --cov-report term-missing \
--cov-report html:${TEST_ARTIFACTS} \ --cov-report html:${TEST_ARTIFACTS} \
--junit-xml=${TEST_ARTIFACTS}/media-downloader.xml \ --junit-xml=${TEST_ARTIFACTS}/media-downloader.xml \
tests/ tests/

View File

@ -25,7 +25,7 @@ A meta of last read/downloaded message is stored in the config file so that in s
### Support: ### Support:
| Category | Support | | Category | Support |
|--|--| |--|--|
|Language | `Python 3.6 ` and above| |Language | `Python 3.7 ` and above|
|Download media types| audio, document, photo, video, video_note, voice| |Download media types| audio, document, photo, video, video_note, voice|
### ToDo: ### ToDo:
@ -39,21 +39,21 @@ $ git clone https://github.com/Dineshkarthik/telegram_media_downloader.git
$ cd telegram_media_downloader $ cd telegram_media_downloader
$ make install $ make install
``` ```
For Windows which doesn't have `make` inbuilt For Windows which doesn't have `make` inbuilt
```sh ```sh
$ git clone https://github.com/Dineshkarthik/telegram_media_downloader.git $ git clone https://github.com/Dineshkarthik/telegram_media_downloader.git
$ cd telegram_media_downloader $ cd telegram_media_downloader
$ pip3 install -r requirements.txt $ pip3 install -r requirements.txt
``` ```
## Configuration ## Configuration
All the configurations are passed to the Telegram Media Downloader via `config.yaml` file. All the configurations are passed to the Telegram Media Downloader via `config.yaml` file.
**Getting your API Keys:** **Getting your API Keys:**
The very first step requires you to obtain a valid Telegram API key (API id/hash pair): The very first step requires you to obtain a valid Telegram API key (API id/hash pair):
1. Visit [https://my.telegram.org/apps](https://my.telegram.org/apps) and log in with your Telegram Account. 1. Visit [https://my.telegram.org/apps](https://my.telegram.org/apps) and log in with your Telegram Account.
2. Fill out the form to register a new Telegram application. 2. Fill out the form to register a new Telegram application.
3. Done! The API key consists of two parts: **api_id** and **api_hash**. 3. Done! The API key consists of two parts: **api_id** and **api_hash**.
@ -123,18 +123,17 @@ All the downloaded media will be stored inside respective direcotry named in t
| voice_note | path/to/project/voice_note | | voice_note | path/to/project/voice_note |
## Proxy ## Proxy
`Socks5` proxy is supported in this project currently. To use it, simply create a `config.ini` file in the path of this project, and edit it with your proxy server info as follow: `socks4, socks5, http` proxies are supported in this project currently. To use it, add the following to the bottom of your `config.yaml` file
```ini ```yaml
[proxy] proxy:
enabled = True scheme: socks5
hostname = 127.0.0.1 hostname: 11.22.33.44
port = 1080 port: 1234
username = username: your_username
password = password: your_password
``` ```
If your proxy doesnt require authorization you can omit username and password. Then the proxy will automatically be enabled.
Then the proxy will automatically be enabled.
## Contributing ## Contributing
### Contributing Guidelines ### Contributing Guidelines

View File

@ -10,4 +10,4 @@ coverage:
patch: no patch: no
comment: comment:
require_changes: true require_changes: true

View File

@ -1,6 +1,9 @@
black==22.3.0
isort==5.10.1
mock==4.0.3 mock==4.0.3
mypy==0.942 mypy==0.961
pylint==2.13.7 pre-commit==2.20.0
pylint==2.14.5
pytest==7.0.1 pytest==7.0.1
pytest-cov==3.0.0 pytest-cov==3.0.0
types-PyYAML==6.0.7 types-PyYAML==6.0.10

View File

@ -47,9 +47,7 @@ def update_config(config: dict):
logger.info("Updated last read message_id to config file") logger.info("Updated last read message_id to config file")
def _can_download( def _can_download(_type: str, file_formats: dict, file_format: Optional[str]) -> bool:
_type: str, file_formats: dict, file_format: Optional[str]
) -> bool:
""" """
Check if the given file format can be downloaded. Check if the given file format can be downloaded.
@ -176,7 +174,7 @@ async def download_media(
for retry in range(3): for retry in range(3):
try: try:
if message.media is None: if message.media is None:
return message.message_id return message.id
for _type in media_types: for _type in media_types:
_media = getattr(message, _type, None) _media = getattr(message, _type, None)
if _media is None: if _media is None:
@ -196,48 +194,48 @@ async def download_media(
) )
if download_path: if download_path:
logger.info("Media downloaded - %s", download_path) logger.info("Media downloaded - %s", download_path)
DOWNLOADED_IDS.append(message.message_id) DOWNLOADED_IDS.append(message.id)
break break
except pyrogram.errors.exceptions.bad_request_400.BadRequest: except pyrogram.errors.exceptions.bad_request_400.BadRequest:
logger.warning( logger.warning(
"Message[%d]: file reference expired, refetching...", "Message[%d]: file reference expired, refetching...",
message.message_id, message.id,
) )
message = await client.get_messages( # type: ignore message = await client.get_messages( # type: ignore
chat_id=message.chat.id, # type: ignore chat_id=message.chat.id, # type: ignore
message_ids=message.message_id, message_ids=message.id,
) )
if retry == 2: if retry == 2:
# pylint: disable = C0301 # pylint: disable = C0301
logger.error( logger.error(
"Message[%d]: file reference expired for 3 retries, download skipped.", "Message[%d]: file reference expired for 3 retries, download skipped.",
message.message_id, message.id,
) )
FAILED_IDS.append(message.message_id) FAILED_IDS.append(message.id)
except TypeError: except TypeError:
# pylint: disable = C0301 # pylint: disable = C0301
logger.warning( logger.warning(
"Timeout Error occurred when downloading Message[%d], retrying after 5 seconds", "Timeout Error occurred when downloading Message[%d], retrying after 5 seconds",
message.message_id, message.id,
) )
await asyncio.sleep(5) await asyncio.sleep(5)
if retry == 2: if retry == 2:
logger.error( logger.error(
"Message[%d]: Timing out after 3 reties, download skipped.", "Message[%d]: Timing out after 3 reties, download skipped.",
message.message_id, message.id,
) )
FAILED_IDS.append(message.message_id) FAILED_IDS.append(message.id)
except Exception as e: except Exception as e:
# pylint: disable = C0301 # pylint: disable = C0301
logger.error( logger.error(
"Message[%d]: could not be downloaded due to following exception:\n[%s].", "Message[%d]: could not be downloaded due to following exception:\n[%s].",
message.message_id, message.id,
e, e,
exc_info=True, exc_info=True,
) )
FAILED_IDS.append(message.message_id) FAILED_IDS.append(message.id)
break break
return message.message_id return message.id
async def process_messages( async def process_messages(
@ -281,7 +279,7 @@ async def process_messages(
] ]
) )
last_message_id = max(message_ids) last_message_id: int = max(message_ids)
return last_message_id return last_message_id
@ -312,10 +310,9 @@ async def begin_import(config: dict, pagination_limit: int) -> dict:
) )
await client.start() await client.start()
last_read_message_id: int = config["last_read_message_id"] last_read_message_id: int = config["last_read_message_id"]
messages_iter = client.iter_history( messages_iter = client.get_chat_history(
config["chat_id"], config["chat_id"],
offset_id=last_read_message_id, offset_id=last_read_message_id,
reverse=True,
) )
messages_list: list = [] messages_list: list = []
pagination_count: int = 0 pagination_count: int = 0

8
mypy.ini Normal file
View File

@ -0,0 +1,8 @@
[mypy]
warn_return_any = True
[mypy-yaml.*]
ignore_missing_imports = True
[mypy-tests.*]
ignore_errors = True

View File

@ -251,7 +251,7 @@ indent-after-paren=4
indent-string=' ' indent-string=' '
# Maximum number of characters on a single line. # Maximum number of characters on a single line.
max-line-length=80 max-line-length=90
# Maximum number of lines in a module. # Maximum number of lines in a module.
max-module-lines= max-module-lines=

View File

@ -1,22 +0,0 @@
[tool.black]
line-length = 79
target-version = ['py36', 'py37', 'py38']
exclude = '''
(
/(
\.eggs # exclude a few common directories in the
| \.git # root of the project
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| bin
| lib
| include
)/
)
'''

View File

@ -1,4 +1,4 @@
Pyrogram==1.4.12 Pyrogram==2.0.33
PyYAML==6.0 PyYAML==6.0
rich==12.1.0 rich==12.5.1
TgCrypto==1.2.3 TgCrypto==1.2.3

View File

@ -12,7 +12,7 @@ setup(
download_url="https://github.com/Dineshkarthik/telegram_media_downloader/releases/latest", download_url="https://github.com/Dineshkarthik/telegram_media_downloader/releases/latest",
py_modules=["media_downloader"], py_modules=["media_downloader"],
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 5 - Production/Stable",
"Environment :: Console", "Environment :: Console",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Intended Audience :: Developers", "Intended Audience :: Developers",
@ -22,10 +22,11 @@ setup(
"Natural Language :: English", "Natural Language :: English",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Internet", "Topic :: Internet",
"Topic :: Communications", "Topic :: Communications",
"Topic :: Communications :: Chat", "Topic :: Communications :: Chat",
@ -37,5 +38,5 @@ setup(
"Community": "https://t.me/tgmdnews", "Community": "https://t.me/tgmdnews",
"Source": "https://github.com/Dineshkarthik/telegram_media_downloader", "Source": "https://github.com/Dineshkarthik/telegram_media_downloader",
}, },
python_requires=">=3.6", python_requires="~=3.7",
) )

View File

@ -1,24 +1,24 @@
"""Unittest module for media downloader.""" """Unittest module for media downloader."""
import os import asyncio
import copy import copy
import logging import logging
import os
import platform import platform
import unittest import unittest
import asyncio
import mock import mock
import pyrogram import pyrogram
import pytest import pytest
from media_downloader import ( from media_downloader import (
_get_media_meta,
_can_download, _can_download,
_get_media_meta,
_is_exist, _is_exist,
download_media,
update_config,
begin_import, begin_import,
process_messages, download_media,
main, main,
process_messages,
update_config,
) )
MOCK_DIR: str = "/root/project" MOCK_DIR: str = "/root/project"
@ -53,7 +53,7 @@ class Chat:
class MockMessage: class MockMessage:
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.message_id = kwargs.get("id") self.id = kwargs.get("id")
self.media = kwargs.get("media") self.media = kwargs.get("media")
self.audio = kwargs.get("audio", None) self.audio = kwargs.get("audio", None)
self.document = kwargs.get("document", None) self.document = kwargs.get("document", None)
@ -134,9 +134,7 @@ async def mock_process_message(*args, **kwargs):
async def async_process_messages(client, messages, media_types, file_formats): async def async_process_messages(client, messages, media_types, file_formats):
result = await process_messages( result = await process_messages(client, messages, media_types, file_formats)
client, messages, media_types, file_formats
)
return result return result
@ -153,7 +151,7 @@ class MockClient:
async def stop(self): async def stop(self):
pass pass
async def iter_history(self, *args, **kwargs): async def get_chat_history(self, *args, **kwargs):
items = [ items = [
MockMessage( MockMessage(
id=1213, id=1213,
@ -219,11 +217,11 @@ class MockClient:
async def download_media(self, *args, **kwargs): async def download_media(self, *args, **kwargs):
mock_message = args[0] mock_message = args[0]
if mock_message.message_id in [7, 8]: if mock_message.id in [7, 8]:
raise pyrogram.errors.exceptions.bad_request_400.BadRequest raise pyrogram.errors.exceptions.bad_request_400.BadRequest
elif mock_message.message_id == 9: elif mock_message.id == 9:
raise pyrogram.errors.exceptions.unauthorized_401.Unauthorized raise pyrogram.errors.exceptions.unauthorized_401.Unauthorized
elif mock_message.message_id == 11: elif mock_message.id == 11:
raise TypeError raise TypeError
return kwargs["file_name"] return kwargs["file_name"]
@ -289,9 +287,7 @@ class MediaDownloaderTestCase(unittest.TestCase):
) )
self.assertEqual( self.assertEqual(
( (
platform_generic_path( platform_generic_path("/root/project/document/sample_document.pdf"),
"/root/project/document/sample_document.pdf"
),
"pdf", "pdf",
), ),
result, result,
@ -495,9 +491,7 @@ class MediaDownloaderTestCase(unittest.TestCase):
} }
update_config(conf) update_config(conf)
mock_open.assert_called_with("config.yaml", "w") mock_open.assert_called_with("config.yaml", "w")
mock_yaml.dump.assert_called_with( mock_yaml.dump.assert_called_with(conf, mock.ANY, default_flow_style=False)
conf, mock.ANY, default_flow_style=False
)
@mock.patch("media_downloader.update_config") @mock.patch("media_downloader.update_config")
@mock.patch("media_downloader.pyrogram.Client", new=MockClient) @mock.patch("media_downloader.pyrogram.Client", new=MockClient)

View File

@ -27,7 +27,7 @@ class FileManagementTestCase(unittest.TestCase):
result = get_next_name(self.test_file) result = get_next_name(self.test_file)
excepted_result = os.path.join(self.this_dir, "file-test-copy3.txt") excepted_result = os.path.join(self.this_dir, "file-test-copy3.txt")
self.assertEqual(result, excepted_result) self.assertEqual(result, excepted_result)
def test_manage_duplicate_file(self): def test_manage_duplicate_file(self):
result = manage_duplicate_file(self.test_file_copy_2) result = manage_duplicate_file(self.test_file_copy_2)
self.assertEqual(result, self.test_file_copy_1) self.assertEqual(result, self.test_file_copy_1)

View File

@ -27,4 +27,4 @@ class MetaTestCase(unittest.TestCase):
self.assertEqual(result1, False) self.assertEqual(result1, False)
result2 = LogFilter().filter(MockLog(funcName="Synced")) result2 = LogFilter().filter(MockLog(funcName="Synced"))
self.assertEqual(result2, True) self.assertEqual(result2, True)

View File

@ -1,7 +1,5 @@
"""Init namespace""" """Init namespace"""
__version__ = "1.5.1" __version__ = "2.0.0"
__license__ = "MIT License" __license__ = "MIT License"
__copyright__ = ( __copyright__ = "Copyright (C) 2019 Dineshkarthik <https://github.com/Dineshkarthik>"
"Copyright (C) 2019 Dineshkarthik <https://github.com/Dineshkarthik>"
)

View File

@ -6,9 +6,7 @@ from rich.console import Console
from . import __copyright__, __license__, __version__ from . import __copyright__, __license__, __version__
APP_VERSION = f"Telegram Media Downloader {__version__}" APP_VERSION = f"Telegram Media Downloader {__version__}"
DEVICE_MODEL = ( DEVICE_MODEL = f"{platform.python_implementation()} {platform.python_version()}"
f"{platform.python_implementation()} {platform.python_version()}"
)
SYSTEM_VERSION = f"{platform.system()} {platform.release()}" SYSTEM_VERSION = f"{platform.system()} {platform.release()}"
LANG_CODE = "en" LANG_CODE = "en"

View File

@ -7,6 +7,7 @@ from rich.markdown import Markdown
from . import __version__ from . import __version__
# pylint: disable = C0301 # pylint: disable = C0301
def check_for_updates() -> None: def check_for_updates() -> None:
"""Checks for new releases. """Checks for new releases.