Compare commits
18 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7443d600e7 | ||
![]() |
bf71a8103d | ||
f7351aecf8 | |||
436553809a | |||
fb49da2edd | |||
![]() |
9a12f39f3e | ||
![]() |
9ec601b263 | ||
![]() |
1763a6b96f | ||
360338ccb2 | |||
278a2d94a8 | |||
![]() |
c3d30a1cc8 | ||
![]() |
1063c239b6 | ||
![]() |
6a50dc4c86 | ||
![]() |
efcabd2ee4 | ||
746189862a | |||
01cf53c775 | |||
7587796934 | |||
f4a4c46fbc |
14 changed files with 169 additions and 67 deletions
41
README.md
41
README.md
|
@ -1,3 +1,40 @@
|
|||
# team-1
|
||||
# ChillBox
|
||||
|
||||
Test
|
||||
> *A project by Pausetta.org, Simone Tesini, Francesco De Carlo, Leonardo Segala, Matteo Peretto*
|
||||
|
||||
**ChillBox** is a web app that lets you create a shared radio station with a democratic voting system, so everyone gets to enjoy their favorite music together.
|
||||
Perfect for venues like swimming pools, cafés, or even lively parties.
|
||||
|
||||
---
|
||||
|
||||
## 🎵 Voting System
|
||||
|
||||
Joining a ChillBox room is easy: users can either scan the QR code displayed on the host screen or use GPS to find nearby rooms.
|
||||
Hosts can set a location range, ensuring only people physically present can add or vote for songs.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Ranking Algorithm
|
||||
|
||||
ChillBox uses a smart ranking algorithm to decide what plays next. The score of each song is based on:
|
||||
|
||||
* Votes from users
|
||||
* How recently similar songs (same genre or artist) have been played (less = better)
|
||||
* A bit of randomness to keep things interesting
|
||||
* A strong penalty for songs played too recently
|
||||
|
||||
---
|
||||
|
||||
## 👐 Hands-Off Experience
|
||||
|
||||
ChillBox is designed to be almost entirely hands-free.
|
||||
Once the host sets up a room and optionally connects a screen or projector
|
||||
(to show the current track, QR code, etc.), ChillBox takes care of the rest.
|
||||
|
||||
ChillBox comes with built-in automatic moderation to keep the music fair and on-theme.
|
||||
|
||||
* Users can’t vote for the same song multiple times.
|
||||
* A cooldown prevents users from spamming song requests.
|
||||
* Hosts can define preferred genres and overall mood, so no one can hijack your chill beach vibes with unexpected death metal.
|
||||
|
||||
That said, hosts still have access to essential controls, like pause and skip, if needed.
|
||||
|
|
15
SPEECH.md
Normal file
15
SPEECH.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# speech
|
||||
|
||||
## Home screen
|
||||
We start here in the home page.
|
||||
We can see this little radar animation, which means that the app is looking for nearby ChillBox rooms to join.
|
||||
It uses GPS for this feature.
|
||||
|
||||
## Join room
|
||||
When we join a room, the server checks our location and checks if it's within a specified range.
|
||||
That way, you must physically be in the location to actually be able to add new songs
|
||||
|
||||
## Talk about the host
|
||||
As you can see here (and hear) on the left, the host is already playing some music.
|
||||
Now i will add a song on the client side and it will pop up in the list.
|
||||
|
|
@ -29,9 +29,9 @@ init_db(state.db)
|
|||
state.rooms[1000] = Room(
|
||||
id=1000,
|
||||
coord=Coordinates(46.6769043, 11.1851585),
|
||||
name="Test Room",
|
||||
pin=None,
|
||||
tags=set(),
|
||||
name="Lido Scena",
|
||||
pin=1234,
|
||||
tags=set(["chill", "raggaetton", "spanish", "latino", "mexican", "rock"]),
|
||||
range_size=150,
|
||||
songs={},
|
||||
history=[],
|
||||
|
@ -73,7 +73,7 @@ def on_leave(data):
|
|||
@app.get("/api/join")
|
||||
def join():
|
||||
room_id = request.args.get("room")
|
||||
code = request.args.get("code")
|
||||
code = request.args.get("pin")
|
||||
|
||||
if room_id is None:
|
||||
return error("Missing room id")
|
||||
|
@ -81,7 +81,10 @@ def join():
|
|||
if (room := state.rooms.get(int(room_id))) is None:
|
||||
return error("Invalid room")
|
||||
|
||||
if room.pin is not None and room.pin != code:
|
||||
if room.pin is not None:
|
||||
if code is None:
|
||||
return error("Missing code")
|
||||
if int(room.pin) != int(code):
|
||||
return error("Invalid code")
|
||||
|
||||
distance = distance_between_coords(
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { type Room } from "$lib/types"
|
||||
let { room }: { room: Room } = $props()
|
||||
let showPinModal: boolean = $state(false)
|
||||
let pin: number = $state()
|
||||
</script>
|
||||
|
||||
<a
|
||||
class="flex w-82 cursor-pointer flex-row items-center rounded-md border border-dark-pine-muted bg-light-pine-overlay p-3 hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20"
|
||||
href="/room/{room.id}"
|
||||
<div
|
||||
class="flex gap-2 w-82 cursor-pointer flex-col rounded-md border border-dark-pine-muted bg-light-pine-overlay p-3 hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20"
|
||||
>
|
||||
<button
|
||||
class="flex flex-row items-center"
|
||||
onclick={() => {
|
||||
if (!room.private) {
|
||||
window.location.href = "/room/" + room.id
|
||||
return
|
||||
}
|
||||
showPinModal = !showPinModal
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row">
|
||||
{room.name}
|
||||
|
@ -16,4 +27,19 @@
|
|||
<div class="font-mono">{Math.round(room.distance)}m</div>
|
||||
<div class="rounded bg-light-pine-blue px-2 py-0.5 text-dark-pine-text dark:bg-dark-pine-blue">Join</div>
|
||||
</div>
|
||||
</a>
|
||||
</button>
|
||||
{#if showPinModal}
|
||||
<input
|
||||
placeholder="PIN (requied)"
|
||||
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay dark:bg-dark-pine-base hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
|
||||
type="number"
|
||||
bind:value={pin}
|
||||
/>
|
||||
<button
|
||||
onclick={() => {
|
||||
window.location.href = `/room/${room.id}?pin=${pin}`
|
||||
}}
|
||||
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay dark:bg-dark-pine-base hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2">JOIN</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,20 +1,39 @@
|
|||
<script lang="ts">
|
||||
import { LoaderCircle } from "@lucide/svelte"
|
||||
|
||||
const COOLDOWN_SECS = 10
|
||||
|
||||
let { roomId } = $props()
|
||||
let input = $state("")
|
||||
let disabled: boolean = $state(false)
|
||||
let loading: boolean = $state(false)
|
||||
let cooldowned: boolean = $state(false)
|
||||
let errorMsg: string = $state()
|
||||
|
||||
$effect(() => {
|
||||
console.log("cooldowned is now", cooldowned)
|
||||
})
|
||||
|
||||
async function sendSong() {
|
||||
disabled = true
|
||||
await fetch(`/api/addsong?room=${roomId}&query=${input}`, { method: "POST" })
|
||||
loading = true
|
||||
const res = await fetch(`/api/addsong?room=${roomId}&query=${input}`, { method: "POST" })
|
||||
const json = await res.json()
|
||||
input = ""
|
||||
disabled = false
|
||||
loading = false
|
||||
|
||||
if (!json.success) {
|
||||
errorMsg = json.error
|
||||
}
|
||||
|
||||
cooldowned = true
|
||||
setTimeout(() => {
|
||||
cooldowned = false
|
||||
console.log("unset cooldown")
|
||||
}, COOLDOWN_SECS * 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={`flex h-full w-full flex-row items-center gap-2 rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 ${disabled ? "disabled" : ""}`}
|
||||
class={`flex h-full w-full flex-row items-center gap-2 rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 ${loading ? "disabled" : ""}`}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -22,20 +41,29 @@
|
|||
class="h-[50px] w-3/4 rounded px-4 font-bold text-white outline-none"
|
||||
bind:value={input}
|
||||
onkeydown={(e) => {
|
||||
errorMsg = null
|
||||
if (e.key == "Enter") {
|
||||
sendSong()
|
||||
}
|
||||
}}
|
||||
{disabled}
|
||||
disabled={loading}
|
||||
/>
|
||||
{#if disabled}
|
||||
{#if loading}
|
||||
<span class="animate-spin">
|
||||
<LoaderCircle />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<button class="i-lucide-check h-[40px] w-1/4 cursor-pointer rounded border border-0 font-semibold shadow-xl duration-100 hover:scale-105 active:scale-90 dark:bg-dark-pine-blue" onclick={sendSong}
|
||||
>Add</button
|
||||
<button
|
||||
disabled={cooldowned}
|
||||
class="i-lucide-check h-[40px] w-1/4 rounded font-semibold shadow-xl duration-100 active:scale-90 {!cooldowned
|
||||
? 'cursor-pointer bg-light-pine-blue hover:scale-105 dark:bg-dark-pine-blue '
|
||||
: 'bg-light-pine-muted dark:bg-light-pine-muted'}"
|
||||
onclick={sendSong}>Add</button
|
||||
>
|
||||
<span class="i-lucide-chevrons-left"></span>
|
||||
</div>
|
||||
|
||||
<p class="text-red-500 font-semibold">
|
||||
{errorMsg}
|
||||
</p>
|
||||
|
|
|
@ -7,9 +7,8 @@
|
|||
|
||||
let picked_suggestions: string[] = $state([])
|
||||
|
||||
async function vote(idx: number, amount: number, songId: string) {
|
||||
async function vote(amount: number, songId: string) {
|
||||
if (picked_suggestions.includes(songId)) return console.log("rejecting vote")
|
||||
suggestions[idx].upvote += amount
|
||||
await fetch(`/api/song/voting?room=${roomId}&song=${songId}&increment=${amount}`, { method: "POST" })
|
||||
picked_suggestions.push(songId)
|
||||
console.log("accepted vote")
|
||||
|
@ -26,7 +25,7 @@
|
|||
<p>No suggestions yet! Try to add a new one using the Add button</p>
|
||||
{/if}
|
||||
|
||||
{#each suggestions as sug, idx}
|
||||
{#each suggestions as sug}
|
||||
<div
|
||||
class="flex h-[80px] w-full flex-row gap-2 rounded-md border-dark-pine-muted bg-light-pine-overlay p-2 shadow-md duration-100 hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20"
|
||||
>
|
||||
|
@ -48,7 +47,7 @@
|
|||
class={!picked_suggestions.includes(sug.uuid) ? "text-light-pine-green duration-100 hover:scale-150 dark:text-dark-pine-green" : "text-light-pine-muted dark:text-dark-pine-muted"}
|
||||
disabled={!!picked_suggestions.includes(sug.uuid)}
|
||||
onclick={async () => {
|
||||
await vote(idx, 1, sug.uuid)
|
||||
await vote(1, sug.uuid)
|
||||
}}><ThumbsUp /></button
|
||||
>
|
||||
<p class="font-semibold text-light-pine-text dark:text-dark-pine-text">{sug.upvote}</p>
|
||||
|
@ -56,7 +55,7 @@
|
|||
class={!picked_suggestions.includes(sug.uuid) ? "text-light-pine-red duration-100 hover:scale-150 dark:text-dark-pine-red" : "text-light-pine-muted dark:text-dark-pine-muted"}
|
||||
disabled={!!picked_suggestions.includes(sug.uuid)}
|
||||
onclick={async () => {
|
||||
await vote(idx, -1, sug.uuid)
|
||||
await vote(-1, sug.uuid)
|
||||
}}><ThumbsDown /></button
|
||||
>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { get_coords, type Coordinates } from "./gps"
|
||||
import { type Coordinates } from "./gps"
|
||||
import { parseSong, parseSuggestion, type FetchError, type Song, type Suggestion } from "./types"
|
||||
|
||||
export const joinRoom = async function (roomId: string, coords: Coordinates): Promise<[FetchError | null, string]> {
|
||||
let res = await fetch(`/api/join?room=${roomId}&lat=${coords.latitude}&lon=${coords.longitude}`)
|
||||
export const joinRoom = async function (roomId: string, coords: Coordinates, pin: string): Promise<[FetchError | null, string]> {
|
||||
let res = await fetch(`/api/join?room=${roomId}&lat=${coords.latitude}&lon=${coords.longitude}&pin=${pin}`)
|
||||
|
||||
if (res.status != 200) {
|
||||
return [{ code: 400, message: "Cannot join the room" }, ""]
|
||||
|
|
|
@ -23,12 +23,12 @@
|
|||
<span class="lilita-one-regular text-6xl font-bold">ChillBox</span>
|
||||
</div>
|
||||
|
||||
<img src="/smerdo_radar_bonus.gif" alt="radar" class="h-64 w-64" />
|
||||
<img src="/radar_bonus.gif" alt="radar" class="h-64 w-64" />
|
||||
<span class="animate-pulse text-sm italic">Scanning for rooms near you...</span>
|
||||
|
||||
<button
|
||||
onclick={() => {
|
||||
window.location.href = "/room/new"
|
||||
window.location.href = "/room/create"
|
||||
}}
|
||||
class="fixed right-4 bottom-4 flex flex-row gap-1 rounded-xl bg-light-pine-blue p-2 text-dark-pine-text sm:right-20 md:right-40 lg:right-80 dark:bg-dark-pine-blue"
|
||||
>
|
||||
|
|
|
@ -112,5 +112,7 @@
|
|||
<button class="rounded-md border border-dark-pine-muted p-2 hover:scale-105 active:scale-90 dark:bg-dark-pine-blue" onclick={playNext}>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<img class="absolute right-1 bottom-1" src="/api/room/qrcode?room=1000&pin=1234" />
|
||||
<!-- @PERETTO fix here pls -->
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
}
|
||||
|
||||
let sugg, queue, index
|
||||
;[returnError] = await joinRoom(data.roomId, coords)
|
||||
;[returnError] = await joinRoom(data.roomId, coords, data.pin)
|
||||
if (returnError) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,5 +3,6 @@ import type { PageLoad } from "./$types"
|
|||
export const load: PageLoad = ({ params, url }) => {
|
||||
return {
|
||||
roomId: params.id || "",
|
||||
pin: url.searchParams.get("pin") || "",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,7 @@
|
|||
|
||||
let name: string = $state()
|
||||
let range: number = $state()
|
||||
|
||||
const privateStyle = "bg-red-500"
|
||||
const publicStyle = "bg-green-500"
|
||||
let pin: number = $state()
|
||||
|
||||
async function createRoom() {
|
||||
if (creating) {
|
||||
|
@ -22,16 +20,12 @@
|
|||
return
|
||||
}
|
||||
|
||||
let pin
|
||||
if (privateRoom) {
|
||||
pin = Math.floor(Math.random() * 10000)
|
||||
} else {
|
||||
pin = ""
|
||||
}
|
||||
|
||||
creating = true
|
||||
|
||||
const res = await fetch(`/api/room/new?name=${encodeURIComponent(name)}&coords=${coord.latitude},${coord.longitude}&range=${encodeURIComponent(range ?? "100")}&pin=${pin}`, { method: "POST" })
|
||||
const res = await fetch(
|
||||
`/api/room/new?name=${encodeURIComponent(name)}&coords=${coord.latitude},${coord.longitude}&range=${encodeURIComponent(range ?? "100")}&pin=${encodeURIComponent(pin ?? "")}`,
|
||||
{ method: "POST" }
|
||||
)
|
||||
|
||||
const json = await res.json()
|
||||
|
||||
|
@ -50,8 +44,10 @@
|
|||
<div class="flex flex-col gap-3 w-1/2">
|
||||
<input
|
||||
bind:value={name}
|
||||
placeholder="Room name"
|
||||
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
|
||||
placeholder="Room name (Required)"
|
||||
class="{name
|
||||
? ''
|
||||
: 'border-2 border-red-500'} p-2 text-xl rounded-md bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
|
||||
/>
|
||||
|
||||
<input
|
||||
|
@ -67,21 +63,16 @@
|
|||
<span>{coord.latitude},{coord.longitude}</span>
|
||||
</p>
|
||||
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
Public Room:
|
||||
<button class="cursor-pointer w-10 h-10 rounded-lg duration-200 flex items-center justify-center {privateRoom ? privateStyle : publicStyle}" onclick={() => (privateRoom = !privateRoom)}>
|
||||
{#if privateRoom}
|
||||
<X />
|
||||
{:else}
|
||||
<Check />
|
||||
{/if}
|
||||
</button>
|
||||
{#if !privateRoom}
|
||||
<p class="text-sm italic">The room is flagged as public, everyone can join</p>
|
||||
{/if}
|
||||
</div>
|
||||
<input
|
||||
bind:value={pin}
|
||||
type="number"
|
||||
max="9999"
|
||||
placeholder="PIN (none if public)"
|
||||
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer flex flex-row items-center justify-center gap-3 border border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 rounded-lg h-10 font-bold"
|
||||
onclick={createRoom}
|
||||
>
|
||||
|
|
Before Width: | Height: | Size: 420 KiB After Width: | Height: | Size: 420 KiB |
Before Width: | Height: | Size: 446 KiB After Width: | Height: | Size: 446 KiB |
Loading…
Add table
Add a link
Reference in a new issue