fixes for some play states, mapping track metadata

This commit is contained in:
2023-11-08 16:56:17 -06:00
parent 8c862342de
commit f05f2c59e8
9 changed files with 163 additions and 71 deletions

View File

@@ -9,22 +9,21 @@ export default async function ListenByCollectionID({ params }: { params: { colle
if (!id) return <NotFound /> if (!id) return <NotFound />
const controller = new MusicController(); const controller = new MusicController();
const result = await controller.getByID(id); const collection = await controller.getByID(id);
if (!result) return <NotFound /> if (!collection) return <NotFound />
// const path = S3Service.asEndpoint(result.pathtoentry); const trackList = await S3Service.prepareTrackList(collection.pathtoentry);
const entries = await S3Service.getURLs(result.pathtoentry);
return ( return (
<div> <div>
<header> <header>
<h1>{result.name}</h1> <h1>{collection.name}</h1>
<p>{result.shortdescription}</p> <p>{collection.shortdescription}</p>
</header> </header>
<p>{result.longdescription}</p> <p>{collection.longdescription}</p>
<Suspense fallback={<p>Loading...</p>}> <Suspense fallback={<p>Loading...</p>}>
<AudioGallery urlList={entries} /> <AudioGallery trackList={trackList} collection={collection} />
</Suspense> </Suspense>
</div> </div>
) )

View File

@@ -1,14 +1,18 @@
'use client'; 'use client';
import { MusicStreamingEntry } from "@/server/db/schema";
import { AudioTrack } from "./AudioTrack" import { AudioTrack } from "./AudioTrack"
import { Maybe } from "@/util/helpers"; import { _Object } from "@aws-sdk/client-s3";
import { TrackWithURL } from "@/server/s3/service";
export function AudioGallery({ urlList }: { urlList: Maybe<string[]> }) { export function AudioGallery({ trackList, collection }: { trackList: TrackWithURL[], collection: MusicStreamingEntry }) {
return ( return (
<div> <div className="p-8 m-8 rounded-lg bg-neutral-600">
{ urlList { trackList.length
? urlList.map((each, idx) => { ? trackList.map((each, idx) => {
return <AudioTrack src={each} key={idx} /> if (!(each.Key.includes(".wav") || each.Key.includes(".mp3"))) return <div key={idx} />
}) : <p>No audio results found for this work.</p> return <AudioTrack metadata={each} collection={collection} key={idx} />
})
: <p>No audio results found for this work.</p>
} }
</div> </div>
) )

View File

@@ -1,14 +1,14 @@
type ButtonProps = { type ButtonProps = {
children: React.ReactNode; children: React.ReactNode;
conditionalExp: string;
onClick: (...args: []) => void; onClick: (...args: []) => void;
extraClasses?: string;
disabled?: boolean disabled?: boolean
} }
export function _Button({ children, conditionalExp, onClick, disabled=false }: ButtonProps) { export function _Button({ children, extraClasses="", onClick, disabled=false }: ButtonProps) {
return ( return (
<button disabled={disabled} onClick={onClick} className={ <button disabled={disabled} onClick={onClick} className={
conditionalExp + " transition-all duration-700 rounded-full bg-stone-400 border-black h-16 w-16 flex flex-col items-center justify-center" extraClasses + " transition-all duration-700 rounded-full bg-stone-400 border-black h-16 w-16 flex flex-col items-center justify-center"
}> }>
{ children } { children }
</button> </button>

View File

@@ -5,9 +5,10 @@ import { FaArrowDown, FaArrowUp, FaPause, FaPlay } from "react-icons/fa";
import { useCallback } from "react"; import { useCallback } from "react";
import { _Button } from "./_Button"; import { _Button } from "./_Button";
import Link from "@/components/ui/Link"; import Link from "@/components/ui/Link";
import { prettyFileName } from "@/util/helpers";
export default function AudioPlayer() { export default function AudioPlayer() {
const { isPlaying, isOpen, currentTrack, setIsPlaying, setIsOpen } = useAudio(); const { isPlaying, isOpen, currentTrack, currentCollection, setIsPlaying, setIsOpen } = useAudio();
const changePlayState = useCallback(() => { const changePlayState = useCallback(() => {
if (!currentTrack) { if (!currentTrack) {
@@ -19,13 +20,14 @@ export default function AudioPlayer() {
}, [currentTrack, isPlaying, setIsPlaying]) }, [currentTrack, isPlaying, setIsPlaying])
return ( return (
<div id="audio-player-panel" className={`${isOpen ? "bg-stone-300 border-black dark:bg-black dark:border-stone-300 text-black dark:text-stone-300 " : "bg-opacity-0 "} fixed bottom-0 right-0 flex items-center justify-between w-1/2 h-36 rounded-tl-xl z-0 transition-all duration-700 px-8`}> <>
<div id="audio-player-panel" className={`${isOpen ? "bg-stone-300 border-black dark:bg-black dark:border-stone-300 text-black dark:text-stone-300 translate-y-0 " : " translate-y-32 "} fixed bottom-0 right-0 flex items-center justify-between w-1/2 h-36 rounded-tl-xl z-0 transition-all duration-700 px-8`}>
{/* track info, if it is set */} {/* track info, if it is set */}
<div className="flex flex-col"> <div className="flex flex-col">
{ !isOpen ? null : currentTrack ? ( { !isOpen ? null : currentTrack ? (
<> <>
<h3>{currentTrack.name}</h3> <h3>{prettyFileName(currentTrack.Key)}</h3>
<p>{currentTrack.artist ?? "(no artist listed)"} - {currentTrack.year ?? "(no year listed)"}</p> <p>{currentCollection?.artist ?? "(no artist listed)"} - {currentCollection?.year ?? "(no year listed)"}</p>
</> </>
) : ( ) : (
<> <>
@@ -35,23 +37,24 @@ export default function AudioPlayer() {
)} )}
</div> </div>
<div id="audio-panel-actions" className="flex items-center justify-end"> <div id="audio-panel-actions" className="flex items-center justify-end pr-16">
{/* conditionally-rendered button to close audio player once it's open */} {/* conditionally-rendered button to close audio player once it's open */}
<_Button disabled={!currentTrack} conditionalExp={isOpen ? "opacity-100 translate-y-0 mr-4 " : "opacity-0 translate-y-32 mr-4 "} onClick={changePlayState}> <_Button extraClasses={"fixed right-28 " + (isOpen ? "-bottom-30" : "bottom-10")} disabled={!currentTrack} onClick={changePlayState}>
{ isPlaying { isPlaying
? <FaPause className="h-8 w-8 text-black" /> ? <FaPause className="h-8 w-8 text-black" />
: <FaPlay className="h-8 w-8 text-black" /> : <FaPlay className="h-8 w-8 text-black" />
} }
</_Button> </_Button>
<_Button onClick={() => setIsOpen(!isOpen)} conditionalExp={isOpen ? "right-36" : "right-12"}>
{ isOpen
? <FaArrowDown className="h-8 w-8 text-black" />
: <FaArrowUp className="h-8 w-8 text-black" />
}
</_Button>
</div> </div>
</div> </div>
{/* floating button to open/close the player */}
<_Button extraClasses="fixed bottom-10 right-10" onClick={() => setIsOpen(!isOpen)}>
{ isOpen
? <FaArrowDown className="h-8 w-8 text-black" />
: <FaArrowUp className="h-8 w-8 text-black" />
}
</_Button>
</>
) )
} }

View File

@@ -1,4 +1,49 @@
'use client'; 'use client';
export function AudioTrack({ src }: { src: string }) { import useAudio from "@/hooks/useAudio";
return <audio src={src} controls /> import { MusicStreamingEntry } from "@/server/db/schema";
import { TrackWithURL } from "@/server/s3/service";
import { prettyFileName } from "@/util/helpers";
import { useCallback, useMemo } from "react";
import { FaPause, FaPlay } from "react-icons/fa";
export function AudioTrack({ collection, metadata }: { collection: MusicStreamingEntry, metadata: TrackWithURL }) {
const { isPlaying, currentTrack, setIsPlaying, handleTrackChange } = useAudio();
const isMatchingTrack: boolean = useMemo(() => {
return currentTrack?.Key == metadata.Key;
}, [currentTrack, metadata]);
const handleClick = useCallback(() => {
console.log({ isMatchingTrack, isPlaying, currentTrack })
if (!currentTrack) {
handleTrackChange(metadata, collection);
return;
}
if (isMatchingTrack && isPlaying) {
setIsPlaying(false);
return;
}
handleTrackChange(metadata, collection);
}, [currentTrack, collection, metadata, isMatchingTrack, isPlaying, setIsPlaying, handleTrackChange])
return (
<div className="flex">
<div>
<button className="mr-3" onClick={handleClick}>
{ isMatchingTrack && isPlaying
? <FaPause className="h-8 w-8 text-black" />
: <FaPlay className="h-8 w-8 text-black" />
}
</button>
</div>
<div className="flex flex-col">
<h3>{prettyFileName(metadata.Key)}</h3>
{ collection.artist && <small>{collection.artist}</small> }
</div>
</div>
)
} }

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { MusicStreamingEntry } from "@/server/db/schema"; import { MusicStreamingEntry } from "@/server/db/schema";
import { createContext, createRef, useCallback, useContext, useRef, useState } from "react"; import { TrackWithURL } from "@/server/s3/service";
import { createContext, createRef, useCallback, useContext, useEffect, useRef, useState } from "react";
type AudioContextType = { type AudioContextType = {
// boolean properties // boolean properties
@@ -10,16 +11,17 @@ type AudioContextType = {
setIsOpen: (isOpen: boolean) => void; setIsOpen: (isOpen: boolean) => void;
// track properties // track properties
/** @deprecated */ // duration: number;
duration: number; // volume: number;
volume: number; // setVolume: (volume: number) => void;
setVolume: (volume: number) => void; // currentTime: number;
currentTime: number; // setCurrentTime: (currentTime: number) => void;
setCurrentTime: (currentTime: number) => void; handleTrackChange: (metadata: TrackWithURL, collection: MusicStreamingEntry) => void;
handleTrackChange: (audioSource: string, track: MusicStreamingEntry) => void;
// identifiers // identifiers
currentTrack?: MusicStreamingEntry; currentTrack?: TrackWithURL;
currentCollection?: MusicStreamingEntry;
setCurrentCollection: (collection: MusicStreamingEntry) => void;
} }
const initialContext: AudioContextType = { const initialContext: AudioContextType = {
@@ -27,38 +29,48 @@ const initialContext: AudioContextType = {
setIsPlaying: () => {}, setIsPlaying: () => {},
isOpen: false, isOpen: false,
setIsOpen: () => {}, setIsOpen: () => {},
volume: 0.5, // volume: 0.5,
setVolume: () => {}, // setVolume: () => {},
duration: 0, // duration: 0,
currentTime: 0, // currentTime: 0,
setCurrentTime: () => {}, // setCurrentTime: () => {},
currentTrack: undefined,
handleTrackChange: () => {}, handleTrackChange: () => {},
currentTrack: undefined,
currentCollection: undefined,
setCurrentCollection: () => {},
} }
const AudioContext = createContext<AudioContextType>(initialContext); const AudioContext = createContext<AudioContextType>(initialContext);
const AudioProvider = ({ children }: { children: React.ReactNode }) => { const AudioProvider = ({ children }: { children: React.ReactNode }) => {
const [currentTrack, setCurrentTrack] = useState<MusicStreamingEntry>(); const [currentCollection, setCurrentCollection] = useState<MusicStreamingEntry>();
const [currentTrack, setCurrentTrack] = useState<TrackWithURL>();
const [audioSource, setAudioSource] = useState<string>(); const [audioSource, setAudioSource] = useState<string>();
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [volume, setVolume] = useState(0.5); // const [volume, setVolume] = useState(0.5);
const [duration, setDuration] = useState(0); // const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0); // const [currentTime, setCurrentTime] = useState(0);
const audioRef = createRef<HTMLAudioElement>(); const audioRef = createRef<HTMLAudioElement>();
const handleTrackChange = useCallback((audioSource: string, track: MusicStreamingEntry) => { const handleTrackChange = useCallback((metadata: TrackWithURL, collection: MusicStreamingEntry) => {
setCurrentTrack(track); setCurrentTrack(metadata);
setAudioSource(audioSource); setAudioSource(metadata.url);
setCurrentCollection(collection);
setIsOpen(true);
setIsPlaying(true); setIsPlaying(true);
}, []) }, [])
useEffect(() => {
isPlaying ? audioRef.current?.play() : audioRef.current?.pause();
}, [isPlaying, audioRef])
return ( return (
<AudioContext.Provider value={{ <AudioContext.Provider value={{
currentTrack, isOpen, isPlaying, volume, duration, currentTime, currentTrack, isOpen, isPlaying, currentCollection,
setCurrentTime, setIsOpen, handleTrackChange, setIsPlaying, setVolume setIsOpen, handleTrackChange, setIsPlaying, setCurrentCollection,
}}> }}>
{children} {children}
<audio className="hidden" style={{ display: "none" }} ref={audioRef} controls autoPlay={isPlaying} src={audioSource} /> <audio className="hidden" style={{ display: "none" }} ref={audioRef} controls autoPlay={isPlaying} src={audioSource} />

View File

@@ -15,7 +15,7 @@ type ControllerOptions<T extends { [key: string]: any }> = {
} }
export default abstract class BaseController<T extends { [key: string]: any }> { export default abstract class BaseController<T extends { [key: string]: any }> {
#db: pg.Client protected db: pg.Client
// #bucket: S3Client // #bucket: S3Client
// #cache: Redis // #cache: Redis
@@ -23,7 +23,7 @@ export default abstract class BaseController<T extends { [key: string]: any }> {
parser?: FullParserType<T> parser?: FullParserType<T>
constructor(options: ControllerOptions<T>) { constructor(options: ControllerOptions<T>) {
this.#db = must(createDBClient); this.db = must(createDBClient);
// this.#bucket = must(createS3Client); // this.#bucket = must(createS3Client);
// this.#cache = must(createRedisClient); // this.#cache = must(createRedisClient);
@@ -35,8 +35,8 @@ export default abstract class BaseController<T extends { [key: string]: any }> {
'use server'; 'use server';
try { try {
// we'll enable cache here later // we'll enable cache here later
await this.#db.connect(); await this.db.connect();
const result = await this.#db.query(`SELECT * FROM ${this.tableName}`); const result = await this.db.query(`SELECT * FROM ${this.tableName}`);
if (this.parser) { if (this.parser) {
result.rows.forEach((row, idx) => { result.rows.forEach((row, idx) => {
@@ -53,16 +53,16 @@ export default abstract class BaseController<T extends { [key: string]: any }> {
console.log({ error }); console.log({ error });
return null; return null;
} finally { } finally {
await this.#db.end(); await this.db.end();
} }
} }
async getByID(id: number, projection?: (keyof T)[]): Promise<Maybe<T>> { async getByID(id: number, projection?: (keyof T)[]): Promise<Maybe<T>> {
try { try {
await this.#db.connect(); await this.db.connect();
const finalProjection = projection?.join(", ") ?? "*"; const finalProjection = projection?.join(", ") ?? "*";
const result = await this.#db.query(`SELECT ${finalProjection} FROM ${this.tableName} WHERE id = ${id}`); const result = await this.db.query(`SELECT ${finalProjection} FROM ${this.tableName} WHERE id = ${id}`);
if (this.parser) { if (this.parser) {
const parsed = this.parser(result.rows[0]); const parsed = this.parser(result.rows[0]);
@@ -74,7 +74,7 @@ export default abstract class BaseController<T extends { [key: string]: any }> {
console.log({ error }); console.log({ error });
return null; return null;
} finally { } finally {
await this.#db.end(); await this.db.end();
} }
} }
} }

View File

@@ -4,6 +4,8 @@ import createS3Client from "./createClient";
import { Maybe, must } from "@/util/helpers"; import { Maybe, must } from "@/util/helpers";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
export type TrackWithURL = (Required<Pick<_Object, "Key">> & _Object) & { url: string }
export default class S3Service { export default class S3Service {
static asEndpoint(key: string): string { static asEndpoint(key: string): string {
return `${env.S3_ENDPOINT}/${env.S3_BUCKET}/${key}`; return `${env.S3_ENDPOINT}/${env.S3_BUCKET}/${key}`;
@@ -59,22 +61,45 @@ export default class S3Service {
} }
} }
static async getURLs(key: string): Promise<Maybe<string[]>> { static async getURLs(key: string): Promise<{
files: Maybe<_Object[]>,
urlList: Maybe<string[]>
}> {
try { try {
const files = await this.listObjects(key); const files = await this.listObjects(key);
const output = new Array<string>(); const urlList = new Array<string>();
files?.forEach(async(file) => { files?.forEach(async(file) => {
if (file.Key) { if (file.Key) {
const url = await this.getURL(file.Key); const url = await this.getURL(file.Key);
if (url) output.push(url); if (url) urlList.push(url);
} }
}); });
return output; return { files, urlList }
} catch (error) { } catch (error) {
console.log({ error }); console.log({ error });
return null; return { files: null, urlList: null }
}
}
static async prepareTrackList(key: string): Promise<TrackWithURL[]> {
const result = new Array<TrackWithURL>();
try {
const files = await this.listObjects(key);
files?.forEach(async(file) => {
if (file.Key) {
const url = await this.getURL(file.Key);
if (url) result.push({ ...file, url, Key: file.Key });
}
})
return result;
} catch (error) {
console.log({ error });
return new Array<TrackWithURL>();
} }
} }
} }

View File

@@ -7,4 +7,8 @@ export function must<T = any>(func: CallableFunction): T {
} }
} }
export function prettyFileName(key: string): string {
return key.split('/').pop()?.split(".").shift() ?? "INVALID KEY";
}
export type Maybe<T> = T | null | undefined; export type Maybe<T> = T | null | undefined;