diff --git a/README.md b/README.md index 65f5fc8..84edd62 100644 --- a/README.md +++ b/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. diff --git a/SPEECH.md b/SPEECH.md new file mode 100644 index 0000000..2ab507f --- /dev/null +++ b/SPEECH.md @@ -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. + diff --git a/backend/Dockerfile b/backend/Dockerfile index c6a1412..3390030 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -12,5 +12,6 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 -CMD ["flask", "--app", "src.app", "run", "--debug", "--host=0.0.0.0"] +# CMD ["flask", "--app", "src.app", "run", "--debug", "--host=0.0.0.0"] +CMD ["python3", "src/app.py"] # flask --app src.app run --host=0.0.0.0 --port=5001 --debug diff --git a/backend/requirements.txt b/backend/requirements.txt index 5eae02e..cb18a70 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,4 +4,5 @@ flask-socketio dotenv requests qrcode -Pillow \ No newline at end of file +Pillow +eventlet>=0.33 diff --git a/backend/src/app.py b/backend/src/app.py index c11cf37..28c66e9 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,17 +1,17 @@ import uuid from dataclasses import asdict + import dotenv +from connect import get_connection from flask import Flask, Response, jsonify, request from flask_cors import CORS from flask_socketio import SocketIO, join_room, leave_room - -from .state import State -from .connect import get_connection -from .room import Room -from .song import Song, init_db, get_song_by_title_artist, add_song_in_db, get_song_by_uuid -from .song_fetch import query_search, yt_get_audio_url, yt_search_song -from .qrcode_gen import generate_qr - +from gps import Coordinates, distance_between_coords +from qrcode_gen import generate_qr +from room import Room +from song import Song, add_song_in_db, get_song_by_title_artist, get_song_by_uuid, init_db +from song_fetch import query_search, yt_get_audio_url, yt_search_song +from state import State dotenv.load_dotenv() @@ -28,11 +28,11 @@ init_db(state.db) state.rooms[1000] = Room( id=1000, - coord=(1.0, 5.5), - name="Test Room", - pin=None, - tags=set(), - range_size=100, + coord=Coordinates(46.6769043, 11.1851585), + name="Lido Scena", + pin=1234, + tags=set(["chill", "raggaetton", "spanish", "latino", "mexican", "rock"]), + range_size=150, songs={}, history=[], playing=[], @@ -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,8 +81,22 @@ 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: - return error("Invalid 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( + lhs=room.coord, + rhs=Coordinates( + latitude=float(request.args["lat"]), + longitude=float(request.args["lon"]), + ), + ) + + if distance > room.range_size: + return error("You are not within the room range") return {"success": True, "ws": f"/ws/{room_id}"} @@ -140,8 +154,8 @@ def room_new(): lat, lon = room_cords.split(",") room = Room( - id=max(state.rooms or [0]) + 1, # - coord=(float(lat), float(lon)), + id=max(state.rooms or [0]) + 1, + coord=Coordinates(float(lat), float(lon)), range_size=int(room_range), name=room_name, pin=room_pin, @@ -159,6 +173,14 @@ def room_new(): @app.get("/api/room") def room(): + lat = request.args.get("lat") + lon = request.args.get("lon") + + if lat and lon: + user_coords = Coordinates(latitude=float(lat), longitude=float(lon)) + else: + return error("Missing user coordinates") + return [ { "id": room.id, @@ -166,8 +188,10 @@ def room(): "private": room.pin is not None, "coords": room.coord, "range": room.range_size, + "distance": d, } for room in state.rooms.values() + if (d := distance_between_coords(user_coords, room.coord)) <= room.range_size ] @@ -189,6 +213,9 @@ def add_song(): ## song not found, downolad from YT yt_video_id = yt_search_song(info.title, info.artist) + if yt_video_id is None: + return error("No video found on youtube") + ## add in DB song = Song( uuid=str(uuid.uuid4()), @@ -201,6 +228,15 @@ def add_song(): add_song_in_db(song) + if len(room.tags) > 0: + tag_ok = False + for tag in song.tags: + if tag in room.tags: + tag_ok = True + + if not tag_ok: + return error("Song genre does not belong to this room") + ## add the song in the room if does not exists if song.uuid not in room.songs: room.songs[song.uuid] = (song, 1) # start with one vote @@ -273,4 +309,4 @@ def get_audio_url(): if __name__ == "__main__": - socketio.run(app, debug=True) + socketio.run(app, host="0.0.0.0", port=5000, debug=True) diff --git a/backend/src/gps.py b/backend/src/gps.py index 546531b..98bda0c 100644 --- a/backend/src/gps.py +++ b/backend/src/gps.py @@ -1,9 +1,11 @@ import math +from dataclasses import dataclass +@dataclass class Coordinates: - latitude: int - longitude: int + latitude: float + longitude: float def distance_between_coords(lhs: Coordinates, rhs: Coordinates) -> float: diff --git a/backend/src/room.py b/backend/src/room.py index 5109884..cb775b6 100644 --- a/backend/src/room.py +++ b/backend/src/room.py @@ -1,7 +1,8 @@ import random from dataclasses import dataclass -from .song import Song +from gps import Coordinates +from song import Song USER_SCORE_WEIGHT = 0.7 ARTIST_WEIGHT = 0.1 @@ -30,7 +31,7 @@ class Rank: @dataclass class Room: id: int - coord: tuple[float, float] + coord: Coordinates name: str pin: int | None tags: set[str] diff --git a/backend/src/song.py b/backend/src/song.py index 7409d06..f95609b 100644 --- a/backend/src/song.py +++ b/backend/src/song.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from sqlite3 import Cursor -from .connect import get_connection +from connect import get_connection def init_db(db: Cursor): diff --git a/backend/src/song_fetch.py b/backend/src/song_fetch.py index 665e2c9..905d126 100644 --- a/backend/src/song_fetch.py +++ b/backend/src/song_fetch.py @@ -1,10 +1,11 @@ -import requests -import urllib.parse -import os.path import os +import os.path import sys +import urllib.parse from dataclasses import dataclass +import requests + sys.path.append("/yt-dlp") import yt_dlp @@ -17,7 +18,7 @@ class SongInfo: tags: list[str] -def _lastfm_search(query: str) -> tuple[str, str]: +def _lastfm_search(query: str) -> tuple[str, str] | None: response = requests.get( url="https://ws.audioscrobbler.com/2.0/?method=track.search&format=json", params={"limit": 5, "track": query, "api_key": os.environ["LASTFM_API_KEY"]}, @@ -25,7 +26,10 @@ def _lastfm_search(query: str) -> tuple[str, str]: assert response.status_code == 200 - track_info = response.json()["results"]["trackmatches"]["track"][0] + tracks = response.json()["results"]["trackmatches"]["track"] + if len(tracks) == 0: + return None + track_info = tracks[0] return track_info["name"], track_info["artist"] @@ -42,11 +46,16 @@ def _lastfm_getinfo(name: str, artist: str) -> tuple[str, list[str]]: # ( image track_info = response.json()["track"] - image_url = urllib.parse.urlparse(track_info["album"]["image"][0]["#text"]) + image_id = "" + if "album" in track_info: + image_url = urllib.parse.urlparse(track_info["album"]["image"][0]["#text"]) + image_id = os.path.splitext(os.path.basename(image_url.path))[0] + else: + print("this song haas no image", flush=True) return ( # track_info["mbid"], - os.path.splitext(os.path.basename(image_url.path))[0], + image_id, [t["name"] for t in track_info["toptags"]["tag"]], ) @@ -68,14 +77,17 @@ def _yt_search(query: str) -> tuple[str, str]: def query_search(query: str) -> SongInfo | None: - name, artist = _lastfm_search(query) + res = _lastfm_search(query) + if res is None: + return None + name, artist = res img_id, tags = _lastfm_getinfo(name, artist) return SongInfo(artist=artist, title=name, img_id=img_id, tags=tags) -def yt_search_song(name: str, artist: str) -> str: # video id +def yt_search_song(name: str, artist: str) -> str | None: # video id ydl_opts = { "format": "bestaudio", "default_search": "ytsearch1", @@ -86,6 +98,9 @@ def yt_search_song(name: str, artist: str) -> str: # video id with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(f"{name!r} - {artist!r}", download=False) + if len(info["entries"]) == 0: + return None + return info["entries"][0]["id"] diff --git a/backend/src/state.py b/backend/src/state.py index e1f3e7c..73d05d2 100644 --- a/backend/src/state.py +++ b/backend/src/state.py @@ -4,7 +4,7 @@ from sqlite3 import Cursor from flask import Flask from flask_socketio import SocketIO -from .room import Room +from room import Room @dataclass diff --git a/frontend/src/app.html b/frontend/src/app.html index 7af2f6d..0e6ef01 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -1,15 +1,13 @@ - - - - - - %sveltekit.head% - - - -
%sveltekit.body%
- - + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ diff --git a/frontend/src/lib/components/QueueSlider.svelte b/frontend/src/lib/components/QueueSlider.svelte index ec5a647..5df1e8d 100644 --- a/frontend/src/lib/components/QueueSlider.svelte +++ b/frontend/src/lib/components/QueueSlider.svelte @@ -8,6 +8,10 @@ queueSongs[playingIndex], playingIndex == queueSongs.length - 1 ? createEmptySong() : queueSongs[playingIndex + 1], ]) + + $effect(() => { + console.log(displaySongs) + })
@@ -19,20 +23,22 @@ class={`flex h-[60vw] max-h-[250px] w-[60vw] max-w-[250px] items-center justify-center ${i === 1 ? "spin-slower rounded-full border-2 border-black" : "rounded"} object-cover`} > {#if i === 1} -
+
{/if} - Song cover + Song cover
{#if i === 1} -

{song.title} - {song.artist}

+

{song.title} - {song.artist}

{/if} {:else} -
- {#if i === 1} -

No song in queue

- {/if} -
+
{/if} {/each} diff --git a/frontend/src/lib/components/RoomComponent.svelte b/frontend/src/lib/components/RoomComponent.svelte index e98e000..0192683 100644 --- a/frontend/src/lib/components/RoomComponent.svelte +++ b/frontend/src/lib/components/RoomComponent.svelte @@ -1,18 +1,45 @@ - + + {#if showPinModal} + + + {/if} + diff --git a/frontend/src/lib/components/SuggestionInput.svelte b/frontend/src/lib/components/SuggestionInput.svelte index 09982fd..d19dd17 100644 --- a/frontend/src/lib/components/SuggestionInput.svelte +++ b/frontend/src/lib/components/SuggestionInput.svelte @@ -1,19 +1,69 @@ -
- +
+ { + errorMsg = null + if (e.key == "Enter") { + sendSong() + } + }} + disabled={loading} + /> + {#if loading} + + + + {/if} +
+ +

+ {errorMsg} +

diff --git a/frontend/src/lib/components/SuggestionList.svelte b/frontend/src/lib/components/SuggestionList.svelte index 285d242..6c667a3 100644 --- a/frontend/src/lib/components/SuggestionList.svelte +++ b/frontend/src/lib/components/SuggestionList.svelte @@ -1,41 +1,62 @@
{#if suggestions.length == 0} -

No suggestions yet! Try to add a new one using the Add button

+

No suggestions yet! Try to add a new one using the Add button

{/if} - {#each suggestions as sug, idx} -
-
- Song cover -
-

{sug.title}

+ {#each suggestions as sug} +
+
+ Song cover +
+ {sug.title}

{sug.artist}

{ + await vote(1, sug.uuid) + }}> -

{sug.upvote}

+

{sug.upvote}

{ + await vote(-1, sug.uuid) + }}>
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 0c2e8f5..87182fc 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -10,19 +10,19 @@ const SongSchema = z.object({ }) export type Song = z.infer -export const parseSong = async function(song: any): Promise { +export const parseSong = async function (song: any): Promise { let resp = await SongSchema.parseAsync(song) return resp } -export const createEmptySong = function(): Song { +export const createEmptySong = function (): Song { return { uuid: "-1", title: "", artist: "", tags: [""], image_id: "", - youtube_id: 0, + youtube_id: "", } } @@ -31,21 +31,22 @@ const SuggestionSchema = SongSchema.extend({ }) export type Suggestion = z.infer -export const parseSuggestion = async function(sugg: any): Promise { +export const parseSuggestion = async function (sugg: any): Promise { let resp = await SuggestionSchema.parseAsync(sugg) return resp } const RoomSchema = z.object({ - id: z.string(), + id: z.number(), name: z.string(), private: z.boolean(), - coords: z.tuple([z.number(), z.number()]), - range: z.number().int() + coords: z.object({ latitude: z.number(), longitude: z.number() }), + range: z.number().int(), + distance: z.number() }) export type Room = z.infer -export const parseRoom = async function(room: any): Promise { +export const parseRoom = async function (room: any): Promise { let resp = await RoomSchema.parseAsync(room) return resp } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 1b806c5..470da85 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,9 +1,10 @@ +import { type Coordinates } from "./gps" import { parseSong, parseSuggestion, type FetchError, type Song, type Suggestion } from "./types" -export const joinRoom = async function (roomId: string): Promise<[FetchError | null, string]> { - let resp = await fetch("/api/join?room=" + roomId) +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 (resp.status != 200) { + if (res.status != 200) { return [{ code: 400, message: "Cannot join the room" }, ""] } diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index e4c699e..0ab9b2b 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -4,6 +4,6 @@ let { children } = $props() -
+
{@render children()}
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 319ce8d..b0b35bf 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,9 +1,20 @@
@@ -12,46 +23,22 @@ ChillBox
- radar + radar Scanning for rooms near you... -
- - - - - - - - - - - - + {#each rooms as room} + + {/each}
- - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/routes/admin/[id]/+page.svelte b/frontend/src/routes/admin/[id]/+page.svelte index e017cc5..02f5f08 100644 --- a/frontend/src/routes/admin/[id]/+page.svelte +++ b/frontend/src/routes/admin/[id]/+page.svelte @@ -15,14 +15,37 @@ let currentPlaying = $derived(queueSongs[playingIndex]) let audioController = $state() + let playerInfo = $state({ + playing: false, + currentTime: 0, + duration: 0, + }) + async function playOnEnd() { let url = await getStreamingUrl(currentPlaying.uuid) if (audioController) { audioController.src = url - audioController.play() + await audioController.play() } } + $effect(() => { + if (audioController) { + audioController.ontimeupdate = () => { + playerInfo.currentTime = audioController?.currentTime || 0 + } + audioController.onloadedmetadata = () => { + playerInfo.duration = audioController?.duration || 0 + } + audioController.onplay = () => { + playerInfo.playing = true + } + audioController.onpause = () => { + playerInfo.playing = false + } + } + }) + onMount(async () => { let songs, index ;[returnError, songs, index] = await getQueueSongs(data.roomId) @@ -30,12 +53,21 @@ queueSongs = songs playingIndex = index - if (audioController) { - audioController.src = await getStreamingUrl(currentPlaying.uuid) - audioController.play() + if (queueSongs.length == 0) { + playNext() } }) + $effect(() => { + playOnEnd() + }) + + const formatTime = (t: number) => { + const min = Math.floor(t / 60) + const sec = Math.floor(t % 60) + return `${min}:${sec.toString().padStart(2, "0")}` + } + async function playNext() { let songs, index ;[returnError, songs, index] = await triggerPlayNext(data.roomId) @@ -44,6 +76,20 @@ queueSongs = songs playingIndex = index + + if (audioController) { + audioController.pause() + audioController.currentTime = 0 + } + } + + function seek(e: Event) { + const target = e.target as HTMLInputElement + const seekTime = parseFloat(target.value) + playerInfo.currentTime = seekTime + if (audioController) { + audioController.currentTime = seekTime + } } @@ -52,7 +98,21 @@ {:else}
- - + + + +
+

{formatTime(playerInfo.currentTime)} - {formatTime(playerInfo.duration)}

+ +
+ + +
+
+ +
{/if} diff --git a/frontend/src/routes/room/[id]/+page.svelte b/frontend/src/routes/room/[id]/+page.svelte index e53cb58..84d8d67 100644 --- a/frontend/src/routes/room/[id]/+page.svelte +++ b/frontend/src/routes/room/[id]/+page.svelte @@ -8,6 +8,7 @@ import { getQueueSongs, getSuggestions, joinRoom } from "$lib/utils.js" import type { FetchError } from "$lib/types.js" import { io, Socket } from "socket.io-client" + import { get_coords } from "$lib/gps.js" let { data } = $props() @@ -23,8 +24,14 @@ onMount(async () => { // Join the room + let { coords, error } = await get_coords() + if (error || coords == null) { + // Default to Lido + coords = { latitude: 46.6769043, longitude: 11.1851585 } + } + let sugg, queue, index - ;[returnError] = await joinRoom(data.roomId) + ;[returnError] = await joinRoom(data.roomId, coords, data.pin) if (returnError) { return } @@ -69,7 +76,7 @@ {#if returnError} {:else} -
+
diff --git a/frontend/src/routes/room/[id]/+page.ts b/frontend/src/routes/room/[id]/+page.ts index 969ffc5..f3bea98 100644 --- a/frontend/src/routes/room/[id]/+page.ts +++ b/frontend/src/routes/room/[id]/+page.ts @@ -1,13 +1,8 @@ -import { error, type Load, type ServerLoad } from "@sveltejs/kit" import type { PageLoad } from "./$types" -import type { FetchError } from "$lib/util" -import { parseSuggestion, type Suggestion } from "$lib/types" -export const load: PageLoad = async ({ params }) => { - if (params.id) { - return { - roomId: params.id, - } +export const load: PageLoad = ({ params, url }) => { + return { + roomId: params.id || "", + pin: url.searchParams.get("pin") || "", } - error(400, "Please provide a room id") } diff --git a/frontend/src/routes/room/create/+page.svelte b/frontend/src/routes/room/create/+page.svelte new file mode 100644 index 0000000..d0009f3 --- /dev/null +++ b/frontend/src/routes/room/create/+page.svelte @@ -0,0 +1,83 @@ + + +
+

Create Room

+ +
+ + + + +

+ Room Coordinates: + {coord.latitude},{coord.longitude} +

+ + + + +
+
diff --git a/frontend/static/android-chrome-192x192.png b/frontend/static/android-chrome-192x192.png new file mode 100644 index 0000000..ddc7955 Binary files /dev/null and b/frontend/static/android-chrome-192x192.png differ diff --git a/frontend/static/android-chrome-512x512.png b/frontend/static/android-chrome-512x512.png new file mode 100644 index 0000000..71ec4aa Binary files /dev/null and b/frontend/static/android-chrome-512x512.png differ diff --git a/frontend/static/apple-touch-icon.png b/frontend/static/apple-touch-icon.png new file mode 100644 index 0000000..a60445a Binary files /dev/null and b/frontend/static/apple-touch-icon.png differ diff --git a/frontend/static/favicon-16x16.png b/frontend/static/favicon-16x16.png new file mode 100644 index 0000000..6bb39f3 Binary files /dev/null and b/frontend/static/favicon-16x16.png differ diff --git a/frontend/static/favicon-32x32.png b/frontend/static/favicon-32x32.png new file mode 100644 index 0000000..7d938c2 Binary files /dev/null and b/frontend/static/favicon-32x32.png differ diff --git a/frontend/static/favicon.ico b/frontend/static/favicon.ico new file mode 100644 index 0000000..f5208bb Binary files /dev/null and b/frontend/static/favicon.ico differ diff --git a/frontend/static/favicon.svg b/frontend/static/favicon.svg deleted file mode 100644 index cc5dc66..0000000 --- a/frontend/static/favicon.svg +++ /dev/null @@ -1 +0,0 @@ -svelte-logo \ No newline at end of file diff --git a/frontend/static/manifest.json b/frontend/static/manifest.json index f042a95..8a414d3 100644 --- a/frontend/static/manifest.json +++ b/frontend/static/manifest.json @@ -1,21 +1,21 @@ { - "name": "Chillbox Music Player", - "short_name": "Chillbox", - "start_url": "/", - "display": "standalone", - "background_color": "#334155", - "theme_color": "#334155", - "orientation": "portrait-primary", - "icons": [ - { - "src": "/icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "/icon-192.png", - "sizes": "192x192", - "type": "image/png" - } - ] + "name": "Chillbox Music Player", + "short_name": "Chillbox", + "start_url": "/", + "display": "standalone", + "background_color": "#334155", + "theme_color": "#334155", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + } + ] } diff --git a/frontend/static/smerdoradar.gif b/frontend/static/radar.gif similarity index 100% rename from frontend/static/smerdoradar.gif rename to frontend/static/radar.gif diff --git a/frontend/static/smerdo_radar_bonus.gif b/frontend/static/radar_bonus.gif similarity index 100% rename from frontend/static/smerdo_radar_bonus.gif rename to frontend/static/radar_bonus.gif