├── requirements.txt ├── .gitignore ├── .idea ├── vcs.xml ├── .gitignore ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── misc.xml ├── modules.xml └── spotify_downloader_telegram__bot.iml ├── sample.env ├── telegram ├── __init__.py ├── utils.py ├── album_callback_query.py ├── playlist_callback_query.py ├── artist_callback_query.py ├── new_message.py └── song_callback_query.py ├── spotify ├── __init__.py ├── utils.py ├── playlist.py ├── album.py ├── artist.py └── song.py ├── main.py ├── models.py ├── LICENSE ├── consts.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xerion12/python-spotify-bot/HEAD/requirements.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .cache 3 | *.session 4 | *.session-journal 5 | *.db 6 | *.db-journal 7 | ffmpeg.exe 8 | 9 | covers 10 | songs -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | BOT_TOKE=secret 2 | SPOTIFY_CLIENT_ID=secret 3 | SPOTIFY_CLIENT_SECRET=secret 4 | TELEGRAM_API_ID=secret 5 | TELEGRAM_API_HASH=secret 6 | GENIUS_ACCESS_TOKEN=secret 7 | BOT_ID=@TEST 8 | DB_CHANNEL_ID=id -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /telegram/__init__.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | from telethon import TelegramClient 3 | 4 | BOT_TOKEN = config("BOT_TOKEN") 5 | API_ID = config("TELEGRAM_API_ID") 6 | API_HASH = config('TELEGRAM_API_HASH') 7 | CLIENT = TelegramClient('session', API_ID, API_HASH) 8 | DB_CHANNEL_ID = int(config('DB_CHANNEL_ID')) 9 | BOT_ID = config('BOT_ID') 10 | -------------------------------------------------------------------------------- /spotify/__init__.py: -------------------------------------------------------------------------------- 1 | import lyricsgenius 2 | import spotipy 3 | from decouple import config 4 | 5 | SPOTIFY = spotipy.Spotify( 6 | client_credentials_manager=spotipy.oauth2.SpotifyClientCredentials(client_id=config("SPOTIPY_CLIENT_ID"), 7 | client_secret=config("SPOTIPY_CLIENT_SECRET"))) 8 | 9 | GENIUS = lyricsgenius.Genius(config("GENIUS_ACCESS_TOKEN")) 10 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import telebot 2 | from telegram import BOT_TOKEN, CLIENT, song_callback_query,\ 3 | album_callback_query, new_message, artist_callback_query,\ 4 | playlist_callback_query 5 | if __name__ == '__main__': 6 | try: 7 | print('[BOT] Starting...') 8 | CLIENT.start(bot_token=BOT_TOKEN) 9 | CLIENT.run_until_disconnected() 10 | except Exception as e: 11 | print(f'[BOT] Error: {e}') 12 | 13 | -------------------------------------------------------------------------------- /.idea/spotify_downloader_telegram__bot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine, Column, Integer, ForeignKey, String 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import relationship, sessionmaker 4 | 5 | engine = create_engine(f'sqlite:///database.db') 6 | Base = declarative_base() 7 | 8 | 9 | # Define the User table 10 | class User(Base): 11 | __tablename__ = 'users' 12 | id = Column(Integer, primary_key=True) 13 | telegram_id = Column(Integer, unique=True) 14 | 15 | 16 | # Define the Song table 17 | class SongRequest(Base): 18 | __tablename__ = 'songs' 19 | id = Column(Integer, primary_key=True) 20 | user_id = Column(Integer, ForeignKey('users.id')) 21 | spotify_id = Column(String, nullable=False) 22 | song_id_in_group = Column(Integer) 23 | group_id = Column(Integer) 24 | user = relationship('User', backref='songs') 25 | 26 | 27 | # Create the tables in the database if they don't exist 28 | Base.metadata.create_all(engine) 29 | 30 | # Create a session to interact with the database 31 | Session = sessionmaker(bind=engine) 32 | session = Session() 33 | -------------------------------------------------------------------------------- /telegram/utils.py: -------------------------------------------------------------------------------- 1 | from telethon import events, Button 2 | from telethon.tl import types 3 | 4 | from consts import NOT_FOUND_STICKER 5 | from spotify.utils import search_single 6 | 7 | 8 | async def handle_search_message(event: events.NewMessage.Event): 9 | msg = event.message.message 10 | song_items = search_single(msg) 11 | 12 | # Create inline keyboard buttons 13 | # Create buttons for each song item 14 | buttons = [] 15 | for song_item in song_items: 16 | # Create a new row for each button 17 | button = [Button.inline(f'{song_item.track_name} - {song_item.artist_name}', data=f"song:{song_item.id}")] 18 | buttons.append(button) 19 | 20 | # Create the reply message with buttons 21 | reply_message = "No results found." 22 | if buttons: 23 | reply_message = "🔎Here are the search results:" 24 | 25 | # Send the message with buttons as a reply to the original message 26 | await event.reply(reply_message, buttons=buttons) 27 | else: 28 | await event.respond('❌') 29 | await event.reply(reply_message, ) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nima 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 | -------------------------------------------------------------------------------- /spotify/utils.py: -------------------------------------------------------------------------------- 1 | from spotify import SPOTIFY 2 | from spotify.song import Song 3 | 4 | 5 | def album(link): 6 | results = SPOTIFY.album_tracks(link) 7 | albums = results['items'] 8 | while results['next']: 9 | results = SPOTIFY.next(results) 10 | albums.extend(results['items']) 11 | return albums 12 | 13 | 14 | def artist(link): 15 | results = SPOTIFY.artist_top_tracks(link) 16 | albums = results['tracks'] 17 | return albums 18 | 19 | 20 | def search_album(track): 21 | results = SPOTIFY.search(track) 22 | return results['tracks']['items'][0]['album']['external_urls']['spotify'] 23 | 24 | 25 | def playlist(link): 26 | results = SPOTIFY.playlist_items(link) 27 | return results['items'][:50] 28 | 29 | 30 | def search_single(q) -> list[Song]: 31 | results = SPOTIFY.search(q) 32 | songs_list = [] 33 | for item in results['tracks']['items']: 34 | songs_list.append(Song(item['id'])) 35 | return songs_list 36 | 37 | 38 | def search_artist(artist): 39 | results = SPOTIFY.search(artist) 40 | return results['tracks']['items'][0]['artists'][0]["external_urls"]['spotify'] 41 | -------------------------------------------------------------------------------- /consts.py: -------------------------------------------------------------------------------- 1 | WELCOME_MESSAGE = '''Hi, 2 | A Simple Telegram Bot That Lets You Download Tracks From Spotify And YouTube! 3 | Supports Downloading Tracks, Albums, Playlists, Artists! 🎧📱💿''' 4 | 5 | ARTISTS_MESSAGE = '''Please send the name of the artist like this: Artist Name''' 6 | 7 | SINGLE_MESSAGE = '''Please send the name of the song like this: 8 | Song Name 9 | or for better search, use this format: 10 | Song Name - Artist Name''' 11 | 12 | ALBUM_MESSAGE = '''Please send the name of the album like this: 13 | Album Name 14 | or for better search, use this format: 15 | Album Name - Artist Name''' 16 | 17 | NOT_FOUND_STICKER = 'CAACAgQAAxkBAAIFSWBF_m3GHUtZJxQzobvD_iWxYVClAAJuAgACh4hSOhXuVi2-7-xQHgQ' 18 | 19 | NOT_IN_DB = '❌💾 Song is not in database, this might take longer' 20 | 21 | DOWNLOADING = 'downloading...' 22 | 23 | UPLOADING = 'uploading...' 24 | 25 | PROCESSING = 'processing...' 26 | 27 | ALREADY_IN_DB = '💾already in database' 28 | 29 | NO_LYRICS_FOUND = '❌No lyrics found' 30 | 31 | SONG_NOT_FOUND = '❌Song Not Found' 32 | 33 | ALBUM_HAS_SENT_SUCCESSFULLY = '🎧album sent!' 34 | 35 | PLAYLIST_HAS_SENT_SUCCESSFULLY = '🎧playlist first 50 songs has sent!' 36 | -------------------------------------------------------------------------------- /spotify/playlist.py: -------------------------------------------------------------------------------- 1 | from telethon import Button 2 | 3 | from spotify import SPOTIFY 4 | 5 | 6 | class Playlist: 7 | def __init__(self, link): 8 | self.spotify = SPOTIFY.playlist(link) 9 | self.id = self.spotify['id'] 10 | self.spotify_link = self.spotify['external_urls']['spotify'] 11 | self.playlist_name = self.spotify['name'] 12 | self.description = self.spotify['description'] 13 | self.owner_name = self.spotify['owner']['display_name'] 14 | self.followers_count = self.spotify['followers']['total'] 15 | self.track_count = len(self.spotify['tracks']['items']) 16 | self.playlist_image = self.spotify['images'][0]['url'] 17 | self.uri = self.spotify['uri'] 18 | 19 | async def playlist_template(self): 20 | message = f''' 21 | ▶️Playlist: {self.playlist_name} 22 | 📝Description: {self.description} 23 | 👤Owner: {self.owner_name} 24 | 🩷Followers: {self.followers_count} 25 | 🔢Total Track: {self.track_count} 26 | 27 | [IMAGE]({self.playlist_image}) 28 | {self.uri} 29 | ''' 30 | 31 | buttons = [[Button.inline(f'📩Download Playlist Tracks!', data=f"download_playlist_songs:{self.id}")], 32 | [Button.inline(f'🖼️Download Playlist Image!', data=f"download_playlist_image:{self.id}")], 33 | [Button.url(f'🎵Listen on Spotify', self.spotify_link)], 34 | ] 35 | return message, buttons 36 | 37 | @staticmethod 38 | def get_playlist_tracks(link): 39 | return SPOTIFY.playlist_tracks(link, limit=50)['items'] 40 | -------------------------------------------------------------------------------- /telegram/album_callback_query.py: -------------------------------------------------------------------------------- 1 | from telethon import events 2 | 3 | from consts import PROCESSING, ALBUM_HAS_SENT_SUCCESSFULLY 4 | from spotify.album import Album 5 | from spotify.song import Song 6 | from telegram import CLIENT, BOT_ID 7 | 8 | 9 | @CLIENT.on(events.CallbackQuery(pattern='download_album_songs')) 10 | async def download_album_songs_callback_query(event: events.CallbackQuery.Event): 11 | data = event.data.decode('utf-8') 12 | album_id = data[21:] 13 | print(f'[TELEGRAM] download album songs callback query: {album_id}') 14 | album = Album(album_id) 15 | processing = await event.respond(PROCESSING) 16 | for song_id in album.track_list: 17 | await Song.upload_on_telegram(event=event, song_id=song_id) 18 | await processing.delete() 19 | await event.respond(ALBUM_HAS_SENT_SUCCESSFULLY) 20 | 21 | 22 | @CLIENT.on(events.CallbackQuery(pattern='download_album_image')) 23 | async def download_album_image_callback_query(event: events.CallbackQuery.Event): 24 | data = event.data.decode('utf-8') 25 | song_id = data[21:] 26 | print(f'[TELEGRAM] download album image callback query: {song_id}') 27 | await event.respond(BOT_ID, file=Album(song_id).album_cover) 28 | 29 | 30 | @CLIENT.on(events.CallbackQuery(pattern='album_artist')) 31 | async def album_artist_callback_query(event: events.CallbackQuery.Event): 32 | data = event.data.decode('utf-8') 33 | album_id = data[13:] 34 | song = Album(album_id) 35 | print(f'[TELEGRAM] album artists callback query: {album_id}') 36 | message = await song.artist_buttons_telethon_templates() 37 | await event.respond(message=message[0], buttons=message[1]) 38 | -------------------------------------------------------------------------------- /telegram/playlist_callback_query.py: -------------------------------------------------------------------------------- 1 | from telethon import events 2 | 3 | from consts import PROCESSING, ALBUM_HAS_SENT_SUCCESSFULLY, PLAYLIST_HAS_SENT_SUCCESSFULLY 4 | from spotify.playlist import Playlist 5 | from spotify.song import Song 6 | from telegram import CLIENT, BOT_ID 7 | 8 | 9 | @CLIENT.on(events.CallbackQuery(pattern='download_playlist_songs')) 10 | async def download_album_songs_callback_query(event: events.CallbackQuery.Event): 11 | data = event.data.decode('utf-8') 12 | playlist_id = data[24:] 13 | print(f'[TELEGRAM] download playlist songs callback query: {playlist_id}') 14 | processing = await event.respond(PROCESSING) 15 | for song_id in Playlist.get_playlist_tracks(playlist_id): 16 | print(song_id) 17 | await Song.upload_on_telegram(event=event, song_id=song_id['track']['id']) 18 | await processing.delete() 19 | await event.respond(PLAYLIST_HAS_SENT_SUCCESSFULLY) 20 | 21 | 22 | @CLIENT.on(events.CallbackQuery(pattern='download_playlist_image')) 23 | async def download_album_image_callback_query(event: events.CallbackQuery.Event): 24 | data = event.data.decode('utf-8') 25 | playlist_id = data[24:] 26 | print(f'[TELEGRAM] download playlist image callback query: {playlist_id}') 27 | await event.respond(BOT_ID, file=Playlist(playlist_id).playlist_image) 28 | 29 | 30 | @CLIENT.on(events.CallbackQuery(pattern='playlist:')) 31 | async def album_artist_callback_query(event: events.CallbackQuery.Event): 32 | data = event.data.decode('utf-8') 33 | playlist_id = data[9:] 34 | playlist = Playlist(playlist_id) 35 | print(f'[TELEGRAM] playlist callback query: {playlist_id}') 36 | message = await playlist.playlist_template() 37 | await event.respond(message=message[0], buttons=message[1]) 38 | -------------------------------------------------------------------------------- /spotify/album.py: -------------------------------------------------------------------------------- 1 | from telethon import Button 2 | 3 | from spotify import SPOTIFY 4 | 5 | 6 | class Album: 7 | def __init__(self, link): 8 | self.spotify = SPOTIFY.album(link) 9 | self.id = self.spotify['id'] 10 | self.album_name = self.spotify['name'] 11 | self.artists_list = self.spotify['artists'] 12 | self.artist_name = self.artists_list[0]['name'] 13 | self.spotify_link = self.spotify['external_urls']['spotify'] 14 | self.album_cover = self.spotify['images'][0]['url'] 15 | self.release_date = self.spotify['release_date'] 16 | self.total_tracks = self.spotify['total_tracks'] 17 | self.track_list = [x['id'] for x in self.spotify['tracks']['items']] 18 | self.uri = self.spotify['uri'] 19 | 20 | async def album_telegram_template(self): 21 | message = f''' 22 | 💿 Album : `{self.album_name}` 23 | 🎤 Artist : `{self.artist_name}` 24 | 🎧 Total tracks : `{self.total_tracks}` 25 | 📅 Release Date : `{self.release_date}` 26 | 27 | [IMAGE]({self.album_cover}) 28 | {self.uri} 29 | ''' 30 | 31 | buttons = [[Button.inline(f'📩Download Album Tracks!', data=f"download_album_songs:{self.id}")], 32 | [Button.inline(f'🖼️Download Album Image!', data=f"download_album_image:{self.id}")], 33 | [Button.inline(f'🧑‍🎨View Album Artists!', data=f"album_artist:{self.id}")], 34 | [Button.url(f'🎵Listen on Spotify', self.spotify_link)], 35 | ] 36 | 37 | return message, self.album_cover, buttons 38 | 39 | async def artist_buttons_telethon_templates(self): 40 | message = f"{self.album_name} album Artist's" 41 | buttons = [[Button.inline(artist['name'], data=f"artist:{artist['id']}")] 42 | for artist in self.artists_list] 43 | return message, buttons 44 | -------------------------------------------------------------------------------- /telegram/artist_callback_query.py: -------------------------------------------------------------------------------- 1 | from telethon import events 2 | 3 | from spotify.artist import Artist 4 | from telegram import CLIENT, BOT_ID 5 | 6 | 7 | @CLIENT.on(events.CallbackQuery(pattern='download_artist_image')) 8 | async def download_album_image_callback_query(event: events.CallbackQuery.Event): 9 | data = event.data.decode('utf-8') 10 | artist_id = data[22:] 11 | print(f'[TELEGRAM] download artist image callback query: {artist_id}') 12 | await event.respond(BOT_ID, file=Artist(artist_id).artist_profile) 13 | 14 | 15 | @CLIENT.on(events.CallbackQuery(pattern='artist_top_tracks')) 16 | async def artist_top_tracks_callback_query(event: events.CallbackQuery.Event): 17 | data = event.data.decode('utf-8') 18 | artist_id = data[18:] 19 | artist = Artist(artist_id) 20 | print(f'[TELEGRAM] artist top tracks callback query: {artist.id}') 21 | message = await artist.artist_top_tracks_template() 22 | await event.respond(message[0], buttons=message[1]) 23 | 24 | 25 | @CLIENT.on(events.CallbackQuery(pattern='artist_albums')) 26 | async def artist_albums_callback_query(event: events.CallbackQuery.Event): 27 | data = event.data.decode('utf-8') 28 | artist_id = data[14:] 29 | artist = Artist(artist_id) 30 | print(f'[TELEGRAM] artist albums callback query: {artist.id}') 31 | message = await artist.artist_albums_template() 32 | await event.respond(message[0], buttons=message[1]) 33 | 34 | 35 | @CLIENT.on(events.CallbackQuery(pattern='artist:')) 36 | async def album_callback_query(event: events.CallbackQuery.Event): 37 | data = event.data.decode('utf-8') 38 | print(data) 39 | artist_id = data[7:] 40 | print(f'[TELEGRAM] artist callback query: {artist_id}') 41 | artist = Artist(artist_id) 42 | message = await artist.artist_telethon_template() 43 | await event.respond(message=message[0], buttons=message[1], ) 44 | -------------------------------------------------------------------------------- /spotify/artist.py: -------------------------------------------------------------------------------- 1 | from telethon import Button 2 | 3 | from spotify import SPOTIFY 4 | 5 | 6 | class Artist: 7 | def __init__(self, artist_id): 8 | self.spotify = SPOTIFY.artist(artist_id) 9 | self.id = self.spotify['id'] 10 | self.artist_name = self.spotify['name'] 11 | self.followers_count = self.spotify['followers']['total'] 12 | self.genres = self.spotify['genres'] 13 | self.uri = self.spotify['uri'] 14 | self.artist_profile = self.spotify['images'][0]['url'] 15 | self.spotify_link = self.spotify['external_urls']['spotify'] 16 | 17 | async def artist_telethon_template(self): 18 | message = f''' 19 | 👤 Artist :`{self.artist_name}` 20 | 🩷 Followers : `{self.followers_count}` 21 | 🎶 Genres : `{self.genres}` 22 | 23 | [IMAGE]({self.artist_profile}) 24 | {self.uri} 25 | ''' 26 | 27 | buttons = [[Button.inline(f'🖼️Download Artist Image!', data=f"download_artist_image:{self.id}")], 28 | [Button.inline(f"👀View Artist Top Tracks!", data=f"artist_top_tracks:{self.id}")], 29 | [Button.inline(f'🧑‍🎨View Artist Albums!', data=f"artist_albums:{self.id}")], 30 | [Button.url(f'🎵Listen on Spotify', self.spotify_link)], 31 | ] 32 | 33 | return message, buttons 34 | 35 | async def artist_top_tracks_template(self): 36 | top_tracks = SPOTIFY.artist_top_tracks(self.id) 37 | buttons = [[Button.inline(f"{track['name']} - {track['artists'][0]['name']}", 38 | data=f"song:{track['id']}")] for track in top_tracks['tracks']] 39 | return self.artist_name, buttons 40 | 41 | async def artist_albums_template(self): 42 | top_tracks = SPOTIFY.artist_albums(self.id) 43 | buttons = [[Button.inline(f"{album['name']} - {album['artists'][0]['name']}", 44 | data=f"artist:{album['id']}")] for album in top_tracks['items']] 45 | return self.artist_name, buttons -------------------------------------------------------------------------------- /telegram/new_message.py: -------------------------------------------------------------------------------- 1 | from telethon import events, client 2 | 3 | from consts import WELCOME_MESSAGE 4 | from spotify.album import Album 5 | from spotify.artist import Artist 6 | from spotify.playlist import Playlist 7 | from spotify.song import Song 8 | from telegram import CLIENT 9 | from telegram.utils import handle_search_message 10 | 11 | 12 | @CLIENT.on(events.NewMessage(pattern='/start')) 13 | async def start(event): 14 | await event.respond(WELCOME_MESSAGE) 15 | 16 | 17 | async def handle_track(event: events.NewMessage.Event, msg_link): 18 | print(f'[TELEGRAM] song callback query: {msg_link}') 19 | message = await Song(msg_link).song_telethon_template() 20 | await event.respond(message[0], thumb=message[1], buttons=message[2]) 21 | 22 | 23 | async def handle_album(event: events.NewMessage.Event, msg_link): 24 | print(f'[TELEGRAM] album callback query: {msg_link}') 25 | message = await Album(msg_link).album_telegram_template() 26 | await event.respond(message[0], thumb=message[1], buttons=message[2]) 27 | 28 | 29 | async def handle_artist(event: events.NewMessage.Event, msg_link): 30 | print(f'[TELEGRAM] artist callback query: {msg_link}') 31 | message = await Artist(msg_link).artist_telethon_template() 32 | await event.respond(message[0], buttons=message[1]) 33 | 34 | 35 | async def handle_playlist(event: events.NewMessage.Event, msg_link): 36 | print(f'[TELEGRAM] playlist callback query: {msg_link}') 37 | message = await Playlist(msg_link).playlist_template() 38 | await event.respond(message[0], buttons=message[1]) 39 | 40 | 41 | @CLIENT.on(events.NewMessage) 42 | async def download(event: events.NewMessage.Event): 43 | # message is private 44 | if event.is_private and not event.raw_text.startswith('/start'): 45 | msg = event.raw_text 46 | print(f'[TELEGRAM] New message: {msg}') 47 | msg_link = text_finder(msg) 48 | if msg_link.startswith('https://open.spotify.com/'): 49 | # Process different types of Spotify links 50 | if 'album' in msg_link: 51 | await handle_album(event, msg_link) 52 | elif 'track' in msg_link: 53 | await handle_track(event, msg_link) 54 | elif 'playlist' in msg_link: 55 | await handle_playlist(event, msg_link) 56 | elif 'artist' in msg_link: 57 | await handle_artist(event, msg_link) 58 | else: 59 | await handle_search_message(event) 60 | else: 61 | await handle_search_message(event) 62 | 63 | 64 | def text_finder(txt): 65 | index = txt.find("https://open.spotify.com") 66 | if index != -1: 67 | return txt[index:] 68 | return '' 69 | -------------------------------------------------------------------------------- /telegram/song_callback_query.py: -------------------------------------------------------------------------------- 1 | from telethon import events, types 2 | from telethon.tl.types import PeerUser 3 | 4 | from consts import NOT_IN_DB, PROCESSING, DOWNLOADING, UPLOADING, ALREADY_IN_DB, NO_LYRICS_FOUND 5 | from models import session, SongRequest 6 | from spotify.album import Album 7 | from spotify.song import Song 8 | from telegram import CLIENT, DB_CHANNEL_ID, BOT_ID 9 | 10 | 11 | @CLIENT.on(events.CallbackQuery(pattern='song')) 12 | async def song_callback_query(event: events.CallbackQuery.Event): 13 | data = event.data.decode('utf-8') 14 | print(f'[TELEGRAM] song callback query: {data}') 15 | message = await Song(data[5:]).song_telethon_template() 16 | await event.respond(message[0], thumb=message[1], buttons=message[2]) 17 | 18 | 19 | @CLIENT.on(events.CallbackQuery(pattern='download_song')) 20 | async def send_song_callback_query(event: events.CallbackQuery.Event): 21 | data = event.data.decode('utf-8') 22 | song_id = data[14:] 23 | print(f'[TELEGRAM] download song callback query: {song_id}') 24 | await Song.upload_on_telegram(event, song_id) 25 | 26 | 27 | @CLIENT.on(events.CallbackQuery(pattern='track_lyrics')) 28 | async def track_lyrics_callback_query(event: events.CallbackQuery.Event): 29 | data = event.data.decode('utf-8') 30 | song_id = data[13:] 31 | print(f'[TELEGRAM] track lyrics callback query: {data}') 32 | print(song_id) 33 | print(f'[TELEGRAM] track lyrics callback query: {song_id}') 34 | lyrics = Song(song_id).lyrics() 35 | if lyrics is None: 36 | await event.respond(NO_LYRICS_FOUND) 37 | else: 38 | await event.respond(f'{lyrics}\n\n{BOT_ID}') 39 | 40 | 41 | @CLIENT.on(events.CallbackQuery(pattern='download_song_image')) 42 | async def download_image_callback_query(event: events.CallbackQuery.Event): 43 | data = event.data.decode('utf-8') 44 | song_id = data[20:] 45 | print(f'[TELEGRAM] download song image callback query: {song_id}') 46 | await event.respond(BOT_ID, file=Song(song_id).album_cover) 47 | 48 | 49 | @CLIENT.on(events.CallbackQuery(pattern='track_artist')) 50 | async def track_artist_callback_query(event: events.CallbackQuery.Event): 51 | data = event.data.decode('utf-8') 52 | song_id = data[13:] 53 | song = Song(song_id) 54 | print(f'[TELEGRAM] track artists callback query: {song_id}') 55 | message = await song.artist_buttons_telethon_templates() 56 | await event.respond(message=message[0], buttons=message[1]) 57 | 58 | 59 | @CLIENT.on(events.CallbackQuery(pattern='album')) 60 | async def album_callback_query(event: events.CallbackQuery.Event): 61 | data = event.data.decode('utf-8') 62 | album_id = data[6:] 63 | print(f'[TELEGRAM] album callback query: {album_id}') 64 | message = await Album(album_id).album_telegram_template() 65 | await event.respond(message[0], thumb=message[1], buttons=message[2]) 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | The Spotify & Music Downloader Telegram Bot is a powerful and versatile Python application that transforms your Telegram chat into a personal music hub. It allows users to seamlessly search for, retrieve, and download high-quality audio directly through Telegram by leveraging the Spotify and YouTube platforms. 3 | 4 | This bot is designed for music enthusiasts who want a convenient way to access and save music. Simply send a Spotify link (track, album, or playlist) or just type the name of a song, artist, or album, and the bot will fetch and deliver the music to you. 5 | ## Features 6 | 7 | - Download tracks, albums, playlists, and artist information from Spotify. 8 | - Search for music by song name, album name, or artist name. 9 | - Retrieve top tracks and albums of an artist. 10 | - Download music from YouTube. 11 | 12 | ## Installation 13 | 14 | 1. Clone the repository: 15 | 16 | ``` 17 | git clone https://github.com/nimiology/spotify_downloader_telegram__bot.git 18 | cd spotify_downloader_telegram__bot 19 | ``` 20 | 21 | 2. Install dependencies: 22 | 23 | ``` 24 | pip install -r requirements.txt 25 | ``` 26 | 27 | 3. Set up your .env file with sample.env: 28 | - `BOT_TOKEN`: Telegram bot token - You can obtain this by creating a new bot on Telegram using the BotFather bot. BotFather will provide you with a token for your bot. 29 | - `SPOTIFY_CLIENT_ID`: Spotify client ID - These are obtained by registering your application on the Spotify Developer Dashboard. After registration, you'll receive both the client ID and client secret. 30 | - `SPOTIFY_CLIENT_SECRET`: Spotify client secret - These are obtained by registering your application on the Spotify Developer Dashboard. After registration, you'll receive both the client ID and client secret. 31 | - `TELEGRAM_API_ID`: Telegram api ID - You can get these by creating an application on the Telegram API website (https://my.telegram.org). After creating the application, you'll receive the API ID and API hash 32 | - `TELEGRAM_API_HASH`: Telegram api hash - You can get these by creating an application on the Telegram API website (https://my.telegram.org). After creating the application, you'll receive the API ID and API hash 33 | - `GENIUS_ACCESS_TOKEN`: Genius API access token - You can obtain this by registering your application on the Genius Developer website (https://genius.com/api-clients). After registering, you'll receive an access token for using the Genius API. 34 | - `BOT_ID`: Telegram bot username - This is the username of your Telegram bot, which you set when creating the bot on the BotFather. You can use this variable as song caption too. 35 | - `DB_CHANNEL_ID`: Telegram channel ID - This is the chat ID of the channel you want to use for your database. You can obtain this by adding your bot to the channel and using a tool like https://t.me/JsonDumpBot bot in the Telegram to find out the ID of the channel. 36 | 37 | 4. Run the bot: 38 | 39 | ``` 40 | python main.py 41 | ``` 42 | 43 | ## Usage 44 | 45 | 1. Start the bot by sending `/start` command. 46 | 2. Send a Spotify or song name to download music. 47 | 3. Search for music by sending song, album, or artist names. 48 | 49 | ## Contributing 50 | 51 | Contributions are welcome! If you want to contribute to this project, feel free to open an issue or submit a pull 52 | request. 53 | 54 | ## License 55 | 56 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 95 | -------------------------------------------------------------------------------- /spotify/song.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | import requests 5 | from telethon.tl import types 6 | from telethon.tl.types import PeerUser 7 | from youtube_search import YoutubeSearch 8 | import yt_dlp 9 | import eyed3.id3 10 | import eyed3 11 | from telethon import Button, events 12 | 13 | from consts import DOWNLOADING, UPLOADING, PROCESSING, ALREADY_IN_DB, NOT_IN_DB, SONG_NOT_FOUND 14 | from models import session, User, SongRequest 15 | from spotify import SPOTIFY, GENIUS 16 | from telegram import DB_CHANNEL_ID, CLIENT, BOT_ID 17 | 18 | if not os.path.exists('covers'): 19 | os.makedirs('covers') 20 | 21 | 22 | class Song: 23 | def __init__(self, link): 24 | self.spotify = SPOTIFY.track(link) 25 | self.id = self.spotify['id'] 26 | self.spotify_link = self.spotify['external_urls']['spotify'] 27 | self.track_name = self.spotify['name'] 28 | self.artists_list = self.spotify['artists'] 29 | self.artist_name = self.artists_list[0]['name'] 30 | self.artists = self.spotify['artists'] 31 | self.track_number = self.spotify['track_number'] 32 | self.album = self.spotify['album'] 33 | self.album_id = self.album['id'] 34 | self.album_name = self.album['name'] 35 | self.release_date = int(self.spotify['album']['release_date'][:4]) 36 | self.duration = int(self.spotify['duration_ms']) 37 | self.duration_to_seconds = int(self.duration / 1000) 38 | self.album_cover = self.spotify['album']['images'][0]['url'] 39 | self.path = f'songs' 40 | self.file = f'{self.path}/{self.id}.mp3' 41 | self.uri = self.spotify['uri'] 42 | 43 | def features(self): 44 | if len(self.artists) > 1: 45 | features = "(Ft." 46 | for artistPlace in range(0, len(self.artists)): 47 | try: 48 | if artistPlace < len(self.artists) - 2: 49 | artistft = self.artists[artistPlace + 1]['name'] + ", " 50 | else: 51 | artistft = self.artists[artistPlace + 1]['name'] + ")" 52 | features += artistft 53 | except: 54 | pass 55 | else: 56 | features = "" 57 | return features 58 | 59 | def convert_time_duration(self): 60 | target_datetime_ms = self.duration 61 | base_datetime = datetime.datetime(1900, 1, 1) 62 | delta = datetime.timedelta(0, 0, 0, target_datetime_ms) 63 | 64 | return base_datetime + delta 65 | 66 | def download_song_cover(self): 67 | response = requests.get(self.album_cover) 68 | image_file_name = f'covers/{self.id}.png' 69 | image = open(image_file_name, "wb") 70 | image.write(response.content) 71 | image.close() 72 | return image_file_name 73 | 74 | def yt_link(self): 75 | results = list(YoutubeSearch(str(self.track_name + " " + self.artist_name)).to_dict()) 76 | time_duration = self.convert_time_duration() 77 | yt_url = None 78 | 79 | for yt in results: 80 | yt_time = yt["duration"] 81 | yt_time = datetime.datetime.strptime(yt_time, '%M:%S') 82 | difference = abs((yt_time - time_duration).total_seconds()) 83 | 84 | if difference <= 3: 85 | yt_url = yt['url_suffix'] 86 | break 87 | if yt_url is None: 88 | return None 89 | 90 | yt_link = str("https://www.youtube.com/" + yt_url) 91 | return yt_link 92 | 93 | def yt_download(self, yt_link=None): 94 | options = { 95 | # PERMANENT options 96 | 'format': 'bestaudio/best', 97 | 'keepvideo': True, 98 | 'outtmpl': f'{self.path}/{self.id}', 99 | 'postprocessors': [{ 100 | 'key': 'FFmpegExtractAudio', 101 | 'preferredcodec': 'mp3', 102 | 'preferredquality': '320' 103 | }], 104 | } 105 | if yt_link is None: 106 | yt_link = self.yt_link() 107 | with yt_dlp.YoutubeDL(options) as mp3: 108 | mp3.download([yt_link]) 109 | 110 | def lyrics(self): 111 | try: 112 | return GENIUS.search_song(self.track_name, self.artist_name).lyrics 113 | except: 114 | return None 115 | 116 | def song_meta_data(self): 117 | mp3 = eyed3.load(self.file) 118 | mp3.tag.artist_name = self.artist_name 119 | mp3.tag.album_name = self.album_name 120 | mp3.tag.album_artist = self.artist_name 121 | mp3.tag.title = self.track_name + self.features() 122 | mp3.tag.track_num = self.track_number 123 | mp3.tag.year = self.track_number 124 | 125 | lyrics = self.lyrics() 126 | if lyrics is not None: 127 | mp3.tag.lyrics.set(lyrics) 128 | 129 | mp3.tag.images.set(3, open(self.download_song_cover(), 'rb').read(), 'image/png') 130 | mp3.tag.save() 131 | 132 | def download(self, yt_link=None): 133 | if os.path.exists(self.file): 134 | print(f'[SPOTIFY] Song Already Downloaded: {self.track_name} by {self.artist_name}') 135 | return self.file 136 | print(f'[YOUTUBE] Downloading {self.track_name} by {self.artist_name}...') 137 | self.yt_download(yt_link=yt_link) 138 | print(f'[SPOTIFY] Song Metadata: {self.track_name} by {self.artist_name}') 139 | self.song_meta_data() 140 | print(f'[SPOTIFY] Song Downloaded: {self.track_name} by {self.artist_name}') 141 | return self.file 142 | 143 | async def song_telethon_template(self): 144 | message = f''' 145 | 🎧 Title :`{self.track_name}` 146 | 🎤 Artist : `{self.artist_name}{self.features()}` 147 | 💿 Album : `{self.album_name}` 148 | 📅 Release Date : `{self.release_date}` 149 | 150 | [IMAGE]({self.album_cover}) 151 | {self.uri} 152 | ''' 153 | 154 | buttons = [[Button.inline(f'📩Download Track!', data=f"download_song:{self.id}")], 155 | [Button.inline(f'🖼️Download Track Image!', data=f"download_song_image:{self.id}")], 156 | [Button.inline(f'👀View Track Album!', data=f"album:{self.album_id}")], 157 | [Button.inline(f'🧑‍🎨View Track Artists!', data=f"track_artist:{self.id}")], 158 | [Button.inline(f'📃View Track Lyrics!', data=f"track_lyrics:{self.id}")], 159 | [Button.url(f'🎵Listen on Spotify', self.spotify_link)], 160 | ] 161 | 162 | return message, self.album_cover, buttons 163 | 164 | async def artist_buttons_telethon_templates(self): 165 | message = f"{self.track_name} track Artist's" 166 | buttons = [[Button.inline(artist['name'], data=f"artist:{artist['id']}")] 167 | for artist in self.artists_list] 168 | return message, buttons 169 | 170 | def save_db(self, user_id: int, song_id_in_group: int): 171 | user = session.query(User).filter_by(telegram_id=user_id).first() 172 | if not user: 173 | user = User(telegram_id=user_id) 174 | session.add(user) 175 | session.commit() 176 | session.add(SongRequest( 177 | spotify_id=self.id, 178 | user_id=user.id, 179 | song_id_in_group=song_id_in_group, 180 | group_id=DB_CHANNEL_ID 181 | )) 182 | session.commit() 183 | 184 | @staticmethod 185 | async def progress_callback(processing, sent_bytes, total): 186 | percentage = sent_bytes / total * 100 187 | await processing.edit(f"Uploading: {percentage:.2f}%") 188 | 189 | @staticmethod 190 | async def upload_on_telegram(event: events.CallbackQuery.Event, song_id): 191 | processing = await event.respond(PROCESSING) 192 | 193 | # first check if the song is already in the database 194 | song_db = session.query(SongRequest).filter_by(spotify_id=song_id).first() 195 | if song_db: 196 | db_message = await processing.edit(ALREADY_IN_DB) 197 | message_id = song_db.song_id_in_group 198 | else: 199 | # if not, create a new message in the database 200 | song = Song(song_id) 201 | db_message = await event.respond(NOT_IN_DB) 202 | # update processing message 203 | await processing.edit(DOWNLOADING) 204 | # see if the song is on yt 205 | yt_link = song.yt_link() 206 | if yt_link is None: 207 | print(f'[YOUTUBE] song not found: {song.uri}') 208 | await processing.delete() 209 | await event.respond(f"{song.track_name}\n{SONG_NOT_FOUND}") 210 | return 211 | file_path = song.download(yt_link=yt_link) 212 | await processing.edit(UPLOADING) 213 | 214 | upload_file = await CLIENT.upload_file(file_path) 215 | new_message = await CLIENT.send_file( 216 | DB_CHANNEL_ID, 217 | caption=BOT_ID, 218 | file=upload_file, 219 | supports_streaming=True, 220 | attributes=( 221 | types.DocumentAttributeAudio(title=song.track_name, duration=song.duration_to_seconds, 222 | performer=song.artist_name),), 223 | 224 | ) 225 | await processing.delete() 226 | song.save_db(event.sender_id, new_message.id) 227 | message_id = new_message.id 228 | 229 | # forward the message 230 | await CLIENT.forward_messages( 231 | entity=event.chat_id, # Destination chat ID 232 | messages=message_id, # Message ID to forward 233 | from_peer=PeerUser(int(DB_CHANNEL_ID)) # ID of the chat/channel where the message is from 234 | ) 235 | await db_message.delete() 236 | --------------------------------------------------------------------------------