Final commit
This commit is contained in:
parent
91fffb3294
commit
c35e0716af
372 changed files with 16591 additions and 1 deletions
BIN
backend/.DS_Store
vendored
Normal file
BIN
backend/.DS_Store
vendored
Normal file
Binary file not shown.
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.env
|
40
backend/.zed/settings.json
Normal file
40
backend/.zed/settings.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"lsp": {
|
||||
"deno": {
|
||||
"settings": {
|
||||
"deno": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"language_servers": [
|
||||
"deno",
|
||||
"!typescript-language-server",
|
||||
"!vtsls",
|
||||
"!eslint"
|
||||
],
|
||||
"formatter": "language_server"
|
||||
},
|
||||
"TypeScript": {
|
||||
"language_servers": [
|
||||
"deno",
|
||||
"!typescript-language-server",
|
||||
"!vtsls",
|
||||
"!eslint"
|
||||
],
|
||||
"formatter": "language_server"
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": [
|
||||
"deno",
|
||||
"!typescript-language-server",
|
||||
"!vtsls",
|
||||
"!eslint"
|
||||
],
|
||||
"formatter": "language_server"
|
||||
}
|
||||
}
|
||||
}
|
16
backend/.zed/tasks.json
Normal file
16
backend/.zed/tasks.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{
|
||||
"label": "Run Deno Backend",
|
||||
"command": "cd .. && docker build -f backend/Dockerfile -t deno-backend ./ && docker run --rm -it -p 8000:8000 deno-backend",
|
||||
"env": {},
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "always",
|
||||
"reveal_target": "dock",
|
||||
"hide": "never"
|
||||
},
|
||||
{
|
||||
"label": "Run Deno Test in line",
|
||||
"command": "deno test --env-file=${ZED_WORKTREE_ROOT}/.env --allow-all --filter \"$(sed -n '${ZED_ROW}p' $ZED_FILE | grep -o 'Deno\\.test(\"[^\"]*\"' | sed 's/Deno\\.test(\"//' | sed 's/\"$//')\" $ZED_FILE"
|
||||
}
|
||||
]
|
13
backend/Dockerfile
Normal file
13
backend/Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
FROM denoland/deno:latest
|
||||
|
||||
# Create working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy source
|
||||
COPY backend/ .
|
||||
|
||||
# Compile the main app
|
||||
RUN deno cache main.ts
|
||||
|
||||
# Run the app
|
||||
CMD ["deno", "run", "--allow-net", "--allow-env", "main.ts"]
|
48
backend/ai-selector.ts
Normal file
48
backend/ai-selector.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { GoogleGenAI } from "npm:@google/genai@1.12.0";
|
||||
import { assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import schema from "./schemas/AiSuggestionsOutput.json" with { type: "json" };
|
||||
import { AiSuggestionsOutput } from "./types/ai.ts";
|
||||
|
||||
const ai = new GoogleGenAI({});
|
||||
|
||||
// Generate songs using AI
|
||||
export async function generateSongs(
|
||||
userPrompt: string,
|
||||
rules: string,
|
||||
): Promise<AiSuggestionsOutput> {
|
||||
const rulesText = rules && rules.trim().length > 0
|
||||
? `Make sure that they follow the following set of rules: "${rules}".`
|
||||
: "";
|
||||
const aiPrompt = `
|
||||
Based on this music request: "${userPrompt}", generate 15 suggestion for songs.
|
||||
Make sure they are real, popular songs that exist on Spotify.
|
||||
Make sure to follow the following set of costraints in order to suggest songs but bear in mind
|
||||
to return an error only in extreme cases. Ypu can be a little open even if genres clash with eah other a little bit.
|
||||
The rules are: "${rulesText}"
|
||||
The type should be either succes or error.
|
||||
Make the error as user friendly as possible ans concise, and don't make it happen too often.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-2.5-flash",
|
||||
contents: aiPrompt,
|
||||
config: { responseMimeType: "application/json", responseSchema: schema },
|
||||
});
|
||||
|
||||
const text = response.candidates?.at(0)?.content?.parts?.at(0)?.text;
|
||||
if (!text) {
|
||||
throw Error("Text not found");
|
||||
}
|
||||
|
||||
return JSON.parse(text) as AiSuggestionsOutput;
|
||||
}
|
||||
|
||||
Deno.test("generateSongs basic", async () => {
|
||||
const prompt = "Give me 5 songs by the queens";
|
||||
const rules = "Songs must vool";
|
||||
const songs = await generateSongs(prompt, rules);
|
||||
|
||||
console.log(songs);
|
||||
|
||||
assert(songs.type === "success");
|
||||
});
|
65
backend/ai-suggestion.ts
Normal file
65
backend/ai-suggestion.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { GoogleGenAI } from "npm:@google/genai@1.12.0";
|
||||
import { assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import schema from "./schemas/AiSnewuggestionOutput.json" with { type: "json" };
|
||||
import { GetNewSuggestInput, GetNewSuggestOutput } from "./types/api.ts";
|
||||
import { AiSnewuggestionOutput } from "./types/ai.ts";
|
||||
|
||||
const ai = new GoogleGenAI({});
|
||||
|
||||
// Generate songs using AI
|
||||
export async function generateVibes(
|
||||
input: GetNewSuggestInput,
|
||||
): Promise<AiSnewuggestionOutput> {
|
||||
const { rules } = input;
|
||||
let aiPrompt = "";
|
||||
let contextText = "";
|
||||
|
||||
if (input.type === "scratch-suggestion") {
|
||||
contextText =
|
||||
`The music request is for a place: "${input.room_name}". Your job is to find 15 songs that match the vibe of this place.`;
|
||||
} else if (input.type === "from-songs-suggestion") {
|
||||
const songList = input.songs.map((s, i) => `${i + 1}. ${s.song}`).join(
|
||||
"\n",
|
||||
);
|
||||
contextText =
|
||||
`The music request is based on the last 10 songs:\n${songList}\nYour job is to 15 find songs that match the vibe of these songs.`;
|
||||
}
|
||||
|
||||
const rulesText = rules && rules.trim().length > 0
|
||||
? `Make sure that they follow the following set of rules: "${rules}".`
|
||||
: "";
|
||||
|
||||
aiPrompt = `
|
||||
${contextText}
|
||||
Make sure they are real, popular songs that exist on Spotify.
|
||||
${rulesText}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-2.5-flash",
|
||||
contents: aiPrompt,
|
||||
config: { responseMimeType: "application/json", responseSchema: schema },
|
||||
});
|
||||
|
||||
const text = response.candidates?.at(0)?.content?.parts?.at(0)?.text;
|
||||
if (!text) {
|
||||
throw Error("Text not found");
|
||||
}
|
||||
|
||||
return JSON.parse(text) as AiSnewuggestionOutput;
|
||||
}
|
||||
|
||||
Deno.test("generateVibes with array of songs", async () => {
|
||||
const input: GetNewSuggestInput = {
|
||||
type: "from-songs-suggestion" as const,
|
||||
rules: "The songs must be great for food",
|
||||
songs: [
|
||||
{ song: "Something - The Beatles" },
|
||||
{ song: "Come Together - The Beatles" },
|
||||
{ song: "Help! - The Beatles" },
|
||||
],
|
||||
};
|
||||
const result = await generateVibes(input);
|
||||
|
||||
console.log(result);
|
||||
});
|
0
backend/deno.json
Normal file
0
backend/deno.json
Normal file
209
backend/deno.lock
generated
Normal file
209
backend/deno.lock
generated
Normal file
|
@ -0,0 +1,209 @@
|
|||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"jsr:@std/assert@*": "1.0.13",
|
||||
"jsr:@std/internal@^1.0.6": "1.0.10",
|
||||
"npm:@google/genai@1.12.0": "1.12.0"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/assert@1.0.13": {
|
||||
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.10": {
|
||||
"integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"@google/genai@1.12.0": {
|
||||
"integrity": "sha512-JBkQsULVexdM9zY4iXbm3A2dJ7El/hSPGCnxuRWPJNgeqcfYuyUnPTSy+I/v+MvTbz/occVmONSD6wn+17QLkg==",
|
||||
"dependencies": [
|
||||
"google-auth-library",
|
||||
"ws"
|
||||
]
|
||||
},
|
||||
"agent-base@7.1.4": {
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
|
||||
},
|
||||
"base64-js@1.5.1": {
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"bignumber.js@9.3.1": {
|
||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="
|
||||
},
|
||||
"buffer-equal-constant-time@1.0.1": {
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||
},
|
||||
"debug@4.4.1": {
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dependencies": [
|
||||
"ms"
|
||||
]
|
||||
},
|
||||
"ecdsa-sig-formatter@1.0.11": {
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"dependencies": [
|
||||
"safe-buffer"
|
||||
]
|
||||
},
|
||||
"extend@3.0.2": {
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"gaxios@6.7.1": {
|
||||
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
|
||||
"dependencies": [
|
||||
"extend",
|
||||
"https-proxy-agent",
|
||||
"is-stream",
|
||||
"node-fetch",
|
||||
"uuid"
|
||||
]
|
||||
},
|
||||
"gcp-metadata@6.1.1": {
|
||||
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
|
||||
"dependencies": [
|
||||
"gaxios",
|
||||
"google-logging-utils",
|
||||
"json-bigint"
|
||||
]
|
||||
},
|
||||
"google-auth-library@9.15.1": {
|
||||
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
|
||||
"dependencies": [
|
||||
"base64-js",
|
||||
"ecdsa-sig-formatter",
|
||||
"gaxios",
|
||||
"gcp-metadata",
|
||||
"gtoken",
|
||||
"jws"
|
||||
]
|
||||
},
|
||||
"google-logging-utils@0.0.2": {
|
||||
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="
|
||||
},
|
||||
"gtoken@7.1.0": {
|
||||
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
|
||||
"dependencies": [
|
||||
"gaxios",
|
||||
"jws"
|
||||
]
|
||||
},
|
||||
"https-proxy-agent@7.0.6": {
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dependencies": [
|
||||
"agent-base",
|
||||
"debug"
|
||||
]
|
||||
},
|
||||
"is-stream@2.0.1": {
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
|
||||
},
|
||||
"json-bigint@1.0.0": {
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"dependencies": [
|
||||
"bignumber.js"
|
||||
]
|
||||
},
|
||||
"jwa@2.0.1": {
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"dependencies": [
|
||||
"buffer-equal-constant-time",
|
||||
"ecdsa-sig-formatter",
|
||||
"safe-buffer"
|
||||
]
|
||||
},
|
||||
"jws@4.0.0": {
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"dependencies": [
|
||||
"jwa",
|
||||
"safe-buffer"
|
||||
]
|
||||
},
|
||||
"ms@2.1.3": {
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node-fetch@2.7.0": {
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": [
|
||||
"whatwg-url"
|
||||
]
|
||||
},
|
||||
"safe-buffer@5.2.1": {
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"tr46@0.0.3": {
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"uuid@9.0.1": {
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"bin": true
|
||||
},
|
||||
"webidl-conversions@3.0.1": {
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"whatwg-url@5.0.0": {
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": [
|
||||
"tr46",
|
||||
"webidl-conversions"
|
||||
]
|
||||
},
|
||||
"ws@8.18.3": {
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
|
||||
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
|
||||
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
|
||||
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
|
||||
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
|
||||
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
|
||||
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
|
||||
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
|
||||
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
|
||||
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
|
||||
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
|
||||
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
|
||||
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
|
||||
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
|
||||
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
|
||||
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
|
||||
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
|
||||
"https://deno.land/std@0.224.0/dotenv/load.ts": "587b342f0f6a3df071331fe6ba1c823729ab68f7d53805809475e486dd4161d7",
|
||||
"https://deno.land/std@0.224.0/dotenv/mod.ts": "0180eaeedaaf88647318811cdaa418cc64dc51fb08354f91f5f480d0a1309f7d",
|
||||
"https://deno.land/std@0.224.0/dotenv/parse.ts": "09977ff88dfd1f24f9973a338f0f91bbdb9307eb5ff6085446e7c423e4c7ba0c",
|
||||
"https://deno.land/std@0.224.0/dotenv/stringify.ts": "275da322c409170160440836342eaa7cf012a1d11a7e700d8ca4e7f2f8aa4615",
|
||||
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
|
||||
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
|
||||
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
|
||||
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
|
||||
"https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea",
|
||||
"https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
|
||||
"https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/parseUtil.ts": "f791e6e65a0340d85ad37d26cd7a3ba67126cd9957eac2b7163162155283abb1",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/util.ts": "8baf19b19b2fca8424380367b90364b32503b6b71780269a6e3e67700bb02774",
|
||||
"https://deno.land/x/zod@v3.22.4/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268",
|
||||
"https://deno.land/x/zod@v3.22.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c",
|
||||
"https://deno.land/x/zod@v3.22.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4",
|
||||
"https://deno.land/x/zod@v3.22.4/types.ts": "724185522fafe43ee56a52333958764c8c8cd6ad4effa27b42651df873fc151e"
|
||||
}
|
||||
}
|
21
backend/generate-schemas.sh
Executable file
21
backend/generate-schemas.sh
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
generate_schema_for_type() {
|
||||
yes | npx typescript-json-schema --noExtraProps true --required --refs false --out "temp_schemas/$1.json" "./types/**.ts" "$1"
|
||||
}
|
||||
|
||||
generate_schemas() {
|
||||
rm -rf temp_schemas
|
||||
mkdir temp_schemas
|
||||
|
||||
generate_schema_for_type "AiSuggestionsOutput" &
|
||||
generate_schema_for_type "AiSnewuggestionOutput" &
|
||||
wait
|
||||
|
||||
rm -rf ./schemas
|
||||
mv temp_schemas schemas
|
||||
}
|
||||
|
||||
# Run the schema generation
|
||||
generate_schemas
|
||||
echo "Schema generated successfully!"
|
190
backend/main.ts
Normal file
190
backend/main.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
const PORT = parseInt(Deno.env.get("PORT") || "8000");
|
||||
import {
|
||||
GetNewSuggestInput,
|
||||
GetNewSuggestOutput,
|
||||
GetSongsTextParams,
|
||||
GetTrackInput,
|
||||
GetYoutubeInput,
|
||||
} from "./types/api.ts";
|
||||
import { getSong } from "./routes/get-song.ts";
|
||||
import { getSpotifyTrackById } from "./routes/get-track.ts";
|
||||
import { getVids } from "./routes/get-video.ts";
|
||||
import { getSpotifyAccessToken } from "./spotify-api.ts";
|
||||
import { generateVibes } from "./ai-suggestion.ts";
|
||||
import { getNewSuggestion } from "./routes/get-new-suggest.ts";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
pattern: new URLPattern({ pathname: "/music" }),
|
||||
handler: async (req: Request) => {
|
||||
const params = await req.json();
|
||||
const songs = await getSong(params);
|
||||
|
||||
return new Response(JSON.stringify(songs), {
|
||||
status: 200,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: new URLPattern({ pathname: "/health" }),
|
||||
handler: (_req: Request) => {
|
||||
return new Response("All good", {
|
||||
status: 200,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: new URLPattern({ pathname: "/video" }),
|
||||
handler: async (req: Request) => {
|
||||
const params: GetYoutubeInput = await req.json();
|
||||
const vids = await getVids(params);
|
||||
|
||||
return new Response(JSON.stringify(vids), {
|
||||
status: 200,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: new URLPattern({ pathname: "/track" }),
|
||||
handler: async (req: Request) => {
|
||||
const params: GetTrackInput = await req.json();
|
||||
const track = await getSpotifyTrackById(params.trackId);
|
||||
|
||||
return new Response(JSON.stringify(track), {
|
||||
status: 200,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: new URLPattern({ pathname: "/newSuggestions" }),
|
||||
handler: async (req: Request) => {
|
||||
const params: GetNewSuggestInput = await req.json();
|
||||
const suggestions: GetNewSuggestOutput = await getNewSuggestion(params);
|
||||
return new Response(JSON.stringify(suggestions), {
|
||||
status: 200,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const cors = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
};
|
||||
|
||||
const handler = async (req: Request): Promise<Response> => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: cors,
|
||||
});
|
||||
}
|
||||
|
||||
let response: Response | undefined;
|
||||
try {
|
||||
for (const route of routes) {
|
||||
const match = route.pattern.exec(req.url);
|
||||
if (match) {
|
||||
response = await route.handler(req);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Server error:", error);
|
||||
response = new Response(error.toString() || "Internal server error", {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
||||
response ??= new Response("Not found", { status: 404 });
|
||||
|
||||
for (const [key, value] of Object.entries(cors)) {
|
||||
response.headers.set(key, value);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
Deno.serve({ hostname: "0.0.0.0", port: PORT }, handler);
|
||||
|
||||
Deno.test("music API complete workflow", async () => {
|
||||
const spotify_api_key = getSpotifyAccessToken();
|
||||
if (!spotify_api_key) throw Error("Testing token not found");
|
||||
|
||||
const testInput: GetSongsTextParams = {
|
||||
prompt: "i want summer feeling music",
|
||||
rules: "make it fun and suitable for children",
|
||||
};
|
||||
const res = await fetch(`http://localhost:${PORT}/music`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(testInput),
|
||||
});
|
||||
|
||||
if (res.status !== 200) throw new Error(`Unexpected status: ${res.status}`);
|
||||
const json = await res.json();
|
||||
console.log("API Response:", json);
|
||||
});
|
||||
|
||||
Deno.test("video API complete workflow", async () => {
|
||||
const yt_api_key = Deno.env.get("YT_API_KEY");
|
||||
if (!yt_api_key) throw Error("Testing token not found");
|
||||
|
||||
const input: GetYoutubeInput = {
|
||||
title: "Here Comes The Sun",
|
||||
artist: "The Beatles",
|
||||
};
|
||||
const res = await fetch(`http://localhost:${PORT}/video`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (res.status !== 200) throw new Error(`Unexpected status: ${res.status}`);
|
||||
const json = await res.json();
|
||||
console.log("Video API Response:", json);
|
||||
});
|
||||
|
||||
Deno.test("track API complete workflow", async () => {
|
||||
const spotify_api_key = getSpotifyAccessToken();
|
||||
if (!spotify_api_key) throw Error("Testing token not found");
|
||||
|
||||
const input: GetTrackInput = { trackId: "6dGnYIeXmHdcikdzNNDMm2" };
|
||||
const res = await fetch(`http://localhost:${PORT}/track`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (res.status !== 200) throw new Error(`Unexpected status: ${res.status}`);
|
||||
const json = await res.json();
|
||||
console.log("Track API Response:", json);
|
||||
});
|
||||
|
||||
Deno.test("new suggestion API complete workflow", async () => {
|
||||
const testInput: GetNewSuggestInput = {
|
||||
type: "from-songs-suggestion",
|
||||
rules: "The songs should be upbeat and suitable for a party",
|
||||
songs: [
|
||||
{ song: "Thriller - Michael Jackson" },
|
||||
{ song: "Uptown Funk - Bruno Mars" },
|
||||
{ song: "24K Magic - Bruno Mars" },
|
||||
],
|
||||
};
|
||||
|
||||
const res = await fetch(`http://localhost:${PORT}/newSuggestions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(testInput),
|
||||
});
|
||||
|
||||
if (res.status !== 200) throw new Error(`Unexpected status: ${res.status}`);
|
||||
|
||||
const json = await res.json();
|
||||
console.log("New Suggestion API Response:", json);
|
||||
|
||||
// Validate that the response contains a songs array.
|
||||
if (!json.songs || !Array.isArray(json.songs)) {
|
||||
throw new Error("Response does not contain a valid songs array");
|
||||
}
|
||||
});
|
15
backend/routes/get-new-suggest.ts
Normal file
15
backend/routes/get-new-suggest.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { searchSpotifyTracks } from "../spotify-api.ts";
|
||||
import { GetNewSuggestInput, GetNewSuggestOutput } from "../types/api.ts";
|
||||
import { generateVibes } from "../ai-suggestion.ts";
|
||||
|
||||
export async function getNewSuggestion(
|
||||
params: GetNewSuggestInput,
|
||||
): Promise<GetNewSuggestOutput> {
|
||||
const ai_suggestion = await generateVibes(params);
|
||||
|
||||
const suggestionResults = await searchSpotifyTracks(
|
||||
ai_suggestion.songs,
|
||||
);
|
||||
|
||||
return { songs: suggestionResults };
|
||||
}
|
18
backend/routes/get-song.ts
Normal file
18
backend/routes/get-song.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { generateSongs } from "../ai-selector.ts";
|
||||
import { searchSpotifyTracks } from "../spotify-api.ts";
|
||||
import { GetSongsTextOutput, GetSongsTextParams } from "../types/api.ts";
|
||||
|
||||
export async function getSong(
|
||||
params: GetSongsTextParams,
|
||||
): Promise<GetSongsTextOutput> {
|
||||
const ai_response = await generateSongs(params.prompt, params.rules);
|
||||
if (ai_response.type == "error") {
|
||||
return ai_response;
|
||||
}
|
||||
|
||||
const spotifyResults = await searchSpotifyTracks(
|
||||
ai_response.songs,
|
||||
);
|
||||
|
||||
return { type: "success", songs: spotifyResults };
|
||||
}
|
87
backend/routes/get-track.ts
Normal file
87
backend/routes/get-track.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { SpotifyTrack } from "../types/spotify.ts";
|
||||
import { getSpotifyAccessToken } from "../spotify-api.ts";
|
||||
import { assert } from "jsr:@std/assert/assert";
|
||||
|
||||
// Simple in-memory cache for track objects by ID
|
||||
const trackCache = new Map<string, SpotifyTrack>();
|
||||
|
||||
/**
|
||||
* Inner function that performs the actual API call without retries.
|
||||
*/
|
||||
async function fetchSpotifyTrackById(trackId: string): Promise<SpotifyTrack> {
|
||||
const accessToken = await getSpotifyAccessToken();
|
||||
const url = `https://api.spotify.com/v1/tracks/${trackId}?market=US`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch track: ${response.status} ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const track: SpotifyTrack = await response.json();
|
||||
|
||||
// Cache the result
|
||||
trackCache.set(trackId, track);
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a Spotify track by its ID, with caching.
|
||||
* Returns the full SpotifyTrack object.
|
||||
*/
|
||||
export async function getSpotifyTrackById(
|
||||
trackId: string,
|
||||
): Promise<SpotifyTrack> {
|
||||
// Check cache first
|
||||
if (trackCache.has(trackId)) {
|
||||
return trackCache.get(trackId)!;
|
||||
}
|
||||
|
||||
const maxRetries = 10;
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fetchSpotifyTrackById(trackId);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt === maxRetries) {
|
||||
throw Error(`Attempt ${attempt}: ${lastError}`);
|
||||
}
|
||||
// Optional: add delay between retries
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
}
|
||||
|
||||
Deno.test("getSpotifyTrackById basic", async () => {
|
||||
// Example: Blinding Lights by The Weeknd
|
||||
const trackId = "2EqlS6tkEnglzr7tkKAAYD";
|
||||
|
||||
// First call (should fetch from API)
|
||||
const track1 = await getSpotifyTrackById(trackId);
|
||||
assert(track1.id === trackId);
|
||||
assert(typeof track1.name === "string");
|
||||
assert(Array.isArray(track1.artists));
|
||||
|
||||
// Second call (should hit cache)
|
||||
const start = performance.now();
|
||||
const track2 = await getSpotifyTrackById(trackId);
|
||||
const elapsed = performance.now() - start;
|
||||
|
||||
console.log(track2);
|
||||
console.log("Second call elapsed:", elapsed, "ms");
|
||||
assert(track2.id === trackId);
|
||||
assert(typeof track2.name === "string");
|
||||
assert(Array.isArray(track2.artists));
|
||||
assert(elapsed < 10, `Second call took too long: ${elapsed}ms`);
|
||||
});
|
9
backend/routes/get-video.ts
Normal file
9
backend/routes/get-video.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { GetYoutubeInput, GetYoutubeOutput } from "../types/api.ts";
|
||||
import { searchYTVideo } from "../youtube-api.ts";
|
||||
|
||||
export async function getVids(
|
||||
params: GetYoutubeInput,
|
||||
): Promise<GetYoutubeOutput> {
|
||||
const videoId = await searchYTVideo(params);
|
||||
return { videoId };
|
||||
}
|
30
backend/schemas/AiSnewuggestionOutput.json
Normal file
30
backend/schemas/AiSnewuggestionOutput.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"songs": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"artist": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"artist",
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"songs"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
|
56
backend/schemas/AiSuggestionsOutput.json
Normal file
56
backend/schemas/AiSuggestionsOutput.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"songs": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"artist": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"artist",
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"const": "success",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"songs",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "error",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
136
backend/spotify-api.ts
Normal file
136
backend/spotify-api.ts
Normal file
|
@ -0,0 +1,136 @@
|
|||
import { assert } from "jsr:@std/assert/assert";
|
||||
import { SpotifySearchResponse, SpotifyTrack } from "./types/spotify.ts";
|
||||
import { SongSearch } from "./types/ai.ts";
|
||||
|
||||
export async function searchSpotifyTracks(
|
||||
songs: SongSearch[],
|
||||
): Promise<SpotifyTrack[]> {
|
||||
const results: SpotifyTrack[] = [];
|
||||
|
||||
const trackPromises = songs.map(async (song) => {
|
||||
try {
|
||||
const query = `track:"${song.title}" artist:"${song.artist}"`;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
type: "track",
|
||||
market: "US",
|
||||
limit: "1",
|
||||
offset: "0",
|
||||
});
|
||||
|
||||
const url = `https://api.spotify.com/v1/search?${params.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + await getSpotifyAccessToken(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw Error("Unsuccesful response");
|
||||
}
|
||||
|
||||
const data: SpotifySearchResponse = await response.json();
|
||||
|
||||
const track = data.tracks.items.at(0);
|
||||
if (track) {
|
||||
return track;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const tracks = await Promise.all(trackPromises);
|
||||
for (const track of tracks) {
|
||||
if (track) {
|
||||
results.push(track);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Spotify access token using Client Credentials Flow.
|
||||
* Reads credentials from environment variables, caches token in memory with expiry.
|
||||
* Throws if credentials are missing or request fails.
|
||||
*/
|
||||
let spotifyTokenCache: { token: string; expiresAt: number } | null = null;
|
||||
|
||||
export async function getSpotifyAccessToken(): Promise<string> {
|
||||
const clientId = Deno.env.get("SPOTIFY_CLIENT_ID");
|
||||
const clientSecret = Deno.env.get("SPOTIFY_CLIENT_SECRET");
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error("Spotify credentials not found in environment variables.");
|
||||
}
|
||||
|
||||
// If cached and not expired, return cached token
|
||||
if (
|
||||
spotifyTokenCache &&
|
||||
spotifyTokenCache.expiresAt > Date.now()
|
||||
) {
|
||||
return spotifyTokenCache.token;
|
||||
}
|
||||
|
||||
const tokenResponse = await fetch("https://accounts.spotify.com/api/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Authorization": "Basic " +
|
||||
btoa(`${clientId}:${clientSecret}`),
|
||||
},
|
||||
body: "grant_type=client_credentials",
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch Spotify access token: ${tokenResponse.status} ${await tokenResponse
|
||||
.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
const accessToken = tokenData.access_token;
|
||||
const expiresIn = tokenData.expires_in; // seconds
|
||||
|
||||
if (!accessToken || !expiresIn) {
|
||||
throw new Error("Invalid token response from Spotify.");
|
||||
}
|
||||
|
||||
// Cache token with expiry
|
||||
spotifyTokenCache = {
|
||||
token: accessToken,
|
||||
expiresAt: Date.now() + expiresIn * 1000 - 10_000, // 10s early
|
||||
};
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
Deno.test("getSpotifyAccessToken caching", async () => {
|
||||
await getSpotifyAccessToken();
|
||||
|
||||
const start2 = performance.now();
|
||||
const token2 = await getSpotifyAccessToken();
|
||||
const elapsed2 = performance.now() - start2;
|
||||
|
||||
assert(elapsed2 < 10, `Second call took too long: ${elapsed2}ms`);
|
||||
});
|
||||
|
||||
Deno.test("searchSpotifyTracks basic", async () => {
|
||||
const songs = [
|
||||
{ title: "Blinding Lights", artist: "The Weeknd" },
|
||||
{ title: "Levitating", artist: "Dua Lipa" },
|
||||
];
|
||||
|
||||
const spotify_api_key = getSpotifyAccessToken();
|
||||
if (!spotify_api_key) throw Error("Testing token not found");
|
||||
const tracks = await searchSpotifyTracks(songs);
|
||||
|
||||
console.log(tracks);
|
||||
|
||||
assert(tracks.length > 0);
|
||||
});
|
19
backend/types/ai.ts
Normal file
19
backend/types/ai.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
export type AiSuggestionsOutput = {
|
||||
type: "success";
|
||||
songs: {
|
||||
title: string;
|
||||
artist: string;
|
||||
}[];
|
||||
} | {
|
||||
type: "error";
|
||||
error: string;
|
||||
};
|
||||
|
||||
export interface SongSearch {
|
||||
title: string;
|
||||
artist: string;
|
||||
}
|
||||
|
||||
export interface AiSnewuggestionOutput {
|
||||
songs: SongSearch[];
|
||||
}
|
47
backend/types/api.ts
Normal file
47
backend/types/api.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { SpotifyTrack } from "./spotify.ts";
|
||||
|
||||
export interface GetSongsTextParams {
|
||||
prompt: string;
|
||||
rules: string;
|
||||
}
|
||||
|
||||
export type GetSongsTextOutput = {
|
||||
type: "success";
|
||||
songs: SpotifyTrack[];
|
||||
} | {
|
||||
type: "error";
|
||||
error: string;
|
||||
};
|
||||
|
||||
export interface GetYoutubeInput {
|
||||
title: string;
|
||||
artist: string;
|
||||
}
|
||||
|
||||
export interface GetYoutubeOutput {
|
||||
videoId?: string;
|
||||
}
|
||||
|
||||
export interface GetTrackInput {
|
||||
trackId: string;
|
||||
}
|
||||
|
||||
export interface GetTrackOutput {
|
||||
track: SpotifyTrack;
|
||||
}
|
||||
|
||||
export type GetNewSuggestInput =
|
||||
& {
|
||||
rules: string;
|
||||
}
|
||||
& ({
|
||||
type: "scratch-suggestion";
|
||||
room_name: string;
|
||||
} | {
|
||||
type: "from-songs-suggestion";
|
||||
songs: { song: string }[];
|
||||
});
|
||||
|
||||
export type GetNewSuggestOutput = {
|
||||
songs: SpotifyTrack[];
|
||||
};
|
97
backend/types/spotify.ts
Normal file
97
backend/types/spotify.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
export interface SpotifyArtist {
|
||||
external_urls: {
|
||||
spotify: string;
|
||||
};
|
||||
href: string;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export interface SpotifyAlbum {
|
||||
album_type: string;
|
||||
total_tracks: number;
|
||||
available_markets: string[];
|
||||
external_urls: {
|
||||
spotify: string;
|
||||
};
|
||||
href: string;
|
||||
id: string;
|
||||
images: Array<{
|
||||
url: string;
|
||||
height: number;
|
||||
width: number;
|
||||
}>;
|
||||
name: string;
|
||||
release_date: string;
|
||||
release_date_precision: string;
|
||||
type: string;
|
||||
uri: string;
|
||||
artists: SpotifyArtist[];
|
||||
is_playable?: boolean;
|
||||
}
|
||||
|
||||
export interface SpotifyTrack {
|
||||
album: SpotifyAlbum;
|
||||
artists: SpotifyArtist[];
|
||||
available_markets: string[];
|
||||
disc_number: number;
|
||||
duration_ms: number;
|
||||
explicit: boolean;
|
||||
external_ids: {
|
||||
isrc?: string;
|
||||
};
|
||||
external_urls: {
|
||||
spotify: string;
|
||||
};
|
||||
href: string;
|
||||
id: string;
|
||||
is_playable?: boolean;
|
||||
name: string;
|
||||
popularity: number;
|
||||
preview_url: string | null;
|
||||
track_number: number;
|
||||
type: string;
|
||||
uri: string;
|
||||
is_local: boolean;
|
||||
}
|
||||
|
||||
export interface SpotifySearchResponse {
|
||||
tracks: {
|
||||
href: string;
|
||||
limit: number;
|
||||
next: string | null;
|
||||
offset: number;
|
||||
previous: string | null;
|
||||
total: number;
|
||||
items: SpotifyTrack[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SearchResultTrack {
|
||||
id: string;
|
||||
name: string;
|
||||
artists: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
spotify_url: string;
|
||||
}>;
|
||||
album: {
|
||||
id: string;
|
||||
name: string;
|
||||
release_date: string;
|
||||
images: Array<{
|
||||
url: string;
|
||||
height: number;
|
||||
width: number;
|
||||
}>;
|
||||
spotify_url: string;
|
||||
};
|
||||
duration_ms: number;
|
||||
popularity: number;
|
||||
preview_url: string | null;
|
||||
spotify_url: string;
|
||||
explicit: boolean;
|
||||
available_markets: string[];
|
||||
}
|
4
backend/types/youtube.ts
Normal file
4
backend/types/youtube.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface Vids {
|
||||
title: string;
|
||||
artist: string;
|
||||
}
|
46
backend/youtube-api.ts
Normal file
46
backend/youtube-api.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { assert } from "jsr:@std/assert/assert";
|
||||
import { Vids } from "./types/youtube.ts";
|
||||
|
||||
export async function searchYTVideo(
|
||||
videos: Vids,
|
||||
): Promise<string | undefined> {
|
||||
const query = `${videos.title} ${videos.artist}`;
|
||||
const params = new URLSearchParams({
|
||||
part: "snippet",
|
||||
q: query,
|
||||
type: "video",
|
||||
key: Deno.env.get("YT_API_KEY")!, // Replace with your actual API key
|
||||
});
|
||||
|
||||
const endpoint =
|
||||
`https://www.googleapis.com/youtube/v3/search?${params.toString()}`;
|
||||
|
||||
const response = await fetch(endpoint);
|
||||
if (!response.ok) {
|
||||
throw new Error(`YouTube API error: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const videoId = data.items && data.items.length > 0
|
||||
? data.items[0].id.videoId
|
||||
: undefined;
|
||||
|
||||
if (!videoId) {
|
||||
throw new Error("No video found for the given title and artist.");
|
||||
}
|
||||
|
||||
return videoId;
|
||||
}
|
||||
|
||||
Deno.test("searchYT", async () => {
|
||||
const yt_api_key = Deno.env.get("YT_API_KEY");
|
||||
if (!yt_api_key) throw Error("Testing token not found");
|
||||
const videoId = await searchYTVideo({
|
||||
title: "Here Comes The Sun",
|
||||
artist: "The Beatles",
|
||||
});
|
||||
|
||||
console.log("Here's the video ID:", videoId);
|
||||
|
||||
assert(typeof videoId === "string" && videoId);
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue