From 65a6bfdd6470a4179efb11d8f6cfacc837f78a47 Mon Sep 17 00:00:00 2001 From: subinps <64341611+subinps@users.noreply.github.com> Date: Wed, 9 Jun 2021 00:15:59 +0530 Subject: [PATCH] Release V2.0 added ytdl added deezer play added youtube live streaming fixed LOG_GROUP added requester name in playlist added LICENCE added Dockerfile fixed stopradio error added inline search added commands --- Dockerfile | 11 ++ LICENSE | 21 +++ Procfile | 1 + README.md | 70 +++++++ app.json | 69 +++++++ config.py | 62 ++++++ main.py | 148 +++++++++++++++ plugins/callback.py | 188 +++++++++++++++++++ plugins/commands.py | 102 ++++++++++ plugins/inline.py | 80 ++++++++ plugins/player.py | 449 ++++++++++++++++++++++++++++++++++++++++++++ plugins/radio.py | 44 +++++ requirements.txt | 12 ++ runtime.txt | 1 + user.py | 30 +++ utils.py | 272 +++++++++++++++++++++++++++ 16 files changed, 1560 insertions(+) create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Procfile create mode 100644 README.md create mode 100644 app.json create mode 100644 config.py create mode 100644 main.py create mode 100644 plugins/callback.py create mode 100644 plugins/commands.py create mode 100644 plugins/inline.py create mode 100644 plugins/player.py create mode 100644 plugins/radio.py create mode 100644 requirements.txt create mode 100644 runtime.txt create mode 100644 user.py create mode 100644 utils.py 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()