├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── bookdl ├── __init__.py ├── __main__.py ├── common.py ├── database │ ├── __init__.py │ ├── files.py │ └── users.py ├── helpers │ ├── __init__.py │ ├── convert.py │ ├── downloader.py │ └── uploader.py ├── telegram │ ├── __init__.py │ ├── plugins │ │ ├── common.py │ │ ├── download.py │ │ └── file_query.py │ └── utils │ │ └── filters.py └── working_dir │ └── config.ini.sample ├── docker ├── README.md ├── docker-compose.yml └── docker-config.ini.sample ├── requirements.txt └── runtime.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | **/.classpath 3 | **/.dockerignore 4 | **/.env 5 | **/.git 6 | **/.gitignore 7 | **/.project 8 | **/.settings 9 | **/.toolstarget 10 | **/.vs 11 | **/.vscode 12 | **/*.*proj.user 13 | **/*.dbmdl 14 | **/*.jfm 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/compose* 19 | **/Dockerfile* 20 | **/node_modules 21 | **/npm-debug.log 22 | **/obj 23 | **/secrets.dev.yaml 24 | **/values.dev.yaml 25 | README.md 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | /venv 3 | *.ini 4 | *.cfg 5 | *.session 6 | *.session-journal 7 | **/__pycache__/ 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # For more information, please refer to https://aka.ms/vscode-docker-python 2 | FROM python:3.10.4-slim 3 | 4 | # Keeps Python from generating .pyc files in the container 5 | ENV PYTHONDONTWRITEBYTECODE=1 6 | 7 | # Turns off buffering for easier container logging 8 | ENV PYTHONUNBUFFERED=1 9 | 10 | WORKDIR /app 11 | COPY . /app 12 | 13 | # Install pip requirements 14 | RUN python -m pip install -r requirements.txt 15 | 16 | # Creates a non-root user with an explicit UID and adds permission to access the /app folder 17 | # For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers 18 | RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app 19 | USER appuser 20 | 21 | CMD ["python", "-m", "bookdl"] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Samfun75 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python -m bookdl -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bookdl Bot 2 | 3 | This is a working example to integrate [libgenesis](https://github.com/Samfun75/libgenesis) (an api for Libgen.rs) with a telegram bot. 4 | 5 | ## Requirments 6 | 7 | - libgenesis [View](https://github.com/Samfun75/libgenesis) 8 | - Pyrogram [View](https://github.com/pyrogram/pyrogram) 9 | - Pymongo 10 | 11 | ## Installation 12 | 13 | ### Docker 14 | 15 | Guide for installation using docker: 16 | 17 | Docker Instructions 18 | ### Heroku 19 | 20 | Use the below button to deploy. 21 | 22 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/Samfun75/BookdlBot) 23 | 24 | ### Other systems 25 | 26 | Clone repository 27 | 28 | ```bash 29 | git clone https://github.com/Samfun75/BookdlBot 30 | ``` 31 | 32 | Change to repository directory 33 | 34 | ```bash 35 | cd BookdlBot 36 | ``` 37 | 38 | Install requirements and dependencies 39 | 40 | ```python 41 | pip3 install -r requirements.txt 42 | ``` 43 | 44 | Create a new `config.ini` using the sample available at `bookdl/working_dir/config.ini.sample` in `bookdl/working_dir/`. 45 | 46 | ```ini 47 | # Here is a sample of config file and what it should include: 48 | 49 | # More info on API_ID and API_HASH can be found here: https://docs.pyrogram.org/intro/setup#api-keys 50 | 51 | [pyrogram] 52 | api_id = 1234567 53 | api_hash = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 54 | 55 | # More info on Bot API Key/token can be found here: https://core.telegram.org/bots#6-botfather 56 | 57 | [bot-configuration] 58 | bot_token = 123456789:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 59 | session = BookdlBot 60 | dustbin = -100xxxxxxxxx # Used to store uploaded book. id of a channel where the bot is admin 61 | allowed_users = [123456789, 987654321] # Telegram id of users allowed to use the bot. If the bot is open to all put empty array like this [] 62 | 63 | # Mongodb Credentials 64 | 65 | [database] 66 | db_host = xxxxxxxxxx.xxxxx.mongodb.net or localhost # In this section db_host is the address of the machine where the MongoDB is running 67 | db_username = username 68 | db_password = password 69 | db_name = BookdlBot 70 | db_type = Mongo_Atlas (or Mongo_Community) 71 | 72 | # ConvertAPI secret 73 | 74 | [convert] 75 | convert_api = secretkey 76 | ``` 77 | 78 | Run bot with 79 | 80 | `python -m bookdl` 81 | 82 | stop with CTRL+C 83 | 84 | ## Usage 85 | 86 | - Send /start and press Search 🔍 and on the open inline query type the book name 87 | - Send a book's MD5 like 'MD5:a382109f7fdde3be5b2cb4f82d97443b' 88 | - Send a book link from 'library.lol, libgen.lc, libgen.gs or b-ok.cc' 89 | 90 | After performing one of the above seteps a book detail with 'Download' button is returned and press the button to download and upload the book to telegram. 91 | 92 | For books other that PDF, a 🔃 Convert to PDF button is included and it uses converapi to convert the ebook to pdf 93 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BookdlBot", 3 | "description": "https://telegram.dog/BookdlBot", 4 | "keywords": [ 5 | "telegram", 6 | "best", 7 | "book", 8 | "downloader", 9 | "uploader" 10 | ], 11 | "success_url": "https://github.com/Samfun75/Bookdlbot", 12 | "website": "https://github.com/Samfun75/Bookdlbot", 13 | "repository": "https://github.com/Samfun75/Bookdlbot", 14 | "env": { 15 | "ENV": { 16 | "description": "Setting this to ANYTHING will enable VARs when in ENV mode", 17 | "value": "ANYTHING", 18 | "required": true 19 | }, 20 | "TG_BOT_TOKEN": { 21 | "description": "Your bot token, as a string.", 22 | "required": true 23 | }, 24 | "TG_API_ID": { 25 | "description": "Get this value from https://my.telegram.org", 26 | "required": true 27 | }, 28 | "TG_API_HASH": { 29 | "description": "Get this value from https://my.telegram.org", 30 | "required": true 31 | }, 32 | "TG_DUSTBIN_CHAT": { 33 | "description": "Watch this video to understand what the dustbin is: https://youtu.be/vgzMacnI5Z8", 34 | "required": true 35 | }, 36 | "ALLOWED_USERS": { 37 | "description": "A list of users (user ids) allowed to use the bot. Please enter in the format [123123123, 321321321]", 38 | "required": true 39 | }, 40 | "DATABASE_DB_HOST": { 41 | "description": "Watch this video to understand what database is: https://youtu.be/vgzMacnI5Z8", 42 | "required": true 43 | }, 44 | "DATABASE_DB_USERNAME": { 45 | "description": "Watch this video to understand what database is: https://youtu.be/vgzMacnI5Z8", 46 | "required": true 47 | }, 48 | "DATABASE_DB_PASSWORD": { 49 | "description": "Watch this video to understand what database is: https://youtu.be/vgzMacnI5Z8", 50 | "required": true 51 | }, 52 | "DATABASE_DB_NAME": { 53 | "description": "Watch this video to understand what database is: https://youtu.be/vgzMacnI5Z8", 54 | "required": true 55 | }, 56 | "DATABASE_DB_TYPE": { 57 | "description": "The type of Mongodb you are using. Enter 'Mongo_Atlas' or 'Mongo_Community'", 58 | "required": true 59 | }, 60 | "CONVERT_API": { 61 | "description": "ConvertAPI secret key, used to convert books to PDF", 62 | "required": true 63 | } 64 | }, 65 | "buildpacks": [ 66 | { 67 | "url": "heroku/python" 68 | } 69 | ], 70 | "formation": { 71 | "worker": { 72 | "quantity": 1, 73 | "size": "free" 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /bookdl/__init__.py: -------------------------------------------------------------------------------- 1 | """Basic console logging for the application.""" 2 | import logging 3 | 4 | logging.basicConfig( 5 | level=logging.INFO, 6 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 7 | ) 8 | 9 | logging.getLogger(__name__) 10 | -------------------------------------------------------------------------------- /bookdl/__main__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | from pyrogram import idle 4 | from bookdl.telegram import BookDLBot 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | async def main(): 10 | await BookDLBot.start() 11 | await idle() 12 | 13 | 14 | if __name__ == "__main__": 15 | BookDLBot.run(main()) 16 | logger.info("Services Terminated!") 17 | -------------------------------------------------------------------------------- /bookdl/common.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ast 3 | from pathlib import Path 4 | import configparser 5 | 6 | 7 | class Common: 8 | def __init__(self): 9 | """Common: are commonly shared variables across the application that is loaded from the config file or env.""" 10 | self.working_dir = Path('bookdl/working_dir') 11 | 12 | self.is_env = bool(os.environ.get("ENV", None)) 13 | if self.is_env: 14 | self.tg_api_id = int(os.environ.get("TG_API_ID")) 15 | self.tg_api_hash = os.environ.get("TG_API_HASH") 16 | self.session_name = "BookdlBot" 17 | self.bot_api_token = os.environ.get("TG_BOT_TOKEN") 18 | self.bot_dustbin = int(os.environ.get("TG_DUSTBIN_CHAT", "-100")) 19 | self.allowed_users = ast.literal_eval( 20 | os.environ.get("ALLOWED_USERS", '[]')) 21 | self.convert_api = os.environ.get("CONVERT_API", None) 22 | self.db_host = os.environ.get("DATABASE_DB_HOST", None) 23 | self.db_username = os.environ.get("DATABASE_DB_USERNAME", None) 24 | self.db_password = os.environ.get("DATABASE_DB_PASSWORD", None) 25 | self.db_name = os.environ.get("DATABASE_DB_NAME", "BookdlBot") 26 | if os.environ.get("DATABASE_DB_TYPE", 27 | None).lower().split('_')[1] == 'community': 28 | self.db_type = 'mongodb' 29 | else: 30 | self.db_type = 'mongodb+srv' 31 | else: 32 | self.app_config = configparser.ConfigParser() 33 | 34 | self.app_config_file = "bookdl/working_dir/config.ini" 35 | self.app_config.read(self.app_config_file) 36 | 37 | self.tg_api_id = int(self.app_config.get("pyrogram", "api_id")) 38 | self.tg_api_hash = self.app_config.get("pyrogram", "api_hash") 39 | self.session_name = self.app_config.get("bot-configuration", 40 | "session") 41 | self.bot_api_token = self.app_config.get("bot-configuration", 42 | "bot_token") 43 | self.bot_dustbin = int( 44 | self.app_config.get("bot-configuration", "dustbin")) 45 | self.allowed_users = ast.literal_eval( 46 | self.app_config.get("bot-configuration", 47 | "allowed_users", 48 | fallback='[]')) 49 | self.convert_api = self.app_config.get("convert", 50 | "convert_api", 51 | fallback=None) 52 | self.db_host = self.app_config.get("database", 53 | "db_host", 54 | fallback=None) 55 | self.db_username = self.app_config.get("database", 56 | "db_username", 57 | fallback=None) 58 | self.db_password = self.app_config.get("database", 59 | "db_password", 60 | fallback=None) 61 | self.db_name = self.app_config.get("database", 62 | "db_name", 63 | fallback="BookdlBot") 64 | if self.app_config.get( 65 | "database", 66 | "db_type").lower().split('_')[1] == 'community': 67 | self.db_type = 'mongodb' 68 | else: 69 | self.db_type = 'mongodb+srv' 70 | -------------------------------------------------------------------------------- /bookdl/database/__init__.py: -------------------------------------------------------------------------------- 1 | """Bookdl is the mongo DB connection for the application.""" 2 | 3 | import sys 4 | import pymongo 5 | import logging 6 | from bookdl.common import Common 7 | from urllib.parse import quote 8 | 9 | 10 | class BookdlDB: 11 | """Database init""" 12 | 13 | def __init__(self): 14 | 15 | if Common().db_username is not None and Common().db_password is not None and Common().db_host is not None: 16 | connection_string = f"{Common().db_type}://{Common().db_username}:{quote(Common().db_password)}@{Common().db_host}/?retryWrites=true&w=majority" 17 | 18 | self.db_client = pymongo.MongoClient(connection_string) 19 | self.db = self.db_client[Common().db_name] 20 | else: 21 | logging.info( 22 | "No Database Credentials or Database host Detected. \n Bot can't work without DB \n Exiting Bot...") 23 | sys.exit() 24 | -------------------------------------------------------------------------------- /bookdl/database/files.py: -------------------------------------------------------------------------------- 1 | import re 2 | from bookdl.database import BookdlDB 3 | from bson.objectid import ObjectId 4 | 5 | 6 | class BookdlFiles: 7 | def __init__(self): 8 | """ 9 | MegaFiles is the mongo collection that holds the documents for the files that are uploaded via the bot. 10 | 11 | Functions: 12 | insert_new_files: save new documents to the collection that contains details of the new files that are 13 | uploaded. 14 | count_files_by_url: count and returns the number of documents for the file with the same url. 15 | get_files_by_url: returns the documents for the files with a given url. 16 | get_file_by_file_id: returns the document for the file with the given telegram file_id. 17 | get_file_by_file_name: returns the documents for the files with the given file name. 18 | """ 19 | self.files_collection = BookdlDB().db['Files'] 20 | 21 | async def insert_new_files(self, title: str, file_name: str, msg_id: int, 22 | chat_id: int, md5: str, file_type: str, 23 | coverurl: str, file_id: str): 24 | self.files_collection.insert_one({ 25 | "title": title, 26 | "file_name": file_name, 27 | "msg_id": msg_id, 28 | "chat_id": chat_id, 29 | "md5": md5, 30 | "file_type": file_type, 31 | "coverurl": coverurl, 32 | "file_id": file_id 33 | }) 34 | 35 | async def count_files_by_md5(self, md5: str): 36 | return self.files_collection.count_documents({"md5": md5}) 37 | 38 | async def get_file_by_md5(self, md5: str, typ: str): 39 | return self.files_collection.find_one({"md5": md5, "file_type": typ}) 40 | 41 | async def get_file_by_mongo_id(self, file_id: str): 42 | return self.files_collection.find_one({"_id": ObjectId(file_id)}) 43 | 44 | async def get_file_by_name(self, file_name: str, row_limit: int): 45 | return self.files_collection.find({ 46 | "title": 47 | re.compile(file_name, re.IGNORECASE) 48 | }).limit(row_limit) 49 | -------------------------------------------------------------------------------- /bookdl/database/users.py: -------------------------------------------------------------------------------- 1 | from bookdl.database import BookdlDB 2 | 3 | 4 | class BookdlUsers: 5 | def __init__(self): 6 | """ 7 | BookdlUsers is the mongo collection for the documents that holds the details of the users. 8 | 9 | Functions: 10 | insert_user: insert new documents, that contains the details of the new users who started using the bot. 11 | 12 | get_user: return the document that contains the user_id for the the given telegram user id. 13 | 14 | """ 15 | self.user_collection = BookdlDB().db["Users"] 16 | 17 | async def insert_user(self, user_id: int): 18 | if self.user_collection.count_documents({"user_id": user_id}) > 0: 19 | return False 20 | else: 21 | self.user_collection.insert_one( 22 | { 23 | "user_id": user_id, 24 | "downloaded": [], 25 | } 26 | ) 27 | 28 | async def get_user(self, user_id: int): 29 | return self.user_collection.find_one({"user_id": user_id}) 30 | -------------------------------------------------------------------------------- /bookdl/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import asyncio 3 | import logging 4 | from typing import Tuple 5 | from libgenesis import Libgen 6 | from libgenesis.utils import Util as libgenUtil 7 | import humanfriendly as size 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Util: 13 | 14 | @staticmethod 15 | async def get_md5(link: str) -> str: 16 | regex_md5 = re.compile( 17 | r'(?<=/main/)([\w-]+)|(?<=md5=)([\w-]+)|(?<=/md5/)([\w-]+)') 18 | match = re.search(regex_md5, link) 19 | if match is not None: 20 | for md5 in match.groups(): 21 | if md5 is not None: 22 | return md5 23 | return None 24 | 25 | @staticmethod 26 | async def get_detail(md5: str, 27 | return_fields: list = [], 28 | retry: int = 0) -> Tuple[str, dict]: 29 | try: 30 | result = await Libgen().search( 31 | query=md5, 32 | search_field='md5', 33 | return_fields=[ 34 | 'title', 'author', 'publisher', 'year', 'language', 35 | 'volumeinfo', 'filesize', 'extension', 'timeadded', 36 | 'timelastmodified', 'coverurl' 37 | ] if not return_fields else return_fields) 38 | book_id = list(result.keys())[0] 39 | return book_id, result[book_id] 40 | except ConnectionError as e: 41 | if str(e).find('max_user_connections') != -1 and retry <= 3: 42 | logger.info(f'Retry {retry + 1}') 43 | await asyncio.sleep(2) 44 | return await Util().get_detail(md5, 45 | return_fields, 46 | retry=retry + 1) 47 | else: 48 | await libgenUtil.raise_error(500, str(e)) 49 | 50 | @staticmethod 51 | async def get_formatted(detail: dict) -> str: 52 | formated = '' 53 | formated += ('Title: **' + str(detail['title']) + '**\n') if detail['title'] else '' 54 | formated += ('Author: **' + str(detail['author']) + '**\n') if detail['author'] else '' 55 | formated += ('Volume: **' + str(detail['volumeinfo']) + '**\n') if detail['volumeinfo'] else '' 56 | formated += ('Publisher: **' + str(detail['publisher']) + '**\n') if detail['publisher'] else '' 57 | formated += ('Year: **' + str(detail['year']) + '**\n') if detail['year'] else '' 58 | formated += ('Language: **' + str(detail['language']) + '**\n') if detail['language'] else '' 59 | formated += (f"Size: **{size.format_size(int(detail['filesize']), binary=True)}**\n") if detail['filesize'] else '' 60 | formated += ('Extention: **' + str(detail['extension']) + '**\n') if detail['extension'] else '' 61 | formated += ('Added Time: **' + str(detail['timeadded']) + '**\n') if detail['timeadded'] else '' 62 | formated += ('Last Modified Time: **' + \ 63 | str(detail['timelastmodified']) + '**\n') if detail['timelastmodified'] else '' 64 | return formated 65 | -------------------------------------------------------------------------------- /bookdl/helpers/convert.py: -------------------------------------------------------------------------------- 1 | import time 2 | import shutil 3 | import asyncio 4 | import logging 5 | import aiohttp 6 | import aiofiles 7 | import convertapi 8 | from pathlib import Path 9 | import humanfriendly as size 10 | from bookdl.helpers import Util 11 | from bookdl.common import Common 12 | from pyrogram.types import Message 13 | from bookdl.telegram import BookDLBot 14 | from sanitize_filename import sanitize 15 | from convertapi.exceptions import ApiError 16 | from bookdl.helpers.uploader import Uploader 17 | from bookdl.database.files import BookdlFiles 18 | from libgenesis.download import LibgenDownload 19 | from pyrogram.errors import MessageNotModified, FloodWait 20 | 21 | logger = logging.getLogger(__name__) 22 | convert_status = {} 23 | 24 | 25 | class Convert: 26 | def __init__(self): 27 | convertapi.api_secret = Common().convert_api 28 | 29 | async def convert_to_pdf(self, md5: str, msg: Message): 30 | ack_msg = await msg.reply_text('About to convert book to PDF...', 31 | quote=True) 32 | book = await BookdlFiles().get_file_by_md5(md5=md5, 33 | typ='application/pdf') 34 | if book: 35 | await BookDLBot.copy_message(chat_id=msg.chat.id, 36 | from_chat_id=book['chat_id'], 37 | message_id=book['msg_id']) 38 | await ack_msg.delete() 39 | return 40 | _, detail = await Util().get_detail( 41 | md5, return_fields=['mirrors', 'title', 'extension', 'coverurl']) 42 | 43 | temp_dir = Path.joinpath( 44 | Common().working_dir, 45 | Path(f'{ack_msg.chat.id}+{ack_msg.id}')) 46 | if not Path.is_dir(temp_dir): 47 | Path.mkdir(temp_dir) 48 | 49 | direct_links = await LibgenDownload().get_directlink( 50 | detail['mirrors']['main']) 51 | extension = detail['extension'] 52 | params = { 53 | 'File': direct_links[1], 54 | 'PdfVersion': '2.0', 55 | 'OpenZoom': '100', 56 | 'PdfTitle': '@SamfunBookdlbot - ' + detail['title'], 57 | 'RotatePage': 'ByPage' 58 | } 59 | stat_var = f"{ack_msg.chat.id}{ack_msg.id}" 60 | convert_status[stat_var] = {'Done': False} 61 | try: 62 | loop = asyncio.get_event_loop() 63 | convert_process = loop.run_in_executor(None, self.__convert, 64 | params, extension, stat_var) 65 | start_time = time.time() 66 | while True: 67 | if convert_status[stat_var]['Done']: 68 | break 69 | else: 70 | try: 71 | await ack_msg.edit_text( 72 | f'Convertion to PDF started... {int(time.time() - start_time)}' 73 | ) 74 | except MessageNotModified as e: 75 | logger.error(e) 76 | except FloodWait as e: 77 | logger.error(e) 78 | await asyncio.sleep(e.x) 79 | await asyncio.sleep(2) 80 | Result = await convert_process 81 | except ApiError as e: 82 | logger.error(e) 83 | await ack_msg.edit_text(e) 84 | shutil.rmtree(temp_dir) 85 | return 86 | 87 | file_path = Path.joinpath( 88 | temp_dir, 89 | Path('[@SamfunBookdlbot] ' + sanitize(detail['title']) + '.pdf')) 90 | detail[ 91 | 'cost'] = f'ConvertAPI Cost: **{Result.conversion_cost}** seconds.' 92 | await ack_msg.edit_text(f'About to download converted file...') 93 | try: 94 | async with aiohttp.ClientSession() as dl_ses: 95 | async with dl_ses.get(Result.file.url) as resp: 96 | total_size = int(Result.file.size) 97 | file_name = Result.file.filename 98 | 99 | async with aiofiles.open(file_path, mode="wb") as dl_file: 100 | current = 0 101 | logger.info(f'Starting download: {file_name}') 102 | start_time = time.time() 103 | async for chunk in resp.content.iter_chunked(1024 * 104 | 1024): 105 | await dl_file.write(chunk) 106 | current += len(chunk) 107 | if time.time() - start_time > 2: 108 | await ack_msg.edit_text( 109 | f'Downloading: **{detail["title"]}**\n' 110 | f"Status: **{size.format_size(current, binary=True)}** of **{size.format_size(total_size, binary=True)}**" 111 | ) 112 | start_time = time.time() 113 | except Exception as e: 114 | logger.exception(e) 115 | return None 116 | await Uploader().upload_book(file_path, ack_msg, md5, detail=detail) 117 | 118 | @staticmethod 119 | def __convert(params, extension, stat_var): 120 | convertapi.api_secret = Common().convert_api 121 | logger.info('Conversion Started...') 122 | try: 123 | result = convertapi.convert('pdf', 124 | params, 125 | from_format=extension, 126 | timeout=120) 127 | logger.info('Conversion Finished!') 128 | except ApiError as e: 129 | convert_status[stat_var]['Done'] = True 130 | logger.error('Conversion Failed!') 131 | raise e 132 | finally: 133 | convert_status[stat_var]['Done'] = True 134 | return result 135 | -------------------------------------------------------------------------------- /bookdl/helpers/downloader.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from pathlib import Path 4 | from ..helpers import Util 5 | import humanfriendly as size 6 | from libgenesis import Libgen 7 | from mimetypes import guess_type 8 | from bookdl.common import Common 9 | from pyrogram.types import Message 10 | from bookdl.telegram import BookDLBot 11 | from bookdl.helpers.uploader import Uploader 12 | from bookdl.database.files import BookdlFiles 13 | from sanitize_filename.sanitize_filename import sanitize 14 | from pyrogram.errors import MessageNotModified, FloodWait 15 | 16 | logger = logging.getLogger(__name__) 17 | status_progress = {} 18 | 19 | 20 | class Downloader: 21 | @staticmethod 22 | async def download_book(md5: str, msg: Message): 23 | ack_msg = await msg.reply_text('About to download book...', quote=True) 24 | _, detail = await Util().get_detail( 25 | md5, return_fields=['extension', 'title', 'coverurl']) 26 | typ, _ = guess_type(detail['title'] + '.' + detail['extension']) 27 | book = await BookdlFiles().get_file_by_md5(md5=md5, typ=typ) 28 | 29 | if book: 30 | await BookDLBot.copy_message(chat_id=msg.chat.id, 31 | from_chat_id=book['chat_id'], 32 | message_id=book['msg_id']) 33 | await ack_msg.delete() 34 | return 35 | link = f'http://library.lol/main/{md5}' 36 | temp_dir = Path.joinpath( 37 | Common().working_dir, 38 | Path(f'{ack_msg.chat.id}+{ack_msg.id}')) 39 | file = await Libgen().download( 40 | link, 41 | dest_folder=temp_dir, 42 | progress=Downloader().download_progress_hook, 43 | progress_args=[ 44 | ack_msg.chat.id, ack_msg.id, detail['title'] 45 | ]) 46 | file_path = Path.joinpath( 47 | temp_dir, 48 | Path('[@SamfunBookdlbot] ' + sanitize(detail['title']) + 49 | f'.{detail["extension"]}')) 50 | if Path.is_file(file): 51 | Path.rename(file, file_path) 52 | await Uploader().upload_book( 53 | file_path if Path.is_file(file_path) else file, 54 | ack_msg, 55 | md5, 56 | detail=detail) 57 | 58 | @staticmethod 59 | async def download_progress_hook(current, total, chat_id, message_id, 60 | title): 61 | try: 62 | await BookDLBot.edit_message_text( 63 | chat_id=chat_id, 64 | message_id=message_id, 65 | text=f"Downloading: **{title}**\n" 66 | f"Status: **{size.format_size(current, binary=True)}** of **{size.format_size(total, binary=True)}**" 67 | ) 68 | except MessageNotModified as e: 69 | logger.error(e) 70 | except FloodWait as e: 71 | logger.error(e) 72 | await asyncio.sleep(e.x) 73 | -------------------------------------------------------------------------------- /bookdl/helpers/uploader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import shutil 4 | import logging 5 | import asyncio 6 | import requests 7 | import aiofiles 8 | from pathlib import Path 9 | import humanfriendly as size 10 | from bookdl.helpers import Util 11 | from bookdl.common import Common 12 | from pyrogram.types import Message 13 | from bookdl.telegram import BookDLBot 14 | from bookdl.database.files import BookdlFiles 15 | from pyrogram.errors import FloodWait, MessageNotModified 16 | 17 | logger = logging.getLogger(__name__) 18 | status_progress = {} 19 | 20 | 21 | class Uploader: 22 | @staticmethod 23 | async def upload_book(file_path: Path, 24 | ack_msg: Message, 25 | md5: str, 26 | detail: dict = None): 27 | ack_msg = await ack_msg.edit_text('About to upload book...') 28 | _, detail = (None, detail) or await Util().get_detail( 29 | md5=md5, return_fields=['coverurl', 'title']) 30 | 31 | thumb = await Uploader().get_thumb(detail['coverurl'], ack_msg) 32 | status_progress[f"{ack_msg.chat.id}{ack_msg.id}"] = {} 33 | status_progress[f"{ack_msg.chat.id}{ack_msg.id}"][ 34 | "last_upload_updated"] = time.time() 35 | try: 36 | file_message = await ack_msg.reply_document( 37 | document=file_path, 38 | progress=Uploader().upload_progress_hook, 39 | progress_args=[ 40 | ack_msg.chat.id, ack_msg.id, file_path.name 41 | ], 42 | thumb=thumb, 43 | caption=file_path.name + 44 | (('\n' + detail['cost']) if 'cost' in detail.keys() else '')) 45 | await ack_msg.delete() 46 | await Uploader().send_file_to_dustbin(file_message, md5, detail) 47 | except FloodWait as e: 48 | logger.error(e) 49 | await asyncio.sleep(e.x) 50 | except Exception as e: 51 | logger.error(e) 52 | finally: 53 | if Path.is_dir(file_path.parent): 54 | shutil.rmtree(file_path.parent) 55 | 56 | @staticmethod 57 | async def send_file_to_dustbin(file_message: Message, md5: str, 58 | detail: dict): 59 | fd_msg = await file_message.copy(chat_id=Common().bot_dustbin) 60 | 61 | await BookdlFiles().insert_new_files( 62 | title=detail['title'], 63 | file_name=fd_msg.document.file_name, 64 | msg_id=fd_msg.id, 65 | chat_id=fd_msg.chat.id, 66 | md5=md5, 67 | file_type=fd_msg.document.mime_type, 68 | coverurl=detail['coverurl'] if detail['coverurl'] else '', 69 | file_id=fd_msg.document.file_id) 70 | 71 | @staticmethod 72 | async def upload_progress_hook(current, total, chat_id, message_id, 73 | file_name): 74 | if (time.time() - status_progress[f"{chat_id}{message_id}"] 75 | ["last_upload_updated"]) > 1: 76 | try: 77 | await BookDLBot.edit_message_text( 78 | chat_id=chat_id, 79 | message_id=message_id, 80 | text=f"Uploading: **{file_name}**\n" 81 | f"Status: **{size.format_size(current, binary=True)}** of **{size.format_size(total, binary=True)}**" 82 | ) 83 | except MessageNotModified as e: 84 | logger.error(e) 85 | except FloodWait as e: 86 | logger.error(e) 87 | await asyncio.sleep(e.x) 88 | 89 | status_progress[f"{chat_id}{message_id}"][ 90 | "last_upload_updated"] = time.time() 91 | 92 | @staticmethod 93 | async def get_thumb(url: str, ack_msg: Message): 94 | file_name = os.path.basename(url) 95 | thumb_file = Path.joinpath( 96 | Common().working_dir, 97 | Path(f'{ack_msg.chat.id}+{ack_msg.id}'), Path(file_name)) 98 | resp = requests.get(url, allow_redirects=True) 99 | async with aiofiles.open(thumb_file, mode='wb') as dl_file: 100 | await dl_file.write(resp.content) 101 | return thumb_file 102 | -------------------------------------------------------------------------------- /bookdl/telegram/__init__.py: -------------------------------------------------------------------------------- 1 | """BookDLBot Pyrogram Client.""" 2 | from pyrogram import Client 3 | from bookdl.common import Common 4 | 5 | 6 | BookDLBot = Client( 7 | name=Common().session_name, 8 | api_id=Common().tg_api_id, 9 | api_hash=Common().tg_api_hash, 10 | bot_token=Common().bot_api_token, 11 | workers=200, 12 | workdir=Common().working_dir, 13 | plugins=dict(root="bookdl/telegram/plugins"), 14 | app_version="2.0", 15 | device_model="BookdlBot", 16 | system_version="Ubuntu/Linux", 17 | in_memory=True 18 | ) 19 | -------------------------------------------------------------------------------- /bookdl/telegram/plugins/common.py: -------------------------------------------------------------------------------- 1 | from ...telegram import Common 2 | from bookdl.database.users import BookdlUsers 3 | from pyrogram import filters, emoji, Client 4 | from pyrogram.types import Message, InlineQuery, InlineKeyboardMarkup, InlineKeyboardButton 5 | 6 | 7 | @Client.on_message(filters.command("start", prefixes=["/"])) 8 | async def start_message_handler(c: Client, m: Message): 9 | await BookdlUsers().insert_user(m.from_user.id) 10 | await m.reply_text( 11 | text=f"Hello! My name is **Bookdl Bot** {emoji.BOOKS} \n" 12 | "I can help you download books try typing in inline mode", 13 | reply_markup=InlineKeyboardMarkup([ 14 | [ 15 | InlineKeyboardButton( 16 | f'Search {emoji.MAGNIFYING_GLASS_TILTED_LEFT}', 17 | switch_inline_query_current_chat="") 18 | ], 19 | [ 20 | InlineKeyboardButton( 21 | f'Search from downloaded {emoji.MAGNIFYING_GLASS_TILTED_LEFT}', 22 | switch_inline_query_current_chat="dl: ") 23 | ] 24 | ])) 25 | 26 | 27 | @Client.on_message(filters.command("help", prefixes=["/"])) 28 | async def help_message_handler(c: Client, m: Message): 29 | await m.reply_text( 30 | text="Hello! I'm **BookdlBot.**\n" 31 | "Original bot [SamfunBookdlBot](https://t.me/SamfunBookdlbot)\n" 32 | "Source [Bookdlbot](https://github.com/Samfun75/BookdlBot)\n\n" 33 | "Here are the commands you can use:\n" 34 | "/start : Start the bot.\n" 35 | "/help: Show this helpful message\n\n" 36 | "You can also download books by sending the link if you have a book link from the following sites\n" 37 | "library.lol, libgen.lc, libgen.gs or b-ok.cc\n\n" 38 | "Or you can send the book's md5 like so\n" 39 | "MD5:a382109f7fdde3be5b2cb4f82d97443b\n\n" 40 | "If you send a document(book) or audio(audiobook) to the dustbin channel, the bot " 41 | "will save the document or audio to the database and it is available for search.\n\n" 42 | "Conversion to PDF from other ebook types is done using ConvertAPI", 43 | disable_web_page_preview=True) 44 | 45 | 46 | @Client.on_message(group=-1) 47 | async def stop_user_from_doing_anything(_, message: Message): 48 | allowed_users = Common().allowed_users 49 | if allowed_users: 50 | if message.chat.id in allowed_users or message.chat.id == Common( 51 | ).bot_dustbin: 52 | message.continue_propagation() 53 | else: 54 | message.stop_propagation() 55 | else: 56 | message.continue_propagation() 57 | 58 | 59 | @Client.on_inline_query(group=-1) 60 | async def stop_user_from_doing_anything_inline(_, iq: InlineQuery): 61 | allowed_users = Common().allowed_users 62 | if allowed_users and iq.from_user: 63 | if iq.from_user.id not in allowed_users: 64 | iq.stop_propagation() 65 | else: 66 | iq.continue_propagation() 67 | else: 68 | if iq.from_user: 69 | iq.continue_propagation() 70 | else: 71 | iq.stop_propagation() 72 | -------------------------------------------------------------------------------- /bookdl/telegram/plugins/download.py: -------------------------------------------------------------------------------- 1 | import re 2 | import logging 3 | import tldextract 4 | from pyrogram import enums 5 | from ..utils import filters 6 | from bookdl.helpers import Util 7 | from pyrogram import emoji, Client 8 | from bookdl.helpers.convert import Convert 9 | from bookdl.database.users import BookdlUsers 10 | from bookdl.helpers.downloader import Downloader 11 | from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery 12 | 13 | logger = logging.getLogger(__name__) 14 | mirrors = ['library.lol', 'libgen.lc', 'libgen.gs', 'b-ok.cc'] 15 | 16 | 17 | @Client.on_message(filters.private & filters.text, group=0) 18 | async def new_message_dl_handler(c: Client, m: Message): 19 | await BookdlUsers().insert_user(m.from_user.id) 20 | 21 | if m.text.startswith('MD5:'): 22 | md5 = m.text.splitlines()[0].split(':')[1].strip() 23 | await book_process(m, md5) 24 | else: 25 | regex = re.compile( 26 | r'^(?:http|ftp)s?://' # http:// or https:// domain... 27 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' 28 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 29 | r'(?::\d+)?' # optional port 30 | r'(?:/?|[/?]\S+)$', 31 | re.IGNORECASE) 32 | 33 | td = tldextract.extract(m.text) 34 | domain = str(td.domain) + '.' + str(td.suffix) 35 | 36 | if re.match(regex, m.text) and domain in mirrors: 37 | md5 = await Util().get_md5(m.text) 38 | await book_process(m, md5) 39 | else: 40 | await m.reply_text( 41 | text=f'Sorry links from {domain} not supported.', quote=True) 42 | 43 | 44 | async def book_process(msg: Message, md5: str): 45 | _, detail = await Util().get_detail(md5) 46 | if detail: 47 | inline_buttons = [[ 48 | InlineKeyboardButton( 49 | text=f"{emoji.FLOPPY_DISK} Download Book", 50 | callback_data=f"download_{msg.chat.id}_{msg.id}_{md5}") 51 | ]] 52 | if detail['extension'] not in ['pdf', 'zip', 'rar']: 53 | inline_buttons.append([ 54 | InlineKeyboardButton( 55 | text=f"{emoji.CLOCKWISE_VERTICAL_ARROWS} Convert to PDF", 56 | callback_data=f"convert_{msg.chat.id}_{msg.id}_{md5}") 57 | ]) 58 | formated = await Util().get_formatted(detail) 59 | 60 | if detail['coverurl']: 61 | await msg.reply_photo( 62 | photo=detail['coverurl'], 63 | caption=f"**Book Detail**\n\n" 64 | f"{formated}", 65 | reply_markup=InlineKeyboardMarkup(inline_buttons), 66 | parse_mode=enums.ParseMode.MARKDOWN, 67 | quote=True) 68 | else: 69 | await msg.reply_text( 70 | text=f"**Book Detail**\n\n" 71 | f"{formated}", 72 | reply_markup=InlineKeyboardMarkup(inline_buttons), 73 | disable_web_page_preview=True, 74 | parse_mode=enums.ParseMode.MARKDOWN, 75 | quote=True) 76 | else: 77 | await msg.reply_text( 78 | text= 79 | "Couldn't get details for this Book. Sorry, Try searching in the bot or get another supported link or md5!", 80 | reply_markup=None, 81 | quote=True) 82 | 83 | 84 | @Client.on_callback_query(filters.callback_query("download"), group=1) 85 | async def callback_download_handler(c: Client, cb: CallbackQuery): 86 | params = cb.payload.split('_') 87 | 88 | cb_chat = int(params[0]) if len(params) > 0 else None 89 | cb_message_id = int(params[1]) if len(params) > 1 else None 90 | org_msg = await c.get_messages( 91 | cb_chat, cb_message_id) if cb_message_id is not None else None 92 | md5 = str(params[2]) if len(params) > 2 else None 93 | 94 | await cb.answer('Download starting...') 95 | await Downloader().download_book(md5, org_msg) 96 | 97 | 98 | @Client.on_callback_query(filters.callback_query("convert"), group=1) 99 | async def callback_convert_handler(c: Client, cb: CallbackQuery): 100 | params = cb.payload.split('_') 101 | 102 | cb_chat = int(params[0]) if len(params) > 0 else None 103 | cb_message_id = int(params[1]) if len(params) > 1 else None 104 | org_msg = await c.get_messages( 105 | cb_chat, cb_message_id) if cb_message_id is not None else None 106 | md5 = str(params[2]) if len(params) > 2 else None 107 | 108 | await cb.answer('Conversion starting...') 109 | await Convert().convert_to_pdf(md5, org_msg) 110 | -------------------------------------------------------------------------------- /bookdl/telegram/plugins/file_query.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pyrogram import emoji 3 | from pyrogram import Client 4 | from ..utils import filters 5 | from libgenesis import Libgen 6 | from ...telegram import Common 7 | from bookdl.database.files import BookdlFiles 8 | from pyrogram.types import Message, InlineQuery, InlineQueryResultArticle, InputTextMessageContent, \ 9 | InlineQueryResultCachedDocument 10 | 11 | 12 | @Client.on_inline_query() 13 | async def inline_query_handler(c: Client, iq: InlineQuery): 14 | q = iq.query.strip() 15 | res = [] 16 | if len(q) < 2: 17 | await iq.answer( 18 | results=[], 19 | cache_time=0, 20 | switch_pm_text='You must enter at least 2 characters to search', 21 | switch_pm_parameter="okay", 22 | ) 23 | return 24 | if q.startswith('dl:'): 25 | q = q.split(':')[1].strip() 26 | q_res_data = await BookdlFiles().get_file_by_name(q, 50) 27 | if q_res_data: 28 | for file in q_res_data: 29 | res.append( 30 | InlineQueryResultCachedDocument( 31 | id=str(file['_id']), 32 | document_file_id=file['file_id'], 33 | caption=file['title'], 34 | title=file['title'], 35 | description=f"File Name: {file['file_name']}\n" 36 | f"File Type: {file['file_type']}", 37 | )) 38 | else: 39 | if q: 40 | result = await Libgen(result_limit=50 41 | ).search(query=q, 42 | return_fields=[ 43 | 'title', 'pages', 'language', 44 | 'publisher', 'year', 'author', 45 | 'extension', 'coverurl', 46 | 'volumeinfo', 'mirrors', 'md5' 47 | ]) 48 | if result is not None: 49 | for item in result: 50 | res.append( 51 | InlineQueryResultArticle( 52 | title=result[item]['title'], 53 | description=f"Author: {result[item]['author']}\n" 54 | f"Volume: {result[item]['volumeinfo']} Year: {result[item]['year']} Pages: {result[item]['pages']}\n" 55 | f"Language: {result[item]['language']} Extension: {result[item]['extension']}\n" 56 | f"Publisher: {result[item]['publisher']}\n", 57 | thumb_url="https://cdn3.iconfinder.com/data/icons/" 58 | "education-vol-1-34/512/15_File_files_office-256.png" 59 | if result[item]['coverurl'] is None else 60 | result[item]['coverurl'], 61 | input_message_content=InputTextMessageContent( 62 | message_text=f"MD5: {result[item]['md5']}\n" 63 | f"Title: **{result[item]['title']}.**\n" 64 | f"Author: **{result[item]['author']}.**"), 65 | reply_markup=None)) 66 | 67 | if res: 68 | await iq.answer(results=res, cache_time=60, is_personal=False) 69 | else: 70 | await iq.answer( 71 | results=[], 72 | cache_time=0, 73 | switch_pm_text=f'{emoji.CROSS_MARK} No results for "{q}"', 74 | switch_pm_parameter="okay", 75 | ) 76 | 77 | 78 | @Client.on_message(filters.chat(Common().bot_dustbin) & 79 | (filters.document | filters.audio), 80 | group=0) 81 | async def manually_save_to_db(c: Client, m: Message): 82 | if m.audio is not None: 83 | await BookdlFiles().insert_new_files(title=m.audio.title, 84 | file_name=m.audio.file_name, 85 | msg_id=m.id, 86 | chat_id=m.chat.id, 87 | md5='md5', 88 | file_type=m.audio.mime_type, 89 | coverurl='', 90 | file_id=m.audio.file_id) 91 | ack_msg = await m.reply_text( 92 | text=f'**{m.audio.file_name}** has been added to DB.', quote=True) 93 | else: 94 | await BookdlFiles().insert_new_files(title=m.document.file_name, 95 | file_name=m.document.file_name, 96 | msg_id=m.id, 97 | chat_id=m.chat.id, 98 | md5='md5', 99 | file_type=m.document.mime_type, 100 | coverurl='', 101 | file_id=m.document.file_id) 102 | ack_msg = await m.reply_text( 103 | text=f'**{m.document.file_name}** has been added to DB.', 104 | quote=True) 105 | 106 | await asyncio.sleep(3) 107 | await ack_msg.delete() 108 | -------------------------------------------------------------------------------- /bookdl/telegram/utils/filters.py: -------------------------------------------------------------------------------- 1 | from pyrogram.filters import * 2 | from pyrogram.types import CallbackQuery 3 | 4 | 5 | def callback_query(args: str, payload=True): 6 | """ 7 | Accepts arg at all times. 8 | 9 | If payload is True, extract payload from callback and assign to callback.payload 10 | If payload is False, only check if callback exactly matches argument 11 | """ 12 | 13 | async def func(ftl, __, query: CallbackQuery): 14 | if payload: 15 | thing = r"{}\_" 16 | if re.search(re.compile(thing.format(ftl.data)), query.data): 17 | search = re.search(re.compile(r"\_{1}(.*)"), query.data) 18 | if search: 19 | query.payload = search.group(1) 20 | else: 21 | query.payload = None 22 | 23 | return True 24 | 25 | return False 26 | else: 27 | if ftl.data == query.data: 28 | return True 29 | 30 | return False 31 | 32 | return create(func, 'CustomCallbackQuery', data=args) 33 | -------------------------------------------------------------------------------- /bookdl/working_dir/config.ini.sample: -------------------------------------------------------------------------------- 1 | [pyrogram] 2 | api_id = 1234567 3 | api_hash = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 4 | 5 | [bot-configuration] 6 | bot_token = 123456789:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 7 | session = BookdlBot 8 | dustbin = -100xxxxxxxxx 9 | allowed_users = [123456789, 987654321] 10 | 11 | [database] 12 | db_host = xxxxxxxxxx.xxxxx.mongodb.net or whatever 13 | db_username = username 14 | db_password = password 15 | db_name = BookdlBot 16 | db_type = Mongo_Atlas (or Mongo_Community) 17 | 18 | [convert] 19 | convert_api = secretkey 20 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Docker Guide For BookdlBot 🐳 # 2 | 3 | Credits for guide: [megadlbot](https://github.com/eyaadh/megadlbot_oss/blob/master/DockerReadme.md) project 4 | 5 | ## Install docker ## 6 | 7 | - Follow the official docker [installation guide](https://docs.docker.com/engine/install/) 8 | 9 | ## Install Docker-compose ## 10 | 11 | - Easiest way to install docker-compose is
12 | ```sudo pip install docker-compose``` 13 | - Also you can check other official methods of installing docker-compose [here](https://docs.docker.com/compose/install/) 14 | 15 | ## Run Bookdlbot ## 16 | 17 | - We dont need to clone the repo (yeah Docker-compose does that for us) 18 | - Setup configs (the 2 files in `docker` folder) 19 | - Download the sample config file
20 | - ```mkdir bookdlbot && cd bookdlbot```
Create and change to a working directory 21 | - ```wget https://github.com/Samfun75/bookdlbot/raw/master/docker/docker-config.ini.sample -O docker-config.ini```
Download docker-config.ini file 22 | - ```vim docker-config.ini```
Fill all the variables witht correct values and save 23 | 24 | - Download the yml file for docker-compose 25 | - ```wget https://github.com/Samfun75/bookdlbot/raw/master/docker/docker-compose.yml```
Download docker-comose.yml 26 | - Finally start the bot
27 | ```docker-compose up -d``` 28 | - Voila !! The bot should be running now
29 | Check logs with ```docker-compose logs -f``` 30 | 31 | ## How to stop the bot ## 32 | - Stop Command 33 | - ```docker-compose stop```
This will just stop the containers. Built images won't be removed.
So next time you can start with ``docker-compose start`` command and it will start up quickly. 34 | 35 | - Down command 36 | - ```docker-compose down```
This will stop and delete the built images. So next time you have to do ``docker-compose up -d`` to start the build and start the bot. 37 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | bookdlbot: 5 | image: bookdl 6 | container_name: bookdlbot 7 | build: 8 | context: https://github.com/Samfun75/BookdlBot.git 9 | restart: always 10 | env_file: 11 | - docker-config.ini 12 | -------------------------------------------------------------------------------- /docker/docker-config.ini.sample: -------------------------------------------------------------------------------- 1 | ENV = ANYTHING 2 | 3 | # [pyrogram] 4 | TG_API_ID = 1234567 5 | TG_API_HASH = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 6 | 7 | # [bot-configuration] 8 | TG_BOT_TOKEN = 123456789:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 9 | TG_DUSTBIN_CHAT = -100123456789 10 | ALLOWED_USERS = [123456789, 987654321] 11 | 12 | # [database] 13 | DATABASE_DB_HOST = xxxxxxxxxx.xxxxx.mongodb.net or where you hosted the db 14 | DATABASE_DB_USERNAME = username 15 | DATABASE_DB_PASSWORD = password 16 | DATABASE_DB_NAME = StreamerBot 17 | DATABASE_DB_TYPE = Mongo_Atlas (or Mongo_Community) 18 | 19 | # [convert] 20 | CONVERT_API = xxxxxxxxxxxxxxxxxx 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.8.0 2 | convertapi==1.4.0 3 | humanfriendly==10.0 4 | libgenesis==0.1.9 5 | lxml==4.9.1 6 | pymongo[srv]==4.0.2 7 | pyrogram==2.0.14 8 | requests==2.27.1 9 | sanitize-filename==1.2.0 10 | tgCrypto==1.2.3 11 | tldextract==3.2.0 12 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.4 2 | --------------------------------------------------------------------------------