Skip to content

Commit

Permalink
Add playlist selector and ability to add and remove from playlists
Browse files Browse the repository at this point in the history
  • Loading branch information
espidev committed Jul 26, 2023
1 parent f3f145a commit 8a6c5fd
Show file tree
Hide file tree
Showing 14 changed files with 444 additions and 91 deletions.
73 changes: 73 additions & 0 deletions website/app/api/playlist/[playlistId]/add-track/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { checkAuthenticated } from "@/util/api";
import { getDB } from "@/util/db";
import { NextResponse } from "next/server";

// POST /playlist/[playlistId]/add-track
// Add a track to the playlist
// Request body:
// { trackId }

export async function POST(request: Request, { params }: { params: { playlistId: string } }) {
const playlistId = params.playlistId;
const { trackId } = await request.json();

// check authorization
const tokenUuid = await checkAuthenticated();
if (tokenUuid === null) {
return NextResponse.json({ error: "not authorized" }, { status: 401 });
}

const conn = await getDB();

try {
// check if playlist exists
const playlistRes = await conn.query(`
SELECT id FROM playlist WHERE id = $1 AND account_uuid = $2
`, [playlistId, tokenUuid]);

if (playlistRes.rowCount === 0) {
await conn.end();
return NextResponse.json({ error: "playlist not found or not authorized to update" }, { status: 404 });
}

// check if track exists
const trackRes = await conn.query(`
SELECT id FROM track WHERE id = $1 AND account_uuid = $2
`, [trackId, tokenUuid]);

if (trackRes.rowCount === 0) {
await conn.end();
return NextResponse.json({ error: 'track not found' }, { status: 404 });
}

// check how many tracks there are
const trackCountRes = await conn.query(`
SELECT COUNT(*) FROM playlist_tracks WHERE playlist_id = $1
`, [playlistId]);

const trackCount = trackCountRes.rows[0].count;

// insert the track association
await conn.query(
`
INSERT INTO playlist_tracks(account_uuid, playlist_id, track_id, position, added_on)
VALUES (
$1,
$2,
$3,
$4,
$5
)
`,
[tokenUuid, playlistId, trackId, trackCount, new Date().toISOString()]
);

await conn.end();

return NextResponse.json({ status: 'success' }, { status: 200 });
} catch (error) {
await conn.end();
console.error("Error updating playlist:", error);
return NextResponse.json({ error: 'an error occurred while updating the playlist' }, { status: 500 });
}
}
105 changes: 105 additions & 0 deletions website/app/api/playlist/[playlistId]/remove-track/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { checkAuthenticated } from "@/util/api";
import { getDB } from "@/util/db";
import { DBPlaylistTrack } from "@/util/models/playlist";
import { NextResponse } from "next/server";

// POST /playlist/[playlistId]/remove-track
// Add a track to the playlist
// Request body:
// { position } or { trackId }

export async function POST(request: Request, { params }: { params: { playlistId: string } }) {
const playlistId = params.playlistId;
let { position, trackId } = await request.json();

if (position === undefined && trackId === undefined) {
return NextResponse.json({ error: 'invalid query' }, { status: 400 });
}

// check authorization
const tokenUuid = await checkAuthenticated();
if (tokenUuid === null) {
return NextResponse.json({ error: "not authorized" }, { status: 401 });
}

const conn = await getDB();

try {
// check if playlist exists
const playlistRes = await conn.query(`
SELECT id FROM playlist WHERE id = $1 AND account_uuid = $2
`, [playlistId, tokenUuid]);

if (playlistRes.rowCount === 0) {
await conn.end();
return NextResponse.json({ error: "playlist not found or not authorized to update" }, { status: 404 });
}

// check how many tracks there are
const tracksRes = await conn.query(`
SELECT * FROM playlist_tracks WHERE playlist_id = $1
`, [playlistId]);

const trackCount = tracksRes.rowCount;

// check that the requested deletion position is in bounds
if (position !== undefined && (position >= trackCount || position < 0)) {
await conn.end();
return NextResponse.json({ error: 'position out of bounds' }, { status: 400 })
}

await conn.query(`BEGIN`);

// delete entry
if (trackId !== undefined) {

// HACK: we assume that there is only one occurrence of the track in the playlist, which is true for now...

const deleteRes = await conn.query(`
DELETE FROM playlist_tracks
WHERE playlist_id = $1 AND track_id = $2
RETURNING *
`, [playlistId, trackId]);

// if the track wasn't found
if (deleteRes.rowCount === 0) {
await conn.query(`COMMIT`);
await conn.end();
return NextResponse.json({ error: 'track not found in playlist' }, { status: 404 });
}

// add the position it was deleted at
position = deleteRes.rows[0].position;

} else {
await conn.query(`
DELETE FROM playlist_tracks WHERE playlist_id = $1 AND position = $2
`, [playlistId, position]);
}

const playlistTracks = tracksRes.rows as DBPlaylistTrack[];

// update other entries (all position spots after)
for (const playlistTrack of playlistTracks) {
if (playlistTrack.position > position) {

await conn.query(`
UPDATE playlist_tracks SET position = $1 WHERE playlist_id = $2 AND position = $3 AND track_id = $4
`, [playlistTrack.position - 1, playlistId, playlistTrack.position, playlistTrack.track_id]);

}
}

await conn.query(`COMMIT`);
await conn.end();

return NextResponse.json({ status: 'success' }, { status: 200 });
} catch (error) {

await conn.query(`ROLLBACK`);

await conn.end();
console.error("Error updating playlist:", error);
return NextResponse.json({ error: 'an error occurred while updating the playlist' }, { status: 500 });
}
}
3 changes: 2 additions & 1 deletion website/app/api/playlist/[playlistId]/tracks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export async function GET(request: Request, { params }: { params: { playlistId:
LEFT OUTER JOIN album ON track_to_album.album_id = album.id
LEFT OUTER JOIN track_to_genre ON t.id = track_to_genre.track_id
LEFT OUTER JOIN genre ON track_to_genre.genre_id = genre.id
FULL OUTER JOIN playlist ON playlist_tracks.playlist_id = playlist.id
FULL OUTER JOIN playlist_tracks AS pt2 ON t.id = pt2.track_id
FULL OUTER JOIN playlist ON pt2.playlist_id = playlist.id
WHERE playlist_tracks.playlist_id = $1
GROUP BY t.id, playlist_tracks.position
ORDER BY playlist_tracks.position ASC
Expand Down
36 changes: 22 additions & 14 deletions website/app/collection/albums/[albumId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ export default function CollectionAlbumPage({params} : {params: {albumId: string
const [tracks, setTracks] = useState([] as APITrack[]);
const [alerts, setAlerts] = useState([] as AlertEntry[]);

const loadAlbum = () => {
apiGetAlbum(albumId)
.then(res => {
setAlbum(res.data as APIAlbum);

// Fetch and set the tracks
apiGetAlbumTracks(albumId).then(res_tracks => {
setTracks(res_tracks.data as APITrack[]);
})
})
.catch(err => {
setAlerts([...alerts, { severity: "error", message: "Error fetching artists, see console for details." }]);
console.error(err);
});
}

useEffect(() => {
// wait for credentials to be loaded
if (!loginState.loggedInStateValid) {
Expand All @@ -49,19 +65,7 @@ export default function CollectionAlbumPage({params} : {params: {albumId: string
}

// load album content
apiGetAlbum(albumId)
.then(res => {
setAlbum(res.data as APIAlbum);

// Fetch and set the tracks
apiGetAlbumTracks(albumId).then(res_tracks => {
setTracks(res_tracks.data as APITrack[]);
})
})
.catch(err => {
setAlerts([...alerts, { severity: "error", message: "Error fetching artists, see console for details." }]);
console.error(err);
})
loadAlbum();
}, [loginState]);

if (!loginState.loggedInStateValid) {
Expand Down Expand Up @@ -125,7 +129,11 @@ export default function CollectionAlbumPage({params} : {params: {albumId: string
</div>
</div>

<TrackTable tracks={tracks} handleTrackClick={handleTrackClick} />
<TrackTable
tracks={tracks}
handleTrackClick={handleTrackClick}
handleTrackUpdate={loadAlbum}
/>
</Box>
</div>
);
Expand Down
31 changes: 18 additions & 13 deletions website/app/collection/artists/[artistId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,7 @@ export default function CollectionArtistPage({
const [albums, setAlbums] = useState([] as APIAlbum[]);
const [alerts, setAlerts] = useState([] as AlertEntry[]);

useEffect(() => {
// wait for credentials to be loaded
if (!loginState.loggedInStateValid) {
return;
}

// if not logged in, go to login page
if (!loginState.isLoggedIn) {
router.push("/login");
return;
}

// load artist content
const loadArtist = () => {
apiGetArtist(artistId)
.then((res) => {
setArtist(res.data as APIArtist);
Expand All @@ -82,6 +70,22 @@ export default function CollectionArtistPage({
]);
console.error(err);
});
}

useEffect(() => {
// wait for credentials to be loaded
if (!loginState.loggedInStateValid) {
return;
}

// if not logged in, go to login page
if (!loginState.isLoggedIn) {
router.push("/login");
return;
}

// load artist content
loadArtist();
}, [loginState]);

if (!loginState.loggedInStateValid) {
Expand Down Expand Up @@ -169,6 +173,7 @@ export default function CollectionArtistPage({
tracks={tracks}
handleTrackClick={handleTrackClick}
hideArtistCol={true}
handleTrackUpdate={loadArtist}
/>
</Box>
<Box
Expand Down
35 changes: 19 additions & 16 deletions website/app/collection/genres/[genreId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";
import { useLoginStateContext } from "@/components/loginstateprovider";
import { APIGenre } from "@/util/models/genre";
import { APIAlbum } from "@/util/models/album";
import { Box, Button, Grid, Typography } from "@mui/material";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
Expand All @@ -14,7 +13,6 @@ import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { APITrack } from "@/util/models/track";
import TrackTable from "@/components/trackTable";
import { useAppStateContext } from "@/components/appstateprovider";
import AlbumCard from "@/components/albumCard";

function formatDuration(duration: number): string {
const minutes = Math.floor(duration / 60);
Expand Down Expand Up @@ -44,19 +42,7 @@ export default function CollectionGenrePage({
const [tracks, setTracks] = useState([] as APITrack[]);
const [alerts, setAlerts] = useState([] as AlertEntry[]);

useEffect(() => {
// wait for credentials to be loaded
if (!loginState.loggedInStateValid) {
return;
}

// if not logged in, go to login page
if (!loginState.isLoggedIn) {
router.push("/login");
return;
}

// load genre content
const loadGenre = () => {
apiGetGenre(genreId)
.then((res) => {
console.log(res.data);
Expand All @@ -77,7 +63,23 @@ export default function CollectionGenrePage({
]);
console.error(err);
});
// load albums
}

useEffect(() => {
// wait for credentials to be loaded
if (!loginState.loggedInStateValid) {
return;
}

// if not logged in, go to login page
if (!loginState.isLoggedIn) {
router.push("/login");
return;
}

// load genre content
loadGenre();

}, [loginState]);

if (!loginState.loggedInStateValid) {
Expand Down Expand Up @@ -153,6 +155,7 @@ export default function CollectionGenrePage({
tracks={tracks}
handleTrackClick={handleTrackClick}
hideGenreCol={true}
handleTrackUpdate={loadGenre}
/>
</Box>
</Box>
Expand Down
Loading

0 comments on commit 8a6c5fd

Please sign in to comment.