mirror of
https://github.com/20kaushik02/spotify-manager.git
synced 2025-12-06 06:44:07 +00:00
bugfixes, improving API request wrapper
This commit is contained in:
parent
ca1ad74834
commit
7eec2adc7a
@ -1,5 +1,4 @@
|
||||
import dotenvFlow from "dotenv-flow";
|
||||
dotenvFlow.config();
|
||||
import _ from "./config/dotenv.ts";
|
||||
|
||||
import { resolve } from "path";
|
||||
export default {
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { axiosInstance } from "./axios.ts";
|
||||
|
||||
import { type AxiosResponse, type AxiosRequestConfig } from "axios";
|
||||
import {
|
||||
type AxiosResponse,
|
||||
type AxiosRequestConfig,
|
||||
type RawAxiosRequestHeaders,
|
||||
} from "axios";
|
||||
import type {
|
||||
AddItemsToPlaylist,
|
||||
EndpointHandlerBaseArgs,
|
||||
@ -9,7 +13,6 @@ import type {
|
||||
GetPlaylist,
|
||||
GetPlaylistItems,
|
||||
RemovePlaylistItems,
|
||||
Req,
|
||||
Res,
|
||||
} from "spotify_manager/index.d.ts";
|
||||
|
||||
@ -34,7 +37,7 @@ enum allowedMethods {
|
||||
* @param inlineData true if `data` is to be placed inside config (say, axios' delete method)
|
||||
*/
|
||||
const singleRequest = async <RespDataType>(
|
||||
req: Req,
|
||||
authHeaders: RawAxiosRequestHeaders,
|
||||
res: Res,
|
||||
method: allowedMethods,
|
||||
path: string,
|
||||
@ -43,7 +46,7 @@ const singleRequest = async <RespDataType>(
|
||||
inlineData: boolean = false
|
||||
): Promise<AxiosResponse<RespDataType, any> | null> => {
|
||||
let resp: AxiosResponse<RespDataType, any>;
|
||||
config.headers = { ...config.headers, ...req.session.authHeaders };
|
||||
config.headers = { ...config.headers, ...authHeaders };
|
||||
try {
|
||||
if (!data || inlineData) {
|
||||
if (data) config.data = data ?? null;
|
||||
@ -87,15 +90,12 @@ const singleRequest = async <RespDataType>(
|
||||
interface GetCurrentUsersProfileArgs extends EndpointHandlerBaseArgs {}
|
||||
const getCurrentUsersProfile: (
|
||||
opts: GetCurrentUsersProfileArgs
|
||||
) => Promise<GetCurrentUsersProfile | null> = async ({ req, res }) => {
|
||||
) => Promise<GetCurrentUsersProfile | null> = async ({ authHeaders, res }) => {
|
||||
const response = await singleRequest<GetCurrentUsersProfile>(
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
allowedMethods.Get,
|
||||
"/me",
|
||||
{
|
||||
headers: { Authorization: `Bearer ${req.session.accessToken}` },
|
||||
}
|
||||
"/me"
|
||||
);
|
||||
return response ? response.data : null;
|
||||
};
|
||||
@ -104,9 +104,12 @@ interface GetCurrentUsersPlaylistsFirstPageArgs
|
||||
extends EndpointHandlerBaseArgs {}
|
||||
const getCurrentUsersPlaylistsFirstPage: (
|
||||
opts: GetCurrentUsersPlaylistsFirstPageArgs
|
||||
) => Promise<GetCurrentUsersPlaylists | null> = async ({ req, res }) => {
|
||||
) => Promise<GetCurrentUsersPlaylists | null> = async ({
|
||||
authHeaders,
|
||||
res,
|
||||
}) => {
|
||||
const response = await singleRequest<GetCurrentUsersPlaylists>(
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
allowedMethods.Get,
|
||||
`/me/playlists`,
|
||||
@ -126,12 +129,12 @@ interface GetCurrentUsersPlaylistsNextPageArgs extends EndpointHandlerBaseArgs {
|
||||
const getCurrentUsersPlaylistsNextPage: (
|
||||
opts: GetCurrentUsersPlaylistsNextPageArgs
|
||||
) => Promise<GetCurrentUsersPlaylists | null> = async ({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
nextURL,
|
||||
}) => {
|
||||
const response = await singleRequest<GetCurrentUsersPlaylists>(
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
allowedMethods.Get,
|
||||
nextURL
|
||||
@ -146,13 +149,13 @@ interface GetPlaylistDetailsFirstPageArgs extends EndpointHandlerBaseArgs {
|
||||
const getPlaylistDetailsFirstPage: (
|
||||
opts: GetPlaylistDetailsFirstPageArgs
|
||||
) => Promise<GetPlaylist | null> = async ({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
initialFields,
|
||||
playlistID,
|
||||
}) => {
|
||||
const response = await singleRequest<GetPlaylist>(
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
allowedMethods.Get,
|
||||
`/playlists/${playlistID}/`,
|
||||
@ -170,9 +173,13 @@ interface GetPlaylistDetailsNextPageArgs extends EndpointHandlerBaseArgs {
|
||||
}
|
||||
const getPlaylistDetailsNextPage: (
|
||||
opts: GetPlaylistDetailsNextPageArgs
|
||||
) => Promise<GetPlaylistItems | null> = async ({ req, res, nextURL }) => {
|
||||
) => Promise<GetPlaylistItems | null> = async ({
|
||||
authHeaders,
|
||||
res,
|
||||
nextURL,
|
||||
}) => {
|
||||
const response = await singleRequest<GetPlaylistItems>(
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
allowedMethods.Get,
|
||||
nextURL
|
||||
@ -187,13 +194,13 @@ interface AddItemsToPlaylistArgs extends EndpointHandlerBaseArgs {
|
||||
const addItemsToPlaylist: (
|
||||
opts: AddItemsToPlaylistArgs
|
||||
) => Promise<AddItemsToPlaylist | null> = async ({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
nextBatch,
|
||||
playlistID,
|
||||
}) => {
|
||||
const response = await singleRequest<AddItemsToPlaylist>(
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
allowedMethods.Post,
|
||||
`/playlists/${playlistID}/tracks`,
|
||||
@ -212,7 +219,7 @@ interface RemovePlaylistItemsArgs extends EndpointHandlerBaseArgs {
|
||||
const removePlaylistItems: (
|
||||
opts: RemovePlaylistItemsArgs
|
||||
) => Promise<RemovePlaylistItems | null> = async ({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
nextBatch,
|
||||
playlistID,
|
||||
@ -221,7 +228,7 @@ const removePlaylistItems: (
|
||||
// API doesn't document this kind of deletion via the 'positions' field
|
||||
// but see here: https://github.com/spotipy-dev/spotipy/issues/95#issuecomment-2263634801
|
||||
const response = await singleRequest<RemovePlaylistItems>(
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
allowedMethods.Delete,
|
||||
`/playlists/${playlistID}/tracks`,
|
||||
@ -243,11 +250,11 @@ interface CheckPlaylistEditableArgs extends EndpointHandlerBaseArgs {
|
||||
}
|
||||
const checkPlaylistEditable: (
|
||||
opts: CheckPlaylistEditableArgs
|
||||
) => Promise<boolean> = async ({ req, res, playlistID, userID }) => {
|
||||
) => Promise<boolean> = async ({ authHeaders, res, playlistID, userID }) => {
|
||||
let checkFields = ["collaborative", "owner(id)"];
|
||||
|
||||
const checkFromData = await getPlaylistDetailsFirstPage({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
initialFields: checkFields.join(),
|
||||
playlistID,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { RequestHandler } from "express";
|
||||
|
||||
import curriedLogger from "../utils/logger.ts";
|
||||
const logger = curriedLogger(import.meta.filename);
|
||||
import logger from "../utils/logger.ts";
|
||||
|
||||
const __controller_func: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
|
||||
@ -48,6 +48,7 @@ const callback: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { code, state, error } = req.query;
|
||||
const storedState = req.cookies ? req.cookies[stateKey] : null;
|
||||
let authHeaders;
|
||||
|
||||
// check state
|
||||
if (state === null || state !== storedState) {
|
||||
@ -76,14 +77,18 @@ const callback: RequestHandler = async (req, res) => {
|
||||
logger.debug("Tokens obtained.");
|
||||
req.session.accessToken = tokenResponse.data.access_token;
|
||||
req.session.refreshToken = tokenResponse.data.refresh_token;
|
||||
authHeaders = {
|
||||
Authorization: `Bearer ${req.session.accessToken}`,
|
||||
};
|
||||
} else {
|
||||
logger.error("login failed", { statusCode: tokenResponse.status });
|
||||
res
|
||||
.status(tokenResponse.status)
|
||||
.send({ message: "Error: Login failed" });
|
||||
return null;
|
||||
}
|
||||
|
||||
const userData = await getCurrentUsersProfile({ req, res });
|
||||
const userData = await getCurrentUsersProfile({ authHeaders, res });
|
||||
if (!userData) return null;
|
||||
|
||||
req.session.user = {
|
||||
|
||||
@ -37,13 +37,20 @@ import logger from "../utils/logger.ts";
|
||||
*/
|
||||
const updateUser: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
let currentPlaylists: PlaylistModel_Pl[] = [];
|
||||
if (!req.session.user)
|
||||
throw new ReferenceError("sessionData does not have user object");
|
||||
throw new ReferenceError("session does not have user object");
|
||||
const { authHeaders } = req.session;
|
||||
if (!authHeaders)
|
||||
throw new ReferenceError("session does not have auth headers");
|
||||
const uID = req.session.user.id;
|
||||
|
||||
let currentPlaylists: PlaylistModel_Pl[] = [];
|
||||
|
||||
// get first 50
|
||||
const respData = await getCurrentUsersPlaylistsFirstPage({ req, res });
|
||||
const respData = await getCurrentUsersPlaylistsFirstPage({
|
||||
authHeaders,
|
||||
res,
|
||||
});
|
||||
if (!respData) return null;
|
||||
|
||||
currentPlaylists = respData.items.map((playlist) => {
|
||||
@ -57,7 +64,7 @@ const updateUser: RequestHandler = async (req, res) => {
|
||||
// keep getting batches of 50 till exhausted
|
||||
while (nextURL) {
|
||||
const nextData = await getCurrentUsersPlaylistsNextPage({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
nextURL,
|
||||
});
|
||||
@ -217,7 +224,7 @@ const fetchUser: RequestHandler = async (req, res) => {
|
||||
// return null;
|
||||
// }
|
||||
if (!req.session.user)
|
||||
throw new ReferenceError("sessionData does not have user object");
|
||||
throw new ReferenceError("session does not have user object");
|
||||
const uID = req.session.user.id;
|
||||
|
||||
const currentPlaylists = await Playlists.findAll({
|
||||
@ -259,7 +266,7 @@ const createLink: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
// await sleep(1000);
|
||||
if (!req.session.user)
|
||||
throw new ReferenceError("sessionData does not have user object");
|
||||
throw new ReferenceError("session does not have user object");
|
||||
const uID = req.session.user.id;
|
||||
|
||||
let fromPl, toPl;
|
||||
@ -351,7 +358,7 @@ const createLink: RequestHandler = async (req, res) => {
|
||||
const removeLink: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
if (!req.session.user)
|
||||
throw new Error("sessionData does not have user object");
|
||||
throw new ReferenceError("session does not have user object");
|
||||
const uID = req.session.user.id;
|
||||
|
||||
let fromPl, toPl;
|
||||
@ -416,12 +423,16 @@ interface _GetPlaylistTracks {
|
||||
}
|
||||
const _getPlaylistTracks: (
|
||||
opts: _GetPlaylistTracksArgs
|
||||
) => Promise<_GetPlaylistTracks | null> = async ({ req, res, playlistID }) => {
|
||||
) => Promise<_GetPlaylistTracks | null> = async ({
|
||||
authHeaders,
|
||||
res,
|
||||
playlistID,
|
||||
}) => {
|
||||
let initialFields = ["snapshot_id,tracks(next,items(is_local,track(uri)))"];
|
||||
let mainFields = ["next", "items(is_local,track(uri))"];
|
||||
|
||||
const respData = await getPlaylistDetailsFirstPage({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
initialFields: initialFields.join(),
|
||||
playlistID,
|
||||
@ -460,7 +471,7 @@ const _getPlaylistTracks: (
|
||||
// keep getting batches of 50 till exhausted
|
||||
while (nextURL) {
|
||||
const nextData = await getPlaylistDetailsNextPage({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
nextURL,
|
||||
});
|
||||
@ -514,7 +525,7 @@ interface _PopulateSingleLinkCoreArgs extends EndpointHandlerBaseArgs {
|
||||
const _populateSingleLinkCore: (
|
||||
opts: _PopulateSingleLinkCoreArgs
|
||||
) => Promise<{ toAddNum: number; localNum: number } | null> = async ({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
link,
|
||||
}) => {
|
||||
@ -523,12 +534,12 @@ const _populateSingleLinkCore: (
|
||||
toPl = link.to;
|
||||
|
||||
const fromPlaylist = await _getPlaylistTracks({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
playlistID: fromPl.id,
|
||||
});
|
||||
const toPlaylist = await _getPlaylistTracks({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
playlistID: toPl.id,
|
||||
});
|
||||
@ -547,7 +558,7 @@ const _populateSingleLinkCore: (
|
||||
while (toTrackURIs.length > 0) {
|
||||
const nextBatch = toTrackURIs.splice(0, 100);
|
||||
const addData = await addItemsToPlaylist({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
nextBatch,
|
||||
playlistID: fromPl.id,
|
||||
@ -566,8 +577,11 @@ const _populateSingleLinkCore: (
|
||||
const populateSingleLink: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
if (!req.session.user)
|
||||
throw new Error("sessionData does not have user object");
|
||||
throw new ReferenceError("session does not have user object");
|
||||
const uID = req.session.user.id;
|
||||
const { authHeaders } = req.session;
|
||||
if (!authHeaders)
|
||||
throw new ReferenceError("session does not have auth headers");
|
||||
const link = { from: req.body.from, to: req.body.to };
|
||||
let fromPl, toPl;
|
||||
|
||||
@ -599,7 +613,7 @@ const populateSingleLink: RequestHandler = async (req, res) => {
|
||||
|
||||
if (
|
||||
!(await checkPlaylistEditable({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
playlistID: fromPl.id,
|
||||
userID: uID,
|
||||
@ -608,7 +622,7 @@ const populateSingleLink: RequestHandler = async (req, res) => {
|
||||
return null;
|
||||
|
||||
const result = await _populateSingleLinkCore({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
link: { from: fromPl, to: toPl },
|
||||
});
|
||||
@ -651,18 +665,22 @@ interface _PruneSingleLinkCoreArgs extends EndpointHandlerBaseArgs {
|
||||
*/
|
||||
const _pruneSingleLinkCore: (
|
||||
opts: _PruneSingleLinkCoreArgs
|
||||
) => Promise<{ toDelNum: number } | null> = async ({ req, res, link }) => {
|
||||
) => Promise<{ toDelNum: number } | null> = async ({
|
||||
authHeaders,
|
||||
res,
|
||||
link,
|
||||
}) => {
|
||||
try {
|
||||
const fromPl = link.from,
|
||||
toPl = link.to;
|
||||
|
||||
const fromPlaylist = await _getPlaylistTracks({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
playlistID: fromPl.id,
|
||||
});
|
||||
const toPlaylist = await _getPlaylistTracks({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
playlistID: toPl.id,
|
||||
});
|
||||
@ -684,7 +702,7 @@ const _pruneSingleLinkCore: (
|
||||
while (indexes.length > 0) {
|
||||
const nextBatch = indexes.splice(Math.max(indexes.length - 100, 0), 100);
|
||||
const delResponse = await removePlaylistItems({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
nextBatch,
|
||||
playlistID: toPl.id,
|
||||
@ -705,8 +723,11 @@ const _pruneSingleLinkCore: (
|
||||
const pruneSingleLink: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
if (!req.session.user)
|
||||
throw new Error("sessionData does not have user object");
|
||||
throw new ReferenceError("session does not have user object");
|
||||
const uID = req.session.user.id;
|
||||
const { authHeaders } = req.session;
|
||||
if (!authHeaders)
|
||||
throw new ReferenceError("session does not have auth headers");
|
||||
const link = { from: req.body.from, to: req.body.to };
|
||||
|
||||
let fromPl, toPl;
|
||||
@ -738,7 +759,7 @@ const pruneSingleLink: RequestHandler = async (req, res) => {
|
||||
|
||||
if (
|
||||
!(await checkPlaylistEditable({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
playlistID: toPl.id,
|
||||
userID: uID,
|
||||
@ -747,7 +768,7 @@ const pruneSingleLink: RequestHandler = async (req, res) => {
|
||||
return null;
|
||||
|
||||
const result = await _pruneSingleLinkCore({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
link: {
|
||||
from: fromPl,
|
||||
|
||||
@ -16,8 +16,14 @@ import logger from "../utils/logger.ts";
|
||||
*/
|
||||
const fetchUserPlaylists: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { authHeaders } = req.session;
|
||||
if (!authHeaders)
|
||||
throw new ReferenceError("session does not have auth headers");
|
||||
// get first 50
|
||||
const respData = await getCurrentUsersPlaylistsFirstPage({ req, res });
|
||||
const respData = await getCurrentUsersPlaylistsFirstPage({
|
||||
authHeaders,
|
||||
res,
|
||||
});
|
||||
if (!respData) return null;
|
||||
|
||||
let tmpData = structuredClone(respData);
|
||||
@ -32,7 +38,7 @@ const fetchUserPlaylists: RequestHandler = async (req, res) => {
|
||||
// keep getting batches of 50 till exhausted
|
||||
while (nextURL) {
|
||||
const nextData = await getCurrentUsersPlaylistsNextPage({
|
||||
req,
|
||||
authHeaders,
|
||||
res,
|
||||
nextURL,
|
||||
});
|
||||
@ -51,4 +57,5 @@ const fetchUserPlaylists: RequestHandler = async (req, res) => {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { fetchUserPlaylists };
|
||||
|
||||
5
index.ts
5
index.ts
@ -91,7 +91,10 @@ app.use("/health", (_req, res) => {
|
||||
});
|
||||
app.use("/auth-health", isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const respData = await getCurrentUsersProfile({ req, res });
|
||||
const { authHeaders } = req.session;
|
||||
if (!authHeaders)
|
||||
throw new ReferenceError("session does not have auth headers");
|
||||
const respData = await getCurrentUsersProfile({ authHeaders, res });
|
||||
if (!respData) return null;
|
||||
res.status(200).send({ message: "OK" });
|
||||
return null;
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { AxiosRequestHeaders } from "axios";
|
||||
import type { RequestHandler } from "express";
|
||||
|
||||
import { sessionName } from "../constants.ts";
|
||||
@ -9,7 +8,7 @@ export const isAuthenticated: RequestHandler = (req, res, next) => {
|
||||
if (req.session.accessToken) {
|
||||
req.session.authHeaders = {
|
||||
Authorization: `Bearer ${req.session.accessToken}`,
|
||||
} as AxiosRequestHeaders;
|
||||
};
|
||||
next();
|
||||
} else {
|
||||
const delSession = req.session.destroy((error) => {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Router } from "express";
|
||||
const router: Router = Router();
|
||||
const playlistRouter: Router = Router();
|
||||
|
||||
import { fetchUserPlaylists } from "../controllers/playlists.ts";
|
||||
|
||||
// import { validate } from "../validators/index.ts";
|
||||
|
||||
router.get("/me", fetchUserPlaylists);
|
||||
playlistRouter.get("/me", fetchUserPlaylists);
|
||||
|
||||
export default router;
|
||||
export default playlistRouter;
|
||||
|
||||
4
types/express-session.d.ts
vendored
4
types/express-session.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import type { AxiosRequestHeaders } from "axios";
|
||||
import type { RawAxiosRequestHeaders } from "axios";
|
||||
import type { User } from "spotify_manager/index.d.ts";
|
||||
|
||||
declare module "express-session" {
|
||||
@ -6,7 +6,7 @@ declare module "express-session" {
|
||||
interface SessionData {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
authHeaders: AxiosRequestHeaders;
|
||||
authHeaders: RawAxiosRequestHeaders;
|
||||
user: User;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { RawAxiosRequestHeaders } from "axios";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
|
||||
export type Req = Request;
|
||||
@ -5,6 +6,6 @@ export type Res = Response;
|
||||
export type Next = NextFunction;
|
||||
|
||||
export interface EndpointHandlerBaseArgs {
|
||||
req: Req;
|
||||
authHeaders: RawAxiosRequestHeaders;
|
||||
res: Res;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user