├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── CONTRIBUTING.md
├── README.md
├── docker-compose.yml
├── next-app
├── .dockerignore
├── .env.example
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── Dockerfile
├── Dockerfile.dev
├── Dockerfile.prod
├── README.md
├── app
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ ├── spaces
│ │ │ └── route.ts
│ │ ├── streams
│ │ │ ├── downvote
│ │ │ │ └── route.ts
│ │ │ ├── empty-queue
│ │ │ │ └── route.ts
│ │ │ ├── my
│ │ │ │ └── route.ts
│ │ │ ├── next
│ │ │ │ └── route.ts
│ │ │ ├── remove
│ │ │ │ └── route.ts
│ │ │ ├── route.ts
│ │ │ └── upvote
│ │ │ │ └── route.ts
│ │ └── user
│ │ │ └── route.ts
│ ├── auth
│ │ └── page.tsx
│ ├── dashboard
│ │ └── [spaceId]
│ │ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── home
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── opengraph-image.png
│ ├── page.tsx
│ └── spaces
│ │ └── [spaceId]
│ │ └── page.tsx
├── components.json
├── components
│ ├── Appbar.tsx
│ ├── ErrorScreen.tsx
│ ├── HomeView.tsx
│ ├── LoadingScreen.tsx
│ ├── OldStreamView.tsx
│ ├── SpacesCard.tsx
│ ├── StreamView
│ │ ├── AddSongForm.tsx
│ │ ├── NowPlaying.tsx
│ │ ├── Queue.tsx
│ │ └── index.tsx
│ ├── ThemeSwitcher.tsx
│ ├── auth
│ │ ├── auth-screen.tsx
│ │ ├── sign-in-card.tsx
│ │ └── sign-up-card.tsx
│ ├── provider.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── cardSkeleton.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── separator.tsx
│ │ ├── skeleton.tsx
│ │ └── sonner.tsx
├── context
│ └── socket-context.tsx
├── docker-compose.yaml
├── hooks
│ └── useRedirect.ts
├── lib
│ ├── auth-options.ts
│ ├── db.ts
│ └── utils.ts
├── middleware.ts
├── next-env.d.ts
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── prisma
│ ├── migrations
│ │ ├── 20240828211233_init
│ │ │ └── migration.sql
│ │ ├── 20240828212629_added_ids
│ │ │ └── migration.sql
│ │ ├── 20240828214340_add_unique_constraint
│ │ │ └── migration.sql
│ │ ├── 20240828215837_add_video_metadata
│ │ │ └── migration.sql
│ │ ├── 20240829004903_added_current_stream
│ │ │ └── migration.sql
│ │ ├── 20240829011327_add_cascade
│ │ │ └── migration.sql
│ │ ├── 20240829012157_added_played_field
│ │ │ └── migration.sql
│ │ ├── 20240829012523_optional
│ │ │ └── migration.sql
│ │ ├── 20240829023538_added_user_guard
│ │ │ └── migration.sql
│ │ ├── 20240829030120_remove_added
│ │ │ └── migration.sql
│ │ ├── 20240830172435_add_added_by_to_stream
│ │ │ └── migration.sql
│ │ ├── 20240914062749_added_spaces
│ │ │ └── migration.sql
│ │ ├── 20240914070128_on_delete_cascade_for_spaces
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ └── schema.prisma
├── public
│ ├── google.png
│ ├── next.svg
│ ├── opengraph-image.png
│ └── vercel.svg
├── schema
│ └── credentials-schema.ts
├── tailwind.config.ts
├── tsconfig.json
└── types
│ ├── auth-types.ts
│ ├── index.d.ts
│ └── next-auth.d.ts
└── ws
├── .dockerignore
├── .env.example
├── .gitignore
├── Dockerfile
├── Dockerfile.dev
├── package.json
├── pnpm-lock.yaml
├── prisma
└── schema.prisma
├── src
├── StreamManager.ts
├── app.ts
└── utils.ts
├── tsconfig.json
└── tsconfig.tsbuildinfo
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build on PR
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 |
14 | - name: Use Node.js
15 | uses: actions/setup-node@v3
16 | with:
17 | node-version: "20"
18 |
19 | - name: Install pnpm
20 | run: npm install -g pnpm
21 |
22 | - name: Install Dependencies
23 | working-directory: next-app
24 | run: pnpm install
25 |
26 | - name: Generate Prisma client
27 | working-directory: next-app
28 | run: pnpm run postinstall
29 |
30 | - name: Run Build
31 | working-directory: next-app
32 | run: pnpm run build
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | */node_modules
2 | */.env
3 | */.next
4 | */dist
5 | */package-lock.json
6 | .DS_Store
7 | */.DS_Store.DS_Store
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for your interest in contributing to this repository. To ensure a smooth and collaborative environment, please follow these guidelines. Before contributing, set up the project locally using the steps outlined in [README.md](./README.md).
4 |
5 | ## Why these guidelines ?
6 |
7 | Our goal is to create a healthy and inclusive space for contributions. Remember that open-source contribution is a collaborative effort, not a competition.
8 |
9 | ## General guidelines
10 |
11 | - Work only on one issue at a time since it will provide an opportunity for others to contribute as well.
12 |
13 | - Note that each open-source repository generally has its own guidelines, similar to these. Always read them before starting your contributions.
14 |
15 | ## How to get an issue assigned
16 |
17 | - To get an issue assigned, provide a small description as to how you are planning to tackle this issue.
18 |
19 | > Ex - If the issue is about UI changes, you should create a design showing how you want it to look on the UI (make it using figma, paint, etc)
20 |
21 | - This will allow multiple contributors to discuss their approach to tackle the issue. The maintainer will then assign the issue.
22 |
23 | ## After getting the issue assigned
24 |
25 | - Create your own branch instead of working directly on the main branch.
26 |
27 | - Provide feedback every 24-48 hours if an issue is assigned to you. Otherwise, it may be reassigned.
28 |
29 | - When submitting a pull request, please provide a screenshot or a screen-recording showcasing your work.
30 |
31 | ## Don't while contributing
32 |
33 | - Avoid comments like "Please assign this issue to me" or "can i work on this issue ?"
34 |
35 | - Refrain from tagging the maintainer to assign issues or review pull requests.
36 |
37 | - Don't make any pull request for issues you are not assigned to. It will be closed without merging.
38 |
39 | Happy Contributing!
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Muzer - 100xDevs
2 |
3 | ## Table of Contents
4 |
5 | - [Table of Contents](#table-of-contents)
6 | - [Installation](#installation)
7 | - [With Docker](#with-docker)
8 | - [Without Docker](#without-docker)
9 | - [Usage](#usage)
10 | - [Contributing](#contributing)
11 | - [Contributors](#contributors)
12 |
13 | ## Installation
14 |
15 | ### With Docker
16 |
17 | 1. Clone the repository:
18 | ```bash
19 | git clone https://github.com/code100x/muzer.git
20 | ```
21 |
22 | 2. Navigate to the project directory:
23 | ```bash
24 | cd muzer
25 | ```
26 |
27 | 3. Create a `.env` file based on the `.env.example` file and configure everything in both the `next-app` and `ws` folders.
28 |
29 | 4. Run the following command to start the application:
30 | ```bash
31 | docker compose up -d
32 | ```
33 |
34 | ### Without Docker
35 |
36 | 1. Clone the repository:
37 | ```bash
38 | git clone https://github.com/code100x/muzer.git
39 | ```
40 |
41 | 2. Navigate to the project directory:
42 | ```bash
43 | cd muzer
44 | ```
45 |
46 | 3. Now Install the dependencies:
47 | ```bash
48 | cd next-app
49 | pnpm install
50 | cd ..
51 | cd ws
52 | pnpm install
53 | ```
54 | 4. Create a `.env` file based on the `.env.example` file and configure everything in both the `next-app` and `ws` folders.
55 |
56 | 5. For postgres, you need to run the following command:
57 | ```bash
58 | docker run -d \
59 | --name muzer-db \
60 | -e POSTGRES_USER=myuser \
61 | -e POSTGRES_PASSWORD=mypassword \
62 | -e POSTGRES_DB=mydatabase \
63 | -p 5432:5432 \
64 | postgres
65 | ```
66 |
67 | 6. For redis, you need to run the following command:
68 | ```bash
69 | docker run -d \
70 | --name muzer-redis \
71 | -e REDIS_USERNAME=admin \
72 | -e REDIS_PASSWORD=root \
73 | -e REDIS_PORT=6379 \
74 | -e REDIS_HOST="127.0.0.1" \
75 | -e REDIS_BROWSER_STACK_PORT=8001 \
76 | redis/redis-stack:latest
77 | ```
78 |
79 | 7. Now do the following:
80 | ```bash
81 | cd next-app
82 | pnpm postinstall
83 | cd ..
84 | cd ws
85 | pnpm postinstall
86 | ```
87 |
88 | 8. Run the following command to start the application:
89 | ```bash
90 | cd next-app
91 | pnpm dev
92 | cd ..
93 | cd ws
94 | pnpm dev
95 | ```
96 |
97 | 9. To access the prisma studio, run the following command:
98 | ```bash
99 | cd next-app
100 | pnpm run prisma:studio
101 | ```
102 |
103 | ## Usage
104 |
105 | 1. Access the application in your browser at http://localhost:3000
106 | 2. Access the redis stack at http://localhost:8001/redis-stack/browser
107 | 3. Access the prisma studio at http://localhost:5555
108 |
109 | ## Contributing
110 |
111 | We welcome contributions from the community! To contribute to Muzer, follow these steps:
112 |
113 | 1. Fork the repository.
114 |
115 | 2. Create a new branch (`git checkout -b feature/fooBar`).
116 |
117 | 3. Make your changes and commit them (`git commit -am 'Add some fooBar'`).
118 |
119 | 4. Push to the branch (`git push origin -u feature/fooBar`).
120 |
121 | 5. Create a new Pull Request.
122 |
123 | For major changes, please open an issue first to discuss what you would like to change.
124 |
125 | Read our [contribution guidelines](./CONTRIBUTING.md) for more details.
126 |
127 | ## Contributors
128 |
129 |
130 |
131 |
132 |
133 | If you continue to face issues, please open a GitHub issue with details about the problem you're experiencing.
134 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | app:
3 | container_name: muzer-docker
4 | build:
5 | context: ./next-app
6 | dockerfile: Dockerfile.dev
7 | env_file:
8 | - ./next-app/.env
9 | ports:
10 | - "3000:3000"
11 | - "5555:5555"
12 | depends_on:
13 | postgres:
14 | condition: service_healthy
15 | redis:
16 | condition: service_healthy
17 | volumes:
18 | - ./next-app:/usr/src/app
19 | - /usr/src/app/node_modules
20 |
21 | postgres:
22 | container_name: prisma-postgres
23 | image: postgres:alpine
24 | restart: always
25 | env_file:
26 | - ./next-app/.env
27 | ports:
28 | - "5432:5432"
29 | volumes:
30 | - postgres-data:/var/lib/postgresql/data
31 | healthcheck:
32 | test: ["CMD-SHELL", "pg_isready -U postgres"]
33 | interval: 10s
34 | timeout: 5s
35 | retries: 5
36 |
37 | redis:
38 | container_name: redis-server
39 | image: redis/redis-stack:latest
40 | restart: always
41 | env_file:
42 | - ./ws/.env
43 | ports:
44 | - "6379:6379"
45 | - "8001:8001"
46 | environment:
47 | REDIS_ARGS: "--requirepass root --user admin on >root ~* allcommands --user default off nopass nocommands"
48 | volumes:
49 | - redis-data:/data
50 | healthcheck:
51 | test: ["CMD", "redis-cli", "ping"]
52 | interval: 10s
53 | timeout: 5s
54 | retries: 5
55 |
56 | websockets:
57 | container_name: websockets
58 | restart: always
59 | build:
60 | context: ./ws
61 | dockerfile: Dockerfile.dev
62 | env_file:
63 | - ./ws/.env
64 | ports:
65 | - "8080:8080"
66 | depends_on:
67 | postgres:
68 | condition: service_healthy
69 | redis:
70 | condition: service_healthy
71 | volumes:
72 | - ./ws:/usr/src/app
73 | - /usr/src/app/node_modules
74 |
75 | volumes:
76 | postgres-data:
77 | external: false
78 | redis-data:
79 | external: false
--------------------------------------------------------------------------------
/next-app/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .git
3 | .gitignore
4 | .env.example
--------------------------------------------------------------------------------
/next-app/.env.example:
--------------------------------------------------------------------------------
1 | # Without Docker Local Setup
2 | GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID
3 | GOOGLE_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET
4 | NEXTAUTH_URL=http://localhost:3000
5 | NEXTAUTH_SECRET=YOUR_NEXTAUTH_SECRET
6 | DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres
7 | NEXT_PUBLIC_SECRET=YOUR_NEXTAUTH_SECRET
8 | NEXT_PUBLIC_WSS_URL="ws://localhost:8080"
9 |
10 | # With Docker
11 |
12 | # APP
13 | APP_PORT=3000
14 | GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID
15 | GOOGLE_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET
16 | NEXTAUTH_SECRET="YOUR_NEXTAUTH_SECRET"
17 | NEXT_PUBLIC_SECRET="YOUR_NEXTAUTH_SECRET"
18 | NEXT_PUBLIC_WSS_URL="ws://localhost:3000"
19 | NEXT_PUBLIC_PUBLICKEY="YOUR_PUBLIC_KEY"
20 | NEXT_PUBLIC_SOL_PER_PAYMENT="YOUR_SOL_PER_PAYMENT"
21 |
22 | # Postgres DB
23 | DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres
24 | POSTGRES_PORT=5432
25 |
26 | # Prisma-Studio
27 | POSTGRES_URL=postgresql://postgres:postgres@postgres:5432/postgres
28 | POSTGRES_HOST=postgres
29 | POSTGRES_USERNAME=postgres
30 | POSTGRES_PASSWORD=postgres
--------------------------------------------------------------------------------
/next-app/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "react-hooks/exhaustive-deps": "off"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/next-app/.gitignore:
--------------------------------------------------------------------------------
1 | .next
2 | node_modules
3 | dist
4 | .env
5 |
--------------------------------------------------------------------------------
/next-app/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["prettier-plugin-tailwindcss"]
3 | }
4 |
--------------------------------------------------------------------------------
/next-app/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY package.json .
6 |
7 | RUN npm install pnpm -g --ignore-scripts
8 |
9 | RUN pnpm install --ignore-scripts
10 |
11 | COPY . .
12 |
13 | RUN DATABASE_URL=$DATABASE_URL npx prisma generate
14 | RUN DATABASE_URL=$DATABASE_URL pnpm run build
15 |
16 | EXPOSE 3000
17 |
18 | CMD ["pnpm", "run", "start"]
19 |
--------------------------------------------------------------------------------
/next-app/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | # Stage 1: Install dependencies
2 | FROM node:22-alpine AS installer
3 |
4 | WORKDIR /usr/src/app
5 | RUN corepack enable pnpm
6 | COPY package.json pnpm-lock.yaml ./
7 | RUN pnpm install --frozen-lockfile --ignore-scripts
8 | COPY . .
9 | RUN pnpm postinstall
10 |
11 | # Stage 2: Development server
12 | FROM node:22-alpine AS dev
13 |
14 | WORKDIR /usr/src/app
15 | RUN corepack enable pnpm
16 | COPY --from=installer /usr/src/app/ ./
17 | CMD [ "pnpm", "dev:docker" ]
--------------------------------------------------------------------------------
/next-app/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | # Stage 1: Install dependencies
2 | FROM node:22-alpine AS installer
3 |
4 | WORKDIR /usr/src/app
5 | RUN apk add --no-cache libc6-compat
6 | COPY package.json pnpm-lock.yaml ./
7 | COPY prisma ./prisma
8 |
9 | RUN \
10 | if [ -f pnpm-lock.yaml ]; then \
11 | corepack enable pnpm && pnpm install --frozen-lockfile --ignore-scripts; \
12 | else \
13 | echo "pnpm-lock.yaml not found" && exit 1; \
14 | fi
15 |
16 | # Stage 2: Build stage
17 | FROM node:22-alpine AS builder
18 | WORKDIR /usr/src/app
19 |
20 | COPY --from=installer /usr/src/app/node_modules ./node_modules
21 | COPY --from=installer /usr/src/app/prisma ./prisma
22 |
23 | RUN DATABASE_URL=$DATABASE_URL npx prisma generate
24 |
25 | COPY . .
26 |
27 | RUN \
28 | if [ -f pnpm-lock.yaml ]; then \
29 | corepack enable pnpm && pnpm run build; \
30 | else \
31 | echo "pnpm-lock.yaml not found" && exit 1; \
32 | fi
33 |
34 | # Stage 3: Run stage
35 | FROM node:22-alpine AS runner
36 | WORKDIR /usr/src/app
37 |
38 | ENV NODE_ENV production
39 |
40 | RUN addgroup --system --gid 1001 nodejs
41 | RUN adduser --system --uid 1001 nextjs
42 |
43 | COPY --from=builder usr/src/app/public ./public
44 |
45 | RUN mkdir .next
46 | RUN chown nextjs:nodejs .next
47 |
48 | COPY --from=builder --chown=nextjs:nodejs usr/src/app/.next/standalone ./
49 | COPY --from=builder --chown=nextjs:nodejs usr/src/app/.next/static ./.next/static
50 |
51 | USER nextjs
52 |
53 | EXPOSE 3000
54 |
55 | # For Standalone build
56 | ENV PORT 3000
57 | ENV HOSTNAME "0.0.0.0"
58 |
59 | CMD [ "node", "server.js" ]
60 |
--------------------------------------------------------------------------------
/next-app/README.md:
--------------------------------------------------------------------------------
1 | Muzer
2 |
3 | ## Table of contents
4 |
5 |
6 | 1. Clone the repository:
7 | ```bash
8 | git clone https://github.com/code100x/muzer
9 | ```
10 | 2. Navigate to the project directory:
11 | ```bash
12 | cd muzer
13 | ```
14 | 3. Run the following command to start the application:
15 | ```bash
16 | docker volume create postgres-data # (optional) run this command if you face any mount volume / volume not exist error
17 | docker-compose up -d
18 | ```
19 |
20 | ### Without Docker
21 |
22 | 1. clone the repository:
23 | ```bash
24 | git clone https://github.com/code100x/muzer
25 | ```
26 | 2. Navigate to the project directory:
27 | ```bash
28 | cd muzer
29 | ```
30 | 3. (optional) Start a PostgreSQL database using Docker:
31 | ```bash
32 | docker run -d \
33 | --name muzer-db \
34 | -e POSTGRES_USER=myuser \
35 | -e POSTGRES_PASSWORD=mypassword \
36 | -e POSTGRES_DB=mydatabase \
37 | -p 5432:5432 \
38 | postgres
39 | ```
40 | based on this command the connection url will be
41 | ```
42 | DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase?schema=public
43 | ```
44 | 4. Create a `.env` file based on the `.env.example` file and configure the `DATABASE_URL` with your postgreSQL connection string.
45 | 5. Install dependencies:
46 | ```bash
47 | pnpm install
48 | ```
49 | 6. Run database migrations:
50 | ```bash
51 | pnpm run prisma:migrate
52 | ```
53 | 7. Start the development server:
54 | ```bash
55 | pnpm run dev
56 | ```
57 | ## Usage
58 |
59 | 1. Access the aplication in your browser at `http://localhost:3000`
60 |
--------------------------------------------------------------------------------
/next-app/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 |
3 | import { authOptions } from "@/lib/auth-options";
4 |
5 | const handler = NextAuth(authOptions);
6 |
7 | export { handler as GET, handler as POST };
--------------------------------------------------------------------------------
/next-app/app/api/spaces/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import prisma from "@/lib/db";
3 | import { authOptions } from "@/lib/auth-options";
4 | import { getServerSession } from "next-auth";
5 |
6 | export async function POST(req: NextRequest) {
7 | try {
8 |
9 | const session = await getServerSession(authOptions);
10 |
11 |
12 | if (!session?.user?.id) {
13 | return NextResponse.json(
14 | { success: false, message: "You must be logged in to create a space" },
15 | { status: 401 }
16 | );
17 | }
18 |
19 |
20 | const data = await req.json();
21 |
22 |
23 | if (!data.spaceName) {
24 | return NextResponse.json(
25 | { success: false, message: "Space name is required" },
26 | { status: 400 }
27 | );
28 | }
29 |
30 |
31 | const space = await prisma.space.create({
32 | data: {
33 | name: data.spaceName,
34 | hostId: session.user.id,
35 | },
36 | });
37 |
38 |
39 | return NextResponse.json(
40 | { success: true, message: "Space created successfully", space },
41 | { status: 201 }
42 | );
43 | } catch (error: any) {
44 |
45 | if (error.message === "Unauthenticated Request") {
46 | return NextResponse.json(
47 | { success: false, message: "You must be logged in to create a space" },
48 | { status: 401 }
49 | );
50 | }
51 |
52 |
53 | return NextResponse.json(
54 | { success: false, message: `An unexpected error occurred: ${error.message}` },
55 | { status: 500 }
56 | );
57 | }
58 | }
59 |
60 |
61 |
62 | export async function DELETE(req:NextRequest) {
63 | try {
64 | const spaceId = req.nextUrl.searchParams.get("spaceId");
65 | const session = await getServerSession(authOptions);
66 | if (!session?.user?.id) {
67 | return NextResponse.json(
68 | { success: false, message: "You must be logged in to delete a space" },
69 | { status: 401 }
70 | );
71 | }
72 |
73 | if(!spaceId){
74 | return NextResponse.json(
75 | { success: false, message: "Space Id is required" },
76 | { status: 401 }
77 | );
78 | }
79 | console.log(spaceId)
80 | const space = await prisma.space.findUnique({
81 | where: { id: spaceId },
82 | });
83 |
84 | if (!space) {
85 | return NextResponse.json(
86 | { success: false, message: "Space not found" },
87 | { status: 404 }
88 | );
89 | }
90 |
91 |
92 | if (space.hostId !== session.user.id) {
93 | return NextResponse.json(
94 | { success: false, message: "You are not authorized to delete this space" },
95 | { status: 403 }
96 | );
97 | }
98 |
99 |
100 | await prisma.space.delete({
101 | where: { id: spaceId },
102 | });
103 |
104 |
105 | return NextResponse.json(
106 | { success: true, message: "Space deleted successfully" },
107 | { status: 200 }
108 | );
109 | } catch (error: any) {
110 |
111 | console.error("Error deleting space:", error);
112 | return NextResponse.json(
113 | { success: false, message: `Error deleting space: ${error.message}` },
114 | { status: 500 }
115 | );
116 | }
117 | }
118 |
119 | export async function GET(req:NextRequest) {
120 | try {
121 | const session = await getServerSession(authOptions);
122 | if (!session?.user?.id) {
123 | return NextResponse.json(
124 | { success: false, message: "You must be logged in to retrieve space information" },
125 | { status: 401 }
126 | );
127 | }
128 | const spaceId = req.nextUrl.searchParams.get("spaceId");
129 |
130 | // If spaceId exist return the hostId
131 | if (spaceId) {
132 | const space = await prisma.space.findUnique({
133 | where: { id: spaceId },
134 | select: { hostId: true },
135 | });
136 |
137 | if (!space) {
138 | return NextResponse.json(
139 | { success: false, message: "Space not found" },
140 | { status: 404 }
141 | );
142 | }
143 |
144 | return NextResponse.json(
145 | { success: true, message: "Host ID retrieved successfully", hostId: space.hostId },
146 | { status: 200 }
147 | );
148 | }
149 |
150 | // If no spaceId is provided, retrieve all spaces
151 | const spaces=await prisma.space.findMany({
152 | where:{
153 | hostId:session.user.id
154 | }
155 | })
156 | return NextResponse.json(
157 | { success: true, message: "Spaces retrieved successfully", spaces },
158 | { status: 200 })
159 |
160 | } catch (error:any) {
161 | console.error("Error retrieving space:", error);
162 | return NextResponse.json(
163 | { success: false, message: `Error retrieving space: ${error.message}` },
164 | { status: 500 }
165 | );
166 |
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/next-app/app/api/streams/downvote/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/lib/auth-options";
2 | import db from "@/lib/db";
3 | import { getServerSession } from "next-auth";
4 | import { NextRequest, NextResponse } from "next/server";
5 | import { z } from "zod";
6 |
7 | const UpvoteSchema = z.object({
8 | streamId: z.string(),
9 | });
10 |
11 | export async function POST(req: NextRequest) {
12 | const session = await getServerSession(authOptions);
13 |
14 | if (!session?.user.id) {
15 | return NextResponse.json(
16 | {
17 | message: "Unauthenticated",
18 | },
19 | {
20 | status: 403,
21 | },
22 | );
23 | }
24 | const user = session.user;
25 |
26 | try {
27 | const data = UpvoteSchema.parse(await req.json());
28 | await db.upvote.delete({
29 | where: {
30 | userId_streamId: {
31 | userId: user.id,
32 | streamId: data.streamId,
33 | },
34 | },
35 | });
36 |
37 | return NextResponse.json({
38 | message: "Done!",
39 | });
40 | } catch (e) {
41 | return NextResponse.json(
42 | {
43 | message: "Error while upvoting",
44 | },
45 | {
46 | status: 403,
47 | },
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/next-app/app/api/streams/empty-queue/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/lib/auth-options";
2 | import db from "@/lib/db";
3 | import { getServerSession } from "next-auth";
4 | import { NextRequest, NextResponse } from "next/server";
5 |
6 | export async function POST(req:NextRequest) {
7 | const session = await getServerSession(authOptions);
8 |
9 | if (!session?.user) {
10 | return NextResponse.json(
11 | {
12 | message: "Unauthenticated",
13 | },
14 | {
15 | status: 403,
16 | },
17 | );
18 | }
19 | const user = session.user;
20 | const data = await req.json()
21 |
22 | try {
23 | await db.stream.updateMany({
24 | where: {
25 | userId: user.id,
26 | played: false,
27 | spaceId:data.spaceId
28 | },
29 | data: {
30 | played: true,
31 | playedTs: new Date(),
32 | },
33 | });
34 |
35 | return NextResponse.json({
36 | message: "Queue emptied successfully",
37 | });
38 | } catch (error) {
39 | console.error("Error emptying queue:", error);
40 | return NextResponse.json(
41 | {
42 | message: "Error while emptying the queue",
43 | },
44 | {
45 | status: 500,
46 | },
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/next-app/app/api/streams/my/route.ts:
--------------------------------------------------------------------------------
1 | import db from "@/lib/db";
2 | import { authOptions } from "@/lib/auth-options";
3 | import { getServerSession } from "next-auth";
4 | import { NextRequest, NextResponse } from "next/server";
5 |
6 | export async function GET(req: NextRequest) {
7 | const session = await getServerSession(authOptions);
8 |
9 | if (!session?.user) {
10 | return NextResponse.json(
11 | {
12 | message: "Unauthenticated",
13 | },
14 | {
15 | status: 403,
16 | },
17 | );
18 | }
19 | const user = session.user;
20 |
21 | const streams = await db.stream.findMany({
22 | where: {
23 | userId: user.id,
24 | },
25 | include: {
26 | _count: {
27 | select: {
28 | upvotes: true,
29 | },
30 | },
31 | upvotes: {
32 | where: {
33 | userId: user.id,
34 | },
35 | },
36 | },
37 | });
38 |
39 | return NextResponse.json({
40 | streams: streams.map(({ _count, ...rest }) => ({
41 | ...rest,
42 | upvotes: _count.upvotes,
43 | haveUpvoted: rest.upvotes.length ? true : false,
44 | })),
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/next-app/app/api/streams/next/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/lib/auth-options";
2 | import db from "@/lib/db";
3 | import { getServerSession } from "next-auth";
4 | import { NextRequest, NextResponse } from "next/server";
5 |
6 | export async function GET(req: NextRequest) {
7 | const session = await getServerSession(authOptions);
8 |
9 | if (!session?.user.id) {
10 | return NextResponse.json(
11 | {
12 | message: "Unauthenticated",
13 | },
14 | {
15 | status: 403,
16 | },
17 | );
18 | }
19 | const user = session.user;
20 | const spaceId = req.nextUrl.searchParams.get("spaceId");
21 |
22 | const mostUpvotedStream = await db.stream.findFirst({
23 | where: {
24 | userId: user.id,
25 | played: false,
26 | spaceId:spaceId
27 | },
28 | orderBy: {
29 | upvotes: {
30 | _count: "desc",
31 | },
32 | },
33 | });
34 |
35 | await Promise.all([
36 | db.currentStream.upsert({
37 | where: {
38 | spaceId:spaceId as string
39 | },
40 | update: {
41 | userId: user.id,
42 | streamId: mostUpvotedStream?.id,
43 | spaceId:spaceId
44 | },
45 | create: {
46 | userId: user.id,
47 | streamId: mostUpvotedStream?.id,
48 | spaceId:spaceId
49 | },
50 | }),
51 | db.stream.update({
52 | where: {
53 | id: mostUpvotedStream?.id ?? "",
54 | },
55 | data: {
56 | played: true,
57 | playedTs: new Date(),
58 | },
59 | }),
60 | ]);
61 |
62 | return NextResponse.json({
63 | stream: mostUpvotedStream,
64 | });
65 | }
66 |
--------------------------------------------------------------------------------
/next-app/app/api/streams/remove/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/lib/auth-options";
2 | import db from "@/lib/db";
3 | import { getServerSession } from "next-auth";
4 | import { NextRequest, NextResponse } from "next/server";
5 | import { z } from "zod";
6 |
7 | const RemoveStreamSchema = z.object({
8 | streamId: z.string(),
9 | spaceId:z.string()
10 | });
11 |
12 | export async function DELETE(req: NextRequest) {
13 | const session = await getServerSession(authOptions);
14 |
15 | if (!session?.user.id) {
16 | return NextResponse.json(
17 | {
18 | message: "Unauthenticated",
19 | },
20 | {
21 | status: 403,
22 | },
23 | );
24 | }
25 | const user = session.user;
26 |
27 | try {
28 | const { searchParams } = new URL(req.url);
29 | const streamId = searchParams.get("streamId");
30 | const spaceId = searchParams.get('spaceId')
31 |
32 | if (!streamId) {
33 | return NextResponse.json(
34 | {
35 | message: "Stream ID is required",
36 | },
37 | {
38 | status: 400,
39 | },
40 | );
41 | }
42 |
43 | await db.stream.delete({
44 | where: {
45 | id: streamId,
46 | userId: user.id,
47 | spaceId:spaceId
48 | },
49 | });
50 |
51 | return NextResponse.json({
52 | message: "Song removed successfully",
53 | });
54 | } catch (e) {
55 | return NextResponse.json(
56 | {
57 | message: "Error while removing the song",
58 | },
59 | {
60 | status: 400,
61 | },
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/next-app/app/api/streams/route.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { NextRequest, NextResponse } from "next/server";
3 | import db from "@/lib/db";
4 | //@ts-ignore
5 | import youtubesearchapi from "youtube-search-api";
6 | import { YT_REGEX } from "@/lib/utils";
7 | import { getServerSession } from "next-auth";
8 | import { authOptions } from "@/lib/auth-options";
9 |
10 |
11 | const CreateStreamSchema = z.object({
12 | creatorId: z.string(),
13 | url: z.string(),
14 | spaceId:z.string()
15 | });
16 |
17 | const MAX_QUEUE_LEN = 20;
18 |
19 | export async function POST(req: NextRequest) {
20 | try {
21 | const session = await getServerSession(authOptions);
22 |
23 | if (!session?.user.id) {
24 | return NextResponse.json(
25 | {
26 | message: "Unauthenticated",
27 | },
28 | {
29 | status: 403,
30 | },
31 | );
32 | }
33 | const user = session.user;
34 |
35 | const data = CreateStreamSchema.parse(await req.json());
36 |
37 | if (!data.url.trim()) {
38 | return NextResponse.json(
39 | {
40 | message: "YouTube link cannot be empty",
41 | },
42 | {
43 | status: 400,
44 | },
45 | );
46 | }
47 |
48 | const isYt = data.url.match(YT_REGEX);
49 | const videoId = data.url ? data.url.match(YT_REGEX)?.[1] : null;
50 | if (!isYt || !videoId) {
51 | return NextResponse.json(
52 | {
53 | message: "Invalid YouTube URL format",
54 | },
55 | {
56 | status: 400,
57 | },
58 | );
59 | }
60 |
61 | const res = await youtubesearchapi.GetVideoDetails(videoId);
62 |
63 | // Check if the user is not the creator
64 | if (user.id !== data.creatorId) {
65 | const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
66 | const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
67 |
68 | const userRecentStreams = await db.stream.count({
69 | where: {
70 | userId: data.creatorId,
71 | addedBy: user.id,
72 | createAt: {
73 | gte: tenMinutesAgo,
74 | },
75 | },
76 | });
77 |
78 | // Check for duplicate song in the last 10 minutes
79 | const duplicateSong = await db.stream.findFirst({
80 | where: {
81 | userId: data.creatorId,
82 | extractedId: videoId,
83 | createAt: {
84 | gte: tenMinutesAgo,
85 | },
86 | },
87 | });
88 | if (duplicateSong) {
89 | return NextResponse.json(
90 | {
91 | message: "This song was already added in the last 10 minutes",
92 | },
93 | {
94 | status: 429,
95 | },
96 | );
97 | }
98 |
99 | // Rate limiting checks for non-creator users
100 | const streamsLastTwoMinutes = await db.stream.count({
101 | where: {
102 | userId: data.creatorId,
103 | addedBy: user.id,
104 | createAt: {
105 | gte: twoMinutesAgo,
106 | },
107 | },
108 | });
109 |
110 | if (streamsLastTwoMinutes >= 2) {
111 | return NextResponse.json(
112 | {
113 | message:
114 | "Rate limit exceeded: You can only add 2 songs per 2 minutes",
115 | },
116 | {
117 | status: 429,
118 | },
119 | );
120 | }
121 |
122 | if (userRecentStreams >= 5) {
123 | return NextResponse.json(
124 | {
125 | message:
126 | "Rate limit exceeded: You can only add 5 songs per 10 minutes",
127 | },
128 | {
129 | status: 429,
130 | },
131 | );
132 | }
133 | }
134 |
135 | const thumbnails = res.thumbnail.thumbnails;
136 | thumbnails.sort((a: { width: number }, b: { width: number }) =>
137 | a.width < b.width ? -1 : 1,
138 | );
139 |
140 | const existingActiveStreams = await db.stream.count({
141 | where: {
142 | spaceId: data.spaceId,
143 | played: false,
144 | },
145 | });
146 |
147 | if (existingActiveStreams >= MAX_QUEUE_LEN) {
148 | return NextResponse.json(
149 | {
150 | message: "Queue is full",
151 | },
152 | {
153 | status: 429,
154 | },
155 | );
156 | }
157 |
158 | const stream = await db.stream.create({
159 | data: {
160 | userId: data.creatorId,
161 | addedBy: user.id,
162 | url: data.url,
163 | extractedId: videoId,
164 | type: "Youtube",
165 | title: res.title ?? "Can't find video",
166 | smallImg:
167 | (thumbnails.length > 1
168 | ? thumbnails[thumbnails.length - 2].url
169 | : thumbnails[thumbnails.length - 1].url) ??
170 | "https://cdn.pixabay.com/photo/2024/02/28/07/42/european-shorthair-8601492_640.jpg",
171 | bigImg:
172 | thumbnails[thumbnails.length - 1].url ??
173 | "https://cdn.pixabay.com/photo/2024/02/28/07/42/european-shorthair-8601492_640.jpg",
174 | spaceId:data.spaceId
175 | },
176 | });
177 |
178 | return NextResponse.json({
179 | ...stream,
180 | hasUpvoted: false,
181 | upvotes: 0,
182 | });
183 | } catch (e) {
184 | console.error(e);
185 | return NextResponse.json(
186 | {
187 | message: "Error while adding a stream",
188 | },
189 | {
190 | status: 500,
191 | },
192 | );
193 | }
194 | }
195 |
196 | export async function GET(req: NextRequest) {
197 | const spaceId = req.nextUrl.searchParams.get("spaceId");
198 | const session = await getServerSession(authOptions);
199 | if (!session?.user.id) {
200 | return NextResponse.json(
201 | {
202 | message: "Unauthenticated",
203 | },
204 | {
205 | status: 403,
206 | },
207 | );
208 | }
209 | const user = session.user;
210 |
211 | if (!spaceId) {
212 | return NextResponse.json({
213 | message: "Error"
214 | }, {
215 | status: 411
216 | })
217 | }
218 |
219 | const [space, activeStream] = await Promise.all([
220 | db.space.findUnique({
221 | where: {
222 | id: spaceId,
223 | },
224 | include: {
225 | streams: {
226 | include: {
227 | _count: {
228 | select: {
229 | upvotes: true
230 | }
231 | },
232 | upvotes: {
233 | where: {
234 | userId: session?.user.id
235 | }
236 | }
237 |
238 | },
239 | where:{
240 | played:false
241 | }
242 | },
243 | _count: {
244 | select: {
245 | streams: true
246 | }
247 | },
248 |
249 | }
250 |
251 | }),
252 | db.currentStream.findFirst({
253 | where: {
254 | spaceId: spaceId
255 | },
256 | include: {
257 | stream: true
258 | }
259 | })
260 | ]);
261 |
262 | const hostId =space?.hostId;
263 | const isCreator = session.user.id=== hostId
264 |
265 | return NextResponse.json({
266 | streams: space?.streams.map(({_count, ...rest}) => ({
267 | ...rest,
268 | upvotes: _count.upvotes,
269 | haveUpvoted: rest.upvotes.length ? true : false
270 | })),
271 | activeStream,
272 | hostId,
273 | isCreator,
274 | spaceName:space?.name
275 | });
276 | }
277 |
--------------------------------------------------------------------------------
/next-app/app/api/streams/upvote/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/lib/auth-options";
2 | import db from "@/lib/db";
3 | import { getServerSession } from "next-auth";
4 | import { NextRequest, NextResponse } from "next/server";
5 | import { z } from "zod";
6 |
7 | const UpvoteSchema = z.object({
8 | streamId: z.string(),
9 | spaceId:z.string()
10 | });
11 |
12 | export async function POST(req: NextRequest) {
13 | const session = await getServerSession(authOptions);
14 |
15 | if (!session?.user) {
16 | return NextResponse.json(
17 | {
18 | message: "Unauthenticated",
19 | },
20 | {
21 | status: 403,
22 | },
23 | );
24 | }
25 | const user = session.user;
26 |
27 | try {
28 | const data = UpvoteSchema.parse(await req.json());
29 | await db.upvote.create({
30 | data: {
31 | userId: user.id,
32 | streamId: data.streamId,
33 | },
34 | });
35 | return NextResponse.json({
36 | message: "Done!",
37 | });
38 | } catch (e) {
39 | return NextResponse.json(
40 | {
41 | message: "Error while upvoting",
42 | },
43 | {
44 | status: 403,
45 | },
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/next-app/app/api/user/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/lib/auth-options";
2 | import { getServerSession } from "next-auth";
3 | import { NextRequest, NextResponse } from "next/server";
4 |
5 | export const GET = async (req: NextRequest) => {
6 | const session = await getServerSession(authOptions);
7 |
8 | if (!session?.user.id) {
9 | return NextResponse.json(
10 | {
11 | message: "Unauthenticated",
12 | },
13 | {
14 | status: 403,
15 | },
16 | );
17 | }
18 | return NextResponse.json({
19 | user: session.user,
20 | });
21 | };
22 |
23 | // dont static render
24 | export const dynamic = "force-dynamic";
25 |
--------------------------------------------------------------------------------
/next-app/app/auth/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useSession } from "next-auth/react";
4 | import { useRouter } from "next/navigation";
5 |
6 | import { SignInFlow } from "@/types/auth-types";
7 | import AuthScreen from "@/components/auth/auth-screen";
8 |
9 | export default function AuthPage({
10 | searchParams,
11 | }: {
12 | searchParams: { authType: SignInFlow; mailId?: string };
13 | }) {
14 | const formType = searchParams.authType;
15 | const session = useSession();
16 | const router = useRouter();
17 |
18 | if (session.status === "authenticated") {
19 | return router.push("/");
20 | }
21 | return ;
22 | }
23 |
--------------------------------------------------------------------------------
/next-app/app/dashboard/[spaceId]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useEffect, useState } from "react";
3 | import { useSocket } from "@/context/socket-context";
4 | import jwt from "jsonwebtoken";
5 | import StreamView from "@/components/StreamView";
6 | import ErrorScreen from "@/components/ErrorScreen";
7 | import LoadingScreen from "@/components/LoadingScreen";
8 |
9 |
10 |
11 | export default function Component({params:{spaceId}}:{params:{spaceId:string}}) {
12 |
13 |
14 | const { socket, user, loading, setUser, connectionError } = useSocket();
15 |
16 |
17 | const [creatorId,setCreatorId]=useState(null);
18 | const [loading1, setLoading1] = useState(true);
19 |
20 |
21 |
22 |
23 |
24 | useEffect(()=>{
25 | async function fetchHostId(){
26 | try {
27 | const response = await fetch(`/api/spaces/?spaceId=${spaceId}`,{
28 | method:"GET"
29 | });
30 | const data = await response.json()
31 | if (!response.ok || !data.success) {
32 | throw new Error(data.message || "Failed to retreive space's host id");
33 | }
34 | setCreatorId(data.hostId)
35 |
36 |
37 | } catch (error) {
38 |
39 | }
40 | finally{
41 | setLoading1(false)
42 | }
43 | }
44 | fetchHostId();
45 | },[spaceId])
46 |
47 |
48 |
49 | useEffect(() => {
50 | if (user && socket && creatorId) {
51 | const token = user.token || jwt.sign(
52 | {
53 | creatorId: creatorId,
54 | userId: user?.id,
55 | },
56 | process.env.NEXT_PUBLIC_SECRET || "",
57 | {
58 | expiresIn: "24h",
59 | }
60 | );
61 |
62 | socket?.send(
63 | JSON.stringify({
64 | type: "join-room",
65 | data: {
66 | token,
67 | spaceId
68 | },
69 | })
70 | );
71 | if(!user.token){
72 | setUser({ ...user, token });
73 | }
74 |
75 | }
76 | }, [user,spaceId,creatorId,socket]);
77 |
78 | if (connectionError) {
79 | return Cannot connect to socket server;
80 | }
81 |
82 | if (loading) {
83 | return ;
84 | }
85 |
86 | if (!user) {
87 | return Please Log in....;
88 | }
89 | if(loading1){
90 | return
91 | }
92 |
93 |
94 | if(user.id!=creatorId){
95 | return You are not the creator of this space
96 | }
97 |
98 |
99 |
100 |
101 | return ;
102 |
103 | }
104 |
105 | export const dynamic = "auto";
106 |
--------------------------------------------------------------------------------
/next-app/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/muzer/650ad9098446b0ce473b0741bb9e3b90a5685d92/next-app/app/favicon.ico
--------------------------------------------------------------------------------
/next-app/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root {
8 | height: 100%;
9 | }
10 |
11 | @layer base {
12 | :root {
13 | --background: 0 0% 100%;
14 | --foreground: 224 71.4% 4.1%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 224 71.4% 4.1%;
18 |
19 | --popover: 0 0% 100%;
20 | --popover-foreground: 224 71.4% 4.1%;
21 |
22 | --primary: 271.48 81.33% 55.88%;
23 | --primary-foreground: 210 20% 98%;
24 |
25 | --secondary: 220 14.3% 95.9%;
26 | --secondary-foreground: 220.9 39.3% 11%;
27 |
28 | --muted: 220 14.3% 95.9%;
29 | --muted-foreground: 220 8.9% 46.1%;
30 |
31 | --accent: 220 14.3% 95.9%;
32 | --accent-foreground: 220.9 39.3% 11%;
33 |
34 | --destructive: 0 84.2% 60.2%;
35 | --destructive-foreground: 210 20% 98%;
36 |
37 | --border: 220 13% 91%;
38 |
39 | --input: 220 13% 91%;
40 |
41 | --ring: 271.48 81.33% 55.88%;
42 |
43 | --radius: 0.5rem;
44 |
45 | --chart-1: 12 76% 61%;
46 | --chart-2: 173 58% 39%;
47 | --chart-3: 197 37% 24%;
48 | --chart-4: 43 74% 66%;
49 | --chart-5: 27 87% 67%;
50 | }
51 |
52 | .dark {
53 | --background: 224 71.4% 4.1%;
54 | --foreground: 210 20% 98%;
55 |
56 | --card: 224 71.4% 4.1%;
57 | --card-foreground: 210 20% 98%;
58 |
59 | --popover: 224 71.4% 4.1%;
60 | --popover-foreground: 210 20% 98%;
61 |
62 | --primary: 271.48 81.33% 55.88%;
63 | --primary-foreground: 210 20% 98%;
64 |
65 | --secondary: 215 27.9% 16.9%;
66 | --secondary-foreground: 210 20% 98%;
67 |
68 | --muted: 215 27.9% 16.9%;
69 | --muted-foreground: 217.9 10.6% 64.9%;
70 |
71 | --accent: 215 27.9% 16.9%;
72 | --accent-foreground: 210 20% 98%;
73 |
74 | --destructive: 0 62.8% 30.6%;
75 | --destructive-foreground: 210 20% 98%;
76 |
77 | --border: 215 27.9% 16.9%;
78 |
79 | --input: 215 27.9% 16.9%;
80 |
81 | --ring: 271.48 81.33% 55.88%;
82 |
83 | --chart-1: 220 70% 50%;
84 | --chart-2: 160 60% 45%;
85 | --chart-3: 30 80% 55%;
86 | --chart-4: 280 65% 60%;
87 | --chart-5: 340 75% 55%;
88 | }
89 | }
90 |
91 | @layer base {
92 | * {
93 | @apply border-border;
94 | }
95 | body {
96 | @apply text-foreground;
97 | }
98 | }
99 |
100 |
101 | input:-webkit-autofill,
102 | input:-webkit-autofill:hover,
103 | input:-webkit-autofill:focus,
104 | textarea:-webkit-autofill,
105 | textarea:-webkit-autofill:hover,
106 | textarea:-webkit-autofill:focus,
107 | select:-webkit-autofill,
108 | select:-webkit-autofill:hover,
109 | select:-webkit-autofill:focus {
110 | -webkit-text-fill-color: white;
111 | -webkit-box-shadow: 0 0 0px 1000px transparent inset;
112 | box-shadow: 0 0 0px 1000px transparent inset;
113 | transition: background-color 5000s ease-in-out 0s;
114 | color: white;
115 | caret-color: white;
116 | }
117 |
--------------------------------------------------------------------------------
/next-app/app/home/page.tsx:
--------------------------------------------------------------------------------
1 | import HomeView from "@/components/HomeView";
2 | import { authOptions } from "@/lib/auth-options";
3 | import { getServerSession } from "next-auth";
4 |
5 |
6 |
7 | export default async function Home(){
8 | const session =await getServerSession(authOptions);
9 |
10 | if (!session?.user.id) {
11 | return Please Log in....
;
12 | }
13 | return
14 |
15 | }
--------------------------------------------------------------------------------
/next-app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import { Providers, ThemeProvider } from "@/components/provider";
4 |
5 | import "./globals.css";
6 | import { Toaster } from "sonner";
7 |
8 | const inter = Inter({ subsets: ["latin"] });
9 |
10 | type ToasterProps = React.ComponentProps;
11 |
12 | const toastOptions: ToasterProps = {
13 | theme: "dark",
14 | richColors: true,
15 | closeButton: true,
16 | pauseWhenPageIsHidden: true,
17 | };
18 |
19 | export const metadata: Metadata = {
20 | metadataBase: new URL(
21 | process.env.NEXTAUTH_URL || "https://muzer.100xdevs.com/",
22 | ),
23 | keywords:
24 | "music stream, fan interaction, live streaming, high-quality audio, curate music, Muzer",
25 | title: "Muzer | Fan-Curated Live Music Streaming",
26 | description:
27 | "Live fan-curated music streaming. High-quality audio, real-time engagement.",
28 | openGraph: {
29 | type: "website",
30 | locale: "en_IE",
31 | url: `${process.env.NEXTAUTH_URL}/opengraph-image.png`,
32 | images: "/opengraph-image.png",
33 | siteName: "Infra",
34 | },
35 | icons: [
36 | {
37 | url: `${process.env.NEXTAUTH_URL}/favicon.ico`,
38 | },
39 | ],
40 | };
41 |
42 | export default function RootLayout({
43 | children,
44 | }: Readonly<{
45 | children: React.ReactNode;
46 | }>) {
47 | return (
48 |
49 |
50 |
51 |
57 | {children}
58 |
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/next-app/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/muzer/650ad9098446b0ce473b0741bb9e3b90a5685d92/next-app/app/opengraph-image.png
--------------------------------------------------------------------------------
/next-app/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Button } from "@/components/ui/button";
3 | import { Input } from "@/components/ui/input";
4 | //@ts-ignore
5 | import { Users, Radio, Headphones } from "lucide-react";
6 | import { Appbar } from "@/components/Appbar";
7 | import { getServerSession } from "next-auth";
8 | import { redirect } from "next/navigation";
9 | import { authOptions } from "@/lib/auth-options";
10 |
11 | export default async function LandingPage() {
12 | const session = await getServerSession(authOptions);
13 |
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Let Your Fans Choose the Beat
24 |
25 |
26 | Empower your audience to curate your music stream. Connect with
27 | fans like never before.
28 |
29 |
30 |
31 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Key Features
52 |
53 |
54 |
55 |
56 |
Fan Interaction
57 |
Let fans choose the music.
58 |
59 |
60 |
61 |
Live Streaming
62 |
Stream with real-time input.
63 |
64 |
65 |
66 |
67 | High-Quality Audio
68 |
69 |
Crystal clear sound quality.
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | Ready to Transform Your Streams?
80 |
81 |
82 | Join MusicStreamChoice today and create unforgettable
83 | experiences.
84 |
85 |
86 |
87 | {/* */}
110 |
111 |
112 |
113 |
114 |
133 |
134 | );
135 | }
136 |
--------------------------------------------------------------------------------
/next-app/app/spaces/[spaceId]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useEffect, useState } from "react";
3 | import { useSocket } from "@/context/socket-context";
4 | import jwt from "jsonwebtoken";
5 | import StreamView from "@/components/StreamView";
6 | import ErrorScreen from "@/components/ErrorScreen";
7 | import LoadingScreen from "@/components/LoadingScreen";
8 | import { useRouter } from "next/navigation";
9 |
10 |
11 | // Default styles that can be overridden by your app
12 | import '@solana/wallet-adapter-react-ui/styles.css';
13 |
14 |
15 |
16 | export default function Component({params:{spaceId}}:{params:{spaceId:string}}) {
17 |
18 |
19 | const { socket, user, loading, setUser, connectionError } = useSocket();
20 |
21 |
22 | const [creatorId,setCreatorId]=useState();
23 | const [loading1, setLoading1] = useState(true);
24 | const router = useRouter();
25 |
26 |
27 |
28 | console.log(spaceId)
29 |
30 | useEffect(()=>{
31 | async function fetchHostId(){
32 | try {
33 | const response = await fetch(`/api/spaces/?spaceId=${spaceId}`,{
34 | method:"GET"
35 | });
36 | const data = await response.json()
37 | if (!response.ok || !data.success) {
38 | throw new Error(data.message || "Failed to retreive space's host id");
39 | }
40 | setCreatorId(data.hostId)
41 |
42 |
43 | } catch (error) {
44 |
45 | }
46 | finally{
47 | setLoading1(false)
48 | }
49 | }
50 | fetchHostId();
51 | },[spaceId])
52 |
53 |
54 |
55 | useEffect(() => {
56 | if (user && socket && creatorId) {
57 | const token = user.token || jwt.sign(
58 | {
59 | creatorId: creatorId,
60 | userId: user?.id,
61 | },
62 | process.env.NEXT_PUBLIC_SECRET || "",
63 | {
64 | expiresIn: "24h",
65 | }
66 | );
67 |
68 | socket?.send(
69 | JSON.stringify({
70 | type: "join-room",
71 | data: {
72 | token,
73 | spaceId
74 | },
75 | })
76 | );
77 | if(!user.token){
78 | setUser({ ...user, token });
79 | }
80 |
81 | }
82 | }, [user,spaceId,creatorId,socket]);
83 |
84 | if (connectionError) {
85 | return Cannot connect to socket server;
86 | }
87 |
88 | if (loading) {
89 | return ;
90 | }
91 |
92 | if (!user) {
93 | return Please Log in....;
94 | }
95 | if(loading1){
96 | return
97 | }
98 |
99 | if(creatorId===user.id){
100 | router.push(`/dashboard/${spaceId}`)
101 | }
102 |
103 |
104 |
105 |
106 | return ;
107 |
108 | }
109 |
110 | export const dynamic = "auto";
111 |
--------------------------------------------------------------------------------
/next-app/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/next-app/components/Appbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { signIn, signOut, useSession } from "next-auth/react";
3 | import { Button } from "@/components/ui/button";
4 | import { useRouter } from "next/navigation";
5 | import { ThemeSwitcher } from "./ThemeSwitcher";
6 | import { WalletMultiButton } from "@solana/wallet-adapter-react-ui";
7 | import Link from "next/link";
8 |
9 |
10 | export function Appbar({ showThemeSwitch = true , isSpectator=false }) {
11 | const session = useSession();
12 | const router = useRouter();
13 |
14 | return (
15 |
16 |
{
18 | router.push("/home");
19 | }}
20 | className={`flex flex-col justify-center text-lg font-bold hover:cursor-pointer ${showThemeSwitch ? "" : "text-white"}`}
21 | >
22 | Muzer
23 |
24 |
25 | {isSpectator &&
}
26 | {session.data?.user && (
27 |
37 | )}
38 | {!session.data?.user && (
39 |
40 |
46 |
54 |
60 |
61 |
62 | )}
63 |
64 | {showThemeSwitch &&
}
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/next-app/components/ErrorScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from "react";
2 |
3 | export default function ErrorScreen({ children }: PropsWithChildren) {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/next-app/components/HomeView.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { toast } from "sonner";
3 | import { Appbar } from "@/components/Appbar";
4 | import { Button } from "@/components/ui/button";
5 | import {
6 | Dialog,
7 | DialogContent,
8 | DialogHeader,
9 | DialogTitle,
10 | DialogFooter,
11 | } from "@/components/ui/dialog";
12 | import { useEffect, useMemo, useState } from "react";
13 | import CardSkeleton from "./ui/cardSkeleton";
14 | import SpacesCard from "./SpacesCard";
15 |
16 | interface Space {
17 | endTime?: Date | null;
18 | hostId: string;
19 | id: string;
20 | isActive: boolean;
21 | name: string;
22 | startTime: Date | null;
23 | }
24 |
25 | export default function HomeView() {
26 | const [isCreateSpaceOpen, setIsCreateSpaceOpen] = useState(false);
27 | const [spaceName, setSpaceName] = useState("");
28 | const [spaces, setSpaces] = useState(null);
29 | const [loading, setIsLoading] = useState(false);
30 |
31 | useEffect(() => {
32 | const fetchSpaces = async () => {
33 | setIsLoading(true);
34 | try {
35 | const response = await fetch("/api/spaces", {
36 | method: "GET",
37 | });
38 |
39 | const data = await response.json();
40 |
41 | if (!response.ok || !data.success) {
42 | throw new Error(data.message || "Failed to fetch spaces");
43 | }
44 |
45 | const fetchedSpaces: Space[] = data.spaces;
46 | setSpaces(fetchedSpaces);
47 | } catch (error) {
48 | toast.error("Error fetching spaces");
49 | } finally {
50 | setIsLoading(false);
51 | }
52 | };
53 | fetchSpaces();
54 | }, []);
55 |
56 | const handleCreateSpace = async () => {
57 | setIsCreateSpaceOpen(false);
58 | try {
59 | const response = await fetch(`/api/spaces`, {
60 | method: "POST",
61 | headers: {
62 | "Content-Type": "application/json",
63 | },
64 | body: JSON.stringify({
65 | spaceName: spaceName,
66 | }),
67 | });
68 | const data = await response.json();
69 |
70 | if (!response.ok || !data.success) {
71 | throw new Error(data.message || "Failed to create space");
72 | }
73 |
74 | const newSpace = data.space;
75 | setSpaces((prev) => {
76 | const updatedSpaces: Space[] = prev ? [...prev, newSpace] : [newSpace];
77 | return updatedSpaces;
78 | });
79 | toast.success(data.message);
80 | } catch (error: any) {
81 | toast.error(error.message || "Error Creating Space");
82 | }
83 | };
84 |
85 | const handleDeleteSpace = async (spaceId: string) => {
86 | try {
87 | const response = await fetch(`/api/spaces/?spaceId=${spaceId}`, {
88 | method: "DELETE",
89 | });
90 | const data = await response.json();
91 |
92 | if (!response.ok || !data.success) {
93 | throw new Error(data.message || "Failed to delete space");
94 | }
95 | setSpaces((prev) => {
96 | const updatedSpaces: Space[] = prev
97 | ? prev.filter((space) => space.id !== spaceId)
98 | : [];
99 | return updatedSpaces;
100 | });
101 | toast.success(data.message);
102 | } catch (error: any) {
103 | toast.error(error.message || "Error Deleting Space");
104 | }
105 | };
106 |
107 | const renderSpaces = useMemo(() => {
108 | if (loading) {
109 | return (
110 | <>
111 |
112 |
113 |
114 |
115 |
116 |
117 | >
118 | );
119 | }
120 |
121 | if (spaces && spaces.length > 0) {
122 | return spaces.map((space) => (
123 |
128 | ));
129 | }
130 | }, [loading, spaces, handleDeleteSpace]);
131 |
132 | return (
133 |
134 |
135 |
136 |
137 | Spaces
138 |
139 |
147 |
148 |
149 | {renderSpaces}
150 |
151 |
152 |
191 |
192 | );
193 | }
194 |
--------------------------------------------------------------------------------
/next-app/components/LoadingScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function LoadingScreen() {
4 | return (
5 |
6 |
7 |
Loading...
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/next-app/components/OldStreamView.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState, useEffect, useRef } from "react";
3 | import { Button } from "@/components/ui/button";
4 | import { Input } from "@/components/ui/input";
5 | import { Card, CardContent } from "@/components/ui/card";
6 | import { ChevronUp, ChevronDown, Share2, Play, Trash2, X, MessageCircle, Instagram, Twitter} from "lucide-react";
7 | import { toast } from "sonner";
8 | import { Appbar } from "./Appbar";
9 | import LiteYouTubeEmbed from "react-lite-youtube-embed";
10 | import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
11 | import { YT_REGEX } from "../lib/utils";
12 | import YouTubePlayer from "youtube-player";
13 | import { useSession } from "next-auth/react";
14 | import type { Session } from "next-auth";
15 | import Image from "next/image";
16 | import {
17 | Dialog,
18 | DialogContent,
19 | DialogHeader,
20 | DialogTitle,
21 | DialogDescription,
22 | DialogFooter,
23 | } from "@/components/ui/dialog";
24 | import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
25 |
26 |
27 |
28 | interface Video {
29 | id: string;
30 | type: string;
31 | url: string;
32 | extractedId: string;
33 | title: string;
34 | smallImg: string;
35 | bigImg: string;
36 | active: boolean;
37 | userId: string;
38 | upvotes: number;
39 | haveUpvoted: boolean;
40 | spaceId:string
41 | }
42 |
43 | interface CustomSession extends Omit {
44 | user: {
45 | id: string;
46 | name?: string | null;
47 | email?: string | null;
48 | image?: string | null;
49 | };
50 | }
51 |
52 | const REFRESH_INTERVAL_MS = 10 * 1000;
53 |
54 | export default function StreamView({
55 | creatorId,
56 | playVideo = false,
57 | spaceId
58 | }: {
59 | creatorId: string;
60 | playVideo: boolean;
61 | spaceId:string;
62 | }) {
63 | const [inputLink, setInputLink] = useState("");
64 | const [queue, setQueue] = useState