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 />
const controller = new MusicController();
const result = await controller.getByID(id);
if (!result) return <NotFound />
const collection = await controller.getByID(id);
if (!collection) return <NotFound />
// const path = S3Service.asEndpoint(result.pathtoentry);
const entries = await S3Service.getURLs(result.pathtoentry);
const trackList = await S3Service.prepareTrackList(collection.pathtoentry);
return (
<div>
<header>
<h1>{result.name}</h1>
<p>{result.shortdescription}</p>
<h1>{collection.name}</h1>
<p>{collection.shortdescription}</p>
</header>
<p>{result.longdescription}</p>
<p>{collection.longdescription}</p>
<Suspense fallback={<p>Loading...</p>}>
<AudioGallery urlList={entries} />
<AudioGallery trackList={trackList} collection={collection} />
</Suspense>
</div>
)

View File

@@ -1,14 +1,18 @@
'use client';
import { MusicStreamingEntry } from "@/server/db/schema";
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 (
<div>
{ urlList
? urlList.map((each, idx) => {
return <AudioTrack src={each} key={idx} />
}) : <p>No audio results found for this work.</p>
<div className="p-8 m-8 rounded-lg bg-neutral-600">
{ trackList.length
? trackList.map((each, idx) => {
if (!(each.Key.includes(".wav") || each.Key.includes(".mp3"))) return <div key={idx} />
return <AudioTrack metadata={each} collection={collection} key={idx} />
})
: <p>No audio results found for this work.</p>
}
</div>
)

View File

@@ -1,14 +1,14 @@
type ButtonProps = {
children: React.ReactNode;
conditionalExp: string;
onClick: (...args: []) => void;
extraClasses?: string;
disabled?: boolean
}
export function _Button({ children, conditionalExp, onClick, disabled=false }: ButtonProps) {
export function _Button({ children, extraClasses="", onClick, disabled=false }: ButtonProps) {
return (
<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 }
</button>

View File

@@ -5,9 +5,10 @@ import { FaArrowDown, FaArrowUp, FaPause, FaPlay } from "react-icons/fa";
import { useCallback } from "react";
import { _Button } from "./_Button";
import Link from "@/components/ui/Link";
import { prettyFileName } from "@/util/helpers";
export default function AudioPlayer() {
const { isPlaying, isOpen, currentTrack, setIsPlaying, setIsOpen } = useAudio();
const { isPlaying, isOpen, currentTrack, currentCollection, setIsPlaying, setIsOpen } = useAudio();
const changePlayState = useCallback(() => {
if (!currentTrack) {
@@ -19,13 +20,14 @@ export default function AudioPlayer() {
}, [currentTrack, isPlaying, setIsPlaying])
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 */}
<div className="flex flex-col">
{ !isOpen ? null : currentTrack ? (
<>
<h3>{currentTrack.name}</h3>
<p>{currentTrack.artist ?? "(no artist listed)"} - {currentTrack.year ?? "(no year listed)"}</p>
<h3>{prettyFileName(currentTrack.Key)}</h3>
<p>{currentCollection?.artist ?? "(no artist listed)"} - {currentCollection?.year ?? "(no year listed)"}</p>
</>
) : (
<>
@@ -35,23 +37,24 @@ export default function AudioPlayer() {
)}
</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 */}
<_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
? <FaPause className="h-8 w-8 text-black" />
: <FaPlay className="h-8 w-8 text-black" />
}
</_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>
{/* 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';
export function AudioTrack({ src }: { src: string }) {
return <audio src={src} controls />
import useAudio from "@/hooks/useAudio";
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';
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 = {
// boolean properties
@@ -10,16 +11,17 @@ type AudioContextType = {
setIsOpen: (isOpen: boolean) => void;
// track properties
/** @deprecated */
duration: number;
volume: number;
setVolume: (volume: number) => void;
currentTime: number;
setCurrentTime: (currentTime: number) => void;
handleTrackChange: (audioSource: string, track: MusicStreamingEntry) => void;
// duration: number;
// volume: number;
// setVolume: (volume: number) => void;
// currentTime: number;
// setCurrentTime: (currentTime: number) => void;
handleTrackChange: (metadata: TrackWithURL, collection: MusicStreamingEntry) => void;
// identifiers
currentTrack?: MusicStreamingEntry;
currentTrack?: TrackWithURL;
currentCollection?: MusicStreamingEntry;
setCurrentCollection: (collection: MusicStreamingEntry) => void;
}
const initialContext: AudioContextType = {
@@ -27,38 +29,48 @@ const initialContext: AudioContextType = {
setIsPlaying: () => {},
isOpen: false,
setIsOpen: () => {},
volume: 0.5,
setVolume: () => {},
duration: 0,
currentTime: 0,
setCurrentTime: () => {},
currentTrack: undefined,
// volume: 0.5,
// setVolume: () => {},
// duration: 0,
// currentTime: 0,
// setCurrentTime: () => {},
handleTrackChange: () => {},
currentTrack: undefined,
currentCollection: undefined,
setCurrentCollection: () => {},
}
const AudioContext = createContext<AudioContextType>(initialContext);
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 [isPlaying, setIsPlaying] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [volume, setVolume] = useState(0.5);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
// const [volume, setVolume] = useState(0.5);
// const [duration, setDuration] = useState(0);
// const [currentTime, setCurrentTime] = useState(0);
const audioRef = createRef<HTMLAudioElement>();
const handleTrackChange = useCallback((audioSource: string, track: MusicStreamingEntry) => {
setCurrentTrack(track);
setAudioSource(audioSource);
const handleTrackChange = useCallback((metadata: TrackWithURL, collection: MusicStreamingEntry) => {
setCurrentTrack(metadata);
setAudioSource(metadata.url);
setCurrentCollection(collection);
setIsOpen(true);
setIsPlaying(true);
}, [])
useEffect(() => {
isPlaying ? audioRef.current?.play() : audioRef.current?.pause();
}, [isPlaying, audioRef])
return (
<AudioContext.Provider value={{
currentTrack, isOpen, isPlaying, volume, duration, currentTime,
setCurrentTime, setIsOpen, handleTrackChange, setIsPlaying, setVolume
currentTrack, isOpen, isPlaying, currentCollection,
setIsOpen, handleTrackChange, setIsPlaying, setCurrentCollection,
}}>
{children}
<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 }> {
#db: pg.Client
protected db: pg.Client
// #bucket: S3Client
// #cache: Redis
@@ -23,7 +23,7 @@ export default abstract class BaseController<T extends { [key: string]: any }> {
parser?: FullParserType<T>
constructor(options: ControllerOptions<T>) {
this.#db = must(createDBClient);
this.db = must(createDBClient);
// this.#bucket = must(createS3Client);
// this.#cache = must(createRedisClient);
@@ -35,8 +35,8 @@ export default abstract class BaseController<T extends { [key: string]: any }> {
'use server';
try {
// we'll enable cache here later
await this.#db.connect();
const result = await this.#db.query(`SELECT * FROM ${this.tableName}`);
await this.db.connect();
const result = await this.db.query(`SELECT * FROM ${this.tableName}`);
if (this.parser) {
result.rows.forEach((row, idx) => {
@@ -53,16 +53,16 @@ export default abstract class BaseController<T extends { [key: string]: any }> {
console.log({ error });
return null;
} finally {
await this.#db.end();
await this.db.end();
}
}
async getByID(id: number, projection?: (keyof T)[]): Promise<Maybe<T>> {
try {
await this.#db.connect();
await this.db.connect();
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) {
const parsed = this.parser(result.rows[0]);
@@ -74,7 +74,7 @@ export default abstract class BaseController<T extends { [key: string]: any }> {
console.log({ error });
return null;
} 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 { getSignedUrl } from "@aws-sdk/s3-request-presigner";
export type TrackWithURL = (Required<Pick<_Object, "Key">> & _Object) & { url: string }
export default class S3Service {
static asEndpoint(key: string): string {
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 {
const files = await this.listObjects(key);
const output = new Array<string>();
const urlList = new Array<string>();
files?.forEach(async(file) => {
if (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) {
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;