fixes for some play states, mapping track metadata
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user