Skip to content

Commit

Permalink
Search Page: Add Youtube provider (#4)
Browse files Browse the repository at this point in the history
* WIP

* wip

* wip

* ✨ youtube search works fine (wip: CSS)

* wip: ytmusic-api

* WIP

* 🔧 next config images

* ✨ switched to yt-music api -> /search is done

* 🐛 TS build error

* ♻️ search page - improve loading status

* ♻️ fix client things

* ♻️ moved playerRef to zustand YTPlayerStore

* ✨ add volume control and better handling of currentPlaybackState.device

* ✨ handle youtube player mute

* ♻️ youtube search works fine

* ♻️ remove contextUri on search page tracklist

* ✨ add search dropdown to select a provider

* ♻️ clownesque

* 💄 search bar

* ♻️ add "origin" key in track object

* 🐛 previous tracks are now hidden when search page is fetching data

* ♻️ add "origin" key in tracks

* ♻️

---------

Co-authored-by: Faïssal <fhattou.ext@unowhy.com>
Co-authored-by: Faïssal <fhattou@ippon.fr>
  • Loading branch information
3 people authored Feb 7, 2024
1 parent 6a4eab7 commit 5f3176b
Show file tree
Hide file tree
Showing 41 changed files with 939 additions and 157 deletions.
8 changes: 7 additions & 1 deletion app/album/[albumId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import TrackListHeader from "@/components/TrackListHeader";

import AlbumCopyrights from "./AlbumCopyrights";
import AlbumReleaseDate from "./AlbumReleaseDate";
import { FullTrack, TrackOrigin } from "@/types";

const Album: NextPage = () => {
const { albumId } = useParams();
Expand All @@ -38,6 +39,11 @@ const Album: NextPage = () => {

if (error) return "Error....";

const tracks: FullTrack[] | undefined = album?.tracks.items.map((item) => ({
...item,
origin: TrackOrigin.SPOTIFY,
}));

return (
<>
<div className="sm:relative">
Expand All @@ -61,7 +67,7 @@ const Album: NextPage = () => {
showAlbumName: false,
showPlaybackControls: true,
}}
tracks={album?.tracks.items}
tracks={tracks}
/>
</div>

Expand Down
24 changes: 24 additions & 0 deletions app/api/search/youtube/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";
import YTMusic from "ytmusic-api";

export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const query = searchParams.get("query");

if (!query) throw new Error("search/youtube: query not provided");

const ytmusic = new YTMusic();

try {
await ytmusic.initialize();
const songs = await ytmusic.searchSongs(query);
const videos = await ytmusic.searchVideos(query);

// TODO display the most closest to the search
const result = await Promise.all([...songs, ...videos]);

return NextResponse.json({ result });
} catch (error) {
console.log("Catch", error);
}
}
4 changes: 1 addition & 3 deletions app/library/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { NextPage } from "next";

import Container from "@/components/Container";
import CustomLink from "@/components/CustomLink";

const Library: NextPage = () => {
const Library = () => {
return (
<Container>
<div className="space-y-2">
Expand Down
9 changes: 8 additions & 1 deletion app/library/saved-tracks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { useCallback } from "react";

import { FullTrack, TrackOrigin } from "@/types";

import useFetch from "@/hooks/useFetch";
import useSpotify from "@/hooks/useSpotify";

Expand All @@ -16,7 +18,12 @@ function SavedTracks() {
[spotifyApi]
);
const savedTracks = useFetch(getSavedTracks);
const formattedSavedTracks = savedTracks?.items.map((item) => item.track);
const formattedSavedTracks: FullTrack[] | undefined = savedTracks?.items.map(
(item) => ({
...item.track,
origin: TrackOrigin.SPOTIFY,
})
);

if (!savedTracks) return null;

Expand Down
11 changes: 8 additions & 3 deletions app/playlist/[playlistId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { NextPage } from "next";
import { useParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";

import { FullTrack, TrackOrigin } from "@/types";

import useDominantColor from "@/hooks/useDominantColor";
import useSpotify from "@/hooks/useSpotify";

Expand Down Expand Up @@ -36,18 +38,21 @@ const Playlist: NextPage = () => {
const dominantColor = useDominantColor(playlist?.images[0].url);
const backgroundColor = generateRGBString(dominantColor);

const formattedItems = useMemo(
const formattedItems: FullTrack[] | undefined = useMemo(
() =>
playlist?.tracks.items
.filter((item) => !item.is_local) // TODO remove this filter when LocalTrack component is ready
.map((item) => item.track),
.map((item) => ({
...(item.track as SpotifyApi.TrackObjectFull),
origin: TrackOrigin.SPOTIFY,
})),
[playlist]
);

if (error || isPending) return null;

const formattedTracks = {
...playlist.tracks,
...playlist?.tracks,
items: formattedItems ?? [],
};

Expand Down
98 changes: 82 additions & 16 deletions app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@

import { useQuery } from "@tanstack/react-query";

import {
FullTrack,
SearchProvider,
TrackOrigin,
YTMusicSongDetailed,
} from "@/types";

import useSpotify from "@/hooks/useSpotify";

import searchMapper from "@/lib/searchMapper";

import BlurBackground from "@/components/BlurBackground";
import Container from "@/components/Container";
import HorizontalSlider from "@/components/HorizontalSlider";
Expand All @@ -18,42 +27,77 @@ type SearchType =
| "show"
| "episode";

const Search = ({ searchParams }: { searchParams?: { query: string } }) => {
const Search = ({
searchParams,
}: {
searchParams: { provider?: SearchProvider; query?: string };
}) => {
const spotifyApi = useSpotify();
const { provider, query } = searchParams;

const search = async () => {
if (!searchParams?.query) return;
if (!query) return;

const types: SearchType[] = ["album", "artist", "playlist", "track"];

const { body } = await spotifyApi.search(searchParams.query, types, {
const { body } = await spotifyApi.search(query, types, {
limit: 5,
});

return body;
};

const {
isPending,
error,
data: searchResponse,
} = useQuery({
queryKey: ["search", searchParams?.query],
const { isFetching, data: searchResponse } = useQuery({
queryKey: ["search", query],
queryFn: search,
enabled: Boolean(query) && provider !== "youtube",
});

const artists = searchResponse?.artists?.items;
const albums = searchResponse?.albums?.items;
const playlists = searchResponse?.playlists?.items;

const searchYoutube = async () => {
if (!query) return;

try {
const res = await fetch(`/api/search/youtube?query=${query}`);
const json = await res.json();

const result = json.result as YTMusicSongDetailed[];

return result;
} catch (error) {
console.error(error);
}
};

const { isFetching: isYtbFetching, data: searchYoutubeResponse } = useQuery({
queryKey: ["search-ytb", query],
queryFn: searchYoutube,
enabled: Boolean(query) && provider === "youtube",
});

const tracks = searchResponse?.tracks?.items ?? [];
const artists = searchResponse?.artists?.items ?? [];
const albums = searchResponse?.albums?.items ?? [];
const playlists = searchResponse?.playlists?.items ?? [];
const tracks: FullTrack[] | undefined = searchResponse?.tracks?.items.map(
(item) => ({
...item,
origin: TrackOrigin.SPOTIFY,
})
);

const formattedSearchResponse = searchMapper(searchYoutubeResponse ?? []);
const ytbTracks: FullTrack[] = formattedSearchResponse.map((item) => ({
...item,
origin: TrackOrigin.YOUTUBE,
}));

return (
<Container>
<BlurBackground />
<div className="space-y-4 sm:space-y-8">
<SearchBar />

{isPending && (
{provider !== "youtube" && isFetching && (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold lowercase mb-2">tracks</h1>
Expand All @@ -75,10 +119,9 @@ const Search = ({ searchParams }: { searchParams?: { query: string } }) => {
</div>
)}

{searchResponse && (
{provider !== "youtube" && searchResponse && (
<div className="space-y-6">
<TrackList
contextUri={tracks[0].album.uri}
options={{
showCoverWithPlayButton: true,
showOrder: true,
Expand All @@ -104,6 +147,29 @@ const Search = ({ searchParams }: { searchParams?: { query: string } }) => {
</div>
</div>
)}

{provider === "youtube" && isYtbFetching && (
<div className="space-y-6">
<h1 className="text-3xl font-bold lowercase mb-2">tracks</h1>
<TrackList.Skeleton length={5} />
</div>
)}

{provider === "youtube" && searchYoutubeResponse && !isYtbFetching && (
<div className="flex items-start justify-between gap-4">
<div className="w-full">
<TrackList
options={{
showCoverWithPlayButton: true,
showOrder: true,
showVisualizer: true,
}}
tracks={ytbTracks}
title="tracks"
/>
</div>
</div>
)}
</div>
</Container>
);
Expand Down
3 changes: 1 addition & 2 deletions app/studio/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { useState } from "react";
import { NextPage } from "next";
import { LightbulbIcon, LightbulbOffIcon } from "lucide-react";
import { SlSizeFullscreen } from "react-icons/sl";
import { MdCloseFullscreen } from "react-icons/md";
Expand All @@ -15,7 +14,7 @@ import generateRGBString from "@/lib/generateRGBString";
import Vinyl from "@/components/Vinyl";
import { Button } from "@/components/ui/button";

const Studio: NextPage = () => {
const Studio = () => {
const currentPlaybackState = usePlayerStore((s) => s.currentPlaybackState);
const [useAlbumColor, setUseAlbumColor] = useState(true);
const [isFullScreen, setIsFullScreen] = useState<boolean>(false);
Expand Down
7 changes: 7 additions & 0 deletions assets/spotify-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions assets/youtube-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions components/ContextMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { Dispatch, SetStateAction, useState } from "react";
import Link from "next/link";

Expand Down
1 change: 1 addition & 0 deletions components/HorizontalSlider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type HorizontalSliderItems =
| SpotifyApi.ArtistObjectFull[]
| SpotifyApi.AlbumObjectFull[]
| SpotifyApi.AlbumObjectSimplified[]
// | SpotifyApi.TrackObjectFull[]
| SpotifyApi.PlaylistObjectSimplified[];

interface HorizontalSliderProps {
Expand Down
5 changes: 1 addition & 4 deletions components/Player/ClosedPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import Link from "next/link";
import { ChevronUpIcon } from "lucide-react";
import { AiOutlineArrowsAlt } from "react-icons/ai";

import { usePlayerStore } from "@/store/usePlayerStore";

import useTrack from "@/hooks/useTrack";
import useDominantColor from "@/hooks/useDominantColor";

import generateRGBString from "@/lib/generateRGBString";

import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";

import Link from "next/link";

import Controls from "./Controls";
import CurrentTrack from "./CurrentTrack";
import DeviceSelector from "./DeviceSelector";
Expand Down
Loading

0 comments on commit 5f3176b

Please sign in to comment.