wip search

This commit is contained in:
Henry Hiles 2026-01-04 20:48:23 -05:00
commit 43d37a3d06
No known key found for this signature in database
12 changed files with 376 additions and 100 deletions

17
deno.lock generated
View file

@ -1,6 +1,7 @@
{ {
"version": "5", "version": "5",
"specifiers": { "specifiers": {
"npm:@tidal-music/api@0.7": "0.7.0",
"npm:@tidal-music/auth@^1.4.0": "1.4.0", "npm:@tidal-music/auth@^1.4.0": "1.4.0",
"npm:@tidal-music/event-producer@^2.4.0": "2.4.0", "npm:@tidal-music/event-producer@^2.4.0": "2.4.0",
"npm:@tidal-music/player@~0.11.2": "0.11.2", "npm:@tidal-music/player@~0.11.2": "0.11.2",
@ -550,6 +551,12 @@
"tslib" "tslib"
] ]
}, },
"@tidal-music/api@0.7.0": {
"integrity": "sha512-WaogpHAlGhwPR/ScapgvyiXMwCIakUQPdR78LVus4vEcXr1m0oKyeuNu54Y+jMSFM26caIk2kPx75yKpiegSmA==",
"dependencies": [
"openapi-fetch"
]
},
"@tidal-music/auth@1.4.0": { "@tidal-music/auth@1.4.0": {
"integrity": "sha512-b/8n6aHYoMuzGbNTT4QOwm9xjTosHCn9F+Vi26Nq1x1OuK+XK9zbuAkSwma/C5eYHy4fPBTcSw82us5K5mUHmA==", "integrity": "sha512-b/8n6aHYoMuzGbNTT4QOwm9xjTosHCn9F+Vi26Nq1x1OuK+XK9zbuAkSwma/C5eYHy4fPBTcSw82us5K5mUHmA==",
"dependencies": [ "dependencies": [
@ -1695,6 +1702,15 @@
"regex-recursion" "regex-recursion"
] ]
}, },
"openapi-fetch@0.15.0": {
"integrity": "sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==",
"dependencies": [
"openapi-typescript-helpers"
]
},
"openapi-typescript-helpers@0.0.15": {
"integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw=="
},
"p-limit@6.2.0": { "p-limit@6.2.0": {
"integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==",
"dependencies": [ "dependencies": [
@ -2309,6 +2325,7 @@
"workspace": { "workspace": {
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [
"npm:@tidal-music/api@0.7",
"npm:@tidal-music/auth@^1.4.0", "npm:@tidal-music/auth@^1.4.0",
"npm:@tidal-music/event-producer@^2.4.0", "npm:@tidal-music/event-producer@^2.4.0",
"npm:@tidal-music/player@~0.11.2", "npm:@tidal-music/player@~0.11.2",

View file

@ -9,6 +9,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@tidal-music/api": "^0.7.0",
"@tidal-music/auth": "^1.4.0", "@tidal-music/auth": "^1.4.0",
"@tidal-music/event-producer": "^2.4.0", "@tidal-music/event-producer": "^2.4.0",
"@tidal-music/player": "^0.11.2", "@tidal-music/player": "^0.11.2",

View file

@ -5,10 +5,82 @@ import "../styles/index.css"
<Layout> <Layout>
<section id="guesses"> <section id="guesses">
<span class="incorrect">A bad answer - Someone</span>
<span class="partial">Killer Queen - Queen</span>
<span class="correct">Bohemian Rhapsody - Queen</span>
<span></span> <span></span>
<span></span> <div class="search-container">
<span></span> <input type="text" id="search" placeholder="Make a guess here..." />
<span class="correct">The Correct Answer - Queen</span> <section id="autocomplete"
><button data-title="Ruler of Everything" data-artists="3524912"
>Ruler of Everything - Tally Hall</button
><button data-title="King of Everything" data-artists="7774122"
>King of Everything - Dominic Fike</button
><button
data-title="The Queen Of All Everything"
data-artists="35020"
>The Queen Of All Everything - OTT</button
><button
data-title="Ruler of Everything"
data-artists="37670866"
>Ruler of Everything - Chonny Jash</button
><button
data-title="Ruler of Everything"
data-artists="15539806"
>Ruler of Everything - Sheet Music Boss</button
><button data-title="Ruler of Everything" data-artists="3616070"
>Ruler of Everything - Samuel Ljungblahd</button
><button
data-title="Ruler of Everything"
data-artists="16653822"
>Ruler of Everything - MixAndMash</button
><button
data-title="Ruler Of Everything"
data-artists="16653822"
>Ruler Of Everything - MixAndMash</button
><button
data-title="Ruler of Everything"
data-artists="30871574"
>Ruler of Everything - TheRealSullyG</button
><button
data-title="Ruler of Everything"
data-artists="31171068"
>Ruler of Everything - Intelevande</button
><button data-title="King of Everything" data-artists="3520382"
>King of Everything - Wiz Khalifa</button
><button
data-title="The Theory of Everything"
data-artists="4133018"
>The Theory of Everything - Jóhann Jóhannsson</button
><button
data-title="Everything Reminds Me Of Her"
data-artists="3501428"
>Everything Reminds Me Of Her - Elliott Smith</button
><button
data-title="At the Bottom of Everything"
data-artists="16906"
>At the Bottom of Everything - Bright Eyes</button
><button
data-title="Everything You Do Is A Balloon"
data-artists="3568992"
>Everything You Do Is A Balloon - Boards of Canada</button
><button data-title="Ruler of My Heart" data-artists="10532"
>Ruler of My Heart - Irma Thomas</button
><button data-title="Ruler Of My Heart" data-artists="10532"
>Ruler Of My Heart - Irma Thomas</button
><button
data-title="Everything Reminds Me Of You"
data-artists="48201581"
>Everything Reminds Me Of You - Take Care</button
><button
data-title="Everything Is Free"
data-artists="4752502,4972344"
>Everything Is Free - Flock Of Dimes, Sylvan Esso</button
><button data-title="Everything" data-artists="8718491"
>Everything - J.I the Prince of N.Y</button
></section
>
</div>
</section> </section>
<div> <div>
<button id="play"> <button id="play">
@ -36,86 +108,5 @@ import "../styles/index.css"
</div> </div>
</Layout> </Layout>
<script> <script src="../scripts/playSong.ts"></script>
import * as player from "@tidal-music/player" <script src="../scripts/search.ts"></script>
import * as auth from "@tidal-music/auth"
import * as eventProducer from "@tidal-music/event-producer"
await auth.init({
clientId: "NkcXqihOmrfZdBla",
clientSecret: "e6ldlWH48BEBOUZV1uIyIWJOf1KUGUEeH6qHkFvNjeU=",
credentialsStorageKey: "ZvZbtTWzx5@1zfiWUluxWD9@di17e1da",
})
player.setCredentialsProvider(auth.credentialsProvider)
player.setEventSender(eventProducer)
class ExtendableTimeout {
private timerId: ReturnType<typeof setTimeout>
private timeStart: number
private fn: () => void
constructor(fn: () => void, time: number) {
this.fn = fn
this.timeStart = Date.now()
this.timerId = setTimeout(fn, time)
}
extend(time: number): void {
clearTimeout(this.timerId)
const elapsed = Date.now() - this.timeStart
const newTime = Math.max(0, time - elapsed)
this.timerId = setTimeout(this.fn, newTime)
}
cancel(): void {
clearTimeout(this.timerId)
}
}
const play = document.querySelector("#play")
const played = document.querySelector("#played")
const skip = document.querySelector("#skip")
const available = document.querySelector("#available")
let phase = 1
let timer: ExtendableTimeout | null
const getTimeout = (phase: number) => (2 ** phase - 1) * 1000
const playVideo = async () => {
await player.load({
productId: "36737274",
productType: "track",
sourceId: "",
sourceType: "",
})
await player.play()
played?.classList.add("playing")
timer?.cancel()
timer = new ExtendableTimeout(() => {
player.pause()
timer = null
played?.classList.remove("playing")
}, getTimeout(phase))
}
play?.addEventListener("click", playVideo)
skip?.addEventListener("click", () => {
if (phase >= 4) return alert("FAIL!!!!")
phase += 1
// @ts-expect-error
available.style["grid-column-end"] = phase + 1
if (timer == null) {
playVideo()
} else {
timer.extend(getTimeout(phase))
}
})
</script>

9
src/scripts/auth.ts Normal file
View file

@ -0,0 +1,9 @@
import * as auth from "@tidal-music/auth"
await auth.init({
clientId: "NkcXqihOmrfZdBla",
clientSecret: "e6ldlWH48BEBOUZV1uIyIWJOf1KUGUEeH6qHkFvNjeU=",
credentialsStorageKey: "ZvZbtTWzx5@1zfiWUluxWD9@di17e1da",
})
export default auth

3
src/scripts/client.ts Normal file
View file

@ -0,0 +1,3 @@
import { createAPIClient } from "@tidal-music/api"
import auth from "./auth"
export const client = createAPIClient(auth.credentialsProvider)

10
src/scripts/debounce.ts Normal file
View file

@ -0,0 +1,10 @@
export const debounce = <T extends (...args: any[]) => void>(
fn: T
): ((...args: Parameters<T>) => void) => {
let timer: ReturnType<typeof setTimeout> | undefined
return (...args: Parameters<T>) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => fn(...args), 500)
}
}

47
src/scripts/playSong.ts Normal file
View file

@ -0,0 +1,47 @@
import player from "./player"
import { ExtendableTimeout } from "./timeout"
const play = document.querySelector("#play")
const played = document.querySelector("#played")
const skip = document.querySelector("#skip")
const available = document.querySelector("#available")
let phase = 1
let timer: ExtendableTimeout | null
const getTimeout = (phase: number) => (2 ** phase - 1) * 1000
const playSong = async () => {
played?.classList.remove("playing")
await player.load({
productId: "36737274",
productType: "track",
sourceId: "",
sourceType: "",
})
await player.play()
played?.classList.add("playing")
timer?.cancel()
timer = new ExtendableTimeout(() => {
player.pause()
timer = null
played?.classList.remove("playing")
}, getTimeout(phase))
}
play?.addEventListener("click", playSong)
skip?.addEventListener("click", () => {
if (phase >= 4) return alert("FAIL!!!!") // TODO
phase += 1
// @ts-expect-error
available.style["grid-column-end"] = phase + 1
if (timer == null) {
playSong()
} else {
timer.extend(getTimeout(phase))
}
})

8
src/scripts/player.ts Normal file
View file

@ -0,0 +1,8 @@
import * as eventProducer from "@tidal-music/event-producer"
import * as player from "@tidal-music/player"
import auth from "./auth"
player.setCredentialsProvider(auth.credentialsProvider)
player.setEventSender(eventProducer)
export default player

89
src/scripts/search.ts Normal file
View file

@ -0,0 +1,89 @@
import { client } from "./client"
import { debounce } from "./debounce"
const search = document.querySelector("#search") as HTMLInputElement
search?.addEventListener(
"input",
debounce(async (event) => {
const target = event.target as HTMLInputElement
const value = target.value
const tracks = await client.GET(
"/searchResults/{id}/relationships/tracks",
{
params: {
path: { id: value },
query: { countryCode: "US" },
},
}
)
if (!tracks.response.ok)
alert(
"An error occurred while fetching tracks, please try again later."
)
if (tracks.data?.data == null) return
const withArtists = await client.GET("/tracks", {
params: {
query: {
"filter[id]": tracks.data.data?.map((track) => track.id),
include: ["artists"],
countryCode: "US",
},
},
})
if (!withArtists.response.ok)
alert(
"An error occurred while fetching track info, please try again later."
)
const autocomplete = document.querySelector("#autocomplete")
autocomplete?.replaceChildren(
...(tracks.data?.data
?.map((orderedTrack) => {
const track = withArtists.data?.data.find(
(track) => track.id == orderedTrack.id
)
if (track == null) return
if (
track.attributes == null ||
!(
"title" in track.attributes &&
"relationships" in track
)
)
return
const span = document.createElement("button")
span.dataset.title = track.attributes.title
span.dataset.artists = track.relationships?.artists.data
?.map((artist) => artist.id)
.join()
span.textContent = `${
track.attributes.title
} - ${track.relationships?.artists.data
?.map((songArtist) => {
const artist = withArtists.data?.included?.find(
(artist) => artist.id == songArtist.id
)
if (
artist?.attributes == null ||
!("name" in artist.attributes)
)
return
return artist?.attributes?.name
})
.join(", ")}`
return span
})
.filter((element) => element != null) as HTMLSpanElement[])
)
})
)

24
src/scripts/timeout.ts Normal file
View file

@ -0,0 +1,24 @@
export class ExtendableTimeout {
private timerId: ReturnType<typeof setTimeout>
private timeStart: number
private fn: () => void
constructor(fn: () => void, time: number) {
this.fn = fn
this.timeStart = Date.now()
this.timerId = setTimeout(fn, time)
}
extend(time: number): void {
clearTimeout(this.timerId)
const elapsed = Date.now() - this.timeStart
const newTime = Math.max(0, time - elapsed)
this.timerId = setTimeout(this.fn, newTime)
}
cancel(): void {
clearTimeout(this.timerId)
}
}

View file

@ -1,5 +1,6 @@
main > * { main > * {
display: flex; display: flex;
gap: 1rem;
width: 100%; width: 100%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -10,26 +11,87 @@ main > * {
flex-direction: column; flex-direction: column;
gap: 2rem; gap: 2rem;
& span { & > span {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-weight: bold; font-weight: bold;
background-color: #000; background: rgb(26, 26, 26);
height: 2.4rem; height: 2.4rem;
width: 100%; width: 100%;
border-radius: 2rem; border-radius: 2rem;
&.correct { &.correct {
background-image: linear-gradient( background: linear-gradient(
to bottom right, to bottom right,
var(--brand-pink) 0%, var(--brand-pink) 0%,
var(--brand-purple) 100% var(--brand-purple) 100%
); );
} }
&.partial {
background: linear-gradient(
to bottom right,
rgb(255, 135, 90) 0%,
rgb(255, 196, 69) 100%
);
}
&.incorrect {
background: linear-gradient(
to bottom right,
rgb(185, 74, 72) 0%,
rgb(157, 38, 29) 100%
);
}
}
& .search-container {
position: relative;
&,
& > * {
width: 100%;
}
& > input {
border: none;
border-radius: 1rem;
padding: 1rem;
background: rgb(220, 218, 218);
&:focus {
outline: 0.3rem solid var(--brand-pink);
}
}
& > section {
border-radius: 1rem;
background-color: rgb(46, 46, 46);
max-height: 30vh;
overflow: scroll;
display: flex;
flex-direction: column;
position: absolute;
top: 3.5rem;
z-index: 3;
border: 0.2rem solid var(--brand-gray);
& button {
width: 100%;
padding: 1rem;
&:not(:first-child) {
border-top: 0.2rem solid var(--brand-gray);
}
}
}
} }
} }

View file

@ -1,3 +1,9 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
html, html,
body { body {
display: flex; display: flex;
@ -10,6 +16,7 @@ body {
:root { :root {
--brand-purple: #6e06ff; --brand-purple: #6e06ff;
--brand-pink: #cc56d0; --brand-pink: #cc56d0;
--brand-gray: rgb(98, 98, 98);
background: linear-gradient(black, rgb(18, 2, 38)); background: linear-gradient(black, rgb(18, 2, 38));
font-family: sans-serif; font-family: sans-serif;
@ -27,7 +34,7 @@ nav {
align-items: center; align-items: center;
padding: 2rem 3rem; padding: 2rem 3rem;
height: 5rem; height: 8rem;
& span { & span {
font-size: 6rem; font-size: 6rem;
text-shadow: 2px 0 var(--brand-purple), -2px 0 var(--brand-purple), text-shadow: 2px 0 var(--brand-purple), -2px 0 var(--brand-purple),
@ -44,26 +51,33 @@ main {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 1rem; gap: 1.5rem;
flex-grow: 1; flex-grow: 1;
} }
button { button {
background-color: transparent;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border: none;
color: white;
&:hover {
filter: brightness(80%);
}
}
:not(#autocomplete) > button {
--border-radius: 2rem; --border-radius: 2rem;
position: relative; position: relative;
background: white; background: white;
color: black; color: black;
border: none;
border-radius: var(--border-radius); border-radius: var(--border-radius);
padding: 0.2rem 1rem; padding: 0.2rem 1rem;
margin: 1rem;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover { &:hover {
background: rgb(211, 211, 211); background: rgb(211, 211, 211);
} }
@ -73,14 +87,15 @@ button {
} }
} }
button::before { :not(#autocomplete) > button::before {
--border-size: 5px; --border-size: 5px;
box-sizing: content-box;
content: ""; content: "";
z-index: -1; z-index: -1;
border-radius: var(--border-radius); border-radius: var(--border-radius);
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: linear-gradient( background: linear-gradient(
to bottom right, to bottom right,
var(--brand-pink) 0%, var(--brand-pink) 0%,
var(--brand-purple) 100% var(--brand-purple) 100%