├── LICENSE ├── README.md ├── main.py └── vid_utils.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Hodza Alban 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # telegram-bot-youtube-downloader 2 | 3 | ### This bot is old and not efficient, this repository will be archived. 4 | 5 | Change TOKEN with your token 6 | 7 | Usage: 8 | - Send link of video (@vid inline is comfortable) 9 | - The bot will download the video and send it 10 | - If the video is larger than 50MB, it is split into smaller parts, 11 | which then need to be concatenated (in linux: cat vid.mp4* > vid.mp4) 12 | 13 | This script require: 14 | - Python3 interpreter 15 | - Telegram python api https://github.com/python-telegram-bot/python-telegram-bot 16 | - youtube-dl https://github.com/rg3/youtube-dl/ (installed on the machine) 17 | 18 | Tips: 19 | - Use PythonAnyWhere for hosting the bot https://www.pythonanywhere.com 20 | 21 | 22 | 23 | ## TODO 24 | - Improve space-requirement of hard-split (is 2 times size_of_video, the goal is size_of_video + 49MB) 25 | - Improve soft-split of the videos 26 | - PEP8 27 | - Remove duplicates of resolution 28 | - Add geo-bypass feature 29 | - Add playlist download feature 30 | - match title with regex 31 | - from video x to video y 32 | - only video uploaded before or after date x 33 | - max-views or min-views 34 | - Subtitle download 35 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from telegram import InlineKeyboardMarkup 4 | from telegram.ext import Updater, CallbackQueryHandler, MessageHandler, Filters 5 | 6 | from vid_utils import Video, BadLink 7 | 8 | logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def get_format(bot, update): 13 | logger.info("from {}: {}".format(update.message.chat_id, update.message.text)) # "history" 14 | 15 | try: 16 | video = Video(update.message.text, init_keyboard=True) 17 | except BadLink: 18 | update.message.reply_text("Bad link") 19 | else: 20 | reply_markup = InlineKeyboardMarkup(video.keyboard) 21 | update.message.reply_text('Choose format:', reply_markup=reply_markup) 22 | 23 | 24 | def download_choosen_format(bot, update): 25 | query = update.callback_query 26 | resolution_code, link = query.data.split(' ', 1) 27 | 28 | bot.edit_message_text(text="Downloading...", 29 | chat_id=query.message.chat_id, 30 | message_id=query.message.message_id) 31 | 32 | video = Video(link) 33 | video.download(resolution_code) 34 | 35 | with video.send() as files: 36 | for f in files: 37 | bot.send_document(chat_id=query.message.chat_id, document=open(f, 'rb')) 38 | 39 | 40 | updater = Updater(token=YOUR_TOKEN) 41 | 42 | updater.dispatcher.add_handler(MessageHandler(Filters.text, get_format)) 43 | updater.dispatcher.add_handler(CallbackQueryHandler(download_choosen_format)) 44 | 45 | 46 | updater.start_polling() 47 | updater.idle() 48 | -------------------------------------------------------------------------------- /vid_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | from glob import glob, escape 4 | from subprocess import Popen, PIPE 5 | from time import strftime, strptime, sleep 6 | from contextlib import contextmanager 7 | 8 | from telegram import InlineKeyboardButton 9 | 10 | class BadLink(Exception): 11 | pass 12 | 13 | 14 | class Video: 15 | def __init__(self, link, init_keyboard=False): 16 | self.link = link 17 | self.file_name = None 18 | 19 | if init_keyboard: 20 | self.formats = self.get_formats() 21 | self.keyboard = self.generate_keyboard() 22 | 23 | def get_formats(self): 24 | formats = [] 25 | 26 | cmd = "youtube-dl -F {}".format(self.link) 27 | p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).communicate() 28 | it = iter(str(p[0], 'utf-8').split('\n')) # iterator of output lines 29 | 30 | try: 31 | while "code extension" not in next(it): pass # Remove garbage lines 32 | except StopIteration: 33 | raise BadLink # Isn't a valid youtube link 34 | 35 | while True: 36 | try: 37 | line = next(it) 38 | if not line: 39 | raise StopIteration # Usually the last line is empty 40 | if "video only" in line: 41 | continue # I don't need video without audio 42 | except StopIteration: 43 | break 44 | else: 45 | format_code, extension, resolution, *_ = line.strip().split() 46 | formats.append([format_code, extension, resolution]) 47 | return formats 48 | 49 | def generate_keyboard(self): 50 | """ Generate a list of InlineKeyboardButton of resolutions """ 51 | kb = [] 52 | 53 | for code, extension, resolution in self.formats: 54 | kb.append([InlineKeyboardButton("{0}, {1}".format(extension, resolution), 55 | callback_data="{} {}".format(code, self.link))]) # maybe callback_data can support a list or tuple? 56 | return kb 57 | 58 | def download(self, resolution_code): 59 | cmd = "youtube-dl -f {0} {1}".format(resolution_code, self.link) 60 | p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).communicate() 61 | 62 | for line in str(p[0], 'utf-8').split('\n'): 63 | if "[download] Destination:" in line: 64 | self.file_name = line[24:] # name of the file 65 | 66 | def check_dimension(self): 67 | if os.path.getsize(self.file_name) > 50 * 1024 * 1023: 68 | os.system('split -b 49M "{0}" "{1}"'.format(self.file_name, self.file_name)) 69 | os.remove(self.file_name) 70 | return glob(escape(self.file_name) + '*') 71 | 72 | @contextmanager 73 | def send(self): 74 | files = self.check_dimension() # split if size >= 50MB 75 | yield files 76 | for f in files: #removing old files 77 | os.remove(f) 78 | 79 | 80 | 81 | 82 | 83 | #__________________________OLD STUFFS, TOUCH CAREFULLY__________________________ 84 | 85 | # this is the soft-split version, require avconv, but the audio isn't synchronized, avconv's problems :( 86 | ''' 87 | def get_duration(filepath): # get duration in seconds 88 | cmd = "avconv -i %s" % filepath 89 | p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) 90 | di = p.communicate() 91 | for line in di: 92 | if line.rfind(b"Duration") > 0: 93 | duration = str(re.findall(b"Duration: (\d+:\d+:[\d.]+)", line)[0]) 94 | return 3600 * int(duration[2: 4]) + 60 * int(duration[5: 7]) + int(duration[8: 10]) 95 | 96 | def check_dimension(f): # if f is bigger than 50MB split it in subvideos 97 | if os.path.getsize(f) > 50 * 1024 * 1023: 98 | duration = get_duration(f) 99 | for i in range(0, duration, 180): 100 | start = strftime("%H:%M:%S", strptime('{0} {1} {2}'.format(i // 3600, (i // 60) % 60, i % 60), "%H %M %S")) # TODO this is not pythonic code! 101 | os.system("""avconv -i '{0}' -vcodec copy -acodec copy -ss {1} -t {2} 'part_{3}.mp4'""".format(f, start, 180, (i // 180) % 180)) 102 | os.remove(f) # delete original file 103 | ''' 104 | --------------------------------------------------------------------------------