From 9b597696d57c8a34fb24098ee5347ff09ade2d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20W=C4=85=C5=9B?= <63346093+KT-Trez@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:38:56 +0200 Subject: [PATCH 1/4] feat: support go server --- README.md | 2 +- api/v3.0.0.yaml | 59 +++++++++++++++++++++++++-- package-lock.json | 27 ++++++++---- package.json | 2 +- src/controllers/v3/song.controller.ts | 23 ++++++----- src/types/global.d.ts | 1 + 6 files changed, 91 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index ee7541e..33eb8b3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Backend service for the Symfi application. * `PORT`: (`number | undefined`) – port to run the server on * `PROXY_DOWNLOAD_ENABLED`: (`true | undefined`) – enable proxy download * `PROXY_DOWNLOAD_ORIGIN`: (`string | undefined`) – origin to proxy download - requests to +* `PROXY_DOWNLOAD_STREAM_ENDPOINT`: (`true | undefined`) – uses newer "/stream" endpoint for proxy download (requires PROXY_DOWNLOAD_ENABLED) ## License diff --git a/api/v3.0.0.yaml b/api/v3.0.0.yaml index 0269f9a..854ba50 100644 --- a/api/v3.0.0.yaml +++ b/api/v3.0.0.yaml @@ -9,6 +9,8 @@ info: servers: - description: Development server url: http://localhost:5000/ + - description: Primary server + url: https://symfi-api.was.org.pl paths: /v3/ping: get: @@ -20,11 +22,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Success' - /v3/song/:id: + /v3/song/{id}: get: + deprecated: true description: Download a song by its id parameters: - - name: songId + - name: id in: path required: true schema: @@ -34,7 +37,7 @@ paths: '200': description: Song found content: - audio/mpeg3: + audio/webm: schema: format: binary type: string @@ -46,6 +49,7 @@ paths: $ref: '#/components/responses/YouTubeError' /v3/song/download: get: + deprecated: true description: Get the song download URL parameters: - name: id @@ -67,6 +71,29 @@ paths: $ref: '#/components/responses/NotFound' '502': $ref: '#/components/responses/YouTubeError' + /v3/song/meta/{id}: + get: + description: Get the song download URL + parameters: + - name: id + in: path + required: true + schema: + type: string + example: 'dQw4w9WgXcQ' + responses: + '200': + description: Success response with the song download URL in the meta field + content: + application/json: + schema: + $ref: '#/components/schemas/Success' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '502': + $ref: '#/components/responses/YouTubeError' /v3/song/search: get: description: Search for a song @@ -102,6 +129,30 @@ paths: $ref: '#/components/schemas/Error' '502': $ref: '#/components/responses/YouTubeError' + /v3/song/stream/{id}: + get: + description: Download a song by its id + parameters: + - name: id + in: path + required: true + schema: + type: string + example: 'dQw4w9WgXcQ' + responses: + '200': + description: Song found + content: + audio/webm: + schema: + format: binary + type: string + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '502': + $ref: '#/components/responses/YouTubeError' /v3/song/suggestion: get: description: Get suggestions for songs @@ -212,7 +263,7 @@ components: id: description: "YouTube video ID" example: "dQw4w9WgXcQ" - type: integer + type: string name: description: "Song title" example: "Never Gonna Give You Up" diff --git a/package-lock.json b/package-lock.json index 6cb2d8a..3eb2cdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "express-validator": "^7.1.0", "file-system-cache": "2.4.7", "tsconfig-paths": "^4.2.0", - "youtubei.js": "10.4.0" + "youtubei.js": "^10.5.0" }, "devDependencies": { "@biomejs/biome": "^1.8.3", @@ -933,6 +933,12 @@ "node": ">=14.21.3" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.1.0.tgz", + "integrity": "sha512-+2Mx67Y3skJ4NCD/qNSdBJNWtu6x6Qr53jeNg+QcwiL6mt0wK+3jwHH2x1p7xaYH6Ve2JKOVn0OxU35WsmqI9A==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -5599,14 +5605,15 @@ } }, "node_modules/youtubei.js": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-10.4.0.tgz", - "integrity": "sha512-FZahkkg5ROyH/FgJ4czy/xDNkqHbJTCUQzumQlnR+2Q7m6HaWghAFWWJUTcexemGuu7t/5EuyQz98eBgKQRMog==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-10.5.0.tgz", + "integrity": "sha512-iyA+VF28c15tCCKH9ExM2RKC3zYiHzA/eixGlJ3vERANkuI+xYKzAZ4vtOhmyqwrAddu88R/DkzEsmpph5NWjg==", "funding": [ "https://github.com/sponsors/LuanRT" ], "license": "MIT", "dependencies": { + "@bufbuild/protobuf": "^2.0.0", "jintr": "^2.1.1", "tslib": "^2.5.0", "undici": "^5.19.1" @@ -6246,6 +6253,11 @@ "dev": true, "optional": true }, + "@bufbuild/protobuf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.1.0.tgz", + "integrity": "sha512-+2Mx67Y3skJ4NCD/qNSdBJNWtu6x6Qr53jeNg+QcwiL6mt0wK+3jwHH2x1p7xaYH6Ve2JKOVn0OxU35WsmqI9A==" + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -9741,10 +9753,11 @@ "dev": true }, "youtubei.js": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-10.4.0.tgz", - "integrity": "sha512-FZahkkg5ROyH/FgJ4czy/xDNkqHbJTCUQzumQlnR+2Q7m6HaWghAFWWJUTcexemGuu7t/5EuyQz98eBgKQRMog==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-10.5.0.tgz", + "integrity": "sha512-iyA+VF28c15tCCKH9ExM2RKC3zYiHzA/eixGlJ3vERANkuI+xYKzAZ4vtOhmyqwrAddu88R/DkzEsmpph5NWjg==", "requires": { + "@bufbuild/protobuf": "^2.0.0", "jintr": "^2.1.1", "tslib": "^2.5.0", "undici": "^5.19.1" diff --git a/package.json b/package.json index 31e46a0..f2bb564 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "express-validator": "^7.1.0", "file-system-cache": "2.4.7", "tsconfig-paths": "^4.2.0", - "youtubei.js": "10.4.0" + "youtubei.js": "^10.5.0" }, "description": "", "devDependencies": { diff --git a/src/controllers/v3/song.controller.ts b/src/controllers/v3/song.controller.ts index 749ed85..44e7744 100644 --- a/src/controllers/v3/song.controller.ts +++ b/src/controllers/v3/song.controller.ts @@ -14,6 +14,7 @@ const download = async ( next: NextFunction, ) => { const id = req.query.id; + const isProxyEnabled = process.env.PROXY_DOWNLOAD_ENABLED?.match(/true/i); try { // search instance of the YouTube's API @@ -24,13 +25,16 @@ const download = async ( const info = await youtube.getBasicInfo(id); // redirect request to the local endpoint that streams audio - if (process.env.PROXY_DOWNLOAD_ENABLED) { - const origin = - process.env.PROXY_DOWNLOAD_ORIGIN || - `${req.protocol}://${req.get('host')}`; - const redirect = new URL(`/v3/song/${id}`, origin); - res.status(200).json(new ApiSuccess('Video found', redirect.href)); - return; + if (isProxyEnabled) { + const dynamicOrigin = `${req.protocol}://${req.get('host')}`; + const origin = process.env.PROXY_DOWNLOAD_ORIGIN || dynamicOrigin; + + const dynamicPath = `/v3/song/${id}`; + const path = (process.env.PROXY_DOWNLOAD_STREAM_ENDPOINT?.match(/true/i) && `/v3/song/stream/${id}`) || dynamicPath; + + const url = new URL(path, origin); + + return res.status(200).json(new ApiSuccess('Video found', url.href)); } const format = info.chooseFormat({ @@ -138,10 +142,9 @@ const songId = async ( try { const stream = await youtube.download(id, { - // todo: fix in youtubei.js - // format: 'm4a', + format: 'webm', type: 'audio', - // quality: 'best', + quality: 'best', }); for await (const chunk of Utils.streamToIterable(stream)) { diff --git a/src/types/global.d.ts b/src/types/global.d.ts index eacfc7b..a530017 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -10,6 +10,7 @@ declare global { PORT?: string; PROXY_DOWNLOAD_ENABLED?: 'true'; PROXY_DOWNLOAD_ORIGIN?: string; + PROXY_DOWNLOAD_STREAM_ENDPOINT?: string; npm_package_version?: string; } From 799695ad43f5b7627c39cd3b513dbcc775b5280e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20W=C4=85=C5=9B?= <63346093+KT-Trez@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:41:09 +0200 Subject: [PATCH 2/4] fix: lint --- biome.json | 5 +---- src/controllers/v3/song.controller.ts | 13 ++++++++----- src/services/download.service.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/biome.json b/biome.json index 6b85cde..0ab15d5 100644 --- a/biome.json +++ b/biome.json @@ -1,10 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "files": { - "ignore": [ - "package.json", - "package-lock.json" - ], + "ignore": ["package.json", "package-lock.json"], "include": ["**/*.ts", "**/*.json"] }, "formatter": { diff --git a/src/controllers/v3/song.controller.ts b/src/controllers/v3/song.controller.ts index 44e7744..2450323 100644 --- a/src/controllers/v3/song.controller.ts +++ b/src/controllers/v3/song.controller.ts @@ -2,7 +2,7 @@ import { ApiErrorV2, ApiSuccess, CollectionFormatResource, - SongResource + SongResource, } from '@resources'; import type { CollectionFormat, NoBody, NoParams, NoQuery, Song } from '@types'; import type { NextFunction, Request, Response } from 'express'; @@ -30,7 +30,10 @@ const download = async ( const origin = process.env.PROXY_DOWNLOAD_ORIGIN || dynamicOrigin; const dynamicPath = `/v3/song/${id}`; - const path = (process.env.PROXY_DOWNLOAD_STREAM_ENDPOINT?.match(/true/i) && `/v3/song/stream/${id}`) || dynamicPath; + const path = + (process.env.PROXY_DOWNLOAD_STREAM_ENDPOINT?.match(/true/i) && + `/v3/song/stream/${id}`) || + dynamicPath; const url = new URL(path, origin); @@ -159,7 +162,7 @@ const songId = async ( const error = new ApiErrorV2( 404, 'Not Found', - 'The requested video was not found.' + 'The requested video was not found.', ); return next(error); @@ -169,7 +172,7 @@ const songId = async ( const error = new ApiErrorV2( 404, 'Not Found', - 'Matching formats in the requested video were not found.' + 'Matching formats in the requested video were not found.', ); return next(error); @@ -179,7 +182,7 @@ const songId = async ( 502, 'Bad Gateway', 'Cannot connect to YouTube servers at the moment.', - err + err, ); next(error); diff --git a/src/services/download.service.ts b/src/services/download.service.ts index 05370df..9ce41ff 100644 --- a/src/services/download.service.ts +++ b/src/services/download.service.ts @@ -32,7 +32,7 @@ export const getResource = async ( ): Promise => { const youtube = await Innertube.create({ cache: new UniversalCache(true), - generate_session_locally: true + generate_session_locally: true, }); const stream = await youtube.download(resourceId, { From a3e83bb9af4d5127b667c3ad30b5fe07f486cde8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20W=C4=85=C5=9B?= <63346093+KT-Trez@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:49:25 +0200 Subject: [PATCH 3/4] feat: support download using new go server * add temporary support for the new `/v3/song/meta` endpoint * add go server support for API v2 --- api/{v3.0.0.yaml => v3.x.x.yaml} | 2 +- biome.json | 12 +--- src/controllers/v2/media.controller.ts | 10 ++- src/controllers/v3/song.controller.ts | 99 +++++++------------------- src/routers/v3/song.router.ts | 26 +++++-- 5 files changed, 60 insertions(+), 89 deletions(-) rename api/{v3.0.0.yaml => v3.x.x.yaml} (99%) diff --git a/api/v3.0.0.yaml b/api/v3.x.x.yaml similarity index 99% rename from api/v3.0.0.yaml rename to api/v3.x.x.yaml index 854ba50..89c70a4 100644 --- a/api/v3.0.0.yaml +++ b/api/v3.x.x.yaml @@ -5,7 +5,7 @@ info: name: MIT url: https://opensource.org/licenses/MIT title: Symfi API - version: 3.0.0 + version: 3.1.0 servers: - description: Development server url: http://localhost:5000/ diff --git a/biome.json b/biome.json index 0ab15d5..2db7119 100644 --- a/biome.json +++ b/biome.json @@ -9,22 +9,14 @@ "indentStyle": "space", "indentWidth": 2, "lineEnding": "lf", - "lineWidth": 80 + "lineWidth": 120 }, "javascript": { "formatter": { "arrowParentheses": "asNeeded", "quoteStyle": "single" }, - "globals": [ - "afterAll", - "beforeAll", - "beforeEach", - "describe", - "expect", - "it", - "jest" - ] + "globals": ["afterAll", "beforeAll", "beforeEach", "describe", "expect", "it", "jest"] }, "linter": { "enabled": true, diff --git a/src/controllers/v2/media.controller.ts b/src/controllers/v2/media.controller.ts index acda89d..87580d9 100644 --- a/src/controllers/v2/media.controller.ts +++ b/src/controllers/v2/media.controller.ts @@ -12,8 +12,16 @@ const getMediaURL = async ( // redirect request to the local endpoint that streams audio if (process.env.PROXY_DOWNLOAD_ENABLED) { + const streamEndpointEnv = process.env.PROXY_DOWNLOAD_STREAM_ENDPOINT; + const hasCustomStreamEndpoint = streamEndpointEnv?.match(/true/i); + + const origin = process.env.PROXY_DOWNLOAD_ORIGIN || `${req.protocol}://${req.get('host')}`; + const path = hasCustomStreamEndpoint ? `/v3/song/stream/${id}` : `/v3/song/${id}`; + + const url = new URL(path, origin); + return res.status(200).json({ - link: `${req.protocol}://${req.get('host')}/v2/content/youtube/${id}`, + link: url.href, }); } diff --git a/src/controllers/v3/song.controller.ts b/src/controllers/v3/song.controller.ts index 2450323..760ae36 100644 --- a/src/controllers/v3/song.controller.ts +++ b/src/controllers/v3/song.controller.ts @@ -1,9 +1,4 @@ -import { - ApiErrorV2, - ApiSuccess, - CollectionFormatResource, - SongResource, -} from '@resources'; +import { ApiErrorV2, ApiSuccess, CollectionFormatResource, SongResource } from '@resources'; import type { CollectionFormat, NoBody, NoParams, NoQuery, Song } from '@types'; import type { NextFunction, Request, Response } from 'express'; import { Innertube, UniversalCache, Utils } from 'youtubei.js'; @@ -26,14 +21,11 @@ const download = async ( // redirect request to the local endpoint that streams audio if (isProxyEnabled) { - const dynamicOrigin = `${req.protocol}://${req.get('host')}`; - const origin = process.env.PROXY_DOWNLOAD_ORIGIN || dynamicOrigin; + const streamEndpointEnv = process.env.PROXY_DOWNLOAD_STREAM_ENDPOINT; + const hasCustomStreamEndpoint = streamEndpointEnv?.match(/true/i); - const dynamicPath = `/v3/song/${id}`; - const path = - (process.env.PROXY_DOWNLOAD_STREAM_ENDPOINT?.match(/true/i) && - `/v3/song/stream/${id}`) || - dynamicPath; + const origin = process.env.PROXY_DOWNLOAD_ORIGIN || `${req.protocol}://${req.get('host')}`; + const path = hasCustomStreamEndpoint ? `/v3/song/stream/${id}` : `/v3/song/${id}`; const url = new URL(path, origin); @@ -45,38 +37,31 @@ const download = async ( type: 'audio', }); - res - .status(200) - .json( - new ApiSuccess('Video found', format.decipher(youtube.session.player)), - ); + res.status(200).json(new ApiSuccess('Video found', format.decipher(youtube.session.player))); } catch (err) { - if ( - err instanceof Error && - /this video is unavailable/i.test(err.message) - ) { - next( - new ApiErrorV2(404, 'Not Found', 'The requested video was not found.'), - ); + if (err instanceof Error && /this video is unavailable/i.test(err.message)) { + next(new ApiErrorV2(404, 'Not Found', 'The requested video was not found.')); return; } - next( - new ApiErrorV2( - 502, - 'Bad Gateway', - 'Cannot connect to YouTube servers at the moment.', - err, - ), - ); + next(new ApiErrorV2(502, 'Bad Gateway', 'Cannot connect to YouTube servers at the moment.', err)); } }; +const meta = (req: Request, res: Response, _: NextFunction) => { + const id = req.params.id; + + res.redirect(307, `/v3/song/download?id=${id}`); +}; + const search = async ( req: Request< never, CollectionFormat, undefined, - { page?: string; q: string } + { + page?: string; + q: string; + } >, res: Response>, next: NextFunction, @@ -95,13 +80,7 @@ const search = async ( }); if (search.videos.length <= 0) { - return next( - new ApiErrorV2( - 404, - 'No resource', - 'The videos matching requested query were not found.', - ), - ); + return next(new ApiErrorV2(404, 'No resource', 'The videos matching requested query were not found.')); } const resourceMap = new Map(); @@ -120,14 +99,7 @@ const search = async ( res.status(200).json(data); } catch (err) { - next( - new ApiErrorV2( - 502, - 'Bad Gateway', - 'Cannot connect to YouTube servers at the moment.', - err, - ), - ); + next(new ApiErrorV2(502, 'Bad Gateway', 'Cannot connect to YouTube servers at the moment.', err)); } }; @@ -159,31 +131,18 @@ const songId = async ( const isError = err instanceof Error; if (isError && /this video is unavailable/i.test(err.message)) { - const error = new ApiErrorV2( - 404, - 'Not Found', - 'The requested video was not found.', - ); + const error = new ApiErrorV2(404, 'Not Found', 'The requested video was not found.'); return next(error); } if (isError && /no matching formats found/i.test(err.message)) { - const error = new ApiErrorV2( - 404, - 'Not Found', - 'Matching formats in the requested video were not found.', - ); + const error = new ApiErrorV2(404, 'Not Found', 'Matching formats in the requested video were not found.'); return next(error); } - const error = new ApiErrorV2( - 502, - 'Bad Gateway', - 'Cannot connect to YouTube servers at the moment.', - err, - ); + const error = new ApiErrorV2(502, 'Bad Gateway', 'Cannot connect to YouTube servers at the moment.', err); next(error); } @@ -211,19 +170,13 @@ const suggestion = async ( res.status(200).json(data); } catch (err) { - next( - new ApiErrorV2( - 502, - 'Bad Gateway', - 'Cannot connect to YouTube servers at the moment.', - err, - ), - ); + next(new ApiErrorV2(502, 'Bad Gateway', 'Cannot connect to YouTube servers at the moment.', err)); } }; export const songController = { download, + meta, search, songId, suggestion, diff --git a/src/routers/v3/song.router.ts b/src/routers/v3/song.router.ts index 58f49d5..e3ae457 100644 --- a/src/routers/v3/song.router.ts +++ b/src/routers/v3/song.router.ts @@ -2,6 +2,7 @@ import { songController } from '@controllers'; import { requestValidatorService } from '@services'; import express from 'express'; import { param, query } from 'express-validator'; +import { Innertube, UniversalCache } from 'youtubei.js'; const router = express.Router(); @@ -18,12 +19,29 @@ router.get( songController.download, ); +router.get( + ['/meta', '/meta/:id'], + param('id') + .notEmpty() + .withMessage('required, must be a string') + .bail() + .custom(async (_, { req }) => { + const audioID = req.params?.id; + + const youtube = await Innertube.create({ + cache: new UniversalCache(false), + }); + + return !!(await youtube.getInfo(audioID)); + }) + .withMessage('incorrect id, no such song'), + requestValidatorService, + songController.meta, +); + router.get( '/search', - query('page') - .optional() - .isInt({ min: 0 }) - .withMessage('optional, must be a number greater than or equal to 0'), + query('page').optional().isInt({ min: 0 }).withMessage('optional, must be a number greater than or equal to 0'), query('q') .exists() .withMessage('required') From b98f743de04da952315a7b4d08db69e7993cd2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20W=C4=85=C5=9B?= <63346093+KT-Trez@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:50:26 +0200 Subject: [PATCH 4/4] fix: lint --- src/controllers/v2/content.controller.ts | 11 +-- src/controllers/v2/search.controller.ts | 4 +- src/main.ts | 20 ++---- src/resources/ApiError.ts | 14 +--- src/resources/Song.ts | 24 ++----- src/routers/v2/content.router.ts | 4 +- src/routers/v3.router.ts | 4 +- src/services/logger.service.ts | 5 +- src/services/requestValidator.service.ts | 14 +--- tests/e2e/v3/song.spec.ts | 91 +++++++++--------------- 10 files changed, 54 insertions(+), 137 deletions(-) diff --git a/src/controllers/v2/content.controller.ts b/src/controllers/v2/content.controller.ts index 807edf3..76f227c 100644 --- a/src/controllers/v2/content.controller.ts +++ b/src/controllers/v2/content.controller.ts @@ -26,10 +26,7 @@ const checkIdsCorrectness = async ( try { const data = mediaInfoPromises - .filter( - (promise): promise is PromiseFulfilledResult => - promise.status === 'fulfilled', - ) + .filter((promise): promise is PromiseFulfilledResult => promise.status === 'fulfilled') .map(({ value }) => new VideoInfoToMediaInfoAdapter(value)); res.status(200).json(data); @@ -38,11 +35,7 @@ const checkIdsCorrectness = async ( } }; -const streamAudio = async ( - req: Request<{ id: string }>, - res: Response, - next: NextFunction, -) => { +const streamAudio = async (req: Request<{ id: string }>, res: Response, next: NextFunction) => { // get resource id and path to resource if it is cached const resourceID = decodeURI(req.params.id); const cachedPath = cache.getSync(resourceID); diff --git a/src/controllers/v2/search.controller.ts b/src/controllers/v2/search.controller.ts index 4ad8edf..e94e159 100644 --- a/src/controllers/v2/search.controller.ts +++ b/src/controllers/v2/search.controller.ts @@ -8,7 +8,9 @@ const searchThroughYouTube = async ( Record, MediaInfo[], undefined, - { query: string } + { + query: string; + } >, res: Response, next: NextFunction, diff --git a/src/main.ts b/src/main.ts index 86125bb..bbd12cd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,11 +6,7 @@ import { ApiError, ApiErrorV2 } from '@resources'; import { v2Router, v3Router } from '@routers'; import { Logger } from '@services'; import cors from 'cors'; -import express, { - type NextFunction, - type Request, - type Response, -} from 'express'; +import express, { type NextFunction, type Request, type Response } from 'express'; import { rateLimit } from 'express-rate-limit'; import { Cache } from 'file-system-cache'; @@ -39,11 +35,7 @@ export const cache = new Cache({ export const limiter = rateLimit({ limit: 500, // max 100 requests per windowMs legacyHeaders: false, - message: new ApiErrorV2( - 429, - 'Too Many Requests', - 'You have exceeded the 100 requests in 15 minutes limit!', - ), + message: new ApiErrorV2(429, 'Too Many Requests', 'You have exceeded the 100 requests in 15 minutes limit!'), standardHeaders: true, windowMs: 15 * 60 * 1000, // 15 minutes }); @@ -64,9 +56,7 @@ app.use('/v2', limiter, v2Router); app.use('/v3', limiter, v3Router); app.all('*', (_req, _res, next: NextFunction) => { - next( - new ApiErrorV2(404, 'Not Found', 'The requested resource was not found.'), - ); + next(new ApiErrorV2(404, 'Not Found', 'The requested resource was not found.')); }); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -82,7 +72,5 @@ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { }); export const server = app.listen(port, () => { - logger.log( - `Status: [STARTED], PORT: [${port}], Version: [v${process.env.npm_package_version}]`, - ); + logger.log(`Status: [STARTED], PORT: [${port}], Version: [v${process.env.npm_package_version}]`); }); diff --git a/src/resources/ApiError.ts b/src/resources/ApiError.ts index e9f9e30..dbb5a92 100644 --- a/src/resources/ApiError.ts +++ b/src/resources/ApiError.ts @@ -3,12 +3,7 @@ export class ApiError extends Error { message: string; status: number; - constructor( - message: string, - status: number, - cause?: unknown, - errors?: string[], - ) { + constructor(message: string, status: number, cause?: unknown, errors?: string[]) { super(message, { cause }); this.errors = errors; this.message = message; @@ -23,12 +18,7 @@ export class ApiErrorV2 extends Error { // noinspection JSUnusedGlobalSymbols success = false; - constructor( - http_status: number, - message: string, - reason: string, - cause?: unknown, - ) { + constructor(http_status: number, message: string, reason: string, cause?: unknown) { super(message, { cause }); this.http_status = http_status; this.message = message; diff --git a/src/resources/Song.ts b/src/resources/Song.ts index b278087..08fb306 100644 --- a/src/resources/Song.ts +++ b/src/resources/Song.ts @@ -4,11 +4,7 @@ import type { Channel, Duration, Song, Views } from '@types'; import { YTNodes } from 'youtubei.js'; import { exhaustiveCheck } from '../utils'; -export type SongResourceCreatorArgs = - | YTNodes.CompactVideo - | YTNodes.GridVideo - | YTNodes.PlaylistVideo - | YTNodes.Video; +export type SongResourceCreatorArgs = YTNodes.CompactVideo | YTNodes.GridVideo | YTNodes.PlaylistVideo | YTNodes.Video; export class SongResource implements Song { channel!: Channel; @@ -51,14 +47,8 @@ export class SongResource implements Song { #fromCompactVideoOrVideo(video: YTNodes.CompactVideo | YTNodes.Video) { const thumbnail = - video.best_thumbnail?.url || - video.thumbnails.at(0)?.url || - this.#getPlaceholder(video.title.toString()); - const views = video.view_count - .toString() - .replace(/,/g, '') - .split(' ') - .at(0); + video.best_thumbnail?.url || video.thumbnails.at(0)?.url || this.#getPlaceholder(video.title.toString()); + const views = video.view_count.toString().replace(/,/g, '').split(' ').at(0); this.channel = { name: video.author.name, @@ -79,9 +69,7 @@ export class SongResource implements Song { } #fromGridVideo(video: YTNodes.GridVideo) { - const thumbnail = - video.thumbnails.at(0)?.url || - this.#getPlaceholder(video.title.toString()); + const thumbnail = video.thumbnails.at(0)?.url || this.#getPlaceholder(video.title.toString()); const views = video.views.toString().replace(/,/g, '').split(' ').at(0); this.channel = { @@ -103,9 +91,7 @@ export class SongResource implements Song { } #fromPlaylistVideo(video: YTNodes.PlaylistVideo) { - const thumbnail = - video.thumbnails.at(0)?.url || - this.#getPlaceholder(video.title.toString()); + const thumbnail = video.thumbnails.at(0)?.url || this.#getPlaceholder(video.title.toString()); this.channel = { name: video.author.name, diff --git a/src/routers/v2/content.router.ts b/src/routers/v2/content.router.ts index 860c02c..02d1217 100644 --- a/src/routers/v2/content.router.ts +++ b/src/routers/v2/content.router.ts @@ -30,9 +30,7 @@ router.use(express.json()); router.post( '/check', - body() - .isArray({ min: 1 }) - .withMessage('incorrect payload, ids to check should be an array'), + body().isArray({ min: 1 }).withMessage('incorrect payload, ids to check should be an array'), requestValidatorService, contentController.checkIdsCorrectness, ); diff --git a/src/routers/v3.router.ts b/src/routers/v3.router.ts index 8cceabf..4c4e518 100644 --- a/src/routers/v3.router.ts +++ b/src/routers/v3.router.ts @@ -11,9 +11,7 @@ router.get('/ping', (_req, res: Response) => { }); router.get('/version', (_req, res: Response) => { - res - .status(200) - .json(new ApiSuccess(process.env.npm_package_version || '4.x.x')); + res.status(200).json(new ApiSuccess(process.env.npm_package_version || '4.x.x')); }); export { router as v3Router }; diff --git a/src/services/logger.service.ts b/src/services/logger.service.ts index e173038..cc61bd0 100644 --- a/src/services/logger.service.ts +++ b/src/services/logger.service.ts @@ -45,10 +45,7 @@ export class Logger { second: '2-digit', year: 'numeric', }).format(new Date()); - const pad = ''.padEnd( - Logger.labelsLength - Logger.labels[options].length, - ' ', - ); + const pad = ''.padEnd(Logger.labelsLength - Logger.labels[options].length, ' '); const severity = this.color(Logger.labels[options], options); switch (options) { diff --git a/src/services/requestValidator.service.ts b/src/services/requestValidator.service.ts index 63405d9..90227d3 100644 --- a/src/services/requestValidator.service.ts +++ b/src/services/requestValidator.service.ts @@ -11,21 +11,11 @@ const errorFormatter = (err: ValidationError) => { } }; -export const requestValidatorService = ( - req: Request, - _: Response, - next: NextFunction, -) => { +export const requestValidatorService = (req: Request, _: Response, next: NextFunction) => { const errors = validationResult(req); if (errors.isEmpty()) { next(); } else { - next( - new ApiErrorV2( - 400, - 'Bad Request', - errors.formatWith(errorFormatter).array().join('\n'), - ), - ); + next(new ApiErrorV2(400, 'Bad Request', errors.formatWith(errorFormatter).array().join('\n'))); } }; diff --git a/tests/e2e/v3/song.spec.ts b/tests/e2e/v3/song.spec.ts index 3264bbd..a639220 100644 --- a/tests/e2e/v3/song.spec.ts +++ b/tests/e2e/v3/song.spec.ts @@ -23,8 +23,7 @@ describe('test "/v3/song" router', () => { const errorTestCases: ErrorTestCase[] = [ { message: 'Bad Request', - reason: - 'query [id]: required\nquery [id]: must be a string\nquery [id]: must be not empty', + reason: 'query [id]: required\nquery [id]: must be a string\nquery [id]: must be not empty', status: 400, }, { @@ -59,30 +58,24 @@ describe('test "/v3/song" router', () => { }, ); - const successTestCases: SuccessTestCase[] = [ - { params: { id: 'dQw4w9WgXcQ' } }, - ]; + const successTestCases: SuccessTestCase[] = [{ params: { id: 'dQw4w9WgXcQ' } }]; - it.each(successTestCases)( - 'should return download link for the query: $params', - async ({ params }) => { - const res = await agent.get('/v3/song/download').query(params || {}); + it.each(successTestCases)('should return download link for the query: $params', async ({ params }) => { + const res = await agent.get('/v3/song/download').query(params || {}); - expect(res.status).toBe(200); - expect(res.body.http_status).toEqual(200); - expect(res.body.message).toEqual('Video found'); - expect(res.body.meta).toMatch(/^https?:\S+\.\S{2,3}/); - expect(res.body.success).toBeTruthy(); - }, - ); + expect(res.status).toBe(200); + expect(res.body.http_status).toEqual(200); + expect(res.body.message).toEqual('Video found'); + expect(res.body.meta).toMatch(/^https?:\S+\.\S{2,3}/); + expect(res.body.success).toBeTruthy(); + }); }); describe('test "/v3/song/search" endpoint', () => { const errorTestCases: ErrorTestCase[] = [ { message: 'Bad Request', - reason: - 'query [q]: required\nquery [q]: must be a string\nquery [q]: must be not empty', + reason: 'query [q]: required\nquery [q]: must be a string\nquery [q]: must be not empty', status: 400, }, { @@ -111,29 +104,22 @@ describe('test "/v3/song" router', () => { }, ); - const successTestCases: SuccessTestCase[] = [ - { params: { q: 'rick' } }, - { params: { q: 123 } }, - ]; + const successTestCases: SuccessTestCase[] = [{ params: { q: 'rick' } }, { params: { q: 123 } }]; - it.each(successTestCases)( - 'should return search results for the search query: $params', - async ({ params }) => { - const res = await agent.get('/v3/song/search').query(params || {}); + it.each(successTestCases)('should return search results for the search query: $params', async ({ params }) => { + const res = await agent.get('/v3/song/search').query(params || {}); - expect(res.status).toBe(200); - expect(res.body).toHaveProperty('objects'); - expect(res.body.objects.length).toBeGreaterThan(0); - }, - ); + expect(res.status).toBe(200); + expect(res.body).toHaveProperty('objects'); + expect(res.body.objects.length).toBeGreaterThan(0); + }); }); describe('test "/v3/song/suggestion" endpoint', () => { const errorTestCases: ErrorTestCase[] = [ { message: 'Bad Request', - reason: - 'query [q]: required\nquery [q]: must be a string\nquery [q]: must be not empty', + reason: 'query [q]: required\nquery [q]: must be a string\nquery [q]: must be not empty', status: 400, }, { @@ -164,21 +150,15 @@ describe('test "/v3/song" router', () => { }, ); - const successTestCases: SuccessTestCase[] = [ - { params: { q: 'rick' } }, - { params: { q: 123 } }, - ]; + const successTestCases: SuccessTestCase[] = [{ params: { q: 'rick' } }, { params: { q: 123 } }]; - it.each(successTestCases)( - 'should return search suggestions for the search query: $params', - async ({ params }) => { - const res = await agent.get('/v3/song/suggestion').query(params || {}); + it.each(successTestCases)('should return search suggestions for the search query: $params', async ({ params }) => { + const res = await agent.get('/v3/song/suggestion').query(params || {}); - expect(res.status).toBe(200); - expect(res.body).toHaveProperty('objects'); - expect(res.body.objects.length).toBeGreaterThan(0); - }, - ); + expect(res.status).toBe(200); + expect(res.body).toHaveProperty('objects'); + expect(res.body.objects.length).toBeGreaterThan(0); + }); }); describe('test "/v3/song/:id" endpoint', () => { @@ -203,19 +183,14 @@ describe('test "/v3/song" router', () => { }, ); - const successTestCases: SuccessTestCase[] = [ - { params: { id: 'dQw4w9WgXcQ' } }, - ]; + const successTestCases: SuccessTestCase[] = [{ params: { id: 'dQw4w9WgXcQ' } }]; - it.each(successTestCases)( - 'should return song info for the param: $params', - async ({ params }) => { - const res = await agent.get(`/v3/song/${params?.id}`); + it.each(successTestCases)('should return song info for the param: $params', async ({ params }) => { + const res = await agent.get(`/v3/song/${params?.id}`); - expect(res.status).toBe(200); - expect(res.get('Connection')).toMatch(/close/); - expect(res.get('Transfer-Encoding')).toMatch(/chunked/); - }, - ); + expect(res.status).toBe(200); + expect(res.get('Connection')).toMatch(/close/); + expect(res.get('Transfer-Encoding')).toMatch(/chunked/); + }); }); });