├── .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 | {/*
88 | */} 94 | 102 | 108 | 109 | {/*
*/} 110 |
111 |
112 |
113 |
114 |
115 |

116 | © 2023 MusicStreamChoice. All rights reserved. 117 |

118 | 132 |
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 | 153 | 154 | 155 | 156 | Create new space 157 | 158 |
159 | 165 | ) => { 170 | setSpaceName(e.target.value); 171 | }} 172 | /> 173 |
174 |
175 | 176 | 182 | 188 | 189 |
190 |
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([]); 65 | const [currentVideo, setCurrentVideo] = useState