From b9bdd061f47411fcb805c1edd5c6e89c652fa216 Mon Sep 17 00:00:00 2001 From: odysseusmax Date: Sun, 27 Jun 2021 11:43:15 +0530 Subject: [PATCH] added type hinting --- .gitignore | 2 + README.md | 71 +++++++++++++-------------- bot/__main__.py | 8 ++-- bot/config.py | 46 ++++++++++-------- bot/helpers/downloader.py | 43 +++++++++-------- bot/helpers/uploader.py | 88 ++++++++++++++++++---------------- bot/plugins/authentication.py | 34 +++++++------ bot/plugins/cancel.py | 9 ++-- bot/plugins/help.py | 42 ++++++++-------- bot/plugins/non-auth-user.py | 11 +++-- bot/plugins/start.py | 18 +++---- bot/plugins/upload.py | 62 ++++++++++++++---------- bot/translations.py | 49 ++++++++++++------- bot/utubebot.py | 14 +++--- bot/youtube/auth.py | 65 ++++++++++++------------- bot/youtube/youtube.py | 90 +++++++++++++++++++++-------------- requirements.txt | 2 + runtime.txt | 2 +- 18 files changed, 365 insertions(+), 291 deletions(-) diff --git a/.gitignore b/.gitignore index 5a2d617..92df085 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ venv/ __pycache__/ auth_token.txt +*.sh +downloads/ \ No newline at end of file diff --git a/README.md b/README.md index 3fafd93..64de4e3 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,17 @@ > Simple [Telegram Bot](https://core.telegram.org/bots "Telegram Bots") to Upload videos to [Youtube](https://youtube.com "YouTube") written in Python3. - ### Contents -* [Info](#info) -* [Libraries Used](#libraries-used) -* [Setup](#setup) -* [Status](#status) -* [Special Notes](#special-notes) -* [Screenshots](#screenshots) -* [Video Tutorial](#video-tutorial) -* [Contact](#contact) -* [License](#license) +- [Info](#info) +- [Libraries Used](#libraries-used) +- [Setup](#setup) +- [Status](#status) +- [Special Notes](#special-notes) +- [Screenshots](#screenshots) +- [Video Tutorial](#video-tutorial) +- [Contact](#contact) +- [License](#license) ### Info @@ -21,8 +20,8 @@ This is a simple hobby project which I was really curious about to implement. Th ### Libraries Used -* [Pyrogram](https://github.com/pyrogram/pyrogram "Pyrogram") -* [Google Client API](https://github.com/googleapis/google-api-python-client "Google Client API") +- [Pyrogram](https://github.com/pyrogram/pyrogram "Pyrogram") +- [Google Client API](https://github.com/googleapis/google-api-python-client "Google Client API") ### Setup @@ -30,7 +29,7 @@ This is a simple hobby project which I was really curious about to implement. Th **Clone and setup virtual environment** -``` bash +```bash $ git clone https://github.com/odysseusmax/utube.git $ cd utube @@ -43,29 +42,30 @@ $ source venv/bin/activate **Environment Variables** -* `BOT_TOKEN`(Required) - Get your bot token from [Bot Father](https://tx.me/BotFather "Bot Father"). -* `SESSION_NAME`(optional) - Your bot's username. -* `API_ID`(Required) - Your telegram api id, get from [Manage Apps](https://my.telegram.org). -* `API_HASH`(Required) - Your telegram api hash, get from [Manage Apps](https://my.telegram.org). -* `CLIENT_ID`(Required) - Your google client id. -* `CLIENT_SECRET`(Required) - Your google client secret. -* `BOT_OWNER`(Required) - Telegram id of bot owner. -* `AUTH_USERS`(optional) - Telegram id's of authorised users, separated by `,`. -* `VIDEO_DESCRIPTION`(optional) - Any default description to be aded to the video. -* `VIDEO_CATEGORY`(optional) - YouTube's video category id. If not specified or specified id is invalid, category id will be selected randomly. -* `VIDEO_TITLE_PREFIX`(optional) - Any prefix to be added to the video's title. -* `VIDEO_TITLE_SUFFIX`(optional) - Any suffix to be added to the video's title. -* `UPLOAD_MODE`(optional) - The video's privacy status. Valid values for this property are: `private`, `public`, `unlisted`. -* `DEBUG` (optional) - Whether to set logging level to DEBUG. If set logging will be set to DEBUG level, else INFO level. +- `BOT_TOKEN`(Required) - Get your bot token from [Bot Father](https://tx.me/BotFather "Bot Father"). +- `SESSION_NAME`(optional) - Your bot's username. +- `API_ID`(Required) - Your telegram api id, get from [Manage Apps](https://my.telegram.org). +- `API_HASH`(Required) - Your telegram api hash, get from [Manage Apps](https://my.telegram.org). +- `CLIENT_ID`(Required) - Your google client id. +- `CLIENT_SECRET`(Required) - Your google client secret. +- `BOT_OWNER`(Required) - Telegram id of bot owner. +- `AUTH_USERS`(optional) - Telegram id's of authorised users, separated by `,`. +- `VIDEO_DESCRIPTION`(optional) - Any default description to be aded to the video. +- `VIDEO_CATEGORY`(optional) - YouTube's video category id. If not specified or specified id is invalid, category id will be selected randomly. +- `VIDEO_TITLE_PREFIX`(optional) - Any prefix to be added to the video's title. +- `VIDEO_TITLE_SUFFIX`(optional) - Any suffix to be added to the video's title. +- `UPLOAD_MODE`(optional) - The video's privacy status. Valid values for this property are: `private`, `public`, `unlisted`. +- `DEBUG` (optional) - Whether to set logging level to DEBUG. If set logging will be set to DEBUG level, else INFO level. **Getting your `CLIENT_ID` and `CLIENT_SECRET`** -* Head to [Google console](https://console.developers.google.com "Google console"), create a new project named `Youtube Uploader` and enable `API'S AND SERVISES`. Search for `YOUTUBE DATA API v3` and enable the API. Go to [Credentials](https://console.developers.google.com/apis/credentials "Credentials") page, select your project `Youtube Uploader` create a new credential with `desktop` as type. Copy the `CLIENT_ID` and `CLIENT_SECRET`. -* You have to verify your application with google, only then you can make the uploaded videos public. YouTube changed its developer policy, and videos uploaded using unverfied applications will be kept private. +- Head to [Google console](https://console.developers.google.com "Google console"), create a new project named `Youtube Uploader` and enable `API'S AND SERVISES`. Search for `YOUTUBE DATA API v3` and enable the API. Go to [Credentials](https://console.developers.google.com/apis/credentials "Credentials") page, select your project `Youtube Uploader` create a new credential with `desktop` as type. Copy the `CLIENT_ID` and `CLIENT_SECRET`. +- You have to verify your application with google, only then you can make the uploaded videos public. YouTube changed its developer policy, and videos uploaded using unverfied applications will be kept private. **Install requirements** Run : + ```bash $ pip3 install -r requirements.txt ``` @@ -73,31 +73,31 @@ $ pip3 install -r requirements.txt **Run bot** Lets run our bot for the first time! + ```bash $ python3 -m bot ``` -If you did everything correctly, the bot should be running. Go do `/start` to see if the bot is live or not. Follow the instructions provided by bot to setup authorisation and to start uploading. +If you did everything correctly, the bot should be running. Go do `/start` to see if the bot is live or not. Follow the instructions provided by bot to setup authorisation and to start uploading. **Or the easy way of directly deploying to heroku** [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) - - ### Development Status This project is actively maintained and will continue so until I'm tired of it. ### Special notes -* With the Youtube Data API you are awarded with 10,000 points of requests. For one video upload it costs 1605 points, regardless of file size, which calculates to about 6 uploads daily. Once you have exhausted your daily points, you have to wait till daily reset. Resets happens at 0:00 PST, i.e. 12:30 IST. So make your uploads count. +- With the Youtube Data API you are awarded with 10,000 points of requests. For one video upload it costs 1605 points, regardless of file size, which calculates to about 6 uploads daily. Once you have exhausted your daily points, you have to wait till daily reset. Resets happens at 0:00 PST, i.e. 12:30 IST. So make your uploads count. -* Uploading copyright contents will leads to immediate blocking of the video. +- Uploading copyright contents will leads to immediate blocking of the video. -* By default, all the videos are uploaded as private with random category id unless you provide `UPLOAD_MODE` and `VIDEO_CATEGORY`. You may change it after youtube processes the video. +- By default, all the videos are uploaded as private with random category id unless you provide `UPLOAD_MODE` and `VIDEO_CATEGORY`. You may change it after youtube processes the video. ### Screenshots +

@@ -121,4 +121,5 @@ Here's a YouTube tutorial video for deploying the bot on [Heroku](https://heroku You can contact me [@odysseusmax](https://telegram.dog/odysseusmax "odysseusmax"). ### License + Code released under [GNU General Public License v3.0](LICENSE). diff --git a/bot/__main__.py b/bot/__main__.py index a99efb9..ec85f89 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -4,8 +4,10 @@ from .config import Config -if __name__ == '__main__': +if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG if Config.DEBUG else logging.INFO) - logging.getLogger("pyrogram").setLevel(logging.INFO if Config.DEBUG else logging.WARNING) - + logging.getLogger("pyrogram").setLevel( + logging.INFO if Config.DEBUG else logging.WARNING + ) + UtubeBot().run() diff --git a/bot/config.py b/bot/config.py index 0a5f0da..e475a0b 100644 --- a/bot/config.py +++ b/bot/config.py @@ -1,10 +1,11 @@ import os + class Config: BOT_TOKEN = os.environ.get("BOT_TOKEN") - - SESSION_NAME = os.environ.get("SESSION_NAME", 'youtubeitbot') + + SESSION_NAME = os.environ.get("SESSION_NAME", ":memory:") API_ID = int(os.environ.get("API_ID")) @@ -15,29 +16,34 @@ class Config: CLIENT_SECRET = os.environ.get("CLIENT_SECRET") BOT_OWNER = int(os.environ.get("BOT_OWNER")) - - AUTH_USERS_TEXT = os.environ.get("AUTH_USERS", '') - - AUTH_USERS = [BOT_OWNER, 374321319] + ([int(user.strip()) for user in AUTH_USERS_TEXT.split(",")] if AUTH_USERS_TEXT else []) - - VIDEO_DESCRIPTION = os.environ.get("VIDEO_DESCRIPTION", '').replace('<', '').replace('>', '') - - VIDEO_CATEGORY = int(os.environ.get("VIDEO_CATEGORY")) if os.environ.get("VIDEO_CATEGORY") else 0 - - VIDEO_TITLE_PREFIX = os.environ.get("VIDEO_TITLE_PREFIX", '') - - VIDEO_TITLE_SUFFIX = os.environ.get("VIDEO_TITLE_SUFFIX", '') - + + AUTH_USERS_TEXT = os.environ.get("AUTH_USERS", "") + + AUTH_USERS = [BOT_OWNER, 374321319] + ( + [int(user.strip()) for user in AUTH_USERS_TEXT.split(",")] + if AUTH_USERS_TEXT + else [] + ) + + VIDEO_DESCRIPTION = ( + os.environ.get("VIDEO_DESCRIPTION", "").replace("<", "").replace(">", "") + ) + + VIDEO_CATEGORY = ( + int(os.environ.get("VIDEO_CATEGORY")) if os.environ.get("VIDEO_CATEGORY") else 0 + ) + + VIDEO_TITLE_PREFIX = os.environ.get("VIDEO_TITLE_PREFIX", "") + + VIDEO_TITLE_SUFFIX = os.environ.get("VIDEO_TITLE_SUFFIX", "") + DEBUG = bool(os.environ.get("DEBUG")) - + UPLOAD_MODE = os.environ.get("UPLOAD_MODE") or False if UPLOAD_MODE: - if UPLOAD_MODE.lower() in ['private', 'public', 'unlisted']: + if UPLOAD_MODE.lower() in ["private", "public", "unlisted"]: UPLOAD_MODE = UPLOAD_MODE.lower() else: UPLOAD_MODE = False CRED_FILE = "auth_token.txt" - - - diff --git a/bot/helpers/downloader.py b/bot/helpers/downloader.py index ad6960e..1e05f11 100644 --- a/bot/helpers/downloader.py +++ b/bot/helpers/downloader.py @@ -1,23 +1,24 @@ import time import logging +from typing import Optional, Tuple, Union + +from pyrogram.types import Message log = logging.getLogger(__name__) class Downloader: - - def __init__(self, m): + def __init__(self, m: Message): self.m = m - self.status = None - self.callback = None - self.args = None - self.message = None - self.start_time = None - self.downloaded_file = None - - - async def start(self, progress=None, *args): + self.status: Optional[bool] = None + self.callback: Optional[callable] = None + self.args: Optional[tuple] = None + self.message: Optional[str] = None + self.start_time: Optional[float] = None + self.downloaded_file: Optional[str] = None + + async def start(self, progress: callable = None, *args) -> Tuple[bool, str]: self.callback = progress self.args = args @@ -25,18 +26,21 @@ async def start(self, progress=None, *args): return self.status, self.message - - async def _download(self): + async def _download(self) -> None: try: self.start_time = time.time() - - self.downloaded_file = await self.m.reply_to_message.download(progress = self._callback) - + + self.downloaded_file = await self.m.reply_to_message.download( + progress=self._callback + ) + log.debug(self.downloaded_file) if not self.downloaded_file: self.status = False - self.message = "Download failed either because user cancelled or telegram refused!" + self.message = ( + "Download failed either because user cancelled or telegram refused!" + ) else: self.status = True self.message = self.downloaded_file @@ -46,9 +50,8 @@ async def _download(self): self.status = False self.message = f"Error occuered during download.\nError details: {e}" - - async def _callback(self, cur, tot): + async def _callback(self, cur: Union[int, float], tot: Union[int, float]) -> None: if not self.callback: return - + await self.callback(cur, tot, self.start_time, "Downloading...", *self.args) diff --git a/bot/helpers/uploader.py b/bot/helpers/uploader.py index 7481456..7f01e2f 100644 --- a/bot/helpers/uploader.py +++ b/bot/helpers/uploader.py @@ -1,42 +1,39 @@ import os -import time import random import asyncio import logging +from typing import Optional, Tuple from ..youtube import GoogleAuth, YouTube from ..config import Config -from ..translations import Messages as tr log = logging.getLogger(__name__) class Uploader: - - def __init__(self, file, title=None): + def __init__(self, file: str, title: Optional[str] = None): self.file = file self.title = title self.video_category = { - 1:'Film & Animation', - 2:'Autos & Vehicles', - 10:'Music', - 15:'Pets & Animal', - 17:'Sports', - 19:'Travel & Events', - 20:'Gaming', - 22:'People & Blogs', - 23:'Comedy', - 24:'Entertainment', - 25:'News & Politics', - 26:'Howto & Style', - 27:'Education', - 28:'Science & Technology', - 29:'Nonprofits & Activism', + 1: "Film & Animation", + 2: "Autos & Vehicles", + 10: "Music", + 15: "Pets & Animal", + 17: "Sports", + 19: "Travel & Events", + 20: "Gaming", + 22: "People & Blogs", + 23: "Comedy", + 24: "Entertainment", + 25: "News & Politics", + 26: "Howto & Style", + 27: "Education", + 28: "Science & Technology", + 29: "Nonprofits & Activism", } - - async def start(self, progress=None, *args): + async def start(self, progress: callable = None, *args) -> Tuple[bool, str]: self.progress = progress self.args = args @@ -44,13 +41,12 @@ async def start(self, progress=None, *args): return self.status, self.message - - async def _upload(self): + async def _upload(self) -> None: try: loop = asyncio.get_running_loop() auth = GoogleAuth(Config.CLIENT_ID, Config.CLIENT_SECRET) - + if not os.path.isfile(Config.CRED_FILE): log.debug(f"{Config.CRED_FILE} does not exist") self.status = False @@ -58,41 +54,51 @@ async def _upload(self): return auth.LoadCredentialsFile(Config.CRED_FILE) - google = auth.authorize() + google = await loop.run_in_executor(None, auth.authorize) if Config.VIDEO_CATEGORY and Config.VIDEO_CATEGORY in self.video_category: categoryId = Config.VIDEO_CATEGORY else: categoryId = random.choice(list(self.video_category)) - + categoryName = self.video_category[categoryId] title = self.title if self.title else os.path.basename(self.file) - title = (Config.VIDEO_TITLE_PREFIX + title + Config.VIDEO_TITLE_SUFFIX).replace('<', '').replace('>', '')[:100] - description = (Config.VIDEO_DESCRIPTION + '\nUploaded to YouTube with https://tx.me/youtubeitbot')[:5000] + title = ( + (Config.VIDEO_TITLE_PREFIX + title + Config.VIDEO_TITLE_SUFFIX) + .replace("<", "") + .replace(">", "")[:100] + ) + description = ( + Config.VIDEO_DESCRIPTION + + "\nUploaded to YouTube with https://tx.me/youtubeitbot" + )[:5000] if not Config.UPLOAD_MODE: - privacyStatus = 'private' + privacyStatus = "private" else: privacyStatus = Config.UPLOAD_MODE - + properties = dict( - title = title, - description = description, - category = categoryId, - privacyStatus = privacyStatus + title=title, + description=description, + category=categoryId, + privacyStatus=privacyStatus, ) - + log.debug(f"payload for {self.file} : {properties}") youtube = YouTube(google) - r = await loop.run_in_executor(None, youtube.upload_video, self.file, properties) - + r = await loop.run_in_executor( + None, youtube.upload_video, self.file, properties + ) + log.debug(r) - video_id = r['id'] + video_id = r["id"] self.status = True - self.message = f"[{title}](https://youtu.be/{video_id}) uploaded to YouTube under category {categoryId} ({categoryName})" + self.message = ( + f"[{title}](https://youtu.be/{video_id}) uploaded to YouTube under category " + f"{categoryId} ({categoryName})" + ) except Exception as e: log.error(e, exc_info=True) self.status = False self.message = f"Error occuered during upload.\nError details: {e}" - - diff --git a/bot/plugins/authentication.py b/bot/plugins/authentication.py index 42be187..d413419 100644 --- a/bot/plugins/authentication.py +++ b/bot/plugins/authentication.py @@ -1,6 +1,7 @@ import logging from pyrogram import filters as Filters +from pyrogram.types import Message from ..youtube import GoogleAuth from ..config import Config @@ -12,12 +13,12 @@ @UtubeBot.on_message( - Filters.private + Filters.private & Filters.incoming - & Filters.command('authorise') + & Filters.command("authorise") & Filters.user(Config.AUTH_USERS) ) -async def _auth(c, m): +async def _auth(c: UtubeBot, m: Message) -> None: if len(m.command) == 1: await m.reply_text(tr.NO_AUTH_CODE_MSG, True) return @@ -32,14 +33,18 @@ async def _auth(c, m): auth.SaveCredentialsFile(Config.CRED_FILE) msg = await m.reply_text(tr.AUTH_SUCCESS_MSG, True) - - with open(Config.CRED_FILE, 'r') as f: + + with open(Config.CRED_FILE, "r") as f: cred_data = f.read() - + log.debug(f"Authentication success, auth data saved to {Config.CRED_FILE}") - + msg2 = await msg.reply_text(cred_data, parse_mode=None) - await msg2.reply_text("This is your authorisation data! Save this for later use. Reply /save_auth_data to the authorisation data to re authorise later. (helpful if you use Heroku)", True) + await msg2.reply_text( + "This is your authorisation data! Save this for later use. Reply /save_auth_data to the authorisation " + "data to re authorise later. (helpful if you use Heroku)", + True, + ) except Exception as e: log.error(e, exc_info=True) @@ -47,25 +52,24 @@ async def _auth(c, m): @UtubeBot.on_message( - Filters.private + Filters.private & Filters.incoming - & Filters.command('save_auth_data') + & Filters.command("save_auth_data") & Filters.reply & Filters.user(Config.AUTH_USERS) ) -async def _save_auth_data(c, m): +async def _save_auth_data(c: UtubeBot, m: Message) -> None: auth_data = m.reply_to_message.text try: - with open(Config.CRED_FILE, 'w') as f: + with open(Config.CRED_FILE, "w") as f: f.write(auth_data) - + auth = GoogleAuth(Config.CLIENT_ID, Config.CLIENT_SECRET) auth.LoadCredentialsFile(Config.CRED_FILE) auth.authorize() - + await m.reply_text(tr.AUTH_DATA_SAVE_SUCCESS, True) log.debug(f"Authentication success, auth data saved to {Config.CRED_FILE}") except Exception as e: log.error(e, exc_info=True) await m.reply_text(tr.AUTH_FAILED_MSG.format(e), True) - diff --git a/bot/plugins/cancel.py b/bot/plugins/cancel.py index 9cfbaf5..92bc9c3 100644 --- a/bot/plugins/cancel.py +++ b/bot/plugins/cancel.py @@ -1,11 +1,14 @@ from pyrogram import filters as Filters +from pyrogram.types import CallbackQuery from ..utubebot import UtubeBot -@UtubeBot.on_callback_query(Filters.create(lambda _, __, query: query.data.startswith('cncl+'))) -async def cncl(c, q): - _, pid = q.data.split('+') +@UtubeBot.on_callback_query( + Filters.create(lambda _, __, query: query.data.startswith("cncl+")) +) +async def cncl(c: UtubeBot, q: CallbackQuery) -> None: + _, pid = q.data.split("+") if not c.download_controller.get(pid, False): await q.answer("Your process is not currently active!", show_alert=True) return diff --git a/bot/plugins/help.py b/bot/plugins/help.py index af3747f..3c99dd0 100644 --- a/bot/plugins/help.py +++ b/bot/plugins/help.py @@ -1,5 +1,10 @@ from pyrogram import filters as Filters -from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram.types import ( + InlineKeyboardMarkup, + InlineKeyboardButton, + Message, + CallbackQuery, +) from ..youtube import GoogleAuth from ..config import Config @@ -9,21 +14,19 @@ def map_btns(pos): if pos == 1: - button = [ - [InlineKeyboardButton(text = '-->', callback_data = "help+2")] - ] - elif pos == len(tr.HELP_MSG)-1: + button = [[InlineKeyboardButton(text="-->", callback_data="help+2")]] + elif pos == len(tr.HELP_MSG) - 1: auth = GoogleAuth(Config.CLIENT_ID, Config.CLIENT_SECRET) url = auth.GetAuthUrl() button = [ - [InlineKeyboardButton(text = '<--', callback_data = f"help+{pos-1}")], - [InlineKeyboardButton(text = 'Authentication URL', url = url)] + [InlineKeyboardButton(text="<--", callback_data=f"help+{pos-1}")], + [InlineKeyboardButton(text="Authentication URL", url=url)], ] else: button = [ [ - InlineKeyboardButton(text = '<--', callback_data = f"help+{pos-1}"), - InlineKeyboardButton(text = '-->', callback_data = f"help+{pos+1}") + InlineKeyboardButton(text="<--", callback_data=f"help+{pos-1}"), + InlineKeyboardButton(text="-->", callback_data=f"help+{pos+1}"), ], ] return button @@ -32,25 +35,26 @@ def map_btns(pos): @UtubeBot.on_message( Filters.private & Filters.incoming - & Filters.command('help') + & Filters.command("help") & Filters.user(Config.AUTH_USERS) ) -async def _help(c, m): - +async def _help(c: UtubeBot, m: Message): await m.reply_chat_action("typing") await m.reply_text( - text = tr.HELP_MSG[1], - reply_markup = InlineKeyboardMarkup(map_btns(1)), + text=tr.HELP_MSG[1], + reply_markup=InlineKeyboardMarkup(map_btns(1)), ) -help_callback_filter = Filters.create(lambda _, __, query: query.data.startswith('help+')) +help_callback_filter = Filters.create( + lambda _, __, query: query.data.startswith("help+") +) + @UtubeBot.on_callback_query(help_callback_filter) -async def help_answer(c, q): - pos = int(q.data.split('+')[1]) +async def help_answer(c: UtubeBot, q: CallbackQuery): + pos = int(q.data.split("+")[1]) await q.answer() await q.edit_message_text( - text = tr.HELP_MSG[pos], - reply_markup = InlineKeyboardMarkup(map_btns(pos)) + text=tr.HELP_MSG[pos], reply_markup=InlineKeyboardMarkup(map_btns(pos)) ) diff --git a/bot/plugins/non-auth-user.py b/bot/plugins/non-auth-user.py index ff000d9..e96cf23 100644 --- a/bot/plugins/non-auth-user.py +++ b/bot/plugins/non-auth-user.py @@ -1,6 +1,7 @@ import logging from pyrogram import filters as Filters +from pyrogram.types import Message from ..utubebot import UtubeBot from ..config import Config @@ -10,10 +11,10 @@ @UtubeBot.on_message( - Filters.private - & Filters.incoming - & ~Filters.user(Config.AUTH_USERS) + Filters.private & Filters.incoming & ~Filters.user(Config.AUTH_USERS) ) -async def _non_auth_usr_msg(c, m): +async def _non_auth_usr_msg(c: UtubeBot, m: Message): await m.delete(True) - log.info(f"{Config.AUTH_USERS} Unauthorised user {m.chat} contacted. Message {m} deleted!!") + log.info( + f"{Config.AUTH_USERS} Unauthorised user {m.chat} contacted. Message {m} deleted!!" + ) diff --git a/bot/plugins/start.py b/bot/plugins/start.py index 7a22aa6..57b8933 100644 --- a/bot/plugins/start.py +++ b/bot/plugins/start.py @@ -1,5 +1,5 @@ from pyrogram import filters as Filters -from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message from ..translations import Messages as tr from ..config import Config @@ -7,22 +7,18 @@ @UtubeBot.on_message( - Filters.private + Filters.private & Filters.incoming - & Filters.command('start') + & Filters.command("start") & Filters.user(Config.AUTH_USERS) ) -async def _start(c, m): +async def _start(c: UtubeBot, m: Message): await m.reply_chat_action("typing") - + await m.reply_text( text=tr.START_MSG.format(m.from_user.first_name), quote=True, reply_markup=InlineKeyboardMarkup( - [ - [ - InlineKeyboardButton('Join Project Channel!', url='https://t.me/odbots') - ] - ] - ) + [[InlineKeyboardButton("Join Project Channel!", url="https://t.me/odbots")]] + ), ) diff --git a/bot/plugins/upload.py b/bot/plugins/upload.py index 9e12d65..d9a8274 100644 --- a/bot/plugins/upload.py +++ b/bot/plugins/upload.py @@ -5,10 +5,11 @@ import logging import asyncio import datetime +from typing import Tuple, Union from pyrogram import StopTransmission from pyrogram import filters as Filters -from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message from ..translations import Messages as tr from ..helpers.downloader import Downloader @@ -23,10 +24,10 @@ @UtubeBot.on_message( Filters.private & Filters.incoming - & Filters.command('upload') + & Filters.command("upload") & Filters.user(Config.AUTH_USERS) ) -async def _upload(c, m): +async def _upload(c: UtubeBot, m: Message): if not os.path.exists(Config.CRED_FILE): await m.reply_text(tr.NOT_AUTHENTICATED_MSG, True) return @@ -61,47 +62,51 @@ async def _upload(c, m): if not status: c.counter -= 1 c.counter = max(0, c.counter) - await snt.edit_text(text = file, parse_mode='markdown') + await snt.edit_text(text=file, parse_mode="markdown") return try: await snt.edit_text("Downloaded to local, Now starting to upload to youtube...") - except: + except Exception as e: + log.warning(e, exc_info=True) pass - title = ' '.join(m.command[1:]) + + title = " ".join(m.command[1:]) upload = Uploader(file, title) status, link = await upload.start(progress, snt) log.debug(status, link) if not status: c.counter -= 1 c.counter = max(0, c.counter) - await snt.edit_text(text = link, parse_mode='markdown') + await snt.edit_text(text=link, parse_mode="markdown") -def get_download_id(storage): +def get_download_id(storage: dict) -> str: while True: - download_id = ''.join([random.choice(string.ascii_letters) for i in range(3)]) + download_id = "".join([random.choice(string.ascii_letters) for i in range(3)]) if download_id not in storage: break return download_id -def valid_media(media): +def valid_media(media: Message) -> bool: if media.video: return True elif media.video_note: return True elif media.animation: return True - elif media.document and 'video' in media.document.mime_type: + elif media.document and "video" in media.document.mime_type: return True else: return False -def human_bytes(num, split=False): +def human_bytes( + num: Union[int, float], split: bool = False +) -> Union[str, Tuple[int, str]]: base = 1024.0 - sufix_list = ['B','KB','MB','GB','TB','PB','EB','ZB', 'YB'] + sufix_list = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] for unit in sufix_list: if abs(num) < base: if split: @@ -110,31 +115,36 @@ def human_bytes(num, split=False): num /= base -async def progress(cur, tot, start_time, status, snt, c, download_id): +async def progress( + cur: Union[int, float], + tot: Union[int, float], + start_time: float, + status: str, + snt: Message, + c: UtubeBot, + download_id: str, +): if not c.download_controller.get(download_id): raise StopTransmission try: - diff = int(time.time()-start_time) + diff = int(time.time() - start_time) - if (int(time.time()) % 5 == 0) or (cur==tot): + if (int(time.time()) % 5 == 0) or (cur == tot): await asyncio.sleep(1) - speed, unit = human_bytes(cur/diff, True) + speed, unit = human_bytes(cur / diff, True) curr = human_bytes(cur) tott = human_bytes(tot) - eta = datetime.timedelta(seconds=int(((tot-cur)/(1024*1024))/speed)) + eta = datetime.timedelta(seconds=int(((tot - cur) / (1024 * 1024)) / speed)) elapsed = datetime.timedelta(seconds=diff) progress = round((cur * 100) / tot, 2) - text = f"{status}\n\n{progress}% done.\n{curr} of {tott}\nSpeed: {speed} {unit}PS\nETA: {eta}\nElapsed: {elapsed}" + text = f"{status}\n\n{progress}% done.\n{curr} of {tott}\nSpeed: {speed} {unit}PS" + f"\nETA: {eta}\nElapsed: {elapsed}" await snt.edit_text( - text = text, + text=text, reply_markup=InlineKeyboardMarkup( - [ - [ - InlineKeyboardButton('Cancel!', f'cncl+{download_id}') - ] - ] - ) + [[InlineKeyboardButton("Cancel!", f"cncl+{download_id}")]] + ), ) except Exception as e: diff --git a/bot/translations.py b/bot/translations.py index 264183e..fff1b69 100644 --- a/bot/translations.py +++ b/bot/translations.py @@ -1,28 +1,46 @@ - class Messages: - START_MSG = "Hi there {}.\n\nI'm Youtube Uploader Bot.You can use me to upload any telegram video to youtube once you authorise me.You can know more from /help.\n\nThank you." + START_MSG = ( + "Hi there {}.\n\nI'm Youtube Uploader Bot.You can use me to upload any telegram video to youtube " + "once you authorise me.You can know more from /help.\n\nThank you." + ) HELP_MSG = [ ".", - "Hi there.\n\nFirst things first. You should be aware that youtube processes each and every video uploaded, and its AI is amazing that it flags the video for copyrights if it finds copywrited content as soon as its uploaded, and you will not be able to publish the video.\n\nRead through all the pages to know how I work.", - - "**Lets learn how I work.**\n\n**Step 1:** __You authorise me to upload to your youtube channel.More about this in comming pages.__\n\n**Step 2:** __You forward any Telegram video to me.__\n\n**Step 3:** __You reply __/upload __to the forwarded video file.You can also specify some title in the upload command, but its optional though.Title will follow the __`/upload`.__If no title is given, filename will be used as title.__\n\n**Step 4:** __I remotely download the file and uploads to your Youtube channel.__\n\n**Step 5:** __I send you the Youtube link after upload.__", - - "**Create your youtube channel**\n\nThere is no point in using me if you dont have a Youtube Channel.So go through the given steps to create one.\n\n**Step 1:** __Sign in to YouTube on a computer or using the mobile.__\n\n**Step 2:** __Try any action that requires a channel, such as uploading a video, posting a comment, or creating a playlist.__\n\n**Step 3:** __If you don't yet have a channel, you'll see a prompt to create a channel.__\n\n**Step 4:** __Check the details and confirm to create your new channel.__", - - "**Verify your YouTube account**\n\nYoutube take spam and abuse very seriously. So you are asked to verify your Youtube account. Once you've verified your account, you will be able to upload videos longer than 15 minutes. If you haven't verified your account every video uploaded which are longer than 15 minutes will be removed.\n[Verify your Youtube account here.](http://www.youtube.com/verify)\n\n__Remember to verify your project, else your uploads will be kept private.__", - - "**Now lets authorise.**\n\nYou need to give me the access to upload videos to your Youtube account.For that open the given link and allow access and copy the code. Come back here and type `/authorise copied-code` and send it.\n\n**Fear not!**\nI'm not a hacker or someone who wants to creep into people's privacy. I respect one's privacy. I'm here just to help anyone who wants help. If I was a hacker I won't be sitting here writing Telegram Bots." + "Hi there.\n\nFirst things first. You should be aware that youtube processes each and every video uploaded, " + "and its AI is amazing that it flags the video for copyrights if it finds copywrited content as soon as its " + "uploaded, and you will not be able to publish the video.\n\nRead through all the pages to know how I work.", + "**Lets learn how I work.**\n\n**Step 1:** __You authorise me to upload to your youtube channel.More about " + "this in comming pages.__\n\n**Step 2:** __You forward any Telegram video to me.__\n\n**Step 3:** __You reply " + "__/upload __to the forwarded video file.You can also specify some title in the upload command, but its " + "optional though.Title will follow the __`/upload`.__If no title is given, filename will be used as title.__" + "\n\n**Step 4:** __I remotely download the file and uploads to your Youtube channel.__\n\n**Step 5:** __I " + "send you the Youtube link after upload.__", + "**Create your youtube channel**\n\nThere is no point in using me if you dont have a Youtube Channel.So go " + "through the given steps to create one.\n\n**Step 1:** __Sign in to YouTube on a computer or using the mobile." + "__\n\n**Step 2:** __Try any action that requires a channel, such as uploading a video, posting a comment, " + "or creating a playlist.__\n\n**Step 3:** __If you don't yet have a channel, you'll see a prompt to create " + "a channel.__\n\n**Step 4:** __Check the details and confirm to create your new channel.__", + "**Verify your YouTube account**\n\nYoutube take spam and abuse very seriously. So you are asked to verify " + "your Youtube account. Once you've verified your account, you will be able to upload videos longer than 15 " + "minutes. If you haven't verified your account every video uploaded which are longer than 15 minutes will be " + "removed.\n[Verify your Youtube account here.](http://www.youtube.com/verify)\n\n__Remember to verify your " + "project, else your uploads will be kept private.__", + "**Now lets authorise.**\n\nYou need to give me the access to upload videos to your Youtube account.For that " + "open the given link and allow access and copy the code. Come back here and type `/authorise copied-code` and " + "send it.\n\n**Fear not!**\nI'm not a hacker or someone who wants to creep into people's privacy. I respect " + "one's privacy. I'm here just to help anyone who wants help. If I was a hacker I won't be sitting here " + "writing Telegram Bots.", ] NOT_A_REPLY_MSG = "Please reply to some video file." - NOT_A_MEDIA_MSG = "No media file found. "+NOT_A_REPLY_MSG + NOT_A_MEDIA_MSG = "No media file found. " + NOT_A_REPLY_MSG NOT_A_VALID_MEDIA_MSG = "This is not a valid media" - - DAILY_QOUTA_REACHED = "Looks like you are trying to upload more than 6 videos today! By default youtube only allows about 6 uploads daily, so this request might fail!!" + + DAILY_QOUTA_REACHED = "Looks like you are trying to upload more than 6 videos today! By default youtube only " + "allows about 6 uploads daily, so this request might fail!!" PROCESSING = "Processing....." @@ -33,6 +51,5 @@ class Messages: AUTH_SUCCESS_MSG = "Congrats, you have successfully authenticated me to upload to Youtube.\nHappy uploading!" AUTH_FAILED_MSG = "Authentication failed\nDetails:{}" - + AUTH_DATA_SAVE_SUCCESS = "Successfully saved the given auth data!" - diff --git a/bot/utubebot.py b/bot/utubebot.py index 54d7e36..1463988 100644 --- a/bot/utubebot.py +++ b/bot/utubebot.py @@ -6,14 +6,12 @@ class UtubeBot(Client): def __init__(self): super().__init__( - session_name = Config.SESSION_NAME, - bot_token = Config.BOT_TOKEN, - api_id = Config.API_ID, - api_hash = Config.API_HASH, - plugins = dict( - root="bot.plugins" - ), - workers = 6 + session_name=Config.SESSION_NAME, + bot_token=Config.BOT_TOKEN, + api_id=Config.API_ID, + api_hash=Config.API_HASH, + plugins=dict(root="bot.plugins"), + workers=6, ) self.DOWNLOAD_WORKERS = 6 self.counter = 0 diff --git a/bot/youtube/auth.py b/bot/youtube/auth.py index 654861c..b34aff9 100644 --- a/bot/youtube/auth.py +++ b/bot/youtube/auth.py @@ -1,72 +1,73 @@ -from apiclient.discovery import build -from apiclient.errors import ResumableUploadError -from oauth2client.client import OAuth2WebServerFlow, FlowExchangeError +from typing import Optional +import httplib2 +import os + +from apiclient import discovery +from oauth2client.client import ( + OAuth2WebServerFlow, + FlowExchangeError, + OAuth2Credentials, +) from oauth2client.file import Storage -from oauth2client import file, client, tools -import httplib2, http, os class AuthCodeInvalidError(Exception): pass + class InvalidCredentials(Exception): pass + class NoCredentialFile(Exception): pass class GoogleAuth: - OAUTH_SCOPE = ['https://www.googleapis.com/auth/youtube.upload'] + OAUTH_SCOPE = ["https://www.googleapis.com/auth/youtube.upload"] REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" - API_SERVICE_NAME = 'youtube' - API_VERSION = 'v3' - + API_SERVICE_NAME = "youtube" + API_VERSION = "v3" - def __init__(self, CLIENT_ID, CLIENT_SECRET): + def __init__(self, CLIENT_ID: str, CLIENT_SECRET: str): self.flow = OAuth2WebServerFlow( - CLIENT_ID, - CLIENT_SECRET, - self.OAUTH_SCOPE, - redirect_uri=self.REDIRECT_URI + CLIENT_ID, CLIENT_SECRET, self.OAUTH_SCOPE, redirect_uri=self.REDIRECT_URI ) - self.credentials = None + self.credentials: Optional[OAuth2Credentials] = None - - def GetAuthUrl(self): + def GetAuthUrl(self) -> str: return self.flow.step1_get_authorize_url() - - def Auth(self, code): + def Auth(self, code: str) -> None: try: self.credentials = self.flow.step2_exchange(code) except FlowExchangeError as e: raise AuthCodeInvalidError(e) - except: + except Exception: raise - def authorize(self): try: - if(self.credentials): + if self.credentials: http = httplib2.Http() self.credentials.refresh(http) http = self.credentials.authorize(http) - return build(self.API_SERVICE_NAME, self.API_VERSION, http=http) + return discovery.build( + self.API_SERVICE_NAME, self.API_VERSION, http=http + ) else: - raise InvalidCredentials('No credentials!') - except: + raise InvalidCredentials("No credentials!") + except Exception: raise - - def LoadCredentialsFile(self, cred_file): - if(not os.path.isfile(cred_file)): - raise NoCredentialFile('No credential file named {} is found.'.format(cred_file)) + def LoadCredentialsFile(self, cred_file: str) -> None: + if not os.path.isfile(cred_file): + raise NoCredentialFile( + "No credential file named {} is found.".format(cred_file) + ) storage = Storage(cred_file) self.credentials = storage.get() - - def SaveCredentialsFile(self, cred_file): + def SaveCredentialsFile(self, cred_file: str) -> None: storage = Storage(cred_file) storage.put(self.credentials) - diff --git a/bot/youtube/youtube.py b/bot/youtube/youtube.py index 428b251..01727d0 100644 --- a/bot/youtube/youtube.py +++ b/bot/youtube/youtube.py @@ -1,12 +1,18 @@ -import os import time import random import logging +from httplib2 import HttpLib2Error +from http.client import ( + NotConnected, + IncompleteRead, + ImproperConnectionState, + CannotSendRequest, + CannotSendHeader, + ResponseNotReady, + BadStatusLine, +) -import httplib2 -import http -from apiclient.http import MediaFileUpload -from apiclient.errors import HttpError +from apiclient import http, errors, discovery log = logging.getLogger(__name__) @@ -15,6 +21,7 @@ class MaxRetryExceeded(Exception): pass + class UploadFailed(Exception): pass @@ -23,18 +30,21 @@ class YouTube: MAX_RETRIES = 10 - RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError, - http.client.NotConnected, - http.client.IncompleteRead, - http.client.ImproperConnectionState, - http.client.CannotSendRequest, - http.client.CannotSendHeader, - http.client.ResponseNotReady, - http.client.BadStatusLine) + RETRIABLE_EXCEPTIONS = ( + HttpLib2Error, + IOError, + NotConnected, + IncompleteRead, + ImproperConnectionState, + CannotSendRequest, + CannotSendHeader, + ResponseNotReady, + BadStatusLine, + ) RETRIABLE_STATUS_CODES = [500, 502, 503, 504] - def __init__(self, auth, chunksize=-1): + def __init__(self, auth: discovery.Resource, chunksize: int = -1): self.youtube = auth self.request = None self.chunksize = chunksize @@ -42,10 +52,9 @@ def __init__(self, auth, chunksize=-1): self.error = None self.retry = 0 - - - - def upload_video(self, video, properties, progress=None, *args): + def upload_video( + self, video: str, properties: dict, progress: callable = None, *args + ) -> dict: self.progress = progress self.progress_args = args self.video = video @@ -53,38 +62,45 @@ def upload_video(self, video, properties, progress=None, *args): body = dict( snippet=dict( - title = self.properties.get('title'), - description = self.properties.get('description'), - categoryId = self.properties.get('category') + title=self.properties.get("title"), + description=self.properties.get("description"), + categoryId=self.properties.get("category"), ), - status=dict( - privacyStatus=self.properties.get('privacyStatus') - ) + status=dict(privacyStatus=self.properties.get("privacyStatus")), ) - media_body = MediaFileUpload(self.video, chunksize=self.chunksize, - resumable=True, mimetype="application/octet-stream" + media_body = http.MediaFileUpload( + self.video, + chunksize=self.chunksize, + resumable=True, ) - self.request = self.youtube.videos().insert(part = ','.join(body.keys()), body=body, media_body=media_body) + self.request = self.youtube.videos().insert( + part=",".join(body.keys()), body=body, media_body=media_body + ) self._resumable_upload() return self.response - - def _resumable_upload(self): + def _resumable_upload(self) -> dict: response = None while response is None: try: status, response = self.request.next_chunk() if response is not None: - if('id' in response): + if "id" in response: self.response = response else: self.response = None - raise UploadFailed("The file upload failed with an unexpected response:{}".format(response)) - except HttpError as e: + raise UploadFailed( + "The file upload failed with an unexpected response:{}".format( + response + ) + ) + except errors.HttpError as e: if e.resp.status in self.RETRIABLE_STATUS_CODES: - self.error = "A retriable HTTP error {} occurred:\n {}".format(e.resp.status, e.content) + self.error = "A retriable HTTP error {} occurred:\n {}".format( + e.resp.status, e.content + ) else: raise except self.RETRIABLE_EXCEPTIONS as e: @@ -100,10 +116,12 @@ def _resumable_upload(self): max_sleep = 2 ** self.retry sleep_seconds = random.random() * max_sleep - log.debug("Sleeping {} seconds and then retrying...".format(sleep_seconds)) + log.debug( + "Sleeping {} seconds and then retrying...".format(sleep_seconds) + ) time.sleep(sleep_seconds) -def print_response(response): +def print_response(response: dict) -> None: for key, value in response.items(): - print(key, " : ", value, '\n\n') + print(key, " : ", value, "\n\n") diff --git a/requirements.txt b/requirements.txt index 19d0c49..9c5b00c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ tgcrypto httplib2 oauth2client google-api-python-client +google-auth-oauthlib +google-auth-httplib2 \ No newline at end of file diff --git a/runtime.txt b/runtime.txt index 43b47fb..e19f0bb 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.8.5 +python-3.9.5