├── src ├── app │ ├── favicon.ico │ ├── album │ │ └── [albumId] │ │ │ ├── loading.jsx │ │ │ ├── layout.js │ │ │ └── page.jsx │ ├── favourite │ │ ├── loading.jsx │ │ └── page.jsx │ ├── login │ │ ├── loading.jsx │ │ └── page.jsx │ ├── signup │ │ ├── loading.jsx │ │ └── page.jsx │ ├── search │ │ └── [query] │ │ │ ├── loading.jsx │ │ │ ├── layout.js │ │ │ └── page.jsx │ ├── artist │ │ └── [artistId] │ │ │ ├── loading.jsx │ │ │ ├── layout.js │ │ │ └── page.jsx │ ├── AuthProvider.js │ ├── error.jsx │ ├── not-found.js │ ├── robots.js │ ├── page.jsx │ ├── api │ │ ├── userInfo │ │ │ └── route.js │ │ ├── signup │ │ │ └── route.js │ │ ├── login │ │ │ └── route.js │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.js │ │ ├── favourite │ │ │ └── route.js │ │ ├── userPlaylists │ │ │ ├── route.js │ │ │ └── songs │ │ │ │ └── route.js │ │ └── forgotPassword │ │ │ └── route.js │ ├── myPlaylists │ │ └── [playlistId] │ │ │ └── page.jsx │ ├── playlist │ │ └── [playlistId] │ │ │ ├── loading.jsx │ │ │ ├── layout.js │ │ │ └── page.jsx │ ├── sitemap.js │ ├── reset-password │ │ ├── page.jsx │ │ └── [token] │ │ │ └── page.jsx │ ├── dmca │ │ └── page.jsx │ └── layout.js ├── assets │ ├── chibi.png │ ├── hayasaka.png │ ├── hayasaka6.png │ ├── logo-white.png │ ├── logoWhite.png │ └── hayasaka.svg ├── services │ ├── apiConnector.js │ ├── playlistApi.js │ └── dataAPI.js ├── redux │ ├── Providers.jsx │ ├── features │ │ ├── languagesSlice.js │ │ ├── loadingBarSlice.js │ │ └── playerSlice.js │ └── store.js ├── models │ ├── Playlist.js │ ├── UserData.js │ └── User.js ├── components │ ├── topProgressBar │ │ └── TopProgressBar.jsx │ ├── PlayPause.jsx │ ├── SongListSkeleton.jsx │ ├── Sidebar │ │ ├── Favourites.jsx │ │ ├── Sidebar.jsx │ │ ├── Profile.jsx │ │ ├── Languages.jsx │ │ ├── PlaylistModal.jsx │ │ └── Playlists.jsx │ ├── Homepage │ │ ├── SongCardSkeleton.jsx │ │ ├── OnlineStatus.jsx │ │ ├── ListenAgain.jsx │ │ ├── Swiper.jsx │ │ ├── SongBar.jsx │ │ ├── Home.jsx │ │ └── SongCard.jsx │ ├── PlayButton.jsx │ ├── MusicPlayer │ │ ├── FavouriteButton.jsx │ │ ├── Downloader.jsx │ │ ├── Track.jsx │ │ ├── Seekbar.jsx │ │ ├── FullscreenTrack.jsx │ │ ├── Player.jsx │ │ ├── Controls.jsx │ │ ├── VolumeBar.jsx │ │ └── Lyrics.jsx │ ├── SongsHistory.jsx │ ├── Searchbar.jsx │ ├── Navbar.jsx │ ├── ListenAgainCard.jsx │ └── SongsList.jsx └── utils │ ├── auth.js │ ├── dbconnect.js │ └── mailSender.js ├── public ├── icon-192x192.png ├── icon-256x256.png ├── icon-384x384.png ├── icon-512x512.png ├── vercel.svg ├── manifest.json └── next.svg ├── jsconfig.json ├── postcss.config.js ├── .vscode └── settings.json ├── next.config.js ├── tailwind.config.js ├── .env.example ├── .gitignore ├── LICENSE ├── package.json └── README.md /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/assets/chibi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/src/assets/chibi.png -------------------------------------------------------------------------------- /public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/public/icon-192x192.png -------------------------------------------------------------------------------- /public/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/public/icon-256x256.png -------------------------------------------------------------------------------- /public/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/public/icon-384x384.png -------------------------------------------------------------------------------- /public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/public/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/hayasaka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/src/assets/hayasaka.png -------------------------------------------------------------------------------- /src/assets/hayasaka6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/src/assets/hayasaka6.png -------------------------------------------------------------------------------- /src/assets/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/src/assets/logo-white.png -------------------------------------------------------------------------------- /src/assets/logoWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/himanshu8443/hayasaka/HEAD/src/assets/logoWhite.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "cSpell.words": ["Favourite", "showtip", "Showtip", "toturial", "Toturial"] 4 | } 5 | -------------------------------------------------------------------------------- /src/app/album/[albumId]/loading.jsx: -------------------------------------------------------------------------------- 1 | 2 | const loading = () => { 3 | return ( 4 |
5 | 6 |
7 | ) 8 | } 9 | 10 | export default loading -------------------------------------------------------------------------------- /src/app/favourite/loading.jsx: -------------------------------------------------------------------------------- 1 | 2 | const loading = () => { 3 | return ( 4 |
5 | 6 |
7 | ) 8 | } 9 | 10 | export default loading -------------------------------------------------------------------------------- /src/app/login/loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const loading = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default loading -------------------------------------------------------------------------------- /src/app/signup/loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const loading = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default loading -------------------------------------------------------------------------------- /src/app/search/[query]/loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const loading = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default loading -------------------------------------------------------------------------------- /src/app/artist/[artistId]/loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const loading = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default loading -------------------------------------------------------------------------------- /src/app/AuthProvider.js: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import {SessionProvider} from 'next-auth/react' 4 | 5 | const AuthProvider = ({children}) => { 6 | return ( 7 | 8 | {children} 9 | 10 | ) 11 | } 12 | 13 | export default AuthProvider -------------------------------------------------------------------------------- /src/app/error.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | 4 | const error = () => { 5 | return ( 6 |
7 |

Error Occured Try Again

8 |
9 | ) 10 | } 11 | 12 | export default error -------------------------------------------------------------------------------- /src/app/not-found.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const notfound = () => { 4 | return ( 5 |
6 |

404

7 |

Page Not Found

8 |
9 | ) 10 | } 11 | 12 | export default notfound -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["c.saavncdn.com", "static.saavncdn.com", "www.jiosaavn.com"], 5 | }, 6 | }; 7 | 8 | const withPWA = require("@ducanh2912/next-pwa").default({ 9 | dest: "public", 10 | disable: process.env.NODE_ENV === "development", 11 | }); 12 | 13 | module.exports = withPWA(nextConfig); 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | screens: { 10 | sm: '640px', 11 | md: '768px', 12 | lg: '1024px', 13 | }, 14 | }, 15 | plugins: [], 16 | } 17 | -------------------------------------------------------------------------------- /src/services/apiConnector.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const axiosInstance = axios.create({}); 4 | 5 | 6 | export const apiConnector = (method, url, bodyData, headers, params) => { 7 | return axiosInstance({ 8 | method: method, 9 | url: url, 10 | data: bodyData?bodyData:null, 11 | headers: headers?headers:null, 12 | params: params?params:null 13 | }) 14 | }; -------------------------------------------------------------------------------- /src/redux/Providers.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Provider } from "react-redux"; 4 | import { store, persistor } from "./store"; 5 | import { PersistGate } from "redux-persist/integration/react"; 6 | 7 | export default function Providers({ children }) { 8 | return ( 9 | 10 | 11 | {children} 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URL = mongodb URL 2 | DB_NAME = database name 3 | 4 | 5 | JWT_SECRET = jwt secret 6 | NEXTAUTH_URL= next auth url (http://localhost:3000 or your domain) 7 | 8 | 9 | GOOGLE_CLIENT_ID = Google client id 10 | GOOGLE_CLIENT_SECRET = Google client secret 11 | 12 | 13 | MAIL_HOST = mail host (smtp.gmail.com) 14 | MAIL_USER = mail user 15 | MAIL_PASS = mail password 16 | 17 | NEXT_PUBLIC_SAAVN_API = "https://saavn.dev" # Saavn API URL create your own API from https://github.com/sumitkolhe/jiosaavn-api -------------------------------------------------------------------------------- /src/models/Playlist.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const fileSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true 8 | }, 9 | songs: { 10 | type: Array, 11 | default: [] 12 | }, 13 | user: { 14 | type: mongoose.Schema.Types.ObjectId, 15 | ref: "user" 16 | }, 17 | }, 18 | { timestamps: true } 19 | ); 20 | 21 | export default mongoose.models.playlist || mongoose.model("playlist", fileSchema, "playlist"); -------------------------------------------------------------------------------- /src/redux/features/languagesSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | languages: ['english'], 5 | }; 6 | 7 | const languagesSlice = createSlice({ 8 | name: "languages", 9 | initialState, 10 | reducers: { 11 | setLanguages: (state, action) => { 12 | state.languages = action.payload; 13 | }, 14 | }, 15 | }) 16 | 17 | export const { setLanguages } = languagesSlice.actions; 18 | export default languagesSlice.reducer; 19 | 20 | -------------------------------------------------------------------------------- /src/app/robots.js: -------------------------------------------------------------------------------- 1 | export default function robots() { 2 | const baseUrl = "https://hayasaka.8man.in"; 3 | 4 | return { 5 | rules: [ 6 | { 7 | userAgent: "*", 8 | allow: "/", 9 | disallow: ["/api/", "/myPlaylists/", "/reset-password/"], 10 | }, 11 | { 12 | userAgent: "Googlebot", 13 | allow: "/", 14 | disallow: ["/api/", "/myPlaylists/", "/reset-password/"], 15 | }, 16 | ], 17 | sitemap: `${baseUrl}/sitemap.xml`, 18 | host: baseUrl, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/redux/features/loadingBarSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | progress: 0, 5 | isTyping: false, 6 | }; 7 | 8 | const loadingBarSlice = createSlice({ 9 | name: "loadingBar", 10 | initialState, 11 | reducers: { 12 | setProgress: (state, action) => { 13 | state.progress = action.payload; 14 | }, 15 | setIsTyping: (state, action) => { 16 | state.isTyping = action.payload; 17 | } 18 | }, 19 | }); 20 | 21 | export const { setProgress, setIsTyping } = loadingBarSlice.actions; 22 | export default loadingBarSlice.reducer; -------------------------------------------------------------------------------- /src/components/topProgressBar/TopProgressBar.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { setProgress } from '@/redux/features/loadingBarSlice' 3 | import React from 'react' 4 | import LoadingBar from 'react-top-loading-bar' 5 | import { useDispatch, useSelector } from 'react-redux' 6 | 7 | 8 | const TopProgressBar = () => { 9 | const dispatch = useDispatch(); 10 | const progress= useSelector((state)=>state.loadingBar.progress) 11 | 12 | return ( 13 | dispatch(setProgress(0))} 18 | /> 19 | ) 20 | } 21 | 22 | export default TopProgressBar -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import { getToken } from "next-auth/jwt"; 2 | import User from "@/models/User"; 3 | import dbConnect from "@/utils/dbconnect"; 4 | 5 | 6 | // check if user is logged in 7 | export default async function auth(req) { 8 | const token = await getToken({ req, secret: process.env.JWT_SECRET }); 9 | if (!token) { 10 | return null 11 | } 12 | try { 13 | dbConnect(); 14 | const user = await User.findOne({ email: token.email }); 15 | if (!user) { 16 | return null 17 | } 18 | return user; 19 | } catch (error) { 20 | console.error(error); 21 | return null 22 | } 23 | } -------------------------------------------------------------------------------- /src/utils/dbconnect.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose" 2 | 3 | const MONGODB_URL = process.env.MONGODB_URL; 4 | const DB_NAME = process.env.DB_NAME; 5 | 6 | if (!MONGODB_URL) { 7 | throw new Error( 8 | "Please define the MONGODB_URI environment variable inside .env.local" 9 | ) 10 | } 11 | 12 | 13 | const dbConnect = async () => { 14 | if (mongoose.connection.readyState >= 1) { 15 | return 16 | } 17 | return mongoose.connect(MONGODB_URL, { 18 | dbName: DB_NAME, 19 | useNewUrlParser: true, 20 | useUnifiedTopology: true, 21 | }).then(()=>console.log("connected to db")).catch((err)=>console.log(err)); 22 | } 23 | 24 | export default dbConnect; -------------------------------------------------------------------------------- /src/models/UserData.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const fileSchema = new mongoose.Schema( 4 | { 5 | favourites: { 6 | type: Array, 7 | default: [] 8 | }, 9 | songHistory: { 10 | type: Array, 11 | default: [] 12 | }, 13 | playlists: [ 14 | { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: "playlist" 17 | } 18 | ], 19 | language: { 20 | type: Array, 21 | default: [] 22 | }, 23 | }, 24 | { timestamps: true } 25 | ); 26 | 27 | export default mongoose.models.userData || mongoose.model("userData", fileSchema, "userData"); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # Auto Generated PWA files 39 | **/public/sw.js 40 | **/public/workbox-*.js 41 | **/public/worker-*.js 42 | **/public/sw.js.map 43 | **/public/workbox-*.js.map 44 | **/public/worker-*.js.map 45 | 46 | -------------------------------------------------------------------------------- /src/components/PlayPause.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FaPauseCircle, FaPlayCircle } from 'react-icons/fa'; 3 | 4 | const PlayPause = ({ isPlaying, activeSong, song, handlePause, handlePlay, loading }) => ( 5 | loading ? ( 6 |
7 | ) : 8 | isPlaying && activeSong?.name === song.name ? ( 9 | 14 | ) : ( 15 | 20 | )); 21 | 22 | export default PlayPause; 23 | -------------------------------------------------------------------------------- /src/components/SongListSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SongListSkeleton = () => { 4 | return ( 5 | Array(7).fill().map((_,index) =>( 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | )) 15 | ) 16 | } 17 | 18 | export default SongListSkeleton -------------------------------------------------------------------------------- /src/components/Sidebar/Favourites.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | import { AiFillHeart } from "react-icons/ai"; 4 | 5 | const Favourites = ({ setShowNav }) => { 6 | return ( 7 |
8 | setShowNav(false)} 12 | > 13 |

14 | Favourites 15 |

16 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Favourites; 28 | -------------------------------------------------------------------------------- /src/components/Homepage/SongCardSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SongCardSkeleton = () => { 4 | return ( 5 |
{ 6 | Array(6).fill().map((_, i) => ( 7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | )) 19 | }
20 | ) 21 | } 22 | 23 | export default SongCardSkeleton -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme_color": "#000", 3 | "background_color": "#000", 4 | "display": "standalone", 5 | "scope": "/", 6 | "start_url": "/", 7 | "name": "Hayasaka - Free Music Streaming & Download", 8 | "description": "Stream and download your favorite songs for free. Listen to English, Hindi, Punjabi music and create playlists.", 9 | "short_name": "Hayasaka", 10 | "categories": ["music", "entertainment"], 11 | "icons": [ 12 | { 13 | "src": "/icon-192x192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "/icon-256x256.png", 19 | "sizes": "256x256", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "/icon-384x384.png", 24 | "sizes": "384x384", 25 | "type": "image/png" 26 | }, 27 | { 28 | "src": "/icon-512x512.png", 29 | "sizes": "512x512", 30 | "type": "image/png" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/mailSender.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require("nodemailer"); 2 | 3 | 4 | const mailSender = async (email, title, body) => { 5 | try{ 6 | let transporter = nodemailer.createTransport({ 7 | host:process.env.MAIL_HOST, 8 | port: 587, 9 | auth:{ 10 | user: process.env.MAIL_USER, 11 | pass: process.env.MAIL_PASS, 12 | } 13 | }) 14 | 15 | 16 | let info = await transporter.sendMail({ 17 | from: `"Hayasaka" <${process.env.MAIL_USER}>`, 18 | to:`${email}`, 19 | subject: `${title}`, 20 | html: `${body}`, 21 | }) 22 | console.log(info); 23 | return info; 24 | } 25 | catch(error) { 26 | console.log(error.message); 27 | return error; 28 | } 29 | } 30 | 31 | export default mailSender; 32 | 33 | -------------------------------------------------------------------------------- /src/components/PlayButton.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import { useDispatch } from 'react-redux'; 4 | import { BsFillPlayFill } from 'react-icons/bs'; 5 | 6 | import { playPause, setActiveSong } from "@/redux/features/playerSlice"; 7 | const PlayButton = ({ songList }) => { 8 | const dispatch = useDispatch(); 9 | const handlePlayClick = (song, index) => { 10 | dispatch(setActiveSong({ song, data: songList?.songs, i: index })); 11 | dispatch(playPause(true)); 12 | }; 13 | return ( 14 |
{ handlePlayClick(songList?.songs?.[0], 0); }} 16 | className="flex items-center gap-2 mt-5 rounded-3xl py-2 px-3 hover:border-[#00e6e6] group w-fit cursor-pointer border border-white"> 17 | 18 |

Play

19 |
20 | ) 21 | } 22 | 23 | export default PlayButton -------------------------------------------------------------------------------- /src/components/MusicPlayer/FavouriteButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {AiFillHeart, AiOutlineHeart} from 'react-icons/ai'; 3 | 4 | 5 | const FavouriteButton = ({favouriteSongs, activeSong, loading, handleAddToFavourite, style}) => { 6 | return ( 7 |
e.stopPropagation()} className=' mt-2'> 8 | { favouriteSongs?.length>0 && favouriteSongs?.includes(activeSong.id) ? 9 | 13 | : 14 | 18 | } 19 |
20 | ) 21 | } 22 | 23 | export default FavouriteButton -------------------------------------------------------------------------------- /src/models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const fileSchema = new mongoose.Schema( 4 | { 5 | userName: { 6 | type: String, 7 | required: true, 8 | }, 9 | email: { 10 | type: String, 11 | required: true, 12 | }, 13 | password: { 14 | type: String, 15 | }, 16 | imageUrl: { 17 | type: String, 18 | required: true, 19 | }, 20 | resetPasswordToken: { 21 | type: String, 22 | default: null, 23 | }, 24 | resetPasswordExpires: { 25 | type: Date, 26 | default: null, 27 | }, 28 | isVerified: { 29 | type: Boolean, 30 | default: false, 31 | }, 32 | verificationToken: { 33 | type: String, 34 | default: null, 35 | }, 36 | verificationTokenExpires: { 37 | type: Date, 38 | default: null, 39 | }, 40 | userData: { 41 | type: mongoose.Schema.Types.ObjectId, 42 | ref: "userData", 43 | }, 44 | }, 45 | { timestamps: true } 46 | ); 47 | 48 | export default mongoose.models.user || 49 | mongoose.model("user", fileSchema, "user"); 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 8man 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/SongsHistory.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useEffect } from 'react'; 3 | import { useSelector } from 'react-redux'; 4 | const SongsHistory = () => { 5 | const { activeSong } = useSelector((state) => state.player); 6 | 7 | useEffect(() => { 8 | if (activeSong?.name) { 9 | const storedSongHistory = localStorage?.getItem("songHistory"); 10 | const parsedSongHistory = storedSongHistory ? JSON.parse(storedSongHistory) : []; 11 | if(parsedSongHistory?.find((song) => song?.id === activeSong?.id)){ 12 | const updatedHistory = parsedSongHistory.filter((song) => song?.id !== activeSong?.id); 13 | const songHistory = [activeSong, ...updatedHistory]; 14 | localStorage.setItem("songHistory", JSON.stringify(songHistory)); 15 | }else{ 16 | if(parsedSongHistory?.length > 8) parsedSongHistory?.pop(); 17 | const songHistory = [activeSong,...parsedSongHistory]; 18 | localStorage.setItem("songHistory", JSON.stringify(songHistory)); 19 | } 20 | } 21 | }, [activeSong]); 22 | 23 | return 24 | } 25 | 26 | export default SongsHistory -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hayasaka", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ducanh2912/next-pwa": "^10.2.6", 13 | "@reduxjs/toolkit": "^1.9.5", 14 | "autoprefixer": "10.4.14", 15 | "axios": "^1.4.0", 16 | "bcryptjs": "^2.4.3", 17 | "eslint-config-next": "^14.2.1", 18 | "extract-colors": "^4.0.2", 19 | "get-pixels": "^3.3.3", 20 | "jsonwebtoken": "^9.0.1", 21 | "mongoose": "^7.4.1", 22 | "next": "^14.2.1", 23 | "next-auth": "^4.22.3", 24 | "nodemailer": "^7.0.11", 25 | "postcss": "^8.4.38", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-hot-toast": "^2.4.1", 29 | "react-icons": "^4.10.1", 30 | "react-redux": "^8.1.1", 31 | "react-swipeable": "^7.0.1", 32 | "react-top-loading-bar": "^2.3.1", 33 | "react-use-downloader": "^1.2.4", 34 | "redux": "^4.2.1", 35 | "redux-persist": "^6.0.0", 36 | "swiper": "^10.0.4", 37 | "tailwindcss": "3.3.3", 38 | "uuid": "^9.0.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Homepage/OnlineStatus.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { RiWifiOffLine } from 'react-icons/ri' 3 | import { useEffect, useState } from 'react' 4 | 5 | 6 | 7 | 8 | const OnlineStatus = () => { 9 | const [onLineStatus, setOnLineStatus] = useState(true); 10 | useEffect(() => { 11 | const checkOnline = () => { 12 | if (!navigator.onLine) { 13 | setOnLineStatus(false); 14 | } 15 | else { 16 | setOnLineStatus(true); 17 | } 18 | } 19 | checkOnline(); 20 | }, []); 21 | return ( 22 |
23 | { 24 | !onLineStatus && ( 25 |
26 | Please check your internet connection. 27 | 30 |
31 | ) 32 | } 33 |
34 | ) 35 | } 36 | 37 | export default OnlineStatus -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import { persistReducer, persistStore } from "redux-persist"; 3 | import createWebStorage from "redux-persist/lib/storage/createWebStorage"; 4 | import thunk from "redux-thunk"; 5 | 6 | import playerReducer from "./features/playerSlice"; 7 | import loadingBarReducer from "./features/loadingBarSlice"; 8 | import languagesReducer from "./features/languagesSlice"; 9 | 10 | const createNoopStorage = () => { 11 | return { 12 | getItem(_key) { 13 | return Promise.resolve(null); 14 | }, 15 | setItem(_key, value) { 16 | return Promise.resolve(value); 17 | }, 18 | removeItem(_key) { 19 | return Promise.resolve(); 20 | }, 21 | }; 22 | }; 23 | const storage = 24 | typeof window !== "undefined" 25 | ? createWebStorage("local") 26 | : createNoopStorage(); 27 | 28 | const persistConfig = { 29 | key: "root", 30 | storage, 31 | whitelist: ["languages"], 32 | }; 33 | 34 | const languagePersistedReducer = persistReducer( 35 | persistConfig, 36 | languagesReducer 37 | ); 38 | 39 | export const store = configureStore({ 40 | reducer: { 41 | player: playerReducer, 42 | loadingBar: loadingBarReducer, 43 | languages: languagePersistedReducer, 44 | }, 45 | middleware: [thunk], 46 | }); 47 | 48 | export const persistor = persistStore(store); 49 | -------------------------------------------------------------------------------- /src/app/search/[query]/layout.js: -------------------------------------------------------------------------------- 1 | const siteUrl = "https://hayasaka.8man.in"; 2 | 3 | export async function generateMetadata({ params }) { 4 | const query = decodeURIComponent(params.query); 5 | 6 | return { 7 | title: `${query} - Download & Listen Free`, 8 | description: `Listen to ${query} songs online for free. Download ${query} mp3 songs, albums, and playlists. Stream high quality music on Hayasaka.`, 9 | keywords: [ 10 | query, 11 | `${query} songs`, 12 | `${query} download`, 13 | `${query} mp3`, 14 | `listen ${query} online`, 15 | `${query} music`, 16 | "free music download", 17 | "stream songs", 18 | ], 19 | openGraph: { 20 | title: `${query} - Download & Listen Free | Hayasaka`, 21 | description: `Listen to ${query} songs online for free. Download mp3 songs and stream high quality music.`, 22 | url: `${siteUrl}/search/${params.query}`, 23 | siteName: "Hayasaka", 24 | type: "website", 25 | }, 26 | twitter: { 27 | card: "summary", 28 | title: `${query} - Download & Listen Free | Hayasaka`, 29 | description: `Listen to ${query} songs online for free. Download mp3 songs and stream high quality music.`, 30 | }, 31 | alternates: { 32 | canonical: `${siteUrl}/search/${params.query}`, 33 | }, 34 | }; 35 | } 36 | 37 | export default function SearchLayout({ children }) { 38 | return <>{children}; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/MusicPlayer/Downloader.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { MdOutlineFileDownload } from "react-icons/md"; 4 | import useDownloader from "react-use-downloader"; 5 | import { MdDownloadForOffline } from "react-icons/md"; 6 | 7 | const Downloader = ({ activeSong, icon }) => { 8 | const { size, elapsed, percentage, download, error, isInProgress } = 9 | useDownloader(); 10 | 11 | const songUrl = activeSong?.downloadUrl?.[4]?.url; 12 | const filename = `${activeSong?.name 13 | ?.replace("'", "'") 14 | ?.replace("&", "&")}.mp3`; 15 | 16 | return ( 17 |
{ 19 | e.stopPropagation(); 20 | download(songUrl, filename); 21 | }} 22 | className={`flex mb-1 cursor-pointer w-7`} 23 | > 24 |
30 | {isInProgress ? ( 31 |
32 | {percentage} 33 |
34 | ) : icon === 2 ? ( 35 | 36 | ) : ( 37 | 38 | )} 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default Downloader; 45 | -------------------------------------------------------------------------------- /src/components/MusicPlayer/Track.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | const Track = ({ isPlaying, isActive, activeSong, fullScreen }) => ( 5 |
10 | 24 |
25 |

26 | {activeSong?.name 27 | ? activeSong?.name.replace("'", "'").replace("&", "&") 28 | : "Song"} 29 |

30 |

31 | {activeSong?.artists?.primary 32 | ? activeSong?.artists?.primary?.map((artist, index) => ( 33 | 34 | {artist?.name?.trim()} 35 | 36 | )) 37 | : "Artist"} 38 |

39 |
40 |
41 | ); 42 | 43 | export default Track; 44 | -------------------------------------------------------------------------------- /src/components/MusicPlayer/Seekbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GiFastBackwardButton, GiFastForwardButton } from 'react-icons/gi'; 3 | 4 | const Seekbar = ({ value, min, max, onInput, setSeekTime, appTime, fullScreen }) => { 5 | // converts the time to format 0:00 6 | const getTime = (time) => `${Math.floor(time / 60)}:${(`0${Math.floor(time % 60)}`).slice(-2)}`; 7 | 8 | return ( 9 |
10 | 13 |

{value === 0 ? '0:00' : getTime(value)}

14 | {event.stopPropagation();}} 15 | type="range" 16 | step="any" 17 | value={value} 18 | min={min} 19 | max={max} 20 | onInput={onInput} 21 | className="md:block w-[70vw] md:w-[650px] 2xl:w-[50vw] h-1 mx-4 2xl:mx-6 rounded-lg accent-[#00e6e6] cursor-pointer" 22 | /> 23 |

{max === 0 ? '0:00' : getTime(max)}

24 | 27 |
28 | ); 29 | }; 30 | 31 | export default Seekbar; 32 | -------------------------------------------------------------------------------- /src/components/Homepage/ListenAgain.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import ListenAgainCard from "../ListenAgainCard"; 3 | import { useLayoutEffect, useState } from "react"; 4 | import { useDispatch } from "react-redux"; 5 | import { setAutoAdd } from "@/redux/features/playerSlice"; 6 | import { setLanguages } from "@/redux/features/languagesSlice"; 7 | 8 | const ListenAgain = () => { 9 | const [songHistory, setSongHistory] = useState([]); 10 | const dispatch = useDispatch(); 11 | useLayoutEffect(() => { 12 | setSongHistory( 13 | localStorage?.getItem("songHistory") 14 | ? JSON.parse(localStorage.getItem("songHistory")) 15 | : [] 16 | ); 17 | dispatch( 18 | setAutoAdd( 19 | localStorage?.getItem("autoAdd") 20 | ? JSON.parse(localStorage.getItem("autoAdd")) 21 | : false 22 | ) 23 | ); 24 | }, []); 25 | 26 | return ( 27 |
28 | {/* Listen Again */} 29 | {songHistory?.length > 0 && ( 30 |
31 |

32 | Listen Again 33 |

34 |
35 | {songHistory?.map((song, index) => ( 36 | 42 | ))} 43 |
44 |
45 | )} 46 |
47 | ); 48 | }; 49 | 50 | export default ListenAgain; 51 | -------------------------------------------------------------------------------- /src/app/favourite/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import SongsList from '@/components/SongsList'; 3 | import { getFavourite, getSongData } from '@/services/dataAPI'; 4 | import React, { useEffect } from 'react' 5 | import { useState } from 'react'; 6 | import { useSession } from 'next-auth/react'; 7 | import { redirect } from 'next/navigation'; 8 | 9 | const page = () => { 10 | const [favouriteSongs, setFavouriteSongs] = useState([]); 11 | const [loading, setLoading] = useState(true); 12 | const { status } = useSession(); 13 | 14 | useEffect(() => { 15 | const fetchFavorites = async () => { 16 | setLoading(true); 17 | const res = await getFavourite(); 18 | if (res?.length > 0) { 19 | const favorites = await getSongData(res); 20 | setFavouriteSongs(favorites?.reverse()); 21 | } 22 | setLoading(false); 23 | } 24 | fetchFavorites(); 25 | }, []); 26 | 27 | // redirect if user is authenticated 28 | if (status === 'loading') { 29 | return
30 | 31 |
32 | } 33 | if (status === 'unauthenticated') { 34 | redirect('/login'); 35 | } 36 | return ( 37 |
38 |

Favourites

39 |

Songs

40 | {favouriteSongs?.length <= 0 && loading === false ? 41 |

No Favourite Songs

42 | : 43 | 44 | } 45 |
46 | ) 47 | } 48 | 49 | export default page -------------------------------------------------------------------------------- /src/app/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Homepage from '@/components/Homepage/Home' 3 | import { useEffect, useLayoutEffect } from 'react' 4 | import { useState } from 'react' 5 | 6 | export default function Home() { 7 | const [showtip, setShowtip] = useState(false); 8 | const [toturialComplete, setToturialComplete] = useState(false); 9 | 10 | useLayoutEffect(() => { 11 | setToturialComplete(JSON.parse(localStorage.getItem('toturialComplete'))); 12 | 13 | setTimeout(() => { 14 | if (!toturialComplete) { 15 | setShowtip(true); 16 | } 17 | }, 5000); 18 | }, []) 19 | useEffect(() => { 20 | 21 | }, [toturialComplete]) 22 | 23 | const handleClick = () => { 24 | setShowtip(false); 25 | setToturialComplete(true); 26 | localStorage.setItem('toturialComplete', true); 27 | } 28 | return ( 29 |
30 | {showtip && !toturialComplete && 31 |
32 |
33 |

34 | Create your own Playlists
and 35 | add songs to Favourite 36 |

37 |
38 | 39 |
40 |
41 |
42 |
} 43 |
44 | 45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/redux/features/playerSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | currentSongs: [], 5 | currentIndex: 0, 6 | isActive: false, 7 | isPlaying: false, 8 | activeSong: {}, 9 | fullScreen: false, 10 | autoAdd: false, 11 | }; 12 | 13 | const playerSlice = createSlice({ 14 | name: 'player', 15 | initialState, 16 | reducers: { 17 | setActiveSong: (state, action) => { 18 | if(action.payload.song){ 19 | state.activeSong = action.payload.song; 20 | } 21 | 22 | if(action.payload.data){ 23 | state.currentSongs = action.payload.data; 24 | } 25 | 26 | if(action.payload.i){ 27 | state.currentIndex = action.payload.i; 28 | } 29 | state.isActive = true; 30 | }, 31 | 32 | nextSong: (state, action) => { 33 | 34 | if(state.currentSongs.length>0){ 35 | state.activeSong = state.currentSongs[action.payload]; 36 | 37 | state.currentIndex = action.payload; 38 | state.isActive = true; 39 | } 40 | }, 41 | 42 | prevSong: (state, action) => { 43 | 44 | if(state.currentSongs.length>0){ 45 | state.activeSong = state.currentSongs[action.payload]; 46 | state.currentIndex = action.payload; 47 | state.isActive = true; 48 | } 49 | }, 50 | 51 | playPause: (state, action) => { 52 | state.isPlaying = action.payload; 53 | }, 54 | 55 | setFullScreen: (state, action) => { 56 | state.fullScreen = action.payload; 57 | }, 58 | 59 | setAutoAdd: (state, action) => { 60 | state.autoAdd = action.payload; 61 | } 62 | 63 | }, 64 | }); 65 | 66 | export const { setActiveSong, nextSong, prevSong, playPause, setFullScreen, setAutoAdd } = playerSlice.actions; 67 | 68 | export default playerSlice.reducer; 69 | -------------------------------------------------------------------------------- /src/app/api/userInfo/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getToken } from "next-auth/jwt"; 3 | import User from "@/models/User"; 4 | import dbConnect from "@/utils/dbconnect"; 5 | 6 | 7 | // Get user info 8 | export async function GET(req){ 9 | const token = await getToken({ req, secret: process.env.JWT_SECRET }); 10 | if (!token) { 11 | return NextResponse.json( 12 | { 13 | success: false, 14 | message: "User not logged in", 15 | data: null 16 | }, 17 | { status: 401 } 18 | ); 19 | } 20 | try { 21 | await dbConnect(); 22 | // console.log('token', token); 23 | const user = await User.findOne({ email: token.email }); 24 | if (!user) { 25 | return NextResponse.json( 26 | { 27 | success: false, 28 | message: "User not found", 29 | data: null 30 | }, 31 | { status: 404 } 32 | ); 33 | } 34 | return NextResponse.json( 35 | { 36 | success: true, 37 | message: "User found", 38 | data: { 39 | userName: user.userName, 40 | email: user.email, 41 | imageUrl: user.imageUrl, 42 | isVerified: user.isVerified 43 | } 44 | } 45 | ); 46 | 47 | 48 | } catch (e) { 49 | console.error(e); 50 | return NextResponse.json( 51 | { 52 | success: false, 53 | message: "Something went wrong", 54 | data: null 55 | }, 56 | { status: 500 } 57 | ); 58 | } 59 | } -------------------------------------------------------------------------------- /src/components/Searchbar.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState } from 'react'; 3 | import { FiSearch } from 'react-icons/fi'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useDispatch } from 'react-redux'; 6 | import { setIsTyping } from '@/redux/features/loadingBarSlice'; 7 | 8 | 9 | const Searchbar = () => { 10 | const ref = React.useRef(null); 11 | const dispatch = useDispatch(); 12 | const router = useRouter(); 13 | const [searchTerm, setSearchTerm] = useState(''); 14 | 15 | const handleSubmit = (e) => { 16 | if (searchTerm === '') { 17 | e.preventDefault(); 18 | return; 19 | } 20 | e.preventDefault(); 21 | router.push(`/search/${searchTerm}`); 22 | }; 23 | const handleFocus = () => { 24 | dispatch(setIsTyping(true)); 25 | }; 26 | const handleBlur = () => { 27 | dispatch(setIsTyping(false)); 28 | }; 29 | 30 | return ( 31 |
32 | 35 |
36 |
50 |
51 | ); 52 | }; 53 | 54 | export default Searchbar; -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import logo from '../assets/hayasaka.png' 4 | import Image from 'next/image' 5 | import Searchbar from './Searchbar' 6 | import Link from 'next/link' 7 | import { useDispatch } from 'react-redux' 8 | import { setProgress } from '@/redux/features/loadingBarSlice' 9 | import { MdOutlineMenu } from 'react-icons/md' 10 | import { IoClose } from 'react-icons/io5' 11 | import Sidebar from './Sidebar/Sidebar' 12 | 13 | 14 | const Navbar = () => { 15 | const dispatch = useDispatch(); 16 | const [showNav, setShowNav] = React.useState(false); 17 | return ( 18 | <> 19 |
20 |
21 | setShowNav(true) 23 | } className=' mx-4 text-2xl lg:text-3xl my-auto cursor-pointer' /> 24 |
25 | 26 | { dispatch(setProgress(100)) }} 27 | src={logo} alt="logo" className=' lg:py-2 aspect-video w-[135px] h-[30.741px] lg:h-[58px] lg:w-[190px]' /> 28 | 29 |
30 |
31 | 32 |
33 | 34 | 35 | {/* overlay */} 36 |
setShowNav(false)} 37 | className={`${showNav ? '' : 'hidden'} transition-all duration-200 fixed top-0 left-0 z-30 w-screen h-screen bg-black bg-opacity-50`}>
38 |
setShowNav(false) 40 | } className={`${showNav ? '' : 'hidden'} md:hidden fixed top-7 right-10 z-50 text-3xl text-white`}> 41 | 42 |
43 | 44 | ) 45 | } 46 | 47 | export default Navbar -------------------------------------------------------------------------------- /src/app/api/signup/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import dbConnect from "@/utils/dbconnect"; 3 | import User from "@/models/User"; 4 | import bcrypt from "bcryptjs"; 5 | import UserData from "@/models/UserData"; 6 | 7 | export async function POST(request) { 8 | const {userName, email, password, imageUrl } = await request.json(); 9 | try { 10 | if (!userName || !email || !password || !imageUrl) { 11 | return NextResponse.json( 12 | { 13 | success: false, 14 | message: "Please fill all the fields", 15 | data: null 16 | }, 17 | { status: 400 } 18 | ); 19 | } 20 | await dbConnect(); 21 | 22 | const existingUser = await User.findOne({ email }); 23 | if (existingUser) { 24 | return NextResponse.json( 25 | { 26 | success: false, 27 | message: "User already exists", 28 | data: null 29 | }, 30 | { status: 400 } 31 | ); 32 | } 33 | 34 | const hashedPassword = await bcrypt.hash(password, 10); 35 | const userData = await UserData.create({}); 36 | const result = await User.create({ 37 | userName, 38 | email, 39 | password: hashedPassword, 40 | imageUrl, 41 | userData: userData._id 42 | }); 43 | return NextResponse.json( 44 | { 45 | success: true, 46 | message: "User created successfully", 47 | data: result 48 | } 49 | ); 50 | } catch (e) { 51 | console.error(e); 52 | return NextResponse.json( 53 | { 54 | success: false, 55 | message: "Something went wrong", 56 | data: null 57 | }, 58 | { status: 500 } 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/myPlaylists/[playlistId]/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import SongsList from '@/components/SongsList'; 3 | import {getSongData } from '@/services/dataAPI'; 4 | import React, { useEffect } from 'react' 5 | import { useState } from 'react'; 6 | import { useSession } from 'next-auth/react'; 7 | import { redirect } from 'next/navigation'; 8 | import { getSinglePlaylist } from '@/services/playlistApi'; 9 | 10 | const page = ({params}) => { 11 | const [loading, setLoading] = useState(true); 12 | const [songs, setSongs] = useState([]); 13 | const [playlist, setPlaylist] = useState({}); 14 | const { status } = useSession(); 15 | 16 | useEffect(() => { 17 | const fetchFavorites = async () => { 18 | setLoading(true); 19 | const res = await getSinglePlaylist(params.playlistId); 20 | if (res?.success === true) { 21 | setPlaylist(res?.data); 22 | if (res?.data?.songs?.length > 0) { 23 | const newSongs = await getSongData(res?.data?.songs); 24 | setSongs(newSongs?.reverse()); 25 | } 26 | } 27 | setLoading(false); 28 | } 29 | fetchFavorites(); 30 | }, []); 31 | 32 | // redirect if user is authenticated 33 | if (status === 'loading') { 34 | return
35 | 36 |
37 | } 38 | if (status === 'unauthenticated') { 39 | redirect('/login'); 40 | } 41 | return ( 42 |
43 |

{playlist?.name}

44 |

Songs

45 | {songs?.length === 0 && loading === false ? 46 |

Playlist is Empty

47 | : 48 | 49 | } 50 |
51 | ) 52 | } 53 | 54 | export default page -------------------------------------------------------------------------------- /src/app/playlist/[playlistId]/loading.jsx: -------------------------------------------------------------------------------- 1 | import SongListSkeleton from "@/components/SongListSkeleton"; 2 | import { BsFillPlayFill } from 'react-icons/bs' 3 | 4 | const page = async () => { 5 | return ( 6 |
7 |
8 |
9 |
10 | 13 |
14 |
15 | 16 |
17 |

{ }

18 |
    19 |
  • 20 |
  • 21 |
  • 22 |
23 |
{handlePlayClick(playlistData?.songs?.[0], 0);}} 25 | className="flex items-center gap-2 mt-5 rounded-3xl py-2 px-3 hover:border-[#00e6e6] group w-fit cursor-pointer border border-white"> 26 | 27 |

Play

28 |
29 |
30 |
31 |
32 |

Songs

33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default page; 40 | -------------------------------------------------------------------------------- /src/app/api/login/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import dbConnect from "@/utils/dbconnect"; 3 | import User from "@/models/User"; 4 | import bcrypt from "bcryptjs"; 5 | import Jwt from "jsonwebtoken"; 6 | 7 | 8 | export async function POST(request) { 9 | const { email, password } = await request.json(); 10 | try { 11 | if (!email || !password) { 12 | return NextResponse.json( 13 | { 14 | success: false, 15 | message: "Please fill all the fields", 16 | data: null 17 | }, 18 | { status: 400 } 19 | ); 20 | } 21 | await dbConnect(); 22 | 23 | const user = await User.findOne({ email }); 24 | if (!user) { 25 | return NextResponse.json( 26 | { 27 | success: false, 28 | message: "User does not exist", 29 | data: null 30 | }, 31 | { status: 400 } 32 | ); 33 | } 34 | const isPasswordCorrect = await bcrypt.compare(password, user.password); 35 | if (!isPasswordCorrect) { 36 | return NextResponse.json( 37 | { 38 | success: false, 39 | message: "Invalid credentials", 40 | data: null 41 | }, 42 | { status: 400 } 43 | ); 44 | } 45 | const token = Jwt.sign({ email: user.email, id: user._id }, process.env.JWT_SECRET, { expiresIn: "30d" }); 46 | const response = NextResponse.json( 47 | { 48 | success: true, 49 | message: "User logged in successfully", 50 | data: { token } 51 | } 52 | ); 53 | response.cookies.set("token", token, { 54 | httpOnly: true, 55 | maxAge: 30 * 24 * 60 * 60, 56 | }); 57 | return response; 58 | 59 | 60 | } catch (e) { 61 | console.error(e); 62 | return NextResponse.json( 63 | { 64 | success: false, 65 | message: "Something went wrong", 66 | data: null 67 | }, 68 | { status: 500 } 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/Homepage/Swiper.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Swiper, SwiperSlide } from "swiper/react"; 3 | import "swiper/css"; 4 | import { FreeMode, Navigation, Mousewheel, Keyboard } from "swiper/modules"; 5 | import { MdNavigateNext } from "react-icons/md"; 6 | import { MdNavigateBefore } from "react-icons/md"; 7 | import { useRef } from "react"; 8 | 9 | const SwiperLayout = ({ children, title }) => { 10 | const albumNext = useRef(null); 11 | const ablumPrv = useRef(null); 12 | 13 | return ( 14 |
15 |
16 |

17 | {title} 18 |

19 |
20 |
24 | 25 |
26 |
30 | 31 |
32 |
33 |
34 | { 52 | swiper.params.navigation.prevEl = ablumPrv.current; 53 | swiper.params.navigation.nextEl = albumNext.current; 54 | swiper.navigation.init(); 55 | swiper.navigation.update(); 56 | }} 57 | className="mySwiper [&_.swiper-slide]:w-[120px] [&_.swiper-slide]:sm:w-[150px] [&_.swiper-slide]:lg:w-[180px] [&_.swiper-slide]:xl:w-[220px]" 58 | > 59 | {children} 60 | 61 |
62 | ); 63 | }; 64 | 65 | export default SwiperLayout; 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Project Banner](https://github-production-user-asset-6210df.s3.amazonaws.com/99420590/256470702-de779111-e63e-4ecc-93d1-e79edadf19ed.png)](https://hayasaka.live/) 2 | 3 | This is a free music streaming web application built with Next.js and powered by the [savan API](https://github.com/sumitkolhe/jiosaavn-api). The app allows users to search and stream music from a vast collection of songs available on the Saavn platform. 4 | 5 | ## Features 6 | * Search and stream music from vast collection. 7 | * Play, pause, skip, and control the playback of songs. 8 | * Create your own playlists. 9 | * Add songs to your favorite. 10 | * Auto add similar songs to queue. 11 | * Display song details such as title, artist, album, and album artwork. 12 | * Responsive and mobile-friendly design for a great user experience. 13 | * Minimalistic and intuitive user interface. 14 | *** 15 | ![image](https://github.com/himanshu8443/hayasaka/assets/99420590/158bc035-463e-403b-a23a-db17b83ab7b0) 16 | ___ 17 | ![image](https://github.com/himanshu8443/hayasaka/assets/99420590/864aec2b-8d60-4278-a475-9f7ee6ae7680) 18 | 19 | 20 | *** 21 | ## Installation 22 | 23 | 1. Clone the repository to your local machine. 24 | ```sh 25 | git clone https://github.com/himanshu8443/hayasaka.git 26 | ``` 27 | 28 | 2. Install the required packages. 29 | ```sh 30 | cd hayasaka 31 | ``` 32 | ```sh 33 | npm install 34 | ``` 35 | 36 | 3. Set up the environment variables: 37 | Create env file in root dir. 38 | ``` 39 | MONGODB_URL = MongoDB connection string 40 | DB_NAME = database name 41 | 42 | JWT_SECRET = JWT secret 43 | NEXTAUTH_URL= next auth url (http://localhost:3000 or your domain) 44 | 45 | 46 | GOOGLE_CLIENT_ID = Google client id (https://analytify.io/get-google-client-id-and-client-secret) 47 | GOOGLE_CLIENT_SECRET = Google client secret 48 | 49 | 50 | MAIL_HOST = mail host (smtp.gmail.com) 51 | MAIL_USER = mail user (your gmail address) 52 | MAIL_PASS = mail password (google app password) 53 | 54 | NEXT_PUBLIC_SAAVN_API = "https://saavn.dev" # Saavn API URL create your own API from https://github.com/sumitkolhe/jiosaavn-api 55 | ``` 56 | 57 | 5. Start the development server. 58 | ```sh 59 | npm run dev 60 | ``` 61 | 62 | 6. Open the project in your browser at [`http://localhost:3000`](http://localhost:3000) to view your project. 63 | 64 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/app/playlist/[playlistId]/layout.js: -------------------------------------------------------------------------------- 1 | const siteUrl = "https://hayasaka.8man.in"; 2 | 3 | async function getPlaylistData(id) { 4 | try { 5 | const response = await fetch( 6 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/playlists?id=${id}&limit=50`, 7 | { next: { revalidate: 3600 } } 8 | ); 9 | const data = await response.json(); 10 | return data?.data; 11 | } catch (error) { 12 | console.log(error); 13 | return null; 14 | } 15 | } 16 | 17 | export async function generateMetadata({ params }) { 18 | const playlistData = await getPlaylistData(params.playlistId); 19 | 20 | if (!playlistData) { 21 | return { 22 | title: "Playlist", 23 | description: "Listen to this playlist on Hayasaka", 24 | }; 25 | } 26 | 27 | const playlistName = playlistData?.name || "Playlist"; 28 | const songCount = playlistData?.songCount || 0; 29 | const description = playlistData?.description || ""; 30 | 31 | return { 32 | title: `${playlistName} - Playlist`, 33 | description: `Listen to ${playlistName} playlist. ${songCount} songs. ${description}. Stream and download all songs for free on Hayasaka.`, 34 | keywords: [ 35 | playlistName, 36 | `${playlistName} playlist`, 37 | `${playlistName} songs`, 38 | "music playlist", 39 | "free playlist download", 40 | "stream playlist", 41 | ], 42 | openGraph: { 43 | title: `${playlistName} Playlist | Hayasaka`, 44 | description: `Listen to ${playlistName} playlist. ${songCount} songs. Stream and download for free.`, 45 | url: `${siteUrl}/playlist/${params.playlistId}`, 46 | siteName: "Hayasaka", 47 | type: "music.playlist", 48 | images: playlistData?.image?.[2]?.url 49 | ? [ 50 | { 51 | url: playlistData.image[2].url, 52 | width: 500, 53 | height: 500, 54 | alt: playlistName, 55 | }, 56 | ] 57 | : [], 58 | }, 59 | twitter: { 60 | card: "summary_large_image", 61 | title: `${playlistName} Playlist | Hayasaka`, 62 | description: `Listen to ${playlistName} playlist. ${songCount} songs. Stream and download for free.`, 63 | images: playlistData?.image?.[2]?.url ? [playlistData.image[2].url] : [], 64 | }, 65 | alternates: { 66 | canonical: `${siteUrl}/playlist/${params.playlistId}`, 67 | }, 68 | }; 69 | } 70 | 71 | export default function PlaylistLayout({ children }) { 72 | return <>{children}; 73 | } 74 | -------------------------------------------------------------------------------- /src/app/album/[albumId]/layout.js: -------------------------------------------------------------------------------- 1 | const siteUrl = "https://hayasaka.8man.in"; 2 | 3 | async function getAlbumData(id) { 4 | try { 5 | const response = await fetch( 6 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/albums?id=${id}`, 7 | { next: { revalidate: 3600 } } 8 | ); 9 | const data = await response.json(); 10 | return data?.data; 11 | } catch (error) { 12 | console.log(error); 13 | return null; 14 | } 15 | } 16 | 17 | export async function generateMetadata({ params }) { 18 | const albumData = await getAlbumData(params.albumId); 19 | 20 | if (!albumData) { 21 | return { 22 | title: "Album", 23 | description: "Listen to this album on Hayasaka", 24 | }; 25 | } 26 | 27 | const albumName = albumData?.name || "Album"; 28 | const artistNames = albumData?.primaryArtists || ""; 29 | const songCount = albumData?.songCount || 0; 30 | const year = albumData?.year || ""; 31 | 32 | return { 33 | title: `${albumName} - ${artistNames}`, 34 | description: `Listen to ${albumName} by ${artistNames}. ${songCount} songs. Download and stream ${albumName} album for free on Hayasaka. Released in ${year}.`, 35 | keywords: [ 36 | albumName, 37 | artistNames, 38 | `${albumName} album`, 39 | `${albumName} songs`, 40 | `${albumName} download`, 41 | `${artistNames} songs`, 42 | "album download", 43 | "free music", 44 | ], 45 | openGraph: { 46 | title: `${albumName} by ${artistNames} | Hayasaka`, 47 | description: `Listen to ${albumName} by ${artistNames}. ${songCount} songs. Stream and download for free.`, 48 | url: `${siteUrl}/album/${params.albumId}`, 49 | siteName: "Hayasaka", 50 | type: "music.album", 51 | images: albumData?.image?.[2]?.url 52 | ? [ 53 | { 54 | url: albumData.image[2].url, 55 | width: 500, 56 | height: 500, 57 | alt: albumName, 58 | }, 59 | ] 60 | : [], 61 | }, 62 | twitter: { 63 | card: "summary_large_image", 64 | title: `${albumName} by ${artistNames} | Hayasaka`, 65 | description: `Listen to ${albumName} by ${artistNames}. ${songCount} songs. Stream and download for free.`, 66 | images: albumData?.image?.[2]?.url ? [albumData.image[2].url] : [], 67 | }, 68 | alternates: { 69 | canonical: `${siteUrl}/album/${params.albumId}`, 70 | }, 71 | }; 72 | } 73 | 74 | export default function AlbumLayout({ children }) { 75 | return <>{children}; 76 | } 77 | -------------------------------------------------------------------------------- /src/components/ListenAgainCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BsPlayFill } from "react-icons/bs"; 3 | import { useDispatch } from "react-redux"; 4 | import { 5 | playPause, 6 | setActiveSong, 7 | setFullScreen, 8 | } from "@/redux/features/playerSlice"; 9 | import { BiHeadphone } from "react-icons/bi"; 10 | import { useSelector } from "react-redux"; 11 | 12 | const ListenAgainCard = ({ song, index, SongData }) => { 13 | const { activeSong } = useSelector((state) => state.player); 14 | const dispatch = useDispatch(); 15 | const handlePlayClick = (song, index) => { 16 | dispatch(setActiveSong({ song, data: SongData, i: index })); 17 | dispatch(setFullScreen(true)); 18 | dispatch(playPause(true)); 19 | }; 20 | return ( 21 |
22 |
{ 24 | handlePlayClick(song, index); 25 | }} 26 | className={`flex w-40 md:w-80 items-center mt-5 cursor-pointer group border-b-[1px] border-gray-400 justify-between ${ 27 | activeSong?.id === song?.id && " text-[#00e6e6]" 28 | }`} 29 | > 30 |
31 |
32 |
33 | {song?.name} 40 |
41 | {activeSong?.id === song?.id ? ( 42 | 46 | ) : ( 47 | 51 | )} 52 |
53 |
54 |

55 | {song?.name?.replace("'", "'")?.replace("&", "&")} 56 |

57 |

58 | {song?.artists?.primary?.map((artist) => artist?.name).join(", ")} 59 |

60 |
61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default ListenAgainCard; 68 | -------------------------------------------------------------------------------- /src/app/artist/[artistId]/layout.js: -------------------------------------------------------------------------------- 1 | const siteUrl = "https://hayasaka.8man.in"; 2 | 3 | async function getArtistData(id) { 4 | try { 5 | const response = await fetch( 6 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/artists?id=${id}`, 7 | { next: { revalidate: 3600 } } 8 | ); 9 | const data = await response.json(); 10 | return data?.data; 11 | } catch (error) { 12 | console.log(error); 13 | return null; 14 | } 15 | } 16 | 17 | export async function generateMetadata({ params }) { 18 | const artistData = await getArtistData(params.artistId); 19 | 20 | if (!artistData) { 21 | return { 22 | title: "Artist", 23 | description: "Listen to this artist on Hayasaka", 24 | }; 25 | } 26 | 27 | const artistName = artistData?.name || "Artist"; 28 | const fanCount = artistData?.fanCount 29 | ? `${(artistData.fanCount / 1000000).toFixed(1)}M fans` 30 | : ""; 31 | 32 | return { 33 | title: `${artistName} Songs - Download & Listen Free`, 34 | description: `Listen to ${artistName} songs online for free. ${fanCount}. Download ${artistName} mp3 songs, albums, and top hits. Stream all ${artistName} music on Hayasaka.`, 35 | keywords: [ 36 | artistName, 37 | `${artistName} songs`, 38 | `${artistName} mp3`, 39 | `${artistName} download`, 40 | `${artistName} albums`, 41 | `${artistName} hits`, 42 | `best of ${artistName}`, 43 | "artist songs download", 44 | ], 45 | openGraph: { 46 | title: `${artistName} - Songs, Albums & Music | Hayasaka`, 47 | description: `Listen to ${artistName} songs online for free. ${fanCount}. Stream all ${artistName} music and download mp3.`, 48 | url: `${siteUrl}/artist/${params.artistId}`, 49 | siteName: "Hayasaka", 50 | type: "profile", 51 | images: artistData?.image?.[2]?.url 52 | ? [ 53 | { 54 | url: artistData.image[2].url, 55 | width: 500, 56 | height: 500, 57 | alt: artistName, 58 | }, 59 | ] 60 | : [], 61 | }, 62 | twitter: { 63 | card: "summary_large_image", 64 | title: `${artistName} - Songs, Albums & Music | Hayasaka`, 65 | description: `Listen to ${artistName} songs online for free. Stream and download all ${artistName} music.`, 66 | images: artistData?.image?.[2]?.url ? [artistData.image[2].url] : [], 67 | }, 68 | alternates: { 69 | canonical: `${siteUrl}/artist/${params.artistId}`, 70 | }, 71 | }; 72 | } 73 | 74 | export default function ArtistLayout({ children }) { 75 | return <>{children}; 76 | } 77 | -------------------------------------------------------------------------------- /src/app/sitemap.js: -------------------------------------------------------------------------------- 1 | export default function sitemap() { 2 | const baseUrl = "https://hayasaka.8man.in"; 3 | 4 | // Static pages 5 | const staticPages = [ 6 | { 7 | url: baseUrl, 8 | lastModified: new Date(), 9 | changeFrequency: "daily", 10 | priority: 1, 11 | }, 12 | { 13 | url: `${baseUrl}/login`, 14 | lastModified: new Date(), 15 | changeFrequency: "monthly", 16 | priority: 0.5, 17 | }, 18 | { 19 | url: `${baseUrl}/signup`, 20 | lastModified: new Date(), 21 | changeFrequency: "monthly", 22 | priority: 0.5, 23 | }, 24 | { 25 | url: `${baseUrl}/favourite`, 26 | lastModified: new Date(), 27 | changeFrequency: "weekly", 28 | priority: 0.7, 29 | }, 30 | { 31 | url: `${baseUrl}/dmca`, 32 | lastModified: new Date(), 33 | changeFrequency: "yearly", 34 | priority: 0.3, 35 | }, 36 | ]; 37 | 38 | // Popular search terms for music - these help Google index search pages 39 | const popularSearchTerms = [ 40 | "arijit singh", 41 | "bollywood songs", 42 | "latest hindi songs", 43 | "punjabi songs", 44 | "English songs", 45 | "romantic songs", 46 | "sad songs", 47 | "party songs", 48 | "old hindi songs", 49 | "90s songs", 50 | "atif aslam", 51 | "shreya ghoshal", 52 | "neha kakkar", 53 | "honey singh", 54 | "badshah", 55 | "diljit dosanjh", 56 | "ap dhillon", 57 | "jubin nautiyal", 58 | "armaan malik", 59 | "kumar sanu", 60 | "kishore kumar", 61 | "lata mangeshkar", 62 | "mohammed rafi", 63 | "sonu nigam", 64 | "udit narayan", 65 | "alka yagnik", 66 | "sunidhi chauhan", 67 | "pritam", 68 | "ar rahman", 69 | "vishal shekhar", 70 | "amit trivedi", 71 | "sachin jigar", 72 | "tanishk bagchi", 73 | "b praak", 74 | "darshan raval", 75 | "aashiqui 2", 76 | "kabir singh", 77 | "animal movie songs", 78 | "pushpa songs", 79 | "rrr songs", 80 | "kgf songs", 81 | "love songs", 82 | "workout songs", 83 | "travel songs", 84 | "rain songs", 85 | "sufi songs", 86 | "devotional songs", 87 | "ghazals", 88 | "qawwali", 89 | "indie pop", 90 | "download songs", 91 | ]; 92 | 93 | const searchPages = popularSearchTerms.map((term) => ({ 94 | url: `${baseUrl}/search/${encodeURIComponent(term)}`, 95 | lastModified: new Date(), 96 | changeFrequency: "weekly", 97 | priority: 0.8, 98 | })); 99 | 100 | return [...staticPages, ...searchPages]; 101 | } 102 | -------------------------------------------------------------------------------- /src/components/Homepage/SongBar.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { FaPlayCircle } from "react-icons/fa"; 3 | import { useEffect, useState } from "react"; 4 | import getPixels from "get-pixels"; 5 | import { extractColors } from "extract-colors"; 6 | 7 | const SongBar = ({ playlist, i }) => { 8 | const [cardColor, setCardColor] = useState(); 9 | 10 | useEffect(() => { 11 | const src = playlist?.image?.[1]?.link; 12 | getPixels(src, (err, pixels) => { 13 | if (!err) { 14 | const data = [...pixels.data]; 15 | const width = Math.round(Math.sqrt(data.length / 4)); 16 | const height = width; 17 | 18 | extractColors({ data, width, height }) 19 | .then((colors) => { 20 | setCardColor(colors); 21 | }) 22 | .catch(console.log); 23 | } 24 | }); 25 | }, []); 26 | 27 | return ( 28 | 29 |
39 |

{i + 1}.

40 |
41 | song_img 51 |
52 |

53 | {playlist?.title} 54 |

55 |

56 | {playlist?.language} 57 |

58 |
59 |
60 | 64 |
65 | 66 | ); 67 | }; 68 | 69 | export default SongBar; 70 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.js: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import CredentialsProvider from "next-auth/providers/credentials"; 3 | import User from "@/models/User"; 4 | import bcrypt from "bcryptjs"; 5 | import dbConnect from "@/utils/dbconnect"; 6 | import GoogleProvider from "next-auth/providers/google"; 7 | import UserData from "@/models/UserData"; 8 | 9 | const options = { 10 | providers: [ 11 | GoogleProvider({ 12 | clientId: process.env.GOOGLE_CLIENT_ID, 13 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 14 | }), 15 | 16 | CredentialsProvider({ 17 | name: "Credentials", 18 | credentials: {}, 19 | async authorize(credentials) { 20 | try { 21 | await dbConnect(); 22 | console.log("credentials"); 23 | const { email, password } = credentials; 24 | const user = await User.findOne({ email }); 25 | if (user && (await bcrypt.compare(password, user.password))) { 26 | return user; 27 | } 28 | return null; 29 | } catch (e) { 30 | console.error(e); 31 | return null; 32 | } 33 | }, 34 | }), 35 | ], 36 | pages: { 37 | signIn: "/login", 38 | signOut: "/", 39 | error: "/", 40 | }, 41 | session: { 42 | jwt: true, 43 | maxAge: 30 * 24 * 60 * 60, 44 | }, 45 | secret: process.env.JWT_SECRET, 46 | 47 | callbacks: { 48 | // async session({ session}) { 49 | // const sessionUser = await User.findOne({ email: session.user.email }); 50 | // if (sessionUser) { 51 | // session.user.id = sessionUser._id; 52 | // session.userName = sessionUser.userName; 53 | // session.imageUrl = sessionUser.imageUrl; 54 | // session.isVerified = sessionUser.isVerified; 55 | // } 56 | // return session; 57 | // }, 58 | 59 | async signIn({ account, profile }) { 60 | if (account.provider === "google") { 61 | try { 62 | await dbConnect(); 63 | const { name, email, picture } = profile; 64 | const userDB = await User.findOne({ email }); 65 | if (!userDB) { 66 | const userData = await UserData.create({}); 67 | const newUser = await User.create({ 68 | userName: name, 69 | email, 70 | imageUrl: picture, 71 | userData: userData._id, 72 | isVerified: profile.email_verified, 73 | }); 74 | return newUser; 75 | } 76 | return userDB; 77 | } catch (e) { 78 | console.error(e); 79 | return null; 80 | } 81 | } 82 | return true; 83 | }, 84 | }, 85 | }; 86 | 87 | const handler = (req, res) => NextAuth(req, res, options); 88 | export { handler as GET, handler as POST }; 89 | -------------------------------------------------------------------------------- /src/components/MusicPlayer/FullscreenTrack.jsx: -------------------------------------------------------------------------------- 1 | import Lyrics from "./Lyrics"; 2 | import React from "react"; 3 | import Link from "next/link"; 4 | import { useDispatch } from "react-redux"; 5 | import { setFullScreen } from "@/redux/features/playerSlice"; 6 | import { useSwipeable } from "react-swipeable"; 7 | 8 | const FullscreenTrack = ({ 9 | fullScreen, 10 | activeSong, 11 | handlePrevSong, 12 | handleNextSong, 13 | }) => { 14 | const dispatch = useDispatch(); 15 | const handlers = useSwipeable({ 16 | onSwipedLeft: () => handleNextSong(), 17 | onSwipedRight: () => handlePrevSong(), 18 | onSwipedDown: () => dispatch(setFullScreen(false)), 19 | preventDefaultTouchmoveEvent: true, 20 | preventScrollOnSwipe: true, 21 | trackMouse: true, 22 | }); 23 | 24 | return ( 25 |
30 |
31 |
35 | cover art 40 |
41 |
e.stopPropagation()} 43 | className="w-fit select-none cursor-pointer text-center my-5 ml" 44 | > 45 |

46 | {activeSong?.name 47 | ? activeSong?.name.replace("'", "'").replace("&", "&") 48 | : "Song"} 49 |

50 |

51 | {activeSong?.artists?.primary 52 | ? activeSong?.artists.primary?.map((artist, index) => ( 53 | 54 | { 58 | dispatch(setFullScreen(false)); 59 | }} 60 | > 61 | {artist?.name?.trim()} 62 | 63 | 64 | )) 65 | : "Artist"} 66 |

67 |
68 |
69 |
e.stopPropagation()} 71 | className="flex-col items-center min-[1180px]:flex hidden" 72 | > 73 | 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default FullscreenTrack; 80 | -------------------------------------------------------------------------------- /src/app/playlist/[playlistId]/page.jsx: -------------------------------------------------------------------------------- 1 | import PlayButton from "@/components/PlayButton"; 2 | import SongList from "@/components/SongsList"; 3 | import { getplaylistData, homePageData } from "@/services/dataAPI"; 4 | 5 | const page = async ({ params }) => { 6 | // const [playlistData, setPlaylistData] = useState(null); 7 | // const [loading, setLoading] = useState(true); 8 | // const dispatch = useDispatch(); 9 | 10 | const playlistData = await getplaylistData(params.playlistId); 11 | 12 | return ( 13 |
14 |
15 | {false ? ( 16 |
20 |
21 | 30 |
31 |
32 | ) : ( 33 | {playlistData?.title} 40 | )} 41 | 42 |
43 |

44 | {playlistData?.name} 45 |

46 |
    47 |
  • 48 | {playlistData?.description} 49 |
  • 50 |
51 | 52 |
53 |
54 |
55 |

Songs

56 | 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default page; 63 | 64 | // 4 hour 65 | export const revalidate = 14400; 66 | 67 | export async function generateStaticParams() { 68 | const res = await homePageData(["english", "hindi", "punjabi"]); 69 | return res?.charts?.map((playlist) => ({ 70 | playlistId: playlist?.id.toString(), 71 | })); 72 | } 73 | -------------------------------------------------------------------------------- /src/app/reset-password/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { setProgress } from '@/redux/features/loadingBarSlice'; 3 | import { sendResetPasswordLink } from '@/services/dataAPI'; 4 | import React from 'react' 5 | import { toast } from 'react-hot-toast'; 6 | import { useDispatch } from 'react-redux'; 7 | 8 | 9 | const page = () => { 10 | const dispatch = useDispatch(); 11 | const [formData, setFormData] = React.useState({ 12 | email: '', 13 | }); 14 | const onchange = (e) => { 15 | setFormData({ ...formData, [e.target.name]: e.target.value }); 16 | }; 17 | const handelSubmit = async (e) => { 18 | e.preventDefault(); 19 | try{ 20 | dispatch(setProgress(70)); 21 | const res = await sendResetPasswordLink(formData.email); 22 | if (res.success === true) { 23 | toast.success('Password reset link sent to your email'); 24 | } 25 | else { 26 | toast.error(res.message); 27 | } 28 | } catch (error) { 29 | toast.error(error?.message || 'Something went wrong'); 30 | } finally { 31 | dispatch(setProgress(100)); 32 | } 33 | }; 34 | return ( 35 |
36 |
37 |
38 |

Forgot Password

39 |

Enter your email address and we will send you a link to reset your password.

40 |
41 |
42 | 43 | 44 |
45 |
46 | 51 |
52 |
53 |
54 |
55 | 56 |
) 57 | } 58 | 59 | export default page -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logoWhite from "../../assets/logoWhite.png"; 3 | import Languages from "./Languages"; 4 | import Favourites from "./Favourites"; 5 | import { FaGithub } from "react-icons/fa"; 6 | import { MdOutlineMenu } from "react-icons/md"; 7 | import Image from "next/image"; 8 | import Link from "next/link"; 9 | import Profile from "./Profile"; 10 | import { useDispatch } from "react-redux"; 11 | import Playlists from "./Playlists"; 12 | import { setProgress } from "@/redux/features/loadingBarSlice"; 13 | 14 | const Sidebar = ({ showNav, setShowNav }) => { 15 | const dispatch = useDispatch(); 16 | return ( 17 |
22 |
23 |
24 | setShowNav(false)} 26 | className=" mx-4 text-2xl lg:text-3xl my-auto text-white cursor-pointer" 27 | /> 28 |
29 | 30 | { 32 | dispatch(setProgress(100)); 33 | }} 34 | src={logoWhite} 35 | alt="logo" 36 | className=" lg:py-2 aspect-video w-[139px] h-[31px] lg:h-[62px] lg:w-[190px]" 37 | /> 38 | 39 |
40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 | 57 |

58 | DMCA 59 |

60 | 61 | 66 |

67 | 68 | Github 69 |

70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default Sidebar; 77 | -------------------------------------------------------------------------------- /src/components/MusicPlayer/Player.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | /* eslint-disable jsx-a11y/media-has-caption */ 3 | import React, { useRef, useEffect } from "react"; 4 | 5 | const Player = ({ 6 | activeSong, 7 | isPlaying, 8 | volume, 9 | seekTime, 10 | onEnded, 11 | onTimeUpdate, 12 | onLoadedData, 13 | repeat, 14 | handlePlayPause, 15 | handlePrevSong, 16 | handleNextSong, 17 | setSeekTime, 18 | appTime, 19 | }) => { 20 | const ref = useRef(null); 21 | // eslint-disable-next-line no-unused-expressions 22 | if (ref.current) { 23 | if (isPlaying) { 24 | ref.current.play(); 25 | } else { 26 | ref.current.pause(); 27 | } 28 | } 29 | 30 | // media session metadata: 31 | const mediaMetaData = activeSong.name 32 | ? { 33 | title: activeSong?.name, 34 | artist: activeSong?.primaryArtists, 35 | album: activeSong?.album.name, 36 | artwork: [ 37 | { 38 | src: activeSong.image[2]?.url, 39 | sizes: "500x500", 40 | type: "image/jpg", 41 | }, 42 | ], 43 | } 44 | : {}; 45 | useEffect(() => { 46 | // Check if the Media Session API is available in the browser environment 47 | if ("mediaSession" in navigator) { 48 | // Set media metadata 49 | navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetaData); 50 | 51 | // Define media session event handlers 52 | navigator.mediaSession.setActionHandler("play", onPlay); 53 | navigator.mediaSession.setActionHandler("pause", onPause); 54 | navigator.mediaSession.setActionHandler("previoustrack", onPreviousTrack); 55 | navigator.mediaSession.setActionHandler("nexttrack", onNextTrack); 56 | navigator.mediaSession.setActionHandler("seekbackward", () => { 57 | setSeekTime(appTime - 5); 58 | }); 59 | navigator.mediaSession.setActionHandler("seekforward", () => { 60 | setSeekTime(appTime + 5); 61 | }); 62 | } 63 | }, [mediaMetaData]); 64 | // media session handlers: 65 | const onPlay = () => { 66 | handlePlayPause(); 67 | }; 68 | 69 | const onPause = () => { 70 | handlePlayPause(); 71 | }; 72 | 73 | const onPreviousTrack = () => { 74 | handlePrevSong(); 75 | }; 76 | 77 | const onNextTrack = () => { 78 | handleNextSong(); 79 | }; 80 | 81 | useEffect(() => { 82 | ref.current.volume = volume; 83 | }, [volume]); 84 | // updates audio element only on seekTime change (and not on each rerender): 85 | useEffect(() => { 86 | ref.current.currentTime = seekTime; 87 | }, [seekTime]); 88 | 89 | return ( 90 | <> 91 |