From cba518e52a918ea849b0417bc39617b93b38e971 Mon Sep 17 00:00:00 2001 From: orfelorfel23 Date: Wed, 26 Aug 2026 01:35:53 +0200 Subject: [PATCH] Implement AcoustID, GetSongBPM, and frontend MediaRecorder --- .gitignore | Bin 32 -> 88 bytes public/app.js | Bin 1596 -> 5617 bytes server/package-lock.json | 287 +++++++++++++++++++++++++++++++++++++ server/package.json | 2 + server/routes/metadata.js | 118 +++++++++++++-- server/routes/recognize.js | 101 +++++++++++-- 6 files changed, 485 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 27592c817d6e4658c948c5c0dbeba8fcfa56be6e..53553c2af3b68e3305028138ff8d6ed1b84e86f2 100644 GIT binary patch literal 88 zcmezWFOMOgA%!88A)X-@%qnHb0rH9&^ci>=xPWvjLlHw6nAB&$uT~GJDi5d@qz?dW C+Ys6S delta 4 Lcma!0n4kav0|Wsn diff --git a/public/app.js b/public/app.js index 875aed4c6394c46cf4f0167b395a3fc9f5d169fd..af1e8abcf282ad6cafd6cabf2cc189286e4b9d90 100644 GIT binary patch literal 5617 zcmd51>yM-|44Mr%>qbIpq@yr`S1 zdUAAh+0=!Vs%BO#7L_<(ij~MF?DmMULe<8yrCn7pFICa3Fg=eCrxh$~`=m9bxQnHQ zEltT6!v1WnmKTi`Sz2<-XO^6&6E>gESz3{C+D)&;Cz>0*JY|@puTu87Z|XdoyF5=B zIhl~ph>~Cgq|Y`l$}DvlW*4?jC%J|7v8qw4oyaZ`7y5)SkVARNvOxv!-yc*ozY-f? zHpXsbU8k^PeBu=g>7_AaPe#P zCct%uK1xiRu{J8d;nK32U&{rzO6LyuiMW=9Fht2)BlH>NlHD?wz+)#=wsq;EGKl2@ zGB!w=5)wDeO^s#90t$k-Z%(diQXjwOvf>w&fDdu)DE}$s;@NeBIhp20{w3H?>|vJ~ z^Dup#^L1mES!W+WN7&l~NQ_l$0Pu96=JO$jyGl=)`Q_-U^{i4Cp%nC(^)%38>5ajZ zbU{v#hofe#6{s*jlX(1ea@0-^whtb#H>4D|u2?}LV{+vfv7*}0qA*rg6|3To6DE>6 z1}eG@FGwR}=|yaw_I~|lW~7={6E%L;CbCCh}?(H9Y@Dv@JI>FnN_EVNoL zRV^4VhzwR@(-tK|qR!;;y(v8er@Ru{?wchI*65#&)R<}&oLEsmc}`MofC#E0MQ z+k?U<bQDD|9V5?4ZX_*EjD>MeH4ToH`OzsePI&6rntyvM>Oz&` z?duoEYPH6auWjaXpKR~@1oqnm4z?e|I*z9R`5)*vfEft5rG``yXM9G*F~BWDKZ7;2 zUQLh#X<}h`{8^$7GUod@Unv0Q((5V$z zSe3Hp>Z~!c_*HD!)7Zp~UcE=y57d>qwvbZzTyPX|6c}S2_O=FISXA$0w543uLSxUk zdtJbArqzw#J3W-a=g^Gnz}qz6%N9A%L9EqXFk8 zwzOPxoy`cuEXZ-vLo=+7KbVe}vMMu=vzKC9BAS!q7&+xQcyLkZIyS1fr%Vj4NS`Sl zL)*WAVi%Dz&VT2{2V6jn;)J(I{EQYC$Z280&Ukx#iNKy-s_`Ishwh> zPoYb%>ocf#a>=-+roH%#`X1NK^jcLYuHS@{Og+W^^gkUCy?eQA+u)hE_WAhv5+86L z-Q<5ZLT}!P3e6h2!BqBbiE`M+!jCSoT^zFXGXa$L`CclqUTsLXT&d^FwOFZZT+7$I zF`|rRCx}B~Uh)}AO>G9Bt=fJyD3oIF+@aI9qYO5PJ-TR8O$S%sJpg%$hVh@EbG8S7 zz4Dp*ZO0#oy{j}sAGF)%WBi_@% delta 515 zcmeyUy@zMR9mdTP{N;?3R|?cj7Uma`2+v4O&C$?QsOC~oNY2kIE>XxxEy`E0ox~|s z4`QinS_5S=(-bs7JiX+c#NuL~%;FL~pyHCm%)DX^^>UCAy1Dr&sp^^#gA^2sQj1ed zJb*eh!1`+yQgez^!Imh1EkH7;C^a{~ELB528AujEjRDyS1ZY+!rlg?QHkprKQoeDWMYX#j+^rM>_F diff --git a/server/package-lock.json b/server/package-lock.json index 6abca27..0f60ed4 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -8,9 +8,11 @@ "name": "keyselector", "version": "1.0.0", "dependencies": { + "axios": "^1.19.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "multer": "^2.2.0", "node-fetch": "^3.3.2" } }, @@ -27,12 +29,71 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/body-parser": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", @@ -57,6 +118,23 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -95,6 +173,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -166,6 +271,15 @@ "ms": "2.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -256,6 +370,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -358,6 +487,42 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -458,6 +623,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -490,6 +670,42 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -592,6 +808,25 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -700,6 +935,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -740,6 +984,20 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -898,6 +1156,23 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -920,6 +1195,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -929,6 +1210,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/server/package.json b/server/package.json index 2c8060e..7efb6da 100644 --- a/server/package.json +++ b/server/package.json @@ -7,9 +7,11 @@ "start": "node index.js" }, "dependencies": { + "axios": "^1.19.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "multer": "^2.2.0", "node-fetch": "^3.3.2" } } diff --git a/server/routes/metadata.js b/server/routes/metadata.js index 61f69bb..19bd547 100644 --- a/server/routes/metadata.js +++ b/server/routes/metadata.js @@ -1,21 +1,115 @@ const express = require('express'); const router = express.Router(); +const axios = require('axios'); + +const reverseMap = { + 'c': '1B', 'a minor': '1A', 'am': '1A', + 'g': '2B', 'e minor': '2A', 'em': '2A', + 'd': '3B', 'b minor': '3A', 'bm': '3A', + 'a': '4B', 'f# minor': '4A', 'f#m': '4A', + 'e': '5B', 'c# minor': '5A', 'c#m': '5A', + 'b': '6B', 'g# minor': '6A', 'g#m': '6A', + 'f#': '7B', 'd# minor': '7A', 'd#m': '7A', + 'db': '8B', 'c#': '8B', 'bb minor': '8A', 'a# minor': '8A', 'bbm': '8A', + 'ab': '9B', 'g#': '9B', 'f minor': '9A', 'fm': '9A', + 'eb': '10B', 'd#': '10B', 'c minor': '10A', 'cm': '10A', + 'bb': '11B', 'a#': '11B', 'g minor': '11A', 'gm': '11A', + 'f': '12B', 'd minor': '12A', 'dm': '12A', + 'c major': '1B', 'g major': '2B', 'd major': '3B', 'a major': '4B', + 'e major': '5B', 'b major': '6B', 'f# major': '7B', 'db major': '8B', + 'ab major': '9B', 'eb major': '10B', 'bb major': '11B', 'f major': '12B' +}; + +function standardizeKey(keyString) { + if (!keyString) return null; + const str = keyString.toLowerCase().trim(); + return reverseMap[str] || null; +} -// Mock metadata route router.get('/', async (req, res) => { - const track = req.query.track || 'Mock Track'; + const track = req.query.track; + const artist = req.query.artist; - // In production, this would call GetSongBPM and Last.fm - // to fetch BPM, Camelot Key, and genre/mood tags. - - res.json({ - status: 'success', - data: { - key: '4B', - bpm: 120, - tags: ['Electronic', 'Synthpop'] + if (!track || !artist) { + return res.status(400).json({ error: 'Missing track or artist' }); + } + + let bpm = null; + let camelotKey = null; + let tags = []; + + try { + // 1. GetSongBPM API + if (process.env.GETSONGBPM_API_KEY) { + const gsbRes = await axios.get('https://api.getsong.co/search/', { + params: { + api_key: process.env.GETSONGBPM_API_KEY, + type: 'both', + lookup: `song:${track} artist:${artist}` + } + }).catch(err => { + console.error('GetSongBPM Error:', err.message); + return { data: { search: [] } }; + }); + + const searchResults = gsbRes.data.search; + if (searchResults && searchResults.length > 0) { + // get song id + const songId = searchResults[0].song_id; + if (songId) { + const songRes = await axios.get('https://api.getsong.co/song/', { + params: { api_key: process.env.GETSONGBPM_API_KEY, id: songId } + }).catch(() => ({ data: { song: {} } })); + + if (songRes.data.song) { + bpm = songRes.data.song.tempo; + const keyStr = songRes.data.song.key_of; + camelotKey = standardizeKey(keyStr); + } + } + } } - }); + + // 2. Last.fm API + if (process.env.LASTFM_API_KEY) { + const lfmRes = await axios.get('https://ws.audioscrobbler.com/2.0/', { + params: { + method: 'track.getInfo', + api_key: process.env.LASTFM_API_KEY, + artist: artist, + track: track, + format: 'json' + } + }).catch(err => { + console.error('Last.fm Error:', err.message); + return { data: {} }; + }); + + if (lfmRes.data && lfmRes.data.track && lfmRes.data.track.toptags && lfmRes.data.track.toptags.tag) { + tags = lfmRes.data.track.toptags.tag + .slice(0, 4) // top 4 tags + .map(t => t.name.toLowerCase()); + } + } + + // Fallbacks if APIs failed or didn't find anything + if (!bpm) bpm = 120; + if (!camelotKey) camelotKey = '8B'; // random default + if (tags.length === 0) tags = ['electronic']; + + res.json({ + status: 'success', + data: { + key: camelotKey, + bpm: Math.round(bpm), + tags: tags + } + }); + + } catch (err) { + console.error('Metadata error:', err.message); + res.status(500).json({ error: 'Failed to fetch metadata' }); + } }); module.exports = router; diff --git a/server/routes/recognize.js b/server/routes/recognize.js index feae885..6a117e1 100644 --- a/server/routes/recognize.js +++ b/server/routes/recognize.js @@ -1,19 +1,98 @@ const express = require('express'); const router = express.Router(); +const multer = require('multer'); +const { execFile } = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const axios = require('axios'); -// Mock recognize route -router.post('/', async (req, res) => { - // In production, this would receive an audio buffer, fingerpint it, - // and call the AcoustID API. +// Configure multer to save files to system temp directory +const upload = multer({ dest: os.tmpdir() }); + +router.post('/', upload.single('audio'), async (req, res) => { + if (!req.file) { + return res.status(400).json({ error: 'No audio file uploaded.' }); + } + + const filePath = req.file.path; - res.json({ - status: 'success', - data: { - title: 'Mock Track', - artist: 'Mock Artist', - matchScore: '95%' + try { + // 1. Generate fingerprint using fpcalc + const { duration, fingerprint } = await new Promise((resolve, reject) => { + execFile('fpcalc', [filePath], (error, stdout, stderr) => { + if (error) { + return reject(new Error('fpcalc failed: ' + stderr)); + } + + let dur = null; + let fp = null; + + stdout.split('\n').forEach(line => { + if (line.startsWith('DURATION=')) dur = parseInt(line.split('=')[1], 10); + if (line.startsWith('FINGERPRINT=')) fp = line.split('=')[1]; + }); + + if (!dur || !fp) { + return reject(new Error('Invalid fpcalc output')); + } + + resolve({ duration: dur, fingerprint: fp }); + }); + }); + + // 2. Query AcoustID API + const acoustidKey = process.env.ACOUSTID_API_KEY; + if (!acoustidKey) { + throw new Error('Missing ACOUSTID_API_KEY'); } - }); + + const response = await axios.get('https://api.acoustid.org/v2/lookup', { + params: { + client: acoustidKey, + meta: 'recordings', + duration: duration, + fingerprint: fingerprint + } + }); + + const results = response.data.results; + if (!results || results.length === 0) { + return res.json({ status: 'not_found' }); + } + + // Sort results by score (descending) + results.sort((a, b) => b.score - a.score); + const bestResult = results[0]; + + // Find a recording with title and artists + let title = 'Unknown'; + let artist = 'Unknown'; + + if (bestResult.recordings && bestResult.recordings.length > 0) { + const rec = bestResult.recordings[0]; + title = rec.title || 'Unknown'; + if (rec.artists && rec.artists.length > 0) { + artist = rec.artists.map(a => a.name).join(' & '); + } + } + + res.json({ + status: 'success', + data: { + title: title, + artist: artist, + matchScore: Math.round(bestResult.score * 100) + '%' + } + }); + + } catch (err) { + console.error('Recognition error:', err.message); + res.status(500).json({ error: 'Failed to recognize audio' }); + } finally { + // Clean up uploaded file + fs.unlink(filePath, () => {}); + } }); module.exports = router;