From e39873be5ce3f865dbfa9baf442d4d8d7c5ab968 Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Fri, 11 Sep 2026 14:31:44 +0200 Subject: [PATCH 1/6] Created data model for Artist, Track and Album. Co-authored-by: kfmelen --- webdev-prosjekt1/src/api/deezer.ts | 0 webdev-prosjekt1/src/api/types.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 webdev-prosjekt1/src/api/deezer.ts create mode 100644 webdev-prosjekt1/src/api/types.ts diff --git a/webdev-prosjekt1/src/api/deezer.ts b/webdev-prosjekt1/src/api/deezer.ts new file mode 100644 index 0000000..e69de29 diff --git a/webdev-prosjekt1/src/api/types.ts b/webdev-prosjekt1/src/api/types.ts new file mode 100644 index 0000000..088f73b --- /dev/null +++ b/webdev-prosjekt1/src/api/types.ts @@ -0,0 +1,29 @@ +// This file defines api related data model. + +export type Artist = { + id: number; + name: string; + image: string; +} + +export type Track = { + id: number; + name: string; + cover: string; + artist: string; + album: string; + duration: number; + iscr: string; + link: string; + rank: number; + preview: string; +} + +export type Album = { + id: number; + name: string; + link: string; + nb_tracks: number; + genre_id: number; + record_type: string; +} \ No newline at end of file From fc0a8553128b2bbdaec9fdd047ac65745e67ebd9 Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Fri, 11 Sep 2026 14:31:44 +0200 Subject: [PATCH 2/6] Created data model for Artist, Track and Album. > Co-authored-by: kfmelen --- webdev-prosjekt1/src/api/deezer.ts | 0 webdev-prosjekt1/src/api/types.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 webdev-prosjekt1/src/api/deezer.ts create mode 100644 webdev-prosjekt1/src/api/types.ts diff --git a/webdev-prosjekt1/src/api/deezer.ts b/webdev-prosjekt1/src/api/deezer.ts new file mode 100644 index 0000000..e69de29 diff --git a/webdev-prosjekt1/src/api/types.ts b/webdev-prosjekt1/src/api/types.ts new file mode 100644 index 0000000..088f73b --- /dev/null +++ b/webdev-prosjekt1/src/api/types.ts @@ -0,0 +1,29 @@ +// This file defines api related data model. + +export type Artist = { + id: number; + name: string; + image: string; +} + +export type Track = { + id: number; + name: string; + cover: string; + artist: string; + album: string; + duration: number; + iscr: string; + link: string; + rank: number; + preview: string; +} + +export type Album = { + id: number; + name: string; + link: string; + nb_tracks: number; + genre_id: number; + record_type: string; +} \ No newline at end of file From 82cd5133410ff4834b795f31dd93ffa9566757b6 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Fri, 11 Sep 2026 14:48:52 +0200 Subject: [PATCH 3/6] Revert "Created data model for Artist, Track and Album." This reverts commit fc0a8553128b2bbdaec9fdd047ac65745e67ebd9. --- webdev-prosjekt1/src/api/deezer.ts | 0 webdev-prosjekt1/src/api/types.ts | 29 ----------------------------- 2 files changed, 29 deletions(-) delete mode 100644 webdev-prosjekt1/src/api/deezer.ts delete mode 100644 webdev-prosjekt1/src/api/types.ts diff --git a/webdev-prosjekt1/src/api/deezer.ts b/webdev-prosjekt1/src/api/deezer.ts deleted file mode 100644 index e69de29..0000000 diff --git a/webdev-prosjekt1/src/api/types.ts b/webdev-prosjekt1/src/api/types.ts deleted file mode 100644 index 088f73b..0000000 --- a/webdev-prosjekt1/src/api/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -// This file defines api related data model. - -export type Artist = { - id: number; - name: string; - image: string; -} - -export type Track = { - id: number; - name: string; - cover: string; - artist: string; - album: string; - duration: number; - iscr: string; - link: string; - rank: number; - preview: string; -} - -export type Album = { - id: number; - name: string; - link: string; - nb_tracks: number; - genre_id: number; - record_type: string; -} \ No newline at end of file From c4b54ac1173453fe374aa8d6b5d46cbca208ad47 Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Fri, 11 Sep 2026 14:56:21 +0200 Subject: [PATCH 4/6] Created data model for Artist, Track and Album. Co-authored-by: kfmelen --- webdev-prosjekt1/src/api/deezer.ts | 0 webdev-prosjekt1/src/api/types.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 webdev-prosjekt1/src/api/deezer.ts create mode 100644 webdev-prosjekt1/src/api/types.ts diff --git a/webdev-prosjekt1/src/api/deezer.ts b/webdev-prosjekt1/src/api/deezer.ts new file mode 100644 index 0000000..e69de29 diff --git a/webdev-prosjekt1/src/api/types.ts b/webdev-prosjekt1/src/api/types.ts new file mode 100644 index 0000000..26f86d6 --- /dev/null +++ b/webdev-prosjekt1/src/api/types.ts @@ -0,0 +1,30 @@ +// This file defines api related data model. + +export type Artist = { + id: number; + name: string; + image: string; +} + + +export type Track = { + id: number; + name: string; + cover: string; + artist: string; + album: string; + duration: number; + iscr: string; + link: string; + rank: number; + preview: string; +} + +export type Album = { + id: number; + name: string; + link: string; + nb_tracks: number; + genre_id: number; + record_type: string; +} \ No newline at end of file From 085c00e30e2572b4d4adbf96696a33a94841ac41 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Fri, 11 Sep 2026 23:57:34 +0200 Subject: [PATCH 5/6] Changed music metadata provider to spotify, as deezer did not work in browser. Co-authored-by: johagber --- webdev-prosjekt1/.gitignore | 1 + webdev-prosjekt1/package-lock.json | 30 ++++---- webdev-prosjekt1/package.json | 2 +- webdev-prosjekt1/src/App.tsx | 3 + webdev-prosjekt1/src/api/deezer.ts | 0 webdev-prosjekt1/src/api/spotify.ts | 106 ++++++++++++++++++++++++++++ webdev-prosjekt1/src/api/types.ts | 58 +++++++++++---- 7 files changed, 169 insertions(+), 31 deletions(-) delete mode 100644 webdev-prosjekt1/src/api/deezer.ts create mode 100644 webdev-prosjekt1/src/api/spotify.ts diff --git a/webdev-prosjekt1/.gitignore b/webdev-prosjekt1/.gitignore index a547bf3..19862e3 100644 --- a/webdev-prosjekt1/.gitignore +++ b/webdev-prosjekt1/.gitignore @@ -16,6 +16,7 @@ dist-ssr .vscode/* !.vscode/extensions.json .idea +.env .DS_Store *.suo *.ntvs* diff --git a/webdev-prosjekt1/package-lock.json b/webdev-prosjekt1/package-lock.json index f7f3f88..c8fbc13 100644 --- a/webdev-prosjekt1/package-lock.json +++ b/webdev-prosjekt1/package-lock.json @@ -27,7 +27,7 @@ "globals": "^17.11.0", "typescript": "~6.0.2", "typescript-eslint": "^8.67.0", - "vite": "^8.2.2" + "vite": "^8.3.0" } }, "node_modules/@babel/code-frame": { @@ -2203,9 +2203,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -2329,9 +2329,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -2349,7 +2349,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2611,16 +2611,16 @@ } }, "node_modules/vite": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", - "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.26", - "rolldown": "~1.2.4", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "bin": { @@ -2637,7 +2637,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/webdev-prosjekt1/package.json b/webdev-prosjekt1/package.json index c5d5b5c..00003ce 100644 --- a/webdev-prosjekt1/package.json +++ b/webdev-prosjekt1/package.json @@ -29,6 +29,6 @@ "globals": "^17.11.0", "typescript": "~6.0.2", "typescript-eslint": "^8.67.0", - "vite": "^8.2.2" + "vite": "^8.3.0" } } diff --git a/webdev-prosjekt1/src/App.tsx b/webdev-prosjekt1/src/App.tsx index 113a1b2..dcd63f9 100644 --- a/webdev-prosjekt1/src/App.tsx +++ b/webdev-prosjekt1/src/App.tsx @@ -1,9 +1,12 @@ import { useState } from 'react' import './App.css' +import { searchTrack, searchAlbum, searchArtist } from "./api/spotify" + function App() { return ( <> + {console.log(searchArtist("kygo"))} ) } diff --git a/webdev-prosjekt1/src/api/deezer.ts b/webdev-prosjekt1/src/api/deezer.ts deleted file mode 100644 index e69de29..0000000 diff --git a/webdev-prosjekt1/src/api/spotify.ts b/webdev-prosjekt1/src/api/spotify.ts new file mode 100644 index 0000000..f327449 --- /dev/null +++ b/webdev-prosjekt1/src/api/spotify.ts @@ -0,0 +1,106 @@ +import type { Album, Artist, Track } from "./types"; + + +const BASE_URL = "https://api.spotify.com/v1" +const clientId = import.meta.env.VITE_SPOTIFY_CLIENT_ID; +const clientSecret = import.meta.env.VITE_SPOTIFY_CLIENT_SECRET; + +let accessToken: string | null = null; +let expiresAt = 0; + +async function getSpotifyToken() { + const response = await fetch("https://accounts.spotify.com/api/token", { + method: "POST", + body: new URLSearchParams({ + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + }), + }); + if (!response.ok) { + throw new Error(`Failed to get token: ${response.status}`); + } + + return response.json(); +} + +async function getValidToken() { + if (accessToken && Date.now() < expiresAt) { + return accessToken; + } + + const tokenData = await getSpotifyToken(); + + accessToken = tokenData.access_token; + expiresAt = Date.now() + tokenData.expires_in * 1000; + + return accessToken; +} + + + +export async function searchArtist(query: string): Promise { + const token = await getValidToken(); + + const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=artist&limit=10`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error(`Deezer api error: ${response.status}`); + } + + const json = await response.json(); + const artists: Artist[] = json.artists.items; + console.log(artists); + + return artists; +} + + +export async function searchTrack(query: string): Promise { + const token = await getValidToken(); + + const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=track&limit=10`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error(`Deezer api error: ${response.status}`); + } + + const json = await response.json(); + const tracks: Track[] = json.tracks.items; + console.log(tracks); + + return tracks; +} + + +export async function searchAlbum(query: string): Promise { + const token = await getValidToken(); + + const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=album&limit=10`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error(`Deezer api error: ${response.status}`); + } + + const json = await response.json(); + const albums: Album[] = json.albums.items; + console.log(albums); + + return albums; +} + diff --git a/webdev-prosjekt1/src/api/types.ts b/webdev-prosjekt1/src/api/types.ts index 26f86d6..e2cad8a 100644 --- a/webdev-prosjekt1/src/api/types.ts +++ b/webdev-prosjekt1/src/api/types.ts @@ -3,28 +3,56 @@ export type Artist = { id: number; name: string; - image: string; + type: string; + href: string; + uri: string; + images: Image[]; } export type Track = { - id: number; + id: string; name: string; - cover: string; - artist: string; - album: string; - duration: number; - iscr: string; - link: string; - rank: number; - preview: string; + type: string; + href: string; + artists: { + id: number; + name: string; + type: string; + href: string; + uri: string; + }[]; + album: Album; + duration_ms: number; + explicit: boolean; + track_number: number; + external_ids: { + isrc: string; + }; + is_playable: boolean; + uri: string; } export type Album = { - id: number; + id: string; name: string; - link: string; - nb_tracks: number; - genre_id: number; - record_type: string; + album_type: "album" | "single" | "compilation"; + artists: { + id: number; + name: string; + type: string; + href: string; + uri: string; + }[]; + images: Image[]; + release_date: string; + release_date_precision: "year" | "month" | "day"; + total_tracks: number; + uri: string; +}; + +export type Image = { + url: string; + height: number; + width: number; } \ No newline at end of file From dc97386920c54e372dece2f6bcc67f8793271c2d Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Sun, 13 Sep 2026 10:59:45 +0200 Subject: [PATCH 6/6] Changed names of variables. Co-authored-by: johagber --- webdev-prosjekt1/src/App.tsx | 2 +- webdev-prosjekt1/src/api/spotify.ts | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/webdev-prosjekt1/src/App.tsx b/webdev-prosjekt1/src/App.tsx index dcd63f9..9ba2af2 100644 --- a/webdev-prosjekt1/src/App.tsx +++ b/webdev-prosjekt1/src/App.tsx @@ -6,7 +6,7 @@ import { searchTrack, searchAlbum, searchArtist } from "./api/spotify" function App() { return ( <> - {console.log(searchArtist("kygo"))} + {console.log(searchTrack("Queen"))} ) } diff --git a/webdev-prosjekt1/src/api/spotify.ts b/webdev-prosjekt1/src/api/spotify.ts index f327449..a85d102 100644 --- a/webdev-prosjekt1/src/api/spotify.ts +++ b/webdev-prosjekt1/src/api/spotify.ts @@ -2,8 +2,9 @@ import type { Album, Artist, Track } from "./types"; const BASE_URL = "https://api.spotify.com/v1" -const clientId = import.meta.env.VITE_SPOTIFY_CLIENT_ID; -const clientSecret = import.meta.env.VITE_SPOTIFY_CLIENT_SECRET; +const CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID; +const CLIENT_SECRET = import.meta.env.VITE_SPOTIFY_CLIENT_SECRET; +const LIMIT = 10; let accessToken: string | null = null; let expiresAt = 0; @@ -13,12 +14,12 @@ async function getSpotifyToken() { method: "POST", body: new URLSearchParams({ grant_type: "client_credentials", - client_id: clientId, - client_secret: clientSecret, + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, }), }); if (!response.ok) { - throw new Error(`Failed to get token: ${response.status}`); + throw new Error(`Failed to get Spotify token: ${response.status}`); } return response.json(); @@ -42,7 +43,7 @@ async function getValidToken() { export async function searchArtist(query: string): Promise { const token = await getValidToken(); - const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=artist&limit=10`, { + const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=artist&limit=${LIMIT}`, { method: "GET", headers: { "Authorization": `Bearer ${token}` @@ -50,7 +51,7 @@ export async function searchArtist(query: string): Promise { }); if (!response.ok) { - throw new Error(`Deezer api error: ${response.status}`); + throw new Error(`Spotify api error: ${response.status}`); } const json = await response.json(); @@ -64,7 +65,7 @@ export async function searchArtist(query: string): Promise { export async function searchTrack(query: string): Promise { const token = await getValidToken(); - const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=track&limit=10`, { + const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=track&limit=${LIMIT}`, { method: "GET", headers: { "Authorization": `Bearer ${token}` @@ -72,7 +73,7 @@ export async function searchTrack(query: string): Promise { }); if (!response.ok) { - throw new Error(`Deezer api error: ${response.status}`); + throw new Error(`Spotify api error: ${response.status}`); } const json = await response.json(); @@ -86,7 +87,7 @@ export async function searchTrack(query: string): Promise { export async function searchAlbum(query: string): Promise { const token = await getValidToken(); - const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=album&limit=10`, { + const response = await fetch(`${BASE_URL}/search?q=${encodeURIComponent(query)}&type=album&limit=${LIMIT}`, { method: "GET", headers: { "Authorization": `Bearer ${token}` @@ -94,7 +95,7 @@ export async function searchAlbum(query: string): Promise { }); if (!response.ok) { - throw new Error(`Deezer api error: ${response.status}`); + throw new Error(`Spotify api error: ${response.status}`); } const json = await response.json();