├── .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 |
18 | ### Heroku
19 |
20 | Use the below button to deploy.
21 |
22 | [](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 |
--------------------------------------------------------------------------------