-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
399 additions
and
14 deletions.
There are no files selected for viewing
77 changes: 77 additions & 0 deletions
77
website/app/api/collection/[accountUuid]/favourites/add-track/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { checkAuthenticated } from "@/util/api"; | ||
import { FAVOURITES_PLAYLIST_NAME } from "@/util/constants"; | ||
import { getDB } from "@/util/db"; | ||
import { NextResponse } from "next/server"; | ||
|
||
// POST /collection/[accountUuid]/favourites/add-track | ||
// Add a track to the favourites | ||
// Request body: | ||
// { trackId } | ||
|
||
export async function POST(request: Request, { params }: { params: { accountUuid: string } }) { | ||
const { trackId } = await request.json(); | ||
|
||
// check authorization | ||
const tokenUuid = await checkAuthenticated(); | ||
if (tokenUuid === null || tokenUuid !== params.accountUuid) { | ||
return NextResponse.json({ error: "not authorized" }, { status: 401 }); | ||
} | ||
|
||
const conn = await getDB(); | ||
|
||
try { | ||
|
||
let playlistId = 0; | ||
|
||
// check if playlist exists | ||
const playlistRes = await conn.query(` | ||
SELECT id FROM playlist WHERE name = $1 AND account_uuid = $2 | ||
`, [FAVOURITES_PLAYLIST_NAME, tokenUuid]); | ||
|
||
if (playlistRes.rowCount === 0) { | ||
|
||
// add favourites playlist if it doesn't exist | ||
const insertQuery = await conn.query(` | ||
INSERT INTO playlist (name, account_uuid) VALUES ($1, $2) RETURNING * | ||
`, [FAVOURITES_PLAYLIST_NAME, tokenUuid]); | ||
|
||
playlistId = insertQuery.rows[0].id; | ||
|
||
} else { | ||
playlistId = playlistRes.rows[0].id; | ||
} | ||
|
||
// 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 }); | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
website/app/api/collection/[accountUuid]/favourites/remove-track/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { checkAuthenticated } from "@/util/api"; | ||
import { FAVOURITES_PLAYLIST_NAME } from "@/util/constants"; | ||
import { getDB } from "@/util/db"; | ||
import { DBPlaylistTrack } from "@/util/models/playlist"; | ||
import { NextResponse } from "next/server"; | ||
|
||
// POST /collection/[accountUuid]/favourites/remove-track | ||
// Removes a track from the favourites | ||
// Request body: | ||
// { trackId } | ||
|
||
export async function POST(request: Request, { params }: { params: { accountUuid: string } }) { | ||
let { trackId } = await request.json(); | ||
|
||
// check authorization | ||
const tokenUuid = await checkAuthenticated(); | ||
if (tokenUuid === null || tokenUuid !== params.accountUuid) { | ||
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 name = $1 AND account_uuid = $2 | ||
`, [FAVOURITES_PLAYLIST_NAME, tokenUuid]); | ||
|
||
if (playlistRes.rowCount === 0) { | ||
await conn.end(); | ||
return NextResponse.json({ error: "favourites not found or not authorized to update" }, { status: 404 }); | ||
} | ||
|
||
const playlistId = playlistRes.rows[0].id; | ||
|
||
// check how many tracks there are | ||
const tracksRes = await conn.query(` | ||
SELECT * FROM playlist_tracks WHERE playlist_id = $1 | ||
`, [playlistId]); | ||
|
||
await conn.query(`BEGIN`); | ||
|
||
// delete favourites entry | ||
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 | ||
const position = deleteRes.rows[0].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 }); | ||
} | ||
} |
78 changes: 78 additions & 0 deletions
78
website/app/api/collection/[accountUuid]/favourites/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { checkAuthenticated } from "@/util/api"; | ||
import { FAVOURITES_PLAYLIST_NAME } from "@/util/constants"; | ||
import { getDB } from "@/util/db"; | ||
import { getAPIAlbum } from "@/util/models/album"; | ||
import { getAPIArtist } from "@/util/models/artist"; | ||
import { getAPIGenre } from "@/util/models/genre"; | ||
import { DBPlaylist, getAPIPlaylist } from "@/util/models/playlist"; | ||
import { getAPITrack } from "@/util/models/track"; | ||
import { NextResponse } from "next/server"; | ||
|
||
// GET /api/collection/favourites | ||
// get a user's favourites track list | ||
|
||
export async function GET(request: Request, { params }: { params: { accountUuid: string } }) { | ||
const accountUuid = params.accountUuid; | ||
|
||
// check authorization | ||
const tokenUuid = await checkAuthenticated(); | ||
if (tokenUuid === null || tokenUuid !== accountUuid) { | ||
return NextResponse.json({ error: "not authorized" }, { status: 401 }); | ||
} | ||
|
||
const conn = await getDB(); | ||
|
||
const playlistRes = await conn.query(` | ||
SELECT * FROM playlist WHERE account_uuid = $1 AND name = $2 LIMIT 1 | ||
`, [accountUuid, FAVOURITES_PLAYLIST_NAME]); | ||
|
||
if (playlistRes.rowCount < 1) { | ||
await conn.end(); | ||
return NextResponse.json([], { status: 200 }); | ||
} | ||
|
||
const dbPlaylist = playlistRes.rows[0] as DBPlaylist; | ||
|
||
if (tokenUuid !== dbPlaylist.account_uuid) { | ||
await conn.end(); | ||
return NextResponse.json({ error: "not authorized" }, { status: 401 }); | ||
} | ||
|
||
const trackRes = await conn.query(` | ||
SELECT | ||
t.*, | ||
playlist_tracks.position as playlist_position, | ||
JSON_AGG(artist.*) as artists, | ||
JSON_AGG(album.*) as albums, | ||
JSON_AGG(genre.*) as genres, | ||
JSON_AGG(playlist.*) as playlists | ||
FROM playlist_tracks | ||
INNER JOIN track as t ON t.id = playlist_tracks.track_id | ||
LEFT OUTER JOIN track_to_artist ON t.id = track_to_artist.track_id | ||
LEFT OUTER JOIN artist ON track_to_artist.artist_id = artist.id | ||
LEFT OUTER JOIN track_to_album ON t.id = track_to_album.album_id | ||
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_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 | ||
`, [dbPlaylist.id]); | ||
|
||
await conn.end(); | ||
|
||
const tracks = trackRes.rows.map(track => { | ||
const apiTrack: any = getAPITrack(track); | ||
apiTrack.albums = track.albums.filter((album: any) => album).map((album: any) => getAPIAlbum(album)); | ||
apiTrack.artists = track.artists.filter((artist: any) => artist).map((artist: any) => getAPIArtist(artist)); | ||
apiTrack.genres = track.genres.filter((genre: any) => genre).map((genre: any) => getAPIGenre(genre)); | ||
apiTrack.playlists = track.playlists.filter((playlist: any) => playlist).map((playlist: any) => getAPIPlaylist(playlist)); | ||
apiTrack.playlist_position = track.playlist_position; | ||
|
||
return apiTrack; | ||
}); | ||
|
||
return NextResponse.json(tracks, { status: 200 }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
'use client' | ||
|
||
import { useEffect, useState } from "react"; | ||
import { apiGetCollectionFavourites, apiGetCollectionTracks, apiGetCollectionTracksSearch } from "@/components/apiclient"; | ||
import { useAppStateContext } from "@/components/appstateprovider"; | ||
import { APITrack } from "@/util/models/track"; | ||
import {Grid, TextField, Typography, InputAdornment, CssBaseline} from "@mui/material"; | ||
import { Search } from "@mui/icons-material"; | ||
import { useRouter } from "next/navigation"; | ||
import { useLoginStateContext } from "@/components/loginstateprovider"; | ||
import AlertComponent, { AlertEntry } from "@/components/alerts"; | ||
import TrackTable from "@/components/trackTable"; | ||
import { ThemeProvider } from '@mui/material/styles'; | ||
import { lightTheme, darkTheme } from '@/components/themes'; | ||
import {styled} from '@mui/system'; | ||
|
||
const StyledGrid = styled(Grid)(({ theme }) => ({ | ||
height: 1, | ||
backgroundColor: theme.palette.mode === 'dark' ? 'rgb(5, 10, 25)' : 'rgb(255, 245, 245)', | ||
})); | ||
|
||
export default function CollectionPage() { | ||
const appState = useAppStateContext(); | ||
const theme = appState.theme; | ||
const loginState = useLoginStateContext(); | ||
const router = useRouter(); | ||
|
||
const color = theme === 'dark' ? 'white' : 'rgb(50, 50, 50)'; | ||
|
||
const [tracks, setTracks] = useState([] as APITrack[]); | ||
const [alerts, setAlerts] = useState([] as AlertEntry[]); | ||
|
||
const loadTracks = () => { | ||
apiGetCollectionFavourites(loginState.loggedInUserUuid) | ||
.then((res) => { | ||
setTracks(res.data as APITrack[]); | ||
}) | ||
.catch(err => { | ||
setAlerts([...alerts, { severity: "error", message: "Error fetching tracks, see console for details." }]); | ||
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 tracks | ||
loadTracks(); | ||
}, [loginState]); | ||
|
||
if (!loginState.loggedInStateValid) { | ||
return <></>; | ||
} | ||
|
||
const handleTrackClick = (track: APITrack) => { | ||
appState.changeQueue(tracks, tracks.indexOf(track)); | ||
appState.playCurrentTrack(); | ||
}; | ||
|
||
return ( | ||
<ThemeProvider theme={theme === 'dark' ? darkTheme : lightTheme}> | ||
<CssBaseline /> | ||
<StyledGrid> | ||
<AlertComponent alerts={alerts} setAlerts={setAlerts} /> | ||
|
||
<Grid sx={{ padding: 2, color: color }} container direction="row" justifyContent="space-between"> | ||
<Typography variant="h6">Favourites</Typography> | ||
</Grid> | ||
|
||
{/* Weird. paddingBottom works but not marginBottom. */} | ||
<Grid sx={{ paddingBottom: '5em' }}> | ||
<TrackTable | ||
tracks={tracks} | ||
handleTrackClick={handleTrackClick} | ||
handleTrackUpdate={loadTracks} | ||
/> | ||
</Grid> | ||
</StyledGrid> | ||
</ThemeProvider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.