diff --git a/client/src/App.js b/client/src/App.js index 1ca6bc0..d4bf27b 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -13,6 +13,16 @@ function App() { const [serverEngaged, setServerEngaged] = useState(false); const [peerConnection, setPeerConnection] = useState(); + function cleanUp() { + if (stream != null) { + stream.getTracks().forEach((track) => track.stop()); + } + setOffer(null); + setStream(null); + setPeerConnection(null); + setServerEngaged(false); + } + // Function to initiate the client peer function initiateClientPeer(stream = null) { const peer = new Peer({ @@ -71,8 +81,13 @@ function App() { socket.on("matches", (matches) => { matches = JSON.parse(matches); - setMatches(matches); - console.log("Matches: ", matches); + if (matches) { + setMatches(matches); + console.log("Matches: ", matches); + } else { + console.log("No Matches"); + } + cleanUp(); }); socket.on("downloadStatus", (msg) => { @@ -87,6 +102,20 @@ function App() { console.log("Playlist stat: ", msg); }); + useEffect(() => { + const emitTotalSongs = () => { + socket.emit("totalSongs", ""); + }; + + const intervalId = setInterval(emitTotalSongs, 8000); + + return () => clearInterval(intervalId); + }, []); + + socket.on("totalSongs", (totalSongs) => { + console.log("Total songs in DB: ", totalSongs); + }); + const streamAudio = () => { navigator.mediaDevices .getDisplayMedia({ audio: true }) @@ -109,7 +138,7 @@ function App() { setOffer(JSON.stringify(data)); console.log("Offer should be reset"); }); - setStream(stream); // Set the audio stream to state + setStream(stream); }) .catch((error) => { console.error("Error accessing user media:", error); diff --git a/server.go b/server.go index 4a21bc8..cce064e 100644 --- a/server.go +++ b/server.go @@ -7,6 +7,7 @@ import ( "net/http" "song-recognition/signal" "song-recognition/spotify" + "song-recognition/utils" "strings" "github.com/gin-gonic/gin" @@ -43,9 +44,10 @@ func main() { server := socketio.NewServer(nil) - server.OnConnect("/", func(s socketio.Conn) error { - s.SetContext("") - log.Println("CONNECTED: ", s.ID()) + server.OnConnect("/", func(socket socketio.Conn) error { + socket.SetContext("") + log.Println("CONNECTED: ", socket.ID()) + return nil }) @@ -56,72 +58,21 @@ func main() { s.Emit("initAnswer", signal.Encode(*peerConnection.LocalDescription())) }) - server.OnEvent("/", "engage", func(s socketio.Conn, encodedOffer string) { - log.Println("engage: ", encodedOffer) - - peerConnection := signal.SetupWebRTC(encodedOffer) - - // Allow us to receive 1 audio track - if _, err := peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil { - panic(err) - } - - // Set a handler for when a new remote track starts, this handler saves buffers to disk as - // an Ogg file. - oggFile, err := oggwriter.New("output.ogg", 44100, 1) + server.OnEvent("/", "totalSongs", func(socket socketio.Conn) { + db, err := utils.NewDbClient() if err != nil { - panic(err) + log.Printf("Error connecting to DB: %v", err) + return + } + defer db.Close() + + totalSongs, err := db.TotalSongs() + if err != nil { + log.Println("Log error getting total songs count:", err) + return } - peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { - codec := track.Codec() - if strings.EqualFold(codec.MimeType, webrtc.MimeTypeOpus) { - fmt.Println("Got Opus track, saving to disk as output.opus (44.1 kHz, 1 channel)") - // signal.SaveToDisk(oggFile, track) - // TODO turn match to json here - matches, err := signal.MatchSampleAudio(track) - if err != nil { - panic(err) - } - - jsonData, err := json.Marshal(matches[:5]) - if err != nil { - fmt.Println("Log error: ", err) - return - } - - fmt.Println(string(jsonData)) - - s.Emit("matches", string(jsonData)) - peerConnection.Close() - } - }) - - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { - fmt.Printf("Connection State has changed %s \n", connectionState.String()) - - if connectionState == webrtc.ICEConnectionStateConnected { - fmt.Println("Ctrl+C the remote client to stop the demo") - } else if connectionState == webrtc.ICEConnectionStateFailed || connectionState == webrtc.ICEConnectionStateClosed { - if closeErr := oggFile.Close(); closeErr != nil { - panic(closeErr) - } - - fmt.Println("Done writing media files") - - // Gracefully shutdown the peer connection - if closeErr := peerConnection.Close(); closeErr != nil { - panic(closeErr) - } - - // os.Exit(0) - } - }) - - // Emit answer in base64 - s.Emit("serverEngaged", signal.Encode(*peerConnection.LocalDescription())) + socket.Emit("totalSongs", totalSongs) }) server.OnEvent("/", "newDownload", func(socket socketio.Conn, spotifyURL string) { @@ -178,13 +129,31 @@ func main() { socket.Emit("downloadStatus", fmt.Sprintf("%d songs downloaded from playlist", totalTracksDownloaded)) } else if strings.Contains(spotifyURL, "track") { - // check if track already exist trackInfo, err := spotify.TrackInfo(spotifyURL) if err != nil { fmt.Println("log error: ", err) return } + // check if track already exist + db, err := utils.NewDbClient() + if err != nil { + fmt.Errorf("Log - error connecting to DB: %d", err) + } + defer db.Close() + + chunkTag, err := db.GetChunkTagForSong(trackInfo.Title, trackInfo.Artist) + if err != nil { + fmt.Println("chunkTag error: ", err) + } + + if chunkTag != nil { + socket.Emit("downloadStatus", fmt.Sprintf( + "'%s' by '%s' already exists in the database (https://www.youtube.com/watch?v=%s)", + trackInfo.Title, trackInfo.Artist, chunkTag["youtubeid"])) + return + } + err = spotify.DlSingleTrack(spotifyURL, tmpSongDir) if err != nil { socket.Emit("downloadStatus", fmt.Sprintf("Failed to download '%s' by '%s'", trackInfo.Title, trackInfo.Artist)) @@ -199,6 +168,79 @@ func main() { } }) + server.OnEvent("/", "engage", func(s socketio.Conn, encodedOffer string) { + log.Println("engage: ", encodedOffer) + + peerConnection := signal.SetupWebRTC(encodedOffer) + + // Allow us to receive 1 audio track + if _, err := peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil { + panic(err) + } + + // Set a handler for when a new remote track starts, this handler saves buffers to disk as + // an Ogg file. + oggFile, err := oggwriter.New("output.ogg", 44100, 1) + if err != nil { + panic(err) + } + + peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { + codec := track.Codec() + if strings.EqualFold(codec.MimeType, webrtc.MimeTypeOpus) { + fmt.Println("Got Opus track, saving to disk as output.opus (44.1 kHz, 1 channel)") + // signal.SaveToDisk(oggFile, track) + // TODO turn match to json here + matches, err := signal.MatchSampleAudio(track) + if err != nil { + panic(err) + } + + jsonData, err := json.Marshal(matches) + + if len(matches) > 5 { + jsonData, err = json.Marshal(matches[:5]) + } + + if err != nil { + fmt.Println("Log error: ", err) + return + } + + fmt.Println(string(jsonData)) + + s.Emit("matches", string(jsonData)) + peerConnection.Close() + } + }) + + // Set the handler for ICE connection state + // This will notify you when the peer has connected/disconnected + peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { + fmt.Printf("Connection State has changed %s \n", connectionState.String()) + + if connectionState == webrtc.ICEConnectionStateConnected { + fmt.Println("Ctrl+C the remote client to stop the demo") + } else if connectionState == webrtc.ICEConnectionStateFailed || connectionState == webrtc.ICEConnectionStateClosed { + if closeErr := oggFile.Close(); closeErr != nil { + panic(closeErr) + } + + fmt.Println("Done writing media files") + + // Gracefully shutdown the peer connection + if closeErr := peerConnection.Close(); closeErr != nil { + panic(closeErr) + } + + // os.Exit(0) + } + }) + + // Emit answer in base64 + s.Emit("serverEngaged", signal.Encode(*peerConnection.LocalDescription())) + }) + server.OnError("/", func(s socketio.Conn, e error) { log.Println("meet error:", e) }) diff --git a/shazam/shazam.go b/shazam/shazam.go index 73b8709..dd6c7a6 100644 --- a/shazam/shazam.go +++ b/shazam/shazam.go @@ -26,7 +26,7 @@ const ( ) type ChunkTag struct { - SongName string + SongTitle string SongArtist string YouTubeID string TimeStamp string @@ -45,14 +45,14 @@ func Match(sampleAudio []byte) ([]primitive.M, error) { var chunkTags = make(map[string]primitive.M) var songsTimestamps = make(map[string][]string) for _, chunkfgp := range chunkFingerprints { - listOfChunkTags, err := db.GetChunkData(chunkfgp) + listOfChunkTags, err := db.GetChunkTags(chunkfgp) if err != nil { return nil, fmt.Errorf("error getting chunk data with fingerprint %d: %v", chunkfgp, err) } for _, chunkTag := range listOfChunkTags { timeStamp := fmt.Sprint(chunkTag["timestamp"]) - songKey := fmt.Sprintf("%s by %s", chunkTag["songname"], chunkTag["songartist"]) + songKey := fmt.Sprintf("%s by %s", chunkTag["songtitle"], chunkTag["songartist"]) if songsTimestamps[songKey] == nil { songsTimestamps[songKey] = []string{timeStamp} diff --git a/spotify/downloader.go b/spotify/downloader.go index 8017ad2..4f74db7 100644 --- a/spotify/downloader.go +++ b/spotify/downloader.go @@ -308,7 +308,7 @@ func correctFilename(title, artist string) (string, string) { return title, artist } -func processAndSaveSong(m4aFile, songName, songArtist, ytID string) error { +func processAndSaveSong(m4aFile, songTitle, songArtist, ytID string) error { db, err := utils.NewDbClient() if err != nil { return fmt.Errorf("error connecting to DB: %d", err) @@ -316,7 +316,7 @@ func processAndSaveSong(m4aFile, songName, songArtist, ytID string) error { defer db.Close() // Check if the song has been processed and saved before - songKey := fmt.Sprintf("%s - %s", songName, songArtist) + songKey := fmt.Sprintf("%s - %s", songTitle, songArtist) songExists, err := db.SongExists(songKey) if err != nil { return fmt.Errorf("error checking if song exists: %v", err) @@ -345,10 +345,10 @@ func processAndSaveSong(m4aFile, songName, songArtist, ytID string) error { lines := strings.Split(string(output), "\n") // bitDepth, _ := strconv.Atoi(strings.TrimSpace(lines[1])) sampleRate, _ := strconv.Atoi(strings.TrimSpace(lines[0])) - fmt.Printf("SAMPLE RATE for %s: %v", songName, sampleRate) + fmt.Printf("SAMPLE RATE for %s: %v", songTitle, sampleRate) chunkTag := shazam.ChunkTag{ - SongName: songName, + SongTitle: songTitle, SongArtist: songArtist, YouTubeID: ytID, } @@ -358,8 +358,8 @@ func processAndSaveSong(m4aFile, songName, songArtist, ytID string) error { _, fingerprints := shazam.FingerprintChunks(chunks, &chunkTag) // Save fingerprints to MongoDB - for fgp, chunkData := range fingerprints { - err := db.InsertChunkData(fgp, chunkData) + for fgp, ctag := range fingerprints { + err := db.InsertChunkTag(fgp, ctag) if err != nil { return fmt.Errorf("error inserting document: %v", err) } diff --git a/spotify/spotify.go b/spotify/spotify.go index 58060e3..139b1a9 100644 --- a/spotify/spotify.go +++ b/spotify/spotify.go @@ -138,9 +138,6 @@ func TrackInfo(url string) (*Track, error) { Album: gjson.Get(jsonResponse, "data.trackUnion.albumOfTrack.name").String(), } - fmt.Println("ARTISTS: ", allArtists) - fmt.Println("TRACK: ", track) - return track.buildTrack(), nil } @@ -239,9 +236,11 @@ func jsonList(resourceType, id string, offset, limit int64) (string, error) { func (t *Track) buildTrack() *Track { track := &Track{ - Title: t.Title, - Artist: t.Artist, - Album: t.Album, + Title: t.Title, + Artist: t.Artist, + Artists: t.Artists, + Duration: t.Duration, + Album: t.Album, } return track diff --git a/spotify/youtube.go b/spotify/youtube.go index 39ef8c6..2eadc42 100644 --- a/spotify/youtube.go +++ b/spotify/youtube.go @@ -21,7 +21,7 @@ import ( const developerKey = "AIzaSyC3nBFKqudeMItXnYKEeOUryLKhXnqBL7M" // https://github.com/BharatKalluri/spotifydl/blob/v0.1.0/src/youtube.go -func VideoID(spTrack Track) (string, error) { +func getYoutubeIdWithAPI(spTrack Track) (string, error) { service, err := youtube.NewService(context.TODO(), option.WithAPIKey(developerKey)) if err != nil { log.Fatalf("Error creating new YouTube client: %v", err) @@ -77,10 +77,9 @@ func convertStringDurationToSeconds(durationStr string) int { } // GetYoutubeId takes the query as string and returns the search results video ID's -func GetYoutubeId(spTrack Track) (string, error) { - artists := strings.Join(spTrack.Artists, ", ") - songDurationInSeconds := spTrack.Duration * 60 - searchQuery := fmt.Sprintf("'%s' %s %s", spTrack.Title, artists, spTrack.Album) +func GetYoutubeId(track Track) (string, error) { + songDurationInSeconds := track.Duration * 60 + searchQuery := fmt.Sprintf("'%s' %s %s", track.Title, track.Artist, track.Album) searchResults, err := ytSearch(searchQuery, 10) if err != nil { @@ -99,6 +98,7 @@ func GetYoutubeId(spTrack Track) (string, error) { return result.ID, nil } } + // Else return the first result if nothing is found return searchResults[0].ID, nil } diff --git a/utils/dbClient.go b/utils/dbClient.go index 5d99e48..d4cbbc1 100644 --- a/utils/dbClient.go +++ b/utils/dbClient.go @@ -35,6 +35,16 @@ func (db *DbClient) Close() error { return nil } +func (db *DbClient) TotalSongs() (int, error) { + existingSongsCollection := db.client.Database("song-recognition").Collection("existing-songs") + total, err := existingSongsCollection.CountDocuments(context.Background(), bson.D{}) + if err != nil { + return 0, err + } + + return int(total), nil +} + func (db *DbClient) SongExists(key string) (bool, error) { existingSongsCollection := db.client.Database("song-recognition").Collection("existing-songs") filter := bson.M{"_id": key} @@ -59,7 +69,7 @@ func (db *DbClient) RegisterSong(key string) error { return nil } -func (db *DbClient) InsertChunkData(chunkfgp int64, chunkData interface{}) error { +func (db *DbClient) InsertChunkTag(chunkfgp int64, chunkTag interface{}) error { chunksCollection := db.client.Database("song-recognition").Collection("chunks") filter := bson.M{"fingerprint": chunkfgp} @@ -67,9 +77,9 @@ func (db *DbClient) InsertChunkData(chunkfgp int64, chunkData interface{}) error var result bson.M err := chunksCollection.FindOne(context.Background(), filter).Decode(&result) if err == nil { - // If the fingerprint already exists, append the chunkData to the existing list + // If the fingerprint already exists, append the chunkTag to the existing list // fmt.Println("DUPLICATE FINGERPRINT: ", chunkfgp) - update := bson.M{"$push": bson.M{"chunkData": chunkData}} + update := bson.M{"$push": bson.M{"chunkTags": chunkTag}} _, err := chunksCollection.UpdateOne(context.Background(), filter, update) if err != nil { return fmt.Errorf("error updating chunk data: %v", err) @@ -80,7 +90,7 @@ func (db *DbClient) InsertChunkData(chunkfgp int64, chunkData interface{}) error } // If the document doesn't exist, insert a new document - _, err = chunksCollection.InsertOne(context.Background(), bson.M{"fingerprint": chunkfgp, "chunkData": []interface{}{chunkData}}) + _, err = chunksCollection.InsertOne(context.Background(), bson.M{"fingerprint": chunkfgp, "chunkTags": []interface{}{chunkTag}}) if err != nil { return fmt.Errorf("error inserting chunk data: %v", err) } @@ -88,16 +98,7 @@ func (db *DbClient) InsertChunkData(chunkfgp int64, chunkData interface{}) error return nil } -type chunkData struct { - SongName string `bson:"songName"` - SongArtist string `bson:"songArtist"` - BitDepth int `bson:"bitDepth"` - Channels int `bson:"channels"` - SamplingRate int `bson:"samplingRate"` - TimeStamp string `bson:"timeStamp"` -} - -func (db *DbClient) GetChunkData(chunkfgp int64) ([]primitive.M, error) { +func (db *DbClient) GetChunkTags(chunkfgp int64) ([]primitive.M, error) { chunksCollection := db.client.Database("song-recognition").Collection("chunks") filter := bson.M{"fingerprint": chunkfgp} @@ -111,10 +112,45 @@ func (db *DbClient) GetChunkData(chunkfgp int64) ([]primitive.M, error) { return nil, fmt.Errorf("error retrieving chunk data: %w", err) } - var listOfChunkData []primitive.M - for _, data := range result["chunkData"].(primitive.A) { - listOfChunkData = append(listOfChunkData, data.(primitive.M)) + var listOfChunkTags []primitive.M + for _, data := range result["chunkTags"].(primitive.A) { + listOfChunkTags = append(listOfChunkTags, data.(primitive.M)) } - return listOfChunkData, nil + return listOfChunkTags, nil +} + +func (db *DbClient) GetChunkTagForSong(songTitle, songArtist string) (bson.M, error) { + chunksCollection := db.client.Database("song-recognition").Collection("chunks") + + filter := bson.M{ + "chunkTags": bson.M{ + "$elemMatch": bson.M{ + "songtitle": songTitle, + "songartist": songArtist, + }, + }, + } + + var result bson.M + if err := chunksCollection.FindOne(context.Background(), filter).Decode(&result); err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, fmt.Errorf("error finding chunk: %v", err) + } + + var chunkTag map[string]interface{} + for _, chunk := range result["chunkTags"].(primitive.A) { + chunkMap, ok := chunk.(primitive.M) + if !ok { + continue + } + if chunkMap["songtitle"] == songTitle && chunkMap["songartist"] == songArtist { + chunkTag = chunkMap + break + } + } + + return chunkTag, nil }