mirror of
https://github.com/cgzirim/seek-tune.git
synced 2025-12-17 08:54:19 +00:00
A couple more changes
This commit is contained in:
parent
a1ba649480
commit
439b5442f5
7 changed files with 214 additions and 108 deletions
|
|
@ -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);
|
||||
|
|
|
|||
176
server.go
176
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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue