├── 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 |
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 |
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 |
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 |
{
10 | handleAddToFavourite(activeSong)}} className={`cursor-pointer`}>
11 |
12 |
13 | :
14 |
{
15 | handleAddToFavourite(activeSong)}} className={`cursor-pointer`}>
16 |
17 |
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 | window.location.reload()}>
28 | Retry
29 |
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 |
15 |
23 |
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 |
{e.stopPropagation(); setSeekTime(appTime - 5)}} className="hidden lg:mr-4 lg:block text-white">
11 |
12 |
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 |
{e.stopPropagation(); setSeekTime(appTime + 5)}} className="hidden lg:ml-4 lg:block text-white">
25 |
26 |
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 | Ok
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 |
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 |
15 |
16 |
17 |
{ }
18 |
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 | [](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 | 
16 | ___
17 | 
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 |
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 |
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 |
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 |
32 | ) : (
33 |
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 |
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 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
55 |
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 |
99 | >
100 | );
101 | };
102 |
103 | export default Player;
104 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Profile.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React from 'react'
3 | import { signOut, useSession } from 'next-auth/react'
4 | import { useRouter } from 'next/navigation';
5 | import { getUserInfo } from '@/services/dataAPI';
6 | import { MdLogout } from 'react-icons/md';
7 | import { useState } from 'react';
8 | import { useEffect } from 'react';
9 |
10 |
11 | const Profile = ({setShowNav}) => {
12 | const router = useRouter();
13 | const {status, data} = useSession();
14 |
15 |
16 | const [user, setUser] = useState(null);
17 | useEffect(() => {
18 | const fetchUser = async () => {
19 | const res = await getUserInfo();
20 | // console.log('user',res);
21 | setUser(res);
22 | }
23 | fetchUser();
24 | }, [status]);
25 | return (
26 |
27 | {
28 | status === 'loading' ?
:
29 |
30 | {
31 | status === 'unauthenticated' ?
32 | (
33 |
34 | {
35 | setShowNav(false);
36 | router.push('/login');
37 | }} className=' border-2 border- px-3 py-1 m-2 rounded text-lg border-[#00e6e6]'>
38 | Login
39 |
40 | {
41 | setShowNav(false);
42 | router.push('/signup');
43 | }} className=' border-2 border- px-3 py-1 m-2 rounded text-lg border-[#00e6e6]'>
44 | Signup
45 |
46 |
47 | ):
48 | (
49 |
50 |
51 |
52 |
53 |
{data?.userName || user?.userName}
54 | {
55 | setShowNav(false);
56 | signOut();
57 | }} className='cursor-pointer text-white hover:text-[#00e6e6]' />
58 |
59 |
{data?.user?.email || user?.email }
60 |
61 |
62 | )
63 | }
64 |
65 | }
66 |
67 | )
68 | }
69 |
70 | export default Profile
--------------------------------------------------------------------------------
/src/components/Sidebar/Languages.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { FaChevronDown } from "react-icons/fa";
3 | import { useState } from "react";
4 | import { useSelector, useDispatch } from "react-redux";
5 | import { setLanguages } from "@/redux/features/languagesSlice";
6 |
7 | const Languages = () => {
8 | const dispatch = useDispatch();
9 | const { languages } = useSelector((state) => state.languages);
10 | const [selectedLanguages, setSelectedLanguages] = useState([...languages]);
11 |
12 | const languageList = [
13 | { id: "english", label: "English" },
14 | { id: "haryanvi", label: "Haryanvi" },
15 | { id: "punjabi", label: "Punjabi" },
16 | { id: "hindi", label: "Hindi" },
17 | { id: "rajasthani", label: "Rajasthani" },
18 | { id: "tamil", label: "Tamil" },
19 | { id: "telugu", label: "Telugu" },
20 | { id: "odia", label: "Odia" },
21 | // Add more languages as needed
22 | ];
23 |
24 | const handleLanguageChange = (event) => {
25 | const { value, checked } = event.target;
26 | let updatedLanguages;
27 |
28 | if (checked) {
29 | updatedLanguages = [...selectedLanguages, value];
30 | } else {
31 | updatedLanguages = selectedLanguages.filter((lang) => lang !== value);
32 | }
33 |
34 | setSelectedLanguages(updatedLanguages);
35 | dispatch(setLanguages(updatedLanguages));
36 | };
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
Languages
45 |
Pick which you like to listen
46 |
47 |
48 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default Languages;
79 |
--------------------------------------------------------------------------------
/src/services/playlistApi.js:
--------------------------------------------------------------------------------
1 | // create playlist
2 | export async function createPlaylist(name) {
3 | try {
4 | const response = await fetch(`/api/userPlaylists`, {
5 | method: "POST",
6 | headers: {
7 | "Content-Type": "application/json",
8 | },
9 | body: JSON.stringify({ name }),
10 | });
11 |
12 | const data = await response.json();
13 | return data;
14 | } catch (error) {
15 | console.log("Create playlist error", error);
16 | }
17 | }
18 |
19 | // get user playlists
20 | export async function getUserPlaylists() {
21 | try {
22 | const response = await fetch(`/api/userPlaylists`, {
23 | method: "GET",
24 | headers: {
25 | "Content-Type": "application/json",
26 | },
27 | });
28 |
29 | const data = await response.json();
30 | return data;
31 | } catch (error) {
32 | console.log("Get user playlists error", error);
33 | }
34 | }
35 |
36 | // delete playlist
37 | export async function deletePlaylist(id) {
38 | try {
39 | const response = await fetch(`/api/userPlaylists`, {
40 | method: "DELETE",
41 | headers: {
42 | "Content-Type": "application/json",
43 | },
44 | body: JSON.stringify({
45 | playlistId: id,
46 | }),
47 | });
48 | const data = await response.json();
49 | return data;
50 | } catch (error) {
51 | console.log("Delete playlist error", error);
52 | }
53 | }
54 |
55 | // add song to playlist
56 | export async function addSongToPlaylist(playlistID, song) {
57 | try {
58 | const response = await fetch(`/api/userPlaylists/songs`, {
59 | method: "POST",
60 | headers: {
61 | "Content-Type": "application/json",
62 | },
63 | body: JSON.stringify({
64 | playlistID,
65 | song,
66 | }),
67 | });
68 | const data = await response.json();
69 | return data;
70 | } catch (error) {
71 | console.log("Add song to playlist error", error);
72 | }
73 | }
74 |
75 | // delete song from playlist
76 | export async function deleteSongFromPlaylist(playlistID, song) {
77 | try {
78 | const response = await fetch(`/api/userPlaylists/songs`, {
79 | method: "DELETE",
80 | headers: {
81 | "Content-Type": "application/json",
82 | },
83 | body: JSON.stringify({
84 | playlistID,
85 | song,
86 | }),
87 | });
88 | const data = await response.json();
89 | return data;
90 | } catch (error) {
91 | console.log("Delete song from playlist error", error);
92 | }
93 | }
94 |
95 | // get single playlist by id
96 | export async function getSinglePlaylist(id) {
97 | try {
98 | const response = await fetch(`/api/userPlaylists/songs?playlist=${id}`, {
99 | method: "GET",
100 | headers: {
101 | "Content-Type": "application/json",
102 | },
103 | });
104 | const data = await response.json();
105 | return data;
106 | } catch (error) {
107 | console.log("Get single playlist error", error);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/app/dmca/page.jsx:
--------------------------------------------------------------------------------
1 |
2 | const page = () => {
3 | return (
4 |
5 | {/* disclaimer */}
6 |
Disclaimer
7 |
Hayasaka.live is a music search engine using open source APIs and does not upload or host any files on it's server. If you are a valid owner of any content listed here & want to remove it then please send us an DMCA formatted takedown notice at email . We will remove your content as soon as possible.
8 |
9 |
DMCA takedown request requirements
10 |
We take the intellectual property rights of others seriously and require that our Users do the same. The Digital Millennium Copyright Act (DMCA) established a process for addressing claims of copyright infringement. If you own a copyright or have authority to act on behalf of a copyright owner and want to report a claim that a third party is infringing that material on or through GitLab's services, please submit a DMCA report on our Github repository or email , and we will take appropriate action.
11 |
DMCA takedown request requirements
12 |
To submit a DMCA request, please provide the following information:
13 |
14 | Identification of the material that is claimed to be infringing or to be the subject of infringing activity and that is to be removed or access to which is to be disabled, and information reasonably sufficient to permit the service provider to locate the material. Providing URLs in the body of an email is the best way to help us locate content quickly.
15 | Information reasonably sufficient to permit the service provider to contact the complaining party, such as an address, telephone number, and, if available, an electronic mail address at which the complaining party may be contacted.
16 | A statement that the complaining party has a good faith belief that use of the material in the manner complained of is not authorized by the copyright
17 | A statement that the information in the notification is accurate, and under penalty of perjury, that the complaining party is authorized to act on behalf of the owner of an exclusive right that is allegedly infringed (Note that under Section 512(f) any person who knowingly and materially misrepresents that material or activity is infringing may be subject to liability for damages. In other words, DON'T MAKE FALSE CLAIMS!)
18 | A physical or electronic signature of a person authorized to act on behalf of the owner of an exclusive right that is allegedly infringed.
19 |
20 |
Hayasaka.live does not store any files on our server, we only linked to the media which is hosted on 3rd party services.
21 |
22 | )
23 | }
24 |
25 | export default page
--------------------------------------------------------------------------------
/src/components/Sidebar/PlaylistModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FaPlus } from 'react-icons/fa'
3 | import { useState } from 'react'
4 | import { toast } from 'react-hot-toast';
5 | import { createPlaylist } from '@/services/playlistApi';
6 | import { useDispatch } from 'react-redux';
7 | import { setIsTyping } from '@/redux/features/loadingBarSlice';
8 |
9 |
10 |
11 |
12 |
13 | const PlaylistModal = ({ show, setShow }) => {
14 | const dispatch = useDispatch();
15 | const [name, setName] = useState('');
16 | const [loading, setLoading] = useState(false);
17 |
18 | const handleChange = (e) => {
19 | setName(e.target.value)
20 | }
21 |
22 | const handelCreate = async () => {
23 | console.log(name)
24 | if(name === ''){
25 | toast.error('Playlist name is required')
26 | }
27 | else{
28 | setLoading(true)
29 | const res = await createPlaylist(name);
30 | if(res.success == true){
31 | toast.success(res.message)
32 | setName('')
33 | setShow(false)
34 | }
35 | else{
36 | toast.error(res.message)
37 | }
38 | setLoading(false)
39 | }
40 | }
41 |
42 | // handle focus
43 | const handleFocus = () => {
44 | dispatch(setIsTyping(true));
45 | };
46 | const handleBlur = () => {
47 | dispatch(setIsTyping(false));
48 | };
49 |
50 | return (show &&
51 |
52 |
setShow(false)} className='fixed bg-black bg-opacity-50 top-0 left-0 w-full h-full z-50 flex justify-center items-center text-white'>
53 |
e.stopPropagation() } className='bg-white/5 bg-opacity-80 backdrop-blur-sm rounded-lg w-[500px]'>
54 |
55 |
Create Playlist
56 | setShow(false)} className='text-white text-lg font-semibold'>X
57 |
58 |
59 |
60 | Name
61 |
62 |
63 |
64 | {
65 | loading ?
:
66 | }
67 | Create
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
75 | export default PlaylistModal
--------------------------------------------------------------------------------
/src/components/MusicPlayer/Controls.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { MdSkipNext, MdSkipPrevious } from "react-icons/md";
4 | import { BsFillPauseFill, BsFillPlayFill } from "react-icons/bs";
5 | import { TbRepeat, TbRepeatOnce, TbArrowsShuffle } from "react-icons/tb";
6 | import Downloader from "./Downloader";
7 | import FavouriteButton from "./FavouriteButton";
8 |
9 | const Controls = ({
10 | isPlaying,
11 | repeat,
12 | setRepeat,
13 | shuffle,
14 | setShuffle,
15 | currentSongs,
16 | handlePlayPause,
17 | handlePrevSong,
18 | handleNextSong,
19 | activeSong,
20 | fullScreen,
21 | handleAddToFavourite,
22 | favouriteSongs,
23 | loading,
24 | }) => {
25 | return (
26 |
27 |
34 | {!repeat ? (
35 |
{
40 | e.stopPropagation();
41 | setRepeat((prev) => !prev);
42 | }}
43 | className={`${
44 | !fullScreen ? "hidden sm:block" : " m-3"
45 | } cursor-pointer`}
46 | />
47 | ) : (
48 | {
53 | e.stopPropagation();
54 | setRepeat((prev) => !prev);
55 | }}
56 | className={`${
57 | !fullScreen ? "hidden sm:block" : " m-3"
58 | } cursor-pointer`}
59 | />
60 | )}
61 |
62 | {
63 |
70 | }
71 | {isPlaying ? (
72 |
78 | ) : (
79 |
85 | )}
86 | {
87 |
94 | }
95 | {
100 | e.stopPropagation();
101 | setShuffle((prev) => !prev);
102 | }}
103 | className={`${!fullScreen ? "hidden sm:block" : "m-3"} cursor-pointer`}
104 | />
105 | {activeSong?.downloadUrl?.[4]?.url && (
106 |
107 |
108 |
109 | )}
110 |
111 | );
112 | };
113 |
114 | export default Controls;
115 |
--------------------------------------------------------------------------------
/src/app/reset-password/[token]/page.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { setProgress } from '@/redux/features/loadingBarSlice';
3 | import { resetPassword } from '@/services/dataAPI';
4 | import React from 'react'
5 | import { toast } from 'react-hot-toast';
6 | import { useDispatch } from 'react-redux';
7 | import { redirect } from 'next/navigation';
8 | import { useRouter } from 'next/navigation';
9 |
10 | const page = ({params}) => {
11 | const Router = useRouter();
12 | const dispatch = useDispatch();
13 | const [formData, setFormData] = React.useState({
14 | password: '',
15 | confirmPassword: ''
16 | });
17 | const onchange = (e) => {
18 | setFormData({ ...formData, [e.target.name]: e.target.value });
19 | };
20 | const handelSubmit = async (e) => {
21 | e.preventDefault();
22 | if (formData.password !== formData.confirmPassword) {
23 | toast.error('Password and Confirm Password are not same');
24 | return;
25 | }
26 | const { password, confirmPassword } = formData;
27 | try{
28 | dispatch(setProgress(70));
29 | const res = await resetPassword(password, confirmPassword, params.token);
30 | if (res.success === true) {
31 | toast.success('Password reset successfully');
32 | Router.push('/login');
33 | }
34 | else {
35 | toast.error('Invalid Token');
36 | }
37 | } catch (error) {
38 | toast.error(error?.message || 'Something went wrong');
39 | } finally {
40 | dispatch(setProgress(100));
41 | }
42 |
43 | };
44 |
45 |
46 | return (
47 |
48 |
49 |
50 |
Reset Password
51 |
69 |
70 |
71 |
72 |
73 | )
74 | }
75 |
76 | export default page
--------------------------------------------------------------------------------
/src/app/api/favourite/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 | import UserData from "@/models/UserData";
6 |
7 |
8 |
9 | // Get user data
10 | export async function GET(req){
11 | const token = await getToken({ req, secret: process.env.JWT_SECRET });
12 | if (!token) {
13 | return NextResponse.json(
14 | {
15 | success: false,
16 | message: "User not logged in",
17 | data: null
18 | },
19 | { status: 401 }
20 | );
21 | }
22 | try {
23 | await dbConnect();
24 | const user = await User.findOne({ email: token.email });
25 | if (!user) {
26 | return NextResponse.json(
27 | {
28 | success: false,
29 | message: "User not found",
30 | data: null
31 | },
32 | { status: 404 }
33 | );
34 | }
35 | const userData = await UserData.findById(user.userData);
36 | if (!userData) {
37 | return NextResponse.json(
38 | {
39 | success: false,
40 | message: "User data not found",
41 | data: null
42 | },
43 | { status: 404 }
44 | );
45 | }
46 | return NextResponse.json(
47 | {
48 | success: true,
49 | message: "User Data found",
50 | data: userData
51 | }
52 | );
53 |
54 | } catch (e) {
55 | console.error('get user data error', e);
56 | return NextResponse.json(
57 | {
58 | success: false,
59 | message: "Something went wrong",
60 | data: null
61 | },
62 | { status: 500 }
63 | );
64 | }
65 |
66 | }
67 |
68 |
69 |
70 | // Add to favourites
71 | export async function POST(request) {
72 | const token = await getToken({ req: request, secret: process.env.JWT_SECRET });
73 | if (!token) {
74 | return NextResponse.json(
75 | {
76 | success: false,
77 | message: "User not logged in",
78 | data: null
79 | },
80 | { status: 401 }
81 | );
82 | }
83 | const { id } = await request.json();
84 | try {
85 | await dbConnect();
86 | const user = await User.findOne({ email: token.email });
87 | if (!user) {
88 | return NextResponse.json(
89 | {
90 | success: false,
91 | message: "User not found",
92 | data: null
93 | },
94 | { status: 404 }
95 | );
96 | }
97 | const userData = await UserData.findById(user.userData);
98 | if (!userData) {
99 | return NextResponse.json(
100 | {
101 | success: false,
102 | message: "User data not found",
103 | data: null
104 | },
105 | { status: 404 }
106 | );
107 | }
108 | // console.log('userData',userData.favourites);
109 | if (userData.favourites.includes(id)) {
110 | //remove from favourites
111 | await userData.updateOne({ $pull: { favourites: id } });
112 |
113 | } else {
114 | //add to favourites
115 | await userData.updateOne({ $push: { favourites: id } });
116 | }
117 | // await userData.save();
118 | const favourites = await UserData.findById(user.userData);
119 | return NextResponse.json(
120 | {
121 | success: true,
122 | message: "Favourites updated",
123 | data: favourites
124 | }
125 | );
126 |
127 | } catch (e) {
128 | console.log('add to favourites error', e);
129 | return NextResponse.json(
130 | {
131 | success: false,
132 | message: "Something went wrong",
133 | data: null
134 | },
135 | { status: 500 }
136 | );
137 | }
138 |
139 | }
--------------------------------------------------------------------------------
/src/components/Sidebar/Playlists.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { use } from "react";
3 | import { FaChevronDown } from "react-icons/fa";
4 | import { FaPlus } from "react-icons/fa";
5 | import { BiSolidPlaylist } from "react-icons/bi";
6 | import PlaylistModal from "./PlaylistModal";
7 | import { useState } from "react";
8 | import { useEffect } from "react";
9 | import { deletePlaylist, getUserPlaylists } from "@/services/playlistApi";
10 | import { MdPlaylistPlay } from "react-icons/md";
11 | import { PiDotsThreeVerticalBold } from "react-icons/pi";
12 | import { MdOutlineDeleteOutline } from "react-icons/md";
13 | import Link from "next/link";
14 |
15 | const Playlists = ({ setShowNav }) => {
16 | const [show, setShow] = useState(false);
17 | const [playlists, setPlaylists] = useState([]);
18 | const [showMenu, setShowMenu] = useState(false);
19 |
20 | useEffect(() => {
21 | const getPlaylists = async () => {
22 | const res = await getUserPlaylists();
23 | if (res?.success == true) {
24 | setPlaylists(res?.data?.playlists);
25 | }
26 | };
27 | getPlaylists();
28 | }, [show]);
29 |
30 | const handleDelete = async (id) => {
31 | const res = await deletePlaylist(id);
32 | if (res?.success == true) {
33 | setPlaylists(playlists.filter((playlist) => playlist._id !== id));
34 | }
35 | };
36 |
37 | return (
38 | <>
39 |
40 |
41 |
42 |
43 |
44 |
45 | Playlists
46 |
47 |
48 |
49 |
50 |
51 | {playlists?.map((playlist, index) => (
52 |
56 |
57 |
setShowNav(false)}
59 | className="flex gap-2 items-center"
60 | >
61 |
62 |
63 | {playlist.name}
64 |
65 |
66 |
67 |
68 |
setShowMenu(playlist?._id)}
70 | size={25}
71 | className=" text-gray-300"
72 | />
73 | {showMenu === playlist._id && (
74 | {
76 | setShowMenu(false);
77 | handleDelete(playlist._id);
78 | }}
79 | className="absolute top-0 right-0 bg-gray-900 z-50 hover:bg-gray-800 rounded-lg p-2"
80 | >
81 |
82 | Delete
83 |
84 |
85 | )}
86 |
87 |
88 | ))}
89 |
90 | setShow(true)}
92 | className="text-xs group font-semibold mb-7 flex gap-2 border-[1.5px] border-white rounded-lg px-2 items-center py-2"
93 | >
94 |
95 | Create
96 |
97 |
98 |
99 |
100 |
101 |
102 | {/* overlay */}
103 | {showMenu && (
104 | setShowMenu(false)}
106 | className="fixed top-0 left-0 w-full h-full z-30"
107 | >
108 | )}
109 | >
110 | );
111 | };
112 |
113 | export default Playlists;
114 |
--------------------------------------------------------------------------------
/src/components/Homepage/Home.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { homePageData } from "@/services/dataAPI";
3 | import React from "react";
4 | import { useEffect, useState } from "react";
5 | import { SwiperSlide } from "swiper/react";
6 | import SongCard from "./SongCard";
7 | import { useDispatch, useSelector } from "react-redux";
8 | import SwiperLayout from "./Swiper";
9 | import { setProgress } from "@/redux/features/loadingBarSlice";
10 | import SongCardSkeleton from "./SongCardSkeleton";
11 | import { GiMusicalNotes } from 'react-icons/gi'
12 | import SongBar from "./SongBar";
13 | import OnlineStatus from "./OnlineStatus";
14 | import ListenAgain from "./ListenAgain";
15 |
16 |
17 | const Home = () => {
18 | const [data, setData] = useState("");
19 | const [loading, setLoading] = useState(true);
20 | const dispatch = useDispatch();
21 | const { activeSong, isPlaying, } = useSelector((state) => state.player);
22 | const { languages } = useSelector((state) => state.languages);
23 |
24 | // salutation
25 | const currentTime = new Date();
26 | const currentHour = currentTime.getHours();
27 |
28 | let salutation = '';
29 | if (currentHour >= 5 && currentHour < 12) {
30 | salutation = 'Good morning';
31 | } else if (currentHour >= 12 && currentHour < 18) {
32 | salutation = 'Good afternoon';
33 | } else {
34 | salutation = 'Good evening';
35 | }
36 |
37 |
38 |
39 | useEffect(() => {
40 | const fetchData = async () => {
41 | dispatch(setProgress(70))
42 | const res = await homePageData(languages);
43 | setData(res);
44 | dispatch(setProgress(100))
45 | setLoading(false);
46 | };
47 | fetchData();
48 | }, [languages]);
49 |
50 |
51 |
52 | return (
53 |
54 |
55 |
"{salutation} "
56 |
57 |
58 |
59 | {/* trending */}
60 |
61 | {
62 | loading ? (
63 |
64 | ) : (
65 | <>
66 | {data?.trending?.songs?.map(
67 | (song) =>
68 | (
69 |
70 |
71 |
72 | )
73 | )}
74 |
75 | {data?.trending?.albums?.map(
76 | (song) =>
77 | (
78 |
79 |
80 |
81 | )
82 | )}
83 | >
84 | )
85 | }
86 |
87 |
88 | {/* top charts */}
89 |
90 |
Top Charts
91 |
92 | {
93 | loading ? (
94 |
95 |
96 |
97 | ) : (
98 | data?.charts?.slice(0, 10)?.map(
99 | (playlist, index) =>
100 | (
101 |
102 | ))
103 | )
104 | }
105 |
106 |
107 |
108 | {/* New Releases */}
109 |
110 | {
111 | loading ? (
112 |
113 | ) : (
114 | data?.albums?.map(
115 | (song) =>
116 | (
117 |
118 |
119 |
120 | )
121 | )
122 | )
123 | }
124 |
125 |
126 | {/* featured playlists */}
127 |
128 | {
129 | loading ? (
130 |
131 | ) : (
132 | data?.playlists?.map(
133 | (song) =>
134 | (
135 |
136 |
137 |
138 | )
139 | )
140 | )
141 | }
142 |
143 |
144 |
145 | );
146 | };
147 |
148 | export default Home;
--------------------------------------------------------------------------------
/src/components/MusicPlayer/VolumeBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | BsFillVolumeUpFill,
4 | BsVolumeDownFill,
5 | BsFillVolumeMuteFill,
6 | } from "react-icons/bs";
7 | import { BiAddToQueue } from "react-icons/bi";
8 | import { addSongToPlaylist, getUserPlaylists } from "@/services/playlistApi";
9 | import { toast } from "react-hot-toast";
10 |
11 | const VolumeBar = ({
12 | value,
13 | min,
14 | max,
15 | onChange,
16 | setVolume,
17 | activeSong,
18 | bgColor,
19 | }) => {
20 | const [showMenu, setShowMenu] = useState(false);
21 | const [playlists, setPlaylists] = useState([]);
22 | useEffect(() => {
23 | const getPlaylists = async () => {
24 | const res = await getUserPlaylists();
25 | if (res?.success == true) {
26 | setPlaylists(res?.data?.playlists);
27 | }
28 | };
29 | getPlaylists();
30 | }, []);
31 |
32 | // add song to playlist
33 | const handleAddToPlaylist = async (song, playlistID) => {
34 | setShowMenu(false);
35 | const res = await addSongToPlaylist(playlistID, song);
36 | if (res?.success == true) {
37 | toast.success(res?.message);
38 | } else {
39 | toast.error(res?.message);
40 | }
41 | };
42 | return (
43 | <>
44 |
45 |
46 |
{
48 | e.stopPropagation();
49 | setShowMenu(!showMenu);
50 | }}
51 | title="Add to Playlist"
52 | size={25}
53 | color={"white"}
54 | className={`${!true ? "hidden sm:block" : " m-3"} cursor-pointer`}
55 | />
56 | {showMenu && (
57 | {
59 | setShowMenu(false);
60 | }}
61 | className="absolute text-white bottom-[130%] backdrop-blur-lg rounded-lg p-3 w-32 flex flex-col gap-2 z-[100]"
62 | style={{
63 | backgroundColor: bgColor
64 | ? `rgba(${bgColor.red}, ${bgColor.green}, ${bgColor.blue}, 0.3)`
65 | : "rgba(0,0,0,0.2)",
66 | backdropFilter: "blur(20px)",
67 | }}
68 | >
69 |
70 | Add to Playlist
71 |
72 | {playlists?.length > 0 ? (
73 | playlists?.map((playlist, index) => (
74 |
{
77 | e.stopPropagation();
78 | handleAddToPlaylist(activeSong?.id, playlist._id);
79 | }}
80 | className="text-sm font-semibold flex gap-1 items-center hover:underline"
81 | >
82 | {playlist?.name}
83 |
84 | ))
85 | ) : (
86 |
87 | No Playlist
88 |
89 | )}
90 |
91 | )}
92 |
93 | {value <= 1 && value > 0.5 && (
94 |
setVolume(0)}
99 | />
100 | )}
101 | {value <= 0.5 && value > 0 && (
102 | setVolume(0)}
107 | />
108 | )}
109 | {value === 0 && (
110 | setVolume(1)}
115 | />
116 | )}
117 | {
119 | event.stopPropagation();
120 | }}
121 | type="range"
122 | step="any"
123 | value={value}
124 | min={min}
125 | max={max}
126 | onChange={onChange}
127 | className="2xl:w-24 lg:w-24 md:w-28 h-1 ml-2 accent-[#00e6e6] cursor-pointer"
128 | />
129 |
130 | {/* overlay */}
131 | {showMenu && (
132 | {
134 | e.stopPropagation();
135 | setShowMenu(false);
136 | }}
137 | className=" absolute w-screen h-screen bottom-0 left-0 z-[50]"
138 | >
139 | )}
140 | >
141 | );
142 | };
143 |
144 | export default VolumeBar;
145 |
--------------------------------------------------------------------------------
/src/app/artist/[artistId]/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import SwiperLayout from "@/components/Homepage/Swiper";
3 | import SongCard from "@/components/Homepage/SongCard";
4 | import SongListSkeleton from "@/components/SongListSkeleton";
5 | import SongList from "@/components/SongsList";
6 | import { setProgress } from "@/redux/features/loadingBarSlice";
7 | import {
8 | getArtistAlbums,
9 | getArtistData,
10 | getArtistSongs,
11 | } from "@/services/dataAPI";
12 | import Image from "next/image";
13 | import React from "react";
14 | import { useEffect } from "react";
15 | import { useState } from "react";
16 | import { useDispatch } from "react-redux";
17 | import { SwiperSlide } from "swiper/react";
18 |
19 | const page = ({ params }) => {
20 | const dispatch = useDispatch();
21 | const [artistDetails, setArtistDetails] = useState({});
22 | const [artistSongs, setArtistSongs] = useState([]);
23 | const [loading, setLoading] = useState(true);
24 | const [artistAlbums, setArtistAlbums] = useState([]);
25 |
26 | useEffect(() => {
27 | const fetchData = async () => {
28 | dispatch(setProgress(30));
29 | const details = await getArtistData(params.artistId);
30 | // console.log("details", details);
31 | dispatch(setProgress(60));
32 | setArtistDetails(details);
33 | const songs = await getArtistSongs(params.artistId, 1);
34 | dispatch(setProgress(90));
35 | setArtistSongs(songs);
36 | const albums = await getArtistAlbums(params.artistId, 1);
37 | setArtistAlbums(albums?.albums);
38 | console.log("cccc", albums.albums);
39 | dispatch(setProgress(100));
40 | setLoading(false);
41 | };
42 | fetchData();
43 | }, []);
44 |
45 | return (
46 |
47 |
48 | {loading ? (
49 |
65 | ) : (
66 |
76 | )}
77 |
78 |
79 |
80 | {artistDetails?.name}
81 |
82 |
83 |
84 | {artistDetails?.dominantType}
85 |
86 |
|
87 |
88 | {artistDetails?.dominantLanguage}
89 |
90 |
91 |
92 |
93 | • {artistDetails?.fanCount} listners
94 |
95 |
96 |
97 |
98 |
99 |
100 |
Songs
101 | {loading ? (
102 |
103 | ) : (
104 |
105 |
106 |
107 | )}
108 |
109 |
110 |
111 |
112 | {artistAlbums?.map((album, index) => (
113 |
114 |
115 |
116 | ))}
117 |
118 |
119 |
120 | );
121 | };
122 |
123 | export default page;
124 |
--------------------------------------------------------------------------------
/src/assets/hayasaka.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/MusicPlayer/Lyrics.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { getRecommendedSongs, getlyricsData } from "@/services/dataAPI";
3 | import { useState } from "react";
4 | import React, { useEffect } from "react";
5 | import { useSelector } from "react-redux";
6 | import SongsList from "../SongsList";
7 | import { useDispatch } from "react-redux";
8 | import { setAutoAdd } from "@/redux/features/playerSlice";
9 |
10 | const Lyrics = ({ activeSong }) => {
11 | const dispatch = useDispatch();
12 | const { currentSongs, autoAdd } = useSelector((state) => state.player);
13 | const [lyrics, setLyrics] = useState(null);
14 | const [loading, setLoading] = useState(false);
15 | const [activeTab, setActiveTab] = useState("queue");
16 |
17 | useEffect(() => {
18 | const fetchData = async () => {
19 | setLoading(true);
20 | const res = await getlyricsData(activeSong?.id);
21 | setLyrics(res);
22 | setLoading(false);
23 | };
24 | if (activeSong?.id) fetchData();
25 | }, [activeSong?.id]);
26 |
27 | const handleAutoAdd = (checked) => {
28 | console.log(autoAdd);
29 | if (checked) {
30 | dispatch(setAutoAdd(true));
31 | localStorage.setItem("autoAdd", true);
32 | } else {
33 | dispatch(setAutoAdd(false));
34 | localStorage.setItem("autoAdd", false);
35 | }
36 | };
37 |
38 | return (
39 | {
41 | e.stopPropagation();
42 | }}
43 | >
44 |
45 | {
47 | setActiveTab("queue");
48 | }}
49 | className={`${
50 | activeTab === "queue" ? "border-[#00e6e6] border-b-2" : ""
51 | } text-white text-xl m-3 font-medium `}
52 | >
53 | Queue
54 |
55 | {
57 | setActiveTab("lyrics");
58 | }}
59 | className={`${
60 | activeTab === "lyrics" ? "border-[#00e6e6] border-b-2" : ""
61 | } text-white text-xl m-3 font-medium`}
62 | >
63 | Lyrics
64 |
65 |
66 |
67 | {activeTab === "lyrics" ? (
68 | lyrics?.success ? (
69 |
70 | {lyrics?.data?.lyrics?.split("
").map((line, index) => {
71 | return
{line}
;
72 | })}
73 |
74 | ) : (
75 |
76 | No Lyrics Found
77 |
78 | )
79 | ) : (
80 |
81 |
e.stopPropagation()}
84 | >
85 |
86 | Auto add similar songs to queue
87 |
88 |
89 |
93 | {
95 | handleAutoAdd(e.target.checked);
96 | }}
97 | type="checkbox"
98 | checked={autoAdd}
99 | className="sr-only peer"
100 | name="autoAddButton"
101 | id="autoAddButton"
102 | placeholder="autoAddButton"
103 | title={autoAdd ? "on" : "off"}
104 | >
105 |
106 |
107 |
108 | {currentSongs?.length > 0 ? (
109 |
110 |
116 |
117 | ) : (
118 |
119 | No Songs
120 |
121 | )}
122 |
123 | )}
124 |
125 |
126 | );
127 | };
128 |
129 | export default Lyrics;
130 |
--------------------------------------------------------------------------------
/src/components/Homepage/SongCard.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { memo, useState } from "react";
3 | import Link from "next/link";
4 | import { useDispatch } from "react-redux";
5 | import PlayPause from "../PlayPause";
6 | import {
7 | playPause,
8 | setActiveSong,
9 | setFullScreen,
10 | } from "../../redux/features/playerSlice";
11 | import { getRecommendedSongs, getSongData } from "@/services/dataAPI";
12 | import { useSelector } from "react-redux";
13 |
14 | const SongCard = ({ song, isPlaying, activeSong }) => {
15 | const [loading, setLoading] = useState(false);
16 | const { currentSongs, autoAdd } = useSelector((state) => state.player);
17 |
18 | const dispatch = useDispatch();
19 |
20 | const handlePauseClick = () => {
21 | if (song?.type === "song") {
22 | dispatch(playPause(false));
23 | }
24 | };
25 |
26 | const handlePlayClick = async () => {
27 | if (song?.type === "song") {
28 | setLoading(true);
29 | const Data = await getSongData(song?.id);
30 | const songData = await Data?.[0];
31 | const recommendedSongs = await getRecommendedSongs(
32 | songData?.primaryArtistsId,
33 | songData?.id
34 | );
35 | // remove duplicate songs in recommendedSongs array and currentSongs array
36 | const filteredRecommendedSongs =
37 | recommendedSongs?.filter(
38 | (song) => !currentSongs?.find((s) => s?.id === song?.id)
39 | ) || [];
40 | dispatch(
41 | setActiveSong({
42 | song: songData,
43 | data: currentSongs?.find((s) => s?.id === songData?.id)
44 | ? currentSongs
45 | : autoAdd
46 | ? [...currentSongs, songData, ...filteredRecommendedSongs]
47 | : [...currentSongs, songData],
48 | i: currentSongs?.find((s) => s?.id === songData?.id)
49 | ? currentSongs?.findIndex((s) => s?.id === songData?.id)
50 | : currentSongs?.length,
51 | })
52 | );
53 | dispatch(setFullScreen(true));
54 | dispatch(playPause(true));
55 | setLoading(false);
56 | }
57 | };
58 |
59 | return (
60 |
64 |
{
66 | if (song?.type == "song") {
67 | e.preventDefault();
68 | }
69 | }}
70 | href={
71 | song?.type === "album"
72 | ? `/album/${song?.id}`
73 | : song?.type === "playlist"
74 | ? `/playlist/${song?.id}`
75 | : ""
76 | }
77 | >
78 |
79 |
95 |
111 |
112 |
113 |
114 |
119 | {song?.name?.replaceAll("'", "'")?.replaceAll("&", "&") ||
120 | song?.title}
121 |
122 |
123 | {song?.artists?.primary?.map((artist) => artist?.name).join(", ") ||
124 | song?.artists?.map((artist) => artist?.name).join(", ") ||
125 | (song?.subtitle != "JioSaavn" && song?.subtitle)}
126 |
127 |
128 |
129 |
130 | );
131 | };
132 |
133 | export default memo(
134 | SongCard,
135 | (prev, next) =>
136 | prev.song === next.song &&
137 | prev.activeSong === next.activeSong &&
138 | prev.isPlaying === next.isPlaying
139 | );
140 |
--------------------------------------------------------------------------------
/src/app/login/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { useState } from "react";
4 | import Link from "next/link";
5 | import { signIn } from "next-auth/react";
6 | import { toast } from "react-hot-toast";
7 | import { redirect } from "next/navigation";
8 | import { useDispatch } from "react-redux";
9 | import { setProgress } from "@/redux/features/loadingBarSlice";
10 | import { useSession } from "next-auth/react";
11 | import { FaGoogle } from "react-icons/fa";
12 |
13 | const page = () => {
14 | const { status } = useSession();
15 | const dispatch = useDispatch();
16 | const [formData, setFormData] = useState({
17 | email: "",
18 | password: "",
19 | });
20 |
21 | const onchange = (e) => {
22 | setFormData({ ...formData, [e.target.name]: e.target.value });
23 | };
24 |
25 | const handelSubmit = async (e) => {
26 | e.preventDefault();
27 | try {
28 | dispatch(setProgress(70));
29 | const res = await signIn("credentials", {
30 | redirect: false,
31 | email: formData.email,
32 | password: formData.password,
33 | });
34 | if (!res.error) {
35 | toast.success("Logged in successfully");
36 | } else {
37 | toast.error("Invalid credentials");
38 | }
39 | } catch (error) {
40 | toast.error(error?.message);
41 | } finally {
42 | dispatch(setProgress(100));
43 | }
44 | };
45 |
46 | // redirect if user is authenticated
47 | if (status === "loading") {
48 | return (
49 |
50 |
51 |
52 | );
53 | }
54 | if (status === "authenticated") {
55 | redirect("/");
56 | }
57 | return (
58 |
135 | );
136 | };
137 |
138 | export default page;
139 |
--------------------------------------------------------------------------------
/src/app/api/userPlaylists/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 | import Playlist from "@/models/Playlist";
6 | import UserData from "@/models/UserData";
7 | import auth from "@/utils/auth";
8 |
9 |
10 | // Create a new playlist
11 | export async function POST(req){
12 | const { name } = await req.json();
13 | try {
14 | await dbConnect();
15 | const user = await auth(req);
16 | if (!user) {
17 | return NextResponse.json(
18 | {
19 | success: false,
20 | message: "User not logged in",
21 | data: null
22 | },
23 | { status: 404 }
24 | );
25 | }
26 | const playlist = await Playlist.create({
27 | name,
28 | user: user._id
29 | });
30 |
31 | const userData = await UserData.findById(user.userData);
32 | userData.playlists.push(playlist._id);
33 | await userData.save();
34 | return NextResponse.json(
35 | {
36 | success: true,
37 | message: "Playlist created",
38 | data: {
39 | playlist
40 | }
41 | }
42 | );
43 | } catch (e) {
44 | console.error(e);
45 | return NextResponse.json(
46 | {
47 | success: false,
48 | message: "Something went wrong",
49 | data: null
50 | },
51 | { status: 500 }
52 | );
53 | }
54 | }
55 |
56 | // delete a playlist
57 | export async function DELETE(req){
58 | const { playlistId } = await req.json();
59 | try {
60 | await dbConnect();
61 | const user = await auth(req);
62 | if (!user) {
63 | return NextResponse.json(
64 | {
65 | success: false,
66 | message: "User not logged in",
67 | data: null
68 | },
69 | { status: 404 }
70 | );
71 | }
72 | const playlist = await Playlist.findById(playlistId);
73 | if (!playlist) {
74 | return NextResponse.json(
75 | {
76 | success: false,
77 | message: "Playlist not found",
78 | data: null
79 | },
80 | { status: 404 }
81 | );
82 | }
83 | if (playlist.user.toString() !== user._id.toString()) {
84 | return NextResponse.json(
85 | {
86 | success: false,
87 | message: "You are not authorized to delete this playlist",
88 | data: null
89 | },
90 | { status: 401 }
91 | );
92 | }
93 | await Playlist.deleteOne({ _id: playlistId });
94 | const userData = await UserData.findById(user.userData);
95 | userData.playlists = userData.playlists.filter((playlist) => playlist.toString() !== playlistId);
96 | await userData.save();
97 | return NextResponse.json(
98 | {
99 | success: true,
100 | message: "Playlist deleted",
101 | data: null
102 | }
103 | );
104 | } catch (e) {
105 | console.error(e);
106 | return NextResponse.json(
107 | {
108 | success: false,
109 | message: "Something went wrong",
110 | data: null
111 | },
112 | { status: 500 }
113 | );
114 | }
115 | }
116 |
117 |
118 | // get all playlists
119 | export async function GET(req){
120 | const token = await getToken({ req, secret: process.env.JWT_SECRET });
121 | if (!token) {
122 | return NextResponse.json(
123 | {
124 | success: false,
125 | message: "User not logged in",
126 | data: null
127 | },
128 | { status: 401 }
129 | );
130 | }
131 | try {
132 | await dbConnect();
133 | const user = await User.findOne({ email: token.email });
134 | if (!user) {
135 | return NextResponse.json(
136 | {
137 | success: false,
138 | message: "User not found",
139 | data: null
140 | },
141 | { status: 404 }
142 | );
143 | }
144 | const userData = await UserData.findById(user.userData).populate("playlists");
145 | if (!userData) {
146 | return NextResponse.json(
147 | {
148 | success: false,
149 | message: "User data not found",
150 | data: null
151 | },
152 | { status: 404 }
153 | );
154 | }
155 | return NextResponse.json(
156 | {
157 | success: true,
158 | message: "Playlists fetched",
159 | data: {
160 | playlists: userData.playlists
161 | }
162 | }
163 | );
164 |
165 | } catch (e) {
166 | console.error(e);
167 | return NextResponse.json(
168 | {
169 | success: false,
170 | message: "Something went wrong",
171 | data: null
172 | },
173 | { status: 500 }
174 | );
175 | }
176 | }
--------------------------------------------------------------------------------
/src/app/api/forgotPassword/route.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import User from "@/models/User";
3 | import mailSender from "@/utils/mailSender";
4 | import dbConnect from "@/utils/dbconnect";
5 | import crypto from "crypto";
6 | import bcrypt from "bcryptjs";
7 |
8 |
9 |
10 | // Forgot password mail
11 | export async function POST(request) {
12 | const {email} = await request.json();
13 | if (!email) {
14 | return NextResponse.json(
15 | {
16 | success: false,
17 | message: "User not found",
18 | data: null
19 | },
20 | { status: 401 }
21 | );
22 | }
23 | try {
24 | await dbConnect();
25 | const user = await User.findOne({ email: email });
26 | if (!user) {
27 | return NextResponse.json(
28 | {
29 | success: false,
30 | message: "User not found",
31 | data: null
32 | },
33 | { status: 404 }
34 | );
35 | }
36 |
37 |
38 | const resetPasswordToken = crypto.randomBytes(20).toString('hex');
39 | // 5 minutes password reset token
40 | const resetPasswordExpires = Date.now() + 300000;
41 |
42 | const updatedUser = await User.findByIdAndUpdate(user._id, {
43 | resetPasswordToken,
44 | resetPasswordExpires
45 | }, { new: true });
46 |
47 | if (!updatedUser) {
48 | return NextResponse.json(
49 | {
50 | success: false,
51 | message: "User not found",
52 | data: null
53 | },
54 | { status: 404 }
55 | );
56 | }
57 |
58 | const url = `${process.env.NEXTAUTH_URL}/reset-password/${resetPasswordToken}`;
59 | const title = "Reset Password";
60 | const body = `Click here to reset your password valid for 5 min
`;
61 | const mail = await mailSender(user.email, title, body);
62 | if (mail) {
63 | return NextResponse.json(
64 | {
65 | success: true,
66 | message: "Reset password mail sent",
67 | data: null
68 | }
69 | );
70 | }
71 | return NextResponse.json(
72 | {
73 | success: false,
74 | message: "Something went wrong",
75 | data: null
76 | },
77 | { status: 500 }
78 | );
79 |
80 | } catch (e) {
81 | console.error('Forgot password error', e);
82 | return NextResponse.json(
83 | {
84 | success: false,
85 | message: "Something went wrong",
86 | data: null
87 | },
88 | { status: 500 }
89 | );
90 | }
91 | }
92 |
93 |
94 |
95 |
96 | // Reset password
97 | export async function PUT(request) {
98 | const {token, password, confirmPassword} = await request.json();
99 | if (!token || !password || !confirmPassword) {
100 | return NextResponse.json(
101 | {
102 | success: false,
103 | message: "Please fill all the fields",
104 | data: null
105 | },
106 | { status: 400 }
107 | );
108 | }
109 | try {
110 | // Check if token is valid
111 | await dbConnect();
112 | const checkToken = await User.findOne({ resetPasswordToken: token, resetPasswordExpires: { $gt: Date.now() } });
113 | if (!checkToken) {
114 | return NextResponse.json(
115 | {
116 | success: false,
117 | message: "Token expired",
118 | data: null
119 | },
120 | { status: 401 }
121 | );
122 | }
123 | // Check if password and confirm password are same
124 | if (password !== confirmPassword) {
125 | return NextResponse.json(
126 | {
127 | success: false,
128 | message: "Password and confirm password are not same",
129 | data: null
130 | },
131 | { status: 400 }
132 | );
133 | }
134 | // Hash password
135 | const hashedPassword = await bcrypt.hash(password, 10);
136 | // Update password
137 | const updatedUser = await User.findByIdAndUpdate(checkToken._id, {
138 | password: hashedPassword,
139 | resetPasswordToken: null,
140 | resetPasswordExpires: null
141 | }, { new: true });
142 | if (!updatedUser) {
143 | return NextResponse.json(
144 | {
145 | success: false,
146 | message: "User not found",
147 | data: null
148 | },
149 | { status: 404 }
150 | );
151 | }
152 | return NextResponse.json(
153 | {
154 | success: true,
155 | message: "Password updated successfully",
156 | data: null
157 | }
158 | );
159 |
160 | }
161 | catch (e) {
162 | console.error('Reset password error', e);
163 | return NextResponse.json(
164 | {
165 | success: false,
166 | message: "Something went wrong",
167 | data: null
168 | },
169 | { status: 500 }
170 | );
171 | }
172 | }
--------------------------------------------------------------------------------
/src/app/layout.js:
--------------------------------------------------------------------------------
1 | import Navbar from "@/components/Navbar";
2 | import "./globals.css";
3 | import MusicPlayer from "@/components/MusicPlayer";
4 | import Providers from "@/redux/Providers";
5 | import TopProgressBar from "@/components/topProgressBar/TopProgressBar";
6 | import Favicon from "./favicon.ico";
7 | import SongsHistory from "@/components/SongsHistory";
8 | import { Toaster } from "react-hot-toast";
9 | import AuthProvider from "./AuthProvider";
10 | import { Poppins } from "next/font/google";
11 | import Script from "next/script";
12 |
13 | const poppins = Poppins({
14 | weight: "500",
15 | subsets: ["latin-ext"],
16 | display: "swap",
17 | });
18 |
19 | const siteUrl = "https://hayasaka.8man.in";
20 |
21 | export const metadata = {
22 | metadataBase: new URL(siteUrl),
23 | title: {
24 | default: "Hayasaka - Free Music Streaming & Download | Listen Online",
25 | template: "%s | Hayasaka",
26 | },
27 | description:
28 | "Stream and download your favorite songs for free on Hayasaka. Listen to Bollywood, Hindi, Punjabi, and international music. Create playlists, discover new artists, and enjoy high-quality music streaming.",
29 | keywords: [
30 | "music streaming",
31 | "free music download",
32 | "listen songs online",
33 | "hindi songs",
34 | "bollywood songs",
35 | "punjabi songs",
36 | "download songs free",
37 | "music player",
38 | "online music",
39 | "mp3 download",
40 | "hayasaka music",
41 | "free music app",
42 | "stream music free",
43 | "latest songs",
44 | "new songs 2024",
45 | ],
46 | authors: [{ name: "Hayasaka" }],
47 | creator: "Hayasaka",
48 | publisher: "Hayasaka",
49 | icons: [{ rel: "icon", url: Favicon.src }],
50 | manifest: "/manifest.json",
51 | openGraph: {
52 | type: "website",
53 | locale: "en_US",
54 | url: siteUrl,
55 | siteName: "Hayasaka",
56 | title: "Hayasaka - Free Music Streaming & Download",
57 | description:
58 | "Stream and download your favorite songs for free. Listen to Bollywood, Hindi, Punjabi music and more.",
59 | images: [
60 | {
61 | url: "/icon-512x512.png",
62 | width: 512,
63 | height: 512,
64 | alt: "Hayasaka Music App",
65 | },
66 | ],
67 | },
68 | twitter: {
69 | card: "summary_large_image",
70 | title: "Hayasaka - Free Music Streaming & Download",
71 | description:
72 | "Stream and download your favorite songs for free. Listen to English, Hindi, Punjabi music and more.",
73 | images: ["/icon-512x512.png"],
74 | },
75 | robots: {
76 | index: true,
77 | follow: true,
78 | googleBot: {
79 | index: true,
80 | follow: true,
81 | "max-video-preview": -1,
82 | "max-image-preview": "large",
83 | "max-snippet": -1,
84 | },
85 | },
86 | alternates: {
87 | canonical: siteUrl,
88 | },
89 | };
90 |
91 | const jsonLd = {
92 | "@context": "https://schema.org",
93 | "@graph": [
94 | {
95 | "@type": "WebSite",
96 | "@id": `${siteUrl}/#website`,
97 | url: siteUrl,
98 | name: "Hayasaka",
99 | description: "Free music streaming and download platform",
100 | potentialAction: {
101 | "@type": "SearchAction",
102 | target: {
103 | "@type": "EntryPoint",
104 | urlTemplate: `${siteUrl}/search/{search_term_string}`,
105 | },
106 | "query-input": "required name=search_term_string",
107 | },
108 | },
109 | {
110 | "@type": "Organization",
111 | "@id": `${siteUrl}/#organization`,
112 | name: "Hayasaka",
113 | url: siteUrl,
114 | logo: {
115 | "@type": "ImageObject",
116 | url: `${siteUrl}/icon-512x512.png`,
117 | width: 512,
118 | height: 512,
119 | },
120 | sameAs: [],
121 | },
122 | {
123 | "@type": "WebApplication",
124 | "@id": `${siteUrl}/#webapp`,
125 | name: "Hayasaka",
126 | description:
127 | "Stream and download your favorite songs for free on Hayasaka",
128 | url: siteUrl,
129 | applicationCategory: "MusicApplication",
130 | operatingSystem: "Any",
131 | offers: {
132 | "@type": "Offer",
133 | price: "0",
134 | priceCurrency: "USD",
135 | },
136 | featureList: [
137 | "Free music streaming",
138 | "Music download",
139 | "Create playlists",
140 | "Favorite songs",
141 | "Search songs",
142 | "Artist profiles",
143 | "Album browsing",
144 | ],
145 | },
146 | ],
147 | };
148 |
149 | export default function RootLayout({ children }) {
150 | return (
151 |
152 |
153 |
157 |
158 |
162 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | {children}
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | );
188 | }
189 |
--------------------------------------------------------------------------------
/src/services/dataAPI.js:
--------------------------------------------------------------------------------
1 | // home page data
2 | export async function homePageData(language) {
3 | try {
4 | const response = await fetch(
5 | `${"https://jiosaavn-api-sigma-sandy.vercel.app"}/modules?language=${language.toString()}`,
6 | {
7 | next: {
8 | revalidate: 14400,
9 | },
10 | }
11 | );
12 | const data = await response.json();
13 | return data?.data;
14 | } catch (error) {
15 | console.log(error);
16 | }
17 | }
18 |
19 | // get song data
20 | export async function getSongData(id) {
21 | try {
22 | const response = await fetch(
23 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/songs/${id}`
24 | );
25 | const data = await response.json();
26 | console.log("song data", data);
27 | return data?.data;
28 | } catch (error) {
29 | console.log(error);
30 | }
31 | }
32 |
33 | // get album data
34 | export async function getAlbumData(id) {
35 | try {
36 | const response = await fetch(
37 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/albums?id=${id}`
38 | );
39 | const data = await response.json();
40 | return data?.data;
41 | } catch (error) {
42 | console.log(error);
43 | }
44 | }
45 |
46 | // get playlist data
47 | export async function getplaylistData(id) {
48 | try {
49 | const response = await fetch(
50 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/playlists?id=${id}&limit=50`
51 | );
52 | const data = await response.json();
53 | return data?.data;
54 | } catch (error) {
55 | console.log(error);
56 | }
57 | }
58 |
59 | // get Lyrics data
60 | export async function getlyricsData(id) {
61 | try {
62 | const response = await fetch(
63 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/songs/${id}/lyrics`
64 | );
65 | const data = await response.json();
66 | return data;
67 | } catch (error) {
68 | console.log(error);
69 | }
70 | }
71 |
72 | // get artist data
73 | export async function getArtistData(id) {
74 | try {
75 | const response = await fetch(
76 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/artists?id=${id}`
77 | );
78 | const data = await response.json();
79 | return data?.data;
80 | } catch (error) {
81 | console.log(error);
82 | }
83 | }
84 |
85 | // get artist songs
86 | export async function getArtistSongs(id, page) {
87 | try {
88 | const response = await fetch(
89 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/artists/${id}/songs?page=${page}&`
90 | );
91 | const data = await response.json();
92 | return data?.data;
93 | } catch (error) {
94 | console.log(error);
95 | }
96 | }
97 |
98 | // get artist albums
99 | export async function getArtistAlbums(id, page) {
100 | try {
101 | const response = await fetch(
102 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/artists/${id}/albums?page=${page}`
103 | );
104 | const data = await response.json();
105 | return data?.data;
106 | } catch (error) {
107 | console.log("album error", error);
108 | }
109 | }
110 |
111 | // get search data
112 | export async function getSearchedData(query) {
113 | try {
114 | const response = await fetch(
115 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/search?query=${query}`
116 | );
117 | const data = await response.json();
118 | return data?.data;
119 | } catch (error) {
120 | console.log(error);
121 | }
122 | }
123 |
124 | // add and remove from favourite
125 | export async function addFavourite(id) {
126 | try {
127 | const response = await fetch("/api/favourite", {
128 | method: "POST",
129 | body: JSON.stringify(id),
130 | headers: {
131 | "Content-Type": "application/json",
132 | },
133 | });
134 | const data = await response.json();
135 | return data;
136 | } catch (error) {
137 | console.log("Add favourite API error", error);
138 | }
139 | }
140 |
141 | // get favourite
142 | export async function getFavourite() {
143 | try {
144 | const response = await fetch("/api/favourite");
145 | const data = await response.json();
146 | return data?.data?.favourites;
147 | } catch (error) {
148 | console.log("Get favourite API error", error);
149 | }
150 | }
151 |
152 | // user info
153 | export async function getUserInfo() {
154 | try {
155 | const response = await fetch("/api/userInfo");
156 | const data = await response.json();
157 | return data?.data;
158 | } catch (error) {
159 | console.log("Get user info API error", error);
160 | }
161 | }
162 |
163 | // reset password
164 | export async function resetPassword(password, confirmPassword, token) {
165 | try {
166 | const response = await fetch("/api/forgotPassword", {
167 | method: "PUT",
168 | body: JSON.stringify({ password, confirmPassword, token }),
169 | headers: {
170 | "Content-Type": "application/json",
171 | },
172 | });
173 | const data = await response.json();
174 | return data;
175 | } catch (error) {
176 | console.log("Reset password API error", error);
177 | }
178 | }
179 |
180 | // send reset password link
181 | export async function sendResetPasswordLink(email) {
182 | try {
183 | const response = await fetch("/api/forgotPassword", {
184 | method: "POST",
185 | body: JSON.stringify({ email }),
186 | headers: {
187 | "Content-Type": "application/json",
188 | },
189 | });
190 | const data = await response.json();
191 | return data;
192 | } catch (error) {
193 | console.log("Send reset password link API error", error);
194 | }
195 | }
196 |
197 | // get recommended songs
198 | export async function getRecommendedSongs(artistId, songId) {
199 | try {
200 | const response = await fetch(
201 | `${process.env.NEXT_PUBLIC_SAAVN_API}/api/songs/${songId}/suggestions`
202 | );
203 | const data = await response.json();
204 | return data?.data;
205 | } catch (error) {
206 | console.log(error);
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/app/signup/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { setProgress } from "@/redux/features/loadingBarSlice";
3 | import Link from "next/link";
4 | import React from "react";
5 | import { useState } from "react";
6 | import { toast } from "react-hot-toast";
7 | import { useDispatch } from "react-redux";
8 | import { useRouter } from "next/navigation";
9 | import { useSession } from "next-auth/react";
10 | import { redirect } from "next/navigation";
11 | import { signIn } from "next-auth/react";
12 | import { FaGoogle } from "react-icons/fa";
13 |
14 | const page = () => {
15 | const { status } = useSession();
16 | const router = useRouter();
17 | const [formData, setFormData] = useState({
18 | userName: "",
19 | email: "",
20 | password: "",
21 | });
22 | const onchange = (e) => {
23 | setFormData({ ...formData, [e.target.name]: e.target.value });
24 | };
25 | const dispatch = useDispatch();
26 |
27 | const handelSubmit = async (e) => {
28 | e.preventDefault();
29 | try {
30 | dispatch(setProgress(70));
31 | const res = await fetch("/api/signup", {
32 | method: "POST",
33 | headers: {
34 | "Content-Type": "application/json",
35 | },
36 | body: JSON.stringify({
37 | userName: formData.userName,
38 | email: formData.email,
39 | password: formData.password,
40 | imageUrl: `https://api.dicebear.com/6.x/thumbs/svg?seed=${formData.userName}`,
41 | }),
42 | });
43 | const data = await res.json();
44 | if (data.success === true) {
45 | toast.success("Account created successfully");
46 | router.push("/login");
47 | } else {
48 | toast.error(data?.message);
49 | }
50 | console.log(data);
51 | } catch (error) {
52 | toast.error(error?.message);
53 | } finally {
54 | dispatch(setProgress(100));
55 | }
56 | };
57 |
58 | // redirect if user is authenticated
59 | if (status === "loading") {
60 | return (
61 |
62 |
63 |
64 | );
65 | }
66 | if (status === "authenticated") {
67 | redirect("/");
68 | }
69 | return (
70 |
159 | );
160 | };
161 |
162 | export default page;
163 |
--------------------------------------------------------------------------------
/src/app/album/[albumId]/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { setProgress } from "@/redux/features/loadingBarSlice";
3 | import { playPause, setActiveSong } from "@/redux/features/playerSlice";
4 | import { getAlbumData } from "@/services/dataAPI";
5 | import Link from "next/link";
6 | import React, { useEffect } from "react";
7 | import { useState } from "react";
8 | import { BsPlayFill } from "react-icons/bs";
9 | import { useDispatch } from "react-redux";
10 | import { BsFillPlayFill } from "react-icons/bs";
11 |
12 | const page = ({ params }) => {
13 | const [albumData, setAlbumData] = useState(null);
14 | const [loading, setLoading] = useState(true);
15 | const dispatch = useDispatch();
16 |
17 | useEffect(() => {
18 | const fetchData = async () => {
19 | dispatch(setProgress(50));
20 | const response = await getAlbumData(params.albumId);
21 | dispatch(setProgress(100));
22 | // console.log(response);
23 | setAlbumData(response);
24 | setLoading(false);
25 | };
26 | fetchData();
27 | }, []);
28 |
29 | // Utility function to format the duration in seconds to "m:ss" format (without leading zero for minutes)
30 | function formatDuration(durationInSeconds) {
31 | const minutes = Math.floor(durationInSeconds / 60);
32 | const seconds = Math.round(durationInSeconds % 60);
33 |
34 | if (minutes > 0) {
35 | return `${minutes}:${seconds.toString().padStart(2, "0")}`;
36 | } else {
37 | return `${seconds}`;
38 | }
39 | }
40 |
41 | const handlePlayClick = (song, index) => {
42 | dispatch(setActiveSong({ song, data: albumData?.songs, i: index }));
43 | dispatch(playPause(true));
44 | };
45 |
46 | return (
47 |
48 |
49 | {loading ? (
50 |
66 | ) : (
67 |
74 | )}
75 |
76 |
77 |
{albumData?.name}
78 |
{albumData?.subtitle}
79 |
80 | {albumData?.primaryArtists?.split(",")?.map((artist, index) => (
81 |
82 |
88 | {artist?.trim()?.replaceAll("&", "&")}
89 |
90 | {index < albumData.primaryArtists.split(",").length - 1 && ", "}
91 |
92 | ))}
93 |
94 |
95 | • {albumData?.year}
96 |
97 | • {albumData?.songCount} songs
98 |
99 |
100 |
{
102 | handlePlayClick(albumData?.songs?.[0], 0);
103 | }}
104 | className="flex items-center gap-2 mt-5 group rounded-3xl py-2 px-3 hover:border-[#00e6e6] w-fit cursor-pointer border border-white"
105 | >
106 |
110 |
Play
111 |
112 |
113 |
114 |
115 |
Songs
116 |
117 | {albumData?.songs?.map((song, index) => (
118 |
{
121 | handlePlayClick(song, index);
122 | }}
123 | className="flex items-center mt-5 cursor-pointer group border-b-[1px] border-gray-400 justify-between"
124 | >
125 |
126 |
127 |
128 |
129 | {index + 1}.
130 |
131 |
135 |
136 |
137 |
138 | {song?.name.replace("'", "'").replace("&", "&")}
139 |
140 |
141 |
142 |
143 | {song?.playCount && (
144 |
{song?.playCount} plays
145 | )}
146 |
147 |
148 |
{formatDuration(song?.duration)}
149 |
150 |
151 | ))}
152 |
153 |
154 |
155 | );
156 | };
157 |
158 | export default page;
159 |
--------------------------------------------------------------------------------
/src/app/search/[query]/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import SwiperLayout from "@/components/Homepage/Swiper";
3 | import SongCard from "@/components/Homepage/SongCard";
4 | import { getSearchedData, getSongData } from "@/services/dataAPI";
5 | import React, { useEffect } from "react";
6 | import { useState } from "react";
7 | import { useSelector } from "react-redux";
8 | import { SwiperSlide } from "swiper/react";
9 | import { BsPlayFill } from "react-icons/bs";
10 | import { useDispatch } from "react-redux";
11 | import {
12 | playPause,
13 | setActiveSong,
14 | setFullScreen,
15 | } from "@/redux/features/playerSlice";
16 | import Image from "next/image";
17 | import Link from "next/link";
18 | import SongListSkeleton from "@/components/SongListSkeleton";
19 | import { setProgress } from "@/redux/features/loadingBarSlice";
20 |
21 | const page = ({ params }) => {
22 | const dispatch = useDispatch();
23 | const [query, setQuery] = useState(params.query);
24 | const [searchedData, setSearchedData] = useState(null);
25 | const [loading, setLoading] = useState(true);
26 | const { currentSongs } = useSelector((state) => state.player);
27 |
28 | useEffect(() => {
29 | const fetchData = async () => {
30 | dispatch(setProgress(70));
31 | const response = await getSearchedData(query);
32 | setSearchedData(response);
33 | setLoading(false);
34 | dispatch(setProgress(100));
35 | };
36 | fetchData();
37 | }, [query]);
38 |
39 | const handlePlayClick = async (song) => {
40 | if (song?.type === "song") {
41 | const Data = await getSongData(song?.id);
42 | const songData = await Data?.[0];
43 | dispatch(
44 | setActiveSong({
45 | song: songData,
46 | data: currentSongs?.find((s) => s?.id === songData?.id)
47 | ? currentSongs
48 | : [...currentSongs, songData],
49 | i: currentSongs?.find((s) => s?.id === songData?.id)
50 | ? currentSongs?.findIndex((s) => s?.id === songData?.id)
51 | : currentSongs?.length,
52 | })
53 | );
54 | dispatch(setFullScreen(true));
55 | dispatch(playPause(true));
56 | }
57 | };
58 |
59 | return (
60 |
61 |
62 |
63 |
64 | Search results for "{query.replaceAll("%20", " ")}"
65 |
66 |
67 |
Songs
68 | {searchedData && searchedData?.songs?.results?.length > 0 ? (
69 |
70 | {searchedData?.songs?.results?.map((song, index) => (
71 |
{
74 | handlePlayClick(song);
75 | }}
76 | className="flex items-center mt-5 cursor-pointer group border-b-[1px] border-gray-400 justify-between"
77 | >
78 |
79 |
80 |
87 |
91 |
92 |
93 |
94 | {song?.title
95 | ?.replace("'", "'")
96 | ?.replace("&", "&")}
97 |
98 |
99 |
100 |
101 | {song?.primaryArtists && (
102 |
103 | By: {song?.primaryArtists}
104 |
105 | )}
106 |
107 |
108 | ))}
109 |
110 | ) : (
111 |
112 | )}
113 |
114 |
115 |
116 |
117 | {searchedData &&
118 | searchedData?.albums?.results?.length > 0 &&
119 | searchedData?.albums?.results?.map((song) => (
120 |
121 |
122 |
123 | ))}
124 |
125 |
126 |
127 |
128 | {searchedData &&
129 | searchedData?.artists?.results?.length > 0 &&
130 | searchedData?.artists?.results?.map((artist) => (
131 |
132 |
133 |
134 |
141 |
142 | {artist?.title?.replace("&", "&")}
143 |
144 |
145 | {artist?.description && (
146 |
147 | {artist?.description}
148 |
149 | )}
150 |
151 |
152 |
153 |
154 | ))}
155 |
156 |
157 |
158 |
159 | {searchedData &&
160 | searchedData?.albums?.results?.length > 0 &&
161 | searchedData?.playlists?.results?.map((song) => (
162 |
163 |
164 |
165 | ))}
166 |
167 |
168 |
169 |
170 |
171 | );
172 | };
173 |
174 | export default page;
175 |
--------------------------------------------------------------------------------
/src/app/api/userPlaylists/songs/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 | import Playlist from "@/models/Playlist";
6 | import auth from "@/utils/auth";
7 |
8 |
9 | // add song to playlist
10 | export async function POST(req){
11 | const { playlistID, song } = await req.json();
12 | try {
13 | const user = await auth(req);
14 | if (!user) {
15 | return NextResponse.json(
16 | {
17 | success: false,
18 | message: "User not logged in",
19 | data: null
20 | },
21 | { status: 404 }
22 | );
23 | }
24 | await dbConnect();
25 | if (!user) {
26 | return NextResponse.json(
27 | {
28 | success: false,
29 | message: "User not found",
30 | data: null
31 | },
32 | { status: 404 }
33 | );
34 | }
35 | const playlist = await Playlist.findById(playlistID);
36 | if (!playlist) {
37 | return NextResponse.json(
38 | {
39 | success: false,
40 | message: "Playlist not found",
41 | data: null
42 | },
43 | { status: 404 }
44 | );
45 | }
46 | if (playlist.user.toString() !== user._id.toString()) {
47 | return NextResponse.json(
48 | {
49 | success: false,
50 | message: "Unauthorized",
51 | data: null
52 | },
53 | { status: 401 }
54 | );
55 | }
56 | // check if song already exists in playlist
57 | const songExists = playlist.songs.find((s) => s === song);
58 | if (songExists) {
59 | return NextResponse.json(
60 | {
61 | success: false,
62 | message: "Song already exists in playlist",
63 | data: null
64 | },
65 | { status: 400 }
66 | );
67 | }
68 | await Playlist.findByIdAndUpdate(playlistID, {
69 | $push: {
70 | songs: song
71 | }
72 | });
73 | return NextResponse.json(
74 | {
75 | success: true,
76 | message: "Song added to playlist",
77 | data: null
78 | },
79 | { status: 200 }
80 | );
81 | } catch (e) {
82 | console.error(e);
83 | return NextResponse.json(
84 | {
85 | success: false,
86 | message: "Something went wrong",
87 | data: null
88 | },
89 | { status: 500 }
90 | );
91 | }
92 | }
93 |
94 |
95 | // delete song from playlist
96 | export async function DELETE(req){
97 | const { playlistID, song } = await req.json();
98 | try {
99 | const user = await auth(req);
100 | if (!user) {
101 | return NextResponse.json(
102 | {
103 | success: false,
104 | message: "User not logged in",
105 | data: null
106 | },
107 | { status: 404 }
108 | );
109 | }
110 | await dbConnect();
111 | const playlist = await Playlist.findById(playlistID);
112 | if (!playlist) {
113 | return NextResponse.json(
114 | {
115 | success: false,
116 | message: "Playlist not found",
117 | data: null
118 | },
119 | { status: 404 }
120 | );
121 | }
122 | if (playlist.user.toString() !== user._id.toString()) {
123 | return NextResponse.json(
124 | {
125 | success: false,
126 | message: "Unauthorized",
127 | data: null
128 | },
129 | { status: 401 }
130 | );
131 | }
132 | // check if song exists in playlist
133 | const songExists = playlist.songs.find((s) => s === song);
134 | if (!songExists) {
135 | return NextResponse.json(
136 | {
137 | success: false,
138 | message: "Song does not exist in playlist",
139 | data: null
140 | },
141 | { status: 400 }
142 | );
143 | }
144 | await Playlist.findByIdAndUpdate(playlistID, {
145 | $pull: {
146 | songs: song
147 | }
148 | });
149 | return NextResponse.json(
150 | {
151 | success: true,
152 | message: "Song deleted from playlist",
153 | data: playlist.songs
154 | },
155 | { status: 200 }
156 | );
157 | } catch (e) {
158 | console.error(e);
159 | return NextResponse.json(
160 | {
161 | success: false,
162 | message: "Something went wrong",
163 | data: null
164 | },
165 | { status: 500 }
166 | );
167 | }
168 | }
169 |
170 |
171 | // get songs of playlist
172 | export async function GET(req){
173 | const { searchParams } = new URL(req.url);
174 | const playlistID = searchParams.get("playlist");
175 | // console.log('playlistID', playlistID);
176 | // console.log('searchParams', searchParams);
177 | try {
178 | const user = await auth(req);
179 | if (!user) {
180 | return NextResponse.json(
181 | {
182 | success: false,
183 | message: "User not logged in",
184 | data: null
185 | },
186 | { status: 404 }
187 | );
188 | }
189 | await dbConnect();
190 | const playlist = await Playlist.findById(playlistID);
191 | if (!playlist) {
192 | return NextResponse.json(
193 | {
194 | success: false,
195 | message: "Playlist not found",
196 | data: null
197 | },
198 | { status: 404 }
199 | );
200 | }
201 | if (playlist.user.toString() !== user._id.toString()) {
202 | return NextResponse.json(
203 | {
204 | success: false,
205 | message: "Unauthorized",
206 | data: null
207 | },
208 | { status: 401 }
209 | );
210 | }
211 | return NextResponse.json(
212 | {
213 | success: true,
214 | message: "Songs of playlist",
215 | data: playlist
216 | },
217 | { status: 200 }
218 | );
219 | } catch (e) {
220 | console.error(e);
221 | return NextResponse.json(
222 | {
223 | success: false,
224 | message: "Something went wrong",
225 | data: null
226 | },
227 | { status: 500 }
228 | );
229 | }
230 | }
--------------------------------------------------------------------------------
/src/components/SongsList.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import {
4 | playPause,
5 | setActiveSong,
6 | setFullScreen,
7 | } from "@/redux/features/playerSlice";
8 | import { BsPlayFill } from "react-icons/bs";
9 | import { useDispatch } from "react-redux";
10 | import SongListSkeleton from "./SongListSkeleton";
11 | import { BiHeadphone } from "react-icons/bi";
12 | import { useSelector } from "react-redux";
13 | import { PiDotsThreeVerticalBold } from "react-icons/pi";
14 | import { useState } from "react";
15 | import {
16 | addSongToPlaylist,
17 | deleteSongFromPlaylist,
18 | getUserPlaylists,
19 | } from "@/services/playlistApi";
20 | import { useEffect } from "react";
21 | import { toast } from "react-hot-toast";
22 | import { MdOutlineDeleteOutline } from "react-icons/md";
23 |
24 | const SongsList = ({
25 | SongData,
26 | loading,
27 | hidePlays,
28 | isUserPlaylist,
29 | playlistID,
30 | setSongs,
31 | }) => {
32 | console.log("SongData", SongData);
33 | const { activeSong } = useSelector((state) => state.player);
34 | const [showMenu, setShowMenu] = useState(false);
35 | const [playlists, setPlaylists] = useState([]);
36 | const dispatch = useDispatch();
37 |
38 | const handlePlayClick = (song, index) => {
39 | dispatch(setActiveSong({ song, data: SongData, i: index }));
40 | dispatch(setFullScreen(true));
41 | dispatch(playPause(true));
42 | };
43 |
44 | function formatDuration(durationInSeconds) {
45 | const minutes = Math.floor(durationInSeconds / 60);
46 | const seconds = Math.round(durationInSeconds % 60);
47 |
48 | if (minutes > 0) {
49 | return `${minutes}:${seconds.toString().padStart(2, "0")}`;
50 | } else {
51 | return `${seconds}`;
52 | }
53 | }
54 |
55 | useEffect(() => {
56 | const getPlaylists = async () => {
57 | const res = await getUserPlaylists();
58 | if (res?.success == true) {
59 | setPlaylists(res?.data?.playlists);
60 | }
61 | };
62 | getPlaylists();
63 | }, []);
64 |
65 | // add song to playlist
66 | const handleAddToPlaylist = async (song, playlistID) => {
67 | setShowMenu(false);
68 | const res = await addSongToPlaylist(playlistID, song);
69 | if (res?.success == true) {
70 | toast.success(res?.message);
71 | } else {
72 | toast.error(res?.message);
73 | }
74 | };
75 |
76 | // delete song from playlist
77 | const handleDeleteFromPlaylist = async (playlistID, song) => {
78 | setShowMenu(false);
79 | const res = await deleteSongFromPlaylist(playlistID, song);
80 | if (res?.success == true) {
81 | setSongs((prev) => {
82 | return prev.filter((s) => s?.id?.toString() !== song.toString());
83 | });
84 | toast.success(res?.message);
85 | } else {
86 | toast.error(res?.message);
87 | }
88 | };
89 |
90 | return (
91 | <>
92 |
93 | {!loading && SongData?.length > 0 ? (
94 | SongData?.map((song, index) => (
95 |
{
98 | handlePlayClick(song, index);
99 | }}
100 | className={`flex items-center mt-5 cursor-pointer group border-b-[1px] border-gray-400 justify-between ${
101 | activeSong?.id === song?.id && " text-[#00e6e6]"
102 | }`}
103 | >
104 |
105 |
106 |
113 | {activeSong?.id === song?.id ? (
114 |
118 | ) : (
119 |
123 | )}
124 |
125 |
126 |
127 | {song?.name
128 | ?.replace("'", "'")
129 | ?.replaceAll("&", "&")}
130 |
131 |
132 | {song?.artists?.primary
133 | ?.map((artist) => artist.name)
134 | .join(", ")}
135 |
136 |
137 |
138 |
143 | {song?.playCount && (
144 |
{song?.playCount} plays
145 | )}
146 |
147 |
148 |
{formatDuration(song?.duration)}
149 |
150 |
{
152 | e.stopPropagation();
153 | setShowMenu(song?.id);
154 | }}
155 | size={25}
156 | className=" text-gray-300"
157 | />
158 | {showMenu === song?.id && (
159 | {
161 | setShowMenu(false);
162 | }}
163 | className="absolute text-white top-0 right-0 bg-black/50 bg-opacity-80 backdrop-blur-sm rounded-lg p-3 w-32 flex flex-col gap-2 z-40"
164 | >
165 |
166 | {isUserPlaylist ? null : "Add to playlist"}
167 |
168 | {isUserPlaylist ? (
169 |
{
171 | e.stopPropagation();
172 | handleDeleteFromPlaylist(playlistID, song?.id);
173 | }}
174 | className="text-sm font-semibold flex gap-1 items-center hover:underline"
175 | >
176 | Remove
177 |
178 | ) : playlists?.length > 0 ? (
179 | playlists?.map((playlist, index) => (
180 |
{
183 | e.stopPropagation();
184 | handleAddToPlaylist(song?.id, playlist._id);
185 | }}
186 | className="text-sm font-semibold flex gap-1 items-center hover:underline"
187 | >
188 | {playlist?.name}
189 |
190 | ))
191 | ) : (
192 |
193 | No Playlist
194 |
195 | )}
196 |
197 | )}
198 |
199 |
200 |
201 | ))
202 | ) : (
203 |
204 | )}
205 |
206 | {/* overlay */}
207 | {showMenu && (
208 | setShowMenu(false)}
210 | className="fixed top-0 left-0 w-full h-full z-30"
211 | >
212 | )}
213 | >
214 | );
215 | };
216 |
217 | export default SongsList;
218 |
--------------------------------------------------------------------------------