├── LICENSE ├── README.md ├── config.env ├── main.py ├── plugins ├── __init__.py ├── instagram.py ├── shazam.py ├── spotify.py ├── x.py └── youtube.py ├── requirements.txt ├── run ├── __init__.py ├── bot.py ├── buttons.py ├── channel_checker.py ├── commands.py ├── glob_variables.py ├── messages.py └── version_checker.py └── utils ├── __init__.py ├── broadcast.py ├── database.py ├── helper.py └── tweet_capture.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Adib Nikjou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MusicDownloader-Telegram-Bot 2 | 3 | MusicDownloader-Telegram-Bot is a Telegram bot that allows users to download music/video from Spotify and YouTube. It provides a convenient way to access and download your favorite tracks/videos directly to your device. 4 | 5 | You can test this bot at: 6 | ```telegram.me/spotify_yt_downloader_bot``` 7 | 8 | ## Features 9 | 10 | - Download music from Spotify links 11 | - Search for songs on Spotify using keywords 12 | - Supports different audio formats and qualities 13 | - Option to select between SpotDL and YoutubeDL for downloading 14 | - Broadcast messages to all users or specific subscribers 15 | - Subscription management for users 16 | - Voice recognition for song search 17 | - Screenshot capture of tweets 18 | - Download twitter media 19 | - Download Instagram media 20 | - Download Youtube media 21 | 22 | ## Installation 23 | 24 | Follow these steps to set up the `MusicDownloader-Telegram-Bot` project on your system. 25 | 26 | ### Step 1: Install FFmpeg 27 | 28 | FFmpeg is required for audio processing. Here's how to install it on different operating systems: 29 | 30 | #### Ubuntu/Debian 31 | ```zsh 32 | sudo apt install ffmpeg 33 | ``` 34 | 35 | #### macOS (using Homebrew) 36 | ```zsh 37 | brew install ffmpeg 38 | ``` 39 | 40 | #### Windows 41 | 42 | Download the FFmpeg build from [Here](https://ffmpeg.org/download.html). 43 | Extract the downloaded file and add the bin folder to your system's PATH. 44 | 45 | To verify the installation, run: 46 | ```zsh 47 | ffmpeg -version 48 | ``` 49 | 50 | If you see version information, FFmpeg is installed correctly. 51 | 52 | 53 | ### Step 2: Clone the Repository 54 | 55 | Open a terminal and clone the `MusicDownloader-Telegram-Bot` repository from GitHub: 56 | 57 | ```zsh 58 | git clone https://github.com/AdibNikjou/MusicDownloader-Telegram-Bot.git 59 | ``` 60 | 61 | ### Step 3: Install Python Dependencies 62 | 63 | Navigate to the cloned repository's directory and install the required Python dependencies using `pip`: 64 | 65 | ```zsh 66 | cd MusicDownloader-Telegram-Bot 67 | pip install -r requirements.txt 68 | ``` 69 | 70 | 71 | ### Step 4: Set Up Your Environment Variables 72 | 73 | Create a `config.env` file in the root directory of the project and add the following environment variables: 74 | 75 | - `SPOTIFY_CLIENT_ID=your_spotify_client_id` 76 | - `SPOTIFY_CLIENT_SECRET=your_spotify_client_secret` 77 | - `BOT_TOKEN=your_telegram_bot_token` 78 | - `API_ID=your_telegram_api_id` 79 | - `API_HASH=your_telegram_api_hash` 80 | - `GENIUS_ACCESS_TOKEN=your_genius_access_token` 81 | 82 | ### Step 5: Run the Bot 83 | 84 | With all dependencies installed and environment variables set, you can now run the bot: 85 | 86 | ```zsh 87 | python3 main.py 88 | ``` 89 | 90 | ## Usage 91 | 92 | 1. Start a conversation with the bot by sending the `/start` command. 93 | 2. Share a Spotify link or use the `/search` command followed by a song title or lyrics to find and download music. 94 | 3. Use the `/settings` command to change the audio format and quality. 95 | 4. Subscribe to receive updates and news from the bot. 96 | 5. Use the `/admin` command to access admin features (available only to authorized users). 97 | 98 | ## Commands 99 | 100 | - `/start`: Start the bot and get the welcome message. 101 | - `/search `: Search for songs on Spotify. 102 | - `/settings`: Access settings to change audio format and quality, downloading core, and subscription. 103 | - `/core`: Access directly to core settings to change downloading core. 104 | - `/quality`: Access directly to quality settings to change audio format and quality. 105 | - `/subscribe`: Subscribe to receive updates. 106 | - `/unsubscribe`: Unsubscribe from updates. 107 | - `/help`: Get help on how to use the bot. 108 | - `/ping`: Check the bot's response time. 109 | - `/stats`: Get statistics about the bot's usage. 110 | - `/admin`: Access admin features. 111 | 112 | ## Admin Commands 113 | 114 | - `/broadcast`: Send a message to all subscribed users or specific subscribers. 115 | - Ex: `/broadcast` -> Send a message to all subscribed users. 116 | - Ex: `/broadcast (1297994832,1297994833)` -> Send a message to 1297994832 and 1297994833 only. 117 | - Ex: `/broadcast_to_all` -> Send a message to all users. 118 | - `/stats`: Get statistics about the bot's usage. 119 | 120 | ## Dependencies 121 | 122 | - Python 3.10+ 123 | - Telethon 124 | - Spotipy 125 | - Yt-dlp 126 | - spotdl 127 | - Shazamio 128 | - Pillow 129 | - dotenv 130 | - aiosqlite 131 | - lyricsgenius 132 | - FastTelethonhelper 133 | 134 | ## Contributing 135 | 136 | Contributions are welcome! Please feel free to submit a pull request or open an issue if you find any bugs or have suggestions for improvements. 137 | 138 | ## License 139 | 140 | This project is licensed under the MIT License. See the `LICENSE` file for details. 141 | 142 | ## Contact 143 | 144 | For any inquiries or feedback, please contact the creator: 145 | - Telegram: @AdibNikjou 146 | - Email: adib.n7789@gmail.com 147 | 148 | ## Acknowledgments 149 | 150 | - Spotify API for providing access to music metadata. 151 | - Telegram API for the bot framework. 152 | - Shazam API for voice recognition. 153 | - YoutubeDL for downloading music from YouTube. 154 | -------------------------------------------------------------------------------- /config.env: -------------------------------------------------------------------------------- 1 | ## Use Python 3.10.12 2 | 3 | ADMIN_USER_IDS = "1297994832" #Seperate The admin users by , 4 | 5 | API_ID= "" 6 | API_HASH= "" 7 | 8 | BOT_TOKEN= "" 9 | 10 | SPOTIFY_CLIENT_SECRET = "" 11 | SPOTIFY_CLIENT_ID = "" 12 | 13 | GENIUS_ACCESS_TOKEN = "" -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from run import Bot 2 | from utils import asyncio 3 | 4 | 5 | async def main(): 6 | await Bot.initialize() 7 | await Bot.run() 8 | 9 | 10 | asyncio.run(main()) 11 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from .spotify import SpotifyDownloader 2 | from .shazam import ShazamHelper 3 | from .x import X 4 | from .instagram import Insta 5 | from .youtube import YoutubeDownloader 6 | -------------------------------------------------------------------------------- /plugins/instagram.py: -------------------------------------------------------------------------------- 1 | from utils import bs4, wget 2 | from utils import asyncio, re, requests 3 | 4 | 5 | class Insta: 6 | 7 | @classmethod 8 | def initialize(cls): 9 | cls.headers = { 10 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0", 11 | "Accept": "*/*", 12 | "Accept-Language": "en-US,en;q=0.5", 13 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 14 | "X-Requested-With": "XMLHttpRequest", 15 | "Content-Length": "99", 16 | "Origin": "https://saveig.app", 17 | "Connection": "keep-alive", 18 | "Referer": "https://saveig.app/en", 19 | } 20 | 21 | @staticmethod 22 | def is_instagram_url(text) -> bool: 23 | pattern = r'(?:https?:\/\/)?(?:www\.)?(?:instagram\.com|instagr\.am)(?:\/(?:p|reel|tv|stories)\/(?:[^\s\/]+)|\/([\w-]+)(?:\/(?:[^\s\/]+))?)' 24 | match = re.search(pattern, text) 25 | return bool(match) 26 | 27 | @staticmethod 28 | def extract_url(text) -> str | None: 29 | pattern = r'(https?:\/\/(?:www\.)?(?:ddinstagram\.com|instagram\.com|instagr\.am)\/(?:p|reel|tv|stories)\/[\w-]+\/?(?:\?[^\s]+)?(?:={1,2})?)' 30 | match = re.search(pattern, text) 31 | if match: 32 | return match.group(0) 33 | return None 34 | 35 | @staticmethod 36 | def determine_content_type(text) -> str: 37 | content_types = { 38 | '/p/': 'post', 39 | '/reel/': 'reel', 40 | '/tv': 'igtv', 41 | '/stories/': 'story', 42 | } 43 | 44 | for pattern, content_type in content_types.items(): 45 | if pattern in text: 46 | return content_type 47 | 48 | return None 49 | 50 | @staticmethod 51 | def is_publicly_available(url) -> bool: 52 | try: 53 | response = requests.get(url, headers=Insta.headers) 54 | if response.status_code == 200: 55 | return True 56 | else: 57 | return False 58 | except: 59 | return False 60 | 61 | @staticmethod 62 | async def download_content(client, event, start_message, link) -> bool: 63 | content_type = Insta.determine_content_type(link) 64 | try: 65 | if content_type == 'reel': 66 | await Insta.download_reel(client, event, link) 67 | await start_message.delete() 68 | return True 69 | elif content_type == 'post': 70 | await Insta.download_post(client, event, link) 71 | await start_message.delete() 72 | return True 73 | elif content_type == 'story': 74 | await Insta.download_story(client, event, link) 75 | await start_message.delete() 76 | return True 77 | else: 78 | await event.reply( 79 | "Sorry, unable to find the requested content. Please ensure it's publicly available.") 80 | await start_message.delete() 81 | return True 82 | except: 83 | await event.reply("Sorry, unable to find the requested content. Please ensure it's publicly available.") 84 | await start_message.delete() 85 | return False 86 | 87 | @staticmethod 88 | async def download(client, event) -> bool: 89 | link = Insta.extract_url(event.message.text) 90 | 91 | start_message = await event.respond("Processing Your insta link ....") 92 | try: 93 | if "ddinstagram.com" in link: 94 | raise Exception 95 | link = link.replace("instagram.com", "ddinstagram.com") 96 | return await Insta.download_content(client, event, start_message, link) 97 | except: 98 | await Insta.download_content(client, event, start_message, link) 99 | 100 | @staticmethod 101 | async def download_reel(client, event, link): 102 | try: 103 | meta_tag = await Insta.get_meta_tag(link) 104 | content_value = f"https://ddinstagram.com{meta_tag['content']}" 105 | except: 106 | meta_tag = await Insta.search_saveig(link) 107 | content_value = meta_tag[0] if meta_tag else None 108 | 109 | if content_value: 110 | await Insta.send_file(client, event, content_value) 111 | else: 112 | await event.reply("Oops, something went wrong") 113 | 114 | @staticmethod 115 | async def download_post(client, event, link): 116 | meta_tags = await Insta.search_saveig(link) 117 | if meta_tags: 118 | for meta in meta_tags[:-1]: 119 | await asyncio.sleep(1) 120 | await Insta.send_file(client, event, meta) 121 | else: 122 | await event.reply("Oops, something went wrong") 123 | 124 | @staticmethod 125 | async def download_story(client, event, link): 126 | meta_tag = await Insta.search_saveig(link) 127 | if meta_tag: 128 | await Insta.send_file(client, event, meta_tag[0]) 129 | else: 130 | await event.reply("Oops, something went wrong") 131 | 132 | @staticmethod 133 | async def get_meta_tag(link): 134 | getdata = requests.get(link).text 135 | soup = bs4.BeautifulSoup(getdata, 'html.parser') 136 | return soup.find('meta', attrs={'property': 'og:video'}) 137 | 138 | @staticmethod 139 | async def search_saveig(link): 140 | meta_tag = requests.post("https://saveig.app/api/ajaxSearch", data={"q": link, "t": "media", "lang": "en"}, 141 | headers=Insta.headers) 142 | if meta_tag.ok: 143 | res = meta_tag.json() 144 | return re.findall(r'href="(https?://[^"]+)"', res['data']) 145 | return None 146 | 147 | @staticmethod 148 | async def send_file(client, event, content_value): 149 | try: 150 | await client.send_file(event.chat_id, content_value, caption="Here's your Instagram content") 151 | except: 152 | fileoutput = f"{str(content_value)}" 153 | downfile = wget.download(content_value, out=fileoutput) 154 | await client.send_file(event.chat_id, fileoutput, caption="Here's your Instagram content") 155 | -------------------------------------------------------------------------------- /plugins/shazam.py: -------------------------------------------------------------------------------- 1 | from utils import Shazam, os 2 | 3 | 4 | class ShazamHelper: 5 | 6 | @classmethod 7 | def initialize(cls): 8 | cls.Shazam = Shazam() 9 | 10 | cls.voice_repository_dir = "repository/Voices" 11 | if not os.path.isdir(cls.voice_repository_dir): 12 | os.makedirs(cls.voice_repository_dir, exist_ok=True) 13 | 14 | @staticmethod 15 | async def recognize(file): 16 | try: 17 | out = await ShazamHelper.Shazam.recognize(file) 18 | except: 19 | out = await ShazamHelper.Shazam.recognize_song(file) 20 | return ShazamHelper.extract_song_details(out) 21 | 22 | # Function to extract the Spotify link 23 | @staticmethod 24 | def extract_spotify_link(data): 25 | for provider in data['track']['hub']['providers']: 26 | if provider['type'] == 'SPOTIFY': 27 | for action in provider['actions']: 28 | if action['type'] == 'uri': 29 | return action['uri'] 30 | return None 31 | 32 | @staticmethod 33 | def extract_song_details(data): 34 | 35 | try: 36 | music_name = data['track']['title'] 37 | artists_name = data['track']['subtitle'] 38 | except: 39 | return "" 40 | 41 | song_details = { 42 | 'music_name': music_name, 43 | 'artists_name': artists_name 44 | } 45 | song_details_string = ", ".join(f"{value}" for value in song_details.values()) 46 | return song_details_string 47 | -------------------------------------------------------------------------------- /plugins/spotify.py: -------------------------------------------------------------------------------- 1 | from run import Button, Buttons 2 | from utils import asyncio, re, os, load_dotenv, combinations 3 | from utils import db, SpotifyException, fast_upload, Any 4 | from utils import Image, BytesIO, YoutubeDL, lyricsgenius, aiohttp, InputMediaUploadedDocument 5 | from utils import SpotifyClientCredentials, spotipy, ThreadPoolExecutor, DocumentAttributeAudio 6 | 7 | 8 | class SpotifyDownloader: 9 | 10 | @classmethod 11 | def _load_dotenv_and_create_folders(cls): 12 | try: 13 | load_dotenv('config.env') 14 | cls.SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID") 15 | cls.SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET") 16 | cls.GENIUS_ACCESS_TOKEN = os.getenv("GENIUS_ACCESS_TOKEN") 17 | except FileNotFoundError: 18 | print("Failed to Load .env variables") 19 | 20 | # Create a directory for the download 21 | cls.download_directory = "repository/Musics" 22 | if not os.path.isdir(cls.download_directory): 23 | os.makedirs(cls.download_directory, exist_ok=True) 24 | 25 | cls.download_icon_directory = "repository/Icons" 26 | if not os.path.isdir(cls.download_icon_directory): 27 | os.makedirs(cls.download_icon_directory, exist_ok=True) 28 | 29 | @classmethod 30 | def initialize(cls): 31 | cls._load_dotenv_and_create_folders() 32 | cls.MAXIMUM_DOWNLOAD_SIZE_MB = 50 33 | cls.spotify_account = spotipy.Spotify(client_credentials_manager= 34 | SpotifyClientCredentials(client_id=cls.SPOTIFY_CLIENT_ID, 35 | client_secret=cls.SPOTIFY_CLIENT_SECRET)) 36 | cls.genius = lyricsgenius.Genius(cls.GENIUS_ACCESS_TOKEN) 37 | 38 | @staticmethod 39 | def is_spotify_link(url): 40 | pattern = r'https?://open\.spotify\.com/.*' 41 | return re.match(pattern, url) is not None 42 | 43 | @staticmethod 44 | def identify_spotify_link_type(spotify_url) -> str: 45 | # Define a list of all primary resource types supported by Spotify 46 | resource_types = ['track', 'playlist', 'album', 'artist', 'show', 'episode'] 47 | 48 | for resource_type in resource_types: 49 | try: 50 | # Dynamically call the appropriate method on the Spotify API client 51 | resource = getattr(SpotifyDownloader.spotify_account, resource_type)(spotify_url) 52 | return resource_type 53 | except (SpotifyException, Exception) as e: 54 | # Continue to the next resource type if an exception occurs 55 | continue 56 | 57 | # Return 'none' if no resource type matches 58 | return 'none' 59 | 60 | @staticmethod 61 | async def extract_data_from_spotify_link(event, spotify_url): 62 | 63 | # Identify the type of Spotify link to handle the data extraction accordingly 64 | link_type = SpotifyDownloader.identify_spotify_link_type(spotify_url) 65 | 66 | try: 67 | if link_type == "track": 68 | # Extract track information and construct the link_info dictionary 69 | track_info = SpotifyDownloader.spotify_account.track(spotify_url) 70 | artists = track_info['artists'] 71 | album = track_info['album'] 72 | link_info = { 73 | 'type': "track", 74 | 'track_name': track_info['name'], 75 | 'artist_name': ', '.join(artist['name'] for artist in artists), 76 | 'artist_ids': [artist['id'] for artist in artists], 77 | 'artist_url': artists[0]['external_urls']['spotify'], 78 | 'album_name': album['name'].translate(str.maketrans('', '', '()[]')), 79 | 'album_url': album['external_urls']['spotify'], 80 | 'release_year': album['release_date'].split('-')[0], 81 | 'image_url': album['images'][0]['url'], 82 | 'track_id': track_info['id'], 83 | 'isrc': track_info['external_ids']['isrc'], 84 | 'track_url': track_info['external_urls']['spotify'], 85 | 'youtube_link': None, # Placeholder, will be resolved below 86 | 'preview_url': track_info.get('preview_url'), 87 | 'duration_ms': track_info['duration_ms'], 88 | 'track_number': track_info['track_number'], 89 | 'is_explicit': track_info['explicit'] 90 | } 91 | 92 | # Attempt to enhance track info with additional external data (e.g., YouTube link) 93 | link_info['youtube_link'] = await SpotifyDownloader.extract_yt_video_info(link_info) 94 | return link_info 95 | 96 | elif link_type == "playlist": 97 | # Extract playlist information and compile playlist tracks into a dictionary 98 | playlist_info = SpotifyDownloader.spotify_account.playlist(spotify_url) 99 | 100 | playlist_info_dict = { 101 | 'type': 'playlist', 102 | 'playlist_name': playlist_info['name'], 103 | 'playlist_id': playlist_info['id'], 104 | 'playlist_url': playlist_info['external_urls']['spotify'], 105 | 'playlist_owner': playlist_info['owner']['display_name'], 106 | 'playlist_image_url': playlist_info['images'][0]['url'] if playlist_info['images'] else None, 107 | 'playlist_followers': playlist_info['followers']['total'], 108 | 'playlist_public': playlist_info['public'], 109 | 'playlist_tracks_total': playlist_info['tracks']['total'], 110 | } 111 | return playlist_info_dict 112 | 113 | else: 114 | # Handle unsupported Spotify link types 115 | link_info = {'type': link_type} 116 | print(f"Unsupported Spotify link type provided: {spotify_url}") 117 | return link_info 118 | 119 | except Exception as e: 120 | # Log and handle any errors encountered during information extraction 121 | print(f"Error extracting Spotify information: {e}") 122 | await event.respond("An error occurred while processing the Spotify link. Please try again.") 123 | return {} 124 | 125 | @staticmethod 126 | async def extract_yt_video_info(spotify_link_info): 127 | if spotify_link_info is None: 128 | return None 129 | 130 | video_url = spotify_link_info.get('youtube_link') 131 | if video_url: 132 | return video_url 133 | 134 | artist_name = spotify_link_info["artist_name"] 135 | track_name = spotify_link_info["track_name"] 136 | release_year = spotify_link_info["release_year"] 137 | track_duration = spotify_link_info.get("duration_ms", 0) / 1000 138 | album_name = spotify_link_info.get("album_name", "") 139 | 140 | queries = [ 141 | f'"{artist_name}" "{track_name}" lyrics {release_year}', 142 | f'"{track_name}" by "{artist_name}" {release_year}', 143 | f'"{artist_name}" "{track_name}" "{album_name}" {release_year}', 144 | ] 145 | 146 | ydl_opts = { 147 | 'quiet': True, 148 | 'no_warnings': True, 149 | 'ignoreerrors': True, 150 | 'ytsearch': 3, # Limit the number of search results 151 | 'skip_download': True, 152 | 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', 153 | 'noplaylist': True, # Disable playlist processing 154 | 'nocheckcertificate': True, # Disable SSL certificate verification 155 | 'cachedir': False # Disable caching 156 | } 157 | 158 | executor = ThreadPoolExecutor(max_workers=16) # Use 16 workers for the blocking I/O operation 159 | stop_event = asyncio.Event() 160 | 161 | async def search_query(query): 162 | def extract_info_blocking(): 163 | with YoutubeDL(ydl_opts) as ydl: 164 | try: 165 | info = ydl.extract_info(f'ytsearch:{query}', download=False) 166 | entries = info.get('entries', []) 167 | return entries 168 | except Exception: 169 | return [] 170 | 171 | entries = await asyncio.get_running_loop().run_in_executor(executor, extract_info_blocking) 172 | 173 | if not stop_event.is_set(): 174 | for video_info in entries: 175 | video_url = video_info.get('webpage_url') 176 | video_duration = video_info.get('duration', 0) 177 | 178 | # Compare the video duration with the track duration from Spotify 179 | duration_diff = abs(video_duration - track_duration) 180 | if duration_diff <= 35: 181 | stop_event.set() 182 | return video_url 183 | 184 | return None 185 | 186 | search_tasks = [asyncio.create_task(search_query(query)) for query in queries] 187 | search_results = await asyncio.gather(*search_tasks, return_exceptions=True) 188 | 189 | for result in search_results: 190 | if isinstance(result, Exception): 191 | continue 192 | if result is not None: 193 | return result 194 | 195 | return None 196 | 197 | @staticmethod 198 | async def download_and_send_spotify_info(event, is_query: bool = True) -> bool: 199 | user_id = event.sender_id 200 | waiting_message = None 201 | if is_query: 202 | waiting_message = await event.respond('⏳') 203 | query_data = str(event.data) 204 | spotify_link = query_data.split("/")[-1][:-1] 205 | else: 206 | spotify_link = str(event.message.text) 207 | 208 | # Ensure the user's data is up-to-date 209 | if not await db.get_user_updated_flag(user_id): 210 | await event.respond( 211 | "Our bot has been updated. Please restart the bot with the /start command." 212 | ) 213 | return True 214 | 215 | link_info = await SpotifyDownloader.extract_data_from_spotify_link(event, spotify_url=spotify_link) 216 | if link_info["type"] == "track": 217 | await waiting_message.delete() if is_query else None 218 | return await SpotifyDownloader.send_track_info(event.client, event, link_info) 219 | elif link_info["type"] == "playlist": 220 | return await SpotifyDownloader.send_playlist_info(event.client, event, link_info) 221 | else: 222 | await event.respond( 223 | f"""Unsupported Spotify link type.\n\nThe Bot is currently supports:\n- track \n- playlist\n\nYou 224 | requested: {link_info["type"]} """) 225 | return False 226 | 227 | @staticmethod 228 | async def fetch_and_save_playlist_image(playlist_id, playlist_image_url): 229 | icon_name = f"{playlist_id}.jpeg" 230 | icon_path = os.path.join(SpotifyDownloader.download_icon_directory, icon_name) 231 | 232 | try: 233 | async with aiohttp.ClientSession() as session: 234 | async with session.get(playlist_image_url) as response: 235 | if response.status == 200: 236 | image_data = await response.read() 237 | img = Image.open(BytesIO(image_data)) 238 | img.save(icon_path) 239 | return icon_path 240 | else: 241 | print(f"Failed to download playlist image. Status code: {response.status}") 242 | except Exception as e: 243 | print(f"Error downloading or saving playlist image: {e}") 244 | 245 | return None 246 | 247 | @staticmethod 248 | async def send_playlist_info(client, event, link_info): 249 | playlist_image_url = link_info.get('playlist_image_url') 250 | playlist_name = link_info.get('playlist_name', 'Unavailable') 251 | playlist_id = link_info.get('playlist_id', 'Unavailable') 252 | playlist_url = link_info.get('playlist_url', 'Unavailable') 253 | playlist_owner = link_info.get('playlist_owner', 'Unavailable') 254 | total_tracks = link_info.get('playlist_tracks_total', 0) 255 | collaborative = 'Yes' if link_info.get('collaborative', False) else 'No' 256 | public = 'Yes' if link_info.get('playlist_public', False) else 'No' 257 | followers = link_info.get('playlist_followers', 'Unavailable') 258 | 259 | # Construct the playlist information text 260 | playlist_info = ( 261 | f"🎧 **Playlist: {playlist_name}** 🎶\n\n" 262 | f"---\n\n" 263 | f"**Details:**\n\n" 264 | 265 | f" - 👤 Owner: {playlist_owner}\n" 266 | f" - 👥 Followers: {followers}\n" 267 | 268 | f" - 🎵 Total Tracks: {total_tracks}\n" 269 | f" - 🤝 Collaborative: {collaborative}\n" 270 | f" - 🌐 Public: {public}\n" 271 | 272 | f" - 🎧 Playlist URL: [Listen On Spotify]({playlist_url})\n" 273 | f"---\n\n" 274 | f"**Enjoy the music!** 🎶" 275 | ) 276 | 277 | # Buttons for interactivity 278 | buttons = [ 279 | [Button.inline("Download All Tracks Inside [mp3]", data=f"spotify/dl/playlist/{playlist_id}/all")], 280 | [Button.inline("Download Top 10", data=f"spotify/dl/playlist/{playlist_id}/10")], 281 | [Button.inline("Search Tracks inside", data=f"spotify/s/playlist/{playlist_id}")], 282 | [Button.inline("Cancel", data=b"cancel")] 283 | ] 284 | 285 | # Handle the playlist image if exists 286 | if playlist_image_url: 287 | icon_path = await SpotifyDownloader.fetch_and_save_playlist_image(playlist_id, playlist_image_url) 288 | if icon_path: 289 | sent_message = await client.send_file( 290 | event.chat_id, 291 | icon_path, 292 | caption=playlist_info, 293 | parse_mode='Markdown', 294 | buttons=buttons, 295 | ) 296 | else: 297 | await event.respond(playlist_info, parse_mode='Markdown', buttons=buttons) 298 | else: 299 | await event.respond(playlist_info, parse_mode='Markdown', buttons=buttons) 300 | 301 | return True 302 | 303 | @staticmethod 304 | async def download_icon(link_info): 305 | track_name = link_info['track_name'] 306 | artist_name = link_info['artist_name'] 307 | image_url = link_info["image_url"] 308 | 309 | icon_name = f"{track_name} - {artist_name}.jpeg".replace("/", " ") 310 | icon_path = os.path.join(SpotifyDownloader.download_icon_directory, icon_name) 311 | 312 | if not os.path.isfile(icon_path): 313 | try: 314 | async with aiohttp.ClientSession() as session: 315 | async with session.get(image_url) as response: 316 | if response.status == 200: 317 | image_data = await response.read() 318 | img = Image.open(BytesIO(image_data)) 319 | img.save(icon_path) 320 | else: 321 | print( 322 | f"Failed to download track image for {track_name} - {artist_name}. Status code: {response.status}") 323 | except Exception as e: 324 | print(f"Failed to download or save track image for {track_name} - {artist_name}: {e}") 325 | return icon_path 326 | 327 | @staticmethod 328 | async def send_track_info(client, event, link_info): 329 | user_id = event.sender_id 330 | music_quality = await db.get_user_music_quality(user_id) 331 | downloading_core = await db.get_user_downloading_core(user_id) 332 | 333 | if downloading_core == "Auto": 334 | spotdl = True if (link_info.get('youtube_link') is None) else False 335 | else: 336 | spotdl = downloading_core == "SpotDL" 337 | 338 | def build_file_path(artist, track_name, quality_info, format, make_dir=False): 339 | filename = f"{artist} - {track_name}".replace("/", "") 340 | if not spotdl: 341 | filename += f"-{quality_info['quality']}" 342 | directory = os.path.join(SpotifyDownloader.download_directory, filename) 343 | if make_dir and not os.path.exists(directory): 344 | os.makedirs(directory) 345 | return f"{directory}.{quality_info['format']}" 346 | 347 | def is_track_local(artist_names, track_name): 348 | for r in range(1, len(artist_names) + 1): 349 | for combination in combinations(artist_names, r): 350 | file_path = build_file_path(", ".join(combination), track_name, music_quality, 351 | music_quality['format']) 352 | if os.path.isfile(file_path): 353 | return True, file_path 354 | return False, None 355 | 356 | artist_names = link_info['artist_name'].split(', ') 357 | is_local, file_path = is_track_local(artist_names, link_info['track_name']) 358 | 359 | icon_path = await SpotifyDownloader.download_icon(link_info) 360 | 361 | SpotifyInfoButtons = [ 362 | [Button.inline("Download 30s Preview", 363 | data=f"spotify/dl/30s_preview/{link_info['preview_url'].split('?cid')[0].replace('https://p.scdn.co/mp3-preview/', '')}") 364 | if link_info['preview_url'] is not None else Button.inline("Download 30s Preview", 365 | data=b"unavailable_feature")], 366 | [Button.inline("Download Track", data=f"spotify/dl/music/{link_info['track_id']}")], 367 | [Button.inline("Download Icon", 368 | data=f"spotify/dl/icon/{link_info['image_url'].replace('https://i.scdn.co/image/', '')}")], 369 | [Button.inline("Artist Info", data=f"spotify/artist/{link_info['track_id']}")], 370 | [Button.inline("Lyrics", data=f"spotify/lyrics/{link_info['track_id']}")], 371 | [Button.url("Listen On Spotify", url=link_info["track_url"]), 372 | Button.url("Listen On Youtube", url=link_info['youtube_link']) if link_info[ 373 | 'youtube_link'] else Button.inline("Listen On Youtube", data=b"unavailable_feature")], 374 | [Button.inline("Cancel", data=b"cancel")] 375 | ] 376 | 377 | caption = ( 378 | f"**🎧 Title:** [{link_info['track_name']}]({link_info['track_url']})\n" 379 | f"**🎤 Artist:** [{link_info['artist_name']}]({link_info['artist_url']})\n" 380 | f"**💽 Album:** [{link_info['album_name']}]({link_info['album_url']})\n" 381 | f"**🗓 Release Year:** {link_info['release_year']}\n" 382 | f"**❗️ Is Local:** {is_local}\n" 383 | f"**🌐 ISRC:** {link_info['isrc']}\n" 384 | f"**🔄 Downloaded:** {await db.get_song_downloads(link_info['track_name'])} times\n\n" 385 | f"**Image URL:** [Click here]({link_info['image_url']})\n" 386 | f"**Track id:** {link_info['track_id']}\n" 387 | ) 388 | 389 | try: 390 | await client.send_file( 391 | event.chat_id, 392 | icon_path, 393 | caption=caption, 394 | parse_mode='Markdown', 395 | buttons=SpotifyInfoButtons 396 | ) 397 | return True 398 | except Exception as Err: 399 | print(f"Failed to send track info: {Err}") 400 | return False 401 | 402 | @staticmethod 403 | async def send_local_file(event, file_info, spotify_link_info, is_playlist: bool = False) -> bool: 404 | user_id = event.sender_id 405 | upload_status_message = None 406 | 407 | # Unpack file_info for clarity 408 | was_local = file_info['is_local'] 409 | 410 | # Notify the user about local availability for faster processing, if applicable 411 | local_availability_message = None 412 | if was_local and not is_playlist: 413 | local_availability_message = await event.respond( 414 | "This track is available locally. Preparing it for you now...") 415 | 416 | # Provide feedback to the user during the upload process 417 | if not is_playlist: 418 | upload_status_message = await event.reply("Now uploading... Please hold on.") 419 | 420 | try: 421 | # Indicate ongoing file upload to enhance user experience 422 | async with event.client.action(event.chat_id, 'document'): 423 | # Use a ThreadPoolExecutor to upload files in parallel 424 | await SpotifyDownloader._upload_file( 425 | event, file_info, spotify_link_info, is_playlist 426 | ) 427 | 428 | except Exception as e: 429 | # Handle exceptions and provide feedback 430 | await db.set_file_processing_flag(user_id, 0) # Reset file processing flag 431 | await event.respond(f"Unfortunately, uploading failed.\nReason: {e}") if not is_playlist else None 432 | return False # Returning False signifies the operation didn't complete successfully 433 | 434 | # Clean up feedback messages 435 | if local_availability_message: 436 | await local_availability_message.delete() 437 | 438 | if not is_playlist: 439 | await upload_status_message.delete() 440 | 441 | # Reset file processing flag after completion 442 | await db.set_file_processing_flag(user_id, 0) 443 | 444 | await db.add_or_increment_song(spotify_link_info['track_name']) 445 | # Indicate successful upload operation 446 | return True 447 | 448 | @staticmethod 449 | async def _upload_file(event, file_info, spotify_link_info, playlist: bool = False): 450 | 451 | if not os.path.exists(file_info['icon_path']): 452 | await SpotifyDownloader.download_icon(spotify_link_info) 453 | 454 | # Unpack file_info for clarity 455 | file_path = file_info['file_path'] 456 | icon_path = file_info['icon_path'] 457 | video_url = file_info['video_url'] 458 | 459 | if not playlist: 460 | # Use fast_upload for faster uploads 461 | uploaded_file = await fast_upload( 462 | client=event.client, 463 | file_location=file_path, 464 | reply=None, # No need for a progress bar in this case 465 | name=file_info['file_name'], 466 | progress_bar_function=None 467 | ) 468 | 469 | uploaded_file = await event.client.upload_file(uploaded_file if not playlist else file_path) 470 | uploaded_thumbnail = await event.client.upload_file(icon_path) 471 | 472 | audio_attributes = DocumentAttributeAudio( 473 | duration=0, 474 | title=f"{spotify_link_info['track_name']} - {spotify_link_info['artist_name']}", 475 | performer="@Spotify_YT_Downloader_BOT", 476 | waveform=None, 477 | voice=False 478 | ) 479 | 480 | # Send the uploaded file as music 481 | media = InputMediaUploadedDocument( 482 | file=uploaded_file, 483 | thumb=uploaded_thumbnail, 484 | mime_type='audio/mpeg', # Adjust the MIME type based on your file's format 485 | attributes=[audio_attributes], 486 | ) 487 | 488 | # Send the media to the chat 489 | await event.client.send_file( 490 | event.chat_id, 491 | media, 492 | caption=( 493 | f"🎵 **{spotify_link_info['track_name']}** by **{spotify_link_info['artist_name']}**\n\n" 494 | f"▶️ [Listen on Spotify]({spotify_link_info['track_url']})\n" 495 | + (f"🎥 [Watch on YouTube]({video_url})\n" if video_url else "") 496 | ), 497 | supports_streaming=True, 498 | force_document=False, 499 | thumb=icon_path 500 | ) 501 | 502 | @staticmethod 503 | async def download_spotdl(event, music_quality, spotify_link_info, quite: bool = False, initial_message=None, 504 | audio_option: str = "piped") -> bool | tuple[bool, Any | None] | tuple[bool, bool]: 505 | user_id = event.sender_id 506 | command = f'python3 -m spotdl --format {music_quality["format"]} --audio {audio_option} --output "{SpotifyDownloader.download_directory}" --threads {15} "{spotify_link_info["track_url"]}"' 507 | try: 508 | # Start the subprocess 509 | process = await asyncio.create_subprocess_shell( 510 | command, 511 | stdout=asyncio.subprocess.PIPE, 512 | stderr=asyncio.subprocess.PIPE, 513 | stdin=asyncio.subprocess.DEVNULL 514 | ) 515 | except Exception as e: 516 | await event.respond(f"Failed to download. Error: {e}") 517 | await db.set_file_processing_flag(user_id, 0) 518 | return False 519 | 520 | if initial_message is None and not quite: 521 | # Send an initial message to the user with a progress bar 522 | initial_message = await event.reply("SpotDL: Downloading...\nApproach: Piped") 523 | elif quite: 524 | initial_message = None 525 | 526 | # Function to send updates to the user 527 | async def send_updates(process, message): 528 | while True: 529 | # Read a line from stdout 530 | line = await process.stdout.readline() 531 | line = line.decode().strip() 532 | 533 | print(line) 534 | 535 | if not quite: 536 | if audio_option == "piped": 537 | await message.edit(f"SpotDL: Downloading...\nApproach: Piped\n\n{line}") 538 | elif audio_option == "soundcloud": 539 | await message.edit(f"SpotDL: Downloading...\nApproach: SoundCloud\n\n{line}") 540 | else: 541 | await message.edit(f"SpotDL: Downloading...\nApproach: YouTube\n\n{line}") 542 | 543 | # Check for errors 544 | if any(err in line for err in ( 545 | "LookupError", "FFmpegError", "JSONDecodeError", "ReadTimeout", "KeyError", "Forbidden", 546 | "AudioProviderError")): 547 | if audio_option == "piped": 548 | if not quite: 549 | await message.edit( 550 | f"SpotDL: Downloading...\nApproach: Piped Failed, Using SoundCloud Approach.\n\n{line}") 551 | return False # Indicate that an error occurred 552 | elif audio_option == "soundcloud": 553 | if not quite: 554 | await message.edit( 555 | f"SpotDL: Downloading...\nApproach: SoundCloud Failed, Using Youtube Approach.\n\n{line}") 556 | return False 557 | else: 558 | if not quite: 559 | await message.edit(f"SpotDL: Downloading...\nApproach: All Approaches Failed.\n\n{line}") 560 | return False 561 | elif not line: 562 | return True 563 | 564 | success = await send_updates(process, initial_message) 565 | if not success and audio_option == "piped": 566 | if not quite: 567 | await initial_message.edit( 568 | f"SpotDL: Downloading...\nApproach: Piped Failed, Using SoundCloud Approach.") 569 | return False, initial_message 570 | elif not success and audio_option == "soundcloud": 571 | if not quite: 572 | await initial_message.edit( 573 | f"SpotDL: Downloading...\nApproach: SoundCloud Failed, Using Youtube Approach.") 574 | return False, initial_message 575 | elif not success and audio_option == "youtube": 576 | return False, False 577 | # Wait for the process to finish 578 | await process.wait() 579 | await initial_message.delete() if initial_message else None 580 | return True, True 581 | 582 | @staticmethod 583 | async def download_YoutubeDL(event, file_info, music_quality, is_playlist: bool = False): 584 | user_id = event.sender_id 585 | video_url = file_info['video_url'] 586 | filename = file_info['file_name'] 587 | 588 | download_message = None 589 | if not is_playlist: 590 | download_message = await event.respond("Downloading .") 591 | 592 | async def get_file_size(video_url): 593 | ydl_opts = { 594 | 'format': "bestaudio", 595 | 'default_search': 'ytsearch', 596 | 'noplaylist': True, 597 | "nocheckcertificate": True, 598 | "quiet": True, 599 | "geo_bypass": True, 600 | 'get_filesize': True # Retrieve the file size without downloading 601 | } 602 | 603 | with YoutubeDL(ydl_opts) as ydl: 604 | info_dict = await asyncio.to_thread(ydl.extract_info, video_url, download=False) 605 | file_size = info_dict.get('filesize', None) 606 | return file_size 607 | 608 | async def download_audio(video_url, filename, music_quality): 609 | ydl_opts = { 610 | 'format': "bestaudio", 611 | 'default_search': 'ytsearch', 612 | 'noplaylist': True, 613 | "nocheckcertificate": True, 614 | "outtmpl": f"{SpotifyDownloader.download_directory}/{filename}", 615 | "quiet": True, 616 | "addmetadata": True, 617 | "prefer_ffmpeg": False, 618 | "geo_bypass": True, 619 | "postprocessors": [{'key': 'FFmpegExtractAudio', 'preferredcodec': music_quality['format'], 620 | 'preferredquality': music_quality['quality']}] 621 | } 622 | 623 | with YoutubeDL(ydl_opts) as ydl: 624 | await download_message.edit("Downloading . . .") if not is_playlist else None 625 | await asyncio.to_thread(ydl.extract_info, video_url, download=True) 626 | 627 | async def download_handler(): 628 | file_size_task = asyncio.create_task(get_file_size(video_url)) 629 | file_size = await file_size_task 630 | 631 | if file_size and file_size > SpotifyDownloader.MAXIMUM_DOWNLOAD_SIZE_MB * 1024 * 1024: 632 | await event.respond("Err: File size is more than 50 MB.\nSkipping download.") 633 | await db.set_file_processing_flag(user_id, 0) 634 | return False, None # Skip the download 635 | 636 | if not is_playlist: 637 | await download_message.edit("Downloading . .") 638 | 639 | download_task = asyncio.create_task(download_audio(video_url, filename, music_quality)) 640 | try: 641 | await download_task 642 | return True, download_message 643 | except Exception as ERR: 644 | await event.respond(f"Something Went Wrong Processing Your Query.") 645 | await db.set_file_processing_flag(user_id, 0) 646 | return False, download_message 647 | 648 | return await download_handler() 649 | 650 | @staticmethod 651 | async def download_spotify_file_and_send(event) -> bool: 652 | 653 | user_id = event.sender_id 654 | 655 | query_data = str(event.data) 656 | is_playlist = True if query_data.split("/")[-3] == "playlist" else False 657 | 658 | if is_playlist: 659 | spotify_link = query_data.split("/")[-2] 660 | else: 661 | spotify_link = query_data.split("/")[-1][:-1] 662 | 663 | if await db.get_file_processing_flag(user_id): 664 | await event.respond("Sorry,There is already a file being processed for you.") 665 | return True 666 | 667 | fetch_message = await event.respond("Fetching information... Please wait.") 668 | spotify_link_info = await SpotifyDownloader.extract_data_from_spotify_link(event, spotify_link) 669 | 670 | await db.set_file_processing_flag(user_id, 1) 671 | await fetch_message.delete() 672 | 673 | if spotify_link_info['type'] == "track": 674 | return await SpotifyDownloader.download_track(event, spotify_link_info) 675 | elif spotify_link_info['type'] == "playlist": 676 | return await SpotifyDownloader.download_playlist(event, spotify_link_info, 677 | number_of_downloads=query_data.split("/")[-1][:-1]) 678 | 679 | @staticmethod 680 | async def download_track(event, spotify_link_info, is_playlist: bool = False): 681 | 682 | user_id = event.sender_id 683 | 684 | music_quality = await db.get_user_music_quality(user_id) 685 | downloading_core = await db.get_user_downloading_core(user_id) 686 | 687 | if downloading_core == "Auto": 688 | spotdl = True if (spotify_link_info.get('youtube_link') is None) else False 689 | else: 690 | spotdl = downloading_core == "SpotDL" 691 | 692 | if (spotify_link_info.get('youtube_link', None) is None) and not spotdl: 693 | await db.set_file_processing_flag(user_id, 0) 694 | return False 695 | 696 | file_path, filename, is_local = SpotifyDownloader._determine_file_path(spotify_link_info, music_quality, spotdl) 697 | 698 | file_info = { 699 | "file_name": filename, 700 | "file_path": file_path, 701 | "icon_path": SpotifyDownloader._get_icon_path(spotify_link_info), 702 | "is_local": is_local, 703 | "video_url": spotify_link_info.get('youtube_link') 704 | } 705 | 706 | if is_local: 707 | return await SpotifyDownloader.send_local_file(event, file_info, spotify_link_info, is_playlist) 708 | else: 709 | return await SpotifyDownloader._handle_download(event, spotify_link_info, music_quality, file_info, 710 | spotdl, is_playlist) 711 | 712 | @staticmethod 713 | async def _handle_download(event, spotify_link_info, music_quality, file_info, spotdl, is_playlist): 714 | file_path = file_info["file_path"] 715 | if not spotdl: 716 | result, download_message = await SpotifyDownloader.download_YoutubeDL(event, file_info, music_quality, 717 | is_playlist) 718 | 719 | if os.path.isfile(file_path) and result: 720 | 721 | if not is_playlist: 722 | download_message = await download_message.edit("Downloading . . . .") 723 | download_message = await download_message.edit("Downloading . . . . .") 724 | 725 | await download_message.delete() 726 | 727 | send_file_result = await SpotifyDownloader.send_local_file(event, file_info, spotify_link_info, 728 | is_playlist) 729 | return send_file_result 730 | else: 731 | return False 732 | 733 | else: 734 | result, message = await SpotifyDownloader.download_spotdl(event, music_quality, spotify_link_info) 735 | if not result: 736 | result, message = await SpotifyDownloader.download_spotdl(event, music_quality, 737 | spotify_link_info, is_playlist, 738 | message, 739 | audio_option="soundcloud") 740 | if not result: 741 | result, _ = await SpotifyDownloader.download_spotdl(event, music_quality, spotify_link_info, 742 | is_playlist, 743 | message, audio_option="youtube") 744 | if result and message: 745 | return await SpotifyDownloader.send_local_file(event, file_info, 746 | spotify_link_info, is_playlist) if result else False 747 | else: 748 | return False 749 | 750 | @staticmethod 751 | def _get_icon_path(spotify_link_info): 752 | icon_name = f"{spotify_link_info['track_name']} - {spotify_link_info['artist_name']}.jpeg".replace("/", " ") 753 | return os.path.join(SpotifyDownloader.download_icon_directory, icon_name) 754 | 755 | @staticmethod 756 | def _determine_file_path(spotify_link_info, music_quality, spotdl): 757 | artist_names = spotify_link_info['artist_name'].split(', ') 758 | for r in range(1, len(artist_names) + 1): 759 | for combination in combinations(artist_names, r): 760 | filename = f"{', '.join(combination)} - {spotify_link_info['track_name']}".replace("/", "") 761 | filename += f"-{music_quality['quality']}" if not spotdl else "" 762 | file_path = os.path.join(SpotifyDownloader.download_directory, f"{filename}.{music_quality['format']}") 763 | if os.path.isfile(file_path): 764 | return file_path, filename, True 765 | filename = f"{spotify_link_info['artist_name']} - {spotify_link_info['track_name']}".replace("/", "") 766 | filename += f"-{music_quality['quality']}" if not spotdl else "" 767 | return os.path.join(SpotifyDownloader.download_directory, 768 | f"{filename}.{music_quality['format']}"), filename, False 769 | 770 | @staticmethod 771 | async def download_playlist(event, spotify_link_info, number_of_downloads: str): 772 | playlist_id = spotify_link_info["playlist_id"] 773 | music_quality = None 774 | 775 | await db.set_file_processing_flag(event.sender_id, 1) 776 | 777 | if number_of_downloads == "10": 778 | tracks_info = await SpotifyDownloader.get_playlist_tracks(playlist_id) 779 | elif number_of_downloads == "all": 780 | music_quality = await db.get_user_music_quality(event.sender_id) 781 | new_music_quality = {'format': "mp3", 'quality': 320} 782 | await db.set_user_music_quality(event.sender_id, new_music_quality) 783 | tracks_info = await SpotifyDownloader.get_playlist_tracks(playlist_id, get_all=True) 784 | else: 785 | await db.set_file_processing_flag(event.sender_id, 0) 786 | return await event.respond("Sorry, Something went wrong.\ntry again later.") 787 | 788 | start_message = await event.respond("Checking the playlist ....") 789 | 790 | batch_size = 10 791 | track_batches = [tracks_info[i:i + batch_size] for i in range(0, len(tracks_info), batch_size)] 792 | download_tasks = [] 793 | 794 | await start_message.edit("Sending musics.... Please Hold on.") 795 | 796 | for batch in track_batches: 797 | # Download tracks in the current batch concurrently 798 | download_tasks.extend([SpotifyDownloader.download_track(event, 799 | await SpotifyDownloader.extract_data_from_spotify_link( 800 | event, track["track_id"]), is_playlist=True) for 801 | track in batch]) 802 | 803 | # Wait for all downloads in the batch to complete before proceeding to the next batch 804 | await asyncio.gather(*download_tasks) 805 | download_tasks.clear() # Clear completed tasks 806 | 807 | await start_message.delete() 808 | if music_quality is not None: 809 | await db.set_user_music_quality(event.sender_id, music_quality) 810 | await db.set_file_processing_flag(event.sender_id, 0) 811 | return await event.respond("Enjoy!\n\nOur bot is OpenSource.", buttons=Buttons.source_code_button) 812 | 813 | @staticmethod 814 | async def search_spotify_based_on_user_input(query, limit=10): 815 | results = SpotifyDownloader.spotify_account.search(q=query, limit=limit) 816 | 817 | extracted_details = [] 818 | 819 | for result in results['tracks']['items']: 820 | # Extracting track name, artist's name, release year, and track ID 821 | track_name = result['name'] 822 | artist_name = result['artists'][0]['name'] # Assuming the first artist is the primary one 823 | release_year = result['album']['release_date'] 824 | track_id = result['id'] 825 | 826 | # Append the extracted details to the list 827 | extracted_details.append({ 828 | "track_name": track_name, 829 | "artist_name": artist_name, 830 | "release_year": release_year.split("-")[0], 831 | "track_id": track_id 832 | }) 833 | 834 | return extracted_details 835 | 836 | @staticmethod 837 | async def send_30s_preview(event): 838 | try: 839 | query_data = str(event.data) 840 | preview_url = "https://p.scdn.co/mp3-preview/" + query_data.split("/")[-1][:-1] 841 | await event.respond(file=preview_url) 842 | except Exception as Err: 843 | await event.respond(f"Sorry, Something went wrong:\nError\n{str(Err)}") 844 | 845 | @staticmethod 846 | async def send_artists_info(event): 847 | query_data = str(event.data) 848 | track_id = query_data.split("/")[-1][:-1] 849 | track_info = SpotifyDownloader.spotify_account.track(track_id=track_id) 850 | artist_ids = [artist["id"] for artist in track_info['artists']] 851 | artist_details = [] 852 | 853 | def format_number(number): 854 | if number >= 1000000000: 855 | return f"{number // 1000000000}.{(number % 1000000000) // 100000000}B" 856 | elif number >= 1000000: 857 | return f"{number // 1000000}.{(number % 1000000) // 100000}M" 858 | elif number >= 1000: 859 | return f"{number // 1000}.{(number % 1000) // 100}K" 860 | else: 861 | return str(number) 862 | 863 | for artist_id in artist_ids: 864 | artist = SpotifyDownloader.spotify_account.artist(artist_id.replace("'", "")) 865 | artist_details.append({ 866 | 'name': artist['name'], 867 | 'followers': format_number(artist['followers']['total']), 868 | 'genres': artist['genres'], 869 | 'popularity': artist['popularity'], 870 | 'image_url': artist['images'][0]['url'] if artist['images'] else None, 871 | 'external_url': artist['external_urls']['spotify'] 872 | }) 873 | 874 | # Create a professional artist info message with more details and formatting 875 | message = "🎤 Artists Information :\n\n" 876 | for artist in artist_details: 877 | message += f"🌟 Artist Name: {artist['name']}\n" 878 | message += f"👥 Followers: {artist['followers']}\n" 879 | message += f"🎵 Genres: {', '.join(artist['genres'])}\n" 880 | message += f"📈 Popularity: {artist['popularity']}\n" 881 | if artist['image_url']: 882 | message += f"\n🖼️ Image: Image Url\n" 883 | message += f"🔗 Spotify URL: Spotify Link\n\n" 884 | message += "───────────\n\n" 885 | 886 | # Create buttons with URLs 887 | artist_buttons = [ 888 | [Button.url(f"🎧 {artist['name']}", artist['external_url'])] 889 | for artist in artist_details 890 | ] 891 | artist_buttons.append([Button.inline("Remove", data='cancel')]) 892 | 893 | await event.respond(message, parse_mode='html', buttons=artist_buttons) 894 | 895 | @staticmethod 896 | async def send_music_lyrics(event): 897 | MAX_MESSAGE_LENGTH = 4096 # Telegram's maximum message length 898 | SECTION_HEADER_PATTERN = r'\[.+?\]' # Pattern to match section headers 899 | 900 | query_data = str(event.data) 901 | track_id = query_data.split("/")[-1][:-1] 902 | track_info = SpotifyDownloader.spotify_account.track(track_id=track_id) 903 | artist_names = ",".join(artist['name'] for artist in track_info['artists']) 904 | 905 | waiting_message = await event.respond("Searching For Lyrics in Genius ....") 906 | song = SpotifyDownloader.genius.search_song( 907 | f""" "{track_info['name']}"+"{artist_names}" """) 908 | if song: 909 | await waiting_message.delete() 910 | lyrics = song.lyrics 911 | 912 | if not lyrics: 913 | error_message = "Sorry, I couldn't find the lyrics for this track." 914 | return await event.respond(error_message) 915 | 916 | # Remove 'Embed' and the first line of the lyrics 917 | lyrics = song.lyrics.strip().split('\n', 1)[-1] 918 | lyrics = lyrics.replace('Embed', '').strip() 919 | 920 | metadata = f"**Song:** {track_info['name']}\n**Artist:** {artist_names}\n\n" 921 | 922 | # Split the lyrics into multiple messages if necessary 923 | lyrics_chunks = [] 924 | current_chunk = "" 925 | section_lines = [] 926 | for line in lyrics.split('\n'): 927 | if re.match(SECTION_HEADER_PATTERN, line) or not section_lines: 928 | if section_lines: 929 | section_text = '\n'.join(section_lines) 930 | if len(current_chunk) + len(section_text) + 2 <= MAX_MESSAGE_LENGTH: 931 | current_chunk += section_text + "\n" 932 | else: 933 | lyrics_chunks.append(f"```{current_chunk.strip()}```") 934 | current_chunk = section_text + "\n" 935 | section_lines = [line] 936 | else: 937 | section_lines.append(line) 938 | 939 | # Add the last section to the chunks 940 | if section_lines: 941 | section_text = '\n'.join(section_lines) 942 | if len(current_chunk) + len(section_text) + 2 <= MAX_MESSAGE_LENGTH: 943 | current_chunk += section_text + "\n" 944 | else: 945 | lyrics_chunks.append(f"```{current_chunk.strip()}```") 946 | current_chunk = section_text + "\n" 947 | if current_chunk: 948 | lyrics_chunks.append(f"```{current_chunk.strip()}```") 949 | 950 | for i, chunk in enumerate(lyrics_chunks, start=1): 951 | page_header = f"Page {i}/{len(lyrics_chunks)}\n" 952 | if chunk == "``````": 953 | await waiting_message.delete() if waiting_message is not None else None 954 | error_message = "Sorry, I couldn't find the lyrics for this track." 955 | return await event.respond(error_message) 956 | message = metadata + chunk + page_header 957 | await event.respond(message, buttons=[Button.inline("Remove", data='cancel')]) 958 | else: 959 | await waiting_message.delete() 960 | error_message = "Sorry, I couldn't find the lyrics for this track." 961 | return await event.respond(error_message) 962 | 963 | @staticmethod 964 | async def send_music_icon(event): 965 | try: 966 | query_data = str(event.data) 967 | image_url = "https://i.scdn.co/image/" + query_data.split("/")[-1][:-1] 968 | await event.respond(file=image_url) 969 | except Exception: 970 | await event.reply("An error occurred while processing your request. Please try again later.") 971 | 972 | @staticmethod 973 | async def get_playlist_tracks(playlist_id, limit: int = 10, get_all: bool = False): 974 | 975 | # Retrieve playlist tracks 976 | if get_all: 977 | results = SpotifyDownloader.spotify_account.playlist_items(playlist_id) 978 | else: 979 | results = SpotifyDownloader.spotify_account.playlist_items(playlist_id, limit=limit) 980 | 981 | extracted_details = [] 982 | for item in results['items']: 983 | track = item['track'] 984 | # Extracting track name, artist's name, release year, and track ID 985 | track_name = track['name'] 986 | artist_name = track['artists'][0]['name'] # Assuming the first artist is the primary one 987 | release_year = track['album']['release_date'] 988 | track_id = track['id'] 989 | 990 | # Append the extracted details to the list 991 | extracted_details.append({ 992 | "track_name": track_name, 993 | "artist_name": artist_name, 994 | "release_year": release_year.split("-")[0], # Format release year as YYYY 995 | "track_id": track_id 996 | }) 997 | 998 | return extracted_details 999 | -------------------------------------------------------------------------------- /plugins/x.py: -------------------------------------------------------------------------------- 1 | from run import Button, BotState 2 | from utils import lru_cache 3 | from utils import os, hashlib, re, asyncio 4 | from utils import db, bs4, aiohttp 5 | from utils import TweetCapture 6 | 7 | 8 | class X: 9 | 10 | @classmethod 11 | def initialize(cls): 12 | cls.screen_shot_path = 'repository/ScreenShots' 13 | if not os.path.isdir(cls.screen_shot_path): 14 | os.makedirs(cls.screen_shot_path, exist_ok=True) 15 | 16 | @lru_cache(maxsize=128) # Cache the last 128 screenshots 17 | def get_screenshot_path(tweet_url): 18 | url_hash = hashlib.blake2b(tweet_url.encode()).hexdigest() 19 | filename = f"{url_hash}.png" 20 | return os.path.join(X.screen_shot_path, filename) 21 | 22 | @staticmethod 23 | async def take_screenshot_of_tweet(event, tweet_url): 24 | tweet_message = await event.respond( 25 | "Processing your request ...\nPlease wait while the screenshot is being generated.\nThis may take a few " 26 | "moments.") 27 | 28 | settings = await TweetCapture.get_settings(event.sender_id) 29 | night_mode = settings['night_mode'] 30 | 31 | screenshot_path = X.get_screenshot_path(tweet_url + night_mode) 32 | 33 | if os.path.exists(screenshot_path): 34 | await tweet_message.delete() 35 | return screenshot_path 36 | try: 37 | screenshot_task = asyncio.create_task(TweetCapture.screenshot(tweet_url, screenshot_path, night_mode)) 38 | await screenshot_task 39 | 40 | await tweet_message.delete() 41 | return screenshot_path 42 | 43 | except Exception as Err: 44 | await tweet_message.edit( 45 | f"We apologize for the inconvenience.\nThe requested tweet could not be found. Reason: {str(Err)}") 46 | return None 47 | 48 | @staticmethod 49 | async def send_screenshot(client, event, tweet_url) -> bool: 50 | 51 | screenshot_path = await X.take_screenshot_of_tweet(event, tweet_url) 52 | has_media = await X.has_media(tweet_url) 53 | 54 | screen_shot_message = await event.respond("Uploading the screenshot. Please stand by...") 55 | 56 | button = Button.inline("Download Media", 57 | data=f"X/dl/{tweet_url.replace('https://x.com/', '')}" 58 | ) if has_media else None 59 | try: 60 | await client.send_file( 61 | event.chat_id, 62 | screenshot_path, 63 | caption="Here is the requested tweet screenshot.", 64 | buttons=button 65 | ) 66 | except Exception as Err: 67 | await screen_shot_message.edit(f"Error:\n{str(Err)}") 68 | return False 69 | 70 | await screen_shot_message.delete() 71 | return True 72 | 73 | @staticmethod 74 | def contains_x_or_twitter_link(text): 75 | pattern = r'(https?://(?:www\.)?twitter\.com/[^/\s]+/status/\d+|https?://(?:www\.)?x\.com/[^/\s]+)' 76 | return bool(re.search(pattern, text)) 77 | 78 | @staticmethod 79 | def find_and_return_x_or_twitter_link(text): 80 | pattern = r'(https?://(?:www\.)?twitter\.com/[^/\s]+/status/\d+|https?://(?:www\.)?x\.com/[^?\s]+)' 81 | match = re.search(pattern, text) 82 | return match.group(0) if match else None 83 | 84 | @staticmethod 85 | def normalize_url(link): 86 | if "x.com" in link: 87 | return link.replace("x.com", "fxtwitter.com") 88 | elif "twitter.com" in link: 89 | return link.replace("twitter.com", "fxtwitter.com") 90 | return link 91 | 92 | @staticmethod 93 | async def has_media(link): 94 | normalized_link = X.normalize_url(link) 95 | 96 | try: 97 | async with aiohttp.ClientSession() as session: 98 | async with session.get(normalized_link) as response: 99 | if response.status == 200: 100 | html = await response.text() 101 | soup = bs4.BeautifulSoup(html, "lxml") 102 | meta_tag = soup.find("meta", attrs={"property": "og:video"}) 103 | if meta_tag: 104 | return True 105 | meta_tag = soup.find("meta", attrs={"property": "og:image"}) 106 | if meta_tag: 107 | return True 108 | return False 109 | else: 110 | print(f"Error fetching URL: {normalized_link} (status code: {response.status})") 111 | return False 112 | 113 | except Exception as e: 114 | print(f"Error checking media in URL: {e}") 115 | return False 116 | 117 | @staticmethod 118 | async def fetch_media_url(link): 119 | normalized_link = X.normalize_url(link) 120 | 121 | try: 122 | async with aiohttp.ClientSession() as session: 123 | async with session.get(normalized_link) as response: 124 | if response.status == 200: 125 | html = await response.text() 126 | soup = bs4.BeautifulSoup(html, "lxml") 127 | meta_tag = soup.find("meta", attrs={"property": "og:video"}) 128 | if meta_tag: 129 | return meta_tag['content'] 130 | meta_tag = soup.find("meta", attrs={"property": "og:image"}) 131 | if meta_tag: 132 | return meta_tag['content'] 133 | except Exception as e: 134 | print(f"Error fetching media URL: {e}") 135 | return None 136 | 137 | @staticmethod 138 | async def download(client, event): 139 | 140 | query_data = f"{event.data}" 141 | link = "https://x.com/" + query_data.split("X/dl/")[-1][:-1] 142 | media_url = await X.fetch_media_url(link) 143 | 144 | if media_url: 145 | try: 146 | upload_message = await event.reply("Uploading Media ... Please hold on.") 147 | await client.send_file(event.chat_id, media_url, 148 | caption="Thank you for using - @Spotify_YT_Downloader_Bot") 149 | await upload_message.delete() 150 | except Exception as e: 151 | print(f"Error sending file: {e}") 152 | await event.reply("Oops Invalid link or Media Is Not Available :(") 153 | else: 154 | await event.reply("Oops Invalid link or Media Is Not Available :(") 155 | -------------------------------------------------------------------------------- /plugins/youtube.py: -------------------------------------------------------------------------------- 1 | from utils import YoutubeDL, re, lru_cache, hashlib, InputMediaPhotoExternal, db 2 | from utils import os, InputMediaUploadedDocument, DocumentAttributeVideo, fast_upload 3 | from utils import DocumentAttributeAudio, DownloadError, WebpageMediaEmptyError 4 | from run import Button, Buttons 5 | 6 | 7 | class YoutubeDownloader: 8 | 9 | @classmethod 10 | def initialize(cls): 11 | cls.MAXIMUM_DOWNLOAD_SIZE_MB = 100 12 | cls.DOWNLOAD_DIR = 'repository/Youtube' 13 | 14 | if not os.path.isdir(cls.DOWNLOAD_DIR): 15 | os.mkdir(cls.DOWNLOAD_DIR) 16 | 17 | @lru_cache(maxsize=128) # Cache the last 128 screenshots 18 | def get_file_path(url, format_id, extension): 19 | url = url + format_id + extension 20 | url_hash = hashlib.blake2b(url.encode()).hexdigest() 21 | filename = f"{url_hash}.{extension}" 22 | return os.path.join(YoutubeDownloader.DOWNLOAD_DIR, filename) 23 | 24 | @staticmethod 25 | def is_youtube_link(url): 26 | youtube_patterns = [ 27 | r'(https?\:\/\/)?youtube\.com\/shorts\/([a-zA-Z0-9_-]{11}).*', 28 | r'(https?\:\/\/)?www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})(?!.*list=)', 29 | r'(https?\:\/\/)?youtu\.be\/([a-zA-Z0-9_-]{11})(?!.*list=)', 30 | r'(https?\:\/\/)?www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})(?!.*list=)', 31 | r'(https?\:\/\/)?www\.youtube\.com\/v\/([a-zA-Z0-9_-]{11})(?!.*list=)', 32 | r'(https?\:\/\/)?www\.youtube\.com\/[^\/]+\?v=([a-zA-Z0-9_-]{11})(?!.*list=)', 33 | ] 34 | for pattern in youtube_patterns: 35 | match = re.match(pattern, url) 36 | if match: 37 | return True 38 | return False 39 | 40 | @staticmethod 41 | def extract_youtube_url(text): 42 | # Regular expression patterns to match different types of YouTube URLs 43 | youtube_patterns = [ 44 | r'(https?\:\/\/)?youtube\.com\/shorts\/([a-zA-Z0-9_-]{11}).*', 45 | r'(https?\:\/\/)?www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})(?!.*list=)', 46 | r'(https?\:\/\/)?youtu\.be\/([a-zA-Z0-9_-]{11})(?!.*list=)', 47 | r'(https?\:\/\/)?www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})(?!.*list=)', 48 | r'(https?\:\/\/)?www\.youtube\.com\/v\/([a-zA-Z0-9_-]{11})(?!.*list=)', 49 | r'(https?\:\/\/)?www\.youtube\.com\/[^\/]+\?v=([a-zA-Z0-9_-]{11})(?!.*list=)', 50 | ] 51 | 52 | for pattern in youtube_patterns: 53 | match = re.search(pattern, text) 54 | if match: 55 | video_id = match.group(2) 56 | if 'youtube.com/shorts/' in match.group(0): 57 | return f'https://www.youtube.com/shorts/{video_id}' 58 | else: 59 | return f'https://www.youtube.com/watch?v={video_id}' 60 | 61 | return None 62 | 63 | @staticmethod 64 | def _get_formats(url): 65 | ydl_opts = { 66 | 'listformats': True, 67 | 'no_warnings': True, 68 | 'quiet': True, 69 | } 70 | 71 | with YoutubeDL(ydl_opts) as ydl: 72 | info = ydl.extract_info(url, download=False) 73 | 74 | formats = info['formats'] 75 | return formats 76 | 77 | @staticmethod 78 | async def send_youtube_info(client, event, youtube_link): 79 | url = youtube_link 80 | video_id = (youtube_link.split("?si=")[0] 81 | .replace("https://www.youtube.com/watch?v=", "") 82 | .replace("https://www.youtube.com/shorts/", "")) 83 | formats = YoutubeDownloader._get_formats(url) 84 | 85 | # Download the video thumbnail 86 | with YoutubeDL({'quiet': True}) as ydl: 87 | info = ydl.extract_info(url, download=False) 88 | thumbnail_url = info['thumbnail'] 89 | 90 | # Create buttons for each format 91 | video_formats = [f for f in formats if f.get('vcodec') != 'none' and f.get('acodec') != 'none'] 92 | audio_formats = [f for f in formats if f.get('acodec') != 'none' and f.get('vcodec') == 'none'] 93 | 94 | video_buttons = [] 95 | counter = 0 96 | for f in reversed(video_formats): 97 | extension = f['ext'] 98 | resolution = f.get('resolution') 99 | filesize = f.get('filesize') if f.get('filesize') is not None else f.get('filesize_approx') 100 | if resolution and filesize and counter < 5: 101 | filesize = f"{filesize / 1024 / 1024:.2f} MB" 102 | button_data = f"yt/dl/{video_id}/{extension}/{f['format_id']}/{filesize}" 103 | button = [Button.inline(f"{extension} - {resolution} - {filesize}", data=button_data)] 104 | if not button in video_buttons: 105 | video_buttons.append(button) 106 | counter += 1 107 | 108 | audio_buttons = [] 109 | counter = 0 110 | for f in reversed(audio_formats): 111 | extension = f['ext'] 112 | resolution = f.get('resolution') 113 | filesize = f.get('filesize') if f.get('filesize') is not None else f.get('filesize_approx') 114 | if resolution and filesize and counter < 5: 115 | filesize = f"{filesize / 1024 / 1024:.2f}MB" 116 | button_data = f"yt/dl/{video_id}/{extension}/{f['format_id']}/{filesize}" 117 | button = [Button.inline(f"{extension} - {resolution} - {filesize}", data=button_data)] 118 | if not button in audio_buttons: 119 | audio_buttons.append(button) 120 | counter += 1 121 | 122 | buttons = video_buttons + audio_buttons 123 | buttons.append(Buttons.cancel_button) 124 | 125 | # Set thumbnail attributes 126 | thumbnail = InputMediaPhotoExternal(thumbnail_url) 127 | thumbnail.ttl_seconds = 0 128 | 129 | # Send the thumbnail as a picture with format buttons 130 | try: 131 | await client.send_file( 132 | event.chat_id, 133 | file=thumbnail, 134 | caption="Select a format to download:", 135 | buttons=buttons 136 | ) 137 | except WebpageMediaEmptyError: 138 | await event.respond( 139 | "Select a format to download:", 140 | buttons=buttons 141 | ) 142 | 143 | 144 | @staticmethod 145 | async def download_and_send_yt_file(client, event): 146 | user_id = event.sender_id 147 | 148 | if await db.get_file_processing_flag(user_id): 149 | return await event.respond("Sorry, There is already a file being processed for you.") 150 | 151 | data = event.data.decode('utf-8') 152 | parts = data.split('/') 153 | if len(parts) == 6: 154 | extension = parts[3] 155 | format_id = parts[-2] 156 | filesize = parts[-1].replace("MB", "") 157 | video_id = parts[2] 158 | 159 | if float(filesize) > YoutubeDownloader.MAXIMUM_DOWNLOAD_SIZE_MB: 160 | return await event.answer( 161 | f"⚠️ The file size is more than {YoutubeDownloader.MAXIMUM_DOWNLOAD_SIZE_MB}MB." 162 | , alert=True) 163 | 164 | await db.set_file_processing_flag(user_id, is_processing=True) 165 | 166 | local_availability_message = None 167 | url = "https://www.youtube.com/watch?v=" + video_id 168 | 169 | path = YoutubeDownloader.get_file_path(url, format_id, extension) 170 | 171 | if not os.path.isfile(path): 172 | downloading_message = await event.respond("Downloading the file for you ...") 173 | ydl_opts = { 174 | 'format': format_id, 175 | 'outtmpl': path, 176 | 'quiet': True, 177 | } 178 | 179 | with YoutubeDL(ydl_opts) as ydl: 180 | try: 181 | info = ydl.extract_info(url, download=True) 182 | duration = info.get('duration', 0) 183 | width = info.get('width', 0) 184 | height = info.get('height', 0) 185 | except DownloadError as e: 186 | await db.set_file_processing_flag(user_id, is_processing=False) 187 | return await downloading_message.edit(f"Sorry Something went wrong:\nError:" 188 | f" {str(e).split('Error')[-1]}") 189 | await downloading_message.delete() 190 | else: 191 | local_availability_message = await event.respond( 192 | "This file is available locally. Preparing it for you now...") 193 | 194 | ydl_opts = { 195 | 'format': format_id, 196 | 'outtmpl': path, 197 | 'quiet': True, 198 | } 199 | with YoutubeDL(ydl_opts) as ydl: 200 | try: 201 | info = ydl.extract_info(url, download=False) 202 | duration = info.get('duration', 0) 203 | width = info.get('width', 0) 204 | height = info.get('height', 0) 205 | except DownloadError as e: 206 | await db.set_file_processing_flag(user_id, is_processing=False) 207 | 208 | upload_message = await event.respond("Uploading ... Please hold on.") 209 | 210 | try: 211 | # Indicate ongoing file upload to enhance user experience 212 | async with client.action(event.chat_id, 'document'): 213 | 214 | media = await fast_upload( 215 | client=client, 216 | file_location=path, 217 | reply=None, # No need for a progress bar in this case 218 | name=path, 219 | progress_bar_function=None 220 | ) 221 | 222 | if extension == "mp4": 223 | 224 | uploaded_file = await client.upload_file(media) 225 | 226 | # Prepare the video attributes 227 | video_attributes = DocumentAttributeVideo( 228 | duration=int(duration), 229 | w=int(width), 230 | h=int(height), 231 | supports_streaming=True, 232 | # Add other attributes as needed 233 | ) 234 | 235 | media = InputMediaUploadedDocument( 236 | file=uploaded_file, 237 | thumb=None, 238 | mime_type='video/mp4', 239 | attributes=[video_attributes], 240 | ) 241 | 242 | elif extension == "m4a" or extension == "webm": 243 | 244 | uploaded_file = await client.upload_file(media) 245 | 246 | # Prepare the audio attributes 247 | audio_attributes = DocumentAttributeAudio( 248 | duration=int(duration), 249 | title="Downloaded Audio", # Replace with actual title 250 | performer="@Spotify_YT_Downloader_BOT", # Replace with actual performer 251 | # Add other attributes as needed 252 | ) 253 | 254 | media = InputMediaUploadedDocument( 255 | file=uploaded_file, 256 | thumb=None, # Assuming you have a thumbnail or will set it later 257 | mime_type='audio/m4a' if extension == "m4a" else 'audio/webm', 258 | attributes=[audio_attributes], 259 | ) 260 | 261 | # Send the downloaded file 262 | await client.send_file(event.chat_id, file=media, 263 | caption=f"Enjoy!\n@Spotify_YT_Downloader_BOT", 264 | force_document=False, 265 | # This ensures the file is sent as a video/voice if possible 266 | supports_streaming=True # This enables video streaming 267 | ) 268 | 269 | await upload_message.delete() 270 | await local_availability_message.delete() if local_availability_message else None 271 | await db.set_file_processing_flag(user_id, is_processing=False) 272 | 273 | except Exception as Err: 274 | await db.set_file_processing_flag(user_id, is_processing=False) 275 | return await event.respond(f"Sorry There was a problem with your request.\nReason:{str(Err)}") 276 | else: 277 | await event.answer("Invalid button data.") 278 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pillow 2 | yt-dlp 3 | python-dotenv 4 | spotipy 5 | Telethon 6 | spotdl 7 | shazamio 8 | lyricsgenius 9 | aiosqlite 10 | wget 11 | FastTelethonhelper 12 | lxml 13 | webdriver_manager 14 | selenium -------------------------------------------------------------------------------- /run/__init__.py: -------------------------------------------------------------------------------- 1 | from telethon import TelegramClient, events 2 | from telethon.tl.custom import Button 3 | from telethon.tl.functions.channels import GetParticipantsRequest 4 | from telethon.tl.types import ChannelParticipantsSearch, MessageMediaDocument 5 | from telethon.errors import ChatAdminRequiredError 6 | from telethon.errors.rpcerrorlist import MessageNotModifiedError 7 | from .buttons import Buttons 8 | from .messages import BotMessageHandler 9 | from .glob_variables import BotState 10 | from .commands import BotCommandHandler 11 | from .version_checker import update_bot_version_user_season 12 | from .channel_checker import is_user_in_channel, handle_continue_in_membership_message, \ 13 | respond_based_on_channel_membership 14 | from .bot import Bot 15 | -------------------------------------------------------------------------------- /run/bot.py: -------------------------------------------------------------------------------- 1 | from utils import BroadcastManager, db, asyncio, sanitize_query, TweetCapture 2 | from plugins import SpotifyDownloader, ShazamHelper, X, Insta, YoutubeDownloader 3 | from run import events, Button, MessageMediaDocument, update_bot_version_user_season, is_user_in_channel, \ 4 | handle_continue_in_membership_message 5 | from run import Buttons, BotMessageHandler, BotState, BotCommandHandler, respond_based_on_channel_membership 6 | 7 | 8 | class Bot: 9 | Client = None 10 | 11 | @staticmethod 12 | async def initialize(): 13 | try: 14 | Bot.initialize_spotify_downloader() 15 | await Bot.initialize_database() 16 | Bot.initialize_shazam() 17 | Bot.initialize_x() 18 | Bot.initialize_instagram() 19 | Bot.initialize_youtube() 20 | Bot.initialize_messages() 21 | Bot.initialize_buttons() 22 | await Bot.initialize_action_queries() 23 | print("Bot initialization completed successfully.") 24 | except Exception as e: 25 | print(f"An error occurred during bot initialization: {str(e)}") 26 | 27 | @staticmethod 28 | def initialize_spotify_downloader(): 29 | try: 30 | SpotifyDownloader.initialize() 31 | print("Plugins: Spotify downloader initialized.") 32 | except Exception as e: 33 | print(f"An error occurred while initializing Spotify downloader: {str(e)}") 34 | 35 | @staticmethod 36 | async def initialize_database(): 37 | try: 38 | await db.initialize_database() 39 | await db.reset_all_file_processing_flags() 40 | print("Utils: Database initialized and file processing flags reset.") 41 | except Exception as e: 42 | print(f"An error occurred while initializing the database: {str(e)}") 43 | 44 | @staticmethod 45 | def initialize_shazam(): 46 | try: 47 | ShazamHelper.initialize() 48 | print("Plugins: Shazam initialized.") 49 | except Exception as e: 50 | print(f"An error occurred while initializing Shazam helper: {str(e)}") 51 | 52 | @staticmethod 53 | def initialize_x(): 54 | try: 55 | X.initialize() 56 | print("Plugins: X initialized.") 57 | except Exception as e: 58 | print(f"An error occurred while initializing X: {str(e)}") 59 | 60 | @staticmethod 61 | def initialize_instagram(): 62 | try: 63 | Insta.initialize() 64 | print("Plugins: Instagram initialized.") 65 | except Exception as e: 66 | print(f"An error occurred while initializing Instagram: {str(e)}") 67 | 68 | @staticmethod 69 | def initialize_youtube(): 70 | try: 71 | YoutubeDownloader.initialize() 72 | print("Plugins: Youtube downloader initialized.") 73 | except Exception as e: 74 | print(f"An error occurred while initializing Youtube downloader: {str(e)}") 75 | 76 | @classmethod 77 | def initialize_messages(cls): 78 | # Initialize messages here 79 | cls.start_message = BotMessageHandler.start_message 80 | cls.instruction_message = BotMessageHandler.instruction_message 81 | cls.search_result_message = BotMessageHandler.search_result_message 82 | cls.core_selection_message = BotMessageHandler.core_selection_message 83 | cls.JOIN_CHANNEL_MESSAGE = BotMessageHandler.JOIN_CHANNEL_MESSAGE 84 | cls.search_playlist_message = BotMessageHandler.search_playlist_message 85 | 86 | @classmethod 87 | def initialize_buttons(cls): 88 | # Initialize buttons here 89 | cls.main_menu_buttons = Buttons.main_menu_buttons 90 | cls.back_button = Buttons.back_button 91 | cls.setting_button = Buttons.setting_button 92 | cls.back_button_to_setting = Buttons.back_button_to_setting 93 | cls.cancel_broadcast_button = Buttons.cancel_broadcast_button 94 | cls.admins_buttons = Buttons.admins_buttons 95 | cls.broadcast_options_buttons = Buttons.broadcast_options_buttons 96 | 97 | @classmethod 98 | async def initialize_action_queries(cls): 99 | # Mapping button actions to functions 100 | cls.button_actions = { 101 | b"membership/continue": lambda e: asyncio.create_task(handle_continue_in_membership_message(e)), 102 | b"instructions": lambda e: asyncio.create_task( 103 | BotMessageHandler.edit_message(e, Bot.instruction_message, buttons=Bot.back_button)), 104 | b"back": lambda e: asyncio.create_task( 105 | BotMessageHandler.edit_message(e, f"Hey {e.sender.first_name}!👋\n {Bot.start_message}", 106 | buttons=Bot.main_menu_buttons)), 107 | b"setting": lambda e: asyncio.create_task( 108 | BotMessageHandler.edit_message(e, "Settings :", buttons=Bot.setting_button)), 109 | b"setting/back": lambda e: asyncio.create_task( 110 | BotMessageHandler.edit_message(e, "Settings :", buttons=Bot.setting_button)), 111 | b"setting/quality": lambda e: asyncio.create_task(BotMessageHandler.edit_quality_setting_message(e)), 112 | b"setting/quality/mp3/320": lambda e: asyncio.create_task(Bot.change_music_quality(e, "mp3", "320")), 113 | b"setting/quality/mp3/128": lambda e: asyncio.create_task(Bot.change_music_quality(e, "mp3", "128")), 114 | b"setting/quality/flac": lambda e: asyncio.create_task(Bot.change_music_quality(e, "flac", "693")), 115 | b"setting/core": lambda e: asyncio.create_task(BotMessageHandler.edit_core_setting_message(e)), 116 | b"setting/core/auto": lambda e: asyncio.create_task(Bot.change_downloading_core(e, "Auto")), 117 | b"setting/core/spotdl": lambda e: asyncio.create_task(Bot.change_downloading_core(e, "SpotDL")), 118 | b"setting/core/youtubedl": lambda e: asyncio.create_task(Bot.change_downloading_core(e, "YoutubeDL")), 119 | b"setting/subscription": lambda e: asyncio.create_task( 120 | BotMessageHandler.edit_subscription_status_message(e)), 121 | b"setting/subscription/cancel": lambda e: asyncio.create_task(Bot.cancel_subscription(e)), 122 | b"setting/subscription/cancel/quite": lambda e: asyncio.create_task(Bot.cancel_subscription(e, quite=True)), 123 | b"setting/subscription/add": lambda e: asyncio.create_task(Bot.add_subscription(e)), 124 | b"setting/TweetCapture": lambda e: asyncio.create_task( 125 | BotMessageHandler.edit_tweet_capture_setting_message(e)), 126 | b"setting/TweetCapture/mode/0": lambda e: asyncio.create_task(Bot.change_tweet_capture_night_mode(e, "0")), 127 | b"setting/TweetCapture/mode/1": lambda e: asyncio.create_task(Bot.change_tweet_capture_night_mode(e, "1")), 128 | b"setting/TweetCapture/mode/2": lambda e: asyncio.create_task(Bot.change_tweet_capture_night_mode(e, "2")), 129 | b"cancel": lambda e: e.delete(), 130 | b"admin/cancel_broadcast": lambda e: asyncio.create_task(BotState.set_admin_broadcast(e.sender_id, False)), 131 | b"admin/stats": lambda e: asyncio.create_task(BotCommandHandler.handle_stats_command(e)), 132 | b"admin/broadcast": lambda e: asyncio.create_task( 133 | BotMessageHandler.edit_message(e, "BroadCast Options: ", buttons=Bot.broadcast_options_buttons)), 134 | b"admin/broadcast/all": lambda e: asyncio.create_task(Bot.handle_broadcast(e, send_to_all=True)), 135 | b"admin/broadcast/subs": lambda e: asyncio.create_task(Bot.handle_broadcast(e, send_to_subs=True)), 136 | b"admin/broadcast/specified": lambda e: asyncio.create_task( 137 | Bot.handle_broadcast(e, send_to_specified=True)), 138 | b"unavailable_feature": lambda e: asyncio.create_task(Bot.handle_unavailable_feature(e)) 139 | # Add other actions here 140 | } 141 | 142 | @staticmethod 143 | async def change_music_quality(event, format, quality): 144 | user_id = event.sender_id 145 | music_quality = {'format': format, 'quality': quality} 146 | await db.set_user_music_quality(user_id, music_quality) 147 | await BotMessageHandler.edit_message(event, 148 | f"Quality successfully changed.\n\nFormat: {music_quality['format']}" 149 | f"\nQuality: {music_quality['quality']}", 150 | buttons=Buttons.get_quality_setting_buttons(music_quality)) 151 | 152 | @staticmethod 153 | async def change_downloading_core(event, downloading_core): 154 | user_id = event.sender_id 155 | await db.set_user_downloading_core(user_id, downloading_core) 156 | await BotMessageHandler.edit_message(event, f"Core successfully changed.\n\nCore: {downloading_core}", 157 | buttons=Buttons.get_core_setting_buttons(downloading_core)) 158 | 159 | @staticmethod 160 | async def change_tweet_capture_night_mode(event, mode: str): 161 | user_id = event.sender_id 162 | await TweetCapture.set_settings(user_id, {'night_mode': mode}) 163 | mode_to_show = "Light" 164 | match mode: 165 | case "1": 166 | mode_to_show = "Dark" 167 | case "2": 168 | mode_to_show = "Black" 169 | await BotMessageHandler.edit_message(event, f"Night mode successfully changed.\n\nNight mode: {mode_to_show}", 170 | buttons=Buttons.get_tweet_capture_setting_buttons(mode)) 171 | 172 | @staticmethod 173 | async def cancel_subscription(event, quite: bool = False): 174 | user_id = event.sender_id 175 | if await db.is_user_subscribed(user_id): 176 | await db.remove_subscribed_user(user_id) 177 | if not quite: 178 | await BotMessageHandler.edit_message(event, "You have successfully unsubscribed.", 179 | buttons=Buttons.get_subscription_setting_buttons( 180 | subscription=False)) 181 | else: 182 | await event.respond( 183 | "You have successfully unsubscribed.\nYou Can Subscribe Any Time Using /subscribe command. :)") 184 | 185 | @staticmethod 186 | async def add_subscription(event): 187 | user_id = event.sender_id 188 | if not await db.is_user_subscribed(user_id): 189 | await db.add_subscribed_user(user_id) 190 | await BotMessageHandler.edit_message(event, "You have successfully subscribed.", 191 | buttons=Buttons.get_subscription_setting_buttons(subscription=True)) 192 | 193 | @staticmethod 194 | async def process_bot_interaction(event) -> bool: 195 | user_id = event.sender_id 196 | await update_bot_version_user_season(event) 197 | if not await db.get_user_updated_flag(user_id): 198 | return False 199 | 200 | channels_user_is_not_in = await is_user_in_channel(user_id) 201 | if channels_user_is_not_in != [] and (user_id not in BotState.ADMIN_USER_IDS): 202 | return await respond_based_on_channel_membership(event, None, None, channels_user_is_not_in) 203 | 204 | if await BotState.get_admin_broadcast(user_id) and await BotState.get_send_to_specified_flag(user_id): 205 | await BotState.set_admin_message_to_send(user_id, event.message) 206 | return False 207 | elif await BotState.get_admin_broadcast(user_id): 208 | await BotState.set_admin_message_to_send(user_id, event.message) 209 | return False 210 | return True 211 | 212 | @staticmethod 213 | async def handle_broadcast(e, send_to_all: bool = False, send_to_subs: bool = False, 214 | send_to_specified: bool = False): 215 | 216 | user_id = e.sender_id 217 | if user_id not in BotState.ADMIN_USER_IDS: 218 | return 219 | 220 | if send_to_specified: 221 | await BotState.set_send_to_specified_flag(user_id, True) 222 | 223 | await BotState.set_admin_broadcast(user_id, True) 224 | if send_to_all: 225 | await BroadcastManager.add_all_users_to_temp() 226 | 227 | elif send_to_specified: 228 | await BroadcastManager.remove_all_users_from_temp() 229 | time = 60 230 | time_to_send = await e.respond("Please enter the user_ids (comma-separated) within the next 60 seconds.", 231 | buttons=Bot.cancel_broadcast_button) 232 | 233 | for remaining_time in range(time - 1, 0, -1): 234 | # Edit the message to show the new time 235 | await time_to_send.edit(f"You've Got {remaining_time} seconds to send the user ids seperated with:") 236 | if await BotState.get_admin_broadcast(user_id): 237 | await time_to_send.edit("BroadCast Cancelled by User.", buttons=None) 238 | await BotState.set_send_to_specified_flag(user_id, False) 239 | await BotState.set_admin_message_to_send(user_id, None) 240 | await BotState.set_admin_broadcast(user_id, False) 241 | return 242 | elif await BotState.get_admin_message_to_send(user_id) is not None: 243 | break 244 | await asyncio.sleep(1) 245 | await BotState.set_send_to_specified_flag(user_id, False) 246 | try: 247 | parts = await BotState.get_admin_message_to_send(user_id) 248 | parts = parts.message.replace(" ", "").split(",") 249 | user_ids = [int(part) for part in parts] 250 | for user_id in user_ids: 251 | await BroadcastManager.add_user_to_temp(user_id) 252 | except: 253 | await time_to_send.edit("Invalid command format. Use user_id1,user_id2,...") 254 | await BotState.set_admin_message_to_send(user_id, None) 255 | await BotState.set_admin_broadcast(user_id, False) 256 | return 257 | await BotState.set_admin_message_to_send(user_id, None) 258 | 259 | time = 60 260 | time_to_send = await e.respond(f"You've Got {time} seconds to send your message", 261 | buttons=Bot.cancel_broadcast_button) 262 | 263 | for remaining_time in range(time - 1, 0, -1): 264 | # Edit the message to show the new time 265 | await time_to_send.edit(f"You've Got {remaining_time} seconds to send your message") 266 | if not await BotState.get_admin_broadcast(user_id): 267 | await time_to_send.edit("BroadCast Cancelled by User.", buttons=None) 268 | break 269 | elif await BotState.get_admin_message_to_send(user_id) is not None: 270 | break 271 | await asyncio.sleep(1) 272 | 273 | if await BotState.get_admin_message_to_send(user_id) is None and await BotState.get_admin_broadcast(user_id): 274 | await e.respond("There is nothing to send") 275 | await BotState.set_admin_broadcast(user_id, False) 276 | await BotState.set_admin_message_to_send(user_id, None) 277 | await BroadcastManager.remove_all_users_from_temp() 278 | return 279 | 280 | try: 281 | if await BotState.get_admin_broadcast(user_id) and send_to_specified: 282 | await BroadcastManager.broadcast_message_to_temp_members(Bot.Client, 283 | await BotState.get_admin_message_to_send( 284 | user_id)) 285 | await e.respond("Broadcast initiated.") 286 | elif await BotState.get_admin_broadcast(user_id) and send_to_subs: 287 | await BroadcastManager.broadcast_message_to_sub_members(Bot.Client, 288 | await BotState.get_admin_message_to_send( 289 | user_id), 290 | Buttons.cancel_subscription_button_quite) 291 | await e.respond("Broadcast initiated.") 292 | elif await BotState.get_admin_broadcast(user_id) and send_to_all: 293 | await BroadcastManager.broadcast_message_to_temp_members(Bot.Client, 294 | await BotState.get_admin_message_to_send( 295 | user_id)) 296 | await e.respond("Broadcast initiated.") 297 | except Exception as e: 298 | await e.respond(f"Broadcast Failed: {str(e)}") 299 | await BotState.set_admin_broadcast(user_id, False) 300 | await BotState.set_admin_message_to_send(user_id, None) 301 | await BroadcastManager.remove_all_users_from_temp() 302 | 303 | await BroadcastManager.remove_all_users_from_temp() 304 | await BotState.set_admin_broadcast(user_id, False) 305 | await BotState.set_admin_message_to_send(user_id, None) 306 | 307 | @staticmethod 308 | async def process_audio_file(event, user_id): 309 | if not await Bot.process_bot_interaction(event): 310 | return 311 | 312 | waiting_message_search = await event.respond('⏳') 313 | process_file_message = await event.respond("Processing Your File ...") 314 | 315 | file_path = await event.message.download_media(file=f"{ShazamHelper.voice_repository_dir}") 316 | shazam_recognized = await ShazamHelper.recognize(file_path) 317 | if not shazam_recognized: 318 | await waiting_message_search.delete() 319 | await process_file_message.delete() 320 | await event.respond("Sorry I Couldnt find any song that matches your Voice.") 321 | return 322 | 323 | sanitized_query = await sanitize_query(shazam_recognized) 324 | if not sanitized_query: 325 | await waiting_message_search.delete() 326 | await event.respond("Sorry I Couldnt find any song that matches your Voice.") 327 | return 328 | 329 | search_result = await SpotifyDownloader.search_spotify_based_on_user_input(sanitized_query, limit=10) 330 | button_list = Buttons.get_search_result_buttons(sanitized_query, search_result) 331 | 332 | try: 333 | await event.respond(Bot.search_result_message, buttons=button_list) 334 | except Exception as Err: 335 | await event.respond(f"Sorry There Was an Error Processing Your Request: {str(Err)}") 336 | 337 | await process_file_message.delete() 338 | await waiting_message_search.delete() 339 | 340 | @staticmethod 341 | async def process_spotify_link(event): 342 | if not await Bot.process_bot_interaction(event): 343 | return 344 | 345 | waiting_message = await event.respond('⏳') 346 | info_tuple = await SpotifyDownloader.download_and_send_spotify_info(event, is_query=False) 347 | 348 | if not info_tuple: # if getting info of the link failed 349 | await waiting_message.delete() 350 | return await event.respond("Sorry, There was a problem processing your request.") 351 | 352 | @staticmethod 353 | async def process_text_query(event): 354 | if not await Bot.process_bot_interaction(event): 355 | return 356 | 357 | if len(event.message.text) > 33: 358 | return await event.respond("Your Search Query is too long. :(") 359 | 360 | waiting_message_search = await event.respond('⏳') 361 | sanitized_query = await sanitize_query(event.message.text) 362 | if not sanitized_query: 363 | await event.respond("Your input was not valid. Please try again with a valid search term.") 364 | return 365 | 366 | search_result = await SpotifyDownloader.search_spotify_based_on_user_input(sanitized_query, limit=10) 367 | button_list = Buttons.get_search_result_buttons(sanitized_query, search_result) 368 | 369 | try: 370 | await event.respond(Bot.search_result_message, buttons=button_list) 371 | except Exception as Err: 372 | await event.respond(f"Sorry There Was an Error Processing Your Request: {str(Err)}") 373 | 374 | await waiting_message_search.delete() 375 | 376 | @staticmethod 377 | async def handle_next_prev_page(event): 378 | query_data = str(event.data) 379 | is_playlist = True if query_data.split("/")[1] == "p" else False 380 | current_page = query_data.split("/page/")[-1][:-1] 381 | 382 | search_query = query_data.split("/")[2] 383 | 384 | if current_page == "0" or (current_page == "6" and is_playlist): 385 | return await event.answer("⚠️ Not available.") 386 | 387 | if is_playlist: 388 | search_result = await SpotifyDownloader.get_playlist_tracks(search_query, 389 | limit=int(current_page) * 10) 390 | button_list = Buttons.get_playlist_search_buttons(search_query, search_result, page=int(current_page)) 391 | else: 392 | search_result = await SpotifyDownloader.search_spotify_based_on_user_input(search_query, 393 | limit=int(current_page) * 10) 394 | button_list = Buttons.get_search_result_buttons(search_query, search_result, page=int(current_page)) 395 | 396 | try: 397 | await event.edit(buttons=button_list) 398 | except: 399 | await event.answer("⚠️ Not available.") 400 | 401 | @staticmethod 402 | async def process_x_or_twitter_link(event): 403 | if not await Bot.process_bot_interaction(event): 404 | return 405 | 406 | x_link = X.find_and_return_x_or_twitter_link(event.message.text) 407 | if x_link: 408 | return await X.send_screenshot(Bot.Client, event, x_link) 409 | 410 | @staticmethod 411 | async def process_youtube_link(event): 412 | if not await Bot.process_bot_interaction(event): 413 | return 414 | 415 | waiting_message = await event.respond('⏳') 416 | 417 | youtube_link = YoutubeDownloader.extract_youtube_url(event.message.text) 418 | if not youtube_link: 419 | return await event.respond("Sorry, Bad Youtube Link.") 420 | await YoutubeDownloader.send_youtube_info(Bot.Client, event, youtube_link) 421 | await waiting_message.delete() 422 | 423 | @staticmethod 424 | async def handle_unavailable_feature(event): 425 | await event.answer("not available", alert=True) 426 | 427 | @staticmethod 428 | async def search_inside_playlist(event): 429 | 430 | query_data = str(event.data) 431 | playlist_id = query_data.split("/playlist/")[-1][:-1] 432 | 433 | waiting_message_search = await event.respond('⏳') 434 | search_result = await SpotifyDownloader.get_playlist_tracks(playlist_id) 435 | button_list = Buttons.get_playlist_search_buttons(playlist_id, search_result) 436 | 437 | try: 438 | await event.respond(Bot.search_result_message, buttons=button_list) 439 | except Exception as Err: 440 | await event.respond(f"Sorry There Was an Error Processing Your Request: {str(Err)}") 441 | 442 | await asyncio.sleep(1.5) 443 | await waiting_message_search.delete() 444 | 445 | @staticmethod 446 | async def handle_spotify_callback(event): 447 | handlers = { 448 | "spotify/dl/icon/": SpotifyDownloader.send_music_icon, 449 | "spotify/dl/30s_preview": SpotifyDownloader.send_30s_preview, 450 | "spotify/artist/": SpotifyDownloader.send_artists_info, 451 | "spotify/lyrics": SpotifyDownloader.send_music_lyrics, 452 | "spotify/dl/playlist/": SpotifyDownloader.download_spotify_file_and_send, 453 | "spotify/s/playlist/": Bot.search_inside_playlist, 454 | "spotify/dl/music/": SpotifyDownloader.download_spotify_file_and_send, 455 | "spotify/info/": SpotifyDownloader.download_and_send_spotify_info, 456 | } 457 | 458 | for key, handler in handlers.items(): 459 | if event.data.startswith(key.encode()): 460 | await handler(event) 461 | break 462 | else: 463 | pass 464 | 465 | @staticmethod 466 | async def handle_youtube_callback(client, event): 467 | if event.data.startswith(b"yt/dl/"): 468 | await YoutubeDownloader.download_and_send_yt_file(client, event) 469 | 470 | @staticmethod 471 | async def handle_x_callback(client, event): 472 | if event.data.startswith(b"X/dl"): 473 | await X.download(client, event) 474 | else: 475 | pass # Add another x callbacks here 476 | 477 | @staticmethod 478 | async def callback_query_handler(event): 479 | user_id = event.sender_id 480 | await update_bot_version_user_season(event) 481 | if not await db.get_user_updated_flag(user_id): 482 | return 483 | 484 | action = Bot.button_actions.get(event.data) 485 | if action: 486 | await action(event) 487 | elif event.data.startswith(b"spotify"): 488 | await Bot.handle_spotify_callback(event) 489 | elif event.data.startswith(b"yt"): 490 | await Bot.handle_youtube_callback(Bot.Client, event) 491 | elif event.data.startswith(b"X"): 492 | await Bot.handle_x_callback(Bot.Client, event) 493 | elif event.data.startswith(b"next_page") or event.data.startswith(b"prev_page"): 494 | await Bot.handle_next_prev_page(event) 495 | else: 496 | pass 497 | 498 | @staticmethod 499 | async def handle_message(event): 500 | user_id = event.sender_id 501 | 502 | if isinstance(event.message.media, MessageMediaDocument): 503 | if event.message.media.voice: 504 | await Bot.process_audio_file(event, user_id) 505 | else: 506 | await event.respond("Sorry, I can only process:\n-Text\n-Voice\n-Link") 507 | elif YoutubeDownloader.is_youtube_link(event.message.text): 508 | await Bot.process_youtube_link(event) 509 | elif SpotifyDownloader.is_spotify_link(event.message.text): 510 | await Bot.process_spotify_link(event) 511 | elif X.contains_x_or_twitter_link(event.message.text): 512 | await Bot.process_x_or_twitter_link(event) 513 | elif Insta.is_instagram_url(event.message.text): 514 | await Insta.download(Bot.Client, event) 515 | elif not event.message.text.startswith('/'): 516 | await Bot.process_text_query(event) 517 | 518 | @staticmethod 519 | async def run(): 520 | Bot.Client = await BotState.BOT_CLIENT.start(bot_token=BotState.BOT_TOKEN) 521 | 522 | # Register event handlers 523 | Bot.Client.add_event_handler(BotCommandHandler.start, events.NewMessage(pattern='/start')) 524 | 525 | Bot.Client.add_event_handler(BotCommandHandler.handle_broadcast_command, 526 | events.NewMessage(pattern='/broadcast')) 527 | 528 | Bot.Client.add_event_handler(BotCommandHandler.handle_settings_command, events.NewMessage(pattern='/settings')) 529 | 530 | Bot.Client.add_event_handler(BotCommandHandler.handle_subscribe_command, 531 | events.NewMessage(pattern='/subscribe')) 532 | 533 | Bot.Client.add_event_handler(BotCommandHandler.handle_unsubscribe_command, 534 | events.NewMessage(pattern='/unsubscribe')) 535 | 536 | Bot.Client.add_event_handler(BotCommandHandler.handle_help_command, events.NewMessage(pattern='/help')) 537 | Bot.Client.add_event_handler(BotCommandHandler.handle_quality_command, events.NewMessage(pattern='/quality')) 538 | Bot.Client.add_event_handler(BotCommandHandler.handle_core_command, events.NewMessage(pattern='/core')) 539 | Bot.Client.add_event_handler(BotCommandHandler.handle_admin_command, events.NewMessage(pattern='/admin')) 540 | Bot.Client.add_event_handler(BotCommandHandler.handle_stats_command, events.NewMessage(pattern='/stats')) 541 | Bot.Client.add_event_handler(BotCommandHandler.handle_ping_command, events.NewMessage(pattern='/ping')) 542 | Bot.Client.add_event_handler(BotCommandHandler.handle_search_command, events.NewMessage(pattern='/search')) 543 | 544 | Bot.Client.add_event_handler(BotCommandHandler.handle_user_info_command, 545 | events.NewMessage(pattern='/user_info')) 546 | 547 | Bot.Client.add_event_handler(Bot.callback_query_handler, events.CallbackQuery) 548 | Bot.Client.add_event_handler(Bot.handle_message, events.NewMessage) 549 | 550 | await Bot.Client.run_until_disconnected() 551 | -------------------------------------------------------------------------------- /run/buttons.py: -------------------------------------------------------------------------------- 1 | from run import Button 2 | 3 | 4 | class Buttons: 5 | source_code_button = [ 6 | Button.url("Source Code", url="https://github.com/AdibNikjou/MusicDownloader-Telegram-Bot")] 7 | 8 | main_menu_buttons = [ 9 | [Button.inline("Instructions", b"instructions"), Button.inline("Settings", b"setting")], 10 | source_code_button, 11 | [Button.url("Contact Creator", url="telegram.me/adibnikjou")], 12 | ] 13 | 14 | back_button = Button.inline("<< Back To Main Menu", b"back") 15 | 16 | setting_button = [ 17 | [Button.inline("Core", b"setting/core")], 18 | [Button.inline("Quality", b"setting/quality")], 19 | [Button.inline("TweetCapture", b"setting/TweetCapture")], 20 | [Button.inline("Subscription", b"setting/subscription")], 21 | [back_button] 22 | ] 23 | 24 | back_button_to_setting = Button.inline("<< Back", b"setting/back") 25 | 26 | cancel_broadcast_button = [Button.inline("Cancel BroadCast", data=b"admin/cancel_broadcast")] 27 | 28 | admins_buttons = [ 29 | [Button.inline("Broadcast", b"admin/broadcast")], 30 | [Button.inline("Stats", b"admin/stats")], 31 | [Button.inline("Cancel", b"cancel")] 32 | ] 33 | 34 | broadcast_options_buttons = [ 35 | [Button.inline("Broadcast To All Members", b"admin/broadcast/all")], 36 | [Button.inline("Broadcast To Subscribers Only", b"admin/broadcast/subs")], 37 | [Button.inline("Broadcast To Specified Users Only", b"admin/broadcast/specified")], 38 | [Button.inline("Cancel", b"cancel")] 39 | ] 40 | 41 | continue_button = [Button.inline("Continue", data='membership/continue')] 42 | 43 | cancel_subscription_button_quite = [Button.inline("UnSubscribe", b"setting/subscription/cancel/quite")] 44 | 45 | cancel_button = [Button.inline("Cancel", b"cancel")] 46 | 47 | @staticmethod 48 | def get_tweet_capture_setting_buttons(mode): 49 | match mode: 50 | case "0": 51 | return [ 52 | [Button.inline("🔹 Light mode", data=b"setting/TweetCapture/mode/0")], 53 | [Button.inline("Dark mode", data=b"setting/TweetCapture/mode/1")], 54 | [Button.inline("Black mode", data=b"setting/TweetCapture/mode/2")], 55 | [Buttons.back_button, Buttons.back_button_to_setting] 56 | ] 57 | case "1": 58 | return [ 59 | [Button.inline("Light mode", data=b"setting/TweetCapture/mode/0")], 60 | [Button.inline("🔹 Dark mode", data=b"setting/TweetCapture/mode/1")], 61 | [Button.inline("Black mode", data=b"setting/TweetCapture/mode/2")], 62 | [Buttons.back_button, Buttons.back_button_to_setting] 63 | ] 64 | case "2": 65 | return [ 66 | [Button.inline("Light mode", data=b"setting/TweetCapture/mode/0")], 67 | [Button.inline("Dark mode", data=b"setting/TweetCapture/mode/1")], 68 | [Button.inline("🔹 Black mode", data=b"setting/TweetCapture/mode/2")], 69 | [Buttons.back_button, Buttons.back_button_to_setting] 70 | ] 71 | 72 | @staticmethod 73 | def get_subscription_setting_buttons(subscription): 74 | if subscription: 75 | return [ 76 | [Button.inline("UnSubscribe", data=b"setting/subscription/cancel")], 77 | [Buttons.back_button, Buttons.back_button_to_setting] 78 | ] 79 | else: 80 | return [ 81 | [Button.inline("Subscribe", data=b"setting/subscription/add")], 82 | [Buttons.back_button, Buttons.back_button_to_setting] 83 | ] 84 | 85 | @staticmethod 86 | def get_core_setting_buttons(core): 87 | match core: 88 | case "Auto": 89 | return [ 90 | [Button.inline("🔸 Auto", data=b"setting/core/auto")], 91 | [Button.inline("YoutubeDL", b"setting/core/youtubedl")], 92 | [Button.inline("SpotDL", b"setting/core/spotdl")], 93 | [Buttons.back_button, Buttons.back_button_to_setting], 94 | ] 95 | case "SpotDL": 96 | return [ 97 | [Button.inline("Auto", data=b"setting/core/auto")], 98 | [Button.inline("YoutubeDL", b"setting/core/youtubedl")], 99 | [Button.inline("🔸 SpotDL", b"setting/core/spotdl")], 100 | [Buttons.back_button, Buttons.back_button_to_setting], 101 | ] 102 | case "YoutubeDL": 103 | return [ 104 | [Button.inline("Auto", data=b"setting/core/auto")], 105 | [Button.inline("🔸 YoutubeDL", b"setting/core/youtubedl")], 106 | [Button.inline("SpotDL", b"setting/core/spotdl")], 107 | [Buttons.back_button, Buttons.back_button_to_setting], 108 | ] 109 | 110 | @staticmethod 111 | def get_quality_setting_buttons(music_quality): 112 | if isinstance(music_quality['quality'], int): 113 | music_quality['quality'] = str(music_quality['quality']) 114 | 115 | match music_quality: 116 | case {'format': 'flac', 'quality': "693"}: 117 | return [ 118 | [Button.inline("◽️ Flac", b"setting/quality/flac")], 119 | [Button.inline("Mp3 (320)", b"setting/quality/mp3/320")], 120 | [Button.inline("Mp3 (128)", b"setting/quality/mp3/128")], 121 | [Buttons.back_button, Buttons.back_button_to_setting], 122 | ] 123 | 124 | case {'format': "mp3", 'quality': "320"}: 125 | return [ 126 | [Button.inline("Flac", b"setting/quality/flac")], 127 | [Button.inline("◽️ Mp3 (320)", b"setting/quality/mp3/320")], 128 | [Button.inline("Mp3 (128)", b"setting/quality/mp3/128")], 129 | [Buttons.back_button, Buttons.back_button_to_setting], 130 | ] 131 | 132 | case {'format': "mp3", 'quality': "128"}: 133 | return [ 134 | [Button.inline("Flac", b"setting/quality/flac")], 135 | [Button.inline("Mp3 (320)", b"setting/quality/mp3/320")], 136 | [Button.inline("◽️ Mp3 (128)", b"setting/quality/mp3/128")], 137 | [Buttons.back_button, Buttons.back_button_to_setting], 138 | ] 139 | 140 | @staticmethod 141 | def get_search_result_buttons(sanitized_query, search_result, page=1) -> list: 142 | 143 | button_list = [ 144 | [Button.inline(f"🎧 {details['track_name']} - {details['artist_name']} 🎧 ({details['release_year']})", 145 | data=f"spotify/info/{details['track_id']}")] 146 | for details in search_result[(page-1) * 10:] 147 | ] 148 | 149 | if len(search_result) > 1: 150 | button_list.append([Button.inline("Previous Page", f"prev_page/s/{sanitized_query}/page/{page - 1}"), 151 | Button.inline("Next Page", f"next_page/s/{sanitized_query}/page/{page + 1}")]) 152 | button_list.append([Button.inline("Cancel", b"cancel")]) 153 | 154 | return button_list 155 | 156 | @staticmethod 157 | def get_playlist_search_buttons(playlist_id, search_result, page=1) -> list: 158 | button_list = [ 159 | [Button.inline(f"🎧 {details['track_name']} - {details['artist_name']} 🎧 ({details['release_year']})", 160 | data=f"spotify/info/{details['track_id']}")] 161 | for details in search_result[(page-1) * 10:] 162 | ] 163 | 164 | if len(search_result) > 1: 165 | button_list.append([Button.inline("Previous Page", f"prev_page/p/{playlist_id}/page/{page - 1}"), 166 | Button.inline("Next Page", f"next_page/p/{playlist_id}/page/{page + 1}")]) 167 | button_list.append([Button.inline("Cancel", b"cancel")]) 168 | 169 | return button_list 170 | -------------------------------------------------------------------------------- /run/channel_checker.py: -------------------------------------------------------------------------------- 1 | from .glob_variables import BotState 2 | from run import GetParticipantsRequest, ChannelParticipantsSearch, ChatAdminRequiredError, Button 3 | from .buttons import Buttons 4 | from .messages import BotMessageHandler 5 | from utils import db 6 | 7 | 8 | async def is_user_in_channel(user_id, channel_usernames=None): 9 | if channel_usernames is None: 10 | channel_usernames = BotState.channel_usernames 11 | channels_user_is_not_in = [] 12 | 13 | for channel_username in channel_usernames: 14 | channel = await BotState.BOT_CLIENT.get_entity(channel_username) 15 | offset = 0 16 | while True: 17 | try: 18 | participants = await BotState.BOT_CLIENT(GetParticipantsRequest( 19 | channel, 20 | ChannelParticipantsSearch(''), # Search query, empty for all participants 21 | offset=offset, # Providing the offset 22 | limit=10 ** 9, # Adjust the limit as needed 23 | hash=0 24 | )) 25 | except ChatAdminRequiredError: 26 | print(f"ChatAdminRequiredError: Bot does not have admin privileges in {channel_username}.") 27 | break 28 | if not participants.users: 29 | break # No more participants to fetch 30 | if not any(participant.id == user_id for participant in participants.users): 31 | channels_user_is_not_in.append(channel_username) 32 | break # User found, no need to check other channels 33 | 34 | offset += len(participants.users) # Increment offset for the next batch 35 | return channels_user_is_not_in 36 | 37 | 38 | def join_channel_button(channel_username): 39 | """ 40 | Returns a Button object that, when clicked, directs users to join the specified channel. 41 | """ 42 | return Button.url("Join Channel", f"https://t.me/{channel_username}") 43 | 44 | 45 | async def respond_based_on_channel_membership(event, message_if_in_channels: str = None, buttons: str = None, 46 | channels_user_is_not_in: list = None): 47 | sender_name = event.sender.first_name 48 | user_id = event.sender_id 49 | buttons_if_in_channel = buttons 50 | 51 | channels_user_is_not_in = await is_user_in_channel( 52 | user_id) if channels_user_is_not_in is None else channels_user_is_not_in 53 | 54 | if channels_user_is_not_in != [] and (user_id not in BotState.ADMIN_USER_IDS): 55 | join_channel_buttons = [[join_channel_button(channel)] for channel in channels_user_is_not_in] 56 | join_channel_buttons.append(Buttons.continue_button) 57 | await BotMessageHandler.send_message(event, 58 | f"""Hey {sender_name}!👋 \n{BotMessageHandler.JOIN_CHANNEL_MESSAGE}""", 59 | buttons=join_channel_buttons) 60 | elif message_if_in_channels is not None or (user_id in BotState.ADMIN_USER_IDS): 61 | await BotMessageHandler.send_message(event, f"""{message_if_in_channels}""", 62 | buttons=buttons_if_in_channel) 63 | 64 | 65 | async def handle_continue_in_membership_message(event): 66 | sender_name = event.sender.first_name 67 | user_id = event.sender_id 68 | channels_user_is_not_in = await is_user_in_channel(user_id) 69 | if channels_user_is_not_in != []: 70 | join_channel_buttons = [[join_channel_button(channel)] for channel in channels_user_is_not_in] 71 | join_channel_buttons.append(Buttons.continue_button) 72 | await BotMessageHandler.edit_message(event, 73 | f"""Hey {sender_name}!👋 \n{BotMessageHandler.JOIN_CHANNEL_MESSAGE}""", 74 | buttons=join_channel_buttons) 75 | await event.answer("⚠️ You need to join our channels to continue.") 76 | else: 77 | user_already_in_db = await db.check_username_in_database(user_id) 78 | if not user_already_in_db: 79 | await db.create_user_settings(user_id) 80 | await event.delete() 81 | await respond_based_on_channel_membership(event, f"""Hey {sender_name}!👋 \n{BotMessageHandler.start_message}""", 82 | buttons=Buttons.main_menu_buttons) 83 | -------------------------------------------------------------------------------- /run/commands.py: -------------------------------------------------------------------------------- 1 | from plugins import SpotifyDownloader 2 | from utils import db, asyncio, BroadcastManager, time 3 | from utils import sanitize_query 4 | from .glob_variables import BotState 5 | from .buttons import Buttons 6 | from .messages import BotMessageHandler 7 | from .channel_checker import respond_based_on_channel_membership 8 | from .version_checker import update_bot_version_user_season 9 | 10 | ADMIN_USER_IDS = BotState.ADMIN_USER_IDS 11 | BOT_CLIENT = BotState.BOT_CLIENT 12 | 13 | 14 | class BotCommandHandler: 15 | 16 | @staticmethod 17 | async def start(event): 18 | sender_name = event.sender.first_name 19 | user_id = event.sender_id 20 | 21 | user_already_in_db = await db.check_username_in_database(user_id) 22 | if not user_already_in_db: 23 | await db.create_user_settings(user_id) 24 | await respond_based_on_channel_membership(event, f"""Hey {sender_name}!👋 \n{BotMessageHandler.start_message}""", 25 | buttons=Buttons.main_menu_buttons) 26 | 27 | @staticmethod 28 | async def handle_stats_command(event): 29 | if event.sender_id not in ADMIN_USER_IDS: 30 | return 31 | number_of_users = await db.count_all_user_ids() 32 | number_of_subscribed = await db.count_subscribed_users() 33 | number_of_unsubscribed = number_of_users - number_of_subscribed 34 | await event.respond(f"""Number of Users: {number_of_users} 35 | Number of Subscribed Users: {number_of_subscribed} 36 | Number of Unsubscribed Users: {number_of_unsubscribed}""") 37 | 38 | @staticmethod 39 | async def handle_admin_command(event): 40 | if event.sender_id not in ADMIN_USER_IDS: 41 | return 42 | await BotMessageHandler.send_message(event, "Admin commands:", buttons=Buttons.admins_buttons) 43 | 44 | @staticmethod 45 | async def handle_ping_command(event): 46 | start_time = time.time() 47 | ping_message = await event.reply('Pong!') 48 | end_time = time.time() 49 | response_time = (end_time - start_time) * 1000 50 | await ping_message.edit(f'Pong!\nResponse time: {response_time:3.3f} ms') 51 | 52 | @staticmethod 53 | async def handle_core_command(event): 54 | if await update_bot_version_user_season(event): 55 | user_id = event.sender_id 56 | downloading_core = await db.get_user_downloading_core(user_id) 57 | await respond_based_on_channel_membership(event, 58 | BotMessageHandler.core_selection_message + f"\nCore: {downloading_core}", 59 | buttons=Buttons.get_core_setting_buttons(downloading_core)) 60 | 61 | @staticmethod 62 | async def handle_quality_command(event): 63 | if await update_bot_version_user_season(event): 64 | user_id = event.sender_id 65 | music_quality = await db.get_user_music_quality(user_id) 66 | await respond_based_on_channel_membership(event, 67 | f"Your Quality Setting:\nFormat: {music_quality['format']}\nQuality: {music_quality['quality']}\n\nQualities Available :", 68 | buttons=Buttons.get_quality_setting_buttons(music_quality)) 69 | 70 | @staticmethod 71 | async def handle_help_command(event): 72 | if await update_bot_version_user_season(event): 73 | user_id = event.sender_id 74 | if await db.get_user_updated_flag(user_id): 75 | await respond_based_on_channel_membership(event, BotMessageHandler.instruction_message, 76 | buttons=Buttons.back_button) 77 | 78 | @staticmethod 79 | async def handle_unsubscribe_command(event): 80 | # Check if the user is subscribed 81 | if await update_bot_version_user_season(event): 82 | user_id = event.sender_id 83 | if not await db.is_user_subscribed(user_id): 84 | await respond_based_on_channel_membership(event, "You are not currently subscribed.") 85 | return 86 | await db.remove_subscribed_user(user_id) 87 | await respond_based_on_channel_membership(event, "You have successfully unsubscribed.") 88 | 89 | @staticmethod 90 | async def handle_subscribe_command(event): 91 | # Check if the user is already subscribed 92 | if await update_bot_version_user_season(event): 93 | user_id = event.sender_id 94 | if await db.is_user_subscribed(user_id): 95 | await respond_based_on_channel_membership(event, "You are already subscribed.") 96 | return 97 | await db.add_subscribed_user(user_id) 98 | await respond_based_on_channel_membership(event, "You have successfully subscribed.") 99 | 100 | @staticmethod 101 | async def handle_settings_command(event): 102 | if await update_bot_version_user_season(event): 103 | await respond_based_on_channel_membership(event, "Settings :", buttons=Buttons.setting_button) 104 | 105 | @staticmethod 106 | async def handle_broadcast_command(event): 107 | 108 | user_id = event.sender_id 109 | if user_id not in ADMIN_USER_IDS: 110 | return 111 | 112 | await BotState.set_admin_broadcast(user_id, True) 113 | if event.message.text.startswith('/broadcast_to_all'): 114 | await BroadcastManager.add_all_users_to_temp() 115 | 116 | elif event.message.text.startswith('/broadcast'): 117 | command_parts = event.message.text.split(' ', 1) 118 | 119 | if len(command_parts) == 1: 120 | pass 121 | elif len(command_parts) < 2 or not command_parts[1].startswith('(') or not command_parts[1].endswith(')'): 122 | await event.respond("Invalid command format. Use /broadcast (user_id1,user_id2,...)") 123 | await BotState.set_admin_broadcast(user_id, False) 124 | await BotState.set_admin_message_to_send(user_id, None) 125 | return 126 | 127 | if len(command_parts) != 1: 128 | await BroadcastManager.remove_all_users_from_temp() 129 | user_ids_str = command_parts[1][1:-1] # Remove the parentheses 130 | specified_user_ids = [int(user_id) for user_id in user_ids_str.split(',')] 131 | for user_id in specified_user_ids: 132 | await BroadcastManager.add_user_to_temp(user_id) 133 | await BotState.set_admin_message_to_send(user_id, None) 134 | time = 60 135 | time_to_send = await event.respond(f"You've Got {time} seconds to send your message", 136 | buttons=Buttons.cancel_broadcast_button) 137 | 138 | for remaining_time in range(time - 1, 0, -1): 139 | # Edit the message to show the new time 140 | await time_to_send.edit(f"You've Got {remaining_time} seconds to send your message") 141 | if not await BotState.get_admin_broadcast(user_id): 142 | await time_to_send.edit("BroadCast Cancelled by User.", buttons=None) 143 | break 144 | elif await BotState.get_admin_message_to_send(user_id) is not None: 145 | break 146 | await asyncio.sleep(1) 147 | 148 | # Check if the message is "/broadcast_to_all" 149 | if await BotState.get_admin_message_to_send(user_id) is None and await BotState.get_admin_broadcast(user_id): 150 | await event.respond("There is nothing to send") 151 | await BotState.set_admin_broadcast(user_id, False) 152 | await BotState.set_admin_message_to_send(user_id, None) 153 | await BroadcastManager.remove_all_users_from_temp() 154 | return 155 | 156 | try: 157 | if await BotState.get_admin_broadcast(user_id) and len(command_parts) != 1: 158 | await BroadcastManager.broadcast_message_to_temp_members(BOT_CLIENT, 159 | await BotState.get_admin_message_to_send( 160 | user_id)) 161 | await event.respond("Broadcast initiated.") 162 | elif await BotState.get_admin_broadcast(user_id) and len(command_parts) == 1: 163 | await BroadcastManager.broadcast_message_to_sub_members(BOT_CLIENT, 164 | await BotState.get_admin_message_to_send( 165 | user_id), 166 | Buttons.cancel_subscription_button_quite) 167 | await event.respond("Broadcast initiated.") 168 | except: 169 | try: 170 | if await BotState.get_admin_broadcast(user_id): 171 | await BroadcastManager.broadcast_message_to_temp_members(BOT_CLIENT, 172 | await BotState.get_admin_message_to_send( 173 | user_id)) 174 | await event.respond("Broadcast initiated.") 175 | except Exception as e: 176 | await event.respond(f"Broadcast Failed: {str(e)}") 177 | await BotState.set_admin_broadcast(user_id, False) 178 | await BotState.set_admin_message_to_send(user_id, None) 179 | await BroadcastManager.remove_all_users_from_temp() 180 | 181 | await BroadcastManager.remove_all_users_from_temp() 182 | await BotState.set_admin_broadcast(user_id, False) 183 | await BotState.set_admin_message_to_send(user_id, None) 184 | 185 | @staticmethod 186 | async def handle_search_command(event): 187 | if not await update_bot_version_user_season(event): 188 | return event.respond("We have updated the bot.\n" 189 | "please start over using /start command.") 190 | 191 | search_query = event.message.text[8:] 192 | 193 | if not search_query.strip(): 194 | await event.respond( 195 | "Please provide a search term after the /search command. \nOr simply send me everything you want " 196 | "to Search for.") 197 | return 198 | 199 | waiting_message_search = await event.respond('⏳') 200 | sanitized_query = await sanitize_query(search_query) 201 | if not sanitized_query: 202 | await event.respond("Your input was not valid. Please try again with a valid search term.") 203 | return 204 | 205 | search_result = await SpotifyDownloader.search_spotify_based_on_user_input(sanitized_query) 206 | if len(search_result) == 0: 207 | await waiting_message_search.delete() 208 | await event.respond("Sorry, I couldnt Find any music that matches your Search query.") 209 | return 210 | 211 | button_list = Buttons.get_search_result_buttons(sanitized_query, search_result) 212 | 213 | try: 214 | await event.respond(BotMessageHandler.search_result_message, buttons=button_list) 215 | except Exception as Err: 216 | await event.respond(f"Sorry There Was an Error Processing Your Request: {str(Err)}") 217 | 218 | await asyncio.sleep(1.5) 219 | await waiting_message_search.delete() 220 | 221 | @staticmethod 222 | async def handle_user_info_command(event): 223 | if await update_bot_version_user_season(event): 224 | user_id = event.sender_id 225 | username = f"@{event.sender.username}" if event.sender.username else "No username" 226 | first_name = event.sender.first_name 227 | last_name = event.sender.last_name if event.sender.last_name else "No last name" 228 | is_bot = event.sender.bot 229 | is_verified = event.sender.verified 230 | is_restricted = event.sender.restricted 231 | is_scam = event.sender.scam 232 | is_support = event.sender.support 233 | 234 | # Prepare the user information message 235 | user_info_message = f""" 236 | User Information: 237 | 238 | ID: {user_id} 239 | Username: {username} 240 | 241 | First Name: {first_name} 242 | Last Name: {last_name} 243 | 244 | Is Bot: {is_bot} 245 | Is Verified: {is_verified} 246 | Is Restricted: {is_restricted} 247 | Is Scam: {is_scam} 248 | Is Support: {is_support} 249 | 250 | """ 251 | # Send the user information to the user 252 | await event.reply(user_info_message) 253 | -------------------------------------------------------------------------------- /run/glob_variables.py: -------------------------------------------------------------------------------- 1 | from utils import os, load_dotenv, dataclass, field, asyncio 2 | from telethon import TelegramClient 3 | 4 | 5 | @dataclass 6 | class UserState: 7 | admin_message_to_send: str = None 8 | admin_broadcast: bool = False 9 | send_to_specified_flag: bool = False 10 | search_result: str = None 11 | 12 | 13 | class BotState: 14 | channel_usernames = ["Spotify_yt_downloader"] 15 | user_states = {} 16 | lock = asyncio.Lock() 17 | 18 | load_dotenv('config.env') 19 | 20 | BOT_TOKEN = os.getenv('BOT_TOKEN') 21 | API_ID = os.getenv("API_ID") 22 | API_HASH = os.getenv("API_HASH") 23 | 24 | ADMIN_USER_IDS = os.getenv('ADMIN_USER_IDS').split(',') 25 | 26 | if not all([BOT_TOKEN, API_ID, API_HASH, ADMIN_USER_IDS]): 27 | raise ValueError("Required environment variables are missing.") 28 | 29 | ADMIN_USER_IDS = [int(id) for id in ADMIN_USER_IDS] 30 | BOT_CLIENT = TelegramClient('bot', int(API_ID), API_HASH) 31 | 32 | # @staticmethod #[DEPRECATED] 33 | # def initialize_user_state(user_id): 34 | # if user_id not in BotState.user_states: 35 | # BotState.user_states[user_id] = { 36 | # 'admin_message_to_send': None, 37 | # 'admin_broadcast': False, 38 | # 'send_to_specified_flag': False, 39 | # 'messages': {},no 40 | # 'search_result': None, 41 | # 'tweet_screenshot': None, 42 | # 'youtube_search': None, 43 | # 'waiting_message': None 44 | # } 45 | 46 | @staticmethod 47 | async def initialize_user_state(user_id): 48 | if user_id not in BotState.user_states: 49 | BotState.user_states[user_id] = UserState() 50 | 51 | @staticmethod 52 | async def get_user_state(user_id): 53 | async with BotState.lock: 54 | await BotState.initialize_user_state(user_id) 55 | return BotState.user_states[user_id] 56 | 57 | @staticmethod 58 | async def get_admin_message_to_send(user_id): 59 | user_state = await BotState.get_user_state(user_id) 60 | return user_state.admin_message_to_send 61 | 62 | @staticmethod 63 | async def get_admin_broadcast(user_id): 64 | user_state = await BotState.get_user_state(user_id) 65 | return user_state.admin_broadcast 66 | 67 | @staticmethod 68 | async def get_send_to_specified_flag(user_id): 69 | user_state = await BotState.get_user_state(user_id) 70 | return user_state.send_to_specified_flag 71 | 72 | @staticmethod 73 | async def set_admin_message_to_send(user_id, message): 74 | user_state = await BotState.get_user_state(user_id) 75 | user_state.admin_message_to_send = message 76 | 77 | @staticmethod 78 | async def set_admin_broadcast(user_id, value): 79 | user_state = await BotState.get_user_state(user_id) 80 | user_state.admin_broadcast = value 81 | 82 | @staticmethod 83 | async def set_send_to_specified_flag(user_id, value): 84 | user_state = await BotState.get_user_state(user_id) 85 | user_state.send_to_specified_flag = value 86 | -------------------------------------------------------------------------------- /run/messages.py: -------------------------------------------------------------------------------- 1 | from .glob_variables import BotState 2 | from .buttons import Buttons 3 | from utils import db, TweetCapture 4 | from telethon.errors.rpcerrorlist import MessageNotModifiedError 5 | 6 | 7 | class BotMessageHandler: 8 | start_message = """ 9 | Welcome to your **Music Downloader!** 🎧 10 | 11 | Send me the name of a song or artist, and I'll find and send you the downloadable track. 🎶 12 | 13 | To see what I can do, type: /help 14 | Or simply click the Instructions button below. 👇 15 | """ 16 | 17 | instruction_message = """ 18 | 🎧 Music Downloader 🎧 19 | 20 | 1. Share Spotify/YouTube song link 🔗 21 | 2. Wait for download confirmation 📣 22 | 3. Receive song file 💾 23 | 4. Or send voice message with song sample 24 | for best match and details 🎤🔍📩 25 | 5. Ask for lyrics, artist info, etc. 📜👨‍🎤 26 | 27 | 💡 Tip: Search by title, lyrics, or other details! 28 | 29 | 📺 YouTube Downloader 📺 30 | 31 | 1. Send YouTube video link 🔗 32 | 2. Choose video quality (if prompted) 🎥 33 | 3. Wait for download ⏳ 34 | 4. Receive video file 📤 35 | 36 | 📸 Instagram Downloader 📸 37 | 38 | 1. Send Instagram post/Reel/IGTV link 🔗 39 | 2. Wait for download ⏳ 40 | 3. Receive file 📤 41 | 42 | 🐦 TweetCapture 🐦 43 | 44 | 1. Provide tweet link 🔗 45 | 2. Wait for screenshot 📸 46 | 3. Receive screenshot 🖼️ 47 | 4. For media content, use "Download Media" 48 | button after getting screenshot 📥 49 | 50 | Questions? Ask @adibnikjou 51 | """ 52 | 53 | search_result_message = """🎵 The following are the top search results that correspond to your query: 54 | """ 55 | 56 | core_selection_message = """🎵 Choose Your Preferred Download Core 🎵 57 | 58 | """ 59 | JOIN_CHANNEL_MESSAGE = """It seems you are not a member of our channel yet. 60 | Please join to continue.""" 61 | 62 | search_playlist_message = """The playlist contains these songs:""" 63 | 64 | @staticmethod 65 | async def send_message(event, text, buttons=None): 66 | chat_id = event.chat_id 67 | user_id = event.sender_id 68 | await BotState.initialize_user_state(user_id) 69 | await BotState.BOT_CLIENT.send_message(chat_id, text, buttons=buttons) 70 | 71 | @staticmethod 72 | async def edit_message(event, message_text, buttons=None): 73 | user_id = event.sender_id 74 | 75 | await BotState.initialize_user_state(user_id) 76 | try: 77 | await event.edit(message_text, buttons=buttons) 78 | except MessageNotModifiedError: 79 | pass 80 | 81 | @staticmethod 82 | async def edit_quality_setting_message(e): 83 | music_quality = await db.get_user_music_quality(e.sender_id) 84 | if music_quality: 85 | message = (f"Your Quality Setting:\nFormat: {music_quality['format']}\nQuality: {music_quality['quality']}" 86 | f"\n\nAvailable Qualities :") 87 | else: 88 | message = "No quality settings found." 89 | await BotMessageHandler.edit_message(e, message, buttons=Buttons.get_quality_setting_buttons(music_quality)) 90 | 91 | @staticmethod 92 | async def edit_core_setting_message(e): 93 | downloading_core = await db.get_user_downloading_core(e.sender_id) 94 | if downloading_core: 95 | message = BotMessageHandler.core_selection_message + f"\nCore: {downloading_core}" 96 | else: 97 | message = BotMessageHandler.core_selection_message + "\nNo core setting found." 98 | await BotMessageHandler.edit_message(e, message, buttons=Buttons.get_core_setting_buttons(downloading_core)) 99 | 100 | @staticmethod 101 | async def edit_subscription_status_message(e): 102 | is_subscribed = await db.is_user_subscribed(e.sender_id) 103 | message = f"Subscription settings:\n\nYour Subscription Status: {is_subscribed}" 104 | await BotMessageHandler.edit_message(e, message, 105 | buttons=Buttons.get_subscription_setting_buttons(is_subscribed)) 106 | 107 | @staticmethod 108 | async def edit_tweet_capture_setting_message(e): 109 | night_mode = await TweetCapture.get_settings(e.sender_id) 110 | mode = night_mode['night_mode'] 111 | mode_to_show = "Light" 112 | match mode: 113 | case "1": 114 | mode_to_show = "Dark" 115 | case "2": 116 | mode_to_show = "Black" 117 | message = f"Tweet capture settings:\n\nYour Night Mode: {mode_to_show}" 118 | await BotMessageHandler.edit_message(e, message, buttons=Buttons.get_tweet_capture_setting_buttons(mode)) 119 | -------------------------------------------------------------------------------- /run/version_checker.py: -------------------------------------------------------------------------------- 1 | from utils import db 2 | 3 | 4 | async def update_bot_version_user_season(event) -> bool: 5 | user_id = event.sender_id 6 | if not await db.check_username_in_database(user_id): 7 | await event.respond("We Have Updated The Bot, Please start Over using the /start command.") 8 | await db.set_user_updated_flag(user_id, 0) 9 | return False 10 | await db.set_user_updated_flag(user_id, 1) 11 | return True 12 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from utils.broadcast import BroadcastManager 2 | from utils.database import db 3 | from spotipy.oauth2 import SpotifyClientCredentials 4 | from yt_dlp.utils import DownloadError 5 | from dotenv import load_dotenv 6 | from itertools import combinations 7 | from PIL import Image 8 | from io import BytesIO 9 | from yt_dlp import YoutubeDL 10 | from shazamio import Shazam 11 | import requests, asyncio, re, os 12 | import bs4, wget, hashlib, time 13 | import lyricsgenius 14 | import spotipy 15 | from concurrent.futures import ThreadPoolExecutor 16 | import aiohttp 17 | from telethon import sync 18 | from telethon.tl.functions.messages import SendMediaRequest 19 | from telethon.tl.types import (InputMediaUploadedDocument, 20 | DocumentAttributeAudio, 21 | InputMediaPhotoExternal, 22 | DocumentAttributeVideo) 23 | from FastTelethonhelper import fast_upload 24 | from threading import Thread 25 | import concurrent 26 | from functools import lru_cache, partial 27 | from .tweet_capture import TweetCapture 28 | from .helper import sanitize_query 29 | import io 30 | import sys 31 | from dataclasses import dataclass, field 32 | from spotipy.exceptions import SpotifyException 33 | from typing import Tuple, Any 34 | from telethon.errors.rpcerrorlist import WebpageMediaEmptyError -------------------------------------------------------------------------------- /utils/broadcast.py: -------------------------------------------------------------------------------- 1 | from utils.database import db 2 | 3 | 4 | class BroadcastManager: 5 | 6 | @staticmethod 7 | async def broadcast_message_to_sub_members(client, message, button=None): 8 | """ 9 | Sends a message to all users in the broadcast list. 10 | """ 11 | user_ids = await db.get_subscribed_user_ids() 12 | for user_id in user_ids: 13 | try: 14 | await client.send_message(user_id, message, buttons=button) 15 | except Exception as e: 16 | print(f"Failed to send message to user {user_id}: {e}") 17 | # Optionally, retry sending the message or log the failure for later review 18 | 19 | @staticmethod 20 | async def broadcast_message_to_temp_members(client, message): 21 | """ 22 | Sends a message to all users in the broadcast list. 23 | """ 24 | user_ids = await db.get_temporary_subscribed_user_ids() 25 | for user_id in user_ids: 26 | try: 27 | await client.send_message(user_id, message) 28 | except Exception as e: 29 | print(f"Failed to send message to user {user_id}: {e}") 30 | # Optionally, retry sending the message or log the failure for later review 31 | 32 | @staticmethod 33 | async def add_sub_user(user_id): # check 34 | """ 35 | Adds a user to the broadcast list. 36 | """ 37 | await db.add_subscribed_user(user_id) # Removed 'await' 38 | 39 | @staticmethod 40 | async def remove_sub_user(user_id): # check 41 | """ 42 | Removes a user from the broadcast list. 43 | """ 44 | await db.remove_subscribed_user(user_id) # Removed 'await' 45 | 46 | @staticmethod 47 | async def get_all_sub_user_ids(): 48 | """ 49 | Returns all user IDs in the broadcast list. 50 | """ 51 | return await db.get_subscribed_user_ids() 52 | 53 | @staticmethod 54 | async def clear_user_ids(): 55 | """ 56 | Clears the broadcast list by removing all user IDs. 57 | """ 58 | await db.clear_subscribed_users() 59 | 60 | @staticmethod 61 | async def get_temporary_subscribed_user_ids(): # check 62 | """ 63 | Returns all user IDs in the subscriptions list that are marked as temporarily subscribed. 64 | """ 65 | return await db.get_temporary_subscribed_user_ids() 66 | 67 | @staticmethod 68 | async def add_all_users_to_temp(): 69 | """ 70 | Adds all users from the database to the broadcast list. 71 | """ 72 | # Mark the current subscribed users as temporarily added for the broadcast 73 | await db.mark_temporary_subscriptions() 74 | 75 | @staticmethod 76 | async def remove_all_users_from_temp(): 77 | """ 78 | Remove all users from the database to the broadcast list. 79 | """ 80 | # Mark the current subscribed users as temporarily added for the broadcast 81 | await db.mark_temporary_unsubscriptions() 82 | 83 | @staticmethod 84 | async def add_user_to_temp(user_id): 85 | """ 86 | add a user from the database to the broadcast list. 87 | """ 88 | await db.add_user_to_temp(user_id) 89 | -------------------------------------------------------------------------------- /utils/database.py: -------------------------------------------------------------------------------- 1 | import aiosqlite 2 | import json 3 | import asyncio 4 | 5 | 6 | class ConnectionPool: 7 | def __init__(self, db_name, max_connections=100): 8 | self.db_name = db_name 9 | self.max_connections = max_connections 10 | self.pool = asyncio.Queue() 11 | 12 | async def get_connection(self): 13 | if self.pool.empty(): 14 | conn = await aiosqlite.connect(self.db_name) 15 | else: 16 | conn = await self.pool.get() 17 | return conn 18 | 19 | async def release_connection(self, conn): 20 | if self.pool.qsize() < self.max_connections: 21 | await self.pool.put(conn) 22 | else: 23 | await conn.close() 24 | 25 | 26 | class db: 27 | db_name = 'user_settings.db' 28 | pool = ConnectionPool(db_name) 29 | lock = asyncio.Lock() 30 | 31 | @staticmethod 32 | async def initialize_database(): 33 | conn = await db.get_connection() 34 | try: 35 | await conn.execute('''CREATE TABLE IF NOT EXISTS user_settings 36 | (user_id INTEGER PRIMARY KEY, music_quality TEXT, downloading_core TEXT, 37 | tweet_capture_settings TEXT, 38 | is_file_processing BOOLEAN DEFAULT 0,is_user_updated BOOLEAN DEFAULT 1)''' 39 | ) 40 | await conn.execute('''CREATE TABLE IF NOT EXISTS subscriptions 41 | (user_id INTEGER PRIMARY KEY, subscribed BOOLEAN DEFAULT 1, temporary BOOLEAN DEFAULT 0)''') 42 | await conn.execute('''CREATE TABLE IF NOT EXISTS musics 43 | (filename TEXT PRIMARY KEY, downloads INTEGER DEFAULT 1)''') 44 | await conn.commit() 45 | except: 46 | raise 47 | await db.create_trigger() 48 | await db.set_default_values() 49 | 50 | @classmethod 51 | async def set_default_values(cls): 52 | cls.default_downloading_core: str = "Auto" 53 | cls.default_music_quality: dict = {'format': 'flac', 'quality': '693'} 54 | cls.default_tweet_capture_setting: dict = {'night_mode': '0'} 55 | 56 | @staticmethod 57 | async def get_connection(): 58 | return await db.pool.get_connection() 59 | 60 | @staticmethod 61 | async def release_connection(conn): 62 | await db.pool.release_connection(conn) 63 | 64 | @staticmethod 65 | async def execute_query(query, params=()): 66 | async with db.lock: 67 | conn = await db.get_connection() 68 | try: 69 | async with conn.cursor() as c: 70 | await c.execute(query, params) 71 | await conn.commit() 72 | except aiosqlite.OperationalError as e: 73 | if 'database is locked' in str(e): 74 | await db.execute_query(query, params) 75 | else: 76 | raise e 77 | finally: 78 | await db.release_connection(conn) 79 | 80 | @staticmethod 81 | async def fetch_one(query, params=()): 82 | async with db.lock: 83 | conn = await db.get_connection() 84 | try: 85 | async with conn.cursor() as c: 86 | try: 87 | await c.execute(query, params) 88 | return await c.fetchone() 89 | except Exception as e: 90 | print(f"Error executing query: {query}") 91 | print(f"Parameters: {params}") 92 | print(f"Error details: {e}") 93 | raise e 94 | finally: 95 | await db.release_connection(conn) 96 | 97 | @staticmethod 98 | async def fetch_all(query, params=()): 99 | async with db.lock: 100 | conn = await db.get_connection() 101 | try: 102 | async with conn.cursor() as c: 103 | await c.execute(query, params) 104 | return await c.fetchall() 105 | finally: 106 | await db.release_connection(conn) 107 | 108 | @staticmethod 109 | async def create_trigger(): 110 | await db.execute_query('DROP TRIGGER IF EXISTS add_user_to_subscriptions') 111 | trigger_sql = ''' 112 | CREATE TRIGGER add_user_to_subscriptions 113 | AFTER INSERT ON user_settings 114 | BEGIN 115 | INSERT INTO subscriptions (user_id, subscribed, temporary) 116 | VALUES (NEW.user_id, 1, 0); 117 | END; 118 | ''' 119 | await db.execute_query(trigger_sql) 120 | 121 | @staticmethod 122 | async def create_user_settings(user_id): 123 | music_quality = await db.get_user_music_quality(user_id) 124 | music_quality = json.dumps(music_quality) if music_quality != {} else json.dumps(db.default_music_quality) 125 | 126 | downloading_core = await db.get_user_downloading_core(user_id) 127 | downloading_core = downloading_core if downloading_core else db.default_downloading_core 128 | 129 | tweet_capture_setting = await db.get_user_tweet_capture_settings(user_id) 130 | tweet_capture_setting = json.dumps(tweet_capture_setting) if tweet_capture_setting != {} else json.dumps( 131 | db.default_tweet_capture_setting) 132 | 133 | await db.execute_query('''INSERT OR REPLACE INTO user_settings 134 | (user_id, music_quality, downloading_core, tweet_capture_settings) VALUES (?, ?, ?, ?)''', 135 | (user_id, music_quality, downloading_core, tweet_capture_setting)) 136 | 137 | @staticmethod 138 | async def check_username_in_database(user_id): 139 | query = "SELECT COUNT(*) FROM user_settings WHERE user_id = ?" 140 | result = await db.fetch_one(query, (user_id,)) 141 | if result: 142 | count = result[0] 143 | if count > 0 and await db.get_user_downloading_core(user_id) is not None: 144 | if await db.get_user_music_quality(user_id) != {} and await db.get_user_tweet_capture_settings( 145 | user_id) != {}: 146 | return True 147 | else: 148 | return False 149 | 150 | @staticmethod 151 | async def get_user_music_quality(user_id): 152 | result = await db.fetch_one('SELECT music_quality FROM user_settings WHERE user_id = ?', (user_id,)) 153 | if result: 154 | music_quality = json.loads(result[0]) 155 | return music_quality 156 | else: 157 | return {} 158 | 159 | @staticmethod 160 | async def get_user_downloading_core(user_id): 161 | result = await db.fetch_one('SELECT downloading_core FROM user_settings WHERE user_id = ?', (user_id,)) 162 | if result: 163 | return result[0] 164 | else: 165 | return None 166 | 167 | @staticmethod 168 | async def set_user_music_quality(user_id, music_quality): 169 | serialized_dict = json.dumps(music_quality) 170 | await db.execute_query('UPDATE user_settings SET music_quality = ? WHERE user_id = ?', 171 | (serialized_dict, user_id)) 172 | 173 | @staticmethod 174 | async def set_user_downloading_core(user_id, downloading_core): 175 | await db.execute_query('UPDATE user_settings SET downloading_core = ? WHERE user_id = ?', 176 | (downloading_core, user_id)) 177 | 178 | @staticmethod 179 | async def get_all_user_ids(): 180 | return [row[0] for row in await db.fetch_all('SELECT user_id FROM user_settings')] 181 | 182 | @staticmethod 183 | async def count_all_user_ids(): 184 | return (await db.fetch_one('SELECT COUNT(*) FROM user_settings'))[0] 185 | 186 | @staticmethod 187 | async def add_user_to_temp(user_id): 188 | await db.execute_query('''UPDATE subscriptions SET temporary = 1 WHERE user_id = ?''', (user_id,)) 189 | 190 | @staticmethod 191 | async def remove_user_from_temp(user_id): 192 | await db.execute_query('''UPDATE subscriptions SET temporary = 0 WHERE user_id = ?''', (user_id,)) 193 | 194 | @staticmethod 195 | async def add_subscribed_user(user_id): 196 | await db.execute_query('''UPDATE subscriptions SET subscribed = 1 WHERE user_id = ?''', (user_id,)) 197 | 198 | @staticmethod 199 | async def remove_subscribed_user(user_id): 200 | await db.execute_query('''UPDATE subscriptions SET subscribed = 0 WHERE user_id = ?''', (user_id,)) 201 | 202 | @staticmethod 203 | async def get_subscribed_user_ids(): 204 | return [row[0] for row in await db.fetch_all('SELECT user_id FROM subscriptions WHERE subscribed = 1')] 205 | 206 | @staticmethod 207 | async def clear_subscribed_users(): 208 | await db.execute_query('''UPDATE subscriptions SET subscribed = 0''') 209 | 210 | @staticmethod 211 | async def mark_temporary_subscriptions(): 212 | await db.execute_query('''UPDATE subscriptions SET temporary = 1''') 213 | 214 | @staticmethod 215 | async def mark_temporary_unsubscriptions(): 216 | await db.execute_query('''UPDATE subscriptions SET temporary = 0''') 217 | 218 | @staticmethod 219 | async def get_temporary_subscribed_user_ids(): 220 | return [row[0] for row in await db.fetch_all('SELECT user_id FROM subscriptions WHERE temporary = 1')] 221 | 222 | @staticmethod 223 | async def is_user_subscribed(user_id): 224 | result = await db.fetch_one('SELECT subscribed FROM subscriptions WHERE user_id = ?', (user_id,)) 225 | return result is not None and result[0] == 1 226 | 227 | @staticmethod 228 | async def update_user_is_admin(user_id, is_admin): 229 | await db.execute_query('UPDATE user_settings SET is_admin = ? WHERE user_id = ?', (is_admin, user_id)) 230 | 231 | @staticmethod 232 | async def is_user_admin(user_id): 233 | result = await db.fetch_one('SELECT is_admin FROM user_settings WHERE user_id = ?', (user_id,)) 234 | return result is not None and result[0] == 1 235 | 236 | @staticmethod 237 | async def set_admin_broadcast(user_id, admin_broadcast): 238 | await db.execute_query('UPDATE user_settings SET admin_broadcast = ? WHERE user_id = ?', 239 | (admin_broadcast, user_id)) 240 | 241 | @staticmethod 242 | async def get_admin_broadcast(user_id): 243 | result = await db.fetch_one('SELECT admin_broadcast FROM user_settings WHERE user_id = ?', (user_id,)) 244 | if result: 245 | return result[0] 246 | return False # Default to False if the user is not found or the flag is not set 247 | 248 | @staticmethod 249 | async def count_subscribed_users(): 250 | result = await db.fetch_one('SELECT COUNT(*) FROM subscriptions WHERE subscribed = 1') 251 | return result[0] if result else 0 252 | 253 | @staticmethod 254 | async def set_user_updated_flag(user_id, is_user_updated): 255 | is_user_updated_value = 1 if is_user_updated else 0 256 | await db.execute_query('UPDATE user_settings SET is_user_updated = ? WHERE user_id = ?', 257 | (is_user_updated_value, user_id)) 258 | 259 | @staticmethod 260 | async def get_user_updated_flag(user_id): 261 | result = await db.fetch_one('SELECT is_user_updated FROM user_settings WHERE user_id = ?', (user_id,)) 262 | if result: 263 | return bool(result[0]) 264 | return False 265 | 266 | @staticmethod 267 | async def set_file_processing_flag(user_id, is_processing): 268 | is_processing_value = 1 if is_processing else 0 269 | await db.execute_query('UPDATE user_settings SET is_file_processing = ? WHERE user_id = ?', 270 | (is_processing_value, user_id)) 271 | 272 | @staticmethod 273 | async def get_file_processing_flag(user_id): 274 | result = await db.fetch_one('SELECT is_file_processing FROM user_settings WHERE user_id = ?', (user_id,)) 275 | if result: 276 | return bool(result[0]) if result[0] else False 277 | return False # Return False if the file is not found or the flag is not set 278 | 279 | @staticmethod 280 | async def reset_all_file_processing_flags(): 281 | await db.execute_query('UPDATE user_settings SET is_file_processing = 0') 282 | 283 | @staticmethod 284 | async def increment_download_counter(filename): 285 | await db.execute_query('UPDATE musics SET downloads = downloads + 1 WHERE filename = ?', (filename,)) 286 | 287 | @staticmethod 288 | async def add_or_increment_song(filename): 289 | try: 290 | query = 'INSERT INTO musics (filename) VALUES (?)' 291 | await db.execute_query(query, (filename,)) 292 | except aiosqlite.IntegrityError: 293 | query = 'UPDATE musics SET downloads = downloads + 1 WHERE filename = ?' 294 | await db.execute_query(query, (filename,)) 295 | 296 | @staticmethod 297 | async def get_total_downloads(): 298 | result = await db.fetch_one('SELECT SUM(downloads) FROM musics') 299 | return result[0] if result else 0 300 | 301 | @staticmethod 302 | async def get_song_downloads(filename): 303 | result = await db.fetch_one('SELECT downloads FROM musics WHERE filename = ?', (filename,)) 304 | return result[0] if result else 0 305 | 306 | @staticmethod 307 | async def set_user_tweet_capture_settings(user_id, tweet_capture_settings): 308 | serialized_info = json.dumps(tweet_capture_settings) 309 | await db.execute_query('UPDATE user_settings SET tweet_capture_settings = ? WHERE user_id = ?', 310 | (serialized_info, user_id)) 311 | 312 | @staticmethod 313 | async def get_user_tweet_capture_settings(user_id): 314 | result = await db.fetch_one('SELECT tweet_capture_settings FROM user_settings WHERE user_id = ?', (user_id,)) 315 | if result is not None: 316 | return json.loads(result[0]) if result[0] else {} 317 | return {} # Return an empty dictionary if the user is not found or the Spotify link info is not set 318 | -------------------------------------------------------------------------------- /utils/helper.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | async def sanitize_query(query): 5 | # Remove non-alphanumeric characters and spaces 6 | sanitized_query = re.sub(r'\W+', ' ', query) 7 | # Trim leading and trailing spaces 8 | sanitized_query = sanitized_query.strip() 9 | return sanitized_query 10 | -------------------------------------------------------------------------------- /utils/tweet_capture.py: -------------------------------------------------------------------------------- 1 | """ 2 | Acknowledgment of Contributions 3 | 4 | The team behind this project would like to extend our heartfelt gratitude to the wonderful contributors of the tweetcapture repository (https://github.com/xacnio/tweetcapture). 5 | Their initial implementation of the TweetCapture classes has been incredibly helpful, and we've adapted and modified this code to fit the specific needs and use cases of our current project. 6 | 7 | We're truly grateful for the efforts and insights shared by the tweetcapture project team. 8 | 9 | """ 10 | from .database import db 11 | from selenium import webdriver 12 | from selenium.webdriver.chrome.options import Options 13 | from selenium.webdriver.common.by import By 14 | from selenium.webdriver.support.ui import WebDriverWait 15 | from selenium.webdriver.support import expected_conditions as EC 16 | from selenium.common.exceptions import WebDriverException 17 | import queue 18 | 19 | 20 | class AsyncWebDriver: 21 | def __init__(self, driver): 22 | self.driver = driver 23 | 24 | async def __aenter__(self): 25 | return self.driver 26 | 27 | async def __aexit__(self, exc_type, exc_val, exc_tb): 28 | if self.driver: 29 | return self.driver.quit() 30 | 31 | 32 | class TweetCapture: 33 | max_drivers = 5 34 | driver_pool = queue.Queue() 35 | 36 | @classmethod 37 | async def get_driver(cls): 38 | if cls.driver_pool.empty(): 39 | chrome_options = cls.setup_chrome_options() 40 | try: 41 | driver = webdriver.Chrome(options=chrome_options) 42 | driver.set_window_size(1080, 1920) 43 | except Exception as e: 44 | print(f"Failed to initialize Chrome driver: {str(e)}") 45 | return None 46 | else: 47 | driver = cls.driver_pool.get() 48 | 49 | if driver is not None: 50 | return AsyncWebDriver(driver) 51 | else: 52 | return None 53 | 54 | @classmethod 55 | async def release_driver(cls, driver): 56 | cls.driver_pool.put(driver) 57 | if cls.driver_pool.qsize() > cls.max_drivers: 58 | driver = cls.driver_pool.get() 59 | driver.quit() 60 | 61 | @staticmethod 62 | def setup_chrome_options(): 63 | chrome_options = Options() 64 | chrome_options.add_argument("--headless") 65 | chrome_options.add_argument("--disable-gpu") 66 | chrome_options.add_argument("--no-sandbox") 67 | chrome_options.add_argument("--disable-dev-shm-usage") 68 | chrome_options.add_argument("--disable-extensions") 69 | return chrome_options 70 | 71 | @staticmethod 72 | def set_night_mode(driver, tweet_url, night_mode): 73 | """ 74 | Sets the night mode and adds cookies to the Selenium WebDriver instance. 75 | 76 | """ 77 | driver.get(tweet_url) 78 | # Set the night mode cookie 79 | driver.add_cookie( 80 | {"name": "night_mode", "value": (night_mode if night_mode is not None else "0")} 81 | ) 82 | 83 | @staticmethod 84 | def dismiss_cookie_accept(driver): 85 | try: 86 | cookie_accept_button = driver.find_element(By.CSS_SELECTOR, 87 | "div[role='button'][class*='r-sdzlij'][class*='r-1phboty']") 88 | driver.execute_script("arguments[0].click();", cookie_accept_button) 89 | except: 90 | pass 91 | 92 | @staticmethod 93 | def find_main_tweet_element(driver): 94 | tweet_elements = driver.find_elements(By.XPATH, "(//ancestor::article)/..") 95 | for element in tweet_elements: 96 | if len(element.find_elements(By.XPATH, ".//article[contains(@data-testid, 'tweet')]")) > 0: 97 | source = element.get_attribute("innerHTML") 98 | if source.find("M19.498 3h-15c-1.381 0-2.5 1.12-2.5 2.5v13c0 1.38") == -1 and source.find( 99 | 'css-1dbjc4n r-1s2bzr4" id="id__jrl5cg7nxl"') == -1: 100 | main_tweet_details = element.find_elements(By.XPATH, ".//div[contains(@class, 'r-1471scf')]") 101 | if len(main_tweet_details) == 1: 102 | return element 103 | return None 104 | 105 | @staticmethod 106 | async def get_settings(user_id): 107 | return await db.get_user_tweet_capture_settings(user_id) 108 | 109 | @staticmethod 110 | async def set_settings(user_id, settings: dict): 111 | return await db.set_user_tweet_capture_settings(user_id, settings) 112 | 113 | @staticmethod 114 | async def screenshot(tweet_url, screenshot_path, night_mode): 115 | max_retries = 3 # Maximum number of retries 116 | retries = 0 117 | 118 | while retries < max_retries: 119 | async with await TweetCapture.get_driver() as driver: 120 | try: 121 | # Set the night mode 122 | TweetCapture.set_night_mode(driver, tweet_url, night_mode) 123 | 124 | driver.get(tweet_url) 125 | 126 | WebDriverWait(driver, 6).until( 127 | EC.presence_of_element_located((By.XPATH, "(//ancestor::article)/.."))) 128 | main_tweet_element = TweetCapture.find_main_tweet_element(driver) 129 | 130 | if main_tweet_element is None: 131 | raise Exception("Unable to locate the main tweet element.") 132 | 133 | # Scroll to the tweet element 134 | driver.execute_script("arguments[0].scrollIntoView(true);", main_tweet_element) 135 | 136 | # Get the dimensions of the tweet element 137 | tweet_rect = main_tweet_element.rect 138 | width = tweet_rect['width'] 139 | height = tweet_rect['height'] 140 | 141 | # Set the window size to match the tweet dimensions 142 | driver.set_window_size(width, height + 512) 143 | 144 | # Take the screenshot 145 | main_tweet_element.screenshot(screenshot_path) 146 | return # Success, exit the method 147 | except WebDriverException as e: 148 | print(f"Attempt {retries + 1} failed: {str(e)}") 149 | retries += 1 150 | if retries >= max_retries: 151 | raise Exception( 152 | f"Failed to capture screenshot after {max_retries} attempts.\nPlease try again later.") 153 | except Exception as e: 154 | raise Exception(f"Internal Error: {str(e)}\nTry another time.") 155 | --------------------------------------------------------------------------------