diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0d310ea --- /dev/null +++ b/go.mod @@ -0,0 +1,84 @@ +module song-recognition + +go 1.21.6 + +require ( + cloud.google.com/go/compute v1.23.4 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/adrg/strutil v0.3.1 // indirect + github.com/bitly/go-simplejson v0.5.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/giorgisio/goav v0.1.0 // indirect + github.com/go-audio/audio v1.0.0 // indirect + github.com/go-audio/riff v1.0.0 // indirect + github.com/go-audio/wav v1.1.0 // indirect + github.com/go-fingerprint/fingerprint v0.0.0-20140803133125-29397256b7ff // indirect + github.com/go-fingerprint/gochroma v0.0.0-20211004000611-a294aa5ccab6 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.1 // indirect + github.com/hajimehoshi/go-mp3 v0.3.4 // indirect + github.com/kkdai/youtube/v2 v2.10.0 // indirect + github.com/klauspost/compress v1.17.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/pion/datachannel v1.5.5 // indirect + github.com/pion/dtls/v2 v2.2.10 // indirect + github.com/pion/ice/v3 v3.0.3 // indirect + github.com/pion/interceptor v0.1.25 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/mdns v0.0.12 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.13 // indirect + github.com/pion/rtp v1.8.3 // indirect + github.com/pion/sctp v1.8.12 // indirect + github.com/pion/sdp/v3 v3.0.6 // indirect + github.com/pion/srtp/v3 v3.0.1 // indirect + github.com/pion/stun/v2 v2.0.0 // indirect + github.com/pion/transport/v2 v2.2.4 // indirect + github.com/pion/transport/v3 v3.0.1 // indirect + github.com/pion/turn/v3 v3.0.1 // indirect + github.com/pion/webrtc/v4 v4.0.0-beta.9 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/raitonoberu/ytmusic v0.0.0-20220927155833-3d1de71caa11 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/tidwall/gjson v1.17.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + go.mongodb.org/mongo-driver v1.14.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect + go.opentelemetry.io/otel v1.23.0 // indirect + go.opentelemetry.io/otel/metric v1.23.0 // indirect + go.opentelemetry.io/otel/trace v1.23.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/oauth2 v0.17.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/api v0.166.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/grpc v1.61.1 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f9eca2b --- /dev/null +++ b/go.sum @@ -0,0 +1,386 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute v1.23.4 h1:EBT9Nw4q3zyE7G45Wvv3MzolIrCJEuHys5muLY0wvAw= +cloud.google.com/go/compute v1.23.4/go.mod h1:/EJMj55asU6kAFnuZET8zqgwgJ9FvXWXOkkfQZa4ioI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ZekeLu/go-mp3 v0.3.5-pre h1:D2Ttzfp/ZazLKVryN0Hv0kyuKbej0ofU1Cagwu+e8zA= +github.com/ZekeLu/go-mp3 v0.3.5-pre/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= +github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4= +github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6MspPA= +github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/giorgisio/goav v0.1.0 h1:ZyfG3NfX7PMSimv4ulhmnQJf/XeHpMdGCn+afRmY5Oc= +github.com/giorgisio/goav v0.1.0/go.mod h1:RtH8HyxLRLU1iY0pjfhWBKRhnbsnmfoI+FxMwb5bfEo= +github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= +github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= +github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= +github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= +github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= +github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= +github.com/go-fingerprint/fingerprint v0.0.0-20140803133125-29397256b7ff h1:MVMRAz9+9uI8+nzBPHILUVLNzH+jaKKmGmEf5GrgNsM= +github.com/go-fingerprint/fingerprint v0.0.0-20140803133125-29397256b7ff/go.mod h1:p+iFTUBRUOKBOZtWQCQAZHhLI7fC5bMdiDc5B4PPBU0= +github.com/go-fingerprint/gochroma v0.0.0-20211004000611-a294aa5ccab6 h1:ofe4/jf63isEPwFgoy81WKWzuA3gfegrthdJXgdXKJI= +github.com/go-fingerprint/gochroma v0.0.0-20211004000611-a294aa5ccab6/go.mod h1:zsgLdL2ov2nW56GWAEjcSp75I6ZIZjxnToJGZ/ouYwQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.1 h1:9F8GV9r9ztXyAi00gsMQHNoF51xPZm8uj1dpYt2ZETM= +github.com/googleapis/gax-go/v2 v2.12.1/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= +github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= +github.com/gosuri/uiprogress v0.0.0-20170224063937-d0567a9d84a1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0= +github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= +github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= +github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/kkdai/youtube/v2 v2.10.0 h1:s8gSWo3AxIafK560XwDVnha9aPXp3N2HQAh1x81R5Og= +github.com/kkdai/youtube/v2 v2.10.0/go.mod h1:H5MLUXiXYuovcEhQT/uZf7BC/syIbAJlDKCDsG+WDsU= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= +github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA= +github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/ice/v3 v3.0.3 h1:Mu5QkZ2pYmcjq9JETDcDR7F8UzjP1VHmcZmgU0yqsyk= +github.com/pion/ice/v3 v3.0.3/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= +github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc= +github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= +github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= +github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo= +github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8= +github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sctp v1.8.12 h1:2VX50pedElH+is6FI+OKyRTeN5oy4mrk2HjnGa3UCmY= +github.com/pion/sctp v1.8.12/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI= +github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= +github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= +github.com/pion/srtp/v3 v3.0.1 h1:AkIQRIZ+3tAOJMQ7G301xtrD1vekQbNeRO7eY1K8ZHk= +github.com/pion/srtp/v3 v3.0.1/go.mod h1:3R3a1qIOIxBkVTLGFjafKK6/fJoTdQDhcC67HOyMbJ8= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= +github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/turn/v3 v3.0.1 h1:wLi7BTQr6/Q20R0vt/lHbjv6y4GChFtC33nkYbasoT8= +github.com/pion/turn/v3 v3.0.1/go.mod h1:MrJDKgqryDyWy1/4NT9TWfXWGMC7UHT6pJIv1+gMeNE= +github.com/pion/webrtc/v4 v4.0.0-beta.9 h1:xmTVa6aia4fzOSP4Ki/hB7dKKtcIqaPI6YSfGDa5JZE= +github.com/pion/webrtc/v4 v4.0.0-beta.9/go.mod h1:z/hdYIuZUz2MFSdPKf099qRVAyTJwvy2c0nwRItCgZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/raitonoberu/ytmusic v0.0.0-20220927155833-3d1de71caa11 h1:jpddPIqdeF+TxOT1Zzd1u+k9AiVT4XJX/VuyZUnrgZE= +github.com/raitonoberu/ytmusic v0.0.0-20220927155833-3d1de71caa11/go.mod h1:hgP4hPl8kmhAaMjuaxxqKnHa7yA9UkXw4KY97XLyjRs= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= +go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E= +go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= +go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo= +go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= +go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI= +go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.166.0 h1:6m4NUwrZYhAaVIHZWxaKjw1L1vNAjtMwORmKRyEEo24= +google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 h1:g/4bk7P6TPMkAUbUhquq98xey1slwvuVJPosdBqYJlU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/imain.go b/imain.go new file mode 100644 index 0000000..53f122d --- /dev/null +++ b/imain.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "path/filepath" + "song-recognition/shazam" + "song-recognition/spotify" + "song-recognition/utils" + "strings" +) + +func matchSong(songPath string) error { + m4aFileMono := strings.TrimSuffix(songPath, filepath.Ext(songPath)) + "_mono.m4a" + audioBytes, err := spotify.ConvertM4aToMono(songPath, m4aFileMono) + if err != nil { + return fmt.Errorf("error converting M4A file to mono: %v", err) + } + + chunks := shazam.Chunkify(audioBytes) + fingerpints, _ := shazam.FingerprintChunks(chunks, nil) + + for _, fingerprint := range fingerpints { + db, err := utils.NewDbClient() + if err != nil { + return fmt.Errorf("error connecting to DB: %d", err) + } + chunkData, err := db.GetChunkData(fingerprint) + if err != nil { + return fmt.Errorf("error retrieving chunk data: %d", err) + } + fmt.Println("CHUNK DATA: ", chunkData) + } + + return nil +} + +func imain() { + // Example usage + // Open the MP3 file + // mp3FilePath := "spotifydown.com - These Are The Days.mp3" + // signal.Process_and_SaveSong(mp3FilePath, "These Are The Days", "lauren Daigle") + + // https://open.spotify.com/track/3vnKyPnHMunE1bMXYQHFHU?si=34a43de5712c4331 - heaven has come + // https://open.spotify.com/track/7zwSMMJkrRJNvxFO9w42nA?si=fa7cef0f7bd14904 - we raise a sound Nosa and 121SELAH + // https://open.spotify.com/track/52WA7y6ACfdHbzIii6M9iA?si=8aa26d3974394645 - these are the days + + // spotify.DlSingleTrack("https://open.spotify.com/track/3vnKyPnHMunE1bMXYQHFHU?si=34a43de5712c4331", + // "/home/chigozirim/Documents/my-docs/song-recognition/songs/") + + // spotify.DlSingleTrack("https://open.spotify.com/track/7zwSMMJkrRJNvxFO9w42nA?si=fa7cef0f7bd14904", + // "/home/chigozirim/Documents/my-docs/song-recognition/songs/") + + // spotify.DlSingleTrack("https://open.spotify.com/track/52WA7y6ACfdHbzIii6M9iA?si=8aa26d3974394645", + // "/home/chigozirim/Documents/my-docs/song-recognition/songs/") + + spotify.DlPlaylist("https://open.spotify.com/playlist/7EAqBCOVkDZcbccjxZmgjp?si=bbc07260fb784861", + "/home/chigozirim/Documents/my-docs/song-recognition/songs/") + + // err := matchSong("/home/chigozirim/Documents/my-docs/song-recognition/songs/We Raise A Sound - Nosa.m4a") + // if err != nil { + // fmt.Println("error matching song: ", err) + // return + // } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..077de70 --- /dev/null +++ b/main.go @@ -0,0 +1,277 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +//go:build !js +// +build !js + +// save-to-disk is a simple application that shows how to record your webcam/microphone using Pion WebRTC and save VP8/Opus to disk. +package main + +import ( + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/pion/interceptor" + "github.com/pion/interceptor/pkg/intervalpli" + "github.com/pion/webrtc/v4" + + "song-recognition/shazam" + "song-recognition/signal" + + "github.com/pion/webrtc/v4/pkg/media" + "github.com/pion/webrtc/v4/pkg/media/ivfwriter" + "github.com/pion/webrtc/v4/pkg/media/oggwriter" +) + +func saveToDisk(i media.Writer, track *webrtc.TrackRemote) { + defer func() { + if err := i.Close(); err != nil { + panic(err) + } + }() + + for { + rtpPacket, _, err := track.ReadRTP() + if err != nil { + fmt.Println(err) + return + } + if err := i.WriteRTP(rtpPacket); err != nil { + fmt.Println(err) + return + } + } +} + +func saveToBytes(track *webrtc.TrackRemote) ([]byte, error) { + var audioData []byte + + for { + rtpPacket, _, err := track.ReadRTP() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + + // Extract audio payload from RTP packet + payload := rtpPacket.Payload + + // Append audio payload to audioData + audioData = append(audioData, payload...) + // fmt.Println("ByteArray: ", audioData) + } + + return audioData, nil +} + +func matchSampleAudio(track *webrtc.TrackRemote) (string, error) { + // Use time.After to stop after 15 seconds + stop := time.After(50 * time.Second) + + // Use a ticker to process sampleAudio every 2 seconds + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + var sampleAudio []byte + + for { + select { + case <-ticker.C: + // Process sampleAudio every 2 seconds + if len(sampleAudio) > 0 { + match, err := shazam.Match(sampleAudio) + if err != nil { + fmt.Println(err) + return "", nil + } + + // Reset sampleAudio for fresh input + // sampleAudio = nil + if len(match) > 0 { + fmt.Println("FOUND A MATCH! - ", match) + return match, nil + } + } + case <-stop: + // Stop after 15 seconds + fmt.Println("Stopped after 15 seconds") + return "", nil + default: + // Read RTP packets and accumulate sampleAudio + rtpPacket, _, err := track.ReadRTP() + if err != nil { + if err != io.EOF { + return "", fmt.Errorf("error reading RTP packet: %d", err) + } + return "", err + } + + // Extract audio payload from RTP packet + payload := rtpPacket.Payload + + sampleAudio = append(sampleAudio, payload...) + } + } +} + +// nolint:gocognit +func main() { + // Everything below is the Pion WebRTC API! Thanks for using it ❤️. + + // Create a MediaEngine object to configure the supported codec + m := &webrtc.MediaEngine{} + + // Setup the codecs you want to use. + // We'll use a VP8 and Opus but you can also define your own + if err := m.RegisterCodec(webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, + PayloadType: 96, + }, webrtc.RTPCodecTypeVideo); err != nil { + panic(err) + } + if err := m.RegisterCodec(webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 44100, Channels: 1, SDPFmtpLine: "", RTCPFeedback: nil}, + PayloadType: 111, + }, webrtc.RTPCodecTypeAudio); err != nil { + panic(err) + } + + // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. + // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` + // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry + // for each PeerConnection. + i := &interceptor.Registry{} + + // Register a intervalpli factory + // This interceptor sends a PLI every 3 seconds. A PLI causes a video keyframe to be generated by the sender. + // This makes our video seekable and more error resilent, but at a cost of lower picture quality and higher bitrates + // A real world application should process incoming RTCP packets from viewers and forward them to senders + intervalPliFactory, err := intervalpli.NewReceiverInterceptor() + if err != nil { + panic(err) + } + i.Add(intervalPliFactory) + + // Use the default set of Interceptors + if err = webrtc.RegisterDefaultInterceptors(m, i); err != nil { + panic(err) + } + + // Create the API object with the MediaEngine + api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)) + + // Prepare the configuration + config := webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + } + + // Create a new RTCPeerConnection + peerConnection, err := api.NewPeerConnection(config) + if err != nil { + panic(err) + } + + // Allow us to receive 1 audio track, and 1 video track + if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil { + panic(err) + } else if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil { + panic(err) + } + + // oggFile, err := oggwriter.New("output.ogg", 48000, 2) + oggFile, err := oggwriter.New("output.ogg", 44100, 1) + if err != nil { + panic(err) + } + ivfFile, err := ivfwriter.New("output.ivf") + if err != nil { + panic(err) + } + + // Set a handler for when a new remote track starts, this handler saves buffers to disk as + // an ivf file, since we could have multiple video tracks we provide a counter. + // In your application this is where you would handle/process video + 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 (48 kHz, 2 channels)") + // matchSampleAudio(track) + saveToDisk(oggFile, track) + } else if strings.EqualFold(codec.MimeType, webrtc.MimeTypeVP8) { + fmt.Println("Got VP8 track, saving to disk as output.ivf") + saveToDisk(ivfFile, track) + } + }) + + // 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) + } + + if closeErr := ivfFile.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) + } + }) + + // Wait for the offer to be pasted + offer := webrtc.SessionDescription{} + signal.Decode(signal.MustReadStdin(), &offer) + + // Set the remote SessionDescription + err = peerConnection.SetRemoteDescription(offer) + if err != nil { + panic(err) + } + + // Create answer + answer, err := peerConnection.CreateAnswer(nil) + if err != nil { + panic(err) + } + + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + + // Sets the LocalDescription, and starts our UDP listeners + err = peerConnection.SetLocalDescription(answer) + if err != nil { + panic(err) + } + + // Block until ICE Gathering is complete, disabling trickle ICE + // we do this because we only can exchange one signaling message + // in a production application you should exchange ICE Candidates via OnICECandidate + <-gatherComplete + + // Output the answer in base64 so we can paste it in browser + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) + + // Block forever + select {} +} diff --git a/shazam/fft.go b/shazam/fft.go new file mode 100644 index 0000000..22d9b3d --- /dev/null +++ b/shazam/fft.go @@ -0,0 +1,40 @@ +package shazam + +import ( + "math" + "math/cmplx" +) + +// fft performs the Fast Fourier Transform on the input signal. +func Fft(complexArray []complex128) []complex128 { + fftResult := make([]complex128, len(complexArray)) + copy(fftResult, complexArray) // Copy input to result buffer + return recursiveFFT(fftResult) +} + +// recursiveFFT performs the recursive FFT algorithm. +func recursiveFFT(complexArray []complex128) []complex128 { + N := len(complexArray) + if N <= 1 { + return complexArray + } + + even := make([]complex128, N/2) + odd := make([]complex128, N/2) + for i := 0; i < N/2; i++ { + even[i] = complexArray[2*i] + odd[i] = complexArray[2*i+1] + } + + even = recursiveFFT(even) + odd = recursiveFFT(odd) + + fftResult := make([]complex128, N) + for k := 0; k < N/2; k++ { + t := cmplx.Exp(-2i * math.Pi * complex(float64(k), 0) / complex(float64(N), 0)) + fftResult[k] = even[k] + t*odd[k] + fftResult[k+N/2] = even[k] - t*odd[k] + } + + return fftResult +} diff --git a/shazam/shazam.go b/shazam/shazam.go new file mode 100644 index 0000000..02ac2ab --- /dev/null +++ b/shazam/shazam.go @@ -0,0 +1,264 @@ +package shazam + +import ( + "crypto/sha256" + "encoding/binary" + "fmt" + "math" + "math/cmplx" + "math/rand" + "song-recognition/utils" + "sort" + "time" +) + +// Constants +const ( + chunkSize = 4096 // 4KB + fuzzFactor = 2 + bitDepth = 2 + channels = 1 + samplingRate = 44100 +) + +// AudioInfo contains details about the audio data. +type AudioInfo struct { + SongName string + SongArtist string + BitDepth int + Channels int + SamplingRate int + TimeStamp string // TimeStamp for the chunk +} + +func Match(sampleAudio []byte) (string, error) { + sampleChunks := Chunkify(sampleAudio) + chunkFingerprints, _ := FingerprintChunks(sampleChunks, nil) + + db, err := utils.NewDbClient() + if err != nil { + return "", fmt.Errorf("error connecting to DB: %d", err) + } + defer db.Close() + + var results = make(map[string][]string) + for _, chunkfgp := range chunkFingerprints { + listOfChunkData, err := db.GetChunkData(chunkfgp) + if err != nil { + return "", fmt.Errorf("error getting chunk data with fingerpring %d: %v", chunkfgp, err) + } + + for _, chunkData := range listOfChunkData { + timeStamp := fmt.Sprint(chunkData["timestamp"]) + songKey := fmt.Sprintf("%s by %s", chunkData["songname"], chunkData["songartist"]) + + if results[songKey] == nil { + results[songKey] = []string{timeStamp} + } else { + results[songKey] = append(results[songKey], timeStamp) + } + } + } + + fmt.Println("Results: ", results) + + maxMatchCount := 0 + var maxMatch string + + for songKey, timestamps := range results { + differences, err := timeDifference(timestamps) + if err != nil && err.Error() == "insufficient timestamps" { + continue + } else if err != nil { + return "", err + } + + fmt.Printf("%s DIFFERENCES: %d\n", songKey, differences) + if len(differences) >= 2 { + if len(differences) > maxMatchCount { + maxMatchCount = len(differences) + maxMatch = songKey + } + } + } + + fmt.Println("MATCH: ", maxMatch) + return "", nil +} + +func timeDifference(timestamps []string) ([]int, error) { + if len(timestamps) < 2 { + return nil, fmt.Errorf("insufficient timestamps") + } + + layout := "15:04:05" + + timestampsInSeconds := make([]int, len(timestamps)) + for i, ts := range timestamps { + parsedTime, err := time.Parse(layout, ts) + if err != nil { + return nil, fmt.Errorf("error parsing timestamp %q: %w", ts, err) + } + hours := parsedTime.Hour() + minutes := parsedTime.Minute() + seconds := parsedTime.Second() + timestampsInSeconds[i] = (hours * 3600) + (minutes * 60) + seconds + } + + sort.Ints(timestampsInSeconds) + fmt.Println("timeStampsInSeconds: ", timestampsInSeconds) + + differences := []int{} + + for i := len(timestampsInSeconds) - 1; i >= 1; i-- { + difference := timestampsInSeconds[i] - timestampsInSeconds[i-1] + // maxSeconds = 15 + if difference > 0 && difference <= 15 { + differences = append(differences, difference) + } + } + + return differences, nil +} + +// Chunkify divides the input audio data into chunks of bytes. +// It converts each byte in each chunk to a complex number, performs FFT on each +// chunk and returns the FFT results as a slice of slices of complex128. +func Chunkify(audio []byte) [][]complex128 { + totalSize := len(audio) + totalChunksInAudio := totalSize / chunkSize + + chunks := make([][]complex128, totalChunksInAudio) // Slice of complex arrays + + for i := 0; i < totalChunksInAudio; i++ { + complexArray := make([]complex128, chunkSize) // Initialize a complex array for each chunk + + for j := 0; j < chunkSize; j++ { + // convert each byte in chunk to a complex number + b := audio[(i*chunkSize)+j] + complexArray[j] = complex(float64(b), 0) + } + + chunks[i] = Fft(complexArray) + } + + return chunks +} + +// FingerprintChunks processes a collection of audio data represented as chunks of complex numbers and +// generates fingerprints for each chunk based on the magnitude of frequency components within specific frequency ranges. +func FingerprintChunks(chunks [][]complex128, audioInfo *AudioInfo) ([]int64, map[int64]AudioInfo) { + var fingerprintList []int64 + fingerprintMap := make(map[int64]AudioInfo) + + var bytesPerSecond, chunksPerSecond int + var chunkCount int + var chunkTime time.Time + + if audioInfo != nil { + bytesPerSecond = (samplingRate * bitDepth * channels) / 8 + chunksPerSecond = bytesPerSecond / chunkSize + // if chunkSize == 4096 { + // chunksPerSecond = 10 + // } + chunkCount = 0 + chunkTime = time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC) + } + + for _, chunk := range chunks { + if audioInfo != nil { + chunkCount++ + if chunkCount == chunksPerSecond { + chunkCount = 0 + chunkTime = chunkTime.Add(1 * time.Second) + // fmt.Println(chunkTime.Format("15:04:05")) + } + } + + chunkMags := map[string]int{ + "20-60": 0, "60-250": 0, "250-500": 0, + "500-2000": 0, "2000-4000": 0, "4000-8000": 0, "8000-20000": 0, + } + + for _, frequency := range chunk { + magnitude := int(cmplx.Abs(frequency)) + ranges := []struct{ min, max int }{{20, 60}, {60, 250}, {250, 500}, {500, 2000}, {2000, 4000}, {4000, 8000}, {8000, 20001}} + + for _, r := range ranges { + if magnitude >= r.min && magnitude < r.max && + chunkMags[fmt.Sprintf("%d-%d", r.min, r.max)] < magnitude { + chunkMags[fmt.Sprintf("%d-%d", r.min, r.max)] = magnitude + } + } + } + + // fingerprint := fmt.Sprintf("%d-%d-%d-%d-%d-%d-%d", + // chunkMags["20-60"], + // chunkMags["60-250"], + // chunkMags["250-500"], + // chunkMags["500-2000"], + // chunkMags["2000-4000"], + // chunkMags["4000-8000"], + // chunkMags["8000-20000"]) + + // fmt.Println(fingerprint) + + points := [4]int64{ + int64(chunkMags["60-250"]), + int64(chunkMags["250-500"]), + int64(chunkMags["500-2000"]), + int64(chunkMags["2000-4000"])} + key := hash1(points[:]) + // fmt.Printf("%s: %v\n", fingerprint, key) + + if audioInfo != nil { + newAudioInfo := *audioInfo + newAudioInfo.TimeStamp = chunkTime.Format("15:04:05") + fingerprintMap[key] = newAudioInfo + } else { + fingerprintList = append(fingerprintList, key) + } + } + + return fingerprintList, fingerprintMap +} + +func hash(values []int64) int64 { + if len(values) != 7 { + return 0 // Handle invalid input length + } + + var result int64 + for i := 0; i < len(values); i++ { + roundedValue := values[i] - (values[i] % fuzzFactor) + weight := int64(math.Pow10(len(values) - i - 1)) + result += roundedValue * weight + } + + return result +} + +func hash1(values []int64) int64 { + p1, p2, p3, p4 := values[0], values[1], values[2], values[3] + return (p4-(p4%fuzzFactor))*100000000 + + (p3-(p3%fuzzFactor))*100000 + + (p2-(p2%fuzzFactor))*100 + + (p1 - (p1 % fuzzFactor)) +} + +func hash2(values []int64) int64 { + for i := range values { + values[i] += rand.Int63n(fuzzFactor) - fuzzFactor/2 + } + + var buf []byte + for _, v := range values { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(v)) + buf = append(buf, b...) + } + + hash := sha256.Sum256(buf) + + return int64(binary.BigEndian.Uint64(hash[:8])) +} diff --git a/signal/base64.txt b/signal/base64.txt new file mode 100644 index 0000000..e69de29 diff --git a/signal/http.go b/signal/http.go new file mode 100644 index 0000000..4f81afe --- /dev/null +++ b/signal/http.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package signal + +import ( + "fmt" + "io/ioutil" + "net/http" + "strconv" +) + +// HTTPSDPServer starts a HTTP Server that consumes SDPs +func HTTPSDPServer(port int) chan string { + sdpChan := make(chan string) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + body, _ := ioutil.ReadAll(r.Body) + fmt.Fprintf(w, "done") + sdpChan <- string(body) + }) + + go func() { + // nolint: gosec + err := http.ListenAndServe(":"+strconv.Itoa(port), nil) + if err != nil { + panic(err) + } + }() + + return sdpChan +} diff --git a/signal/rand.go b/signal/rand.go new file mode 100644 index 0000000..c55fc6c --- /dev/null +++ b/signal/rand.go @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package signal + +import "github.com/pion/randutil" + +// RandSeq generates a random string to serve as dummy data +// +// It returns a deterministic sequence of values each time a program is run. +// Use rand.Seed() function in your real applications. +func RandSeq(n int) string { + val, err := randutil.GenerateCryptoRandomString(n, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + if err != nil { + panic(err) + } + + return val +} diff --git a/signal/signal.go b/signal/signal.go new file mode 100644 index 0000000..dd32f9c --- /dev/null +++ b/signal/signal.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +// Package signal contains helpers to exchange the SDP session +// description between examples. +package signal + +import ( + "bufio" + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "strings" +) + +// Allows compressing offer/answer to bypass terminal input limits. +const compress = false + +// MustReadStdin blocks until input is received from stdin +func MustReadStdin() string { + filename := "/home/chigozirim/Documents/my-docs/song-recognition/signal/base64.txt" + file, err := os.Open(filename) + if err != nil { + panic(err) + } + defer file.Close() + + r := bufio.NewReaderSize(file, 16384) + + var in string + for { + var err error + in, err = r.ReadString('\n') + if err != io.EOF { + if err != nil { + panic(err) + } + } + in = strings.TrimSpace(in) + if len(in) > 0 { + break + } + } + + fmt.Println("") + + return in +} + +// Encode encodes the input in base64 +// It can optionally zip the input before encoding +func Encode(obj interface{}) string { + b, err := json.Marshal(obj) + if err != nil { + panic(err) + } + + if compress { + b = zip(b) + } + + return base64.StdEncoding.EncodeToString(b) +} + +// Decode decodes the input from base64 +// It can optionally unzip the input after decoding +func Decode(in string, obj interface{}) { + b, err := base64.StdEncoding.DecodeString(in) + if err != nil { + panic(err) + } + + if compress { + b = unzip(b) + } + + err = json.Unmarshal(b, obj) + if err != nil { + panic(err) + } +} + +func zip(in []byte) []byte { + var b bytes.Buffer + gz := gzip.NewWriter(&b) + _, err := gz.Write(in) + if err != nil { + panic(err) + } + err = gz.Flush() + if err != nil { + panic(err) + } + err = gz.Close() + if err != nil { + panic(err) + } + return b.Bytes() +} + +func unzip(in []byte) []byte { + var b bytes.Buffer + _, err := b.Write(in) + if err != nil { + panic(err) + } + r, err := gzip.NewReader(&b) + if err != nil { + panic(err) + } + res, err := ioutil.ReadAll(r) + if err != nil { + panic(err) + } + return res +} diff --git a/spotify/downloader.go b/spotify/downloader.go new file mode 100644 index 0000000..b1dd9ad --- /dev/null +++ b/spotify/downloader.go @@ -0,0 +1,373 @@ +package spotify + +import ( + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "song-recognition/shazam" + "song-recognition/utils" + "strconv" + "strings" + "sync" + "time" + + "github.com/fatih/color" + "github.com/kkdai/youtube/v2" +) + +var yellow = color.New(color.FgYellow) + +func DlSingleTrack(url, savePath string) error { + trackInfo, err := TrackInfo(url) + if err != nil { + return err + } + + fmt.Println("Getting track info...") + time.Sleep(500 * time.Millisecond) + track := []Track{*trackInfo} + + fmt.Println("Now, downloading track...") + err = dlTrack(track, savePath) + if err != nil { + return err + } + + return nil +} + +func DlPlaylist(url, savePath string) error { + tracks, err := PlaylistInfo(url) + if err != nil { + return err + } + + time.Sleep(1 * time.Second) + fmt.Println("Now, downloading playlist...") + err = dlTrack(tracks, savePath) + if err != nil { + fmt.Println(err) + return err + } + + return nil +} + +func dlAlbum(url, savePath string) error { + tracks, err := AlbumInfo(url) + if err != nil { + return err + } + + time.Sleep(1 * time.Second) + fmt.Println("Now, downloading album...") + err = dlTrack(tracks, savePath) + if err != nil { + return err + } + + return nil +} + +func dlTrack(tracks []Track, path string) error { + var wg sync.WaitGroup + var totalTracks int + results := make(chan int, len(tracks)) + numCPUs := runtime.NumCPU() + semaphore := make(chan struct{}, numCPUs) + + for _, t := range tracks { + wg.Add(1) + go func(track Track) { + defer wg.Done() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + + trackCopy := &Track{ + Title: track.Title, + Artist: track.Artist, + Album: track.Album, + } + + id, err := VideoID(*trackCopy) + if id == "" || err != nil { + yellow.Printf("Error (1): '%s' by '%s' could not be downloaded\n", trackCopy.Title, trackCopy.Artist) + return + } + + trackCopy.Title, trackCopy.Artist = correctFilename(trackCopy.Title, trackCopy.Artist) + err = getAudio(id, path, trackCopy.Title, trackCopy.Artist) + if err != nil { + yellow.Printf("Error (2): '%s' by '%s' could not be downloaded\n", trackCopy.Title, trackCopy.Artist) + return + } + // Process and save audio + filename := fmt.Sprintf("%s - %s.m4a", trackCopy.Title, trackCopy.Artist) + route := filepath.Join(path, filename) + err = processAndSaveSong(route, trackCopy.Title, trackCopy.Artist) + if err != nil { + yellow.Println("Error processing audio: ", err) + } + + trackCopy.Title, trackCopy.Artist = correctFilename(trackCopy.Title, trackCopy.Artist) + filePath := fmt.Sprintf("%s%s - %s.m4a", path, trackCopy.Title, trackCopy.Artist) + + if err := addTags(filePath, *trackCopy); err != nil { + yellow.Println("Error adding tags: ", filePath) + return + } + + size, _ := GetFileSize(filePath) + if size < 1 { + DeleteFile(filePath) + } + + fmt.Printf("'%s' by '%s' was downloaded\n", track.Title, track.Artist) + results <- 1 + }(t) + } + + go func() { + wg.Wait() + close(results) + }() + + for result := range results { + totalTracks += result + } + + fmt.Println("Total tracks downloaded:", totalTracks) + return nil + +} + +/* github.com/kkdai/youtube */ +func getAudio(id, path, title, artist string) error { + dir, err := os.Stat(path) + if err != nil { + panic(err) + } + + if !dir.IsDir() { + return errors.New("the path is not valid (not a dir)") + } + + db, err := utils.NewDbClient() + if err != nil { + return fmt.Errorf("error connecting to DB: %d", err) + } + defer db.Close() + + // Check if the song has been processed and saved before + songKey := fmt.Sprintf("%s - %s", title, artist) + songExists, err := db.SongExists(songKey) + if err != nil { + return err + } + if songExists { + fmt.Println("Song exists: ", songKey) + return nil + } + + client := youtube.Client{} + video, err := client.GetVideo(id) + if err != nil { + return err + } + + /* itag code: 140, container: m4a, content: audio, bitrate: 128k */ + /* change the FindByItag parameter to 139 if you want smaller files (but with a bitrate of 48k) */ + formats := video.Formats.Itag(140) + + filename := fmt.Sprintf("%s - %s.m4a", title, artist) + route := filepath.Join(path, filename) + + /* in some cases, when attempting to download the audio + using the library github.com/kkdai/youtube, + the download fails (and shows the file size as 0 bytes) + until the second or third attempt. */ + var fileSize int64 + file, err := os.Create(route) + if err != nil { + return err + } + + for fileSize == 0 { + stream, _, err := client.GetStream(video, &formats[0]) + if err != nil { + return err + } + + if _, err = io.Copy(file, stream); err != nil { + return err + } + + fileSize, _ = GetFileSize(route) + } + defer file.Close() + + return nil +} + +func saveAudioToFile(audioReader io.Reader, path, title, artist string) error { + dir, err := os.Stat(path) + if err != nil { + panic(err) + } + + if !dir.IsDir() { + return errors.New("the path is not valid (not a dir)") + } + + filename := fmt.Sprintf("%s - %s.m4a", title, artist) + route := filepath.Join(path, filename) + + /* in some cases, when attempting to download the audio + using the library github.com/kkdai/youtube, + the download fails (and shows the file size as 0 bytes) + until the second or third attempt. */ + file, err := os.Create(route) + if err != nil { + return err + } + + defer file.Close() + + // Copy the audio stream to the file + _, err = io.Copy(file, audioReader) + if err != nil { + return err + } + + return nil +} + +func addTags(file string, track Track) error { + tempFile := file + index := strings.Index(file, ".m4a") + if index != -1 { + result := tempFile[:index] /* filename but with no extension ('/path/to/title - artist') */ + tempFile = result + "2" + ".m4a" /* just a temporary dumb name ('/path/to/title - artist2.m4a') */ + } + + cmd := exec.Command( + "ffmpeg", + "-i", file, /* /path/to/title - artist.m4a */ + "-c", "copy", + "-metadata", fmt.Sprintf("album_artist=%s", track.Artist), + "-metadata", fmt.Sprintf("title=%s", track.Title), + "-metadata", fmt.Sprintf("artist=%s", track.Artist), + "-metadata", fmt.Sprintf("album=%s", track.Album), + tempFile, /* /path/to/title - artist2.m4a */ + ) + + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println("ERROR FROM CMD:", err) + fmt.Println("FFMPEG Output:", string(out)) + return err + } + // if err := cmd.Run(); err != nil { + // fmt.Println("ERROR FROM CMD: ", err) + // return err + // } + + /* removes '2' from file name */ + if err := os.Rename(tempFile, file); err != nil { + return err + } + + return nil +} + +/* fixes some invalid file names (windows is the capricious one) */ +func correctFilename(title, artist string) (string, string) { + if runtime.GOOS == "windows" { + invalidChars := []byte{'<', '>', '<', ':', '"', '\\', '/', '|', '?', '*'} + for _, invalidChar := range invalidChars { + title = strings.ReplaceAll(title, string(invalidChar), "") + artist = strings.ReplaceAll(artist, string(invalidChar), "") + } + } else { + title = strings.ReplaceAll(title, "/", "\\") + artist = strings.ReplaceAll(artist, "/", "\\") + } + + return title, artist +} + +func processAndSaveSong(m4aFile, songName, songArtist string) error { + db, err := utils.NewDbClient() + if err != nil { + return fmt.Errorf("error connecting to DB: %d", err) + } + defer db.Close() + + // Check if the song has been processed and saved before + songKey := fmt.Sprintf("%s - %s", songName, songArtist) + songExists, err := db.SongExists(songKey) + if err != nil { + return err + } + if songExists { + fmt.Println("Song exists: ", songKey) + return fmt.Errorf("error querying existing songs: %v", err) + } + + // Convert M4A file to mono + m4aFileMono := strings.TrimSuffix(m4aFile, filepath.Ext(m4aFile)) + "_mono.m4a" + defer os.Remove(m4aFileMono) // Ensure the temporary output file is deleted + audioBytes, err := ConvertM4aToMono(m4aFile, m4aFileMono) + if err != nil { + return fmt.Errorf("error converting M4A file to mono: %v", err) + } + + // Run ffprobe to get metadata of the input file + cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=bit_depth,sample_rate", "-of", "default=noprint_wrappers=1:nokey=1", m4aFileMono) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error running ffprobe: %v", err) + } + + // Parse the output to extract bit depth and sampling rate + lines := strings.Split(string(output), "\n") + // bitDepth, _ := strconv.Atoi(strings.TrimSpace(lines[1])) + sampleRate, _ := strconv.Atoi(strings.TrimSpace(lines[0])) + + audioInfo := shazam.AudioInfo{ + SongName: songName, + SongArtist: songArtist, + BitDepth: 2, + Channels: 1, + SamplingRate: sampleRate, + } + + fmt.Println("AUDIO INFO: ", audioInfo) + + // Calculate fingerprints + chunks := shazam.Chunkify(audioBytes) + _, fingerprints := shazam.FingerprintChunks(chunks, &audioInfo) + + // Save fingerprints to MongoDB + for fgp, chunkData := range fingerprints { + err := db.InsertChunkData(fgp, chunkData) + if err != nil { + return fmt.Errorf("error inserting document: %v", err) + } + } + + // Save the song as processed + err = db.RegisterSong(songKey) + if err != nil { + return err + } + + fmt.Println("Fingerprints saved to MongoDB successfully") + return nil +} diff --git a/spotify/spotify.go b/spotify/spotify.go new file mode 100644 index 0000000..3cc3eab --- /dev/null +++ b/spotify/spotify.go @@ -0,0 +1,265 @@ +package spotify + +import ( + "errors" + "fmt" + "io" + "math" + "net/http" + "regexp" + "strings" + "time" + + "github.com/tidwall/gjson" +) + +/* for playlists and albums */ +type ResourceEndpoint struct { + Limit, Offset, TotalCount, Requests int64 +} + +type Track struct { + Title, Artist, Album string +} + +const ( + tokenEndpoint = "https://open.spotify.com/get_access_token?reason=transport&productType=web-player" + trackInitialPath = "https://api-partner.spotify.com/pathfinder/v1/query?operationName=getTrack&variables=" + playlistInitialPath = "https://api-partner.spotify.com/pathfinder/v1/query?operationName=fetchPlaylist&variables=" + albumInitialPath = "https://api-partner.spotify.com/pathfinder/v1/query?operationName=getAlbum&variables=" + trackEndPath = `{"persistedQuery":{"version":1,"sha256Hash":"e101aead6d78faa11d75bec5e36385a07b2f1c4a0420932d374d89ee17c70dd6"}}` + playlistEndPath = `{"persistedQuery":{"version":1,"sha256Hash":"b39f62e9b566aa849b1780927de1450f47e02c54abf1e66e513f96e849591e41"}}` + albumEndPath = `{"persistedQuery":{"version":1,"sha256Hash":"46ae954ef2d2fe7732b4b2b4022157b2e18b7ea84f70591ceb164e4de1b5d5d3"}}` +) + +func accessToken() (string, error) { + resp, err := http.Get(tokenEndpoint) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + accessToken := gjson.Get(string(body), "accessToken") + return accessToken.String(), nil +} + +/* requests to playlist/track endpoints */ +func request(endpoint string) (int, string, error) { + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return 0, "", fmt.Errorf("error on making the request") + } + + bearer, err := accessToken() + if err != nil { + return 0, "", fmt.Errorf("failed to get access token: %w", err) + } + req.Header.Add("Authorization", "Bearer "+bearer) + + resp, err := (&http.Client{}).Do(req) + if err != nil { + return 0, "", fmt.Errorf("error on getting response: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, "", fmt.Errorf("error on reading response: %w", err) + } + + return resp.StatusCode, string(body), nil +} + +func getID(url string) string { + parts := strings.Split(url, "/") + id := strings.Split(parts[4], "?")[0] + return id +} + +func isValidPattern(url, pattern string) bool { + match, _ := regexp.MatchString(pattern, url) + return match +} + +func TrackInfo(url string) (*Track, error) { + trackPattern := `^https:\/\/open\.spotify\.com\/track\/[a-zA-Z0-9]{22}\?si=[a-zA-Z0-9]{16}$` + if !isValidPattern(url, trackPattern) { + return nil, errors.New("invalid track url") + } + + id := getID(url) + endpointQuery := EncodeParam(fmt.Sprintf(`{"uri":"spotify:track:%s"}`, id)) + endpoint := trackInitialPath + endpointQuery + "&extensions=" + EncodeParam(trackEndPath) + + statusCode, jsonResponse, err := request(endpoint) + // fmt.Print("TRACK INFO: ", jsonResponse) + if err != nil { + return nil, fmt.Errorf("error on getting track info: %w", err) + } + + if statusCode != 200 { + return nil, fmt.Errorf("received non-200 status code: %d", statusCode) + } + + track := &Track{ + Title: gjson.Get(jsonResponse, "data.trackUnion.name").String(), + Artist: gjson.Get(jsonResponse, "data.trackUnion.firstArtist.items.0.profile.name").String(), + Album: gjson.Get(jsonResponse, "data.trackUnion.albumOfTrack.name").String(), + } + + var allArtists []string + + if firstArtist := gjson.Get(jsonResponse, "data.trackUnion.firstArtist.items.0.profile.name").String(); firstArtist != "" { + allArtists = append(allArtists, firstArtist) + } + + if artists := gjson.Get(jsonResponse, "data.trackUnion.otherArtists.items").Array(); len(artists) > 0 { + for _, artist := range artists { + if profile := artist.Get("profile").Map(); len(profile) > 0 { + if name := profile["name"].String(); name != "" { + allArtists = append(allArtists, name) + } + } + } + } + + fmt.Println("ARTISTS: ", allArtists) + fmt.Println("TRACK: ", track) + + return track.buildTrack(), nil +} + +func PlaylistInfo(url string) ([]Track, error) { + playlistPattern := `^https:\/\/open\.spotify\.com\/playlist\/[a-zA-Z0-9]{22}\?si=[a-zA-Z0-9]{16}$` + if !isValidPattern(url, playlistPattern) { + return nil, errors.New("invalid playlist url") + } + + totalCount := "data.playlistV2.content.totalCount" + itemsArray := "data.playlistV2.content.items" + tracks, err := resourceInfo(url, "playlist", totalCount, itemsArray) + if err != nil { + return nil, err + } + + return tracks, nil +} + +func AlbumInfo(url string) ([]Track, error) { + albumPattern := `^https:\/\/open\.spotify\.com\/album\/[a-zA-Z0-9-]{22}\?si=[a-zA-Z0-9_-]{22}$` + if !isValidPattern(url, albumPattern) { + return nil, errors.New("invalid album url") + } + + totalCount := "data.albumUnion.discs.items.0.tracks.totalCount" + itemsArray := "data.albumUnion.discs.items" + tracks, err := resourceInfo(url, "album", totalCount, itemsArray) + if err != nil { + return nil, err + } + + return tracks, nil +} + +/* returns playlist/album slice of tracks */ +func resourceInfo(url, resourceType, totalCount, itemList string) ([]Track, error) { + id := getID(url) + eConf := ResourceEndpoint{Limit: 400, Offset: 0} + jsonResponse, err := jsonList(resourceType, id, eConf.Offset, eConf.Limit) + if err != nil { + return nil, err + } + + eConf.TotalCount = gjson.Get(jsonResponse, totalCount).Int() + + if eConf.TotalCount < 1 { + return nil, errors.New("hum, there are no tracks") + } + + name := map[bool]string{true: gjson.Get(jsonResponse, "data.playlistV2.name").String(), false: gjson.Get(jsonResponse, "data.albumUnion.name").String()}[resourceType == "playlist"] + fmt.Printf("Collecting tracks from '%s'...\n", name) + time.Sleep(1 * time.Second) + + eConf.Requests = int64(math.Ceil(float64(eConf.TotalCount) / float64(eConf.Limit))) /* total of requests */ + var tracks []Track + tracks = append(tracks, proccessItems(jsonResponse, resourceType)...) + + for i := 1; i < int(eConf.Requests); i++ { + eConf.pagination() + + jsonResponse, err := jsonList(resourceType, id, eConf.Offset, eConf.Limit) + if err != nil { + return nil, err + } + tracks = append(tracks, proccessItems(jsonResponse, resourceType)...) + } + + fmt.Println("Tracks collected:", len(tracks)) + return tracks, nil +} + +/* gets JSON respond from playlist/album endpoints */ +func jsonList(resourceType, id string, offset, limit int64) (string, error) { + var endpointQuery string + var endpoint string + if resourceType == "playlist" { + endpointQuery = EncodeParam(fmt.Sprintf(`{"uri":"spotify:playlist:%s","offset":%d,"limit":%d}`, id, offset, limit)) + endpoint = playlistInitialPath + endpointQuery + "&extensions=" + EncodeParam(playlistEndPath) + } else { + endpointQuery = EncodeParam(fmt.Sprintf(`{"uri":"spotify:album:%s","locale":"","offset":%d,"limit":%d}`, id, offset, limit)) + endpoint = albumInitialPath + endpointQuery + "&extensions=" + EncodeParam(albumEndPath) + } + + statusCode, jsonResponse, err := request(endpoint) + if err != nil { + return "", fmt.Errorf("error getting tracks: %w", err) + } + + if statusCode != 200 { + return "", fmt.Errorf("received non-200 status code: %d", statusCode) + } + + return jsonResponse, nil +} + +func (t *Track) buildTrack() *Track { + track := &Track{ + Title: t.Title, + Artist: t.Artist, + Album: t.Album, + } + + return track +} + +func (eConf *ResourceEndpoint) pagination() { + eConf.Offset = eConf.Offset + eConf.Limit +} + +/* constructs each Spotify track from JSON body (album/playlist) and returns a slice of tracks */ +func proccessItems(jsonResponse, resourceType string) []Track { + itemList := map[bool]string{true: "data.playlistV2.content.items", false: "data.albumUnion.tracks.items"}[resourceType == "playlist"] + songTitle := map[bool]string{true: "itemV2.data.name", false: "track.name"}[resourceType == "playlist"] + artistName := map[bool]string{true: "itemV2.data.artists.items.0.profile.name", false: "track.artists.items.0.profile.name"}[resourceType == "playlist"] + albumName := map[bool]string{true: "itemV2.data.albumOfTrack.name", false: "data.albumUnion.name"}[resourceType == "playlist"] + + var tracks []Track + items := gjson.Get(jsonResponse, itemList).Array() + + for _, item := range items { + track := &Track{ + Title: item.Get(songTitle).String(), + Artist: item.Get(artistName).String(), + Album: map[bool]string{true: item.Get(albumName).String(), false: gjson.Get(jsonResponse, albumName).String()}[resourceType == "playlist"], + } + + tracks = append(tracks, *track.buildTrack()) + } + + return tracks +} diff --git a/spotify/utils.go b/spotify/utils.go new file mode 100644 index 0000000..9717bb7 --- /dev/null +++ b/spotify/utils.go @@ -0,0 +1,244 @@ +package spotify + +import ( + "archive/zip" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/url" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "strings" + "unicode" + + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +func EncodeParam(s string) string { + return url.QueryEscape(s) +} + +func ToLowerCase(s string) string { + var result string + for _, char := range s { + if char >= 'A' && char <= 'Z' { + result += string(char + 32) + } else { + result += string(char) + } + } + + return result +} + +func isPathValid(path string) bool { + dir, err := os.Stat(path) + if err != nil { + fmt.Println(err) + return false + } + + if !dir.IsDir() { + return false + } + + return true +} + +func NewDir(path string) (string, error) { + if !isPathValid(path) { + return "", errors.New("invalid path") + } + + dirName := "YourMusic" + fullPath := filepath.Join(path, dirName) + + if runtime.GOOS == "windows" { + fullPath = fullPath + "\\" + } else { + fullPath = fullPath + "/" + } + + err := os.Mkdir(fullPath, 0700) + if err != nil { + fmt.Sprintln("Error: %w", err) + return "", err + } + + return fullPath, nil +} + +func ToZip(dir, zipPath string) error { + zipFile, err := os.Create(zipPath) + if err != nil { + return err + } + defer zipFile.Close() + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + err = filepath.Walk(dir, func(filePath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if filePath == dir { + return nil + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name, _ = filepath.Rel(dir, filePath) + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + if !info.IsDir() { + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(writer, file) + if err != nil { + return err + } + } + return nil + }) + + return err +} + +func GetFileSize(file string) (int64, error) { + fileInfo, err := os.Stat(file) + if err != nil { + return 0, err + } + + size := int64(fileInfo.Size()) + return size, nil +} + +func RemoveAccents(s string) string { + t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) + output, _, e := transform.String(t, s) + if e != nil { + panic(e) + } + + return output +} + +func RemoveInvalidChars(input string, invalidChars []byte) string { + filter := func(r rune) rune { + for _, c := range invalidChars { + if byte(r) == c { + return -1 /* remove the char */ + } + } + return r + } + + return strings.Map(filter, input) +} + +func GetLocalIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "" + } + for _, address := range addrs { + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + } + } + + return "" +} + +/* catches interrupt signal (ctrl+c) */ +func SetupCloseHandler(tempDir, zipFile string) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + <-c + DeleteFile(tempDir) + DeleteFile(zipFile) + os.Exit(0) + }() +} + +func DeleteFile(filePath string) { + if _, err := os.Stat(filePath); err == nil { + if err := os.RemoveAll(filePath); err != nil { + fmt.Println("Error deleting file:", err) + } + } +} + +/* used for the last validation in the Match function */ +func ExtractFirstWord(value string) string { + for i := range value { + if value[i] == ' ' { + return value[0:i] + } + } + return value +} + +/* +i don't know why, but there are artists who, +due to their name, they add a hyphen +between some words of their names +on one platform and not on the other +*/ +func CleanAndNormalize(s string) string { + cleaned := strings.ReplaceAll(s, "-", "") + cleaned = strings.ReplaceAll(cleaned, " ", "") + return cleaned +} + +// Convert M4A file from stereo to mono +func ConvertM4aToMono(inputFile, outputFile string) ([]byte, error) { + cmd := exec.Command("ffprobe", "-v", "error", "-show_entries", "stream=channels", "-of", "default=noprint_wrappers=1:nokey=1", inputFile) + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("error running ffprobe: %v, %v", err, string(output)) + } + + audioBytes, err := ioutil.ReadFile(inputFile) + if err != nil { + return nil, fmt.Errorf("error reading input file: %v", err) + } + + channels := strings.TrimSpace(string(output)) + if channels != "1" { + cmd = exec.Command("ffmpeg", "-i", inputFile, "-af", "pan=mono|c0=c0", outputFile) + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("error running ffmpeg: %v", err) + } + + audioBytes, err = ioutil.ReadFile(outputFile) + if err != nil { + return nil, fmt.Errorf("error reading input file: %v", err) + } + } + + return audioBytes, nil +} diff --git a/spotify/youtube.go b/spotify/youtube.go new file mode 100644 index 0000000..38e3fb8 --- /dev/null +++ b/spotify/youtube.go @@ -0,0 +1,39 @@ +package spotify + +import ( + "context" + "fmt" + "log" + + "google.golang.org/api/option" + "google.golang.org/api/youtube/v3" +) + +const developerKey = "AIzaSyC3nBFKqudeMItXnYKEeOUryLKhXnqBL7M" + +// https://github.com/BharatKalluri/spotifydl/blob/v0.1.0/src/youtube.go +func VideoID(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) + return "", err + } + + // Video category ID 10 is for music videos + query := fmt.Sprintf("'%s' %s %s", spTrack.Title, spTrack.Artist, spTrack.Album) /* example: 'Lovesong' The Cure Disintegration */ + call := service.Search.List([]string{"id", "snippet"}).Q(query).VideoCategoryId("10").Type("video") + + response, err := call.Do() + if err != nil { + log.Fatalf("Error making search API call: %v", err) + return "", err + } + for _, item := range response.Items { + switch item.Id.Kind { + case "youtube#video": + return item.Id.VideoId, nil + } + } + // TODO: Handle when the query returns no songs (highly unlikely since the query is coming from spotify though) + return "", nil +} diff --git a/utils/dbClient.go b/utils/dbClient.go new file mode 100644 index 0000000..97c5dde --- /dev/null +++ b/utils/dbClient.go @@ -0,0 +1,189 @@ +package utils + +import ( + "context" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const dbUri string = "mongodb://localhost:27017" + +// DbClient represents a MongoDB client +type DbClient struct { + client *mongo.Client +} + +// NewDbClient creates a new instance of DbClient +func NewDbClient() (*DbClient, error) { + clientOptions := options.Client().ApplyURI(dbUri) + client, err := mongo.Connect(context.Background(), clientOptions) + if err != nil { + return nil, err + } + return &DbClient{client: client}, nil +} + +// Close closes the underlying MongoDB client +func (db *DbClient) Close() error { + if db.client != nil { + return db.client.Disconnect(context.Background()) + } + return nil +} + +func (db *DbClient) SongExists(key string) (bool, error) { + existingSongsCollection := db.client.Database("song-recognition").Collection("existing-songs") + filter := bson.M{"_id": key} + + var result bson.M + if err := existingSongsCollection.FindOne(context.Background(), filter).Decode(&result); err == nil { + return true, nil + } else if err != mongo.ErrNoDocuments { + return false, fmt.Errorf("error querying registered songs: %v", err) + } + + return false, nil +} + +func (db *DbClient) RegisterSong(key string) error { + existingSongsCollection := db.client.Database("song-recognition").Collection("existing-songs") + _, err := existingSongsCollection.InsertOne(context.Background(), bson.M{"_id": key}) + if err != nil { + return fmt.Errorf("error registering song: %v", err) + } + + return nil +} + +func (db *DbClient) InsertChunkData(chunkfgp int64, chunkData interface{}) error { + chunksCollection := db.client.Database("song-recognition").Collection("chunks") + + filter := bson.M{"fingerprint": chunkfgp} + + 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 + fmt.Println("DUPLICATE FINGERPRINT: ", chunkfgp) + update := bson.M{"$push": bson.M{"chunkData": chunkData}} + _, err := chunksCollection.UpdateOne(context.Background(), filter, update) + if err != nil { + return fmt.Errorf("error updating chunk data: %v", err) + } + return nil + } else if err != mongo.ErrNoDocuments { + return err + } + + // If the document doesn't exist, insert a new document + _, err = chunksCollection.InsertOne(context.Background(), bson.M{"fingerprint": chunkfgp, "chunkData": []interface{}{chunkData}}) + if err != nil { + return fmt.Errorf("error inserting chunk data: %v", err) + } + + return nil +} + +// func (db *DbClient) GetChunkData(chunkfgp int64) ([]interface{}, error) { +// chunksCollection := db.client.Database("song-recognition").Collection("chunks") + +// type chunkData struct { +// ChunkData []interface{} `bson:"chunkData"` +// } + +// var result chunkData + +// filter := bson.M{"fingerprint": chunkfgp} +// err := chunksCollection.FindOne(context.Background(), filter).Decode(&result) + +// if err != nil { +// if err == mongo.ErrNoDocuments { +// return nil, nil +// } +// return nil, fmt.Errorf("error retrieving chunk data: %w", err) +// } + +// return result.ChunkData, 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) ([]chunkData, error) { +// chunksCollection := db.client.Database("song-recognition").Collection("chunks") + +// var result []chunkData + +// filter := bson.M{"fingerprint": chunkfgp} +// err := chunksCollection.FindOne(context.Background(), filter).Decode(&result) + +// if err != nil { +// if err == mongo.ErrNoDocuments { +// return nil, nil +// } +// return nil, fmt.Errorf("error retrieving chunk data: %w", err) +// } + +// return result, nil +// } + +// func (db *DbClient) GetChunkData(chunkfgp int64) ([]map[string]interface{}, error) { +// chunksCollection := db.client.Database("song-recognition").Collection("chunks") + +// // Change FindOne to Find to retrieve multiple documents +// filter := bson.M{"fingerprint": chunkfgp} +// cursor, err := chunksCollection.Find(context.Background(), filter) +// if err != nil { +// return nil, fmt.Errorf("error retrieving chunk data: %w", err) +// } +// defer cursor.Close(context.Background()) + +// var results []map[string]interface{} +// for cursor.Next(context.Background()) { +// var data map[string]interface{} // Assuming retrieved data is a map +// if err := cursor.Decode(&data); err != nil { +// return nil, fmt.Errorf("error decoding chunk data: %w", err) +// } + +// // Append original map to the results slice +// results = append(results, data) +// } + +// if err := cursor.Err(); err != nil { +// return nil, fmt.Errorf("error iterating through cursor: %w", err) +// } + +// return results, nil +// } + +func (db *DbClient) GetChunkData(chunkfgp int64) ([]primitive.M, error) { + chunksCollection := db.client.Database("song-recognition").Collection("chunks") + + filter := bson.M{"fingerprint": chunkfgp} + result := bson.M{} + err := chunksCollection.FindOne(context.Background(), filter).Decode(&result) + + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + 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)) + } + + return listOfChunkData, nil +}