を作成すると、次は無条件のプレイリストをプレビューします.js


私は最近Spotifyに多くの時間を費やしていて、ウェブ上で私のプレイリストのいくつかを共有したがっていました.しかし、ちょうどプレイリストにリンクしていないので、私はいくつかの時間は、実際には、アカウントにログインする訪問者を必要とせずに可能だった参照してくださいSpotify APIを使用して周りを散策過ごした.
それがわかるように、我々は非常に多くのことができます!それで、今日、我々は次で単純なSpotifyプレイリスト・プレビューを構築するつもりです.js!こちらですa preview 我々はビルになるか、またはあなたが閲覧することができますsource code .
始めましょう!

足場


最初のオフ、選択のフレームワークやフレームワークを選択します.使っているNext.js …よくそれは素晴らしいです、私はすべてのためにそれを使用しますが、同様に作成するようにアプリを反応のような何かでこれを行うことができます.私も使用しますTailwind スーパークイックスタイリング用.

プレイリストの取得


私たちは、Spotify REST APIからプレイリストを取り出すユーティリティファイルを欲しています.これには2つの機能があります.

getaccessstoken


この関数は、その名前が示すように、Spotify Web APIを使用するように権限を与えます.具体的には、クライアントの資格情報の流れを使用します-サーバーへのサーバーの認証方法だけでは、ユーザー情報にアクセスしないエンドポイントが含まれます.したがって、私たちのプレイリストが公開される限り、これは我々のニーズのためにうまくいきます.
あなたは彼らのSpotifyアプリを作成する必要がありますDashboard そして、あなたのクライアントのIDと秘密をつかむ.保存これらのどこか安全な.env.local ファイル.
SpotifyのAPIは少し奇妙な/それはapplication/x-www-form-urlencoded あなたが使用している場合に対処するトリッキーすることができますFetch 閉じるこの動画はお気に入りから削除されています.幸いにも、この場合、それは我々がハードコードすることができる単純なストリングです.
const getAccessToken = async (): Promise<string> => {
  const authorization = Buffer.from(
    `${process.env.SPOTIFY_CLIENT_ID ?? ''}:${
      process.env.SPOTIFY_CLIENT_SECRET ?? ''
    }`
  ).toString('base64');
  const grant = await fetch('https://accounts.spotify.com/api/token', {
    method: 'POST',
    headers: {
      Authorization: `Basic ${authorization}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: 'grant_type=client_credentials',
  });

  const { access_token } = (await grant.json()) as { access_token: string };

  return access_token;
};
パーフェクト.今、我々はプレイリストのIDを受け取り、すべての相対的な情報やトラックを返すシンプルな機能を使用して、私たちのプレイリストを取得することができます.

GetPlaylist


Spotifyの表面レベル/playlist/:id エンドポイントは、プレイリストについてのハイレベルの情報だけを返します.しかし、それがトラックに来るとき、大きな詳細に入らないので、我々はまた、より深いものへのその後の呼び出しをする必要があります/playlist/:id/tracks エンドポイントをいくつかのデータを取得します.
export const getPlaylist = async (
  id: string
): Promise<{
  data: SpotifyPlaylist;
  tracks: SpotifyTrack[];
}> => {
  const access_token = await getAccessToken();

  const tracksRequest = await fetch(
    `https://api.spotify.com/v1/playlists/${id}/tracks`,
    {
      headers: {
        Authorization: `Bearer ${access_token}`,
      },
    }
  );

  const { items } = (await tracksRequest.json()) as {
    items: SpotifyTrack[];
  };

  const playlistRequest = await fetch(
    `https://api.spotify.com/v1/playlists/${id}`,
    {
      headers: {
        Authorization: `Bearer ${access_token}`,
      },
    }
  );

  const data = (await playlistRequest.json()) as SpotifyPlaylist;

  return {
    data,
    items,
  };
};
今、私たちは、クライアント資格情報の流れを使用してSpotify REST APIをヒットし、我々のプレイリスト+詳細にすべてのトラックを取得ユーティリティ機能があります.画面に描きましょう!

プレイリストのレンダリング


を使用している場合.JS、この次の部分は超簡単です.このスクリプトを実行し、静的生成のためのプロップとしてデータを取得するには、次のように追加します.
export const getStaticProps: GetStaticProps = async () => {
  const { data, items } = await getPlaylist('<your playlist id>');

  return {
    props: {
      data,
      items,
    },
  };
};
さて、実際にこのデータをレンダリングする前に、いくつかのニッチなデータ属性を追加します.

プレイリストの持続時間


Spotifyのプレイリストのエンドポイントは、実際に実行中のプレイリストの期間を追跡していないが、それは問題ではない我々はすべての個々のトラックを取得し、彼らはduration_ms フィールド-ミリ秒単位のトラックの持続時間.
したがって、ワンドを振って、この情報を簡単に減らすことができます.
const getPlaylistDuration = (tracks: SpotifyTrack[]) => (
  (tracks.reduce((acc, track) => acc + track.track.duration_ms, 0) / 3600000).toFixed(1)
);
The 3600000 BTWは1000ミリ秒* 60 * 60です.そして、答えは時間に関して与えられますtoFixed 小数点以下1桁までの解決.

アーティスト数


プレイリストのアーティストの数を表示するには、我々はトラック上で同様のループを行う必要があります.まず、私はすぐに明らかにするだろう素晴らしい理由のために、我々はプレイリスト内のアーティストの記録だけでなく、彼らが表示される方法を何度も行う予定です
const getArtists = (tracks: SpotifyTrack[]) => {
  const artists: { name: string; count: number }[] = [];

  tracks.forEach((track) => {
    track.track.artists.forEach((artist) => {
      const existing = artists.find(({ name }) => name === artist.name);

      if (existing) {
        existing.count += 1;
      } else if (artist.name) {
        artists.push({ name: artist.name, count: 1 });
      }
    });
  });

  return artists;
};
それから、アーティストの数を得るために、我々は単に動くことができます:
const uniqueArtists = new Set(artists.map((artist) => artist.name)).size;

トップアーティスト


今、私は以前のループでアーティストの周波数の前に追跡したい理由は、このプレイリストの中で最も人気のある(または具体的には、定期的に)アーティストに基づいて説明を生成することです!このように5トップアーティストを解決できます.
const getTopArtists = (artists: { name: string; count: number }[]) =>
  artists
    .sort((artist1, artist2) => (artist2.count > artist1.count ? 1 : -1))
    .slice(0, 5)
    .map((artist) => artist.name);

const topArtists = getTopArtists(artists);
釘づけにした.すべてのこのダイナミックな情報を我々がプレイリスト終点から得る静的な情報と結合することによって、我々はかなり気の利いたページをつくることができます:
const Playlist: FC<PlaylistProps> = ({ data, tracks }) => {
  const duration = getPlaylistDuration(tracks);
  const artists = getArtists(tracks);
  const uniqueArtists = new Set(artists.map((artist) => artist.name)).size;
  const topArtists = getTopArtists(artists);
  const description = data.description.endsWith('.')
    ? data.description
    : `${data.description}.`;

  return (
    <div className="container mx-auto grid max-w-2xl gap-8 py-24 px-4">
      <div className="grid gap-4">
        <h1 className="text-3xl font-semibold text-gray-900 dark:text-white">
          {data.name}
        </h1>
        <p className="text-md font-normal text-gray-900 dark:text-white">
          <span>{decode(description)} </span>
          <span>Featuring {formatter.format(topArtists)}.</span>
        </p>
        <p className="text-sm text-gray-500 dark:text-gray-400">
          {[
            `${duration} hours`,
            `${data.tracks.total} tracks`,
            `${uniqueArtists.size} artists`,
          ].join(' · ')}
        </p>
      </div>
      <div>
        <a
          className="inline-flex items-center gap-2 rounded-md bg-[#1DB965] py-3 px-5 text-white transition-all hover:-translate-y-1 hover:bg-[#139E53]"
          href={data.external_urls.spotify}
        >
          <Image src="/spotify.svg" width={16} height={16} alt="" />
          <span>Open in Spotify</span>
          <ArrowUpRight size={16} />
        </a>
      </div>
      <div>{tracks.map(Track)}</div>
    </div>
  );
};
私は私たちのトラックをレンダリングするための下部に小さなループを残したので、コールバック関数として使用するトラックコンポーネントを記述しましょう!

トラックのレンダリング


トラック自体をレンダリングするには難しすぎる必要はありません.
const Track = ({ track }: SpotifyTrack, index: number) => (
  <Fragment key={track.id}>
    {Boolean(index) && (
      <hr className="border-t border-gray-100 dark:border-gray-800" />
    )}
    <div className="relative flex items-center gap-4 p-2">
      <div className="relative flex shrink-0 overflow-hidden rounded-sm">
        <Image src={track.album.images[0].url} width={48} height={48} />
      </div>
      <div className="relative flex flex-1 flex-col">
        <p className="text-md leading-normal text-gray-900 line-clamp-1 dark:text-white">
          {track.name}
        </p>
        <p className="text-sm text-gray-500 line-clamp-1 dark:text-gray-400">
          {track.artists[0].name} &bull; {track.album.name}
        </p>
      </div>
    </div>
  </Fragment>
);
今、本当の仕事が始まる!

ホバーのプレビュー


私たちがトラックに乗り上げるとき、できれば歌のプレビューをしたいです.狂った難しい右の音?我々のために幸運な、Spotifyは時々Aを返しますpreview_url フルトラックの30秒のMP 3プレビューを指すトラックオブジェクトに.
我々はいくつかの時間ベースのJSトリックでこれを組み合わせる場合は、実際には、オーディオをホバーでフェードインすることができます!ここで注意すべき重要なことは、私たちにはできないことですawait the play() たとえそれが約束であるとしても.それを待つことによって、我々は本質的に我々がホバリングした後にあまりにすぐにトラックから立ち上がるとき、大混乱をすることができる機能をロックしています.
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
const [fadeIn, setFadeIn] = useState<ReturnType<typeof setInterval> | null>(
  null
);

const play = () => {
  if (audio || !track.preview_url) {
    return;
  }

  const newAudio = new Audio(track.preview_url);
  newAudio.volume = 0;

  setActiveTrack(track.id);

  newAudio
    .play()
    .catch((error) => {
      const message =
        error instanceof Error ? error.message : (error as string);
      toast.error(message);
    });

  const timer = setInterval(() => {
    if (newAudio.volume < 1) {
      newAudio.volume = Number((newAudio.volume + 0.05).toFixed(2));
    } else if (fadeIn) {
      clearInterval(fadeIn);
    }
  }, 100);

  setFadeIn(timer);
  setAudio(newAudio);
};
今、ぼかし(ホバーオフ)機能のために.これは、オーディオをフェードアウトするためのロジックの同じタイプを採用しています.
const stop = () => {
  if (!audio) {
    return;
  }

  const originalVolume = audio.volume;

  setAudio(null);
  setActiveTrack('');

  if (fadeIn) {
    clearInterval(fadeIn);
  }

  setFadeOut(
    setInterval(() => {
      if (audio.volume > 0) {
        audio.volume = Number((audio.volume - 0.05).toFixed(2));
      } else if (fadeOut) {
        clearInterval(fadeOut);
      }
    }, 100)
  );

  setTimeout(() => {
    audio.pause();
  }, (originalVolume / 0.05) * 100);
};
驚くべき!我々はすぐにプレビューすることができますSpotifyを追跡するだけでそれらを認証する必要がなく、または全体のWebプレーヤーを構築する必要があります.今、我々は以前からこれらの機能を我々のトラック構成要素に配線する必要があるだけです.
トラックが再生を開始するとき、私はユーザーが何が起こっているかを示すためにソートの進行状況バーを表示したい.私たちのプレビューのURLは常に30秒ですので、我々はここでカンニングのビットを使用することができますし、からの移行をdivを作成するwidth: 0 to width: 100% 30秒の遷移時間.
<Fragment key={track.id}>
  {Boolean(index) && (
    <hr className="border-t border-gray-100 dark:border-gray-800" />
  )}
  <div
    className={`relative transition-opacity ${
      activeTrack && activeTrack !== track.id ? 'opacity-50' : 'opacity-100'
    }`}
    onMouseOver={play}
    onMouseLeave={stop}
    onFocus={play}
    onBlur={stop}
    role="button"
    tabIndex={0}
  >
    {Boolean(track.preview_url) && (
      <div
        className={`
        absolute left-0 top-0 h-full bg-gray-100 dark:bg-gray-800
        ${
          audio
            ? 'w-full transition-all duration-[30s] ease-linear'
            : 'w-0'
        }
      `}
      />
    )}
    <div className="relative flex items-center gap-4 p-2">
      <div className="relative flex shrink-0 overflow-hidden rounded-sm">
        <Image src={track.album.images[0].url} width={48} height={48} />
      </div>
      <div className="relative flex flex-1 flex-col">
        <p className="text-md leading-normal text-gray-900 line-clamp-1 dark:text-white">
          {track.name}
        </p>
        <p className="text-sm text-gray-500 line-clamp-1 dark:text-gray-400">
          {track.artists[0].name} &bull; {track.album.name}
        </p>
      </div>
    </div>
  </div>
</Fragment>

ブラウザ制約の取り扱い


最近のブラウザには厳しいものがあるautoplay ビデオとオーディオに適用されるポリシー.彼らはユーザーとして私たちのための素晴らしい、ユーザーエクスペリエンスの結果、広告ブロッカーをインストールし、データ消費を削減するインセンティブを最小限に.しかし、開発者として、我々はそれに対処するために少し多くの仕事をする必要があることを意味します.
に戻るplay() 以前からの機能、ユーザーが最初にドキュメントと相互作用することなくトラックに乗っているならば、それは遊びません.代わりに、コンソールエラーを言います.
Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first.
私たちがこれについて多くのことをすることができません.そして、問題をうまく処理して、ユーザーにそれが働くことを得るために何をしなければならないかについてわかっています.
また、すぐにトラックをオン・オフすると、2つの関数で競合状態が生成されます.
Uncaught (in promise) DOMException: The play() request was interrupted by a call to pause().
ここではどのように変更することができますplay() 以前から機能して、これらのケースをうまく示すことtoast :
import { createGlobalState } from 'react-hooks-global-state';

const { useGlobalState } = createGlobalState({
  interactableNotified: false,
  activeTrack: '',
});
const [interactable, setInteractable] = useState<boolean>(false);
const [interactableNotified, setInteractableNotified] = useGlobalState(
  'interactableNotified'
);

newAudio
  .play()
  .then(() => {
    setInteractable(true);
    if (interactableNotified) {
      setInteractableNotified(false);
      toast.success('Nice! You’re good to go.');
    }
  })
  .catch((error) => {
    const message =
      error instanceof Error ? error.message : (error as string);
    if (message.includes("user didn't interact with the document first")) {
      if (!interactableNotified) {
        toast(
          'Please click anywhere on the page to preview tracks on hover.'
        );
        setInteractableNotified(true);
        return;
      }
      return;
    }

    if (!message.includes('interrupted by a call to pause()')) {
      toast.error(message);
    }
  });

警告


上記のコードは、まだそれが頼りにモバイルで動作しませんmouseEnter / mouseLeave ハンドラが、私は肯定的な思考のビットを理解することができます.プラス側では、それはまた、トラックのプレビューを介してタブを使用することができますので、キーボードにアクセス可能です!
とにかく、それはすべての人々です!ハッピーリスニングとspotify私に従うことを忘れないでください😝