-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
334 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
package spotifytube | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"gobot/config" | ||
"io" | ||
"net/http" | ||
"os" | ||
"strings" | ||
) | ||
|
||
type InspectResponse struct { | ||
Status string `json:"status"` | ||
Source string `json:"source"` | ||
Type string `json:"type"` | ||
Data struct { | ||
ExternalID string `json:"externalId"` | ||
PreviewURL *string `json:"previewUrl"` | ||
Name string `json:"name"` | ||
ArtistNames []string `json:"artistNames"` | ||
AlbumName string `json:"albumName"` | ||
ImageURL string `json:"imageUrl"` | ||
ISRC *string `json:"isrc"` | ||
Duration int `json:"duration"` | ||
URL string `json:"url"` | ||
} `json:"data"` | ||
} | ||
|
||
type SearchResponse struct { | ||
Tracks []Track `json:"tracks"` | ||
} | ||
|
||
type Track struct { | ||
Source string `json:"source"` | ||
Status string `json:"status"` | ||
Data SearchData `json:"data"` | ||
Type string `json:"type"` | ||
} | ||
|
||
type SearchData struct { | ||
ExternalID string `json:"externalId"` | ||
PreviewURL *string `json:"previewUrl"` | ||
Name string `json:"name"` | ||
ArtistNames []string `json:"artistNames"` | ||
AlbumName string `json:"albumName"` | ||
ImageURL string `json:"imageUrl"` | ||
ISRC *string `json:"isrc"` | ||
Duration int `json:"duration"` | ||
URL *string `json:"url"` | ||
} | ||
|
||
const ( | ||
baseUrl = "https://api.musicapi.com/public/" | ||
inspectUrlPath = "inspect/url" | ||
searchUrlPath = "search" | ||
|
||
POST = "POST" | ||
YOUTUBE_MUSIC_PREFIX = "https://music.youtube.com/watch?v=" | ||
) | ||
|
||
// makeAPIRequest is a reusable function to handle API requests | ||
func makeAPIRequest(endpoint, payload string) ([]byte, error) { | ||
client := &http.Client{} | ||
url := baseUrl + endpoint | ||
|
||
req, err := http.NewRequest(POST, url, strings.NewReader(payload)) | ||
if err != nil { | ||
return nil, fmt.Errorf("error creating request: %v", err) | ||
} | ||
|
||
// Add necessary headers | ||
token := "Token " + os.Getenv(config.MusicAPItoken) | ||
req.Header.Add("Content-Type", "application/json") | ||
req.Header.Add("Accept", "application/json") | ||
req.Header.Add("Authorization", token) | ||
|
||
// Make the request | ||
res, err := client.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("error making request: %v", err) | ||
} | ||
defer res.Body.Close() | ||
|
||
// Read the response body | ||
body, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("error reading response: %v", err) | ||
} | ||
|
||
return body, nil | ||
} | ||
|
||
// Search performs a track search on the API and prints the parsed response | ||
// Define a custom type for source selection | ||
type SourceType int | ||
|
||
// Define constants for the possible source options | ||
const ( | ||
SPOTIFY_ONLY SourceType = iota | ||
YOUTUBE_ONLY | ||
BOTH | ||
) | ||
|
||
func Search(track string, artist string, sourceType SourceType) (*SearchResponse, error) { | ||
var sources string | ||
|
||
// Determine the sources based on the sourceType | ||
switch sourceType { | ||
case SPOTIFY_ONLY: | ||
sources = `["spotify"]` | ||
case YOUTUBE_ONLY: | ||
sources = `["youtubeMusic"]` | ||
case BOTH: | ||
sources = `["youtubeMusic", "spotify"]` | ||
default: | ||
return nil, fmt.Errorf("invalid source type") | ||
} | ||
|
||
// Build the payload with the selected sources | ||
payload := fmt.Sprintf(`{ | ||
"track": "%s", | ||
"artist": "%s", | ||
"type": "track", | ||
"sources": %s | ||
}`, track, artist, sources) | ||
|
||
// Make the API request | ||
body, err := makeAPIRequest(searchUrlPath, payload) | ||
if err != nil { | ||
return nil, fmt.Errorf("API request error: %w", err) | ||
} | ||
|
||
// Parse the response into SearchResponse struct | ||
var searchResp SearchResponse | ||
err = json.Unmarshal(body, &searchResp) | ||
if err != nil { | ||
return nil, fmt.Errorf("error unmarshalling JSON: %w", err) | ||
} | ||
|
||
// formattedResp, err := json.MarshalIndent(searchResp, "", " ") | ||
// if err != nil { | ||
// log.Println("Error marshalling JSON:", err) | ||
// return nil, err | ||
// } | ||
// fmt.Println(string(formattedResp)) | ||
return &searchResp, nil | ||
} | ||
|
||
// InspectUrl retrieves information about a specific track by URL and prints the parsed response | ||
func InspectUrl(url string) (*InspectResponse, error) { | ||
// Build the payload | ||
payload := fmt.Sprintf(`{ | ||
"url": "%s" | ||
}`, url) | ||
|
||
// Make the API request | ||
body, err := makeAPIRequest(inspectUrlPath, payload) | ||
if err != nil { | ||
return nil, fmt.Errorf("API request error: %w", err) | ||
} | ||
|
||
// Parse the response into InspectResponse struct | ||
var inspectResp InspectResponse | ||
err = json.Unmarshal(body, &inspectResp) | ||
if err != nil { | ||
return nil, fmt.Errorf("error unmarshalling JSON: %w", err) | ||
} | ||
|
||
// Return the parsed InspectResponse and nil error | ||
return &inspectResp, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
package spotifytube | ||
|
||
import ( | ||
"log" | ||
"os" | ||
"regexp" | ||
"strings" | ||
"time" | ||
|
||
"gobot/config" | ||
|
||
tele "gopkg.in/telebot.v3" | ||
) | ||
|
||
// Constants | ||
const ( | ||
helpMessage = `Hello this is SpotifyTubeBot | ||
**Features:** | ||
- Convert YouTube music URL to Spotify URL and vice versa | ||
- Search song using title/artist | ||
**2 ways to convert URL:** | ||
I. Send URL to the bot | ||
II. Type @spotifytubebot followed by URL in chat box on any conversation | ||
Example: | ||
@spotifytubebot https://music.youtube.com/watch?v=ezVbN7e-L7Y | ||
SpotifyTubeBot made with ❤️ by @crossix` | ||
|
||
SPOTIFY_REGEX = `^(https?://)?((www|open)\.spotify\.com)/.+$` | ||
YOUTUBE_REGEX = `^(https?\:\/\/)?((www|music)\.youtube\.com|youtu\.be)\/.+$` | ||
) | ||
|
||
// Compile regex patterns once | ||
var ( | ||
spotifyPattern = regexp.MustCompile(SPOTIFY_REGEX) | ||
youtubePattern = regexp.MustCompile(YOUTUBE_REGEX) | ||
) | ||
|
||
// Run initializes the bot and starts listening for messages | ||
func Run() { | ||
pref := tele.Settings{ | ||
Token: os.Getenv(config.SpotifytubeBot), | ||
Poller: &tele.LongPoller{Timeout: 10 * time.Second}, | ||
} | ||
|
||
bot, err := tele.NewBot(pref) | ||
if err != nil { | ||
log.Fatal(err) | ||
return | ||
} | ||
|
||
// Define a common help handler | ||
helpHandler := func(c tele.Context) error { | ||
return c.Send(helpMessage, &tele.SendOptions{ | ||
ParseMode: tele.ModeMarkdown, | ||
}) | ||
} | ||
|
||
// Register the help handler for both commands | ||
bot.Handle("/help", helpHandler) | ||
bot.Handle("/start", helpHandler) | ||
|
||
// Handle incoming text messages | ||
bot.Handle(tele.OnText, handleTextMessage) | ||
|
||
// Start the bot | ||
bot.Start() | ||
} | ||
|
||
// handleTextMessage processes incoming text messages | ||
func handleTextMessage(c tele.Context) error { | ||
messageText := c.Text() | ||
|
||
// Check for Spotify URL | ||
if spotifyPattern.MatchString(messageText) { | ||
return handleSpotifyURL(c, messageText) | ||
} | ||
|
||
// Check for YouTube URL | ||
if youtubePattern.MatchString(messageText) { | ||
return handleYoutubeURL(c, messageText) | ||
} | ||
|
||
return nil // No action taken for other messages | ||
} | ||
|
||
// handleSpotifyURL processes Spotify URLs | ||
func handleSpotifyURL(c tele.Context, messageText string) error { | ||
resp, err := InspectUrl(messageText) | ||
if err != nil { | ||
return sendErrorMessage(c, "Invalid Spotify URL") | ||
} | ||
|
||
searchResp, err := Search(resp.Data.Name, resp.Data.ArtistNames[0], YOUTUBE_ONLY) | ||
if err != nil { | ||
return sendErrorMessage(c, "Can't convert") | ||
} | ||
return sendTrackURLs(c, false, searchResp) | ||
} | ||
|
||
// handleYoutubeURL processes YouTube URLs | ||
func handleYoutubeURL(c tele.Context, messageText string) error { | ||
resp, err := InspectUrl(messageText) | ||
if err != nil { | ||
return sendErrorMessage(c, "Invalid YouTube URL") | ||
} | ||
|
||
searchResp, err := Search(resp.Data.Name, resp.Data.ArtistNames[0], SPOTIFY_ONLY) | ||
if err != nil { | ||
return sendErrorMessage(c, "Can't convert") | ||
} | ||
return sendTrackURLs(c, true, searchResp) | ||
} | ||
|
||
// sendErrorMessage sends an error message to the user | ||
func sendErrorMessage(c tele.Context, message string) error { | ||
return c.Send(message) | ||
} | ||
|
||
// sendTrackURLs sends the track URLs to the user | ||
func sendTrackURLs(c tele.Context, isSpotify bool, searchResp *SearchResponse) error { | ||
// Check if there are tracks in the response | ||
if len(searchResp.Tracks) == 0 { | ||
return c.Send("No tracks found.") | ||
} | ||
|
||
// Create a slice to hold the URLs | ||
var urls []string | ||
|
||
// Loop through each track and collect the URLs | ||
for _, track := range searchResp.Tracks { | ||
if isSpotify { | ||
if track.Data.URL != nil { | ||
urls = append(urls, *track.Data.URL) | ||
} else { | ||
log.Println("Warning: track.Data.URL is nil, skipping this entry.") | ||
} | ||
} else { | ||
urls = append(urls, YOUTUBE_MUSIC_PREFIX+ *&track.Data.ExternalID) | ||
} | ||
} | ||
|
||
// Check if any URLs were collected | ||
if len(urls) == 0 { | ||
return c.Send("No URLs found in the track data.") | ||
} | ||
|
||
// Format the URLs for sending | ||
responseMessage := strings.Join(urls, "\n") | ||
|
||
// Send the response message | ||
return c.Send(responseMessage) | ||
} |