├── 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 |
4 |
5 |
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 |
4 |
5 |
93 |
94 |
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 |
--------------------------------------------------------------------------------