Skip to content

Commit

Permalink
feat/room-queue (#47)
Browse files Browse the repository at this point in the history
* backend : endpoint pour obtenir la liste d'attente d'une salle d'écoute

* fonctionnalité (presque) terminée

- manque commentaire
- nettoyage

* refactor : convert snake_case to camelCase

* refactor(backend) : reformat files

* feat(frontend) : create environment variable for store the endpoint of backend
feat(frontend) : musicQueue can be customized with id as parameters (url path : /musicQueue/[id] in place of /musicQueue with id hard coded)

* fix : fix type in JSONTrack (backend and frontend)
- primitive type only
- good type for data stored

* refactor(backend) : remove node-fetch library, now use Fetch API embedded (experimental) in node.js

* refactor(backend) : replace spotify api call with fetch with spotify endpoint by spotify api package

* chore(backend) : for future, allow to have multiple music platform using new class which extends MusicPlatform, and modify factory as in consequence

* refactor(backend) : replace spotify api call with fetch with spotify endpoint by spotify api package

* refactor : refactoring with rule of project : 2 spaces for indents

* refactor : full rename "queue" by "room" in all files

* fix(frontend) : fix active room display on react-native (tested on Android with Expo Go)

* style(frontend) : add TextInput and Button components which permit to route to an active room whose id come from the text input

* refactor : change event name of socket.io in client (frontend) and server (backend) to better blend in app

* style(frontend) : import external font from file in app

* style(frontend) : adapt room page to wireframe design and style

* chore(frontend) : exclude android and ios compilation folder

* chore(frontend) : improve and keep only .env.exemple

* refactor(frontend) : light clean package.json

* fix : delete duplicate files and imports after rebase

* fix(backend) : delete duplicate lines which enabled cors

* fix(backend) : fix routes

* refactor : execute prettier

* refactor : execute prettier after rebase

* refactor : after npm install

* fix(frontend) : make work app after merging code of main and code of this feature

* fix(common) : resolve types file of database

* fix(frontend) : delete duplicate file

* refactor : place JSONTrack interface in commons folder to share the same file between front and back

* refactor(backend) : eslint fix

* refactor : suppress "Function type" warning

* fixup! refactor(backend) : eslint fix

* refactor : reset not my file as main

* docs(frontend) : ide detect problem on this line

* fix(frontend) : remove duplicate line

* refactor : put RoomJSON interface in commons folder/package

* refactor : rename EXPO_PUBLIC_API_ENDPOINT (my name) to EXPO_PUBLIC_BACKEND_API (equivalent name from main branch)

* refactor : review thomas : delete unused View component in room tab

* refactor : thomas's review : rename MusicStorage by RoomStorage everywhere

* refactor : thomas's review : rename Track to TrackMetadata, make constructor private and replace this by static method newTrackMetadata which may return TrackMetadata (previously Track) or Error objects if parameters are invalid (only formal not in their API). Transform of toJSON static method which call music platform with surround of try keyword to avoid to stop program cause by error in external JS API.

* refactor : use rewritten base component for this project in my code

* refactor : clean useless comments through bobby review

* refactor : put every component from expo\app\(tabs)\rooms\[id].tsx in separated files through bobby review

* refactor(backend) : clean useless comments through bobby review

* docs(backend) : clean useless comments and add useful other

* refactor(backend) : extract method which get how many capturing group in a regex

* refactor(backend) : delete useless attributes and methods in MusicPlatform : they have permitted to store all MusicPlatform subclass.
With TrackFactory is now unused

* feat(frontend) : make my components compliant with wireframe

* feat(backend) : refactor room.ts and implement remove a track from a room (with track url or index in room).
TrackFactory is created only when a room is created, not every track is added like before

* feat(backend) : implement remove track from room feature in socket io : event queue:remove and queue:removeLink

* refactor(backend) : set property which contain artist in JSONTrack to access all artists, not only main artist of track.
All artist names are joined with comma separator to object of string type

* fix(backend) : ignore string which is not URL when it tries to build Track object in TrackFactory.ts

* refactor(backend) : Maxence's review : remove hand-defined type

* refactor(backend) : Maxence's review : add space after comme between two artists name

* refactor(backend) : Maxence's review : get image with minimal size  but with width greater of 46 if possible

* refactor(backend) : Maxence's review : remove comment disabling eslint analyse of one case

* refactor(backend) : Maxence's review : split Track object builder with URL and id : new TrackMetadata is renamed to createFromURL and take only URL object as source and for id, you must use constructor which has a public visibility

* refactor(backend) : Maxence's review : rename tracks by queue

* refactor(frontend) : Maxence's review : use already coded function to get backend url

* refactor(frontend) : Maxence's review : rename tracks by queue

* refactor(frontend) : Maxence's review : remove Stack.Screen settings of room page

* refactor(frontend) : Maxence's review : define socket io endpoint to backend through already coded function

* fix(frontend) : remove problem change after rebase

* fix(frontend) : merge noam and me work

* fix(frontend) : catch error of spotify API in Spotify class

* refactor(frontend) : remove useless comment

* fix(backend) : fix forgotten route from main

* fix(backend) : remove little code change between main and here

* fix(backend) : I follow instruction in docs of fastify return reply if async function is used

* fix(frontend) : adjust room create feature

* fix(backend) : delete room mock creator and send to frontend uuid of room when this is created with success

* refactor(backend) : make beautify import

* feet(backend) : create new static method to get room from storage with uuid and code, these use database to check if this exists

* feet(frontend) : allow to add spotify track url to a queue of a room

* refactor: remove gadget differences with the main branch

* fix: kept wrong types during rebase

* fix: removed hard coded port for API url

* refactor(frontend): using CustomTextInput component instead of TextInput from react

* fix: using router.back instead of pushing ../

* fix: using rooms instead of active_rooms

* refactor(backend): stopped using onAny

* refactor(backend): renamed variable to a more precise intent

* style(frontend): fixed slight overflow in tabs height causing vertical scrollbars

* refactor(frontend): using custom components

* refactor(frontend): using components properly

* feat(ui): added style prop to button in case of necessary additional styles

---------

Co-authored-by: MAXOUXAX <[email protected]>
  • Loading branch information
drmolixcool and MAXOUXAX authored Jan 28, 2024
1 parent 2a462a6 commit 5aa4455
Show file tree
Hide file tree
Showing 33 changed files with 4,473 additions and 6,243 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ yarn-error.*
# local env files
.env*.local

# expo env files
.env

# typescript
*.tsbuildinfo

Expand Down
1,293 changes: 446 additions & 847 deletions backend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@fastify/rate-limit": "^9.1.0",
"@supabase/auth-helpers-nextjs": "^0.8.7",
"@supabase/ssr": "^0.0.10",
"@spotify/web-api-ts-sdk": "^1.1.2",
"@supabase/supabase-js": "^2.39.3",
"dotenv": "^16.3.2",
"fastify": "^4.25.2",
Expand Down
64 changes: 64 additions & 0 deletions backend/src/RoomStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Room from "./room";
import { adminSupabase } from "./server";

export default class RoomStorage {
private static singleton: RoomStorage;
private readonly data: Map<string, Room>;

private constructor() {
this.data = new Map();
}

static getRoomStorage(): RoomStorage {
if (this.singleton === undefined) {
this.singleton = new RoomStorage();
}
return this.singleton;
}

async roomFromUuid(rawUuid: string): Promise<Room | null> {
const { data: remoteRoom } = await adminSupabase
.from("rooms")
.select("*")
.eq("id", rawUuid)
.eq("is_active", true)
.single();

if (remoteRoom === null) {
return null;
}

return Room.getOrCreate(this, remoteRoom.id);
}

async roomFromCode(code: string): Promise<Room | null> {
const { data: remoteRoom } = await adminSupabase
.from("rooms")
.select("*")
.eq("code", code)
.eq("is_active", true)
.single();

if (remoteRoom === null) {
return null;
}

return Room.getOrCreate(this, remoteRoom.id);
}

addRoom(room: Room) {
this.data.set(room.uuid, room);
}

removeRoomByUuid(uuid: string) {
this.data.delete(uuid);
}

removeRoom(room: Room) {
this.data.delete(room.uuid);
}

getRoom(activeRoomId: string): Room | null {
return this.data.get(activeRoomId) ?? null;
}
}
36 changes: 36 additions & 0 deletions backend/src/musicplatform/MusicPlatform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { JSONTrack } from "commons/Backend-types";

export default abstract class MusicPlatform {
private readonly urlPattern: RegExp;

protected constructor(urlPattern: RegExp) {
this.urlPattern = new RegExp(urlPattern, "i");
// check if urlPattern have only one capture group (this must capture the id of a track)
const resultRegex = getNbCapturingGroupRegex(this.urlPattern);
if (resultRegex !== 1) {
// TODO remove throw and make like TrackFactory (think to watch history of this file...)
throw new Error(
"il y a plusieurs groupe de capture\n" +
"seul un groupe doit se trouver dans le pattern, et doit retourner l'identifiant unique de l'élément"
);
}
}

trackIdFromUrl(url: URL): string | undefined {
return this.getUrlPattern().exec(url.href)?.slice(1)[0];
}

getUrlPattern(): RegExp {
return new RegExp(this.urlPattern);
}

getClass() {
return this.constructor;
}

abstract getJsonTrack(id: string): Promise<JSONTrack | null>;
}

function getNbCapturingGroupRegex(regex: RegExp) {
return new RegExp(regex.toString() + "|").exec("")?.slice(1).length;
}
35 changes: 35 additions & 0 deletions backend/src/musicplatform/Spotify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import MusicPlatform from "./MusicPlatform";
import { spotify } from "../server";
import { JSONTrack } from "commons/Backend-types";
export default class Spotify extends MusicPlatform {
constructor() {
super(
/^https?:\/\/open\.spotify\.com\/(?:.*\/)?track\/([a-zA-Z0-9]*)(?:\?.*)?$/i
);
}

async getJsonTrack(id: string): Promise<JSONTrack | null> {
let data;
try {
data = await spotify.tracks.get(id);
} catch {
return null;
}

const image = data.album.images.reduce((acc, current) => {
return current.width < acc.width && current.width >= 46 ? current : acc;
});

return {
url: new URL(data.external_urls.spotify).toString(),
title: data.name,
duration: data.duration_ms,
artistsName: data.artists.reduce(
(acc, current) => (acc ? `${acc}, ` : "") + current.name,
""
),
albumName: data.album.name,
imgUrl: new URL(image.url).toString(),
};
}
}
36 changes: 36 additions & 0 deletions backend/src/musicplatform/TrackFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import TrackMetadata from "./TrackMetadata";
import MusicPlatform from "./MusicPlatform";

export default class TrackFactory {
private readonly musicPlatformsList: Map<Function, MusicPlatform>;

constructor() {
this.musicPlatformsList = new Map<Function, MusicPlatform>();
}

register(newPlatform: MusicPlatform): boolean {
if (
Array.from(this.musicPlatformsList.keys()).includes(
newPlatform.getClass()
)
) {
return false;
}
this.musicPlatformsList.set(newPlatform.getClass(), newPlatform);
return true;
}

fromUrl(rawUrl: string | URL): TrackMetadata | null {
if (!URL.canParse(rawUrl)) {
return null;
}
const url = new URL(rawUrl);
for (const musicPlatform of this.musicPlatformsList.values()) {
const trackId = musicPlatform.trackIdFromUrl(url);
if (trackId) {
return new TrackMetadata(musicPlatform, trackId);
}
}
return null;
}
}
29 changes: 29 additions & 0 deletions backend/src/musicplatform/TrackMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import MusicPlatform from "./MusicPlatform";
import { JSONTrack } from "commons/Backend-types";

export default class TrackMetadata {
private readonly platform: MusicPlatform;
private readonly id: string;

public constructor(platform: MusicPlatform, id: string) {
this.platform = platform;
this.id = id;
}

static createFromURL(
platform: MusicPlatform,
url: URL
): TrackMetadata | null {
const id = platform.trackIdFromUrl(url);

if (!id) {
return null;
}

return new TrackMetadata(platform, id);
}

async toJSON(): Promise<JSONTrack | null> {
return await this.platform.getJsonTrack(this.id);
}
}
91 changes: 91 additions & 0 deletions backend/src/room.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { adminSupabase } from "./server";
import createClient from "./lib/supabase";
import Spotify from "./musicplatform/Spotify";
import TrackFactory from "./musicplatform/TrackFactory";
import { JSONTrack, RoomJSON } from "commons/backend-types";
import RoomStorage from "./RoomStorage";

interface Error {
error: { message: string };
}

export async function getUserFromRequest(
request: FastifyRequest,
Expand Down Expand Up @@ -104,3 +112,86 @@ export function endRoom(roomId: string) {
// TODO: Properly end room
// This will set the join code of this room to null, and set is_active to false
}

export default class Room {
public readonly uuid: string;
private readonly queue: JSONTrack[];
private readonly trackFactory: TrackFactory;

private constructor(uuid: string /*...platforms*/) {
this.uuid = uuid;
this.queue = [];

this.trackFactory = new TrackFactory();
this.trackFactory.register(
new Spotify()
) /*, new SoundCloud(), new AppleMusic()*/;
}

static getOrCreate(roomStorage: RoomStorage, uuid: string): Room {
let room = roomStorage.getRoom(uuid);

if (room === null) {
room = new Room(uuid);
roomStorage.addRoom(room);
}

return room;
}

static toJSON(room: Room | null | undefined): RoomJSON | Error {
if (room instanceof Room) {
return { currentActiveRoom: room.uuid, tracks: room.getQueue() };
} else {
return { error: { message: "the given id is not active room" } };
}
}

async add(rawUrl: string) {
const trackMetadata = this.trackFactory.fromUrl(rawUrl);
if (trackMetadata !== null) {
const track = await trackMetadata.toJSON();
if (track !== null) {
if (!this.queue.map((value) => value.url).includes(track.url)) {
this.queue.push(track);
return true;
}
}
}
return false;
}

async removeWithLink(rawUrl: string) {
// try to get the uniform URL of track from lambda url
let trackURL = null;
const trackMetadata = this.trackFactory.fromUrl(rawUrl);
if (trackMetadata !== null) {
const track = await trackMetadata.toJSON();
if (track !== null) {
trackURL = new URL(track.url).toString();
}
}

let track;
for (track of this.queue) {
if (trackURL !== null) {
if (track.url === trackURL) {
return await this.removeWithIndex(this.queue.indexOf(track));
}
}
}
return false;
}

async removeWithIndex(index: number) {
return this.queue.splice(index, 1).length !== 0;
}

getQueue(): JSONTrack[] {
return [...this.queue];
}

size(): number {
return this.queue.length;
}
}
2 changes: 1 addition & 1 deletion backend/src/route/AuthCallbackGET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default async function AuthCallbackGET(
const redirectUrl = decodeURIComponent(request.url).split("redirect_url=")[1];

// redirect user to the redirect url with the refresh token
response.redirect(
return response.redirect(
redirectUrl + "#refresh_token=" + encodeURIComponent(refresh_token)
);
}
Expand Down
22 changes: 22 additions & 0 deletions backend/src/route/RoomIdGET.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FastifyReply, FastifyRequest } from "fastify";
import Room from "../room";
import RoomStorage from "../RoomStorage";

export interface QueryParams {
id: string;
}

export default async function RoomIdGET(
req: FastifyRequest,
reply: FastifyReply
) {
const { id: activeRoomId } = req.params as QueryParams;

const roomStorage = RoomStorage.getRoomStorage();

const room = roomStorage.getRoom(activeRoomId);
if (room === null) {
reply.code(404);
}
return reply.send(Room.toJSON(room));
}
7 changes: 5 additions & 2 deletions backend/src/route/RoomPOST.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { createRoom } from "../room";
import * as repl from "repl";

interface BodyParams {
name: string;
Expand Down Expand Up @@ -33,7 +34,7 @@ function extractFromRequest(req: FastifyRequest): BodyParams {

export default async function RoomPOST(
req: FastifyRequest,
reply: FastifyReply
reply: FastifyReply,
) {
const roomOptions = extractFromRequest(req);

Expand All @@ -46,6 +47,8 @@ export default async function RoomPOST(
roomOptions.maxMusicDuration,
roomOptions.service,
req,
reply
reply,
);

return reply;
}
3 changes: 2 additions & 1 deletion backend/src/route/StreamingServicesGET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export default async function StreamingServicesGET(
error,
} = await adminSupabase.from("streaming_services").select("*");

reply.code(status).send(error ?? streamingServices);
// https://fastify.dev/docs/latest/Reference/Routes/#async-await
return reply.code(status).send(error ?? streamingServices);
}
Loading

0 comments on commit 5aa4455

Please sign in to comment.