diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..84e6de5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM debian:latest + +RUN apt update && apt upgrade -y +RUN apt install git curl python3-pip ffmpeg -y +RUN pip3 install -U pip +RUN cd / +RUN git clone https://github.com/subinps/MusicPlayer.git +RUN cd MusicPlayer +WORKDIR /MusicPlayer +RUN pip3 install -U -r requirements.txt +CMD python3 main.py \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..73ca649 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 SUBIN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..eb131d6 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +worker: python3 main.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..3898a0e --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Telegram Voice Chat Bot with Channel Support. + +A Telegram Bot to Play Audio in Voice Chats With Youtube and Deezer support. +Supports Live streaming from youtube + +``` +Please fork this repository don't import code +Made with Python3 +(C) @subinps +Copyright permission under MIT License +License -> https://github.com/subinps/MusicPlayer/blob/master/LICENSE + +``` + +## Deploy to Heroku + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/subinps/MusicPlayer) + + +### Deploy to VPS + +```sh +git clone https://github.com/subinps/MusicPlayer +cd MusicPlayer +pip3 install -r requirements.txt +# +python3 main.py +``` + +# Vars: +1. `API_ID` : Get From my.telegram.org +2. `API_HASH` : Get from my.telegram.org +3. `BOT_TOKEN` : @Botfather +4. `SESSION_STRING` : Generate From here [![GenerateStringName](https://img.shields.io/badge/repl.it-generateStringName-yellowgreen)](https://repl.it/@subinps/getStringName) +5. `CHAT` : ID of Channel/Group where the bot plays Music. +6. `LOG_GROUP` : Group to send Playlist, if CHAT is a Group +7. `ADMINS` : ID of users who can use admin commands. +8. `ARQ_API` : Get it for free from [@ARQRobot](https://telegram.dog/ARQRobot), This is required for /dplay to work. +8. `STREAM_URL` : Stream URL of radio station or a youtube live video to stream when the bot starts or with /radio command. + +- Enable the worker after deploy the project to Heroku +- Bot will starts radio automatically in given `CHAT` with given `STREAM_URL` after deploy.(24*7 Music even if heroku restarts, radio stream restarts automatically.) +- To play a song use /play as a reply to audio file or a youtube link. +- Use /play to play song from youtube and /dplay to play from Deezer. +- Use /help to know about other commands. + +**Features** + +- Playlist, queue +- Supports Live streaming from youtube +- Supports both deezer and youtube to search songs. +- Play from telegram file supported. +- Starts Radio after if no songs in playlist. +- Automatically downloads audio for the first two tracks in the playlist to ensure smooth playing +- Automatic restart even if heroku restarts. + +### Note + +``` +Contributions are welcomed, But Kanging and editing a few lines wont make you a Developer. +Fork the repo, Do not Import code. + +``` +#### Support + +Connect Me On [Telegram](https://telegram.dog/subinps_bot) + +## Credits +- [Dash Eclipse's](https://github.com/dashezup) for his[tgvc-userbot](https://github.com/callsmusic/tgvc-userbot). + diff --git a/app.json b/app.json new file mode 100644 index 0000000..946f124 --- /dev/null +++ b/app.json @@ -0,0 +1,69 @@ +{ + "name": "Telegram Voice Chat Music Player Bot ", + "description": "Telegram Bot to Play Audio in Telegram Voice Chats", + "repository": "https://github.com/subinps/MusicPlayer", + "keywords": [ + "telegram", + "bot", + "voicechat", + "music", + "python", + "pyrogram", + "pytgcalls", + "tgcalls", + "voip" + ], + "env": { + "API_ID": { + "description": "api_id part of your Telegram API Key from my.telegram.org/apps", + "required": true + }, + "API_HASH": { + "description": "api_hash part of your Telegram API Key from my.telegram.org/apps", + "required": true + }, + "BOT_TOKEN": { + "description": "Bot token of Bot, get from @Botfather", + "required": true + }, + "ARQ_API": { + "description": "get it for free from @ARQRobot", + "required": false + }, + "SESSION_STRING": { + "description": "Session string, read the README to learn how to export it with Pyrogram", + "required": true + }, + "CHAT": { + "description": "ID of Channel or Group where the Bot plays Music", + "required": true + }, + "LOG_GROUP": { + "description": "ID of the group to send playlist If CHAT is a Group, if channel thenleave blank", + "required": false + }, + "ADMINS": { + "description": "ID of Users who can use Admin commands(for multiple users seperated by space)", + "required": true + }, + "STREAM_URL": { + "description": "URL of Radio station or Youtube live video url to stream with /radio command", + "value": "https://youtu.be/zcrUCvBD16k", + "required": false +} + }, + "formation": { + "worker": { + "quantity": 1, + "size": "free" + } + }, + "buildpacks": [ + { + "url": "https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest" + }, + { + "url": "heroku/python" + } + ] +} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..fa869e6 --- /dev/null +++ b/config.py @@ -0,0 +1,62 @@ +#MIT License + +#Copyright (c) 2021 SUBIN + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. +import os +import re +from youtube_dl import YoutubeDL +ydl_opts = { + "geo-bypass": True, + "nocheckcertificate": True + } +ydl = YoutubeDL(ydl_opts) +links=[] +finalurl="" +STREAM=os.environ.get("STREAM_URL", "https://youtu.be/zcrUCvBD16k") +regex = r"^(https?\:\/\/)?(www\.youtube\.com|youtu\.?be)\/.+" +match = re.match(regex,STREAM) +if match: + meta = ydl.extract_info(STREAM, download=False) + formats = meta.get('formats', [meta]) + for f in formats: + links.append(f['url']) + finalurl=links[0] +else: + finalurl=STREAM + +class Config: + ADMIN = os.environ.get("ADMINS", '') + ADMINS = [int(admin) if re.search('^\d+$', admin) else admin for admin in (ADMIN).split()] + API_ID = int(os.environ.get("API_ID", '')) + CHAT = int(os.environ.get("CHAT", "")) + LOG_GROUP=os.environ.get("LOG_GROUP", "") + if LOG_GROUP: + LOG_GROUP=int(LOG_GROUP) + else: + LOG_GROUP=None + STREAM_URL=finalurl + ARQ_API=os.environ.get("ARQ_API", "") + DURATION_LIMIT=int(os.environ.get("DUR", 15)) + API_HASH = os.environ.get("API_HASH", "") + BOT_TOKEN = os.environ.get("BOT_TOKEN", "") + SESSION = os.environ.get("SESSION_STRING", "") + playlist=[] + msg = {} + diff --git a/main.py b/main.py new file mode 100644 index 0000000..83514fa --- /dev/null +++ b/main.py @@ -0,0 +1,148 @@ +#MIT License + +#Copyright (c) 2021 SUBIN + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. +from pyrogram import Client, idle, filters +import os +from threading import Thread +import sys +from config import Config +from utils import mp +import asyncio +from pyrogram.raw import functions, types + + +CHAT=Config.CHAT +bot = Client( + "Musicplayer", + Config.API_ID, + Config.API_HASH, + bot_token=Config.BOT_TOKEN, + plugins=dict(root="plugins") +) +async def main(): + async with bot: + await mp.startupradio() + await asyncio.sleep(2) + await mp.startupradio() + +def stop_and_restart(): + bot.stop() + os.execl(sys.executable, sys.executable, *sys.argv) + +bot.run(main()) +bot.start() +bot.send( + functions.bots.SetBotCommands( + commands=[ + types.BotCommand( + command="start", + description="Check if bot alive" + ), + types.BotCommand( + command="help", + description="Shows help message" + ), + types.BotCommand( + command="play", + description="Play song from youtube/audiofile" + ), + types.BotCommand( + command="dplay", + description="Play song from Deezer" + ), + types.BotCommand( + command="player", + description="Shows current playing song with controls" + ), + types.BotCommand( + command="playlist", + description="Shows the playlist" + ), + types.BotCommand( + command="skip", + description="Skip the current song" + ), + types.BotCommand( + command="join", + description="Join VC" + ), + types.BotCommand( + command="leave", + description="Leave from VC" + ), + types.BotCommand( + command="vc", + description="Ckeck if VC is joined" + ), + types.BotCommand( + command="stop", + description="Stops Playing" + ), + types.BotCommand( + command="radio", + description="Start radio / Live stream" + ), + types.BotCommand( + command="stopradio", + description="Stops radio/Livestream" + ), + types.BotCommand( + command="replay", + description="Replay from beggining" + ), + types.BotCommand( + command="clean", + description="Cleans RAW files" + ), + types.BotCommand( + command="pause", + description="Pause the song" + ), + types.BotCommand( + command="resume", + description="Resume the paused song" + ), + types.BotCommand( + command="mute", + description="Mute in VC" + ), + types.BotCommand( + command="unmute", + description="Unmute in VC" + ), + types.BotCommand( + command="restart", + description="Restart the bot" + ) + ] + ) +) + + +@bot.on_message(filters.command("restart") & filters.user(Config.ADMINS)) +def restart(client, message): + message.reply_text("Restarting...") + Thread( + target=stop_and_restart + ).start() + +idle() +bot.stop() diff --git a/plugins/callback.py b/plugins/callback.py new file mode 100644 index 0000000..4e653fa --- /dev/null +++ b/plugins/callback.py @@ -0,0 +1,188 @@ +#MIT License + +#Copyright (c) 2021 SUBIN + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. + +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery +from pyrogram import Client, emoji +from utils import mp +from config import Config +playlist=Config.playlist + +HELP = """ + +Add the bot and User account in your Group with admin rights. + +Start a VoiceChat + +Use /play or use /play as a reply to an audio file or youtube link. + +You can also use /dplay to play a song from Deezer. + +**Common Commands**: + +**/play** Reply to an audio file or YouTube link to play it or use /play . +**/dplay** Play music from Deezer, Use /dplay +**/player** Show current playing song. +**/help** Show help for commands +**/playlist** Shows the playlist. + +**Admin Commands**: +**/skip** [n] ... Skip current or n where n >= 2 +**/join** Join voice chat. +**/leave** Leave current voice chat +**/vc** Check which VC is joined. +**/stop** Stop playing. +**/radio** Start Radio. +**/stopradio** Stops Radio Stream. +**/replay** Play from the beginning. +**/clean** Remove unused RAW PCM files. +**/pause** Pause playing. +**/resume** Resume playing. +**/mute** Mute in VC. +**/unmute** Unmute in VC. +**/restart** Restarts the Bot. +""" + + +@Client.on_callback_query() +async def cb_handler(client: Client, query: CallbackQuery): + if query.from_user.id not in Config.ADMINS: + await query.answer( + "Who the hell you are", + show_alert=True + ) + return + else: + await query.answer() + if query.data == "replay": + group_call = mp.group_call + if not playlist: + return + group_call.restart_playout() + if not playlist: + pl = f"{emoji.NO_ENTRY} Empty Playlist" + else: + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await query.edit_message_reply_text( + f"{pl}", + parse_mode="Markdown", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton("🔄", callback_data="replay"), + InlineKeyboardButton("⏯", callback_data="pause"), + InlineKeyboardButton("⏩", callback_data="skip") + + ], + ] + ) + ) + + elif query.data == "pause": + if not playlist: + return + else: + mp.group_call.pause_playout() + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await query.edit_message_text(f"{emoji.PLAY_OR_PAUSE_BUTTON} Paused\n\n{pl}", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton("🔄", callback_data="replay"), + InlineKeyboardButton("⏯", callback_data="resume"), + InlineKeyboardButton("⏩", callback_data="skip") + + ], + ] + ) + ) + + + elif query.data == "resume": + if not playlist: + return + else: + mp.group_call.resume_playout() + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await query.edit_message_text(f"{emoji.PLAY_OR_PAUSE_BUTTON} Resumed\n\n{pl}", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton("🔄", callback_data="replay"), + InlineKeyboardButton("⏯", callback_data="pause"), + InlineKeyboardButton("⏩", callback_data="skip") + + ], + ] + ) + ) + + elif query.data=="skip": + if not playlist: + return + else: + await mp.skip_current_playing() + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + try: + await query.edit_message_text(f"{emoji.PLAY_OR_PAUSE_BUTTON} Skipped\n\n{pl}", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton("🔄", callback_data="replay"), + InlineKeyboardButton("⏯", callback_data="pause"), + InlineKeyboardButton("⏩", callback_data="skip") + + ], + ] + ) + ) + except: + pass + elif query.data=="help": + buttons = [ + [ + InlineKeyboardButton('⚙️ Update Channel', url='https://t.me/subin_works'), + InlineKeyboardButton('🤖 Other Bots', url='https://t.me/subin_works/122'), + ], + [ + InlineKeyboardButton('👨🏼‍💻 Developer', url='https://t.me/subinps'), + InlineKeyboardButton('🧩 Source', url='https://github.com/subinps/MusicPlayer'), + ] + ] + reply_markup = InlineKeyboardMarkup(buttons) + await query.edit_message_text( + HELP, + reply_markup=reply_markup + + ) + diff --git a/plugins/commands.py b/plugins/commands.py new file mode 100644 index 0000000..032c53c --- /dev/null +++ b/plugins/commands.py @@ -0,0 +1,102 @@ +#MIT License + +#Copyright (c) 2021 SUBIN + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram import Client, filters + + + +HOME_TEXT = "Helo, [{}](tg://user?id={})\n\nIam MusicPlayer 2.0 which plays music in Channels and Groups 24*7\n\nI can even Stream Youtube Live in Your Voicechat\n\nDeploy Your Own bot from source code below\n\nHit /help to know about available commands." +HELP = """ + +Add the bot and User account in your Group with admin rights. + +Start a VoiceChat + +Use /play or use /play as a reply to an audio file or youtube link. + +You can also use /dplay to play a song from Deezer. + +**Common Commands**: + +**/play** Reply to an audio file or YouTube link to play it or use /play . +**/dplay** Play music from Deezer, Use /dplay +**/player** Show current playing song. +**/help** Show help for commands +**/playlist** Shows the playlist. + +**Admin Commands**: +**/skip** [n] ... Skip current or n where n >= 2 +**/join** Join voice chat. +**/leave** Leave current voice chat +**/vc** Check which VC is joined. +**/stop** Stop playing. +**/radio** Start Radio. +**/stopradio** Stops Radio Stream. +**/replay** Play from the beginning. +**/clean** Remove unused RAW PCM files. +**/pause** Pause playing. +**/resume** Resume playing. +**/mute** Mute in VC. +**/unmute** Unmute in VC. +**/restart** Restarts the Bot. +""" + + + +@Client.on_message(filters.command('start')) +async def start(client, message): + buttons = [ + [ + InlineKeyboardButton('⚙️ Update Channel', url='https://t.me/subin_works'), + InlineKeyboardButton('🤖 Other Bots', url='https://t.me/subin_works/122'), + ], + [ + InlineKeyboardButton('👨🏼‍💻 Developer', url='https://t.me/subinps'), + InlineKeyboardButton('🧩 Source', url='https://github.com/subinps/MusicPlayer'), + ], + [ + InlineKeyboardButton('👨🏼‍🦯 Help', callback_data='help'), + + ] + ] + reply_markup = InlineKeyboardMarkup(buttons) + await message.reply(HOME_TEXT.format(message.from_user.first_name, message.from_user.id), reply_markup=reply_markup) + + + +@Client.on_message(filters.command("help")) +async def show_help(client, message): + buttons = [ + [ + InlineKeyboardButton('⚙️ Update Channel', url='https://t.me/subin_works'), + InlineKeyboardButton('🤖 Other Bots', url='https://t.me/subin_works/122'), + ], + [ + InlineKeyboardButton('👨🏼‍💻 Developer', url='https://t.me/subinps'), + InlineKeyboardButton('🧩 Source', url='https://github.com/subinps/MusicPlayer'), + ] + ] + reply_markup = InlineKeyboardMarkup(buttons) + await message.reply_text( + HELP, + reply_markup=reply_markup + ) diff --git a/plugins/inline.py b/plugins/inline.py new file mode 100644 index 0000000..fdb55f9 --- /dev/null +++ b/plugins/inline.py @@ -0,0 +1,80 @@ +#MIT License + +#Copyright (c) 2021 SUBIN + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. +from pyrogram.handlers import InlineQueryHandler +from youtubesearchpython import VideosSearch +from pyrogram.types import InlineQueryResultArticle, InputTextMessageContent +from pyrogram import Client, errors + + +@Client.on_inline_query() +async def search(client, query): + answers = [] + string = query.query.lower().strip().rstrip() + + if string == "": + await client.answer_inline_query( + query.id, + results=answers, + switch_pm_text=("Search a youtube video"), + switch_pm_parameter="help", + cache_time=0 + ) + return + else: + videosSearch = VideosSearch(string.lower(), limit=50) + for v in videosSearch.result()["result"]: + answers.append( + InlineQueryResultArticle( + title=v["title"], + description=("Duration: {} Views: {}").format( + v["duration"], + v["viewCount"]["short"] + ), + input_message_content=InputTextMessageContent( + "/play https://www.youtube.com/watch?v={}".format( + v["id"] + ) + ), + thumb_url=v["thumbnails"][0]["url"] + ) + ) + try: + await query.answer( + results=answers, + cache_time=0 + ) + except errors.QueryIdInvalid: + await query.answer( + results=answers, + cache_time=0, + switch_pm_text=("Nothing found"), + switch_pm_parameter="", + ) + + +__handlers__ = [ + [ + InlineQueryHandler( + search + ) + ] +] diff --git a/plugins/player.py b/plugins/player.py new file mode 100644 index 0000000..a493dab --- /dev/null +++ b/plugins/player.py @@ -0,0 +1,449 @@ +#MIT License + +#Copyright (c) 2021 SUBIN + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. +import os +from config import Config +from pyrogram import Client, filters, emoji +from pyrogram.methods.messages.download_media import DEFAULT_DOWNLOAD_DIR +from pyrogram.types import Message +from utils import mp, RADIO +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from Python_ARQ import ARQ +from youtube_search import YoutubeSearch +from pyrogram import Client +from aiohttp import ClientSession +import re + +LOG_GROUP=Config.LOG_GROUP + +DURATION_LIMIT = Config.DURATION_LIMIT +ARQ_API=Config.ARQ_API +session = ClientSession() +arq = ARQ("https://thearq.tech",ARQ_API,session) +playlist=Config.playlist + +ADMINS=Config.ADMINS +CHAT=Config.CHAT +LOG_GROUP=Config.LOG_GROUP +playlist=Config.playlist + +@Client.on_message(filters.command("play") | filters.audio & filters.private) +async def yplay(_, message: Message): + type="" + yturl="" + ysearch="" + if message.audio: + type="audio" + m_audio = message + elif message.reply_to_message and message.reply_to_message.audio: + type="audio" + m_audio = message.reply_to_message + else: + if message.reply_to_message: + link=message.reply_to_message.text + regex = r"^(https?\:\/\/)?(www\.youtube\.com|youtu\.?be)\/.+" + match = re.match(regex,link) + if match: + type="youtube" + yturl=message.text + elif " " in message.text: + text = message.text.split(" ", 1) + query = text[1] + regex = r"^(https?\:\/\/)?(www\.youtube\.com|youtu\.?be)\/.+" + match = re.match(regex,query) + if match: + type="youtube" + yturl=query + else: + type="query" + ysearch=query + else: + await message.reply_text("You Didn't gave me anything to play. Send me a audio file or reply /play to an audio file.") + return + if 1 in RADIO: + await mp.stop_radio() + user=f"[{message.from_user.first_name}](tg://user?id={message.from_user.id})" + group_call = mp.group_call + if not group_call.is_connected: + await mp.start_call() + if type=="audio": + if not group_call.is_connected: + await mp.start_call() + if playlist and playlist[-1][2] \ + == m_audio.audio.file_id: + await message.reply_text(f"{emoji.ROBOT} Already added in Playlist") + return + data={1:m_audio.audio.title, 2:m_audio.audio.file_id, 3:"telegram", 4:user} + playlist.append(data) + if len(playlist) == 1: + m_status = await message.reply_text( + f"{emoji.INBOX_TRAY} Downloading and Processing..." + ) + await mp.download_audio(playlist[0]) + file=playlist[0][1] + group_call.input_filename = os.path.join( + _.workdir, + DEFAULT_DOWNLOAD_DIR, + f"{file}.raw" + ) + + await m_status.delete() + print(f"- START PLAYING: {playlist[0][1]}") + if not playlist: + pl = f"{emoji.NO_ENTRY} Empty playlist" + else: + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await message.reply_text(pl) + for track in playlist[:2]: + await mp.download_audio(track) + if LOG_GROUP and message.chat.id != LOG_GROUP: + await mp.send_playlist() + if type=="youtube" or type=="query": + if type=="youtube": + ytquery=yturl + elif type=="query": + ytquery=ysearch + else: + return + msg = await message.reply_text("⚡️ **Fetching Song From YouTube...**") + try: + results = YoutubeSearch(ytquery, max_results=1).to_dict() + url = f"https://youtube.com{results[0]['url_suffix']}" + title = results[0]["title"][:40] + except Exception as e: + await msg.edit( + "Song not found.\nTry inline mode.." + ) + print(str(e)) + return + + data={1:title, 2:url, 3:"youtube", 4:user} + playlist.append(data) + group_call = mp.group_call + if not group_call.is_connected: + await mp.start_call() + client = group_call.client + if len(playlist) == 1: + m_status = await msg.edit( + f"{emoji.INBOX_TRAY} Downloading and Processing..." + ) + await mp.download_audio(playlist[0]) + file=playlist[0][1] + group_call.input_filename = os.path.join( + client.workdir, + DEFAULT_DOWNLOAD_DIR, + f"{file}.raw" + ) + + await m_status.delete() + print(f"- START PLAYING: {playlist[0][1]}") + if not playlist: + pl = f"{emoji.NO_ENTRY} Empty playlist" + else: + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await message.reply_text(pl) + for track in playlist[:2]: + await mp.download_audio(track) + if LOG_GROUP and message.chat.id != LOG_GROUP: + await mp.send_playlist() + + + +@Client.on_message(filters.command("dplay")) +async def deezer(_, message): + user=f"[{message.from_user.first_name}](tg://user?id={message.from_user.id})" + if " " in message.text: + text = message.text.split(" ", 1) + query = text[1] + else: + await message.reply_text("You Didn't gave me anything to play use /dplay ") + return + if 1 in RADIO: + await mp.stop_radio() + user=f"[{message.from_user.first_name}](tg://user?id={message.from_user.id})" + group_call = mp.group_call + if not group_call.is_connected: + await mp.start_call() + msg = await message.reply("⚡️ **Fetching Song From Deezer...**") + try: + songs = await arq.deezer(query,1) + if not songs.ok: + await msg.edit(songs.result) + return + url = songs.result[0].url + title = songs.result[0].title + + except: + await msg.edit("No results found") + return + data={1:title, 2:url, 3:"deezer", 4:user} + playlist.append(data) + group_call = mp.group_call + if not group_call.is_connected: + await mp.start_call() + client = group_call.client + if len(playlist) == 1: + m_status = await msg.edit( + f"{emoji.INBOX_TRAY} Downloading and Processing..." + ) + await mp.download_audio(playlist[0]) + file=playlist[0][1] + group_call.input_filename = os.path.join( + client.workdir, + DEFAULT_DOWNLOAD_DIR, + f"{file}.raw" + ) + await m_status.delete() + print(f"- START PLAYING: {playlist[0][1]}") + if not playlist: + pl = f"{emoji.NO_ENTRY} Empty playlist" + else: + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await message.reply_text(pl) + for track in playlist[:2]: + await mp.download_audio(track) + if LOG_GROUP and message.chat.id != LOG_GROUP: + await mp.send_playlist() + + +@Client.on_message(filters.command("player")) +async def player(_, m: Message): + if not playlist: + await m.reply_text(f"{emoji.NO_ENTRY} No songs are playing") + return + else: + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await m.reply_text( + pl, + parse_mode="Markdown", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton("🔄", callback_data="replay"), + InlineKeyboardButton("⏯", callback_data="pause"), + InlineKeyboardButton("⏩", callback_data="skip") + + ], + + ] + ) + ) + +@Client.on_message(filters.command("skip") & filters.user(ADMINS)) +async def skip_track(_, m: Message): + group_call = mp.group_call + if not group_call.is_connected: + await m.reply("Nothing Playing") + return + if len(m.command) == 1: + await mp.skip_current_playing() + if not playlist: + pl = f"{emoji.NO_ENTRY} Empty playlist" + else: + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await m.reply_text(pl) + if LOG_GROUP and m.chat.id != LOG_GROUP: + await mp.send_playlist() + else: + try: + items = list(dict.fromkeys(m.command[1:])) + items = [int(x) for x in items if x.isdigit()] + items.sort(reverse=True) + text = [] + for i in items: + if 2 <= i <= (len(playlist) - 1): + audio = f"{playlist[i].audio.title}" + playlist.pop(i) + text.append(f"{emoji.WASTEBASKET} {i}. **{audio}**") + else: + text.append(f"{emoji.CROSS_MARK} {i}") + await m.reply_text("\n".join(text)) + if not playlist: + pl = f"{emoji.NO_ENTRY} Empty Playlist" + else: + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await m.reply_text(pl) + if LOG_GROUP and m.chat.id != LOG_GROUP: + await mp.send_playlist() + except (ValueError, TypeError): + await m.reply_text(f"{emoji.NO_ENTRY} Invalid input", + disable_web_page_preview=True) + + +@Client.on_message(filters.command("join") & filters.user(ADMINS)) +async def join_group_call(client, m: Message): + group_call = mp.group_call + if group_call.is_connected: + await m.reply_text(f"{emoji.ROBOT} Already joined voice chat") + return + await mp.start_call() + chat = await client.get_chat(CHAT) + await m.reply_text(f"Succesfully Joined Voice Chat in {chat.title}") + + +@Client.on_message(filters.command("leave") & filters.user(ADMINS)) +async def leave_voice_chat(_, m: Message): + group_call = mp.group_call + if not group_call.is_connected: + await m.reply_text("Not joined any Voicechat yet.") + return + playlist.clear() + group_call.input_filename = '' + await group_call.stop() + await m.reply_text("Left the VoiceChat") + + +@Client.on_message(filters.command("vc") & filters.user(ADMINS)) +async def list_voice_chat(client, m: Message): + group_call = mp.group_call + if group_call.is_connected: + chat_id = int("-100" + str(group_call.full_chat.id)) + chat = await client.get_chat(chat_id) + await m.reply_text( + f"{emoji.MUSICAL_NOTES} **Currently in the voice chat**:\n" + f"- **{chat.title}**" + ) + else: + await m.reply_text(emoji.NO_ENTRY + + "Didn't join any voice chat yet") + + +@Client.on_message(filters.command("stop") & filters.user(ADMINS)) +async def stop_playing(_, m: Message): + group_call = mp.group_call + if not group_call.is_connected: + await m.reply_text("Nothing playing to stop.") + return + group_call.stop_playout() + await m.reply_text(f"{emoji.STOP_BUTTON} Stopped playing") + playlist.clear() + + +@Client.on_message(filters.command("replay") & filters.user(ADMINS)) +async def restart_playing(_, m: Message): + group_call = mp.group_call + if not group_call.is_connected: + await m.reply_text("Nothing playing to replay.") + return + if not playlist: + return + group_call.restart_playout() + await m.reply_text( + f"{emoji.COUNTERCLOCKWISE_ARROWS_BUTTON} " + "Playing from the beginning..." + ) + + +@Client.on_message(filters.command("pause") & filters.user(ADMINS)) +async def pause_playing(_, m: Message): + group_call = mp.group_call + if not group_call.is_connected: + await m.reply_text("Nothing playing to pause.") + return + mp.group_call.pause_playout() + await m.reply_text(f"{emoji.PLAY_OR_PAUSE_BUTTON} Paused", + quote=False) + + + +@Client.on_message(filters.command("resume") & filters.user(ADMINS)) +async def resume_playing(_, m: Message): + if not mp.group_call.is_connected: + await m.reply_text("Nothing paused to resume.") + return + mp.group_call.resume_playout() + await m.reply_text(f"{emoji.PLAY_OR_PAUSE_BUTTON} Resumed", + quote=False) + +@Client.on_message(filters.command("clean") & filters.user(ADMINS)) +async def clean_raw_pcm(client, m: Message): + download_dir = os.path.join(client.workdir, DEFAULT_DOWNLOAD_DIR) + all_fn: list[str] = os.listdir(download_dir) + for track in playlist[:2]: + track_fn = f"{track[1]}.raw" + if track_fn in all_fn: + all_fn.remove(track_fn) + count = 0 + if all_fn: + for fn in all_fn: + if fn.endswith(".raw"): + count += 1 + os.remove(os.path.join(download_dir, fn)) + await m.reply_text(f"{emoji.WASTEBASKET} Cleaned {count} files") + + +@Client.on_message(filters.command("mute") & filters.user(ADMINS)) +async def mute(_, m: Message): + group_call = mp.group_call + if not group_call.is_connected: + await m.reply_text("Nothing playing to mute.") + return + group_call.set_is_mute(True) + await m.reply_text(f"{emoji.MUTED_SPEAKER} Muted") + + +@Client.on_message(filters.command("unmute") & filters.user(ADMINS)) +async def unmute(_, m: Message): + group_call = mp.group_call + if not group_call.is_connected: + await m.reply_text("Nothing playing to mute.") + return + group_call.set_is_mute(False) + await m.reply_text(f"{emoji.SPEAKER_MEDIUM_VOLUME} Unmuted") + +@Client.on_message(filters.command("playlist")) +async def show_playlist(_, m: Message): + group_call = mp.group_call + if not group_call.is_connected: + await m.reply_text("No active Voicechat.") + return + if not playlist: + pl = f"{emoji.NO_ENTRY} Empty Playlist" + else: + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" + for i, x in enumerate(playlist) + ]) + await m.reply_text(pl) + +admincmds=["join", "unmute", "mute", "leave", "clean", "vc", "pause", "resume", "stop", "skip", "radio", "stopradio", "replay", "restart"] + +@Client.on_message(filters.command(admincmds) & ~filters.user(ADMINS)) +async def notforu(_, m: Message): + await m.reply("Who the hell you are") diff --git a/plugins/radio.py b/plugins/radio.py new file mode 100644 index 0000000..d441481 --- /dev/null +++ b/plugins/radio.py @@ -0,0 +1,44 @@ +#MIT License + +#Copyright (c) 2021 SUBIN + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. +from pyrogram import Client, filters +from pyrogram.types import Message +from utils import mp, RADIO +from config import Config +from config import STREAM + +ADMINS=Config.ADMINS + +@Client.on_message(filters.command("radio") & filters.user(ADMINS)) +async def radio(client, message: Message): + if 1 in RADIO: + await message.reply_text("Kindly stop existing Radio Stream /stopradio") + return + await mp.start_radio() + await message.reply_text(f"Started Radio: {STREAM}") + +@Client.on_message(filters.command('stopradio') & filters.user(ADMINS)) +async def stop(_, message: Message): + if 0 in RADIO: + await message.reply_text("Kindly start Radio First /radio") + return + await mp.stop_radio() + await message.reply_text("Radio stream ended.") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..158dea2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +Pyrogram==1.2.9 +TgCrypto==1.2.2 +ffmpeg-python +psutil +pytgcalls +wheel +python-arq +aiohttp +youtube_dl +youtube_search_python +youtube_search +wget diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..30a1be6 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.9.2 diff --git a/user.py b/user.py new file mode 100644 index 0000000..0f92514 --- /dev/null +++ b/user.py @@ -0,0 +1,30 @@ +#MIT License + +#Copyright (c) 2021 SUBIN + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. +from config import Config +from pyrogram import Client + +USER = Client( + Config.SESSION, + Config.API_ID, + Config.API_HASH +) +USER.start() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..428739c --- /dev/null +++ b/utils.py @@ -0,0 +1,272 @@ +#MIT License + +#Copyright (c) 2021 SUBIN + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. +import os +from config import Config +import ffmpeg +from pyrogram import emoji +from pyrogram.methods.messages.download_media import DEFAULT_DOWNLOAD_DIR +from pytgcalls import GroupCall +import signal +from pyrogram import Client +from youtube_dl import YoutubeDL +from os import path +from user import USER +import wget +STREAM_URL=Config.STREAM_URL +CHAT=Config.CHAT +GROUP_CALLS = {} +FFMPEG_PROCESSES = {} +RADIO={6} +LOG_GROUP=Config.LOG_GROUP +DURATION_LIMIT=30 +playlist=Config.playlist +msg=Config.msg + +bot = Client( + "Musicplayervc", + Config.API_ID, + Config.API_HASH, + bot_token=Config.BOT_TOKEN +) +bot.start() + +class DurationLimitError(Exception): + pass + +ydl_opts = { + "format": "bestaudio[ext=m4a]", + "geo-bypass": True, + "nocheckcertificate": True, + "outtmpl": "downloads/%(id)s.%(ext)s", +} +ydl = YoutubeDL(ydl_opts) +def youtube(url: str) -> str: + info = ydl.extract_info(url, False) + duration = round(info["duration"] / 60) + + if duration > DURATION_LIMIT: + raise DurationLimitError( + f"❌ Videos longer than {DURATION_LIMIT} minute(s) aren't allowed, the provided video is {duration} minute(s)" + ) + try: + ydl.download([url]) + except: + raise DurationLimitError( + f"❌ Videos longer than {DURATION_LIMIT} minute(s) aren't allowed, the provided video is {duration} minute(s)" + ) + return path.join("downloads", f"{info['id']}.{info['ext']}") + +class MusicPlayer(object): + def __init__(self): + self.group_call = GroupCall(USER, path_to_log_file='') + self.chat_id = None + + async def send_playlist(self): + if not playlist: + pl = f"{emoji.NO_ENTRY} Empty playlist" + else: + pl = f"{emoji.PLAY_BUTTON} **Playlist**:\n" + "\n".join([ + f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}\n" + for i, x in enumerate(playlist) + ]) + if msg.get('playlist') is not None: + await msg['playlist'].delete() + msg['playlist'] = await self.send_text(pl) + + async def skip_current_playing(self): + group_call = self.group_call + if not playlist: + return + if len(playlist) == 1: + await mp.start_radio() + return + client = group_call.client + download_dir = os.path.join(client.workdir, DEFAULT_DOWNLOAD_DIR) + group_call.input_filename = os.path.join( + download_dir, + f"{playlist[1][1]}.raw" + ) + # remove old track from playlist + old_track = playlist.pop(0) + print(f"- START PLAYING: {playlist[0][1]}") + if LOG_GROUP: + await self.send_playlist() + os.remove(os.path.join( + download_dir, + f"{old_track[1]}.raw") + ) + if len(playlist) == 1: + return + await self.download_audio(playlist[1]) + + async def send_text(self, text): + group_call = self.group_call + client = group_call.client + chat_id = LOG_GROUP + message = await bot.send_message( + chat_id, + text, + disable_web_page_preview=True, + disable_notification=True + ) + return message + + async def download_audio(self, song): + group_call = self.group_call + client = group_call.client + raw_file = os.path.join(client.workdir, DEFAULT_DOWNLOAD_DIR, + f"{song[1]}.raw") + if not os.path.isfile(raw_file): + if song[3] == "telegram": + original_file = await bot.download_media(f"{song[2]}") + ffmpeg.input(original_file).output( + raw_file, + format='s16le', + acodec='pcm_s16le', + ac=2, + ar='48k', + loglevel='error' + ).overwrite_output().run() + os.remove(original_file) + elif song[3] == "youtube": + original_file = youtube(song[2]) + ffmpeg.input(original_file).output( + raw_file, + format='s16le', + acodec='pcm_s16le', + ac=2, + ar='48k', + loglevel='error' + ).overwrite_output().run() + os.remove(original_file) + else: + original_file=wget.download(song[2]) + ffmpeg.input(original_file).output( + raw_file, + format='s16le', + acodec='pcm_s16le', + ac=2, + ar='48k', + loglevel='error' + ).overwrite_output().run() + os.remove(original_file) + + + async def start_radio(self): + group_call = mp.group_call + if group_call.is_connected: + playlist.clear() + group_call.input_filename = '' + await group_call.stop() + process = FFMPEG_PROCESSES.get(CHAT) + if process: + process.send_signal(signal.SIGTERM) + station_stream_url = STREAM_URL + group_call.input_filename = f'radio-{CHAT}.raw' + await group_call.start(CHAT) + try: + RADIO.remove(0) + except: + pass + try: + RADIO.add(1) + except: + pass + process = ffmpeg.input(station_stream_url).output( + group_call.input_filename, + format='s16le', + acodec='pcm_s16le', + ac=2, + ar='48k' + ).overwrite_output().run_async() + FFMPEG_PROCESSES[CHAT] = process + + + + async def stop_radio(self): + if 0 in RADIO: + return + group_call = mp.group_call + if group_call: + playlist.clear() + group_call.input_filename = '' + await group_call.stop() + try: + RADIO.remove(1) + except: + pass + try: + RADIO.add(0) + except: + pass + process = FFMPEG_PROCESSES.get(CHAT) + if process: + process.send_signal(signal.SIGTERM) + + async def start_call(self): + group_call = mp.group_call + await group_call.start(CHAT) + + async def startupradio(self): + group_call = mp.group_call + if group_call: + group_call.stop_playout() + playlist.clear() + group_call.input_filename = f'radio-{CHAT}.raw' + process = FFMPEG_PROCESSES.get(CHAT) + if process: + process.send_signal(signal.SIGTERM) + station_stream_url = STREAM_URL + await group_call.start(CHAT) + try: + RADIO.add(1) + except: + pass + process = ffmpeg.input(station_stream_url).output( + group_call.input_filename, + format='s16le', + acodec='pcm_s16le', + ac=2, + ar='48k' + ).overwrite_output().run_async() + FFMPEG_PROCESSES[CHAT] = process + + +mp = MusicPlayer() + + +# pytgcalls handlers + +@mp.group_call.on_network_status_changed +async def network_status_changed_handler(gc: GroupCall, is_connected: bool): + if is_connected: + mp.chat_id = int("-100" + str(gc.full_chat.id)) + else: + mp.chat_id = None + + +@mp.group_call.on_playout_ended +async def playout_ended_handler(_, __): + if not playlist: + await mp.start_radio() + else: + await mp.skip_current_playing()