From eb84d9fa6b80844285551d84b6762704bbbf1c47 Mon Sep 17 00:00:00 2001 From: subinps <64341611+subinps@users.noreply.github.com> Date: Sun, 3 Oct 2021 21:31:42 +0530 Subject: [PATCH] Release V2 Now supports both audio and video. (You can easily shift audio and video mode using /settings) Added ability to turn of the 24/7 play mode.(player will leave the call if playlist is empty) Added Recording Support (An attempt to overcome 4 hour telegram limit is made and may not be stable). You can set up to forward recordings to a channel. Added Schedule stream support (You can schedule a stream use /schedule command) Now you can control the video quality by setting QUALITY var, [high, medium and low] Added MongoDb Database support (This is an optional variable and I recommend you to use the bot with database. Many of features like /record /settings and /schedule needs a mongodb database for proper functioning.) Now you can promote a member to control your vcplayer using /vcpromote and /vcdemote command. Added admin cache to update admin list of chat (/refresh) Implemented a lot of callback buttons for easier configuration and controlling.(try out /volume, /settings, /record) Many variables moved to database, and now doesn't require the player restart. Added option to change CHAT config easily by sending any command in new CHAT. Fixes: Fixed /seek command skipping song. Edit title fixed. Fixed Lag while playing telegram files. Fixed Anonymous admins cant use commands. Fixed some errors in /stream command --- .gitignore | 1 + README.md | 43 +- app.json | 31 +- config.py | 438 ++++++++--- database.py | 85 +++ font.ttf | Bin 0 -> 46496 bytes logger.py | 8 +- main.py | 49 +- plugins/callback.py | 654 ++++++++++++++--- plugins/commands.py | 342 +++++++-- plugins/controls.py | 269 +++++-- plugins/export_import.py | 80 +- plugins/inline.py | 13 +- plugins/manage_admins.py | 120 +++ plugins/player.py | 509 +++++++++---- plugins/recorder.py | 89 +++ plugins/scheduler.py | 302 ++++++++ requirements.txt | 7 +- start.sh | 9 +- userplugins/group_call.py | 267 +++++++ userplugins/pm_reply.py | 47 -- utils.py | 1466 ++++++++++++++++++++++++++++++------- 22 files changed, 3956 insertions(+), 873 deletions(-) create mode 100644 database.py create mode 100644 font.ttf create mode 100644 plugins/manage_admins.py create mode 100644 plugins/recorder.py create mode 100644 plugins/scheduler.py create mode 100644 userplugins/group_call.py delete mode 100644 userplugins/pm_reply.py diff --git a/.gitignore b/.gitignore index 74ad6307..f30f818b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.session *.session-journal test.py +todo.txt diff --git a/README.md b/README.md index b4764b98..f3b03e82 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![GitHub contributors](https://img.shields.io/github/contributors/subinps/VCPlayerBot?style=flat) ![GitHub forks](https://img.shields.io/github/forks/subinps/VCPlayerBot?style=flat) -Telegram bot to stream videos in telegram voicechat for both groups and channels. Supports live streams, YouTube videos and telegram media. +Telegram bot to stream videos in telegram voicechat for both groups and channels. Supports live streams, YouTube videos and telegram media. With record stream support, Schedule streams, and many more. ## Config Vars: ### Mandatory Vars @@ -15,14 +15,33 @@ Telegram bot to stream videos in telegram voicechat for both groups and channels 3. `BOT_TOKEN` : [@Botfather](https://telegram.dog/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. + +## Recommended Optional Vars + +1. `DATABASE_URI`: MongoDB database Url, get from [mongodb](https://cloud.mongodb.com). This is an optional var, but it is recomonded to use this to experiance the full features. +2. `HEROKU_API_KEY`: Your heroku api key. Get one from [here](https://dashboard.heroku.com/account/applications/authorizations/new) +3. `HEROKU_APP_NAME`: Your heroku apps name. + ### Optional Vars 1. `LOG_GROUP` : Group to send Playlist, if CHAT is a Group() 2. `ADMINS` : ID of users who can use admin commands. 3. `STARTUP_STREAM` : This will be streamed on startups and restarts of bot. You can use either any STREAM_URL or a direct link of any video or a Youtube Live link. You can also use YouTube Playlist.Find a Telegram Link for your playlist from [PlayList Dumb](https://telegram.dog/DumpPlaylist) or get a PlayList from [PlayList Extract](https://telegram.dog/GetAPlaylistbot). The PlayList link should in form `https://t.me/DumpPlaylist/xxx`. -4. `REPLY_MESSAGE` : A reply to those who message the USER account in PM. Leave it blank if you do not need this feature. -5. `ADMIN_ONLY` : Pass `Y` If you want to make /play command only for admins of `CHAT`. By default /play is available for all. -6. `HEROKU_API_KEY`: Your heroku api key. Get one from [here](https://dashboard.heroku.com/account/applications/authorizations/new) -7. `HEROKU_APP_NAME`: Your heroku apps name. +4. `REPLY_MESSAGE` : A reply to those who message the USER account in PM. Leave it blank if you do not need this feature. (Configurable through bot if mongodb added.) +5. `ADMIN_ONLY` : Pass `True` If you want to make /play command only for admins of `CHAT`. By default /play is available for all.(Configurable through bot if mongodb added.) +6. `DATABASE_NAME`: Database name for your mongodb database. +7. `SHUFFLE` : Make it `False` if you dont want to shuffle playlists. (Configurable through bot if mongodb added.) +8. `EDIT_TITLE` : Make it `False` if you do not want the bot to edit video chat title according to playing song. (Configurable through bot if mongodb added.) +9. `RECORDING_DUMP` : A Channel ID with the USER account as admin, to dump video chat recordings. +10. `RECORDING_TITLE`: A custom title for your videochat recordings. +11. `TIME_ZONE` : Time Zone of your country, by default IST +12. `IS_VIDEO_RECORD` : Make it `False` if you do not want to record video, and only audio will be recorded.(Configurable through bot if mongodb added.) +13. `IS_LOOP` ; Make it `False` if you do not want 24 / 7 Video Chat. (Configurable through bot if mongodb added.) +14. `IS_VIDEO` : Make it `False` if you want to use the player as a musicplayer without video. (Configurable through bot if mongodb added.) +15. `PORTRAIT`: Make it `True` if you want the video recording in portrait mode. (Configurable through bot if mongodb added.) +16. `DELAY` : Choose the time limit for commands deletion. 10 sec by default. +18. `QUALITY` : Customize the quality of video chat, use one of `high`, `medium`, `low` . +19. `BITRATE` : Bitrate of audio (Not recommended to change). +20. `FPS` : Fps of video to be played (Not recommended to change.) @@ -53,20 +72,24 @@ python3 main.py ## Features - Playlist, queue. +- Supports Video Recording. +- Supports Scheduling voicechats. +- Cool UI for controling the player. +- Customizabe to audio or video. +- Custom quality for video chats. - Supports Play from Youtube Playlist. - Change VoiceChat title to current playing song name. - Supports Live streaming from youtube - 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. +- Automatic restart even if heroku restarts. (Configurable) - Support exporting and importing playlist. ### Note -[Note To A So Called Dev](https://telegram.dog/GetTGLink/802): +[Note To A So Called Dev](https://telegram.dog/subin_works/203): -Kanging this codes and and editing a few lines and releasing a V.x of your repo wont make you a Developer. +Kanging this codes and and editing a few lines and releasing a V.x or an [alpha](https://telegram.dog/subin_works/204), beta , gama branches of your repo wont make you a Developer. Fork the repo and edit as per your needs. ## LICENSE @@ -76,7 +99,7 @@ Fork the repo and edit as per your needs. ## CREDITS -- [py-tgcalls](https://github.com/pytgcalls/pytgcalls) +- [Laky-64](https://github.com/Laky-64) for [py-tgcalls](https://github.com/pytgcalls/pytgcalls) - [Dan](https://github.com/delivrance) for [Pyrogram](https://github.com/pyrogram/pyrogram) diff --git a/app.json b/app.json index 8097fadf..5877d5a8 100644 --- a/app.json +++ b/app.json @@ -39,13 +39,30 @@ "description": "ID of the group to send playlist If CHAT is a Group, if channel thenleave blank", "required": false }, + "QUALITY": { + "description": "Default quality of your video player, Use one of high, medium or low.", + "value": "high", + "required": false + }, + "DATABASE_URI": { + "description": "Mongo DB database URI , get from https://cloud.mongodb.com, even if this is optional, many of functions may not work if this is not set.", + "required": false + }, "ADMINS": { "description": "ID of Users who can use Admin commands(for multiple users seperated by space)", "required": true }, "ADMIN_ONLY": { - "description": "Change it to 'N' if you want to make /play and /dplay available for everyone. By default only admins of CHAT can use it.", - "value": "Y", + "description": "Change it to True if you want to make /play command available for everyone.", + "value": "False", + "required": false + }, + "HEROKU_API_KEY": { + "description": "Your heroku api key, get it from https://dashboard.heroku.com/account/applications/authorizations/new.", + "required": false + }, + "HEROKU_APP_NAME": { + "description": "Heroku App Name.", "required": false }, "STARTUP_STREAM": { @@ -64,13 +81,5 @@ "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 index 50c213cc..5001fdbd 100644 --- a/config.py +++ b/config.py @@ -12,11 +12,20 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from re import M from logger import LOGGER try: import os - import re import heroku3 + from ast import literal_eval as is_enabled + from pytgcalls.types.input_stream.quality import ( + HighQualityVideo, + HighQualityAudio, + MediumQualityAudio, + MediumQualityVideo, + LowQualityAudio, + LowQualityVideo + ) except ModuleNotFoundError: import os @@ -27,28 +36,6 @@ os.execl(sys.executable, sys.executable, *sys.argv) -Y_PLAY=False -YSTREAM=False -STREAM=os.environ.get("STARTUP_STREAM", "https://www.youtube.com/watch?v=zcrUCvBD16k") -regex = r"^(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)\&?" -match = re.match(regex,STREAM) -if match: - YSTREAM=True - finalurl=STREAM - LOGGER.warning("YouTube Stream is set as STARTUP STREAM") -elif STREAM.startswith("https://t.me/DumpPlaylist"): - try: - msg_id=STREAM.split("/", 4)[4] - finalurl=int(msg_id) - Y_PLAY=True - LOGGER.warning("YouTube Playlist is set as STARTUP STREAM") - except: - finalurl="http://j78dp346yq5r-hls-live.5centscdn.com/safari/live.stream/playlist.m3u8" - LOGGER.error("Unable to fetch youtube playlist, starting Safari TV") - pass -else: - finalurl=STREAM - class Config: #Telegram API Stuffs ADMIN = os.environ.get("ADMINS", '') @@ -58,25 +45,80 @@ class Config: API_HASH = os.environ.get("API_HASH", "") BOT_TOKEN = os.environ.get("BOT_TOKEN", "") SESSION = os.environ.get("SESSION_STRING", "") - BOT_USERNAME=None #Stream Chat and Log Group 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 - STREAM_URL=finalurl - YPLAY=Y_PLAY - YSTREAM=YSTREAM - + STREAM_URL=os.environ.get("STARTUP_STREAM", "https://www.youtube.com/watch?v=zcrUCvBD16k") + + #Database + DATABASE_URI=os.environ.get("DATABASE_URI", None) + DATABASE_NAME=os.environ.get("DATABASE_NAME", "VCPlayerBot") + #heroku API_KEY=os.environ.get("HEROKU_API_KEY", None) APP_NAME=os.environ.get("HEROKU_APP_NAME", None) + + #Optional Configuration + SHUFFLE=is_enabled(os.environ.get("SHUFFLE", 'True')) + ADMIN_ONLY=is_enabled(os.environ.get("ADMIN_ONLY", "False")) + REPLY_MESSAGE=os.environ.get("REPLY_MESSAGE", False) + EDIT_TITLE = os.environ.get("EDIT_TITLE", True) + #others + + RECORDING_DUMP=os.environ.get("RECORDING_DUMP", False) + RECORDING_TITLE=os.environ.get("RECORDING_TITLE", False) + TIME_ZONE = os.environ.get("TIME_ZONE", "Asia/Kolkata") + IS_VIDEO=is_enabled(os.environ.get("IS_VIDEO", 'True')) + IS_LOOP=is_enabled(os.environ.get("IS_LOOP", 'True')) + DELAY=int(os.environ.get("DELAY", '10')) + PORTRAIT=is_enabled(os.environ.get("PORTRAIT", 'False')) + IS_VIDEO_RECORD=is_enabled(os.environ.get("IS_VIDEO_RECORD", 'True')) + + #Quality vars + BITRATE=os.environ.get("BITRATE", False) + FPS=os.environ.get("FPS", False) + CUSTOM_QUALITY=os.environ.get("QUALITY", "HIGH") + + + + + #Dont touch these, these are not for configuring player + GET_FILE={} + DATA={} + STREAM_END={} + SCHEDULED_STREAM={} + DUR={} + msg = {} + + SCHEDULE_LIST=[] + playlist=[] + + ADMIN_CACHE=False + CALL_STATUS=False + YPLAY=False + YSTREAM=False + STREAM_SETUP=False + LISTEN=False + STREAM_LINK=False + IS_RECORDING=False + WAS_RECORDING=False + PAUSE=False + MUTED=False + HAS_SCHEDULE=None + IS_ACTIVE=None + VOLUME=100 + CURRENT_CALL=None + BOT_USERNAME=None + USER_ID=None + + if LOG_GROUP: + LOG_GROUP=int(LOG_GROUP) + else: + LOG_GROUP=None if not API_KEY or \ not APP_NAME: HEROKU_APP=None @@ -84,97 +126,283 @@ class Config: HEROKU_APP=heroku3.from_key(API_KEY).apps()[APP_NAME] - #Optional Configuration - SHUFFLE=bool(os.environ.get("SHUFFLE", True)) - ADMIN_ONLY=os.environ.get("ADMIN_ONLY", "N") - REPLY_MESSAGE=os.environ.get("REPLY_MESSAGE", None) + if EDIT_TITLE in ["NO", 'False']: + EDIT_TITLE=False + LOGGER.info("Title Editing turned off") if REPLY_MESSAGE: REPLY_MESSAGE=REPLY_MESSAGE - LOGGER.warning("Reply Message Found, Enabled PM MSG") + REPLY_PM=True + LOGGER.info("Reply Message Found, Enabled PM MSG") else: REPLY_MESSAGE=None - EDIT_TITLE = os.environ.get("EDIT_TITLE", True) - if EDIT_TITLE == "NO": - EDIT_TITLE=None - LOGGER.warning("Title Editing turned off") + REPLY_PM=False + + if BITRATE: + try: + BITRATE=int(BITRATE) + except: + LOGGER.error("Invalid bitrate specified.") + BITRATE=False + else: + BITRATE=False + + if FPS: + try: + FPS=int(FPS) + except: + LOGGER.error("Invalid FPS specified") + if BITRATE: + FPS=False + if not FPS <= 30: + FPS=False + else: + FPS=False + + if CUSTOM_QUALITY.lower() == 'high': + VIDEO_Q=HighQualityVideo() + AUDIO_Q=HighQualityAudio() + elif CUSTOM_QUALITY.lower() == 'medium': + VIDEO_Q=MediumQualityVideo() + AUDIO_Q=MediumQualityAudio() + elif CUSTOM_QUALITY.lower() == 'low': + VIDEO_Q=LowQualityVideo() + AUDIO_Q=LowQualityAudio() + else: + LOGGER.warning("Invalid QUALITY specified.Defaulting to High.") + VIDEO_Q=HighQualityVideo() + AUDIO_Q=HighQualityVideo() + + + #help strings + PLAY_HELP=""" +__You can play using any of these options__ + +1. Play a video from a YouTube link. + Command: **/play** + __You can use this as a reply to a YouTube link or pass link along command. or as a reply to message to search that in YouTube.__ + +2. Play from a telegram file. + Command: **/play** + __Reply to a supported media(video and documents or audio file ).__ + Note: __For both the cases /fplay also can be used by admins to play the song immediately without waiting for queue to end.__ +3. Play from a YouTube playlist + Command: **/yplay** + __First get a playlist file from @GetPlaylistBot or @DumpPlaylist and reply to playlist file.__ + +4. Live Stream + Command: **/stream** + __Pass a live stream URL or any direct URL to play it as stream.__ + +5. Import an old playlist. + Command: **/import** + __Reply to a previously exported playlist file. __ +""" + SETTINGS_HELP=""" +**You can easily customize you player as per you needs. The following configurations are available:** + +🔹Command: **/settings** + +🔹AVAILABLE CONFIGURATIONS: + +**Player Mode** - __This allows you to run your player as 24/7 music player or only when there is song in queue. +If disabled, player will leave from the call when the playlist is empty. +Otherwise STARTUP_STREAM will be streamed when playlist id empty.__ + +**Video Enabled** - __This allows you to switch between audio and video. +if disabled, video files will be played as audio.__ + +**Admin Only** - __Enabling this will restrict non-admin users from using play command.__ + +**Edit Title** - __Enabling this will edit your VideoChat title to current playing songs name.__ + +**Shuffle Mode** - __Enabling this will shuffle the playlist whenever you import a playlist or using /yplay __ + +**Auto Reply** - __Choose whether to reply the PM messages of playing user account. +You can set up a custom reply message using `REPLY_MESSAGE` confug.__ + +""" + SCHEDULER_HELP=""" +__VCPlayer allows you to schedule a stream. +This means you can schedule a stream for a future date and on the scheduled date, stream will be played automatically. +At present you can schedule a stream for even one year!!. Make sure you have set up a databse, else you will loose your schedules whenever the player restarts. __ + +Command: **/schedule** + +__Reply to a file or a youtube video or even a text message with schedule command. +The replied media or youtube video will be scheduled and will be played on the scheduled date. +The scheduling time is by default in IST and you can change the timezone using `TIME_ZONE` config.__ + +Command: **/slist** +__View your current scheduled streams.__ + +Command: **/cancel** +__Cancel a schedule by its schedule id, You can get the schedule id using /slist command__ + +Command: **/cancelall** +__Cancel all the scheduled streams__ +""" + RECORDER_HELP=""" +__With VCPlayer you can easily record all your video chats. +By default telegram allows you to record for a maximum duration of 4 hours. +An attempt to overcome this limit has been made by automatically restarting the recording after 4 hours__ + +Command: **/record** + +AVAILABLE CONFIGURATIONS: +1. Record Video: __If enabled both the video and audio of the stream will be recorded, otherwise only audio will be recorded.__ + +2. Video dimension: __Choose between portrait and landscape dimensions for your recording__ + +3. Custom Recording Title: __Set up a custom recording title for your recordings. Use a command /rtitle to configure this. +To turn off the custom title, use `/rtitle False `__ + +4. Recording Dumb: __You can set up forwarding all your recordings to a channel, this will be useful since otherwise recordings are sent to saved messages of streaming account. +Setup using `RECORDING_DUMP` config.__ + +⚠️ If you start a recording with vcplayer, make sure you stop the same with vcplayer. + +""" + + CONTROL_HELP=""" +__VCPlayer allows you to control your streams easily__ +1. Skip a song. + Command: **/skip** + __You can pass a number greater than 2 to skip the song in that position.__ - #others - ADMIN_CACHE=False - playlist=[] - msg = {} - FFMPEG_PROCESSES={} - GET_FILE={} - DATA={} - STREAM_END={} - CALL_STATUS=False - PAUSE=False - MUTED=False - STREAM_LINK=False - DUR={} - HELP=""" -How Can I Play Video? - -You have file options. - 1. Play a video from a YouTube link. - Command: /play - You can use this as a reply to a youtube link or pass link along command. - 2. Play from a telegram file. - Command: /play - Reply to a supported media(video and documents). - 3. Play from a YouTube playlist - Command: /yplay - First get a playlist file from @GetPlaylistBot or @DumpPlaylist and reply to playlist file. - 4. Live Stream - Command: /stream - Pass a live stream url or any direct url to play it as stream. - 5. Import an old playlist. - Command: /import - Reply to a previously exported plaulist file. - -How Can I Control Player? -These are commands to control player. - 1. Skip a song. - Command: /skip - You can pass a number greater than 2 to skip the song in that position. 2. Pause the player. - Command: /pause + Command: **/pause** + 3. Resume the player. - Command: /resume + Command: **/resume** + 4. Change Volume. - Command: /volume - Pass the volume in between 1-200. + Command: **/volume** + __Pass the volume in between 1-200.__ + 5. Leave the VC. - Command: /leave + Command: **/leave** + 6. Shuffle the playlist. - Command: /shuffle + Command: **/shuffle** + 7. Clear the current playlist queue. - Command: /clearplaylist + Command: **/clearplaylist** + 8. Seek the video. - Command: /seek - You can pass number of seconds to be skiped. Example: /seek 10 to skip 10 sec. /seek -10 to rewind 10 sec. + Command: **/seek** + __You can pass number of seconds to be skipped. Example: /seek 10 to skip 10 sec. /seek -10 to rewind 10 sec.__ + 9. Mute the player. - Command: /mute + Command: **/mute** + 10. Unmute the player. - Command : /unmute + Command : **/unmute** + 11. Shows the playlist. - Command: /playlist - Use /player to show with control buttons + Command: **/playlist** + __Use /player to show with control buttons__ +""" + + ADMIN_HELP=""" +__VCPlayer allows to control admins, that is you can add admins and remove them easily. +It is recommended to use a MongoDb database for better experience, else all you admins will get reset after restart.__ + +Command: **/vcpromote** +__You can promote a admin with their username or user id or by replying to that users message.__ + +Command: **/vcdemote** +__Remove an admin from admin list__ + +Command: **/refresh** +__Refresh the admin list of chat__ +""" + + MISC_HELP=""" +Command: **/export** +__VCPlayer allows you to export your current playlist for future use.__ +__A json file will be sent to you and the same can be used along /import command.__ + +Command : **/logs** +__If your player went something gone wrong, you can easily check the logs using /logs__ + +Command : **/env** +__Setup your config vars with /env command.__ +__Example: To set up a__ `REPLY_MESSAGE` __use__ `/env REPLY_MESSAGE=Hey, Check out @subin_works rather than spamming in my PM`__ +__You can delete a config var by ommiting a value for that, Example:__ `/env LOG_GROUP=` __this will delete the existing LOG_GROUP config. -How Can I Export My Current Playlist? - 1. Command: /export - To export current playlist for future use. +Command: **/config** +__Same as using /env** -Other Commands - 1. Update and restert the bot. - Command: /update or /restart - 2. Get Logs - Command: /logs - 3. Set / Change heroku config vars. - Command: /env - Set a new config var or change existing one or delete existing one. Example: /env CHAT=-100120202002 to change(if exist else set as new) CHAT config to -100120202002. If no value is passed, the var will be deleted. Example /env REPLY_MESSAGE= , this will delete the REPLY_MESSAGE var. +Command: **/update** +__Updates youe bot with latest changes__ -How Can I Stream In My Group - The source code of this bot is public and can be found at VCPlayerBot.\nYou can deploy your own bot and use in your group. +Tip: __You can easily change the CHAT config by adding the user account and bot account to any other group and any command in new group__ """ + ENV_HELP=""" +**These are the configurable vars available and you can set each one of them using /env command** + + +**Mandatory Vars** + +1. `API_ID` : __Get From [my.telegram.org](https://my.telegram.org/)__ + +2. `API_HASH` : __Get from [my.telegram.org](https://my.telegram.org)__ + +3. `BOT_TOKEN` : __[@Botfather](https://telegram.dog/BotFather)__ + +4. `SESSION_STRING` : __Generate From here [GenerateStringName](https://repl.it/@subinps/getStringName)__ + +5. `CHAT` : __ID of Channel/Group where the bot plays Music.__ + +6. `STARTUP_STREAM` : __This will be streamed on startups and restarts of bot. +You can use either any STREAM_URL or a direct link of any video or a Youtube Live link. +You can also use YouTube Playlist.Find a Telegram Link for your playlist from [PlayList Dumb](https://telegram.dog/DumpPlaylist) or get a PlayList from [PlayList Extract](https://telegram.dog/GetAPlaylistbot). +The PlayList link should in form `https://t.me/DumpPlaylist/xxx`.__ + +**Recommended Optional Vars** + +1. `DATABASE_URI`: __MongoDB database Url, get from [mongodb](https://cloud.mongodb.com). This is an optional var, but it is recomonded to use this to experiance the full features.__ + +2. `HEROKU_API_KEY`: __Your heroku api key. Get one from [here](https://dashboard.heroku.com/account/applications/authorizations/new)__ + +3. `HEROKU_APP_NAME`: __Your heroku app's name.__ + +**Other Optional Vars** +1. `LOG_GROUP` : __Group to send Playlist, if CHAT is a Group__ + +2. `ADMINS` : __ID of users who can use admin commands.__ +3. `REPLY_MESSAGE` : __A reply to those who message the USER account in PM. Leave it blank if you do not need this feature. (Configurable through buttons if mongodb added. Use /settings)__ + +4. `ADMIN_ONLY` : __Pass `True` If you want to make /play command only for admins of `CHAT`. By default /play is available for all.(Configurable through buttons if mongodb added. Use /settings)__ + +5. `DATABASE_NAME`: __Database name for your mongodb database.mongodb__ + +6. `SHUFFLE` : __Make it `False` if you dont want to shuffle playlists. (Configurable through buttons)__ + +7. `EDIT_TITLE` : __Make it `False` if you do not want the bot to edit video chat title according to playing song. (Configurable through buttons if mongodb added. Use /settings)__ + +8. `RECORDING_DUMP` : __A Channel ID with the USER account as admin, to dump video chat recordings.__ + +9. `RECORDING_TITLE`: __A custom title for your videochat recordings.__ + +10. `TIME_ZONE` : __Time Zone of your country, by default IST__ + +11. `IS_VIDEO_RECORD` : __Make it `False` if you do not want to record video, and only audio will be recorded.(Configurable through buttons if mongodb added. Use /record)__ + +12. `IS_LOOP` ; __Make it `False` if you do not want 24 / 7 Video Chat. (Configurable through buttons if mongodb added.Use /settings)__ + +13. `IS_VIDEO` : __Make it `False` if you want to use the player as a musicplayer without video. (Configurable through buttons if mongodb added. Use /settings)__ + +14. `PORTRAIT`: __Make it `True` if you want the video recording in portrait mode. (Configurable through buttons if mongodb added. Use /record)__ + +15. `DELAY` : __Choose the time limit for commands deletion. 10 sec by default.__ + +16. `QUALITY` : __Customize the quality of video chat, use one of `high`, `medium`, `low` . __ + +17. `BITRATE` : __Bitrate of audio (Not recommended to change).__ + +18. `FPS` : __Fps of video to be played (Not recommended to change.)__ + +""" diff --git a/database.py b/database.py new file mode 100644 index 00000000..cd63c4b3 --- /dev/null +++ b/database.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# Copyright (C) @subinps +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from logger import LOGGER +import motor.motor_asyncio +from config import Config + +class Database: + def __init__(self): + self._client = motor.motor_asyncio.AsyncIOMotorClient(Config.DATABASE_URI) + self.db = self._client[Config.DATABASE_NAME] + self.col = self.db.config + self.playlist = self.db.playlist + def new_config(self, name, value): + return dict( + name = name, + value = value, + ) + def add_config(self, name, value): + config = self.new_config(name, value) + self.col.insert_one(config) + + def new_song(self, id_, song): + return dict( + id = id_, + song = song, + ) + + def add_to_playlist(self, id_, song): + song_ = self.new_song(id_, song) + self.playlist.insert_one(song_) + + + async def is_saved(self, name): + config = await self.col.find_one({'name':name}) + return True if config else False + + async def edit_config(self, name, value): + await self.col.update_one({'name': name}, {'$set': {'value': value}}) + + async def get_config(self, name): + config = await self.col.find_one({'name':name}) + return config.get('value') + + async def del_config(self, name): + await self.col.delete_one({'name':name}) + return + + async def is_in_playlist(self, id_): + song = await self.playlist.find_one({'id':id_}) + return True if song else False + + + async def get_song(self, id_): + song_ = await self.playlist.find_one({'id':id_}) + return song_.get('song') + + async def del_song(self, id_): + await self.playlist.delete_one({'id':id_}) + return + + async def clear_playlist(self): + await self.playlist.drop() + return + + async def get_playlist(self): + k = self.playlist.find({}) + l=[] + async for song in k: + song_ = {int(k):v for k,v in song['song'].items()} + l.append(song_) + return l + +db=Database() \ No newline at end of file diff --git a/font.ttf b/font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8f7cb6e51debf1caf4964d89b4e8cb9c58a4c646 GIT binary patch literal 46496 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 ZRF|my2H}F)C!P++aWVWhoQb*f{{UOt9vA=s literal 0 HcmV?d00001 diff --git a/logger.py b/logger.py index 7b4fedf5..e635fffb 100644 --- a/logger.py +++ b/logger.py @@ -17,7 +17,7 @@ import logging logging.basicConfig( - level=logging.WARNING, + level=logging.INFO, format="[%(asctime)s - %(levelname)s] - %(name)s - %(message)s", datefmt='%d-%b-%y %H:%M:%S', handlers=[ @@ -29,8 +29,10 @@ logging.StreamHandler() ] ) -logging.getLogger("pyrogram").setLevel(logging.WARNING) -logging.getLogger("pytgcalls").setLevel(logging.WARNING) + +logging.getLogger("pyrogram").setLevel(logging.ERROR) +logging.getLogger("pytgcalls").setLevel(logging.ERROR) +logging.getLogger("apscheduler").setLevel(logging.ERROR) LOGGER=logging.getLogger(__name__) diff --git a/main.py b/main.py index eca2dcf7..1a121141 100644 --- a/main.py +++ b/main.py @@ -12,16 +12,24 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - -from utils import start_stream -from user import group_call +from utils import ( + play, + start_stream, + startup_check, + sync_from_db +) +from user import group_call, USER from logger import LOGGER from config import Config from pyrogram import idle from bot import bot import asyncio import os +if Config.DATABASE_URI: + from database import Database + db = Database() + if not os.path.isdir("./downloads"): os.makedirs("./downloads") else: @@ -31,12 +39,37 @@ async def main(): await bot.start() Config.BOT_USERNAME = (await bot.get_me()).username - await group_call.start() - await start_stream() - LOGGER.warning(f"{Config.BOT_USERNAME} Started.") + LOGGER.info(f"{Config.BOT_USERNAME} Started.") + try: + await group_call.start() + Config.USER_ID = (await USER.get_me()).id + if Config.DATABASE_URI: + if await db.is_saved("RESTART"): + msg=await db.get_config("RESTART") + if msg: + try: + k=await bot.edit_message_text(msg['chat_id'], msg['msg_id'], text="Succesfully restarted.") + await db.del_config("RESTART") + except: + pass + await sync_from_db() + k=await startup_check() + if k == False: + LOGGER.error("Startup checks not passed , bot is quiting") + await bot.stop() + await group_call.stop() + return + if Config.IS_LOOP: + if Config.playlist: + await play() + LOGGER.info("Loop play enabled and playlist is not empty, resuming playlist.") + else: + LOGGER.info("Loop play enabled , starting playing startup stream.") + await start_stream() + except Exception as e: + LOGGER.error(f"Startup was unsuccesfull, Errors - {e}") + pass await idle() - LOGGER.warning("Stoping") - await group_call.start() await bot.stop() if __name__ == '__main__': diff --git a/plugins/callback.py b/plugins/callback.py index 5301ee3b..1c504532 100644 --- a/plugins/callback.py +++ b/plugins/callback.py @@ -13,146 +13,566 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from utils import get_admins, get_buttons, get_playlist_str, mute, pause, restart_playout, resume, seek_file, shuffle_playlist, skip, unmute -from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery -from pyrogram.errors import MessageNotModified +from logger import LOGGER from pyrogram import Client +from contextlib import suppress from config import Config from asyncio import sleep -from logger import LOGGER +import datetime +import pytz +import calendar +from utils import ( + cancel_all_schedules, + delete_messages, + get_admins, + get_buttons, + get_playlist_str, + leave_call, + mute, + pause, + recorder_settings, + restart, + restart_playout, + resume, + schedule_a_play, + seek_file, + set_config, + settings_panel, + shuffle_playlist, + skip, + start_record_stream, + stop_recording, + sync_to_db, + unmute, + volume, + volume_buttons + ) +from pyrogram.types import ( + InlineKeyboardMarkup, + InlineKeyboardButton, + CallbackQuery +) +from pyrogram.errors import ( + MessageNotModified, + MessageIdInvalid, + QueryIdInvalid +) +from pyrogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup +) + +IST = pytz.timezone(Config.TIME_ZONE) @Client.on_callback_query() async def cb_handler(client: Client, query: CallbackQuery): - admins = await get_admins(Config.CHAT) - if query.from_user.id not in admins and query.data != "help": - await query.answer( - "😒 Played Joji.mp3", - show_alert=True - ) - return - if query.data == "shuffle": - if not Config.playlist: - await query.answer("Playlist is empty.", show_alert=True) + with suppress(MessageIdInvalid, MessageNotModified, QueryIdInvalid): + admins = await get_admins(Config.CHAT) + if query.data.startswith("info"): + me, you = query.data.split("_") + text="Join @subin_works" + if you == "volume": + await query.answer() + await query.message.edit_reply_markup(reply_markup=await volume_buttons()) + return + if you == "player": + if not Config.CALL_STATUS: + return await query.answer("Not Playing anything.", show_alert=True) + await query.message.edit_reply_markup(reply_markup=await get_buttons()) + await query.answer() + return + if you == "video": + text="Toggle your bot to Video / Audio Player." + elif you == "shuffle": + text="Enable or disable auto playlist shuffling" + elif you == "admin": + text="Enable to restrict the play command only for admins." + elif you == "mode": + text="Enabling Non- stop playback will make the player running 24 / 7 and automatic startup when restarting. " + elif you == "title": + text="Enable to edit the VideoChat title to Current playing song's title." + elif you == "reply": + text="Choose whether to auto-reply messaged for userbot. " + elif you == "videorecord": + text = "Enable to record both video and audio, if disabled only audio will be recorded." + elif you == "videodimension": + text = "Choose the recording video's dimensions" + elif you == "rectitle": + text = "A custom title for your chat recordings, Use /rtitle command to set a title" + elif you == "recdumb": + text = "A channel to which all the recordings are forwarded. Make sure The User account is admin over there. Set one using /env or /config." + await query.answer(text=text, show_alert=True) + + + if query.data.startswith("help"): + if query.message.chat.type != "private" and query.message.reply_to_message.from_user is None: + return await query.answer("I cant help you here, since you are an anonymous admin, message me in private chat.", show_alert=True) + elif query.message.chat.type != "private" and query.from_user.id != query.message.reply_to_message.from_user.id: + return await query.answer("Okda") + me, nyav = query.data.split("_") + back=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton("Back", callback_data="help_main"), + InlineKeyboardButton("Close", callback_data="close"), + ], + ] + ) + if nyav == 'main': + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(f"Play", callback_data='help_play'), + InlineKeyboardButton(f"Settings", callback_data=f"help_settings"), + InlineKeyboardButton(f"Recording", callback_data='help_record'), + ], + [ + InlineKeyboardButton("Scheduling", callback_data="help_schedule"), + InlineKeyboardButton("Controling", callback_data='help_control'), + InlineKeyboardButton("Admins", callback_data="help_admin"), + ], + [ + InlineKeyboardButton(f"Misc", callback_data='help_misc'), + InlineKeyboardButton("Config Vars", callback_data='help_env'), + InlineKeyboardButton("Close", callback_data="close"), + ], + ] + ) + await query.message.edit("Showing help menu, Choose from the below options.", reply_markup=reply_markup, disable_web_page_preview=True) + elif nyav == 'play': + await query.message.edit(Config.PLAY_HELP, reply_markup=back, disable_web_page_preview=True) + elif nyav == 'settings': + await query.message.edit(Config.SETTINGS_HELP, reply_markup=back, disable_web_page_preview=True) + elif nyav == 'schedule': + await query.message.edit(Config.SCHEDULER_HELP, reply_markup=back, disable_web_page_preview=True) + elif nyav == 'control': + await query.message.edit(Config.CONTROL_HELP, reply_markup=back, disable_web_page_preview=True) + elif nyav == 'admin': + await query.message.edit(Config.ADMIN_HELP, reply_markup=back, disable_web_page_preview=True) + elif nyav == 'misc': + await query.message.edit(Config.MISC_HELP, reply_markup=back, disable_web_page_preview=True) + elif nyav == 'record': + await query.message.edit(Config.RECORDER_HELP, reply_markup=back, disable_web_page_preview=True) + elif nyav == 'env': + await query.message.edit(Config.ENV_HELP, reply_markup=back, disable_web_page_preview=True) + + if not (query.from_user is None or query.from_user.id in admins): + await query.answer( + "😒 Played Joji.mp3", + show_alert=True + ) return - await shuffle_playlist() - await sleep(1) - try: - await query.message.edit_reply_markup(reply_markup=await get_buttons()) - except MessageNotModified: - pass - - elif query.data.lower() == "pause": - if Config.PAUSE: - await query.answer("Already Paused", show_alert=True) - else: - await pause() - await sleep(1) - try: + #scheduler stuffs + if query.data.startswith("sch"): + if query.message.chat.type != "private" and query.message.reply_to_message.from_user is None: + return await query.answer("You cant use scheduling here, since you are an anonymous admin. Schedule from private chat.", show_alert=True) + if query.message.chat.type != "private" and query.from_user.id != query.message.reply_to_message.from_user.id: + return await query.answer("Okda") + data = query.data + today = datetime.datetime.now(IST) + smonth=today.strftime("%B") + obj = calendar.Calendar() + thisday = today.day + year = today.year + month = today.month + if data.startswith("sch_month"): + none, none , yea_r, month_, day = data.split("_") + if yea_r == "choose": + year=int(year) + months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + button=[] + button_=[] + k=0 + for month in months: + k+=1 + year_ = year + if k < int(today.month): + year_ += 1 + button_.append([InlineKeyboardButton(text=f"{str(month)} {str(year_)}",callback_data=f"sch_showdate_{year_}_{k}")]) + else: + button.append([InlineKeyboardButton(text=f"{str(month)} {str(year_)}",callback_data=f"sch_showdate_{year_}_{k}")]) + button = button + button_ + button.append([InlineKeyboardButton("Close", callback_data="schclose")]) + await query.message.edit("Now Choose the month to schedule a voicechatㅤ ㅤㅤ", reply_markup=InlineKeyboardMarkup(button)) + elif day == "none": + return + else: + year = int(yea_r) + month = int(month_) + date = int(day) + datetime_object = datetime.datetime.strptime(str(month), "%m") + smonth = datetime_object.strftime("%B") + button=[] + if year == today.year and month == today.month and date == today.day: + now = today.hour + else: + now=0 + l = list() + for i in range(now, 24): + l.append(i) + splited=[l[i:i + 6] for i in range(0, len(l), 6)] + for i in splited: + k=[] + for d in i: + k.append(InlineKeyboardButton(text=f"{d}",callback_data=f"sch_day_{year}_{month}_{date}_{d}")) + button.append(k) + if month == today.month and date < today.day and year==today.year+1: + pyear=year-1 + else: + pyear=year + button.append([InlineKeyboardButton("Back", callback_data=f"sch_showdate_{pyear}_{month}"), InlineKeyboardButton("Close", callback_data="schclose")]) + await query.message.edit(f"Choose the hour of {date} {smonth} {year} to schedule a voicechat.", reply_markup=InlineKeyboardMarkup(button)) + + elif data.startswith("sch_day"): + none, none, year, month, day, hour = data.split("_") + year = int(year) + month = int(month) + day = int(day) + hour = int(hour) + datetime_object = datetime.datetime.strptime(str(month), "%m") + smonth = datetime_object.strftime("%B") + if year == today.year and month == today.month and day == today.day and hour == today.hour: + now=today.minute + else: + now=0 + button=[] + l = list() + for i in range(now, 60): + l.append(i) + for i in range(0, len(l), 6): + chunk = l[i:i + 6] + k=[] + for d in chunk: + k.append(InlineKeyboardButton(text=f"{d}",callback_data=f"sch_minute_{year}_{month}_{day}_{hour}_{d}")) + button.append(k) + button.append([InlineKeyboardButton("Back", callback_data=f"sch_month_{year}_{month}_{day}"), InlineKeyboardButton("Close", callback_data="schclose")]) + await query.message.edit(f"Choose minute of {hour}th hour on {day} {smonth} {year} to schedule Voicechat.", reply_markup=InlineKeyboardMarkup(button)) + + elif data.startswith("sch_minute"): + none, none, year, month, day, hour, minute = data.split("_") + year = int(year) + month = int(month) + day = int(day) + hour = int(hour) + minute = int(minute) + datetime_object = datetime.datetime.strptime(str(month), "%m") + smonth = datetime_object.strftime("%B") + if year == today.year and month == today.month and day == today.day and hour == today.hour and minute <= today.minute: + await query.answer("I dont have a timemachine to go to past!!!.") + return + final=f"{day}th {smonth} {year} at {hour}:{minute}" + button=[ + [ + InlineKeyboardButton("Confirm", callback_data=f"schconfirm_{year}-{month}-{day} {hour}:{minute}"), + InlineKeyboardButton("Back", callback_data=f"sch_day_{year}_{month}_{day}_{hour}") + ], + [ + InlineKeyboardButton("Close", callback_data="schclose") + ] + ] + data=Config.SCHEDULED_STREAM.get(f"{query.message.chat.id}_{query.message.message_id}") + if not data: + await query.answer("This schedule is expired", show_alert=True) + if data['3'] == "telegram": + title=data['1'] + else: + title=f"[{data['1']}]({data['2']})" + await query.message.edit(f"Your Stream {title} is now scheduled to start on {final}\n\nClick Confirm to confirm the time.", reply_markup=InlineKeyboardMarkup(button), disable_web_page_preview=True) + + elif data.startswith("sch_showdate"): + tyear=year + none, none, year, month = data.split("_") + datetime_object = datetime.datetime.strptime(month, "%m") + thissmonth = datetime_object.strftime("%B") + obj = calendar.Calendar() + thisday = today.day + year = int(year) + month = int(month) + m=obj.monthdayscalendar(year, month) + button=[] + button.append([InlineKeyboardButton(text=f"{str(thissmonth)} {str(year)}",callback_data=f"sch_month_choose_none_none")]) + days=["Mon", "Tues", "Wed", "Thu", "Fri", "Sat", "Sun"] + f=[] + for day in days: + f.append(InlineKeyboardButton(text=f"{day}",callback_data=f"day_info_none")) + button.append(f) + for one in m: + f=[] + for d in one: + year_=year + if year==today.year and month == today.month and d < int(today.day): + year_ += 1 + if d == 0: + k="\u2063" + d="none" + else: + k=d + f.append(InlineKeyboardButton(text=f"{k}",callback_data=f"sch_month_{year_}_{month}_{d}")) + button.append(f) + button.append([InlineKeyboardButton("Close", callback_data="schclose")]) + await query.message.edit(f"Choose the day of the month you want to schedule the voicechat.\nToday is {thisday} {smonth} {tyear}. Chooosing a date preceeding today will be considered as next year {year+1}", reply_markup=InlineKeyboardMarkup(button)) + + elif data.startswith("schconfirm"): + none, date = data.split("_") + date = datetime.datetime.strptime(date, '%Y-%m-%d %H:%M') + local_dt = IST.localize(date, is_dst=None) + utc_dt = local_dt.astimezone(pytz.utc).replace(tzinfo=None) + job_id=f"{query.message.chat.id}_{query.message.message_id}" + Config.SCHEDULE_LIST.append({"job_id":job_id, "date":utc_dt}) + Config.SCHEDULE_LIST = sorted(Config.SCHEDULE_LIST, key=lambda k: k['date']) + await schedule_a_play(job_id, utc_dt) + await query.message.edit(f"Succesfully scheduled to stream on {date.strftime('%b %d %Y, %I:%M %p')} ") + await delete_messages([query.message, query.message.reply_to_message]) + + elif query.data == 'schcancelall': + await cancel_all_schedules() + await query.message.edit("All Scheduled Streams are cancelled succesfully.") + + elif query.data == "schcancel": + buttons = [ + [ + InlineKeyboardButton('Yes, Iam Sure!!', callback_data='schcancelall'), + InlineKeyboardButton('No', callback_data='schclose'), + ] + ] + await query.message.edit("Are you sure that you want to cancel all the scheduled streams?", reply_markup=InlineKeyboardMarkup(buttons)) + elif data == "schclose": + await query.answer("Menu Closed") + await query.message.delete() + await query.message.reply_to_message.delete() + + if query.data == "shuffle": + if not Config.playlist: + await query.answer("Playlist is empty.") + return + await shuffle_playlist() + await query.answer("Playlist shuffled.") + await sleep(1) await query.message.edit_reply_markup(reply_markup=await get_buttons()) - except MessageNotModified: - pass - elif query.data.lower() == "resume": - if not Config.PAUSE: - await query.answer("Nothing Paused to resume", show_alert=True) - else: - await resume() - await sleep(1) - try: + + elif query.data.lower() == "pause": + if Config.PAUSE: + await query.answer("Already Paused") + else: + await pause() + await query.answer("Stream Paused") + await sleep(1) + await query.message.edit_reply_markup(reply_markup=await get_buttons()) - except MessageNotModified: - pass - - elif query.data=="skip": - if not Config.playlist: - await query.answer("No songs in playlist", show_alert=True) - else: - await skip() - await sleep(1) - if Config.playlist: - title=f"{Config.playlist[0][1]}" - elif Config.STREAM_LINK: - title=f"Stream Using [Url]({Config.DATA['FILE_DATA']['file']})" - else: - title=f"Streaming Startup [stream]({Config.STREAM_URL})" - - try: + + + elif query.data.lower() == "resume": + if not Config.PAUSE: + await query.answer("Nothing Paused to resume") + else: + await resume() + await query.answer("Redumed the stream") + await sleep(1) + await query.message.edit_reply_markup(reply_markup=await get_buttons()) + + elif query.data=="skip": + if not Config.playlist: + await query.answer("No songs in playlist") + else: + await query.answer("Trying to skip from playlist.") + await skip() + await sleep(1) + if Config.playlist: + title=f"{Config.playlist[0][1]}\nㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" + elif Config.STREAM_LINK: + title=f"Stream Using [Url]({Config.DATA['FILE_DATA']['file']})ㅤ ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" + else: + title=f"Streaming Startup [stream]({Config.STREAM_URL}) ㅤ ㅤ ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" await query.message.edit(f"{title}", disable_web_page_preview=True, reply_markup=await get_buttons() ) - except MessageNotModified: - pass - elif query.data=="replay": - if not Config.playlist: - await query.answer("No songs in playlist", show_alert=True) - else: - await restart_playout() - await sleep(1) - try: + + elif query.data=="replay": + if not Config.playlist: + await query.answer("No songs in playlist") + else: + await query.answer("trying to restart player") + await restart_playout() + await sleep(1) await query.message.edit_reply_markup(reply_markup=await get_buttons()) - except MessageNotModified: - pass - - elif query.data=="help": - buttons = [ - [ - InlineKeyboardButton('⚙️ Update Channel', url='https://t.me/subin_works'), - InlineKeyboardButton('🧩 Source', url='https://github.com/subinps/VCPlayerBot'), + + + elif query.data=="help": + buttons = [ + [ + InlineKeyboardButton('⚙️ Update Channel', url='https://t.me/subin_works'), + InlineKeyboardButton('🧩 Source', url='https://github.com/subinps/VCPlayerBot'), + ] ] - ] - reply_markup = InlineKeyboardMarkup(buttons) - try: + reply_markup = InlineKeyboardMarkup(buttons) await query.message.edit( Config.HELP, reply_markup=reply_markup ) - except MessageNotModified: - pass - elif query.data.lower() == "mute": - if Config.MUTED: - await unmute() - else: - await mute() - await sleep(1) - try: - await query.message.edit_reply_markup(reply_markup=await get_buttons()) - except MessageNotModified: - pass - elif query.data.lower() == 'seek': - if not Config.CALL_STATUS: - return await query.answer("Not Playing anything.", show_alert=True) - if not (Config.playlist or Config.STREAM_LINK): - return await query.answer("Startup stream cant be seeked.", show_alert=True) - data=Config.DATA.get('FILE_DATA') - if not data.get('dur', 0) or \ - data.get('dur') == 0: - return await query.answer("This stream cant be seeked..", show_alert=True) - k, reply = await seek_file(10) - if k == False: - return await query.answer(reply, show_alert=True) - try: + + elif query.data.lower() == "mute": + if Config.MUTED: + await unmute() + await query.answer("Unmuted stream") + else: + await mute() + await query.answer("Muted stream") + await sleep(1) + await query.message.edit_reply_markup(reply_markup=await volume_buttons()) + + elif query.data.lower() == 'seek': + if not Config.CALL_STATUS: + return await query.answer("Not Playing anything.") + #if not (Config.playlist or Config.STREAM_LINK): + #return await query.answer("Startup stream cant be seeked.", show_alert=True) + await query.answer("trying to seek.") + data=Config.DATA.get('FILE_DATA') + if not data.get('dur', 0) or \ + data.get('dur') == 0: + return await query.answer("This is a live stream and cannot be seeked.", show_alert=True) + k, reply = await seek_file(10) + if k == False: + return await query.answer(reply, show_alert=True) await query.message.edit_reply_markup(reply_markup=await get_buttons()) - except MessageNotModified: - pass - elif query.data.lower() == 'rewind': - if not Config.CALL_STATUS: - return await query.answer("Not Playing anything.", show_alert=True) - if not (Config.playlist or Config.STREAM_LINK): - return await query.answer("Startup stream cant be seeked.", show_alert=True) - data=Config.DATA.get('FILE_DATA') - if not data.get('dur', 0) or \ - data.get('dur') == 0: - return await query.answer("This stream cant be seeked..", show_alert=True) - k, reply = await seek_file(-10) - if k == False: - return await query.answer(reply, show_alert=True) - try: + + elif query.data.lower() == 'rewind': + if not Config.CALL_STATUS: + return await query.answer("Not Playing anything.") + #if not (Config.playlist or Config.STREAM_LINK): + #return await query.answer("Startup stream cant be seeked.", show_alert=True) + await query.answer("trying to rewind.") + data=Config.DATA.get('FILE_DATA') + if not data.get('dur', 0) or \ + data.get('dur') == 0: + return await query.answer("This is a live stream and cannot be seeked.", show_alert=True) + k, reply = await seek_file(-10) + if k == False: + return await query.answer(reply, show_alert=True) await query.message.edit_reply_markup(reply_markup=await get_buttons()) - except MessageNotModified: - pass - await query.answer() + + elif query.data == 'restart': + if not Config.CALL_STATUS: + if not Config.playlist: + await query.answer("Player is empty, starting STARTUP_STREAM.") + else: + await query.answer('Resuming the playlist') + await query.answer("Restrating the player") + await restart() + await query.message.edit(text=await get_playlist_str(), reply_markup=await get_buttons(), disable_web_page_preview=True) + + elif query.data.startswith("volume"): + me, you = query.data.split("_") + if you == "main": + await query.message.edit_reply_markup(reply_markup=await volume_buttons()) + if you == "add": + vol=Config.VOLUME+10 + if not (1 < vol < 200): + return await query.answer("Only 1-200 range accepted.") + await volume(vol) + Config.VOLUME=vol + await query.message.edit_reply_markup(reply_markup=await volume_buttons()) + elif you == "less": + vol=Config.VOLUME-10 + if not (1 < vol < 200): + return await query.answer("Only 1-200 range accepted.") + await volume(vol) + Config.VOLUME=vol + await query.message.edit_reply_markup(reply_markup=await volume_buttons()) + elif you == "back": + await query.message.edit_reply_markup(reply_markup=await get_buttons()) + + + elif query.data in ["is_loop", "is_video", "admin_only", "edit_title", "set_shuffle", "reply_msg", "set_new_chat", "record", "record_video", "record_dim"]: + if query.data == "is_loop": + Config.IS_LOOP = set_config(Config.IS_LOOP) + await query.message.edit_reply_markup(reply_markup=await settings_panel()) + + elif query.data == "is_video": + Config.IS_VIDEO = set_config(Config.IS_VIDEO) + await query.message.edit_reply_markup(reply_markup=await settings_panel()) + await restart_playout() + + elif query.data == "admin_only": + Config.ADMIN_ONLY = set_config(Config.ADMIN_ONLY) + await query.message.edit_reply_markup(reply_markup=await settings_panel()) + + elif query.data == "edit_title": + Config.EDIT_TITLE = set_config(Config.EDIT_TITLE) + await query.message.edit_reply_markup(reply_markup=await settings_panel()) + + elif query.data == "set_shuffle": + Config.SHUFFLE = set_config(Config.SHUFFLE) + await query.message.edit_reply_markup(reply_markup=await settings_panel()) + + elif query.data == "reply_msg": + Config.REPLY_PM = set_config(Config.REPLY_PM) + await query.message.edit_reply_markup(reply_markup=await settings_panel()) + + elif query.data == "record_dim": + if not Config.IS_VIDEO_RECORD: + return await query.answer("This cant be used for audio recordings") + Config.PORTRAIT=set_config(Config.PORTRAIT) + await query.message.edit_reply_markup(reply_markup=(await recorder_settings())) + elif query.data == 'record_video': + Config.IS_VIDEO_RECORD=set_config(Config.IS_VIDEO_RECORD) + await query.message.edit_reply_markup(reply_markup=(await recorder_settings())) + + elif query.data == 'record': + if Config.IS_RECORDING: + k, msg = await stop_recording() + if k == False: + await query.answer(msg, show_alert=True) + else: + await query.answer("Recording Stopped") + else: + k, msg = await start_record_stream() + if k == False: + await query.answer(msg, show_alert=True) + else: + await query.answer("Recording started") + await query.message.edit_reply_markup(reply_markup=(await recorder_settings())) + + elif query.data == "set_new_chat": + if query.from_user is None: + return await query.answer("You cant do scheduling here, since you are an anonymous admin. Schedule from private chat.", show_alert=True) + if query.from_user.id in Config.SUDO: + await query.answer("Setting up new CHAT") + chat=query.message.chat.id + if Config.IS_RECORDING: + await stop_recording() + await cancel_all_schedules() + await leave_call() + Config.CHAT=chat + Config.ADMIN_CACHE=False + await restart() + await query.message.edit("Succesfully Changed Chat") + await sync_to_db() + else: + await query.answer("This can only be used by SUDO users", show_alert=True) + if not Config.DATABASE_URI: + await query.answer("No DATABASE found, this changes are saved temporarly and will be reverted on restart. Add MongoDb to make this permanant.") + elif query.data.startswith("close"): + if "sudo" in query.data: + if query.from_user.id in Config.SUDO: + await query.message.delete() + else: + await query.answer("This can only be used by SUDO users") + else: + if query.message.chat.type != "private" and query.message.reply_to_message: + if query.message.reply_to_message.from_user is None: + pass + elif query.from_user.id != query.message.reply_to_message.from_user.id: + return await query.answer("Okda") + elif query.from_user.id in Config.ADMINS: + pass + else: + return await query.answer("Okda") + await query.answer("Menu Closed") + await query.message.delete() + await query.answer() diff --git a/plugins/commands.py b/plugins/commands.py index b9b2e3fa..b5394944 100644 --- a/plugins/commands.py +++ b/plugins/commands.py @@ -12,116 +12,328 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - -import asyncio -from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, InputMediaDocument -from utils import is_admin -from pyrogram import Client, filters -from utils import update, is_admin -from config import Config from logger import LOGGER +from contextlib import suppress +from config import Config +import calendar +import pytz +from datetime import datetime +import asyncio import os +from pyrogram.errors.exceptions.bad_request_400 import ( + MessageIdInvalid, + MessageNotModified +) +from pyrogram.types import ( + InlineKeyboardMarkup, + InlineKeyboardButton +) +from utils import ( + cancel_all_schedules, + edit_config, + is_admin, + leave_call, + restart, + restart_playout, + stop_recording, + sync_to_db, + update, + is_admin, + chat_filter, + sudo_filter, + delete_messages +) +from pyrogram import ( + Client, + filters +) +IST = pytz.timezone(Config.TIME_ZONE) +if Config.DATABASE_URI: + from database import db HOME_TEXT = "Hey [{}](tg://user?id={}) 🙋‍♂️\n\nIam A Bot Built To Play or Stream Videos In Telegram VoiceChats.\nI Can Stream Any YouTube Video Or A Telegram File Or Even A YouTube Live." admin_filter=filters.create(is_admin) @Client.on_message(filters.command(['start', f"start@{Config.BOT_USERNAME}"])) async def start(client, message): + if len(message.command) > 1: + if message.command[1] == 'help': + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(f"Play", callback_data='help_play'), + InlineKeyboardButton(f"Settings", callback_data=f"help_settings"), + InlineKeyboardButton(f"Recording", callback_data='help_record'), + ], + [ + InlineKeyboardButton("Scheduling", callback_data="help_schedule"), + InlineKeyboardButton("Controling", callback_data='help_control'), + InlineKeyboardButton("Admins", callback_data="help_admin"), + ], + [ + InlineKeyboardButton(f"Misc", callback_data='help_misc'), + InlineKeyboardButton("Close", callback_data="close"), + ], + ] + ) + await message.reply("Learn to use the VCPlayer, Showing help menu, Choose from the below options.", + reply_markup=reply_markup, + disable_web_page_preview=True + ) + elif 'sch' in message.command[1]: + msg=await message.reply("Checking schedules..") + you, me = message.command[1].split("_", 1) + who=Config.SCHEDULED_STREAM.get(me) + if not who: + return await msg.edit("Something gone somewhere.") + del Config.SCHEDULED_STREAM[me] + whom=f"{message.chat.id}_{msg.message_id}" + Config.SCHEDULED_STREAM[whom] = who + await sync_to_db() + if message.from_user.id not in Config.ADMINS: + return await msg.edit("OK da") + today = datetime.now(IST) + smonth=today.strftime("%B") + obj = calendar.Calendar() + thisday = today.day + year = today.year + month = today.month + m=obj.monthdayscalendar(year, month) + button=[] + button.append([InlineKeyboardButton(text=f"{str(smonth)} {str(year)}",callback_data=f"sch_month_choose_none_none")]) + days=["Mon", "Tues", "Wed", "Thu", "Fri", "Sat", "Sun"] + f=[] + for day in days: + f.append(InlineKeyboardButton(text=f"{day}",callback_data=f"day_info_none")) + button.append(f) + for one in m: + f=[] + for d in one: + year_=year + if d < int(today.day): + year_ += 1 + if d == 0: + k="\u2063" + d="none" + else: + k=d + f.append(InlineKeyboardButton(text=f"{k}",callback_data=f"sch_month_{year_}_{month}_{d}")) + button.append(f) + button.append([InlineKeyboardButton("Close", callback_data="schclose")]) + await msg.edit(f"Choose the day of the month you want to schedule the voicechat.\nToday is {thisday} {smonth} {year}. Chooosing a date preceeding today will be considered as next year {year+1}", reply_markup=InlineKeyboardMarkup(button)) + + + + return buttons = [ [ InlineKeyboardButton('⚙️ Update Channel', url='https://t.me/subin_works'), InlineKeyboardButton('🧩 Source', url='https://github.com/subinps/VCPlayerBot') ], [ - InlineKeyboardButton('👨🏼‍🦯 Help', callback_data='help'), + InlineKeyboardButton('👨🏼‍🦯 Help', callback_data='help_main'), + InlineKeyboardButton('🗑 Close', callback_data='close'), ] ] reply_markup = InlineKeyboardMarkup(buttons) - await message.reply(HOME_TEXT.format(message.from_user.first_name, message.from_user.id), reply_markup=reply_markup) + k = await message.reply(HOME_TEXT.format(message.from_user.first_name, message.from_user.id), reply_markup=reply_markup) + await delete_messages([message, k]) @Client.on_message(filters.command(["help", f"help@{Config.BOT_USERNAME}"])) async def show_help(client, message): - buttons = [ + reply_markup=InlineKeyboardMarkup( [ - InlineKeyboardButton('⚙️ Update Channel', url='https://t.me/subin_works'), - InlineKeyboardButton('🧩 Source', url='https://github.com/subinps/VCPlayerBot'), + [ + InlineKeyboardButton("Play", callback_data='help_play'), + InlineKeyboardButton("Settings", callback_data=f"help_settings"), + InlineKeyboardButton("Recording", callback_data='help_record'), + ], + [ + InlineKeyboardButton("Scheduling", callback_data="help_schedule"), + InlineKeyboardButton("Controling", callback_data='help_control'), + InlineKeyboardButton("Admins", callback_data="help_admin"), + ], + [ + InlineKeyboardButton("Misc", callback_data='help_misc'), + InlineKeyboardButton("Config Vars", callback_data='help_env'), + InlineKeyboardButton("Close", callback_data="close"), + ], ] - ] - reply_markup = InlineKeyboardMarkup(buttons) + ) + if message.chat.type != "private" and message.from_user is None: + k=await message.reply( + text="I cant help you here, since you are an anonymous admin. Get help in PM", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(f"Help", url=f"https://telegram.dog/{Config.BOT_USERNAME}?start=help"), + ] + ] + ),) + await delete_messages([message, k]) + return if Config.msg.get('help') is not None: await Config.msg['help'].delete() Config.msg['help'] = await message.reply_text( - Config.HELP, - reply_markup=reply_markup + "Learn to use the VCPlayer, Showing help menu, Choose from the below options.", + reply_markup=reply_markup, + disable_web_page_preview=True ) + #await delete_messages([message]) @Client.on_message(filters.command(['repo', f"repo@{Config.BOT_USERNAME}"])) async def repo_(client, message): buttons = [ [ InlineKeyboardButton('🧩 Repository', url='https://github.com/subinps/VCPlayerBot'), - InlineKeyboardButton('⚙️ Update Channel', url='https://t.me/subin_works'), - + InlineKeyboardButton('⚙️ Update Channel', url='https://t.me/subin_works'), ], + [ + InlineKeyboardButton("🎞 How to Deploy", url='https://youtu.be/mnWgZMrNe_0'), + InlineKeyboardButton('🗑 Close', callback_data='close'), + ] ] - await message.reply("The source code of this bot is public and can be found at VCPlayerBot.\nYou can deploy your own bot and use in your group.\n\nFeel free to star☀️ the repo if you liked it 🙃.", reply_markup=InlineKeyboardMarkup(buttons)) + await message.reply("The source code of this bot is public and can be found at VCPlayerBot.\nYou can deploy your own bot and use in your group.\n\nFeel free to star☀️ the repo if you liked it 🙃.", reply_markup=InlineKeyboardMarkup(buttons), disable_web_page_preview=True) + await delete_messages([message]) -@Client.on_message(filters.command(['restart', 'update', f"restart@{Config.BOT_USERNAME}", f"update@{Config.BOT_USERNAME}"]) & admin_filter) +@Client.on_message(filters.command(['restart', 'update', f"restart@{Config.BOT_USERNAME}", f"update@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def update_handler(client, message): if Config.HEROKU_APP: - await message.reply("Heroku APP found, Restarting app to update.") + k = await message.reply("Heroku APP found, Restarting app to update.") + if Config.DATABASE_URI: + msg = {"msg_id":k.message_id, "chat_id":k.chat.id} + if not await db.is_saved("RESTART"): + db.add_config("RESTART", msg) + else: + await db.edit_config("RESTART", msg) + await sync_to_db() else: - await message.reply("No Heroku APP found, Trying to restart.") + k = await message.reply("No Heroku APP found, Trying to restart.") + if Config.DATABASE_URI: + msg = {"msg_id":k.message_id, "chat_id":k.chat.id} + if not await db.is_saved("RESTART"): + db.add_config("RESTART", msg) + else: + await db.edit_config("RESTART", msg) + await message.delete() await update() -@Client.on_message(filters.command(['logs', f"logs@{Config.BOT_USERNAME}"]) & admin_filter) +@Client.on_message(filters.command(['logs', f"logs@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def get_logs(client, message): - logs=[] - if os.path.exists("ffmpeg.txt"): - logs.append(InputMediaDocument("ffmpeg.txt", caption="FFMPEG Logs")) - if os.path.exists("ffmpeg.txt"): - logs.append(InputMediaDocument("botlog.txt", caption="Bot Logs")) - if logs: - try: - await message.reply_media_group(logs) - except: - await message.reply("Errors occured while uploading log file.") - pass - logs.clear() + m=await message.reply("Checking logs..") + if os.path.exists("botlog.txt"): + await message.reply_document('botlog.txt', caption="Bot Logs") + await m.delete() + await delete_messages([message]) else: - await message.reply("No log files found.") + k = await m.edit("No log files found.") + await delete_messages([message, k]) -@Client.on_message(filters.command(['env', f"env@{Config.BOT_USERNAME}"]) & filters.user(Config.SUDO)) +@Client.on_message(filters.command(['env', f"env@{Config.BOT_USERNAME}", "config", f"config@{Config.BOT_USERNAME}"]) & sudo_filter & chat_filter) async def set_heroku_var(client, message): - if not Config.HEROKU_APP: - buttons = [[InlineKeyboardButton('Heroku API_KEY', url='https://dashboard.heroku.com/account/applications/authorizations/new')]] - await message.reply( - text="No heroku app found, this command needs the following heroku vars to be set.\n\n1. HEROKU_API_KEY: Your heroku account api key.\n2. HEROKU_APP_NAME: Your heroku app name.", - reply_markup=InlineKeyboardMarkup(buttons)) - return - if " " in message.text: - cmd, env = message.text.split(" ", 1) - if not "=" in env: - return await message.reply("You should specify the value for env.\nExample: /env CHAT=-100213658211") - var, value = env.split("=", 2) - config = Config.HEROKU_APP.config() - if not value: - m=await message.reply(f"No value for env specified. Trying to delete env {var}.") - await asyncio.sleep(2) - if var in config: - del config[var] + with suppress(MessageIdInvalid, MessageNotModified): + m = await message.reply("Checking config vars..") + if " " in message.text: + cmd, env = message.text.split(" ", 1) + if not "=" in env: + await m.edit("You should specify the value for env.\nExample: /env CHAT=-100213658211") + await delete_messages([message, m]) + return + var, value = env.split("=", 2) + else: + await m.edit("You haven't provided any value for env, you should follow the correct format.\nExample: /env CHAT=-1020202020202 to change or set CHAT var.\n/env REPLY_MESSAGE= To delete REPLY_MESSAGE.") + await delete_messages([message, m]) + return + + if Config.DATABASE_URI and var in ["STARTUP_STREAM", "CHAT", "LOG_GROUP", "REPLY_MESSAGE", "DELAY", "RECORDING_DUMP"]: + await m.edit("Mongo DB Found, Setting up config vars...") + await asyncio.sleep(2) + if not value: + await m.edit(f"No value for env specified. Trying to delete env {var}.") + await asyncio.sleep(2) + if var in ["STARTUP_STREAM", "CHAT", "DELAY"]: + await m.edit("This is a mandatory var and cannot be deleted.") + await delete_messages([message, m]) + return + await edit_config(var, False) await m.edit(f"Sucessfully deleted {var}") - config[var] = None + await delete_messages([message, m]) + return else: - await m.edit(f"No env named {var} found. Nothing was changed.") - return - if var in config: - m=await message.reply(f"Variable already found. Now edited to {value}") + if var in ["CHAT", "LOG_GROUP", "RECORDING_DUMP"]: + try: + value=int(value) + except: + await m.edit("You should give me a chat id . It should be an interger.") + await delete_messages([message, m]) + return + if var == "CHAT": + await leave_call() + Config.ADMIN_CACHE=False + if Config.IS_RECORDING: + await stop_recording() + await cancel_all_schedules() + Config.CHAT=int(value) + await restart() + await edit_config(var, int(value)) + await m.edit(f"Succesfully changed {var} to {value}") + await delete_messages([message, m]) + return + else: + if var == "STARTUP_STREAM": + Config.STREAM_SETUP=False + await edit_config(var, value) + await m.edit(f"Succesfully changed {var} to {value}") + await delete_messages([message, m]) + await restart_playout() + return else: - m=await message.reply(f"Variable not found, Now setting as new var.") - await asyncio.sleep(2) - await m.edit(f"Succesfully set {var} with value {value}, Now Restarting to take effect of changes...") - config[var] = str(value) - else: - await message.reply("You haven't provided any value for env, you should follow the correct format.\nExample: /env CHAT=-1020202020202 to change or set CHAT var.\n/env REPLY_MESSAGE= To delete REPLY_MESSAGE.") \ No newline at end of file + if not Config.HEROKU_APP: + buttons = [[InlineKeyboardButton('Heroku API_KEY', url='https://dashboard.heroku.com/account/applications/authorizations/new'), InlineKeyboardButton('🗑 Close', callback_data='close'),]] + await m.edit( + text="No heroku app found, this command needs the following heroku vars to be set.\n\n1. HEROKU_API_KEY: Your heroku account api key.\n2. HEROKU_APP_NAME: Your heroku app name.", + reply_markup=InlineKeyboardMarkup(buttons)) + await delete_messages([message]) + return + config = Config.HEROKU_APP.config() + if not value: + await m.edit(f"No value for env specified. Trying to delete env {var}.") + await asyncio.sleep(2) + if var in ["STARTUP_STREAM", "CHAT", "DELAY", "API_ID", "API_HASH", "BOT_TOKEN", "SESSION_STRING", "ADMINS"]: + await m.edit("These are mandatory vars and cannot be deleted.") + await delete_messages([message, m]) + return + if var in config: + await m.edit(f"Sucessfully deleted {var}") + await asyncio.sleep(2) + await m.edit("Now restarting the app to make changes.") + if Config.DATABASE_URI: + msg = {"msg_id":m.message_id, "chat_id":m.chat.id} + if not await db.is_saved("RESTART"): + db.add_config("RESTART", msg) + else: + await db.edit_config("RESTART", msg) + del config[var] + config[var] = None + else: + k = await m.edit(f"No env named {var} found. Nothing was changed.") + await delete_messages([message, k]) + return + if var in config: + await m.edit(f"Variable already found. Now edited to {value}") + else: + await m.edit(f"Variable not found, Now setting as new var.") + await asyncio.sleep(2) + await m.edit(f"Succesfully set {var} with value {value}, Now Restarting to take effect of changes...") + if Config.DATABASE_URI: + msg = {"msg_id":m.message_id, "chat_id":m.chat.id} + if not await db.is_saved("RESTART"): + db.add_config("RESTART", msg) + else: + await db.edit_config("RESTART", msg) + config[var] = str(value) + + + + diff --git a/plugins/controls.py b/plugins/controls.py index 5457e4b6..f5b377c1 100644 --- a/plugins/controls.py +++ b/plugins/controls.py @@ -12,135 +12,252 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - -from utils import get_playlist_str, is_admin, mute, restart_playout, skip, pause, resume, unmute, volume, get_buttons, is_admin, seek_file, get_player_string -from pyrogram import Client, filters +from logger import LOGGER from pyrogram.types import Message from config import Config -from logger import LOGGER +from pyrogram import ( + Client, + filters +) +from utils import ( + clear_db_playlist, + get_playlist_str, + is_admin, + mute, + restart_playout, + settings_panel, + skip, + pause, + resume, + unmute, + volume, + get_buttons, + is_admin, + seek_file, + delete_messages, + chat_filter, + volume_buttons +) admin_filter=filters.create(is_admin) -@Client.on_message(filters.command(["playlist", f"playlist@{Config.BOT_USERNAME}"]) & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["playlist", f"playlist@{Config.BOT_USERNAME}"]) & chat_filter) async def player(client, message): + if not Config.CALL_STATUS: + await message.reply_text( + "Player is idle, start the player using below button. ㅤㅤㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([message]) + return pl = await get_playlist_str() if message.chat.type == "private": await message.reply_text( pl, disable_web_page_preview=True, + reply_markup=await get_buttons(), ) else: - if Config.msg.get('playlist') is not None: - await Config.msg['playlist'].delete() - Config.msg['playlist'] = await message.reply_text( + if Config.msg.get('player') is not None: + await Config.msg['player'].delete() + Config.msg['player'] = await message.reply_text( pl, disable_web_page_preview=True, + reply_markup=await get_buttons(), ) + await delete_messages([message]) -@Client.on_message(filters.command(["skip", f"skip@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["skip", f"skip@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def skip_track(_, m: Message): + msg=await m.reply('trying to skip from queue..') + if not Config.CALL_STATUS: + await msg.edit( + "Player is idle, start the player using below button. ㅤㅤㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([m]) + return if not Config.playlist: - await m.reply("Playlist is Empty.\nLive Streaming.") + await msg.edit("Playlist is Empty.") + await delete_messages([m, msg]) return if len(m.command) == 1: await skip() else: + #https://github.com/callsmusic/tgvc-userbot/blob/dev/plugins/vc/player.py#L268-L288 try: items = list(dict.fromkeys(m.command[1:])) items = [int(x) for x in items if x.isdigit()] items.sort(reverse=True) for i in items: if 2 <= i <= (len(Config.playlist) - 1): + await msg.edit(f"Succesfully Removed from Playlist- {i}. **{Config.playlist[i][1]}**") + await clear_db_playlist(song=Config.playlist[i]) Config.playlist.pop(i) - await m.reply(f"Succesfully Removed from Playlist- {i}. **{Config.playlist[i][1]}**") + await delete_messages([m, msg]) else: - await m.reply(f"You Cant Skip First Two Songs- {i}") + await msg.edit(f"You cant skip first two songs- {i}") + await delete_messages([m, msg]) except (ValueError, TypeError): - await m.reply_text("Invalid input") + await msg.edit("Invalid input") + await delete_messages([m, msg]) pl=await get_playlist_str() if m.chat.type == "private": - await m.reply_text(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + await msg.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) elif not Config.LOG_GROUP and m.chat.type == "supergroup": - await m.reply_text(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + if Config.msg.get('player'): + await Config.msg['player'].delete() + Config.msg['player'] = await msg.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + await delete_messages([m]) -@Client.on_message(filters.command(["pause", f"pause@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["pause", f"pause@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def pause_playing(_, m: Message): - if Config.PAUSE: - return await m.reply("Already Paused") if not Config.CALL_STATUS: - return await m.reply("Not Playing anything.") - await m.reply("Paused Video Call") + await m.reply_text( + "Player is idle, start the player using below button. ㅤㅤㅤㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([m]) + return + if Config.PAUSE: + k = await m.reply("Already Paused") + await delete_messages([m, k]) + return + k = await m.reply("Paused Video Call") await pause() + await delete_messages([m, k]) -@Client.on_message(filters.command(["resume", f"resume@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["resume", f"resume@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def resume_playing(_, m: Message): - if not Config.PAUSE: - return await m.reply("Nothing paused to resume") if not Config.CALL_STATUS: - return await m.reply("Not Playing anything.") - await m.reply("Resumed Video Call") + await m.reply_text( + "Player is idle, start the player using below button. ㅤㅤㅤㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([m]) + return + if not Config.PAUSE: + k = await m.reply("Nothing paused to resume") + await delete_messages([m, k]) + return + k = await m.reply("Resumed Video Call") await resume() + await delete_messages([m, k]) -@Client.on_message(filters.command(['volume', f"volume@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(['volume', f"volume@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def set_vol(_, m: Message): if not Config.CALL_STATUS: - return await m.reply("Not Playing anything.") + await m.reply_text( + "Player is idle, start the player using below button. ㅤㅤㅤㅤㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([m]) + return if len(m.command) < 2: - await m.reply_text('You forgot to pass volume (1-200).') + await m.reply_text('Change Volume of Your VCPlayer. ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ', reply_markup=await volume_buttons()) + await delete_messages([m]) return - await m.reply_text(f"Volume set to {m.command[1]}") - await volume(int(m.command[1])) + if not 1 < int(m.command[1]) < 200: + await m.reply_text(f"Only 1-200 range is accepeted. ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ", reply_markup=await volume_buttons()) + else: + await volume(int(m.command[1])) + await m.reply_text(f"Succesfully set volume to {m.command[1]} ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ", reply_markup=await volume_buttons()) + await delete_messages([m]) + + -@Client.on_message(filters.command(['mute', f"mute@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(['mute', f"mute@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def set_mute(_, m: Message): if not Config.CALL_STATUS: - return await m.reply("Not Playing anything.") + await m.reply_text( + "Player is idle, start the player using below button. ㅤㅤㅤㅤㅤㅤㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([m]) + return if Config.MUTED: - return await m.reply_text("Already muted.") + k = await m.reply_text("Already muted.") + await delete_messages([m, k]) + return k=await mute() if k: - await m.reply_text(f" 🔇 Succesfully Muted ") + k = await m.reply_text(f" 🔇 Succesfully Muted ") + await delete_messages([m, k]) else: - await m.reply_text("Already muted.") + k = await m.reply_text("Already muted.") + await delete_messages([m, k]) -@Client.on_message(filters.command(['unmute', f"unmute@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(['unmute', f"unmute@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def set_unmute(_, m: Message): if not Config.CALL_STATUS: - return await m.reply("Not Playing anything.") + await m.reply_text( + "Player is idle, start the player using below button. ㅤㅤㅤㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([m]) + return if not Config.MUTED: - return await m.reply("Stream already unmuted.") + k = await m.reply("Stream already unmuted.") + await delete_messages([m, k]) + return k=await unmute() if k: - await m.reply_text(f"🔊 Succesfully Unmuted ") + k = await m.reply_text(f"🔊 Succesfully Unmuted ") + await delete_messages([m, k]) + return else: - await m.reply_text("Not muted, already unmuted.") + k=await m.reply_text("Not muted, already unmuted.") + await delete_messages([m, k]) -@Client.on_message(filters.command(["replay", f"replay@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["replay", f"replay@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def replay_playout(client, m: Message): + msg = await m.reply('Checking player') if not Config.CALL_STATUS: - return await m.reply("Not Playing anything.") - await m.reply_text(f"Replaying from begining") + await msg.edit( + "Player is idle, start the player using below button. ㅤㅤㅤㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([m]) + return + await msg.edit(f"Replaying from begining") await restart_playout() + await delete_messages([m, msg]) -@Client.on_message(filters.command(["player", f"player@{Config.BOT_USERNAME}"]) & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["player", f"player@{Config.BOT_USERNAME}"]) & chat_filter) async def show_player(client, m: Message): + if not Config.CALL_STATUS: + await m.reply_text( + "Player is idle, start the player using below button. ㅤㅤㅤㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([m]) + return data=Config.DATA.get('FILE_DATA') if not data.get('dur', 0) or \ data.get('dur') == 0: - title="Playing Live Stream" + title="Playing Live Stream ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" else: if Config.playlist: - title=f"{Config.playlist[0][1]}" + title=f"{Config.playlist[0][1]} ㅤㅤㅤㅤ\n ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" elif Config.STREAM_LINK: - title=f"Stream Using [Url]({data['file']}) " + title=f"Stream Using [Url]({data['file']}) ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" else: - title=f"Streaming Startup [stream]({Config.STREAM_URL})" + title=f"Streaming Startup [stream]({Config.STREAM_URL}) ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" if m.chat.type == "private": await m.reply_text( title, @@ -155,37 +272,59 @@ async def show_player(client, m: Message): disable_web_page_preview=True, reply_markup=await get_buttons() ) + await delete_messages([m]) -@Client.on_message(filters.command(["seek", f"seek@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["seek", f"seek@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def seek_playout(client, m: Message): if not Config.CALL_STATUS: - return await m.reply("Not Playing anything.") - if not (Config.playlist or Config.STREAM_LINK): - return await m.reply("Startup stream cant be seeked.") + await m.reply_text( + "Player is idle, start the player using below button. ㅤㅤㅤ ㅤㅤ", + disable_web_page_preview=True, + reply_markup=await get_buttons() + ) + await delete_messages([m]) + return data=Config.DATA.get('FILE_DATA') + k=await m.reply("Trying to seek..") if not data.get('dur', 0) or \ data.get('dur') == 0: - return await m.reply("This stream cant be seeked..") + await k.edit("This stream cant be seeked.") + await delete_messages([m, k]) + return if ' ' in m.text: i, time = m.text.split(" ") try: time=int(time) except: - return await m.reply('Invalid time specified') - k, string=await seek_file(time) - if k == False: - return await m.reply(string) - if not data.get('dur', 0) or \ - data.get('dur') == 0: - title="Playing Live Stream" + await k.edit('Invalid time specified') + await delete_messages([m, k]) + return + nyav, string=await seek_file(time) + if nyav == False: + await k.edit(string) + await delete_messages([m, k]) + return + if not data.get('dur', 0)\ + or data.get('dur') == 0: + title="Playing Live Stream ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" else: if Config.playlist: - title=f"{Config.playlist[0][1]}" + title=f"{Config.playlist[0][1]}\nㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" elif Config.STREAM_LINK: - title=f"Stream Using [Url]({data['file']})" + title=f"Stream Using [Url]({data['file']}) ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" else: - title=f"Streaming Startup [stream]({Config.STREAM_URL})" - await m.reply(f"🎸{title}", reply_markup=await get_buttons(), disable_web_page_preview=True) + title=f"Streaming Startup [stream]({Config.STREAM_URL}) ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" + if Config.msg.get('player'): + await Config.msg['player'].delete() + Config.msg['player'] = await k.edit(f"🎸{title}", reply_markup=await get_buttons(), disable_web_page_preview=True) + await delete_messages([m]) else: - await m.reply('No time specified') + await k.edit('No time specified') + await delete_messages([m, k]) + + +@Client.on_message(filters.command(["settings", f"settings@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) +async def settings(client, m: Message): + await m.reply(f"Configure Your VCPlayer Settings Here. ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ", reply_markup=await settings_panel(), disable_web_page_preview=True) + await delete_messages([m]) diff --git a/plugins/export_import.py b/plugins/export_import.py index 959cfb49..3c8cdbc0 100644 --- a/plugins/export_import.py +++ b/plugins/export_import.py @@ -13,21 +13,39 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from utils import get_buttons, is_admin, get_playlist_str, shuffle_playlist, import_play_list -from pyrogram import Client, filters -from pyrogram.types import Message -from config import Config from logger import LOGGER import json import os +from pyrogram.types import Message +from contextlib import suppress +from config import Config +from utils import ( + get_buttons, + is_admin, + get_playlist_str, + shuffle_playlist, + import_play_list, + delete_messages, + chat_filter +) +from pyrogram import ( + Client, + filters +) +from pyrogram.errors import ( + MessageNotModified, + MessageIdInvalid +) + admin_filter=filters.create(is_admin) -@Client.on_message(filters.command(["export", f"export@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["export", f"export@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def export_play_list(client, message: Message): if not Config.playlist: - await message.reply_text("Playlist is Empty") + k=await message.reply_text("Playlist is Empty") + await delete_messages([message, k]) return file=f"{message.chat.id}_{message.message_id}.json" with open(file, 'w+') as outfile: @@ -37,27 +55,35 @@ async def export_play_list(client, message: Message): os.remove(file) except: pass + await delete_messages([message]) -@Client.on_message(filters.command(["import", f"import@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["import", f"import@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def import_playlist(client, m: Message): - if m.reply_to_message is not None and m.reply_to_message.document: - if m.reply_to_message.document.file_name != "PlayList.json": - k=await m.reply("Invalid PlayList file given. Use @GetPlayListBot to get a playlist file. Or Export your current Playlist using /export.") - return - myplaylist=await m.reply_to_message.download() - status=await m.reply("Trying to get details from playlist.") - n=await import_play_list(myplaylist) - if not n: - await status.edit("Errors Occured while importing playlist.") - return - if Config.SHUFFLE: - await shuffle_playlist() - pl=await get_playlist_str() - if m.chat.type == "private": - await status.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) - elif not Config.LOG_GROUP and m.chat.type == "supergroup": - await status.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + with suppress(MessageIdInvalid, MessageNotModified): + if m.reply_to_message is not None and m.reply_to_message.document: + if m.reply_to_message.document.file_name != "PlayList.json": + k=await m.reply("Invalid PlayList file given. Export your current Playlist using /export.") + await delete_messages([m, k]) + return + myplaylist=await m.reply_to_message.download() + status=await m.reply("Trying to get details from playlist.") + n=await import_play_list(myplaylist) + if not n: + await status.edit("Errors Occured while importing playlist.") + await delete_messages([m, status]) + return + if Config.SHUFFLE: + await shuffle_playlist() + pl=await get_playlist_str() + if m.chat.type == "private": + await status.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + elif not Config.LOG_GROUP and m.chat.type == "supergroup": + if Config.msg.get('playlist'): + await Config.msg['playlist'].delete() + Config.msg['playlist'] = await status.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + await delete_messages([m]) + else: + await delete_messages([m, status]) else: - await status.delete() - else: - await m.reply("No playList file given. Use @GetPlayListBot or search for a playlist in @DumpPlaylist to get a playlist file.") + k = await m.reply("No playList file given.") + await delete_messages([m, k]) diff --git a/plugins/inline.py b/plugins/inline.py index 79806657..1e0a6a7f 100644 --- a/plugins/inline.py +++ b/plugins/inline.py @@ -15,10 +15,19 @@ from pyrogram.handlers import InlineQueryHandler from youtubesearchpython import VideosSearch -from pyrogram.types import InlineQueryResultArticle, InputTextMessageContent, InlineKeyboardButton, InlineKeyboardMarkup -from pyrogram import Client, errors from config import Config from logger import LOGGER +from pyrogram.types import ( + InlineQueryResultArticle, + InputTextMessageContent, + InlineKeyboardButton, + InlineKeyboardMarkup +) +from pyrogram import ( + Client, + errors +) + buttons = [ [ diff --git a/plugins/manage_admins.py b/plugins/manage_admins.py new file mode 100644 index 00000000..faf0b0f1 --- /dev/null +++ b/plugins/manage_admins.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# Copyright (C) @subinps +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from logger import LOGGER +from config import Config +from pyrogram import ( + Client, + filters +) +from utils import ( + get_admins, + sync_to_db, + delete_messages, + sudo_filter +) + + +@Client.on_message(filters.command(['vcpromote', f"vcpromote@{Config.BOT_USERNAME}"]) & sudo_filter) +async def add_admin(client, message): + if message.reply_to_message: + if message.reply_to_message.from_user.id is None: + k = await message.reply("You are an anonymous admin, you can't do this.") + await delete_messages([message, k]) + return + user_id=message.reply_to_message.from_user.id + user=message.reply_to_message.from_user + + elif ' ' in message.text: + c, user = message.text.split(" ", 1) + if user.startswith("@"): + user=user.replace("@", "") + try: + user=await client.get_users(user) + except Exception as e: + k=await message.reply(f"I was unable to locate that user.\nError: {e}") + await delete_messages([message, k]) + return + user_id=user.id + else: + try: + user_id=int(user) + user=await client.get_users(user_id) + except: + k=await message.reply(f"You should give a user id or his username with @.") + await delete_messages([message, k]) + return + else: + k=await message.reply("No user specified, reply to a user with /vcpromote or pass a users user id or username.") + await delete_messages([message, k]) + return + if user_id in Config.ADMINS: + k = await message.reply("This user is already an admin.") + await delete_messages([message, k]) + return + Config.ADMINS.append(user_id) + k=await message.reply(f"Succesfully promoted {user.mention} as VC admin") + await sync_to_db() + await delete_messages([message, k]) + + +@Client.on_message(filters.command(['vcdemote', f"vcdemote@{Config.BOT_USERNAME}"]) & sudo_filter) +async def remove_admin(client, message): + if message.reply_to_message: + if message.reply_to_message.from_user.id is None: + k = await message.reply("You are an anonymous admin, you can't do this.") + await delete_messages([message, k]) + return + user_id=message.reply_to_message.from_user.id + user=message.reply_to_message.from_user + elif ' ' in message.text: + c, user = message.text.split(" ", 1) + if user.startswith("@"): + user=user.replace("@", "") + try: + user=await client.get_users(user) + except Exception as e: + k = await message.reply(f"I was unable to locate that user.\nError: {e}") + await delete_messages([message, k]) + return + user_id=user.id + else: + try: + user_id=int(user) + user=await client.get_users(user_id) + except: + k = await message.reply(f"You should give a user id or his username with @.") + await delete_messages([message, k]) + return + else: + k = await message.reply("No user specified, reply to a user with /vcdemote or pass a users user id or username.") + await delete_messages([message, k]) + return + if not user_id in Config.ADMINS: + k = await message.reply("This user is not an admin yet.") + await delete_messages([message, k]) + return + Config.ADMINS.remove(user_id) + k = await message.reply(f"Succesfully Demoted {user.mention}") + await sync_to_db() + await delete_messages([message, k]) + + +@Client.on_message(filters.command(['refresh', f"refresh@{Config.BOT_USERNAME}"]) & filters.user(Config.SUDO)) +async def refresh_admins(client, message): + Config.ADMIN_CACHE=False + await get_admins(Config.CHAT) + k = await message.reply("Admin list has been refreshed") + await sync_to_db() + await delete_messages([message, k]) \ No newline at end of file diff --git a/plugins/player.py b/plugins/player.py index 607ece6c..47317134 100644 --- a/plugins/player.py +++ b/plugins/player.py @@ -12,222 +12,411 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - -from utils import download, get_admins, is_admin, get_buttons, get_link, import_play_list, leave_call, play, get_playlist_str, send_playlist, shuffle_playlist, start_stream, stream_from_link -from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from logger import LOGGER from youtube_search import YoutubeSearch -from pyrogram import Client, filters +from contextlib import suppress from pyrogram.types import Message from youtube_dl import YoutubeDL from datetime import datetime from pyrogram import filters from config import Config -from logger import LOGGER import re +from utils import ( + add_to_db_playlist, + clear_db_playlist, + delete_messages, + download, + get_admins, + get_duration, + is_admin, + get_buttons, + get_link, + import_play_list, + is_audio, + leave_call, + play, + get_playlist_str, + send_playlist, + shuffle_playlist, + start_stream, + stream_from_link, + chat_filter +) +from pyrogram.types import ( + InlineKeyboardMarkup, + InlineKeyboardButton + ) +from pyrogram.errors import ( + MessageIdInvalid, + MessageNotModified + ) +from pyrogram import ( + Client, + filters + ) admin_filter=filters.create(is_admin) -@Client.on_message(filters.command(["play", f"play@{Config.BOT_USERNAME}"]) & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["play", "fplay", f"play@{Config.BOT_USERNAME}", f"fplay@{Config.BOT_USERNAME}"]) & chat_filter) async def add_to_playlist(_, message: Message): - if Config.ADMIN_ONLY == "Y": - admins = await get_admins(Config.CHAT) - if message.from_user.id not in admins: - await message.reply_sticker("CAADBQADsQIAAtILIVYld1n74e3JuQI") - return - type="" - yturl="" - ysearch="" - if message.reply_to_message and message.reply_to_message.video: - msg = await message.reply_text("⚡️ **Checking Telegram Media...**") - type='video' - m_video = message.reply_to_message.video - elif message.reply_to_message and message.reply_to_message.document: - msg = await message.reply_text("⚡️ **Checking Telegram Media...**") - m_video = message.reply_to_message.document - type='video' - if not "video" in m_video.mime_type: - return await msg.edit("The given file is invalid") - else: - if message.reply_to_message: - link=message.reply_to_message.text - regex = r"^(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)\&?" - match = re.match(regex,link) - if match: - type="youtube" - yturl=link - elif " " in message.text: - text = message.text.split(" ", 1) - query = text[1] + with suppress(MessageIdInvalid, MessageNotModified): + if Config.ADMIN_ONLY: + admins = await get_admins(Config.CHAT) + if not (message.from_user is None and message.sender_chat or message.from_user.id in admins): + k=await message.reply_sticker("CAADBQADsQIAAtILIVYld1n74e3JuQI") + await delete_messages([message, k]) + return + type="" + yturl="" + ysearch="" + if message.command[0] == "fplay": + if not (message.from_user is None and message.sender_chat or message.from_user.id in admins): + k=await message.reply("This command is only for admins.") + await delete_messages([message, k]) + return + msg = await message.reply_text("⚡️ **Checking recived input..**") + if message.reply_to_message and message.reply_to_message.video: + await msg.edit("⚡️ **Checking Telegram Media...**") + type='video' + m_video = message.reply_to_message.video + elif message.reply_to_message and message.reply_to_message.document: + await msg.edit("⚡️ **Checking Telegram Media...**") + m_video = message.reply_to_message.document + type='video' + if not "video" in m_video.mime_type: + return await msg.edit("The given file is invalid") + elif message.reply_to_message and message.reply_to_message.audio: + #if not Config.IS_VIDEO: + #return await message.reply("Play from audio file is available only if Video Mode if turned off.\nUse /settings to configure ypur player.") + await msg.edit("⚡️ **Checking Telegram Media...**") + type='audio' + m_video = message.reply_to_message.audio + else: + if message.reply_to_message and message.reply_to_message.text: + query=message.reply_to_message.text + elif " " in message.text: + text = message.text.split(" ", 1) + query = text[1] + else: + await msg.edit("You Didn't gave me anything to play.Reply to a video or a youtube link or a direct link.") + await delete_messages([message, msg]) + return regex = r"^(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)\&?" match = re.match(regex,query) if match: type="youtube" yturl=query + elif query.startswith("http"): + """if Config.IS_VIDEO: + try: + width, height = get_height_and_width(query) + except: + width, height = None, None + LOGGER.error("Unable to get video properties within time.") + if not width or \ + not height: + await msg.edit("This is an invalid link, provide me a direct link or a youtube link.") + await delete_messages([message, msg]) + return """ + #else: + try: + has_audio_ = is_audio(query) + except: + has_audio_ = False + LOGGER.error("Unable to get Audio properties within time.") + if not has_audio_: + await msg.edit("This is an invalid link, provide me a direct link or a youtube link.") + await delete_messages([message, msg]) + return + try: + dur=get_duration(query) + except: + dur=0 + if dur == 0: + await msg.edit("This is a live stream, Use /stream command.") + await delete_messages([message, msg]) + return + type="direct" + url=query else: type="query" ysearch=query + if not message.from_user is None: + user=f"[{message.from_user.first_name}](tg://user?id={message.from_user.id})" + user_id = message.from_user.id else: - await message.reply_text("You Didn't gave me anything to play.Reply to a video or a youtube link.") - return - user=f"[{message.from_user.first_name}](tg://user?id={message.from_user.id})" - if type=="video": + user="Anonymous" + user_id = "anonymous_admin" now = datetime.now() nyav = now.strftime("%d-%m-%Y-%H:%M:%S") - data={1:m_video.file_name, 2:m_video.file_id, 3:"telegram", 4:user, 5:f"{nyav}_{m_video.file_size}"} - Config.playlist.append(data) - await msg.edit("Media added to playlist") - if type=="youtube" or type=="query": - if type=="youtube": - msg = await message.reply_text("⚡️ **Fetching Video From YouTube...**") - url=yturl - elif type=="query": + if type in ["video", "audio"]: + if type == "audio": + title=m_video.title + else: + title=m_video.file_name + data={1:title, 2:m_video.file_id, 3:"telegram", 4:user, 5:f"{nyav}_{m_video.file_size}"} + if message.command[0] == "fplay": + pla = [data] + Config.playlist + Config.playlist = pla + else: + Config.playlist.append(data) + await add_to_db_playlist(data) + await msg.edit("Media added to playlist") + elif type=="youtube" or type=="query": + if type=="youtube": + await msg.edit("⚡️ **Fetching Video From YouTube...**") + url=yturl + elif type=="query": + try: + await msg.edit("⚡️ **Fetching Video From YouTube...**") + ytquery=ysearch + 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.." + ) + LOGGER.error(str(e)) + await delete_messages([message, msg]) + return + else: + return + ydl_opts = { + "geo-bypass": True, + "nocheckcertificate": True + } + ydl = YoutubeDL(ydl_opts) try: - msg = await message.reply_text("⚡️ **Fetching Video From YouTube...**") - ytquery=ysearch - results = YoutubeSearch(ytquery, max_results=1).to_dict() - url = f"https://youtube.com{results[0]['url_suffix']}" - title = results[0]["title"][:40] + info = ydl.extract_info(url, False) except Exception as e: + LOGGER.error(e) await msg.edit( - "Song not found.\nTry inline mode.." - ) + f"YouTube Download Error ❌\nError:- {e}" + ) LOGGER.error(str(e)) + await delete_messages([message, msg]) return + title = info["title"] + data={1:title, 2:url, 3:"youtube", 4:user, 5:f"{nyav}_{user_id}"} + if message.command[0] == "fplay": + pla = [data] + Config.playlist + Config.playlist = pla + else: + Config.playlist.append(data) + await add_to_db_playlist(data) + await msg.edit(f"[{title}]({url}) added to playist", disable_web_page_preview=True) + elif type == "direct": + data={1:"Music", 2:url, 3:"url", 4:user, 5:f"{nyav}_{user_id}"} + if message.command[0] == "fplay": + pla = [data] + Config.playlist + Config.playlist = pla + else: + Config.playlist.append(data) + await add_to_db_playlist(data) + await msg.edit("Link added to playlist") + if not Config.CALL_STATUS \ + and len(Config.playlist) >= 1: + await msg.edit("Downloading and Processing...") + await download(Config.playlist[0], msg) + await play() + elif (len(Config.playlist) == 1 and Config.CALL_STATUS): + await msg.edit("Downloading and Processing...") + await download(Config.playlist[0], msg) + await play() + elif message.command[0] == "fplay": + await msg.edit("Downloading and Processing...") + await download(Config.playlist[0], msg) + await play() else: - return - ydl_opts = { - "geo-bypass": True, - "nocheckcertificate": True - } - ydl = YoutubeDL(ydl_opts) - try: - info = ydl.extract_info(url, False) - except Exception as e: - LOGGER.error(e) - await msg.edit( - f"YouTube Download Error ❌\nError:- {e}" - ) - LOGGER.error(str(e)) - return - title = info["title"] - now = datetime.now() - nyav = now.strftime("%d-%m-%Y-%H:%M:%S") - data={1:title, 2:url, 3:"youtube", 4:user, 5:f"{nyav}_{message.from_user.id}"} - Config.playlist.append(data) - await msg.edit(f"[{title}]({url}) added to playist", disable_web_page_preview=True) - if len(Config.playlist) == 1: - m_status = await msg.edit("Downloading and Processing...") - await download(Config.playlist[0], m_status) - await play() - await m_status.delete() - else: - await send_playlist() - pl=await get_playlist_str() - if message.chat.type == "private": - await message.reply(pl, reply_markup=await get_buttons() ,disable_web_page_preview=True) - elif not Config.LOG_GROUP and message.chat.type == "supergroup": - await message.reply(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) - for track in Config.playlist[:2]: - await download(track) + await send_playlist() + await msg.delete() + pl=await get_playlist_str() + if message.chat.type == "private": + await message.reply(pl, reply_markup=await get_buttons() ,disable_web_page_preview=True) + elif not Config.LOG_GROUP and message.chat.type == "supergroup": + if Config.msg.get('playlist') is not None: + await Config.msg['playlist'].delete() + Config.msg['playlist']=await message.reply(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + await delete_messages([message]) + for track in Config.playlist[:2]: + await download(track) -@Client.on_message(filters.command(["leave", f"leave@{Config.BOT_USERNAME}"]) & admin_filter) +@Client.on_message(filters.command(["leave", f"leave@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def leave_voice_chat(_, m: Message): - if not Config.CALL_STATUS: - return await m.reply("Not joined any voicechat.") + if not Config.CALL_STATUS: + k=await m.reply("Not joined any voicechat.") + await delete_messages([m, k]) + return await leave_call() - await m.reply("Succesfully left videochat.") + k=await m.reply("Succesfully left videochat.") + await delete_messages([m, k]) -@Client.on_message(filters.command(["shuffle", f"shuffle@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["shuffle", f"shuffle@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def shuffle_play_list(client, m: Message): if not Config.CALL_STATUS: - return await m.reply("Not joined any voicechat.") + k = await m.reply("Not joined any voicechat.") + await delete_messages([m, k]) + return else: if len(Config.playlist) > 2: - await m.reply_text(f"Playlist Shuffled.") + k=await m.reply_text(f"Playlist Shuffled.") await shuffle_playlist() - + await delete_messages([m, k]) else: - await m.reply_text(f"You cant shuffle playlist with less than 3 songs.") + k=await m.reply_text(f"You cant shuffle playlist with less than 3 songs.") + await delete_messages([m, k]) -@Client.on_message(filters.command(["clearplaylist", f"clearplaylist@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["clearplaylist", f"clearplaylist@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def clear_play_list(client, m: Message): - if not Config.CALL_STATUS: - return await m.reply("Not joined any voicechat.") if not Config.playlist: - return await m.reply("Playlist is empty. May be Live streaming.") - Config.playlist.clear() - await m.reply_text(f"Playlist Cleared.") - await start_stream() + k = await m.reply("Playlist is empty.") + await delete_messages([m, k]) + return + Config.playlist.clear() + k=await m.reply_text(f"Playlist Cleared.") + await clear_db_playlist(all=True) + if Config.IS_LOOP \ + and not Config.YPLAY: + await start_stream() + else: + await leave_call() + await delete_messages([m, k]) -@Client.on_message(filters.command(["yplay", f"yplay@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["yplay", f"yplay@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def yt_play_list(client, m: Message): - if m.reply_to_message is not None and m.reply_to_message.document: - if m.reply_to_message.document.file_name != "YouTube_PlayList.json": - await m.reply("Invalid PlayList file given. Use @GetPlayListBot or search for a playlist in @DumpPlaylist to get a playlist file.") - return - ytplaylist=await m.reply_to_message.download() - status=await m.reply("Trying to get details from playlist.") - n=await import_play_list(ytplaylist) - if not n: - await status.edit("Errors Occured while importing playlist.") - return - if Config.SHUFFLE: - await shuffle_playlist() - pl=await get_playlist_str() - if m.chat.type == "private": - await status.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) - elif not Config.LOG_GROUP and m.chat.type == "supergroup": - await status.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + with suppress(MessageIdInvalid, MessageNotModified): + if m.reply_to_message is not None and m.reply_to_message.document: + if m.reply_to_message.document.file_name != "YouTube_PlayList.json": + k=await m.reply("Invalid PlayList file given. Use @GetPlayListBot or search for a playlist in @DumpPlaylist to get a playlist file.") + await delete_messages([m, k]) + return + ytplaylist=await m.reply_to_message.download() + status=await m.reply("Trying to get details from playlist.") + n=await import_play_list(ytplaylist) + if not n: + await status.edit("Errors Occured while importing playlist.") + await delete_messages([m, status]) + return + if Config.SHUFFLE: + await shuffle_playlist() + pl=await get_playlist_str() + if m.chat.type == "private": + await status.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + elif not Config.LOG_GROUP and m.chat.type == "supergroup": + if Config.msg.get("playlist") is not None: + await Config.msg['playlist'].delete() + Config.msg['playlist']=await status.edit(pl, disable_web_page_preview=True, reply_markup=await get_buttons()) + await delete_messages([m]) + else: + await delete_messages([m, status]) else: - await status.delete() - else: - await m.reply("No playList file given. Use @GetPlayListBot or search for a playlist in @DumpPlaylist to get a playlist file.") + k=await m.reply("No playList file given. Use @GetPlayListBot or search for a playlist in @DumpPlaylist to get a playlist file.") + await delete_messages([m, k]) -@Client.on_message(filters.command(["stream", f"stream@{Config.BOT_USERNAME}"]) & admin_filter & (filters.chat(Config.CHAT) | filters.private)) +@Client.on_message(filters.command(["stream", f"stream@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) async def stream(client, m: Message): - if m.reply_to_message: - link=m.reply_to_message.text - elif " " in m.text: - text = m.text.split(" ", 1) - link = text[1] - else: - return await m.reply("Provide a link to stream!") - regex = r"^(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)\&?" - match = re.match(regex,link) - if match: - stream_link=await get_link(link) - if not stream_link: - return await m.reply("This is an invalid link.") - else: - stream_link=link - k, msg=await stream_from_link(stream_link) - if k == False: - await m.reply(msg) - return - await m.reply(f"[Streaming]({stream_link}) Started.", disable_web_page_preview=True) - + with suppress(MessageIdInvalid, MessageNotModified): + msg=await m.reply("Checking the recived input.") + if m.reply_to_message and m.reply_to_message.text: + link=m.reply_to_message.text + elif " " in m.text: + text = m.text.split(" ", 1) + link = text[1] + else: + k = await msg.edit("Provide a link to stream!") + await delete_messages([m, k]) + return + regex = r"^(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)\&?" + match = re.match(regex,link) + if match: + stream_link=await get_link(link) + if not stream_link: + k = await msg.edit("This is an invalid link.") + await delete_messages([m, k]) + return + else: + stream_link=link + """if Config.IS_VIDEO: + try: + width, height = get_height_and_width(stream_link) + except: + width, height = None, None + LOGGER.error("Unable to get video properties within time.") + if not width or \ + not height: + k = await msg.edit("This is an invalid link, provide me a direct link or a youtube link.") + await delete_messages([m, k]) + return""" + #else: + try: + is_audio_ = is_audio(stream_link) + except: + is_audio_ = False + LOGGER.error("Unable to get Audio properties within time.") + if not is_audio_: + k = await msg.edit("This is an invalid link, provide me a direct link or a youtube link.") + await delete_messages([m, k]) + return + try: + dur=get_duration(stream_link) + except: + dur=0 + if dur != 0: + k = await msg.edit("This is not a live stream, Use /play command.") + await delete_messages([m, k]) + return + k, msg_=await stream_from_link(stream_link) + if k == False: + k = await msg.edit(msg_) + await delete_messages([m, k]) + return + if Config.msg.get('player'): + await Config.msg['player'].delete() + Config.msg['player']=await msg.edit(f"[Streaming]({stream_link}) Started. ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ", disable_web_page_preview=True, reply_markup=await get_buttons()) + await delete_messages([m]) + -admincmds=["yplay", "leave", "pause", "resume", "skip", "restart", "volume", "shuffle", "clearplaylist", "export", "import", "update", 'replay', 'logs', 'stream', f'stream@{Config.BOT_USERNAME}', f'logs@{Config.BOT_USERNAME}', f"replay@{Config.BOT_USERNAME}", f"yplay@{Config.BOT_USERNAME}", f"leave@{Config.BOT_USERNAME}", f"pause@{Config.BOT_USERNAME}", f"resume@{Config.BOT_USERNAME}", f"skip@{Config.BOT_USERNAME}", f"restart@{Config.BOT_USERNAME}", f"volume@{Config.BOT_USERNAME}", f"shuffle@{Config.BOT_USERNAME}", f"clearplaylist@{Config.BOT_USERNAME}", f"export@{Config.BOT_USERNAME}", f"import@{Config.BOT_USERNAME}", f"update@{Config.BOT_USERNAME}"] +admincmds=["yplay", "leave", "pause", "resume", "skip", "restart", "volume", "shuffle", "clearplaylist", "export", "import", "update", 'replay', 'logs', 'stream', 'fplay', 'schedule', 'record', 'slist', 'cancel', 'cancelall', 'vcpromote', 'vcdemote', 'refresh', 'rtitle', 'seek', 'mute', 'unmute', +f'stream@{Config.BOT_USERNAME}', f'logs@{Config.BOT_USERNAME}', f"replay@{Config.BOT_USERNAME}", f"yplay@{Config.BOT_USERNAME}", f"leave@{Config.BOT_USERNAME}", f"pause@{Config.BOT_USERNAME}", f"resume@{Config.BOT_USERNAME}", f"skip@{Config.BOT_USERNAME}", +f"restart@{Config.BOT_USERNAME}", f"volume@{Config.BOT_USERNAME}", f"shuffle@{Config.BOT_USERNAME}", f"clearplaylist@{Config.BOT_USERNAME}", f"export@{Config.BOT_USERNAME}", f"import@{Config.BOT_USERNAME}", f"update@{Config.BOT_USERNAME}", +f'play@{Config.BOT_USERNAME}', f'schedule@{Config.BOT_USERNAME}', f'record@{Config.BOT_USERNAME}', f'slist@{Config.BOT_USERNAME}', f'cancel@{Config.BOT_USERNAME}', f'cancelall@{Config.BOT_USERNAME}', f'vcpromote@{Config.BOT_USERNAME}', +f'vcdemote@{Config.BOT_USERNAME}', f'refresh@{Config.BOT_USERNAME}', f'rtitle@{Config.BOT_USERNAME}', f'seek@{Config.BOT_USERNAME}', f'mute@{Config.BOT_USERNAME}', f'unmute@{Config.BOT_USERNAME}' +] -@Client.on_message(filters.command(admincmds) & ~admin_filter & (filters.chat(Config.CHAT) | filters.private)) -async def notforu(_, m: Message): - await _.send_cached_media(chat_id=m.chat.id, file_id="CAADBQADEgQAAtMJyFVJOe6-VqYVzAI", caption="You Are Not Authorized", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('⚡️Join Here', url='https://t.me/subin_works')]])) allcmd = ["play", "player", f"play@{Config.BOT_USERNAME}", f"player@{Config.BOT_USERNAME}"] + admincmds -@Client.on_message(filters.command(allcmd) & ~filters.chat(Config.CHAT) & filters.group) +@Client.on_message(filters.command(admincmds) & ~admin_filter & chat_filter) +async def notforu(_, m: Message): + k = await _.send_cached_media(chat_id=m.chat.id, file_id="CAADBQADEgQAAtMJyFVJOe6-VqYVzAI", caption="You Are Not Authorized", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('⚡️Join Here', url='https://t.me/subin_works')]])) + await delete_messages([m, k]) + +@Client.on_message(filters.command(allcmd) & ~chat_filter & filters.group) async def not_chat(_, m: Message): - buttons = [ - [ - InlineKeyboardButton('⚡️Make Own Bot', url='https://github.com/subinps/VCPlayerBot'), - InlineKeyboardButton('🧩 Join Here', url='https://t.me/subin_works'), - ] - ] - await m.reply("You can't use this bot in this group, for that you have to make your own bot from the [SOURCE CODE](https://github.com/subinps/VCPlayerBot) below.", disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup(buttons)) + if m.from_user is not None and m.from_user.id in Config.SUDO: + buttons = [ + [ + InlineKeyboardButton('⚡️Change CHAT', callback_data='set_new_chat'), + ], + [ + InlineKeyboardButton('No', callback_data='closesudo'), + ] + ] + await m.reply("This is not the group which i have been configured to play, Do you want to set this group as default CHAT?", reply_markup=InlineKeyboardMarkup(buttons)) + await delete_messages([m]) + else: + buttons = [ + [ + InlineKeyboardButton('⚡️Make Own Bot', url='https://github.com/subinps/VCPlayerBot'), + InlineKeyboardButton('🧩 Join Here', url='https://t.me/subin_works'), + ] + ] + await m.reply("You can't use this bot in this group, for that you have to make your own bot from the [SOURCE CODE](https://github.com/subinps/VCPlayerBot) below.", disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup(buttons)) diff --git a/plugins/recorder.py b/plugins/recorder.py new file mode 100644 index 00000000..3d7440ff --- /dev/null +++ b/plugins/recorder.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# Copyright (C) @subinps +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from logger import LOGGER +from config import Config +from pyrogram import ( + Client, + filters +) +from utils import ( + chat_filter, + is_admin, + is_admin, + delete_messages, + recorder_settings, + sync_to_db +) +from pyrogram.types import ( + InlineKeyboardMarkup, + InlineKeyboardButton +) + +admin_filter=filters.create(is_admin) + + +@Client.on_message(filters.command(["record", f"record@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) +async def record_vc(bot, message): + await message.reply("Configure you VCPlayer Recording settings from hereㅤㅤ ㅤ", reply_markup=(await recorder_settings())) + await delete_messages([message]) + +@Client.on_message(filters.command(["rtitle", f"rtitle@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) +async def recording_title(bot, message): + m=await message.reply("Checking..") + if " " in message.text: + cmd, title = message.text.split(" ", 1) + else: + await m.edit("Give me a new title. Use /rtitle < Custom Title >\nUse False to revert to default title") + await delete_messages([message, m]) + return + + if Config.DATABASE_URI: + await m.edit("Mongo DB Found, Setting up recording title...") + if title == "False": + await m.edit(f"Sucessfully removed custom recording title.") + Config.RECORDING_TITLE=False + await sync_to_db() + await delete_messages([message, m]) + return + else: + Config.RECORDING_TITLE=title + await sync_to_db() + await m.edit(f"Succesfully changed recording title to {title}") + await delete_messages([message, m]) + return + else: + if not Config.HEROKU_APP: + buttons = [[InlineKeyboardButton('Heroku API_KEY', url='https://dashboard.heroku.com/account/applications/authorizations/new'), InlineKeyboardButton('🗑 Close', callback_data='close'),]] + await m.edit( + text="No heroku app found, this command needs the following heroku vars to be set.\n\n1. HEROKU_API_KEY: Your heroku account api key.\n2. HEROKU_APP_NAME: Your heroku app name.", + reply_markup=InlineKeyboardMarkup(buttons)) + await delete_messages([message]) + return + config = Config.HEROKU_APP.config() + if title == "False": + if "RECORDING_TITLE" in config: + await m.edit(f"Sucessfully removed custom recording title. Now restarting..") + await delete_messages([message]) + del config["RECORDING_TITLE"] + config["RECORDING_TITLE"] = None + else: + await m.edit(f"Its already default title, nothing was changed") + Config.RECORDING_TITLE=False + await delete_messages([message, m]) + else: + await m.edit(f"Succesfully changed recording title to {title}, Now restarting") + await delete_messages([message]) + config["RECORDING_TITLE"] = title \ No newline at end of file diff --git a/plugins/scheduler.py b/plugins/scheduler.py new file mode 100644 index 00000000..96c581a8 --- /dev/null +++ b/plugins/scheduler.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +# Copyright (C) @subinps +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from logger import LOGGER +import re +import calendar +from datetime import datetime +from contextlib import suppress +import pytz +from config import Config +from youtube_search import YoutubeSearch +from youtube_dl import YoutubeDL + +from pyrogram import( + Client, + filters + ) +from pyrogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup +) +from utils import ( + delete_messages, + is_admin, + sync_to_db, + is_audio, + chat_filter, + scheduler +) + +from pyrogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup +) +from pyrogram.errors import ( + MessageIdInvalid, + MessageNotModified +) + + +IST = pytz.timezone(Config.TIME_ZONE) + +admin_filter=filters.create(is_admin) + + + +@Client.on_message(filters.command(["schedule", f"schedule@{Config.BOT_USERNAME}"]) & chat_filter & admin_filter) +async def schedule_vc(bot, message): + with suppress(MessageIdInvalid, MessageNotModified): + type="" + yturl="" + ysearch="" + msg = await message.reply_text("⚡️ **Checking recived input..**") + if message.reply_to_message and message.reply_to_message.video: + await msg.edit("⚡️ **Checking Telegram Media...**") + type='video' + m_video = message.reply_to_message.video + elif message.reply_to_message and message.reply_to_message.document: + await msg.edit("⚡️ **Checking Telegram Media...**") + m_video = message.reply_to_message.document + type='video' + if not "video" in m_video.mime_type: + return await msg.edit("The given file is invalid") + elif message.reply_to_message and message.reply_to_message.audio: + #if not Config.IS_VIDEO: + #return await message.reply("Play from audio file is available only if Video Mode if turned off.\nUse /settings to configure ypur player.") + await msg.edit("⚡️ **Checking Telegram Media...**") + type='audio' + m_video = message.reply_to_message.audio + else: + if message.reply_to_message and message.reply_to_message.text: + query=message.reply_to_message.text + elif " " in message.text: + text = message.text.split(" ", 1) + query = text[1] + else: + await msg.edit("You Didn't gave me anything to schedule. Reply to a video or a youtube link or a direct link.") + await delete_messages([message, msg]) + return + regex = r"^(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)\&?" + match = re.match(regex,query) + if match: + type="youtube" + yturl=query + elif query.startswith("http"): + """if Config.IS_VIDEO: + try: + width, height = get_height_and_width(query) + except: + width, height = None, None + LOGGER.error("Unable to get video properties within time.") + if not width or \ + not height: + await msg.edit("This is an invalid link, provide me a direct link or a youtube link.") + await delete_messages([message, msg]) + return """ + #else: + try: + has_audio_ = is_audio(query) + except: + has_audio_ = False + LOGGER.error("Unable to get Audio properties within time.") + if not has_audio_: + await msg.edit("This is an invalid link, provide me a direct link or a youtube link.") + await delete_messages([message, msg]) + return + type="direct" + url=query + else: + type="query" + ysearch=query + if not message.from_user is None: + user=f"[{message.from_user.first_name}](tg://user?id={message.from_user.id}) - (Scheduled)" + user_id = message.from_user.id + else: + user="Anonymous - (Scheduled)" + user_id = "anonymous_admin" + now = datetime.now() + nyav = now.strftime("%d-%m-%Y-%H:%M:%S") + if type in ["video", "audio"]: + if type == "audio": + title=m_video.title + else: + title=m_video.file_name + data={'1':title, '2':m_video.file_id, '3':"telegram", '4':user, '5':f"{nyav}_{m_video.file_size}"} + sid=f"{message.chat.id}_{msg.message_id}" + Config.SCHEDULED_STREAM[sid] = data + await sync_to_db() + elif type=="youtube" or type=="query": + if type=="youtube": + await msg.edit("⚡️ **Fetching Video From YouTube...**") + url=yturl + elif type=="query": + try: + await msg.edit("⚡️ **Fetching Video From YouTube...**") + ytquery=ysearch + 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.." + ) + LOGGER.error(str(e)) + await delete_messages([message, msg]) + return + else: + return + ydl_opts = { + "quite": True, + "geo-bypass": True, + "nocheckcertificate": True + } + ydl = YoutubeDL(ydl_opts) + try: + info = ydl.extract_info(url, False) + except Exception as e: + LOGGER.error(e) + await msg.edit( + f"YouTube Download Error ❌\nError:- {e}" + ) + LOGGER.error(str(e)) + await delete_messages([message, msg]) + return + title = info["title"] + data={'1':title, '2':url, '3':"youtube", '4':user, '5':f"{nyav}_{user_id}"} + sid=f"{message.chat.id}_{msg.message_id}" + Config.SCHEDULED_STREAM[sid] = data + await sync_to_db() + elif type == "direct": + data={"1":"Music", '2':url, '3':"url", '4':user, '5':f"{nyav}_{user_id}"} + sid=f"{message.chat.id}_{msg.message_id}" + Config.SCHEDULED_STREAM[sid] = data + await sync_to_db() + if message.chat.type!='private' and message.from_user is None: + await msg.edit( + text="You cant schedule from here since you are an anonymous admin. Click the schedule button to schedule through private chat.", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(f"Schedule", url=f"https://telegram.dog/{Config.BOT_USERNAME}?start=sch_{sid}"), + ] + ] + ),) + await delete_messages([message, msg]) + return + today = datetime.now(IST) + smonth=today.strftime("%B") + obj = calendar.Calendar() + thisday = today.day + year = today.year + month = today.month + m=obj.monthdayscalendar(year, month) + button=[] + button.append([InlineKeyboardButton(text=f"{str(smonth)} {str(year)}",callback_data=f"sch_month_choose_none_none")]) + days=["Mon", "Tues", "Wed", "Thu", "Fri", "Sat", "Sun"] + f=[] + for day in days: + f.append(InlineKeyboardButton(text=f"{day}",callback_data=f"day_info_none")) + button.append(f) + for one in m: + f=[] + for d in one: + year_=year + if d < int(today.day): + year_ += 1 + if d == 0: + k="\u2063" + d="none" + else: + k=d + f.append(InlineKeyboardButton(text=f"{k}",callback_data=f"sch_month_{year_}_{month}_{d}")) + button.append(f) + button.append([InlineKeyboardButton("Close", callback_data="schclose")]) + await msg.edit(f"Choose the day of the month you want to schedule the voicechat.\nToday is {thisday} {smonth} {year}. Chooosing a date preceeding today will be considered as next year {year+1}", reply_markup=InlineKeyboardMarkup(button)) + + + + +@Client.on_message(filters.command(["slist", f"slist@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) +async def list_schedule(bot, message): + k=await message.reply("Checking schedules...") + if not Config.SCHEDULE_LIST: + await k.edit("Nothing scheduled to play.") + await delete_messages([k, message]) + return + text="Current Schedules:\n\n" + s=Config.SCHEDULE_LIST + f=1 + for sch in s: + details=Config.SCHEDULED_STREAM.get(sch['job_id']) + if not details['3']=="telegram": + text+=f"{f}. Title: [{details['1']}]({details['2']}) By {details['4']}\n" + else: + text+=f"{f}. Title: {details['1']} By {details['4']}\n" + date = sch['date'] + f+=1 + date_=((pytz.utc.localize(date, is_dst=None).astimezone(IST)).replace(tzinfo=None)).strftime("%b %d %Y, %I:%M %p") + text+=f"Shedule ID : {sch['job_id']}\nSchedule Date : {date_}\n\n" + + await k.edit(text, disable_web_page_preview=True) + await delete_messages([message]) + + +@Client.on_message(filters.command(["cancel", f"cancel@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) +async def delete_sch(bot, message): + with suppress(MessageIdInvalid, MessageNotModified): + m = await message.reply("Finding the scheduled stream..") + if " " in message.text: + cmd, job_id = message.text.split(" ", 1) + else: + buttons = [ + [ + InlineKeyboardButton('Cancel All Schedules', callback_data='schcancel'), + InlineKeyboardButton('No', callback_data='schclose'), + ] + ] + reply_markup = InlineKeyboardMarkup(buttons) + await m.edit("No Schedule ID specified!! Do you want to Cancel all scheduled streams? or you can find schedul id using /slist command.", reply_markup=reply_markup) + await delete_messages([message]) + return + data=Config.SCHEDULED_STREAM.get(job_id) + if not data: + await m.edit("You gave me an invalid schedule ID, check again and send.") + await delete_messages([message, m]) + return + del Config.SCHEDULED_STREAM[job_id] + k=scheduler.get_job(job_id, jobstore=None) + if k: + scheduler.remove_job(job_id, jobstore=None) + old=list(filter(lambda k: k['job_id'] == job_id, Config.SCHEDULE_LIST)) + if old: + for old_ in old: + Config.SCHEDULE_LIST.remove(old_) + await sync_to_db() + await m.edit(f"Succesfully deleted {data['1']} from scheduled list.") + await delete_messages([message, m]) + +@Client.on_message(filters.command(["cancelall", f"cancelall@{Config.BOT_USERNAME}"]) & admin_filter & chat_filter) +async def delete_all_sch(bot, message): + buttons = [ + [ + InlineKeyboardButton('Cancel All Schedules', callback_data='schcancel'), + InlineKeyboardButton('No', callback_data='schclose'), + ] + ] + reply_markup = InlineKeyboardMarkup(buttons) + await message.reply("Do you want to cancel all the scheduled streams?ㅤㅤㅤㅤ ㅤ", reply_markup=reply_markup) + await delete_messages([message]) + + diff --git a/requirements.txt b/requirements.txt index 48b91d62..4ace93fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ git+https://github.com/pyrogram/pyrogram@master -py-tgcalls==0.8.0 +py-tgcalls==0.8.1b25 tgcrypto ffmpeg-python wrapt_timeout_decorator @@ -7,3 +7,8 @@ youtube_dl youtube_search_python youtube_search heroku3 +pillow +motor +dnspython +pytz +apscheduler \ No newline at end of file diff --git a/start.sh b/start.sh index cb2e71ef..6ea0b83f 100644 --- a/start.sh +++ b/start.sh @@ -1,5 +1,12 @@ echo "Cloning Repo...." -git clone https://github.com/subinps/VCPlayerBot /VCPlayerBot +if [[ $BRANCH != "None" ]] +then + echo "Cloning beta branch...." + git clone https://github.com/subinps/VCPlayerBot -b $BRANCH /VCPlayerBot +else + echo "Cloning main branch...." + git clone https://github.com/subinps/VCPlayerBot /VCPlayerBot +fi cd /VCPlayerBot pip3 install -U -r requirements.txt echo "Starting Bot...." diff --git a/userplugins/group_call.py b/userplugins/group_call.py new file mode 100644 index 00000000..4704219a --- /dev/null +++ b/userplugins/group_call.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +# Copyright (C) @subinps +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from logger import LOGGER +from pyrogram.errors import BotInlineDisabled +from pyrogram import Client, filters +from config import Config +from user import group_call +import time +from asyncio import sleep +from pyrogram.raw.base import Update +from pyrogram.raw.functions.channels import GetFullChannel +from pytgcalls import PyTgCalls +from pytgcalls.types import Update +from pyrogram.raw.types import ( + UpdateGroupCall, + GroupCallDiscarded, + UpdateGroupCallParticipants +) +from pytgcalls.types.groups import ( + JoinedVoiceChat, + LeftVoiceChat + ) +from pytgcalls.types.stream import ( + PausedStream, + ResumedStream, + MutedStream, + UnMutedStream, + StreamAudioEnded, + StreamVideoEnded +) +from utils import ( + start_record_stream, + stop_recording, + edit_title, + stream_from_link, + leave_call, + start_stream, + skip, + sync_to_db, + scheduler +) + +async def is_reply(_, client, message): + if Config.REPLY_PM: + return True + else: + return False +reply_filter=filters.create(is_reply) + +DUMBED=[] +async def dumb_it(_, client, message): + if Config.RECORDING_DUMP and Config.LISTEN: + return True + else: + return False +rec_filter=filters.create(dumb_it) + +@Client.on_message(reply_filter & filters.private & ~filters.bot & filters.incoming & ~filters.service & ~filters.me & ~filters.chat([777000, 454000])) +async def reply(client, message): + try: + inline = await client.get_inline_bot_results(Config.BOT_USERNAME, "ETHO_ORUTHAN_PM_VANNU") + m=await client.send_inline_bot_result( + message.chat.id, + query_id=inline.query_id, + result_id=inline.results[0].id, + hide_via=True + ) + old=Config.msg.get(message.chat.id) + if old: + await client.delete_messages(message.chat.id, [old["msg"], old["s"]]) + Config.msg[message.chat.id]={"msg":m.updates[1].message.id, "s":message.message_id} + except BotInlineDisabled: + LOGGER.error(f"Error: Inline Mode for @{Config.BOT_USERNAME} is not enabled. Enable from @Botfather to enable PM Permit.") + await message.reply(f"{Config.REPLY_MESSAGE}\n\nYou can't use this bot in your group, for that you have to make your own bot from the [SOURCE CODE](https://github.com/subinps/VCPlayerBot) below.", disable_web_page_preview=True) + except Exception as e: + LOGGER.error(e) + pass + + +@Client.on_message(filters.private & filters.media & filters.me & rec_filter) +async def dumb_to_log(client, message): + if message.video and message.video.file_name == "record.mp4": + await message.copy(int(Config.RECORDING_DUMP)) + DUMBED.append("video") + if message.audio and message.audio.file_name == "record.ogg": + await message.copy(int(Config.RECORDING_DUMP)) + DUMBED.append("audio") + if Config.IS_VIDEO_RECORD: + if len(DUMBED) == 2: + DUMBED.clear() + Config.LISTEN=False + else: + if len(DUMBED) == 1: + DUMBED.clear() + Config.LISTEN=False + + +@Client.on_message(filters.service & filters.chat(Config.CHAT)) +async def service_msg(client, message): + if message.service == 'voice_chat_started': + Config.IS_ACTIVE=True + k=scheduler.get_job(str(Config.CHAT), jobstore=None) #scheduled records + if k: + await start_record_stream() + LOGGER.info("Resuming recording..") + elif Config.WAS_RECORDING: + LOGGER.info("Previous recording was ended unexpectedly, Now resuming recordings.") + await start_record_stream()#for unscheduled + a = await client.send( + GetFullChannel( + channel=( + await client.resolve_peer( + Config.CHAT + ) + ) + ) + ) + if a.full_chat.call is not None: + Config.CURRENT_CALL=a.full_chat.call.id + LOGGER.info("Voice chat started.") + await sync_to_db() + elif message.service == 'voice_chat_scheduled': + LOGGER.info("VoiceChat Scheduled") + Config.IS_ACTIVE=False + Config.HAS_SCHEDULE=True + await sync_to_db() + elif message.service == 'voice_chat_ended': + Config.IS_ACTIVE=False + LOGGER.info("Voicechat ended") + Config.CURRENT_CALL=None + if Config.IS_RECORDING: + Config.WAS_RECORDING=True + await stop_recording() + await sync_to_db() + else: + pass + +@Client.on_raw_update() +async def handle_raw_updates(client: Client, update: Update, user: dict, chat: dict): + if isinstance(update, UpdateGroupCallParticipants): + if not Config.CURRENT_CALL: + a = await client.send( + GetFullChannel( + channel=( + await client.resolve_peer( + Config.CHAT + ) + ) + ) + ) + if a.full_chat.call is not None: + Config.CURRENT_CALL=a.full_chat.call.id + if Config.CURRENT_CALL and update.call.id == Config.CURRENT_CALL: + all=update.participants + old=list(filter(lambda k: k.peer.user_id == Config.USER_ID, all)) + if old: + for me in old: + if me.volume: + Config.VOLUME=round(int(me.volume)/100) + + + if isinstance(update, UpdateGroupCall) and (update.chat_id == int(-1000000000000-Config.CHAT)): + if update.call is None: + Config.IS_ACTIVE = False + Config.CURRENT_CALL=None + LOGGER.warning("No Active Group Calls Found.") + if Config.IS_RECORDING: + Config.WAS_RECORDING=True + await stop_recording() + LOGGER.warning("Group call was ended and hence stoping recording.") + Config.HAS_SCHEDULE = False + await sync_to_db() + return + + else: + call=update.call + if isinstance(call, GroupCallDiscarded): + Config.CURRENT_CALL=None + Config.IS_ACTIVE=False + if Config.IS_RECORDING: + Config.WAS_RECORDING=True + await stop_recording() + LOGGER.warning("Group Call Was ended") + Config.CALL_STATUS = False + await sync_to_db() + return + Config.IS_ACTIVE=True + Config.CURRENT_CALL=call.id + if Config.IS_RECORDING and not call.record_video_active: + Config.LISTEN=True + await stop_recording() + LOGGER.warning("Recording was ended by user, hence stopping the schedules.") + return + if call.schedule_date: + Config.HAS_SCHEDULE=True + else: + Config.HAS_SCHEDULE=False + await sync_to_db() + +@group_call.on_raw_update() +async def handler(client: PyTgCalls, update: Update): + if isinstance(update, JoinedVoiceChat): + Config.CALL_STATUS = True + if Config.EDIT_TITLE: + await edit_title() + elif isinstance(update, LeftVoiceChat): + Config.CALL_STATUS = False + elif isinstance(update, PausedStream): + Config.DUR['PAUSE'] = time.time() + Config.PAUSE=True + elif isinstance(update, ResumedStream): + pause=Config.DUR.get('PAUSE') + if pause: + diff = time.time() - pause + start=Config.DUR.get('TIME') + if start: + Config.DUR['TIME']=start+diff + Config.PAUSE=False + elif isinstance(update, MutedStream): + Config.MUTED = True + elif isinstance(update, UnMutedStream): + Config.MUTED = False + + + +@group_call.on_stream_end() +async def handler(client: PyTgCalls, update: Update): + if isinstance(update, StreamAudioEnded) or isinstance(update, StreamVideoEnded): + if not Config.STREAM_END.get("STATUS"): + Config.STREAM_END["STATUS"]=str(update) + if Config.STREAM_LINK and len(Config.playlist) == 0: + if Config.IS_LOOP: + await stream_from_link(Config.STREAM_LINK) + else: + await leave_call() + elif not Config.playlist: + if Config.IS_LOOP: + await start_stream() + else: + await leave_call() + else: + await skip() + await sleep(15) #wait for max 15 sec + try: + del Config.STREAM_END["STATUS"] + except: + pass + else: + try: + del Config.STREAM_END["STATUS"] + except: + pass + + + diff --git a/userplugins/pm_reply.py b/userplugins/pm_reply.py deleted file mode 100644 index 7fd0cac8..00000000 --- a/userplugins/pm_reply.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) @subinps -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from pyrogram.errors import BotInlineDisabled -from pyrogram import Client, filters -from logger import LOGGER -from config import Config - -async def is_reply(_, client, message): - if Config.REPLY_MESSAGE: - return True - else: - return False -reply_filter=filters.create(is_reply) - -@Client.on_message(reply_filter & filters.private & ~filters.bot & filters.incoming & ~filters.service & ~filters.me & ~filters.chat([777000, 454000])) -async def reply(client, message): - try: - inline = await client.get_inline_bot_results(Config.BOT_USERNAME, "ETHO_ORUTHAN_PM_VANNU") - m=await client.send_inline_bot_result( - message.chat.id, - query_id=inline.query_id, - result_id=inline.results[0].id, - hide_via=True - ) - old=Config.msg.get(message.chat.id) - if old: - await client.delete_messages(message.chat.id, [old["msg"], old["s"]]) - Config.msg[message.chat.id]={"msg":m.updates[1].message.id, "s":message.message_id} - except BotInlineDisabled: - LOGGER.error(f"Error: Inline Mode for @{Config.BOT_USERNAME} is not enabled. Enable from @Botfather to enable PM Permit.") - await message.reply(f"{Config.REPLY_MESSAGE}\n\nYou can't use this bot in your group, for that you have to make your own bot from the [SOURCE CODE](https://github.com/subinps/VCPlayerBot) below.", disable_web_page_preview=True) - except Exception as e: - LOGGER.error(e) - pass \ No newline at end of file diff --git a/utils.py b/utils.py index 984745dc..82a48592 100644 --- a/utils.py +++ b/utils.py @@ -12,37 +12,73 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . + from logger import LOGGER try: - from pytgcalls.types.input_stream import InputAudioStream, InputVideoStream, AudioParameters, VideoParameters - from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message - from pyrogram.raw.functions.phone import EditGroupCallTitle, CreateGroupCall - from pytgcalls.exceptions import GroupCallNotFound, NoActiveGroupCall - from pyrogram.errors.exceptions.bad_request_400 import BadRequest - from pyrogram.raw.functions.channels import GetFullChannel - from concurrent.futures import CancelledError from pyrogram.raw.types import InputChannel from wrapt_timeout_decorator import timeout - from pytgcalls.types import Update - from user import group_call, USER + from apscheduler.schedulers.asyncio import AsyncIOScheduler + from apscheduler.jobstores.mongodb import MongoDBJobStore + from apscheduler.jobstores.base import ConflictingIdError + from pyrogram.raw.functions.channels import GetFullChannel from pytgcalls import StreamType from youtube_dl import YoutubeDL - from pytgcalls import PyTgCalls + from pyrogram import filters + from pymongo import MongoClient from datetime import datetime from threading import Thread from config import Config - from asyncio import sleep - from signal import SIGINT + from asyncio import sleep from bot import bot import subprocess import asyncio import random + import re import ffmpeg import json import time import sys import os import math + from pyrogram.errors.exceptions.bad_request_400 import ( + BadRequest, + ScheduleDateInvalid + ) + from pytgcalls.types.input_stream import ( + AudioVideoPiped, + AudioPiped, + AudioImagePiped + ) + from pytgcalls.types.input_stream import ( + AudioParameters, + VideoParameters + ) + from pyrogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup, + Message + ) + from pyrogram.raw.functions.phone import ( + EditGroupCallTitle, + CreateGroupCall, + ToggleGroupCallRecord, + StartScheduledGroupCall + ) + from pytgcalls.exceptions import ( + GroupCallNotFound, + NoActiveGroupCall, + InvalidVideoProportion + ) + from PIL import ( + Image, + ImageFont, + ImageDraw + ) + + from user import ( + group_call, + USER + ) except ModuleNotFoundError: import os import sys @@ -50,8 +86,17 @@ file=os.path.abspath("requirements.txt") subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade']) os.execl(sys.executable, sys.executable, *sys.argv) -ffmpeg_log = open("ffmpeg.txt", "w+") +if Config.DATABASE_URI: + from database import db + monclient = MongoClient(Config.DATABASE_URI) + jobstores = { + 'default': MongoDBJobStore(client=monclient, database=Config.DATABASE_NAME, collection='scheduler') + } + scheduler = AsyncIOScheduler(jobstores=jobstores) +else: + scheduler = AsyncIOScheduler() +scheduler.start() async def play(): @@ -59,30 +104,114 @@ async def play(): if song[3] == "telegram": file=Config.GET_FILE.get(song[5]) if not file: + await download(song) + while not file: await sleep(1) + file=Config.GET_FILE.get(song[5]) + LOGGER.info("Downloading the file from TG") while not os.path.exists(file): await sleep(1) + elif song[3] == "direct": + file=song[2] else: file=await get_link(song[2]) if not file: await skip() return False - audio_file, video_file, width, height = await get_raw_files(file) + link, seek, pic, width, height = await chek_the_media(file, title=f"{song[1]}") + if not link: + LOGGER.warning("Unsupported link, Skiping from queue.") + return await sleep(1) if Config.STREAM_LINK: Config.STREAM_LINK=False - await join_call(audio_file, video_file, width, height) - + await join_call(link, seek, pic, width, height) +async def schedule_a_play(job_id, date): + try: + scheduler.add_job(run_schedule, "date", [job_id], id=job_id, run_date=date, max_instances=50, misfire_grace_time=None) + except ConflictingIdError: + LOGGER.warning("This already scheduled") + return + if not Config.CALL_STATUS or not Config.IS_ACTIVE: + if Config.SCHEDULE_LIST[0]['job_id'] == job_id \ + and (date - datetime.now()).total_seconds() < 86400: + song=Config.SCHEDULED_STREAM.get(job_id) + if Config.IS_RECORDING: + scheduler.add_job(start_record_stream, "date", id=str(Config.CHAT), run_date=date, max_instances=50, misfire_grace_time=None) + try: + await USER.send(CreateGroupCall( + peer=(await USER.resolve_peer(Config.CHAT)), + random_id=random.randint(10000, 999999999), + schedule_date=int(date.timestamp()), + title=song['1'] + ) + ) + Config.HAS_SCHEDULE=True + except ScheduleDateInvalid: + LOGGER.error("Unable to schedule VideoChat, since date is invalid") + except Exception as e: + LOGGER.error(f"Error in scheduling voicechat- {e}") + await sync_to_db() + +async def run_schedule(job_id): + data=Config.SCHEDULED_STREAM.get(job_id) + if not data: + LOGGER.error("The Scheduled stream was not played, since data is missing") + old=filter(lambda k: k['job_id'] == job_id, Config.SCHEDULE_LIST) + if old: + Config.SCHEDULE_LIST.remove(old) + await sync_to_db() + pass + else: + if Config.HAS_SCHEDULE: + if not await start_scheduled(): + LOGGER.error("Scheduled stream skiped, Reason - Unable to start a voice chat.") + return + data_ = [{1:data['1'], 2:data['2'], 3:data['3'], 4:data['4'], 5:data['5']}] + Config.playlist = data_ + Config.playlist + await play() + LOGGER.info("Starting Scheduled Stream") + del Config.SCHEDULED_STREAM[job_id] + old=list(filter(lambda k: k['job_id'] == job_id, Config.SCHEDULE_LIST)) + if old: + for old_ in old: + Config.SCHEDULE_LIST.remove(old_) + if not Config.SCHEDULE_LIST: + Config.SCHEDULED_STREAM = {} #clear the unscheduled streams + await sync_to_db() + if len(Config.playlist) <= 1: + return + await download(Config.playlist[1]) + +async def cancel_all_schedules(): + for sch in Config.SCHEDULE_LIST: + job=sch['job_id'] + k=scheduler.get_job(job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + if Config.SCHEDULED_STREAM.get(job): + del Config.SCHEDULED_STREAM[job] + Config.SCHEDULE_LIST.clear() + await sync_to_db() + LOGGER.info("All the schedules are removed") async def skip(): - if Config.STREAM_LINK and len(Config.playlist) == 0: + if Config.STREAM_LINK and len(Config.playlist) == 0 and Config.IS_LOOP: await stream_from_link() return - elif not Config.playlist: + elif not Config.playlist \ + and Config.IS_LOOP: + LOGGER.info("Loop Play enabled, switching to STARTUP_STREAM, since playlist is empty.") await start_stream() return + elif not Config.playlist \ + and not Config.IS_LOOP: + LOGGER.info("Loop Play is disabled, leaving call since playlist is empty.") + await leave_call() + return old_track = Config.playlist.pop(0) + await clear_db_playlist(song=old_track) if old_track[3] == "telegram": file=Config.GET_FILE.get(old_track[5]) try: @@ -90,10 +219,17 @@ async def skip(): except: pass del Config.GET_FILE[old_track[5]] - if not Config.playlist: + if not Config.playlist \ + and Config.IS_LOOP: + LOGGER.info("Loop Play enabled, switching to STARTUP_STREAM, since playlist is empty.") await start_stream() return - LOGGER.warning(f"START PLAYING: {Config.playlist[0][1]}") + elif not Config.playlist \ + and not Config.IS_LOOP: + LOGGER.info("Loop Play is disabled, leaving call since playlist is empty.") + await leave_call() + return + LOGGER.info(f"START PLAYING: {Config.playlist[0][1]}") if Config.DUR.get('PAUSE'): del Config.DUR['PAUSE'] await play() @@ -102,112 +238,358 @@ async def skip(): await download(Config.playlist[1]) -async def join_call(audio, video, width, height, seek=False): - while not os.path.exists(audio) or \ - not os.path.exists(video): - await skip() +async def check_vc(): + a = await bot.send(GetFullChannel(channel=(await bot.resolve_peer(Config.CHAT)))) + if a.full_chat.call is None: + try: + LOGGER.info("No active calls found, creating new") + await USER.send(CreateGroupCall( + peer=(await USER.resolve_peer(Config.CHAT)), + random_id=random.randint(10000, 999999999) + ) + ) + if Config.WAS_RECORDING: + await start_record_stream() + await sleep(2) + return True + except Exception as e: + LOGGER.error(f"Unable to start new GroupCall :- {e}") + return False + else: + if Config.HAS_SCHEDULE: + await start_scheduled() + return True + + +async def join_call(link, seek, pic, width, height): + if not await check_vc(): + LOGGER.error("No voice call found and was unable to create a new one. Exiting...") + return + if Config.HAS_SCHEDULE: + await start_scheduled() if Config.CALL_STATUS: - play=await change_file(audio, video, width, height) + if Config.IS_ACTIVE == False: + Config.CALL_STATUS = False + return await join_call(link, seek, pic, width, height) + play=await change_file(link, seek, pic, width, height) else: - play=await join_and_play(audio, video, width, height) + play=await join_and_play(link, seek, pic, width, height) if play == False: await sleep(1) - await join_call(audio, video, width, height) + await join_call(link, seek, pic, width, height) await sleep(1) + if not seek: + Config.DUR["TIME"]=time.time() + if Config.EDIT_TITLE: + await edit_title() + old=Config.GET_FILE.get("old") + if old: + for file in old: + os.remove(f"./downloads/{file}") + try: + del Config.GET_FILE["old"] + except: + LOGGER.error("Error in Deleting from dict") + pass + await send_playlist() + +async def start_scheduled(): try: - call=group_call.get_call(Config.CHAT) - except GroupCallNotFound: - return await restart() + await USER.send( + StartScheduledGroupCall( + call=( + await USER.send( + GetFullChannel( + channel=( + await USER.resolve_peer( + Config.CHAT + ) + ) + ) + ) + ).full_chat.call + ) + ) + if Config.WAS_RECORDING: + await start_record_stream() + return True except Exception as e: - LOGGER.warning(e) - return await restart() - if str(call.status) != "playing": - await restart() - else: - if not seek: - Config.DUR["TIME"]=time.time() - old=Config.GET_FILE.get("old") - if old: - for file in old: - os.remove(f"./downloads/{file}") - try: - del Config.GET_FILE["old"] - except: - LOGGER.error("Error in deletion") - pass - await send_playlist() - + if 'GROUPCALL_ALREADY_STARTED' in str(e): + LOGGER.warning("Already Groupcall Exist") + return True + else: + Config.HAS_SCHEDULE=False + return await check_vc() -async def join_and_play(audio, video, width, height): +async def join_and_play(link, seek, pic, width, height): try: - await group_call.join_group_call( - int(Config.CHAT), - InputAudioStream( - audio, - AudioParameters( - bitrate=48000, - ), - ), - InputVideoStream( - video, - VideoParameters( - width=width, - height=height, - frame_rate=30, - ), - - ), - stream_type=StreamType().local_stream - ) + if seek: + start=str(seek['start']) + end=str(seek['end']) + if not Config.IS_VIDEO: + await group_call.join_group_call( + int(Config.CHAT), + AudioPiped( + link, + audio_parameters=Config.AUDIO_Q, + additional_ffmpeg_parameters=f'-ss {start} -atend -t {end}', + ), + stream_type=StreamType().pulse_stream, + ) + else: + if pic: + await group_call.join_group_call( + int(Config.CHAT), + AudioImagePiped( + link, + pic, + audio_parameters=Config.AUDIO_Q, + video_parameters=Config.VIDEO_Q, + additional_ffmpeg_parameters=f'-ss {start} -atend -t {end}', ), + stream_type=StreamType().pulse_stream, + ) + else: + if not width \ + or not height: + LOGGER.error("No Valid Video Found and hence removed from playlist.") + return await skip() + if Config.BITRATE and Config.FPS: + await group_call.join_group_call( + int(Config.CHAT), + AudioVideoPiped( + link, + video_parameters=VideoParameters( + width, + height, + Config.FPS, + ), + audio_parameters=AudioParameters( + Config.BITRATE + ), + additional_ffmpeg_parameters=f'-ss {start} -atend -t {end}', + ), + stream_type=StreamType().pulse_stream, + ) + else: + await group_call.join_group_call( + int(Config.CHAT), + AudioVideoPiped( + link, + video_parameters=Config.VIDEO_Q, + audio_parameters=Config.AUDIO_Q, + additional_ffmpeg_parameters=f'-ss {start} -atend -t {end}', + ), + stream_type=StreamType().pulse_stream, + ) + else: + if not Config.IS_VIDEO: + await group_call.join_group_call( + int(Config.CHAT), + AudioPiped( + link, + audio_parameters=Config.AUDIO_Q, + ), + stream_type=StreamType().pulse_stream, + ) + else: + if pic: + await group_call.join_group_call( + int(Config.CHAT), + AudioImagePiped( + link, + pic, + video_parameters=Config.VIDEO_Q, + audio_parameters=Config.AUDIO_Q, + ), + stream_type=StreamType().pulse_stream, + ) + else: + if not width \ + or not height: + LOGGER.error("No Valid Video Found and hence removed from playlist.") + return await skip() + if Config.FPS and Config.BITRATE: + await group_call.join_group_call( + int(Config.CHAT), + AudioVideoPiped( + link, + video_parameters=VideoParameters( + width, + height, + Config.FPS, + ), + audio_parameters=AudioParameters( + Config.BITRATE + ), + ), + stream_type=StreamType().pulse_stream, + ) + else: + await group_call.join_group_call( + int(Config.CHAT), + AudioVideoPiped( + link, + video_parameters=Config.VIDEO_Q, + audio_parameters=Config.AUDIO_Q + ), + stream_type=StreamType().pulse_stream, + ) Config.CALL_STATUS=True + return True except NoActiveGroupCall: try: - LOGGER.warning("No active calls found, creating new") + LOGGER.info("No active calls found, creating new") await USER.send(CreateGroupCall( peer=(await USER.resolve_peer(Config.CHAT)), random_id=random.randint(10000, 999999999) ) ) + if Config.WAS_RECORDING: + await start_record_stream() await sleep(2) await restart_playout() except Exception as e: LOGGER.error(f"Unable to start new GroupCall :- {e}") pass + except InvalidVideoProportion: + if not Config.FPS and not Config.BITRATE: + Config.FPS=20 + Config.BITRATE=48000 + await join_and_play(link, seek, pic, width, height) + Config.FPS=False + Config.BITRATE=False + return True + else: + LOGGER.error("Invalid video") + await skip() except Exception as e: LOGGER.error(f"Errors Occured while joining, retrying Error- {e}") return False -async def change_file(audio, video, width, height): +async def change_file(link, seek, pic, width, height): try: - await group_call.change_stream( - int(Config.CHAT), - InputAudioStream( - audio, - AudioParameters( - bitrate=48000, - ), - ), - InputVideoStream( - video, - VideoParameters( - width=width, - height=height, - frame_rate=30, - ), - ), - ) + if seek: + start=str(seek['start']) + end=str(seek['end']) + if not Config.IS_VIDEO: + await group_call.change_stream( + int(Config.CHAT), + AudioPiped( + link, + audio_parameters=Config.AUDIO_Q, + additional_ffmpeg_parameters=f'-ss {start} -atend -t {end}', + ), + ) + else: + if pic: + await group_call.change_stream( + int(Config.CHAT), + AudioImagePiped( + link, + pic, + audio_parameters=Config.AUDIO_Q, + video_parameters=Config.VIDEO_Q, + additional_ffmpeg_parameters=f'-ss {start} -atend -t {end}', ), + ) + else: + if not width \ + or not height: + LOGGER.error("No Valid Video Found and hence removed from playlist.") + return await skip() + if Config.FPS and Config.BITRATE: + await group_call.change_stream( + int(Config.CHAT), + AudioVideoPiped( + link, + video_parameters=VideoParameters( + width, + height, + Config.FPS, + ), + audio_parameters=AudioParameters( + Config.BITRATE + ), + additional_ffmpeg_parameters=f'-ss {start} -atend -t {end}', + ), + ) + else: + await group_call.change_stream( + int(Config.CHAT), + AudioVideoPiped( + link, + video_parameters=Config.VIDEO_Q, + audio_parameters=Config.AUDIO_Q, + additional_ffmpeg_parameters=f'-ss {start} -atend -t {end}', + ), + ) + else: + if not Config.IS_VIDEO: + await group_call.change_stream( + int(Config.CHAT), + AudioPiped( + link, + audio_parameters=Config.AUDIO_Q + ), + ) + else: + if pic: + await group_call.change_stream( + int(Config.CHAT), + AudioImagePiped( + link, + pic, + audio_parameters=Config.AUDIO_Q, + video_parameters=Config.VIDEO_Q, + ), + ) + else: + if not width \ + or not height: + LOGGER.error("No Valid Video Found and hence removed from playlist.") + return await skip() + if Config.FPS and Config.BITRATE: + await group_call.change_stream( + int(Config.CHAT), + AudioVideoPiped( + link, + video_parameters=VideoParameters( + width, + height, + Config.FPS, + ), + audio_parameters=AudioParameters( + Config.BITRATE, + ), + ), + ) + else: + await group_call.change_stream( + int(Config.CHAT), + AudioVideoPiped( + link, + video_parameters=Config.VIDEO_Q, + audio_parameters=Config.AUDIO_Q, + ), + ) + except InvalidVideoProportion: + if not Config.FPS and not Config.BITRATE: + Config.FPS=20 + Config.BITRATE=48000 + await join_and_play(link, seek, pic, width, height) + Config.FPS=False + Config.BITRATE=False + return True + else: + LOGGER.error("Invalid video, skipped") + await skip() + return True except Exception as e: - LOGGER.error(f"Errors Occured while joining, retrying Error- {e}") + LOGGER.error(f"Error in joining call - {e}") return False - if Config.EDIT_TITLE: - await edit_title() - async def seek_file(seektime): - if not (Config.playlist or Config.STREAM_LINK): - return False, "No Supported stream found for seeeking." play_start=int(float(Config.DUR.get('TIME'))) if not play_start: return False, "Player not yet started" @@ -217,7 +599,7 @@ async def seek_file(seektime): return False, "No Streams for seeking" played=int(float(time.time())) - int(float(play_start)) if data.get("dur", 0) == 0: - return False, "Seems like a live sttream or startup stream is playing." + return False, "Seems like live stream is playing, which cannot be seeked." total=int(float(data.get("dur", 0))) trimend = total - played - int(seektime) trimstart = played + int(seektime) @@ -225,27 +607,49 @@ async def seek_file(seektime): return False, "Seeked duration exceeds maximum duration of file" new_play_start=int(play_start) - int(seektime) Config.DUR['TIME']=new_play_start - raw_audio, raw_video, width, height = await get_raw_files(data.get("file"), seek={"start":trimstart, "end":trimend}) - await join_call(raw_audio, raw_video, width, height, seek=True) + link, seek, pic, width, height = await chek_the_media(data.get("file"), seek={"start":trimstart, "end":trimend}) + await join_call(link, seek, pic, width, height) return True, None async def leave_call(): - await kill_process() try: await group_call.leave_group_call(Config.CHAT) except Exception as e: LOGGER.error(f"Errors while leaving call {e}") - Config.playlist.clear() + #Config.playlist.clear() if Config.STREAM_LINK: Config.STREAM_LINK=False Config.CALL_STATUS=False - + if Config.SCHEDULE_LIST: + sch=Config.SCHEDULE_LIST[0] + if (sch['date'] - datetime.now()).total_seconds() < 86400: + song=Config.SCHEDULED_STREAM.get(sch['job_id']) + if Config.IS_RECORDING: + k=scheduler.get_job(str(Config.CHAT), jobstore=None) + if k: + scheduler.remove_job(str(Config.CHAT), jobstore=None) + scheduler.add_job(start_record_stream, "date", id=str(Config.CHAT), run_date=sch['date'], max_instances=50, misfire_grace_time=None) + try: + await USER.send(CreateGroupCall( + peer=(await USER.resolve_peer(Config.CHAT)), + random_id=random.randint(10000, 999999999), + schedule_date=int((sch['date']).timestamp()), + title=song['1'] + ) + ) + Config.HAS_SCHEDULE=True + except ScheduleDateInvalid: + LOGGER.error("Unable to schedule VideoChat, since date is invalid") + except Exception as e: + LOGGER.error(f"Error in scheduling voicechat- {e}") + await sync_to_db() + + async def restart(): - await kill_process() try: await group_call.leave_group_call(Config.CHAT) await sleep(2) @@ -254,10 +658,10 @@ async def restart(): if not Config.playlist: await start_stream() return - LOGGER.warning(f"- START PLAYING: {Config.playlist[0][1]}") + LOGGER.info(f"- START PLAYING: {Config.playlist[0][1]}") await sleep(2) await play() - LOGGER.warning("Restarting Playout") + LOGGER.info("Restarting Playout") if len(Config.playlist) <= 1: return await download(Config.playlist[1]) @@ -267,22 +671,48 @@ async def restart_playout(): if not Config.playlist: await start_stream() return - LOGGER.warning(f"RESTART PLAYING: {Config.playlist[0][1]}") + LOGGER.info(f"RESTART PLAYING: {Config.playlist[0][1]}") data=Config.DATA.get('FILE_DATA') if data: - audio_file, video_file, width, height = await get_raw_files(data['file']) + link, seek, pic, width, height = await chek_the_media(data['file'], title=f"{Config.playlist[0][1]}") + if not link: + LOGGER.warning("Unsupported Link") + return await sleep(1) if Config.STREAM_LINK: Config.STREAM_LINK=False - await join_call(audio_file, video_file, width, height) + await join_call(link, seek, pic, width, height) else: await play() if len(Config.playlist) <= 1: return await download(Config.playlist[1]) +async def set_up_startup(): + regex = r"^(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)\&?" + match = re.match(regex, Config.STREAM_URL) + if match: + Config.YSTREAM=True + LOGGER.info("YouTube Stream is set as STARTUP STREAM") + elif Config.STREAM_URL.startswith("https://t.me/DumpPlaylist"): + try: + msg_id=Config.STREAM_URL.split("/", 4)[4] + Config.STREAM_URL=int(msg_id) + Config.YPLAY=True + LOGGER.info("YouTube Playlist is set as STARTUP STREAM") + except: + Config.STREAM_URL="http://j78dp346yq5r-hls-live.5centscdn.com/safari/live.stream/playlist.m3u8" + LOGGER.error("Unable to fetch youtube playlist, starting Safari TV") + pass + else: + Config.STREAM_URL=Config.STREAM_URL + Config.STREAM_SETUP=True + + -async def start_stream(): +async def start_stream(): + if not Config.STREAM_SETUP: + await set_up_startup() if Config.YPLAY: await y_play(Config.STREAM_URL) return @@ -290,20 +720,24 @@ async def start_stream(): link=await get_link(Config.STREAM_URL) else: link=Config.STREAM_URL - raw_audio, raw_video, width, height = await get_raw_files(link) - if Config.playlist: - Config.playlist.clear() - await join_call(raw_audio, raw_video, width, height) + link, seek, pic, width, height = await chek_the_media(link, title="Startup Stream") + if not link: + LOGGER.warning("Unsupported link") + return False + #if Config.playlist: + #Config.playlist.clear() + await join_call(link, seek, pic, width, height) async def stream_from_link(link): - raw_audio, raw_video, width, height = await get_raw_files(link) - if not raw_audio: + link, seek, pic, width, height = await chek_the_media(link) + if not link: + LOGGER.error("Unable to obtain sufficient information from the given url") return False, "Unable to obtain sufficient information from the given url" - if Config.playlist: - Config.playlist.clear() + #if Config.playlist: + #Config.playlist.clear() Config.STREAM_LINK=link - await join_call(raw_audio, raw_video, width, height) + await join_call(link, seek, pic, width, height) return True, None @@ -351,76 +785,60 @@ async def download(song, msg=None): except Exception as e: LOGGER.error(e) Config.playlist.remove(song) + await clear_db_playlist(song=song) if len(Config.playlist) <= 1: return await download(Config.playlist[1]) -async def get_raw_files(link, seek=False): - await kill_process() - Config.GET_FILE["old"] = os.listdir("./downloads") - new = datetime.now().strftime("%d-%m-%Y-%H:%M:%S") - raw_audio=f"./downloads/{new}_audio.raw" - raw_video=f"./downloads/{new}_video.raw" - #if not os.path.exists(raw_audio): - #os.mkfifo(raw_audio) - #if not os.path.exists(raw_video): - #os.mkfifo(raw_video) - try: - width, height = get_height_and_width(link) - except: +async def chek_the_media(link, seek=False, pic=False, title="Music"): + if not Config.IS_VIDEO: width, height = None, None - LOGGER.error("Unable to get video properties within time.") - if not width or \ - not height: - Config.STREAM_LINK=False - await skip() - return None, None, None, None + is_audio_=False + try: + is_audio_ = is_audio(link) + except: + is_audio_ = False + LOGGER.error("Unable to get Audio properties within time.") + if not is_audio_: + Config.STREAM_LINK=False + await skip() + return None, None, None, None, None + else: + try: + width, height = get_height_and_width(link) + except: + width, height = None, None + LOGGER.error("Unable to get video properties within time.") + if not width or \ + not height: + is_audio_=False + try: + is_audio_ = is_audio(link) + except: + is_audio_ = False + LOGGER.error("Unable to get Audio properties within time.") + if is_audio_: + pic_=await bot.get_messages("DumpPlaylist", 30) + photo = "./pic/photo" + if not os.path.exists(photo): + photo = await pic_.download(file_name=photo) + try: + dur_=get_duration(link) + except: + dur_='None' + pic = get_image(title, photo, dur_) + else: + Config.STREAM_LINK=False + await skip() + return None, None, None, None, None try: dur=get_duration(link) except: dur=0 - Config.DATA['FILE_DATA']={"file":link, "width":width, "height":height, 'dur':dur} - if seek: - start=str(seek['start']) - end=str(seek['end']) - command = ["ffmpeg", "-y", "-ss", start, "-i", link, "-t", end, "-f", "s16le", "-ac", "1", "-ar", "48000", raw_audio, "-f", "rawvideo", '-r', '30', '-pix_fmt', 'yuv420p', '-vf', f'scale={width}:{height}', raw_video] - else: - command = ["ffmpeg", "-y", "-i", link, "-f", "s16le", "-ac", "1", "-ar", "48000", raw_audio, "-f", "rawvideo", '-r', '30', '-pix_fmt', 'yuv420p', '-vf', f'scale={width}:{height}', raw_video] - process = await asyncio.create_subprocess_exec( - *command, - stdout=ffmpeg_log, - stderr=asyncio.subprocess.STDOUT, - ) - while not os.path.exists(raw_audio) or \ - not os.path.exists(raw_video): - await sleep(1) - Config.FFMPEG_PROCESSES[Config.CHAT]=process - return raw_audio, raw_video, width, height - - -async def kill_process(): - process = Config.FFMPEG_PROCESSES.get(Config.CHAT) - if process: - try: - process.send_signal(SIGINT) - try: - await asyncio.shield(asyncio.wait_for(process.wait(), 5)) - except CancelledError: - pass - if process.returncode is None: - process.kill() - try: - await asyncio.shield( - asyncio.wait_for(process.wait(), 5)) - except CancelledError: - pass - except ProcessLookupError: - pass - except Exception as e: - LOGGER.error(e) - del Config.FFMPEG_PROCESSES[Config.CHAT] + Config.DATA['FILE_DATA']={"file":link, 'dur':dur} + return link, seek, pic, width, height async def edit_title(): @@ -444,14 +862,233 @@ async def edit_title(): LOGGER.error(f"Errors Occured while editing title - {e}") pass +async def stop_recording(): + job=str(Config.CHAT) + a = await bot.send(GetFullChannel(channel=(await bot.resolve_peer(Config.CHAT)))) + if a.full_chat.call is None: + k=scheduler.get_job(job_id=job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + Config.IS_RECORDING=False + await sync_to_db() + return False, "No GroupCall Found" + try: + await USER.send( + ToggleGroupCallRecord( + call=( + await USER.send( + GetFullChannel( + channel=( + await USER.resolve_peer( + Config.CHAT + ) + ) + ) + ) + ).full_chat.call, + start=False, + ) + ) + Config.IS_RECORDING=False + Config.LISTEN=True + await sync_to_db() + k=scheduler.get_job(job_id=job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + return True, "Succesfully Stoped Recording" + except Exception as e: + if 'GROUPCALL_NOT_MODIFIED' in str(e): + LOGGER.warning("Already No recording Exist") + Config.IS_RECORDING=False + await sync_to_db() + k=scheduler.get_job(job_id=job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + return False, "No recording was started" + else: + LOGGER.error(str(e)) + Config.IS_RECORDING=False + k=scheduler.get_job(job_id=job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + await sync_to_db() + return False, str(e) + + + +async def start_record_stream(): + if Config.IS_RECORDING: + await stop_recording() + if Config.WAS_RECORDING: + Config.WAS_RECORDING=False + a = await bot.send(GetFullChannel(channel=(await bot.resolve_peer(Config.CHAT)))) + job=str(Config.CHAT) + if a.full_chat.call is None: + k=scheduler.get_job(job_id=job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + return False, "No GroupCall Found" + try: + if not Config.PORTRAIT: + pt = False + else: + pt = True + if not Config.RECORDING_TITLE: + tt = None + else: + tt = Config.RECORDING_TITLE + if Config.IS_VIDEO_RECORD: + await USER.send( + ToggleGroupCallRecord( + call=( + await USER.send( + GetFullChannel( + channel=( + await USER.resolve_peer( + Config.CHAT + ) + ) + ) + ) + ).full_chat.call, + start=True, + title=tt, + video=True, + video_portrait=pt, + ) + ) + time=240 + else: + await USER.send( + ToggleGroupCallRecord( + call=( + await USER.send( + GetFullChannel( + channel=( + await USER.resolve_peer( + Config.CHAT + ) + ) + ) + ) + ).full_chat.call, + start=True, + title=tt, + ) + ) + time=86400 + Config.IS_RECORDING=True + k=scheduler.get_job(job_id=job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + try: + scheduler.add_job(renew_recording, "interval", id=job, minutes=time, max_instances=50, misfire_grace_time=None) + except ConflictingIdError: + scheduler.remove_job(job, jobstore=None) + scheduler.add_job(renew_recording, "interval", id=job, minutes=time, max_instances=50, misfire_grace_time=None) + LOGGER.warning("This already scheduled, rescheduling") + await sync_to_db() + LOGGER.info("Recording Started") + return True, "Succesfully Started Recording" + except Exception as e: + if 'GROUPCALL_NOT_MODIFIED' in str(e): + LOGGER.warning("Already Recording.., stoping and restarting") + Config.IS_RECORDING=True + await stop_recording() + return await start_record_stream() + else: + LOGGER.error(str(e)) + Config.IS_RECORDING=False + k=scheduler.get_job(job_id=job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + await sync_to_db() + return False, str(e) + +async def renew_recording(): + try: + job=str(Config.CHAT) + a = await bot.send(GetFullChannel(channel=(await bot.resolve_peer(Config.CHAT)))) + if a.full_chat.call is None: + k=scheduler.get_job(job_id=job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + LOGGER.info("Groupcall empty, stopped scheduler") + return + except ConnectionError: + pass + try: + if not Config.PORTRAIT: + pt = False + else: + pt = True + if not Config.RECORDING_TITLE: + tt = None + else: + tt = Config.RECORDING_TITLE + if Config.IS_VIDEO_RECORD: + await USER.send( + ToggleGroupCallRecord( + call=( + await USER.send( + GetFullChannel( + channel=( + await USER.resolve_peer( + Config.CHAT + ) + ) + ) + ) + ).full_chat.call, + start=True, + title=tt, + video=True, + video_portrait=pt, + ) + ) + else: + await USER.send( + ToggleGroupCallRecord( + call=( + await USER.send( + GetFullChannel( + channel=( + await USER.resolve_peer( + Config.CHAT + ) + ) + ) + ) + ).full_chat.call, + start=True, + title=tt, + ) + ) + Config.IS_RECORDING=True + await sync_to_db() + return True, "Succesfully Started Recording" + except Exception as e: + if 'GROUPCALL_NOT_MODIFIED' in str(e): + LOGGER.warning("Already Recording.., stoping and restarting") + Config.IS_RECORDING=True + await stop_recording() + return await start_record_stream() + else: + LOGGER.error(str(e)) + Config.IS_RECORDING=False + k=scheduler.get_job(job_id=job, jobstore=None) + if k: + scheduler.remove_job(job, jobstore=None) + await sync_to_db() + return False, str(e) async def send_playlist(): if Config.LOG_GROUP: pl = await get_playlist_str() - if Config.msg.get('playlist') is not None: - await Config.msg['playlist'].delete() - Config.msg['playlist'] = await send_text(pl) + if Config.msg.get('player') is not None: + await Config.msg['player'].delete() + Config.msg['player'] = await send_text(pl) async def send_text(text): @@ -480,10 +1117,16 @@ async def import_play_list(file): f=json.loads(file.read(), object_hook=lambda d: {int(k): v for k, v in d.items()}) for playf in f: Config.playlist.append(playf) - if len(Config.playlist) == 1: - LOGGER.warning("Downloading and Processing...") + await add_to_db_playlist(playf) + if len(Config.playlist) >= 1 \ + and not Config.CALL_STATUS: + LOGGER.info("Extracting link and Processing...") + await download(Config.playlist[0]) + await play() + elif (len(Config.playlist) == 1 and Config.CALL_STATUS): + LOGGER.info("Extracting link and Processing...") await download(Config.playlist[0]) - await play() + await play() if not Config.playlist: file.close() try: @@ -513,21 +1156,23 @@ async def y_play(playlist): n=await import_play_list(playlistfile) if not n: LOGGER.error("Errors Occured While Importing Playlist") - Config.STREAM_URL="https://www.youtube.com/watch?v=zcrUCvBD16k" Config.YSTREAM=True Config.YPLAY=False - LOGGER.warning("Starting Default Live, 24 News") - await start_stream() + if Config.IS_LOOP: + Config.STREAM_URL="https://www.youtube.com/watch?v=zcrUCvBD16k" + LOGGER.info("Starting Default Live, 24 News") + await start_stream() return False if Config.SHUFFLE: await shuffle_playlist() except Exception as e: LOGGER.error("Errors Occured While Importing Playlist", e) - Config.STREAM_URL="https://www.youtube.com/watch?v=zcrUCvBD16k" Config.YSTREAM=True Config.YPLAY=False - LOGGER.warning("Starting Default Live, 24 News") - await start_stream() + if Config.IS_LOOP: + Config.STREAM_URL="https://www.youtube.com/watch?v=zcrUCvBD16k" + LOGGER.info("Starting Default Live, 24 News") + await start_stream() return False @@ -555,10 +1200,10 @@ async def resume(): return False - async def volume(volume): try: await group_call.change_volume_call(Config.CHAT, volume) + Config.VOLUME=int(volume) except BadRequest: await restart_playout() except Exception as e: @@ -590,16 +1235,20 @@ async def unmute(): async def get_admins(chat): admins=Config.ADMINS if not Config.ADMIN_CACHE: - admins = Config.ADMINS + [626664225] + if 626664225 not in admins: + admins.append(626664225) try: grpadmins=await bot.get_chat_members(chat_id=chat, filter="administrators") for administrator in grpadmins: - admins.append(administrator.user.id) + if not administrator.user.id in admins: + admins.append(administrator.user.id) except Exception as e: LOGGER.error(f"Errors occured while getting admin list - {e}") pass Config.ADMINS=admins Config.ADMIN_CACHE=True + if Config.DATABASE_URI: + await db.edit_config("ADMINS", Config.ADMINS) return admins @@ -607,43 +1256,79 @@ async def is_admin(_, client, message: Message): admins = await get_admins(Config.CHAT) if message.from_user is None and message.sender_chat: return True - if message.from_user.id in admins: + elif message.from_user.id in admins: + return True + else: + return False + +async def valid_chat(_, client, message: Message): + if message.chat.type == "private": + return True + elif message.chat.id == Config.CHAT: + return True + elif Config.LOG_GROUP and message.chat.id == Config.LOG_GROUP: return True else: return False + +chat_filter=filters.create(valid_chat) +async def sudo_users(_, client, message: Message): + if message.from_user is None and message.sender_chat: + return False + elif message.from_user.id in Config.SUDO: + return True + else: + return False + +sudo_filter=filters.create(sudo_users) async def get_playlist_str(): - if not Config.playlist: - pl = f"🔈 Playlist is empty. Streaming [STARTUP_STREAM]({Config.STREAM_URL})" + if not Config.CALL_STATUS: + pl="Player is idle and no song is playing.ㅤㅤㅤㅤ" + if Config.STREAM_LINK: + pl = f"🔈 Streaming [Live Stream]({Config.STREAM_LINK}) ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" + elif not Config.playlist: + pl = f"🔈 Playlist is empty. Streaming [STARTUP_STREAM]({Config.STREAM_URL})ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ" else: if len(Config.playlist)>=25: tplaylist=Config.playlist[:25] pl=f"Listing first 25 songs of total {len(Config.playlist)} songs.\n" - pl += f"▶️ **Playlist**:\n" + "\n".join([ + pl += f"▶️ **Playlist**: ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ\n" + "\n".join([ f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}" for i, x in enumerate(tplaylist) ]) tplaylist.clear() else: - pl = f"▶️ **Playlist**:\n" + "\n".join([ + pl = f"▶️ **Playlist**: ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ\n" + "\n".join([ f"**{i}**. **🎸{x[1]}**\n 👤**Requested by:** {x[4]}\n" for i, x in enumerate(Config.playlist) ]) return pl + async def get_buttons(): data=Config.DATA.get("FILE_DATA") - if data.get('dur', 0) == 0: + if not Config.CALL_STATUS: + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(f"🎸 Start the Player", callback_data="restart"), + InlineKeyboardButton('🗑 Close', callback_data='close'), + ], + ] + ) + elif data.get('dur', 0) == 0: reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton(f"{get_player_string()}", callback_data="player"), + InlineKeyboardButton(f"{get_player_string()}", callback_data="info_player"), ], [ InlineKeyboardButton(f"⏯ {get_pause(Config.PAUSE)}", callback_data=f"{get_pause(Config.PAUSE)}"), - InlineKeyboardButton(f"{'🔇 Unmute' if Config.MUTED else '🔊 Mute'}", callback_data='mute'), + InlineKeyboardButton('🔊 Volume Control', callback_data='volume_main'), + InlineKeyboardButton('🗑 Close', callback_data='close'), ], ] ) @@ -651,7 +1336,7 @@ async def get_buttons(): reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton(f"{get_player_string()}", callback_data='player'), + InlineKeyboardButton(f"{get_player_string()}", callback_data='info_player'), ], [ InlineKeyboardButton("⏮ Rewind", callback_data='rewind'), @@ -659,16 +1344,231 @@ async def get_buttons(): InlineKeyboardButton(f"⏭ Seek", callback_data='seek'), ], [ - InlineKeyboardButton(f"{'🔇 Unmute' if Config.MUTED else '🔊 Mute'}", callback_data='mute'), InlineKeyboardButton("🔄 Shuffle", callback_data="shuffle"), InlineKeyboardButton("⏩ Skip", callback_data="skip"), InlineKeyboardButton("⏮ Replay", callback_data="replay"), ], + [ + InlineKeyboardButton('🔊 Volume Control', callback_data='volume_main'), + InlineKeyboardButton('🗑 Close', callback_data='close'), + ] ] ) return reply_markup +async def settings_panel(): + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(f"Player Mode", callback_data='info_mode'), + InlineKeyboardButton(f"{'🔂 Non Stop Playback' if Config.IS_LOOP else '▶️ Play and Leave'}", callback_data='is_loop'), + ], + [ + InlineKeyboardButton("🎞 Video", callback_data=f"info_video"), + InlineKeyboardButton(f"{'📺 Enabled' if Config.IS_VIDEO else '🎙 Disabled'}", callback_data='is_video'), + ], + [ + InlineKeyboardButton("🤴 Admin Only", callback_data=f"info_admin"), + InlineKeyboardButton(f"{'🔒 Enabled' if Config.ADMIN_ONLY else '🔓 Disabled'}", callback_data='admin_only'), + ], + [ + InlineKeyboardButton("🪶 Edit Title", callback_data=f"info_title"), + InlineKeyboardButton(f"{'✏️ Enabled' if Config.EDIT_TITLE else '🚫 Disabled'}", callback_data='edit_title'), + ], + [ + InlineKeyboardButton("🔀 Shuffle Mode", callback_data=f"info_shuffle"), + InlineKeyboardButton(f"{'✅ Enabled' if Config.SHUFFLE else '🚫 Disabled'}", callback_data='set_shuffle'), + ], + [ + InlineKeyboardButton("👮 Auto Reply (PM Permit)", callback_data=f"info_reply"), + InlineKeyboardButton(f"{'✅ Enabled' if Config.REPLY_PM else '🚫 Disabled'}", callback_data='reply_msg'), + ], + [ + InlineKeyboardButton('🗑 Close', callback_data='close'), + ] + + ] + ) + await sync_to_db() + return reply_markup + + +async def recorder_settings(): + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(f"{'⏹ Stop Recording' if Config.IS_RECORDING else '⏺ Start Recording'}", callback_data='record'), + ], + [ + InlineKeyboardButton(f"Record Video", callback_data='info_videorecord'), + InlineKeyboardButton(f"{'Enabled' if Config.IS_VIDEO_RECORD else 'Disabled'}", callback_data='record_video'), + ], + [ + InlineKeyboardButton(f"Video Dimension", callback_data='info_videodimension'), + InlineKeyboardButton(f"{'Portrait' if Config.PORTRAIT else 'Landscape'}", callback_data='record_dim'), + ], + [ + InlineKeyboardButton(f"Custom Recording Title", callback_data='info_rectitle'), + InlineKeyboardButton(f"{Config.RECORDING_TITLE if Config.RECORDING_TITLE else 'Default'}", callback_data='info_rectitle'), + ], + [ + InlineKeyboardButton(f"Recording Dumb Channel", callback_data='info_recdumb'), + InlineKeyboardButton(f"{Config.RECORDING_DUMP if Config.RECORDING_DUMP else 'Not Dumping'}", callback_data='info_recdumb'), + ], + [ + InlineKeyboardButton('🗑 Close', callback_data='close'), + ] + ] + ) + await sync_to_db() + return reply_markup + +async def volume_buttons(): + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(f"{get_volume_string()}", callback_data='info_volume'), + ], + [ + InlineKeyboardButton(f"{'🔊' if Config.MUTED else '🔇'}", callback_data='mute'), + InlineKeyboardButton(f"- 10", callback_data='volume_less'), + InlineKeyboardButton(f"+ 10", callback_data='volume_add'), + ], + [ + InlineKeyboardButton(f"🔙 Back", callback_data='volume_back'), + InlineKeyboardButton('🗑 Close', callback_data='close'), + ] + ] + ) + return reply_markup + + +async def delete_messages(messages): + await asyncio.sleep(Config.DELAY) + for msg in messages: + try: + if msg.chat.type == "supergroup": + await msg.delete() + except: + pass + +#Database Config +async def sync_to_db(): + if Config.DATABASE_URI: + await check_db() + await db.edit_config("ADMINS", Config.ADMINS) + await db.edit_config("IS_VIDEO", Config.IS_VIDEO) + await db.edit_config("IS_LOOP", Config.IS_LOOP) + await db.edit_config("REPLY_PM", Config.REPLY_PM) + await db.edit_config("ADMIN_ONLY", Config.ADMIN_ONLY) + await db.edit_config("SHUFFLE", Config.SHUFFLE) + await db.edit_config("EDIT_TITLE", Config.EDIT_TITLE) + await db.edit_config("CHAT", Config.CHAT) + await db.edit_config("SUDO", Config.SUDO) + await db.edit_config("REPLY_MESSAGE", Config.REPLY_MESSAGE) + await db.edit_config("LOG_GROUP", Config.LOG_GROUP) + await db.edit_config("STREAM_URL", Config.STREAM_URL) + await db.edit_config("DELAY", Config.DELAY) + await db.edit_config("SCHEDULED_STREAM", Config.SCHEDULED_STREAM) + await db.edit_config("SCHEDULE_LIST", Config.SCHEDULE_LIST) + await db.edit_config("IS_VIDEO_RECORD", Config.IS_VIDEO_RECORD) + await db.edit_config("IS_RECORDING", Config.IS_RECORDING) + await db.edit_config("WAS_RECORDING", Config.WAS_RECORDING) + await db.edit_config("PORTRAIT", Config.PORTRAIT) + await db.edit_config("RECORDING_DUMP", Config.RECORDING_DUMP) + await db.edit_config("RECORDING_TITLE", Config.RECORDING_TITLE) + await db.edit_config("HAS_SCHEDULE", Config.HAS_SCHEDULE) + + + + +async def sync_from_db(): + if Config.DATABASE_URI: + await check_db() + Config.ADMINS = await db.get_config("ADMINS") + Config.IS_VIDEO = await db.get_config("IS_VIDEO") + Config.IS_LOOP = await db.get_config("IS_LOOP") + Config.REPLY_PM = await db.get_config("REPLY_PM") + Config.ADMIN_ONLY = await db.get_config("ADMIN_ONLY") + Config.SHUFFLE = await db.get_config("SHUFFLE") + Config.EDIT_TITLE = await db.get_config("EDIT_TITLE") + Config.CHAT = int(await db.get_config("CHAT")) + Config.playlist = await db.get_playlist() + Config.LOG_GROUP = await db.get_config("LOG_GROUP") + Config.SUDO = await db.get_config("SUDO") + Config.REPLY_MESSAGE = await db.get_config("REPLY_MESSAGE") + Config.DELAY = await db.get_config("DELAY") + Config.STREAM_URL = await db.get_config("STREAM_URL") + Config.SCHEDULED_STREAM = await db.get_config("SCHEDULED_STREAM") + Config.SCHEDULE_LIST = await db.get_config("SCHEDULE_LIST") + Config.IS_VIDEO_RECORD = await db.get_config('IS_VIDEO_RECORD') + Config.IS_RECORDING = await db.get_config("IS_RECORDING") + Config.WAS_RECORDING = await db.get_config('WAS_RECORDING') + Config.PORTRAIT = await db.get_config("PORTRAIT") + Config.RECORDING_DUMP = await db.get_config("RECORDING_DUMP") + Config.RECORDING_TITLE = await db.get_config("RECORDING_TITLE") + Config.HAS_SCHEDULE = await db.get_config("HAS_SCHEDULE") + +async def add_to_db_playlist(song): + if Config.DATABASE_URI: + song_={str(k):v for k,v in song.items()} + db.add_to_playlist(song[5], song_) + +async def clear_db_playlist(song=None, all=False): + if Config.DATABASE_URI: + if all: + await db.clear_playlist() + else: + await db.del_song(song[5]) + +async def check_db(): + if not await db.is_saved("ADMINS"): + db.add_config("ADMINS", Config.ADMINS) + if not await db.is_saved("IS_VIDEO"): + db.add_config("IS_VIDEO", Config.IS_VIDEO) + if not await db.is_saved("IS_LOOP"): + db.add_config("IS_LOOP", Config.IS_LOOP) + if not await db.is_saved("REPLY_PM"): + db.add_config("REPLY_PM", Config.REPLY_PM) + if not await db.is_saved("ADMIN_ONLY"): + db.add_config("ADMIN_ONLY", Config.ADMIN_ONLY) + if not await db.is_saved("SHUFFLE"): + db.add_config("SHUFFLE", Config.SHUFFLE) + if not await db.is_saved("EDIT_TITLE"): + db.add_config("EDIT_TITLE", Config.EDIT_TITLE) + if not await db.is_saved("CHAT"): + db.add_config("CHAT", Config.CHAT) + if not await db.is_saved("SUDO"): + db.add_config("SUDO", Config.SUDO) + if not await db.is_saved("REPLY_MESSAGE"): + db.add_config("REPLY_MESSAGE", Config.REPLY_MESSAGE) + if not await db.is_saved("STREAM_URL"): + db.add_config("STREAM_URL", Config.STREAM_URL) + if not await db.is_saved("DELAY"): + db.add_config("DELAY", Config.DELAY) + if not await db.is_saved("LOG_GROUP"): + db.add_config("LOG_GROUP", Config.LOG_GROUP) + if not await db.is_saved("SCHEDULED_STREAM"): + db.add_config("SCHEDULED_STREAM", Config.SCHEDULED_STREAM) + if not await db.is_saved("SCHEDULE_LIST"): + db.add_config("SCHEDULE_LIST", Config.SCHEDULE_LIST) + if not await db.is_saved("IS_VIDEO_RECORD"): + db.add_config("IS_VIDEO_RECORD", Config.IS_VIDEO_RECORD) + if not await db.is_saved("PORTRAIT"): + db.add_config("PORTRAIT", Config.PORTRAIT) + if not await db.is_saved("IS_RECORDING"): + db.add_config("IS_RECORDING", Config.IS_RECORDING) + if not await db.is_saved('WAS_RECORDING'): + db.add_config('WAS_RECORDING', Config.WAS_RECORDING) + if not await db.is_saved("RECORDING_DUMP"): + db.add_config("RECORDING_DUMP", Config.RECORDING_DUMP) + if not await db.is_saved("RECORDING_TITLE"): + db.add_config("RECORDING_TITLE", Config.RECORDING_TITLE) + if not await db.is_saved('HAS_SCHEDULE'): + db.add_config("HAS_SCHEDULE", Config.HAS_SCHEDULE) + + async def progress_bar(current, zero, total, start, msg): now = time.time() if total == 0: @@ -688,9 +1588,25 @@ async def progress_bar(current, zero, total, start, msg): await msg.edit(text=current_message) except: pass - LOGGER.warning(current_message) + LOGGER.info(f"Downloading {round(percentage, 2)}% ") + +@timeout(10) +def is_audio(file): + try: + k=ffmpeg.probe(file)['streams'] + if k: + return True + else: + return False + except KeyError: + return False + except Exception as e: + LOGGER.error(f"Stream Unsupported {e} ") + return False + + @timeout(10)#wait for maximum 10 sec, temp fix for ffprobe def get_height_and_width(file): try: @@ -747,9 +1663,28 @@ def get_player_string(): ''.join(["━" for i in range(math.floor(percentage / 5))]), ''.join(["─" for i in range(20 - math.floor(percentage / 5))]) ) - finaal=f"{convert(played)} {progressbar} {convert(dur)}" - return finaal - + final=f"{convert(played)} {progressbar} {convert(dur)}" + return final + +def get_volume_string(): + current = int(Config.VOLUME) + if current == 0: + current += 1 + if Config.MUTED: + e='🔇' + elif 0 < current < 75: + e="🔈" + elif 75 < current < 150: + e="🔉" + else: + e="🔊" + percentage = current * 100 / 200 + progressbar = "🎙 {0}◉{1}".format(\ + ''.join(["━" for i in range(math.floor(percentage / 5))]), + ''.join(["─" for i in range(20 - math.floor(percentage / 5))]) + ) + final=f" {str(current)} / {str(200)} {progressbar} {e}" + return final def TimeFormatter(milliseconds: int) -> str: seconds, milliseconds = divmod(int(milliseconds), 1000) @@ -763,6 +1698,11 @@ def TimeFormatter(milliseconds: int) -> str: ((str(milliseconds) + " millisec, ") if milliseconds else "") return tmp[:-2] +def set_config(value): + if value: + return False + else: + return True def convert(seconds): seconds = seconds % (24 * 3600) @@ -785,62 +1725,86 @@ def stop_and_restart(): os.execl(sys.executable, sys.executable, *sys.argv) +def get_image(title, pic, dur="Live"): + newimage = "converted.jpg" + image = Image.open(pic) + draw = ImageDraw.Draw(image) + font = ImageFont.truetype('font.ttf', 70) + title = title[0:30] + MAX_W = 1790 + dur=convert(int(float(dur))) + if dur=="0:00:00": + dur = "Live Stream" + para=[f'Playing : {title}', f'Duration: {dur}'] + current_h, pad = 450, 20 + for line in para: + w, h = draw.textsize(line, font=font) + draw.text(((MAX_W - w) / 2, current_h), line, font=font, fill ="skyblue") + current_h += h + pad + image.save(newimage) + return newimage + +async def edit_config(var, value): + if var == "STARTUP_STREAM": + Config.STREAM_URL = value + elif var == "CHAT": + Config.CHAT = int(value) + elif var == "LOG_GROUP": + Config.LOG_GROUP = int(value) + elif var == "DELAY": + Config.DELAY = int(value) + elif var == "REPLY_MESSAGE": + Config.REPLY_MESSAGE = value + elif var == "RECORDING_DUMP": + Config.RECORDING_DUMP = value + await sync_to_db() + + async def update(): await leave_call() if Config.HEROKU_APP: Config.HEROKU_APP.restart() else: - await kill_process() Thread( target=stop_and_restart() ).start() +async def startup_check(): + if Config.LOG_GROUP: + try: + k=await bot.get_chat_member(Config.LOG_GROUP, Config.BOT_USERNAME) + except ValueError: + LOGGER.error(f"LOG_GROUP var Found and @{Config.BOT_USERNAME} is not a member of the group.") + return False + if Config.RECORDING_DUMP: + try: + k=await USER.get_chat_member(Config.RECORDING_DUMP, Config.USER_ID) + except ValueError: + LOGGER.error(f"RECORDING_DUMP var Found and @{Config.USER_ID} is not a member of the group./ Channel") + return False + if not k.status in ["administrator", "creator"]: + LOGGER.error(f"RECORDING_DUMP var Found and @{Config.USER_ID} is not a admin of the group./ Channel") + return False + if Config.CHAT: + try: + k=await USER.get_chat_member(Config.CHAT, Config.USER_ID) + if not k.status in ["administrator", "creator"]: + LOGGER.warning(f"{Config.USER_ID} is not an admin in {Config.CHAT}, it is recommended to run the user as admin.") + elif k.status in ["administrator", "creator"] and not k.can_manage_voice_chats: + LOGGER.warning(f"{Config.USER_ID} is not having right to manage voicechat, it is recommended to promote with this right.") + except ValueError: + LOGGER.error(f"The user account by which you generated the SESSION_STRING is not found on CHAT ({Config.CHAT})") + return False + try: + k=await bot.get_chat_member(Config.CHAT, Config.BOT_USERNAME) + if not k.status == "administrator": + LOGGER.warning(f"{Config.BOT_USERNAME}, is not an admin in {Config.CHAT}, it is recommended to run the bot as admin.") + except ValueError: + LOGGER.warning(f"Bot Was Not Found on CHAT, it is recommended to add {Config.BOT_USERNAME} to {Config.CHAT}") + pass + if not Config.DATABASE_URI: + LOGGER.warning("No DATABASE_URI , found. It is recommended to use a database.") + return True + - -@group_call.on_raw_update() -async def handler(client: PyTgCalls, update: Update): - if str(update) == "JOINED_VOICE_CHAT": - Config.CALL_STATUS = True - if Config.EDIT_TITLE: - await edit_title() - elif str(update) == "LEFT_VOICE_CHAT": - Config.CALL_STATUS = False - elif str(update) == "PAUSED_STREAM": - Config.DUR['PAUSE'] = time.time() - Config.PAUSE=True - elif str(update) == "RESUMED_STREAM": - pause=Config.DUR.get('PAUSE') - if pause: - diff = time.time() - pause - start=Config.DUR.get('TIME') - if start: - Config.DUR['TIME']=start+diff - Config.PAUSE=False - elif str(update) == 'MUTED_STREAM': - Config.MUTED = True - elif str(update) == 'UNMUTED_STREAM': - Config.MUTED = False - - -@group_call.on_stream_end() -async def handler(client: PyTgCalls, update: Update): - if str(update) == "STREAM_AUDIO_ENDED" or str(update) == "STREAM_VIDEO_ENDED": - if not Config.STREAM_END.get("STATUS"): - Config.STREAM_END["STATUS"]=str(update) - if Config.STREAM_LINK and len(Config.playlist) == 0: - await stream_from_link(Config.STREAM_LINK) - elif not Config.playlist: - await start_stream() - else: - await skip() - await sleep(15) #wait for max 15 sec - try: - del Config.STREAM_END["STATUS"] - except: - pass - else: - try: - del Config.STREAM_END["STATUS"] - except: - pass