mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
233 lines
8.1 KiB
TypeScript
233 lines
8.1 KiB
TypeScript
import { Direction, Orientation, QAbstractSliderSignals, QIcon, QLabel } from "@nodegui/nodegui";
|
|
import { BoxView, GridColumn, GridRow, GridView, Slider, Text, useEventHandler } from "@nodegui/react-nodegui";
|
|
import React, { ReactElement, useContext, useEffect, useRef, useState } from "react";
|
|
import playerContext, { CurrentPlaylist } from "../context/playerContext";
|
|
import { shuffleArray } from "../helpers/shuffleArray";
|
|
import NodeMpv from "node-mpv";
|
|
import { getYoutubeTrack } from "../helpers/getYoutubeTrack";
|
|
import PlayerProgressBar from "./PlayerProgressBar";
|
|
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular, heart } from "../icons";
|
|
import IconButton from "./shared/IconButton";
|
|
import showError from "../helpers/showError";
|
|
import useTrackReaction from "../hooks/useTrackReaction";
|
|
|
|
export const audioPlayer = new NodeMpv(
|
|
{
|
|
audio_only: true,
|
|
auto_restart: true,
|
|
time_update: 1,
|
|
binary: process.env.MPV_EXECUTABLE ?? "/usr/bin/mpv",
|
|
// debug: true,
|
|
// verbose: true,
|
|
},
|
|
["--ytdl-raw-options-set=format=140,http-chunk-size=300000"]
|
|
);
|
|
|
|
function Player(): ReactElement {
|
|
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
|
const { reactToTrack, isFavorite } = useTrackReaction();
|
|
const [isPaused, setIsPaused] = useState(true);
|
|
const [volume, setVolume] = useState<number>(55);
|
|
const [totalDuration, setTotalDuration] = useState(0);
|
|
const [shuffle, setShuffle] = useState<boolean>(false);
|
|
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
|
|
const [isStopped, setIsStopped] = useState<boolean>(false);
|
|
const playlistTracksIds = currentPlaylist?.tracks.map((t) => t.track.id);
|
|
const volumeHandler = useEventHandler<QAbstractSliderSignals>(
|
|
{
|
|
sliderMoved: (value) => {
|
|
setVolume(value);
|
|
},
|
|
sliderReleased: () => {
|
|
localStorage.setItem("volume", volume.toString());
|
|
},
|
|
},
|
|
[]
|
|
);
|
|
const playerRunning = audioPlayer.isRunning();
|
|
const titleRef = useRef<QLabel>();
|
|
|
|
// initial Effect
|
|
useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
if (!playerRunning) {
|
|
await audioPlayer.start();
|
|
await audioPlayer.volume(volume);
|
|
}
|
|
} catch (error) {
|
|
showError(error, "[Failed starting audio player]: ");
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
if (playerRunning) {
|
|
audioPlayer.quit().catch((e: any) => console.log(e));
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// track change effect
|
|
useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
if (currentTrack && playerRunning) {
|
|
const youtubeTrack = await getYoutubeTrack(currentTrack);
|
|
await audioPlayer.load(youtubeTrack.youtube_uri, "replace");
|
|
await audioPlayer.play();
|
|
setIsPaused(false);
|
|
}
|
|
setIsStopped(false);
|
|
} catch (error) {
|
|
if (error.errcode !== 5) {
|
|
setIsStopped(true);
|
|
setIsPaused(true);
|
|
}
|
|
showError(error, "[Failure at track change]: ");
|
|
}
|
|
})();
|
|
}, [currentTrack]);
|
|
|
|
// changing shuffle to default
|
|
useEffect(() => {
|
|
setShuffle(false);
|
|
}, [currentPlaylist])
|
|
|
|
useEffect(() => {
|
|
if (playerRunning) {
|
|
audioPlayer.volume(volume);
|
|
}
|
|
}, [volume]);
|
|
|
|
// for monitoring shuffle playlist
|
|
useEffect(() => {
|
|
if (currentPlaylist) {
|
|
if (shuffle && realPlaylist.length === 0) {
|
|
const shuffledTracks = shuffleArray(currentPlaylist.tracks);
|
|
setRealPlaylist(currentPlaylist.tracks);
|
|
setCurrentPlaylist({ ...currentPlaylist, tracks: shuffledTracks });
|
|
} else if (!shuffle && realPlaylist.length > 0) {
|
|
setCurrentPlaylist({ ...currentPlaylist, tracks: realPlaylist });
|
|
}
|
|
}
|
|
}, [shuffle]);
|
|
|
|
// live Effect
|
|
useEffect(() => {
|
|
if (playerRunning) {
|
|
const statusListener = (status: { property: string; value: any }) => {
|
|
if (status?.property === "duration") {
|
|
setTotalDuration(status.value);
|
|
}
|
|
};
|
|
const stopListener = () => {
|
|
setIsStopped(true);
|
|
setIsPaused(true);
|
|
// go to next track
|
|
if (currentTrack && playlistTracksIds && currentPlaylist?.tracks.length !== 0) {
|
|
const index = playlistTracksIds?.indexOf(currentTrack.id) + 1;
|
|
setCurrentTrack(currentPlaylist?.tracks[index > playlistTracksIds.length - 1 ? 0 : index].track);
|
|
}
|
|
};
|
|
const pauseListener = () => {
|
|
setIsPaused(true);
|
|
};
|
|
const resumeListener = () => {
|
|
setIsPaused(false);
|
|
};
|
|
audioPlayer.on("status", statusListener);
|
|
audioPlayer.on("stopped", stopListener);
|
|
audioPlayer.on("paused", pauseListener);
|
|
audioPlayer.on("resumed", resumeListener);
|
|
return () => {
|
|
audioPlayer.off("status", statusListener);
|
|
audioPlayer.off("stopped", stopListener);
|
|
audioPlayer.off("paused", pauseListener);
|
|
audioPlayer.off("resumed", resumeListener);
|
|
};
|
|
}
|
|
});
|
|
|
|
const handlePlayPause = async () => {
|
|
try {
|
|
if ((await audioPlayer.isPaused()) && playerRunning) {
|
|
await audioPlayer.play();
|
|
setIsStopped(false);
|
|
setIsPaused(false);
|
|
} else {
|
|
await audioPlayer.pause();
|
|
setIsStopped(true);
|
|
setIsPaused(true);
|
|
}
|
|
} catch (error) {
|
|
showError(error, "[Track control failed]: ");
|
|
}
|
|
};
|
|
|
|
const prevOrNext = (constant: number) => {
|
|
if (currentTrack && playlistTracksIds && currentPlaylist) {
|
|
const index = playlistTracksIds.indexOf(currentTrack.id) + constant;
|
|
setCurrentTrack(currentPlaylist.tracks[index > playlistTracksIds?.length - 1 ? 0 : index < 0 ? playlistTracksIds.length - 1 : index].track);
|
|
}
|
|
};
|
|
|
|
async function stopPlayback() {
|
|
try {
|
|
if (playerRunning) {
|
|
setCurrentTrack(undefined);
|
|
setCurrentPlaylist(undefined);
|
|
await audioPlayer.stop();
|
|
}
|
|
} catch (error) {
|
|
showError(error, "[Failed at audio-player stop]: ");
|
|
}
|
|
}
|
|
|
|
const artistsNames = currentTrack?.artists?.map((x) => x.name);
|
|
return (
|
|
<GridView enabled={!!currentTrack} style="flex: 1; max-height: 120px;">
|
|
<GridRow>
|
|
<GridColumn width={2}>
|
|
<Text ref={titleRef} wordWrap>
|
|
{artistsNames && currentTrack
|
|
? `
|
|
<p><b>${currentTrack.name}</b> - ${artistsNames[0]} ${artistsNames.length > 1 ? "feat. " + artistsNames.slice(1).join(", ") : ""}</p>
|
|
`
|
|
: `<b>Oh, dear don't waste time</b>`}
|
|
</Text>
|
|
</GridColumn>
|
|
<GridColumn width={4}>
|
|
<BoxView direction={Direction.TopToBottom} style={`max-width: 600px; min-width: 380px;`}>
|
|
<PlayerProgressBar audioPlayer={audioPlayer} totalDuration={totalDuration} />
|
|
|
|
<BoxView direction={Direction.LeftToRight}>
|
|
<IconButton style={`background-color: ${shuffle ? "orange" : "rgba(255, 255, 255, 0.055)"}`} on={{ clicked: () => setShuffle(!shuffle) }} icon={new QIcon(shuffleIcon)} />
|
|
<IconButton on={{ clicked: () => prevOrNext(-1) }} icon={new QIcon(backward)} />
|
|
<IconButton on={{ clicked: handlePlayPause }} icon={new QIcon(isStopped || isPaused || !currentTrack ? play : pause)} />
|
|
<IconButton on={{ clicked: () => prevOrNext(1) }} icon={new QIcon(forward)} />
|
|
<IconButton icon={new QIcon(stop)} on={{ clicked: stopPlayback }} />
|
|
</BoxView>
|
|
</BoxView>
|
|
</GridColumn>
|
|
<GridColumn width={2}>
|
|
<BoxView>
|
|
<IconButton
|
|
on={{
|
|
clicked() {
|
|
if (currentTrack) {
|
|
reactToTrack({ added_at: Date.now().toString(), track: currentTrack });
|
|
}
|
|
},
|
|
}}
|
|
icon={new QIcon(isFavorite(currentTrack?.id ?? "") ? heart : heartRegular)}
|
|
/>
|
|
<Slider minSize={{ height: 20, width: 80 }} maxSize={{ height: 20, width: 100 }} hasTracking sliderPosition={volume} on={volumeHandler} orientation={Orientation.Horizontal} />
|
|
</BoxView>
|
|
</GridColumn>
|
|
</GridRow>
|
|
</GridView>
|
|
);
|
|
}
|
|
|
|
export default Player;
|