From affd84272cfb40afe82742078ee21c7e755fbb91 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 10:55:46 +0000 Subject: [PATCH 01/23] Matcher: Musicbrainz: Identify Live Albums --- matcher/matcher/providers/musicbrainz.py | 2 ++ matcher/tests/providers/musicbrainz.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/matcher/matcher/providers/musicbrainz.py b/matcher/matcher/providers/musicbrainz.py index 2d87bdf1..03d6302c 100644 --- a/matcher/matcher/providers/musicbrainz.py +++ b/matcher/matcher/providers/musicbrainz.py @@ -219,6 +219,8 @@ def _get_album_type(self, album: Any) -> AlbumType | None: return AlbumType.STUDIO if "remix" in raw_types: return AlbumType.REMIXES + if "live" in raw_types: + return AlbumType.LIVE if "compilation" in raw_types: return AlbumType.COMPILATION if "dj-mix" in raw_types: diff --git a/matcher/tests/providers/musicbrainz.py b/matcher/tests/providers/musicbrainz.py index 7f7c1f5a..dc0bb5f5 100644 --- a/matcher/tests/providers/musicbrainz.py +++ b/matcher/tests/providers/musicbrainz.py @@ -112,6 +112,10 @@ def test_get_album_release_date_month_only(self): def test_get_album_type(self): scenarios: List[Tuple[str, AlbumType]] = [ + # Madonna - I'm Going to tell you a secret + ("876da970-473b-3a01-9aea-79d1fa6b053a", AlbumType.LIVE), + # Madonna - Finally Enough Love + ("7316f52d-7421-43af-b9e8-02e1cab17153", AlbumType.REMIXES), # Massive Attack - No Protection ("54bd7d44-86e1-3e3c-82e0-10febdedcbda", AlbumType.REMIXES), # Girls Aloud - Mixed Up From d788a5f70435508f743cb1dfa5dc05166bcd61fd Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 11:49:39 +0000 Subject: [PATCH 02/23] Matcher: Artist is optional in Album domain model --- matcher/matcher/matcher/album.py | 5 ++++- matcher/matcher/models/api/domain.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/matcher/matcher/matcher/album.py b/matcher/matcher/matcher/album.py index 2dbd31db..b00d0c5f 100644 --- a/matcher/matcher/matcher/album.py +++ b/matcher/matcher/matcher/album.py @@ -21,7 +21,10 @@ def match_and_post_album(album_id: int, album_name: str): context = Context.get() album = context.client.get_album(album_id) (dto, release_date, album_type, genres) = match_album( - album_id, album_name, album.artist.name, album.type + album_id, + album_name, + album.artist.name if album.artist else None, + album.type, ) # We only care about the new album type if the previous type is Studio album_type = ( diff --git a/matcher/matcher/models/api/domain.py b/matcher/matcher/models/api/domain.py index 8554d5db..e9d7914d 100644 --- a/matcher/matcher/models/api/domain.py +++ b/matcher/matcher/models/api/domain.py @@ -19,7 +19,7 @@ class Artist(DataClassJsonMixin): @dataclass class Album(DataClassJsonMixin): name: str - artist: Artist + artist: Optional[Artist] = None type: AlbumType = AlbumType.OTHER release_date: Optional[str] = None From b349dfaba6de1fba886b88a9039c0f4299ff1873 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 15:26:41 +0000 Subject: [PATCH 03/23] Server: Song Group Controller: Add missing ApiTag Decorator --- server/src/song/song-group.controller.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/song/song-group.controller.ts b/server/src/song/song-group.controller.ts index 0c40b862..d019cf7f 100644 --- a/server/src/song/song-group.controller.ts +++ b/server/src/song/song-group.controller.ts @@ -18,7 +18,7 @@ import { Controller, Get, Query } from "@nestjs/common"; import SongGroupService from "./song-group.service"; -import { ApiOperation, PickType } from "@nestjs/swagger"; +import { ApiOperation, ApiTags, PickType } from "@nestjs/swagger"; import { PaginationParameters } from "src/pagination/models/pagination-parameters"; import RelationIncludeQuery from "src/relation-include/relation-include-query.decorator"; import SongQueryParameters from "./models/song.query-params"; @@ -36,11 +36,12 @@ class SongGroupSelector extends PickType(Selector, [ "type", ]) {} +@ApiTags("Song Groups") @Controller("song-groups") export class SongGroupController { constructor(private songGroupService: SongGroupService) {} @ApiOperation({ - summary: "Get many songs", + description: "Get song groups", }) @Response({ handler: SongGroupResponseBuilder, From 75d7ec997d7c53cc32ff50840c15e27349a725d6 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 15:31:30 +0000 Subject: [PATCH 04/23] Server: Illustration Response: Fix Swagger doc --- server/src/illustration/models/illustration.response.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/illustration/models/illustration.response.ts b/server/src/illustration/models/illustration.response.ts index 9de66d5f..dd8c4932 100644 --- a/server/src/illustration/models/illustration.response.ts +++ b/server/src/illustration/models/illustration.response.ts @@ -41,6 +41,7 @@ export class IllustrationResponse extends Illustration { export class IllustratedResponse { @ApiProperty({ nullable: true, + type: IllustrationResponse, description: "Use 'with' query parameter to include this field", }) illustration?: IllustrationResponse | null; From 020766c4a88e6b859f2036469d839df0e1df7d2f Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 15:35:08 +0000 Subject: [PATCH 05/23] Server: Swagger: Update doc for Identifier params --- server/src/identifier/identifier.pipe.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/identifier/identifier.pipe.ts b/server/src/identifier/identifier.pipe.ts index 6219a653..ba733201 100644 --- a/server/src/identifier/identifier.pipe.ts +++ b/server/src/identifier/identifier.pipe.ts @@ -31,8 +31,8 @@ export function ApiIdentifierRoute() { return ApiParam({ name: "idOrSlug", description: - "Identifier of the resource to fetch. Can be a number or a slug

\ - Examples: 123, 'artist-slug', 'artist-slug+album-slug', 'artist-slug+album-slug+release-slug'", + "Identifier of the resource to fetch. Can be a number or a slug.

\ + Examples: 123, 'artist-slug'", }); } From 595caf9be2763792f795d01fee202d073cd57111 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 15:43:39 +0000 Subject: [PATCH 06/23] Server: change http method to update album --- front/src/api/api.ts | 2 +- matcher/matcher/api.py | 18 +++++++++++++++++- server/src/album/album.controller.spec.ts | 12 ++++++------ server/src/album/album.controller.ts | 4 ++-- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index 9264f275..ffe8efe0 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -553,7 +553,7 @@ export default class API { return API.fetch({ route: `/albums/${albumSlugOrId}`, errorMessage: "Update Album Failed", - method: "POST", + method: "PUT", parameters: {}, emptyResponse: true, data: { type: newType }, diff --git a/matcher/matcher/api.py b/matcher/matcher/api.py index 2f19eb08..3bb14adc 100644 --- a/matcher/matcher/api.py +++ b/matcher/matcher/api.py @@ -53,6 +53,22 @@ def _post( raise Exception(response.content) return response + def _put( + self, route: str, json: dict = {}, file_path: str = "" + ) -> requests.Response: + response = requests.put( + f"{self._url}{route}", + headers={ + "x-api-key": self._key, + }, + files={"file": open(file_path, "rb")} if len(file_path) else None, + json=json if len(json.keys()) else None, + ) + if response.status_code != 200: + logging.error("PUTting API failed: ") + raise Exception(response.content) + return response + def post_external_metadata(self, dto: ExternalMetadataDto): self._post("/external-metadata", json=dto.to_dict()) @@ -101,7 +117,7 @@ def post_album_update( genres=genres, type=type.value if type else None, ) - self._post(f"/albums/{album_id}", json=dto.to_dict()) + self._put(f"/albums/{album_id}", json=dto.to_dict()) def post_song_lyrics(self, song_id: int, lyrics: str): self._post(f"/songs/{song_id}/lyrics", json={"lyrics": lyrics}) diff --git a/server/src/album/album.controller.spec.ts b/server/src/album/album.controller.spec.ts index 94eccdac..ce1c7772 100644 --- a/server/src/album/album.controller.spec.ts +++ b/server/src/album/album.controller.spec.ts @@ -435,11 +435,11 @@ describe("Album Controller", () => { describe("Update the album", () => { it("should reassign the compilation album to an artist + change the type", () => { return request(app.getHttpServer()) - .post(`/albums/${dummyRepository.compilationAlbumA.slug}`) + .put(`/albums/${dummyRepository.compilationAlbumA.slug}`) .send({ type: "RemixAlbum", }) - .expect(201) + .expect(200) .expect((res) => { const artist: Album = res.body; expect(artist).toStrictEqual({ @@ -453,11 +453,11 @@ describe("Album Controller", () => { it("should change release date", () => { return request(app.getHttpServer()) - .post(`/albums/${dummyRepository.compilationAlbumA.id}`) + .put(`/albums/${dummyRepository.compilationAlbumA.id}`) .send({ releaseDate: new Date(2024, 0, 2), }) - .expect(201) + .expect(200) .expect((res) => { const artist: Album = res.body; expect(artist).toStrictEqual({ @@ -471,11 +471,11 @@ describe("Album Controller", () => { }); it("should add genres", async () => { await request(app.getHttpServer()) - .post(`/albums/${dummyRepository.compilationAlbumA.id}`) + .put(`/albums/${dummyRepository.compilationAlbumA.id}`) .send({ genres: ["Genre 1", "Genre 2", "Genre 3", "Genre 2"], }) - .expect(201); + .expect(200); await request(app.getHttpServer()) .get( `/albums/${dummyRepository.compilationAlbumA.id}?with=genres`, diff --git a/server/src/album/album.controller.ts b/server/src/album/album.controller.ts index 7832d4f4..02ced3fe 100644 --- a/server/src/album/album.controller.ts +++ b/server/src/album/album.controller.ts @@ -21,7 +21,7 @@ import { Controller, Get, Inject, - Post, + Put, Query, forwardRef, } from "@nestjs/common"; @@ -173,7 +173,7 @@ export default class AlbumController { }) @Role(Roles.Admin, Roles.Microservice) @Response({ handler: AlbumResponseBuilder }) - @Post(":idOrSlug") + @Put(":idOrSlug") async updateAlbum( @IdentifierParam(AlbumService) where: AlbumQueryParameters.WhereInput, From 859905f77b53b33d78c82cd65366aa807540c322 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 15:58:37 +0000 Subject: [PATCH 07/23] Server: External Metadata: Make GET simpler --- front/src/api/api.ts | 8 +- .../external-metadata.controller.spec.ts | 16 +++- .../external-metadata.controller.ts | 89 +++++++++++-------- .../provider.controller.spec.ts | 2 +- 4 files changed, 71 insertions(+), 44 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index ffe8efe0..5ba390d5 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -1240,7 +1240,7 @@ export default class API { key: ["artist", slugOrId, "external-metadata"], exec: () => API.fetch({ - route: `/external-metadata/artist/${slugOrId}`, + route: `/external-metadata?artist=${slugOrId}`, errorMessage: "Metadata could not be loaded", parameters: {}, validator: ArtistExternalMetadata, @@ -1255,7 +1255,7 @@ export default class API { key: ["song", slugOrId, "external-metadata"], exec: () => API.fetch({ - route: `/external-metadata/song/${slugOrId}`, + route: `/external-metadata?song=${slugOrId}`, errorMessage: "Metadata could not be loaded", parameters: {}, validator: SongExternalMetadata, @@ -1269,7 +1269,7 @@ export default class API { key: ["album", slugOrId, "external-metadata"], exec: () => API.fetch({ - route: `/external-metadata/album/${slugOrId}`, + route: `/external-metadata?album=${slugOrId}`, errorMessage: "Metadata could not be loaded", parameters: {}, validator: AlbumExternalMetadata, @@ -1284,7 +1284,7 @@ export default class API { key: ["release", slugOrId, "external-metadata"], exec: () => API.fetch({ - route: `/external-metadata/release/${slugOrId}`, + route: `/external-metadata?release=${slugOrId}`, errorMessage: "Metadata could not be loaded", parameters: {}, validator: ReleaseExternalMetadata, diff --git a/server/src/external-metadata/external-metadata.controller.spec.ts b/server/src/external-metadata/external-metadata.controller.spec.ts index 5cf392c9..c47de732 100644 --- a/server/src/external-metadata/external-metadata.controller.spec.ts +++ b/server/src/external-metadata/external-metadata.controller.spec.ts @@ -109,7 +109,7 @@ describe("External Metadata Controller", () => { describe("Get Metadata", () => { it("should get album metadata", () => { return request(app.getHttpServer()) - .get(`/external-metadata/album/${dummyRepository.albumA1.id}`) + .get(`/external-metadata?album=${dummyRepository.albumA1.id}`) .expect(200) .expect((res) => { expect(res.body).toStrictEqual(createdMetadata); @@ -117,13 +117,23 @@ describe("External Metadata Controller", () => { }); it("should return an error, as metadata does not exists", () => { return request(app.getHttpServer()) - .get(`/external-metadata/album/${dummyRepository.albumB1.id}`) + .get(`/external-metadata?album=${dummyRepository.albumB1.id}`) .expect(404); }); it("should return an error, as album does not exist", () => { return request(app.getHttpServer()) - .get(`/external-metadata/album/-1`) + .get(`/external-metadata?album=-1`) .expect(404); }); + it("should return an error, as no selector were given", () => { + return request(app.getHttpServer()) + .get(`/external-metadata`) + .expect(400); + }); + it("should return an error, as too many selector were given", () => { + return request(app.getHttpServer()) + .get(`/external-metadata?album=a&artist=b`) + .expect(400); + }); }); }); diff --git a/server/src/external-metadata/external-metadata.controller.ts b/server/src/external-metadata/external-metadata.controller.ts index 21c69f62..20f36b78 100644 --- a/server/src/external-metadata/external-metadata.controller.ts +++ b/server/src/external-metadata/external-metadata.controller.ts @@ -16,12 +16,10 @@ * along with this program. If not, see . */ -import { Body, Controller, Get, Injectable, Post } from "@nestjs/common"; -import { ExternalMetadataResponse } from "./models/external-metadata.response"; -import { ApiTags } from "@nestjs/swagger"; +import { Body, Controller, Get, Injectable, Post, Query } from "@nestjs/common"; +import { ApiOperation, ApiPropertyOptional, ApiTags } from "@nestjs/swagger"; import AlbumQueryParameters from "src/album/models/album.query-parameters"; import AlbumService from "src/album/album.service"; -import IdentifierParam from "src/identifier/identifier.pipe"; import ArtistService from "src/artist/artist.service"; import ArtistQueryParameters from "src/artist/models/artist.query-parameters"; import SongQueryParameters from "src/song/models/song.query-params"; @@ -33,49 +31,68 @@ import ExternalMetadataService from "./external-metadata.service"; import { CreateExternalMetadataDto } from "./models/external-metadata.dto"; import Roles from "src/authentication/roles/roles.enum"; import { Role } from "src/authentication/roles/roles.decorators"; +import { IsOptional } from "class-validator"; +import TransformIdentifier from "src/identifier/identifier.transform"; +import { InvalidRequestException } from "src/exceptions/meelo-exception"; + +class Selector { + @IsOptional() + @ApiPropertyOptional({ + description: "Identifier for album", + }) + @TransformIdentifier(AlbumService) + album?: AlbumQueryParameters.WhereInput; + + @IsOptional() + @ApiPropertyOptional({ + description: "Identifier for artist", + }) + @TransformIdentifier(ArtistService) + artist?: ArtistQueryParameters.WhereInput; + + @IsOptional() + @ApiPropertyOptional({ + description: "Identifier for release", + }) + @TransformIdentifier(ReleaseService) + release?: ReleaseQueryParameters.WhereInput; + + @IsOptional() + @ApiPropertyOptional({ + description: "Identifier for song", + }) + @TransformIdentifier(SongService) + song?: SongQueryParameters.WhereInput; +} @ApiTags("External Metadata") @Controller("external-metadata") @Injectable() export default class ExternalMetadataController { constructor(private externalMetadataService: ExternalMetadataService) {} + + @ApiOperation({ + summary: "Create an new metadata entry for any kind of resource", + }) @Role(Roles.Admin, Roles.Microservice) @Post() async saveMetadata(@Body() creationDto: CreateExternalMetadataDto) { return this.externalMetadataService.saveMetadata(creationDto); } - @Get("album/:idOrSlug") - async getAlbumExternalMetadataEntry( - @IdentifierParam(AlbumService) - where: AlbumQueryParameters.WhereInput, - ) { - return this.getExternalMetadataEntry({ album: where }); - } - @Get("artist/:idOrSlug") - async getArtistExternalMetadataEntry( - @IdentifierParam(ArtistService) - where: ArtistQueryParameters.WhereInput, - ) { - return this.getExternalMetadataEntry({ artist: where }); - } - @Get("song/:idOrSlug") - async getSongExternalMetadataEntry( - @IdentifierParam(SongService) - where: SongQueryParameters.WhereInput, - ) { - return this.getExternalMetadataEntry({ song: where }); - } - @Get("release/:idOrSlug") - async getReleaseExternalMetadataEntry( - @IdentifierParam(ReleaseService) - where: ReleaseQueryParameters.WhereInput, - ) { - return this.getExternalMetadataEntry({ release: where }); - } - private getExternalMetadataEntry( - where: ExternalMetadataQueryParameters.WhereInput, - ): Promise { - return this.externalMetadataService.get(where); + @ApiOperation({ + summary: "Get the metadata entry", + }) + @Get() + async getExternalMetadataEntry(@Query() where: Selector) { + const selectorSize = Object.keys(where).length; + if (selectorSize != 1) { + throw new InvalidRequestException( + `Expected at least one query parameter. Got ${selectorSize}`, + ); + } + return this.externalMetadataService.get( + where as ExternalMetadataQueryParameters.WhereInput, + ); } } diff --git a/server/src/external-metadata/provider.controller.spec.ts b/server/src/external-metadata/provider.controller.spec.ts index ff9e5fa8..81dda0f5 100644 --- a/server/src/external-metadata/provider.controller.spec.ts +++ b/server/src/external-metadata/provider.controller.spec.ts @@ -8,7 +8,7 @@ import PrismaModule from "src/prisma/prisma.module"; import PrismaService from "src/prisma/prisma.service"; import SetupApp from "test/setup-app"; import { Provider } from "src/prisma/models"; -import { createReadStream, existsSync, readFileSync, ReadStream } from "fs"; +import { createReadStream, existsSync } from "fs"; import IllustrationService from "src/illustration/illustration.service"; import ProviderService from "./provider.service"; From 48a8563a8856d27c46bad0da2c3f9b20b7d078ba Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 16:01:33 +0000 Subject: [PATCH 08/23] Server: External Provider: Fix doc of POST endpoint --- server/src/external-metadata/provider.controller.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/src/external-metadata/provider.controller.ts b/server/src/external-metadata/provider.controller.ts index 51acb568..2d061ab0 100644 --- a/server/src/external-metadata/provider.controller.ts +++ b/server/src/external-metadata/provider.controller.ts @@ -19,7 +19,6 @@ import { Body, Controller, Get, Injectable, Post, Query } from "@nestjs/common"; import { ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger"; import ProviderService from "./provider.service"; -import { CreatePlaylistDTO } from "src/playlist/models/playlist.dto"; import { Role } from "src/authentication/roles/roles.decorators"; import Roles from "src/authentication/roles/roles.enum"; import IllustrationRepository from "src/illustration/illustration.repository"; @@ -27,7 +26,10 @@ import IdentifierParam from "src/identifier/identifier.pipe"; import ProviderQueryParameters from "./models/provider.query-parameters"; import { Illustration, Provider } from "src/prisma/models"; import { FormDataRequest, MemoryStoredFile } from "nestjs-form-data"; -import { ProviderIconRegistrationDto } from "./models/provider.dto"; +import { + CreateProviderDTO, + ProviderIconRegistrationDto, +} from "./models/provider.dto"; import Response, { ResponseType } from "src/response/response.decorator"; import { PaginationParameters } from "src/pagination/models/pagination-parameters"; @@ -47,7 +49,7 @@ export default class ProviderController { }) @Role(Roles.Default, Roles.Microservice) @ApiOperation({ - summary: "Save a Provider", + summary: "Get many providers", }) async getProviders( @Query() @@ -59,9 +61,9 @@ export default class ProviderController { @Post() @Role(Roles.Admin, Roles.Microservice) @ApiOperation({ - summary: "Save a Provider", + summary: "Save a provider", }) - async createProvider(@Body() dto: CreatePlaylistDTO): Promise { + async createProvider(@Body() dto: CreateProviderDTO): Promise { return this.providerService.create(dto.name); } From b35fe2fadcf9dd6b5786bbe70539e05cb00691c2 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 16:14:10 +0000 Subject: [PATCH 09/23] Server: Fixes for Files and Illustration Doc for Swagger --- server/src/file/file.controller.ts | 14 +++++++------- server/src/illustration/illustration.controller.ts | 8 +++++++- .../models/illustration-dimensions.dto.ts | 9 +++++++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/server/src/file/file.controller.ts b/server/src/file/file.controller.ts index 3749947d..fb835359 100644 --- a/server/src/file/file.controller.ts +++ b/server/src/file/file.controller.ts @@ -47,7 +47,7 @@ class Selector { @IsOptional() @ApiPropertyOptional({ description: - "Filter files by folder. The folder os relative to the parent library", + "Filter files by folder. The folder is relative to the parent library", }) inFolder?: string; @@ -67,21 +67,21 @@ class Selector { @IsOptional() @ApiPropertyOptional({ - description: `Filter files release`, + description: `Filter files by release`, }) @TransformIdentifier(ReleaseService) release?: ReleaseQueryParameters.WhereInput; @IsOptional() @ApiPropertyOptional({ - description: `Filter files song`, + description: `Filter files by song`, }) @TransformIdentifier(SongService) song?: SongQueryParameters.WhereInput; @IsOptional() @ApiPropertyOptional({ - description: `Filter files track`, + description: `Filter files by track`, }) @TransformIdentifier(TrackService) track?: TrackQueryParameters.WhereInput; @@ -97,7 +97,7 @@ export default class FileController { ) {} @ApiOperation({ - summary: "Get one 'File'", + summary: "Get one file entry", }) @Get(":idOrSlug") @Role(Roles.Default, Roles.Microservice) @@ -111,7 +111,7 @@ export default class FileController { } @ApiOperation({ - summary: "Get multiple File entries", + summary: "Get multiple file entries", }) @Role(Roles.Admin, Roles.Microservice) @Get() @@ -128,7 +128,7 @@ export default class FileController { } @ApiOperation({ - summary: "Delete multiple File entries", + summary: "Delete multiple file entries", }) @Role(Roles.Admin, Roles.Microservice) @Delete() diff --git a/server/src/illustration/illustration.controller.ts b/server/src/illustration/illustration.controller.ts index c478f652..89eba4a8 100644 --- a/server/src/illustration/illustration.controller.ts +++ b/server/src/illustration/illustration.controller.ts @@ -28,7 +28,12 @@ import { Query, Response, } from "@nestjs/common"; -import { ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger"; +import { + ApiConsumes, + ApiOkResponse, + ApiOperation, + ApiTags, +} from "@nestjs/swagger"; import IllustrationService from "./illustration.service"; import { IllustrationDimensionsDto } from "./models/illustration-dimensions.dto"; import { Admin, Role } from "src/authentication/roles/roles.decorators"; @@ -55,6 +60,7 @@ export class IllustrationController { @ApiOperation({ summary: "Get an illustration", }) + @ApiOkResponse({ description: "A JPEG binary" }) @Cached() @Get(":id") async getIllustration( diff --git a/server/src/illustration/models/illustration-dimensions.dto.ts b/server/src/illustration/models/illustration-dimensions.dto.ts index 128a1458..bb862b7b 100644 --- a/server/src/illustration/models/illustration-dimensions.dto.ts +++ b/server/src/illustration/models/illustration-dimensions.dto.ts @@ -29,6 +29,10 @@ export class IllustrationDimensionsDto { "Illustration's width: Expected a strictly positive number", }) @IsOptional() + @ApiProperty({ + description: + "If set, will resize so that the image's width matches. Aspect ratio is preserved.", + }) width?: number; @IsPositive({ @@ -36,12 +40,17 @@ export class IllustrationDimensionsDto { "Illustration's height: Expected a strictly positive number", }) @IsOptional() + @ApiProperty({ + description: + "If set, will resize so that the image's height matches. Aspect ratio is preserved.", + }) height?: number; @IsEnum(ImageQuality) @IsOptional() @ApiProperty({ enum: ImageQuality, + description: "Quality preset", }) quality?: ImageQuality; } From abeb80593e7380d2816eed7fd5198af09d7df581 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 16:14:41 +0000 Subject: [PATCH 10/23] Server: Rename endpoint to POST new library --- front/src/api/api.ts | 2 +- server/src/library/library.controller.spec.ts | 6 +++--- server/src/library/library.controller.ts | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index 5ba390d5..07cc6320 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -429,7 +429,7 @@ export default class API { libraryPath: string, ): Promise { return API.fetch({ - route: "/libraries/new", + route: "/libraries", data: { name: libraryName, path: libraryPath }, errorMessage: "Library Creation Failed", parameters: {}, diff --git a/server/src/library/library.controller.spec.ts b/server/src/library/library.controller.spec.ts index bf7ac26b..d01d2871 100644 --- a/server/src/library/library.controller.spec.ts +++ b/server/src/library/library.controller.spec.ts @@ -68,7 +68,7 @@ describe("Library Controller", () => { describe("Create Library (POST /libraries/new)", () => { it("should create a library", async () => { return request(app.getHttpServer()) - .post("/libraries/new") + .post("/libraries") .send({ path: "Music 3/", name: "My New Library", @@ -85,7 +85,7 @@ describe("Library Controller", () => { }); it("should fail, as the body is incomplete", async () => { return request(app.getHttpServer()) - .post("/libraries/new") + .post("/libraries") .send({ path: "/Path", }) @@ -93,7 +93,7 @@ describe("Library Controller", () => { }); it("should fail, as it already exists", async () => { return request(app.getHttpServer()) - .post("/libraries/new") + .post("/libraries") .send({ path: "/Path", name: "Library", diff --git a/server/src/library/library.controller.ts b/server/src/library/library.controller.ts index 838a5cf9..6ff42701 100644 --- a/server/src/library/library.controller.ts +++ b/server/src/library/library.controller.ts @@ -52,7 +52,7 @@ export default class LibraryController { returns: Library, }) @Admin() - @Post("new") + @Post() async createLibrary(@Body() createLibraryDto: CreateLibraryDto) { return this.libraryService.create(createLibraryDto); } @@ -83,7 +83,7 @@ export default class LibraryController { } @ApiOperation({ - summary: "Get all libraries", + summary: "Get many libraries", }) @Get() @Response({ @@ -105,7 +105,8 @@ export default class LibraryController { } @ApiOperation({ - summary: "Delete a library. Hangs while the library gets deleted.", + summary: "Delete a library", + description: "Hangs while the library gets deleted", }) @SetMetadata("request-timeout", 60000) @Admin() From 2876be50513a73f598c5c5b6acca1054bb32fa7c Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 16:16:55 +0000 Subject: [PATCH 11/23] Server: Registration Controller: Add more doc to endpoints --- server/src/registration/registration.controller.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/registration/registration.controller.ts b/server/src/registration/registration.controller.ts index f5561b90..bf2793e3 100644 --- a/server/src/registration/registration.controller.ts +++ b/server/src/registration/registration.controller.ts @@ -24,11 +24,12 @@ import RoleEnum from "src/authentication/roles/roles.enum"; import { FormDataRequest, MemoryStoredFile } from "nestjs-form-data"; import { RegistrationService } from "./registration.service"; -@ApiTags("Metadata") +@ApiTags("Registration") @Controller("metadata") export class MetadataController { constructor(private registrationService: RegistrationService) {} @ApiOperation({ + summary: "Submit a new file and its metadata", description: "Handles the metadata of a single media file, and creates the related artist, album, etc.", }) @@ -41,6 +42,7 @@ export class MetadataController { } @ApiOperation({ + summary: "Update a file and its metadata", description: "Handles the metadata of a single media file, and updates the related artist, album, etc.", }) From 254b7bfa84da553ef9ced723524cff4b7aa9e55a Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 16:34:28 +0000 Subject: [PATCH 12/23] Server: Album Controller: Allow setting release as master --- front/src/api/api.ts | 10 +++-- .../release-contextual-menu.tsx | 2 +- server/src/album/album.controller.spec.ts | 26 +++++++++++ server/src/album/album.controller.ts | 10 ++++- server/src/album/album.service.ts | 44 +++++++------------ .../album/models/album.query-parameters.ts | 2 + server/src/album/models/update-album.dto.ts | 6 +++ server/src/registration/metadata.service.ts | 5 ++- server/src/release/release.controller.spec.ts | 14 ------ server/src/release/release.controller.ts | 19 -------- server/src/release/release.service.spec.ts | 9 ++-- 11 files changed, 76 insertions(+), 71 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index 07cc6320..1055266f 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -1470,13 +1470,15 @@ export default class API { */ static async setReleaseAsMaster( releaseSlugOrId: string | number, + albumSlugOrId: string | number, ): Promise { return API.fetch({ - route: `/releases/${releaseSlugOrId}/master`, - errorMessage: "Release update failed", - parameters: {}, + route: `/albums/${albumSlugOrId}`, method: "PUT", - validator: yup.mixed(), + parameters: {}, + emptyResponse: true, + data: { masterReleaseId: releaseSlugOrId }, + errorMessage: "Release update failed", }); } diff --git a/front/src/components/contextual-menu/release-contextual-menu.tsx b/front/src/components/contextual-menu/release-contextual-menu.tsx index 3aee7da5..7a2f61c3 100644 --- a/front/src/components/contextual-menu/release-contextual-menu.tsx +++ b/front/src/components/contextual-menu/release-contextual-menu.tsx @@ -46,7 +46,7 @@ const ReleaseContextualMenu = (props: ReleaseContextualMenuProps) => { const confirm = useConfirm(); const { t } = useTranslation(); const masterMutation = useMutation(async () => { - return API.setReleaseAsMaster(props.release.id) + return API.setReleaseAsMaster(props.release.id, props.release.albumId) .then(() => { toast.success(t("releaseSetAsMaster")); queryClient.client.invalidateQueries(); diff --git a/server/src/album/album.controller.spec.ts b/server/src/album/album.controller.spec.ts index ce1c7772..aef2f56e 100644 --- a/server/src/album/album.controller.spec.ts +++ b/server/src/album/album.controller.spec.ts @@ -491,6 +491,32 @@ describe("Album Controller", () => { ]); }); }); + it("should set release as master", async () => { + await request(app.getHttpServer()) + .put(`/albums/${dummyRepository.albumA1.id}`) + .send({ + masterReleaseId: dummyRepository.releaseA1_2.id, + }) + .expect(200) + .expect((res) => { + const releaseId = res.body.masterId; + expect(releaseId).toBe(dummyRepository.releaseA1_2.id); + }); + // teardown + await dummyRepository.album.update({ + where: { id: dummyRepository.albumA1.id }, + data: { masterId: dummyRepository.releaseA1_1.id }, + }); + }); + + it("should no set release as master (unrelated release)", () => { + return request(app.getHttpServer()) + .put(`/albums/${dummyRepository.albumA1.id}`) + .send({ + masterReleaseId: dummyRepository.releaseB1_1.id, + }) + .expect(400); + }); }); describe("Album Illustration", () => { diff --git a/server/src/album/album.controller.ts b/server/src/album/album.controller.ts index 02ced3fe..aa6aa6fd 100644 --- a/server/src/album/album.controller.ts +++ b/server/src/album/album.controller.ts @@ -177,10 +177,16 @@ export default class AlbumController { async updateAlbum( @IdentifierParam(AlbumService) where: AlbumQueryParameters.WhereInput, - @Body() updateDTO: UpdateAlbumDTO, + @Body() { masterReleaseId, ...updateAlbumDTO }: UpdateAlbumDTO, ) { const album = await this.albumService.get(where); - return this.albumService.update(updateDTO, { id: album.id }); + return this.albumService.update( + { + master: masterReleaseId ? { id: masterReleaseId } : undefined, + ...updateAlbumDTO, + }, + { id: album.id }, + ); } } diff --git a/server/src/album/album.service.ts b/server/src/album/album.service.ts index 2be155dc..fce42ce1 100644 --- a/server/src/album/album.service.ts +++ b/server/src/album/album.service.ts @@ -33,7 +33,6 @@ import { buildStringSearchParameters } from "src/utils/search-string-input"; import SongService from "src/song/song.service"; import compilationAlbumArtistKeyword from "src/constants/compilation"; import Logger from "src/logger/logger"; -import ReleaseQueryParameters from "src/release/models/release.query-parameters"; import { PrismaError } from "prisma-error-enum"; import ParserService from "src/parser/parser.service"; import deepmerge from "deepmerge"; @@ -53,6 +52,7 @@ import { ResourceEventPriority, } from "src/events/events.service"; import { shuffle } from "src/utils/shuffle"; +import { InvalidRequestException } from "src/exceptions/meelo-exception"; @Injectable() export default class AlbumService extends SearchableRepositoryService { @@ -429,10 +429,26 @@ export default class AlbumService extends SearchableRepositoryService { what: AlbumQueryParameters.UpdateInput, where: AlbumQueryParameters.WhereInput, ) { + if (what.master) { + const newMaster = await this.releaseService.get(what.master); + const album = await this.get(where); + if (newMaster.albumId !== album.id) { + throw new InvalidRequestException( + "Master release of album should be release of said album", + ); + } + } return this.prismaService.album .update({ data: { ...what, + master: what.master + ? { + connect: ReleaseService.formatWhereInput( + what.master, + ), + } + : undefined, releaseDate: what.releaseDate ?? undefined, genres: what.genres ? { @@ -560,32 +576,6 @@ export default class AlbumService extends SearchableRepositoryService { ); } - /** - * Set the release as album's master - * @param releaseWhere the query parameters of the release - * @returns the updated album - */ - async setMasterRelease(releaseWhere: ReleaseQueryParameters.WhereInput) { - const release = await this.releaseService.get(releaseWhere); - - return this.prismaService.album.update({ - where: { id: release.albumId }, - data: { masterId: release.id }, - }); - } - - /** - * unset album's master release - * @param albumWhere the query parameters of the album - * @returns the updated album - */ - async unsetMasterRelease(albumWhere: AlbumQueryParameters.WhereInput) { - return this.prismaService.album.update({ - where: AlbumService.formatWhereInput(albumWhere), - data: { masterId: null }, - }); - } - async onNotFound(error: Error, where: AlbumQueryParameters.WhereInput) { if ( error instanceof Prisma.PrismaClientKnownRequestError && diff --git a/server/src/album/models/album.query-parameters.ts b/server/src/album/models/album.query-parameters.ts index 694930c4..7403dc7e 100644 --- a/server/src/album/models/album.query-parameters.ts +++ b/server/src/album/models/album.query-parameters.ts @@ -23,6 +23,7 @@ import type { RequireAtLeastOne, RequireExactlyOne } from "type-fest"; import type { SearchDateInput } from "src/utils/search-date-input"; import type { SearchStringInput } from "src/utils/search-string-input"; import type { RelationInclude as BaseRelationInclude } from "src/relation-include/models/relation-include"; +import type ReleaseQueryParameters from "src/release/models/release.query-parameters"; import type GenreQueryParameters from "src/genre/models/genre.query-parameters"; import { Album } from "src/prisma/models"; import { @@ -81,6 +82,7 @@ namespace AlbumQueryParameters { export class UpdateInput extends PartialType( PickType(Album, ["type", "releaseDate"] as const), ) { + master?: ReleaseQueryParameters.WhereInput; genres?: string[]; } diff --git a/server/src/album/models/update-album.dto.ts b/server/src/album/models/update-album.dto.ts index 2d17d124..8fe645ef 100644 --- a/server/src/album/models/update-album.dto.ts +++ b/server/src/album/models/update-album.dto.ts @@ -41,4 +41,10 @@ export default class UpdateAlbumDTO { }) @IsOptional() releaseDate?: Date; + + @ApiProperty({ + description: "ID of the release to set as master", + }) + @IsOptional() + masterReleaseId?: number; } diff --git a/server/src/registration/metadata.service.ts b/server/src/registration/metadata.service.ts index c6340df7..76f03434 100644 --- a/server/src/registration/metadata.service.ts +++ b/server/src/registration/metadata.service.ts @@ -230,7 +230,10 @@ export default class MetadataService { ); } if (album.masterId === null) { - this.albumService.setMasterRelease({ id: release.id }); + this.albumService.update( + { master: { id: release.id } }, + { id: album.id }, + ); } if ( !release.releaseDate || diff --git a/server/src/release/release.controller.spec.ts b/server/src/release/release.controller.spec.ts index b2087ae6..f9150ad5 100644 --- a/server/src/release/release.controller.spec.ts +++ b/server/src/release/release.controller.spec.ts @@ -486,20 +486,6 @@ describe("Release Controller", () => { }); }); - describe("Set Release as master (POST /releases/:id/master)", () => { - it("should set release as master", () => { - return request(app.getHttpServer()) - .put(`/releases/${dummyRepository.releaseA1_2.id}/master`) - .expect(200) - .expect((res) => { - const release: Release = res.body; - expect(release).toStrictEqual({ - ...expectedReleaseResponse(dummyRepository.releaseA1_2), - }); - }); - }); - }); - describe("Release Illustration", () => { it("Should return the illustration", async () => { const { illustration } = diff --git a/server/src/release/release.controller.ts b/server/src/release/release.controller.ts index 85cec855..aa0f85fc 100644 --- a/server/src/release/release.controller.ts +++ b/server/src/release/release.controller.ts @@ -37,7 +37,6 @@ import type { Response as ExpressResponse } from "express"; import { ApiOperation, ApiPropertyOptional, ApiTags } from "@nestjs/swagger"; import { TrackResponseBuilder } from "src/track/models/track.response"; import RelationIncludeQuery from "src/relation-include/relation-include-query.decorator"; -import { Admin } from "src/authentication/roles/roles.decorators"; import IdentifierParam from "src/identifier/identifier.pipe"; import Response, { ResponseType } from "src/response/response.decorator"; import { ReleaseResponseBuilder } from "./models/release.response"; @@ -71,8 +70,6 @@ export default class ReleaseController { private releaseService: ReleaseService, @Inject(forwardRef(() => TrackService)) private trackService: TrackService, - @Inject(forwardRef(() => AlbumService)) - private albumService: AlbumService, ) {} @ApiOperation({ @@ -165,20 +162,4 @@ export default class ReleaseController { ) { return this.releaseService.pipeArchive(where, response); } - - @ApiOperation({ - summary: "Set a release as master release", - }) - @Admin() - @Response({ handler: ReleaseResponseBuilder }) - @Put(":idOrSlug/master") - async setAsMaster( - @IdentifierParam(ReleaseService) - where: ReleaseQueryParameters.WhereInput, - ) { - const release = await this.releaseService.get(where); - - await this.albumService.setMasterRelease(where); - return release; - } } diff --git a/server/src/release/release.service.spec.ts b/server/src/release/release.service.spec.ts index c91b45d7..3d585c1c 100644 --- a/server/src/release/release.service.spec.ts +++ b/server/src/release/release.service.spec.ts @@ -303,9 +303,12 @@ describe("Release Service", () => { }); it("should delete the master release", async () => { await trackService.delete({ id: dummyRepository.trackB1_1.id }); - await albumService.setMasterRelease({ - id: dummyRepository.releaseB1_1.id, - }); + await albumService.update( + { + master: { id: dummyRepository.releaseB1_1.id }, + }, + { id: dummyRepository.albumB1.id }, + ); await releaseService.delete({ id: dummyRepository.releaseB1_1.id }); const testRelease = async () => await releaseService.get({ From 72b8301dc1f19fac3ac415706f4504494c49b1ec Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 16:52:16 +0000 Subject: [PATCH 13/23] Server: Document Response type of search-related controllers --- .../src/search/search-history.controller.ts | 43 ++++++++++++++++--- server/src/search/search.controller.ts | 42 +++++++++++++++--- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/server/src/search/search-history.controller.ts b/server/src/search/search-history.controller.ts index af46831c..a9c09d68 100644 --- a/server/src/search/search-history.controller.ts +++ b/server/src/search/search-history.controller.ts @@ -17,7 +17,12 @@ */ import { Body, Controller, Get, Post, Query, Req } from "@nestjs/common"; -import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { + ApiOkResponse, + ApiOperation, + ApiTags, + getSchemaPath, +} from "@nestjs/swagger"; import { CreateSearchHistoryEntry } from "./models/create-search-history-entry.dto"; import { SearchHistoryService } from "./search-history.service"; import Roles from "src/authentication/roles/roles.enum"; @@ -30,10 +35,22 @@ import { Video, } from "src/prisma/models"; import { PaginationParameters } from "src/pagination/models/pagination-parameters"; -import { ArtistResponseBuilder } from "src/artist/models/artist.response"; -import { AlbumResponseBuilder } from "src/album/models/album.response"; -import { SongResponseBuilder } from "src/song/models/song.response"; -import { VideoResponseBuilder } from "src/video/models/video.response"; +import { + ArtistResponse, + ArtistResponseBuilder, +} from "src/artist/models/artist.response"; +import { + AlbumResponse, + AlbumResponseBuilder, +} from "src/album/models/album.response"; +import { + SongResponse, + SongResponseBuilder, +} from "src/song/models/song.response"; +import { + VideoResponse, + VideoResponseBuilder, +} from "src/video/models/video.response"; @ApiTags("Search") @Controller("search/history") @@ -47,7 +64,8 @@ export class SearchHistoryController { ) {} @ApiOperation({ - summary: "Save a searched item", + summary: "Save an entry in the search history", + description: "There should be exactly one ID in the DTO.", }) @Role(Roles.User) @Post() @@ -66,6 +84,19 @@ export class SearchHistoryController { }) @Role(Roles.User) @Get() + @ApiOkResponse({ + schema: { + type: "array", + items: { + oneOf: [ + ArtistResponse, + AlbumResponse, + SongResponse, + VideoResponse, + ].map((resType) => ({ $ref: getSchemaPath(resType) })), + }, + }, + }) async getSearchHistory( @Query() pagination: PaginationParameters, @Req() request: Express.Request, diff --git a/server/src/search/search.controller.ts b/server/src/search/search.controller.ts index 21d92115..46c30101 100644 --- a/server/src/search/search.controller.ts +++ b/server/src/search/search.controller.ts @@ -17,14 +17,31 @@ */ import { Controller, Get, Query } from "@nestjs/common"; -import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { + ApiOkResponse, + ApiOperation, + ApiTags, + getSchemaPath, +} from "@nestjs/swagger"; import { SearchService } from "./search.service"; -import { ArtistResponseBuilder } from "src/artist/models/artist.response"; -import { SongResponseBuilder } from "src/song/models/song.response"; -import { AlbumResponseBuilder } from "src/album/models/album.response"; +import { + ArtistResponse, + ArtistResponseBuilder, +} from "src/artist/models/artist.response"; +import { + SongResponse, + SongResponseBuilder, +} from "src/song/models/song.response"; +import { + AlbumResponse, + AlbumResponseBuilder, +} from "src/album/models/album.response"; import { AlbumWithRelations, Artist, Song } from "src/prisma/models"; import { InvalidRequestException } from "src/exceptions/meelo-exception"; -import { VideoResponseBuilder } from "src/video/models/video.response"; +import { + VideoResponse, + VideoResponseBuilder, +} from "src/video/models/video.response"; import { Video } from "@prisma/client"; @ApiTags("Search") @@ -44,10 +61,23 @@ export class SearchController { No pagination paramters. \ Artists come with their respective illustration. \ Songs come with their artist, featuring artist, illustration and master track. \ - Videos come with their artist, illustration and master track. \ + Videos come with their artist, illustration and master track. \ Albums come with their artist and illustration.", }) @Get() + @ApiOkResponse({ + schema: { + type: "array", + items: { + oneOf: [ + ArtistResponse, + AlbumResponse, + SongResponse, + VideoResponse, + ].map((resType) => ({ $ref: getSchemaPath(resType) })), + }, + }, + }) async search(@Query("query") query?: string) { if (!query) { throw new InvalidRequestException( From 18fdca3f2772a99e732c92ff4bc9a8bdd5c1e079 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 16:54:11 +0000 Subject: [PATCH 14/23] Server: Song Group Controller: fix swagger doc --- server/src/song/song-group.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/song/song-group.controller.ts b/server/src/song/song-group.controller.ts index d019cf7f..a739dddb 100644 --- a/server/src/song/song-group.controller.ts +++ b/server/src/song/song-group.controller.ts @@ -41,7 +41,7 @@ class SongGroupSelector extends PickType(Selector, [ export class SongGroupController { constructor(private songGroupService: SongGroupService) {} @ApiOperation({ - description: "Get song groups", + summary: "Get song groups", }) @Response({ handler: SongGroupResponseBuilder, From 4399fa943a835c285a840c762587f0564e4f56a8 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 7 Jan 2025 16:54:30 +0000 Subject: [PATCH 15/23] Server: Settings Controller: Swagger: Hide properties that are not returned --- server/src/settings/models/settings.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/settings/models/settings.ts b/server/src/settings/models/settings.ts index 0ce9a6b1..b14983ab 100644 --- a/server/src/settings/models/settings.ts +++ b/server/src/settings/models/settings.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; import { Exclude } from "class-transformer"; import { IsBoolean, IsString } from "class-validator"; @@ -36,7 +36,7 @@ export default class Settings { /** * The folder where `settings.json` and metadata are stored */ - @ApiProperty() + @ApiHideProperty() @IsString() @Exclude({ toPlainOnly: true }) meeloFolder: string; @@ -44,7 +44,7 @@ export default class Settings { /** * The base folder where every libraries must be located */ - @ApiProperty() + @ApiHideProperty() @IsString() @Exclude({ toPlainOnly: true }) dataFolder: string; From 844232cb88bfd8dcc98bff02457c79834715cea5 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 13:29:44 +0000 Subject: [PATCH 16/23] Server: Playlist Controller: Remove 'new' prefix on POST endpoint --- front/src/api/api.ts | 4 +-- .../playlist/models/playlist-entry.model.ts | 7 +++-- .../src/playlist/models/playlist.response.ts | 4 +-- .../src/playlist/playlist.controller.spec.ts | 10 +++---- server/src/playlist/playlist.controller.ts | 26 ++++++++++--------- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index 1055266f..b7255fb0 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -256,7 +256,7 @@ export default class API { static async createPlaylist(playlistName: string): Promise { return API.fetch({ - route: "/playlists/new", + route: "/playlists", data: { name: playlistName }, errorMessage: "Playlist Creation Failed", parameters: {}, @@ -1406,7 +1406,7 @@ export default class API { playlistId: number, ): Promise { return API.fetch({ - route: `/playlists/entries/new`, + route: `/playlists/entries`, errorMessage: "Failed to add song to playlist", parameters: {}, data: { songId, playlistId }, diff --git a/server/src/playlist/models/playlist-entry.model.ts b/server/src/playlist/models/playlist-entry.model.ts index 3c344b7d..ad9b0c84 100644 --- a/server/src/playlist/models/playlist-entry.model.ts +++ b/server/src/playlist/models/playlist-entry.model.ts @@ -16,9 +16,12 @@ * along with this program. If not, see . */ +import { ApiProperty } from "@nestjs/swagger"; import { SongWithRelations } from "src/prisma/models"; -export type PlaylistEntryModel = SongWithRelations & { +export class PlaylistEntryModel extends SongWithRelations { + @ApiProperty() entryId: number; + @ApiProperty() index: number; -}; +} diff --git a/server/src/playlist/models/playlist.response.ts b/server/src/playlist/models/playlist.response.ts index d9e16add..0879785b 100644 --- a/server/src/playlist/models/playlist.response.ts +++ b/server/src/playlist/models/playlist.response.ts @@ -36,7 +36,7 @@ export class PlaylistEntryResponse extends SongResponse { }) entryId: number; @ApiProperty({ - description: "Index of the song", + description: "Index of the song in the playlist", }) index: number; } @@ -53,7 +53,7 @@ export class PlaylistEntryResponseBuilder extends ResponseBuilderInterceptor< super(); } - returnType = PlaylistResponse; + returnType = PlaylistEntryResponse; async buildResponse( entry: PlaylistEntryModel, diff --git a/server/src/playlist/playlist.controller.spec.ts b/server/src/playlist/playlist.controller.spec.ts index 458517c6..78c5fe49 100644 --- a/server/src/playlist/playlist.controller.spec.ts +++ b/server/src/playlist/playlist.controller.spec.ts @@ -143,7 +143,7 @@ describe("Playlist Controller", () => { describe("Create Playlist", () => { it("Should Create Playlist", async () => { return request(app.getHttpServer()) - .post(`/playlists/new`) + .post(`/playlists`) .send({ name: "New Playlist", }) @@ -161,7 +161,7 @@ describe("Playlist Controller", () => { }); it("Should Error: Playlist Already Exists", async () => { return request(app.getHttpServer()) - .post(`/playlists/new`) + .post(`/playlists`) .send({ name: dummyRepository.playlist1.name, }) @@ -211,7 +211,7 @@ describe("Playlist Controller", () => { describe("Add Song To Playlist", () => { it("Should Add Song to Playlist Entry", async () => { await request(app.getHttpServer()) - .post(`/playlists/entries/new`) + .post(`/playlists/entries`) .send({ playlistId: dummyRepository.playlist1.id, songId: dummyRepository.songB1.id, @@ -232,7 +232,7 @@ describe("Playlist Controller", () => { it("Should Error: Song not found", async () => { await request(app.getHttpServer()) - .post(`/playlists/entries/new`) + .post(`/playlists/entries`) .send({ playlistId: dummyRepository.playlist1.id, songId: -1, @@ -242,7 +242,7 @@ describe("Playlist Controller", () => { it("Should Error: Playlist not Found", async () => { await request(app.getHttpServer()) - .post(`/playlists/entries/new`) + .post(`/playlists/entries`) .send({ playlistId: -1, songId: dummyRepository.songB1.id, diff --git a/server/src/playlist/playlist.controller.ts b/server/src/playlist/playlist.controller.ts index 3b85359b..44d7ba91 100644 --- a/server/src/playlist/playlist.controller.ts +++ b/server/src/playlist/playlist.controller.ts @@ -53,7 +53,7 @@ import SongQueryParameters from "src/song/models/song.query-params"; export class Selector { @IsOptional() @ApiPropertyOptional({ - description: "Filter playlist by albums that entries belong to", + description: "Get playlists that have a song in common with an album", }) @TransformIdentifier(AlbumService) album?: AlbumQueryParameters.WhereInput; @@ -65,7 +65,7 @@ export default class PlaylistController { constructor(private playlistService: PlaylistService) {} @ApiOperation({ - summary: "Get one Playlist", + summary: "Get one playlist", }) @Get(":idOrSlug") @Response({ handler: PlaylistResponseBuilder }) @@ -79,7 +79,7 @@ export default class PlaylistController { } @ApiOperation({ - summary: "Get Playlists", + summary: "Get many playlists", }) @Get() @Response({ handler: PlaylistResponseBuilder, type: ResponseType.Page }) @@ -100,7 +100,8 @@ export default class PlaylistController { } @ApiOperation({ - summary: "Get Playlist's entries", + summary: "Get a playlist's entries", + description: "Entries as song with an 'entryId' field", }) @Get(":idOrSlug/entries") @Response({ @@ -123,9 +124,9 @@ export default class PlaylistController { } @ApiOperation({ - summary: "Create Playlist", + summary: "Create playlist", }) - @Post("new") + @Post() @Response({ handler: PlaylistResponseBuilder }) async createPlaylist( @Body() @@ -135,7 +136,7 @@ export default class PlaylistController { } @ApiOperation({ - summary: "Update Playlist", + summary: "Update playlist", }) @Put(":idOrSlug") @Response({ handler: PlaylistResponseBuilder }) @@ -149,7 +150,7 @@ export default class PlaylistController { } @ApiOperation({ - summary: "Get one Playlist", + summary: "Delete playlist", }) @Delete(":idOrSlug") async delete( @@ -160,9 +161,9 @@ export default class PlaylistController { } @ApiOperation({ - summary: "Add Song to Playlist", + summary: "Add song to playlist", }) - @Post("entries/new") + @Post("entries") async addSongToPlaylist( @Body() playlistEntryDTO: CreatePlaylistEntryDTO, @@ -174,7 +175,7 @@ export default class PlaylistController { } @ApiOperation({ - summary: "Reorder Entries in Playlist", + summary: "Reorder entries in playlist", }) @Put(":idOrSlug/reorder") async moveEntryInPlaylist( @@ -187,7 +188,8 @@ export default class PlaylistController { } @ApiOperation({ - summary: "Delete Entry in Playlist", + summary: "Delete playlist entry", + description: "This will delete a song from the playlist", }) @Delete("entries/:id") async deleteEntryInPlaylist( From 585897114dd0011a88a5294e67844d680a042ed3 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 13:41:58 +0000 Subject: [PATCH 17/23] Server: Playlist Controller: Make entries-related endpoint consistent --- front/src/api/api.ts | 6 ++-- server/src/playlist/models/playlist.dto.ts | 6 ---- .../src/playlist/playlist.controller.spec.ts | 29 ++++++++++++------- server/src/playlist/playlist.controller.ts | 8 +++-- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index b7255fb0..d3aa4b3f 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -283,7 +283,7 @@ export default class API { entriesIds: number[], ): Promise { return API.fetch({ - route: `/playlists/${playlistSlugOrId}/reorder`, + route: `/playlists/${playlistSlugOrId}/entries/reorder`, data: { entryIds: entriesIds }, parameters: {}, method: "PUT", @@ -1406,10 +1406,10 @@ export default class API { playlistId: number, ): Promise { return API.fetch({ - route: `/playlists/entries`, + route: `/playlists/${playlistId}/entries`, errorMessage: "Failed to add song to playlist", parameters: {}, - data: { songId, playlistId }, + data: { songId }, method: "POST", emptyResponse: true, }); diff --git a/server/src/playlist/models/playlist.dto.ts b/server/src/playlist/models/playlist.dto.ts index e14b0529..763f1b31 100644 --- a/server/src/playlist/models/playlist.dto.ts +++ b/server/src/playlist/models/playlist.dto.ts @@ -25,12 +25,6 @@ export class CreatePlaylistDTO extends PickType(CreatePlaylist, ["name"]) {} export class UpdatePlaylistDTO extends PickType(CreatePlaylist, ["name"]) {} export class CreatePlaylistEntryDTO { - @ApiProperty({ - description: "The ID of the playlist to add the song to", - }) - @IsNumber() - playlistId: number; - @ApiProperty({ description: "The ID of the song", }) diff --git a/server/src/playlist/playlist.controller.spec.ts b/server/src/playlist/playlist.controller.spec.ts index 78c5fe49..a174110d 100644 --- a/server/src/playlist/playlist.controller.spec.ts +++ b/server/src/playlist/playlist.controller.spec.ts @@ -211,9 +211,8 @@ describe("Playlist Controller", () => { describe("Add Song To Playlist", () => { it("Should Add Song to Playlist Entry", async () => { await request(app.getHttpServer()) - .post(`/playlists/entries`) + .post(`/playlists/${dummyRepository.playlist1.id}/entries`) .send({ - playlistId: dummyRepository.playlist1.id, songId: dummyRepository.songB1.id, }) .expect(201); @@ -232,9 +231,8 @@ describe("Playlist Controller", () => { it("Should Error: Song not found", async () => { await request(app.getHttpServer()) - .post(`/playlists/entries`) + .post(`/playlists/${dummyRepository.playlist1.id}/entries`) .send({ - playlistId: dummyRepository.playlist1.id, songId: -1, }) .expect(404); @@ -242,9 +240,8 @@ describe("Playlist Controller", () => { it("Should Error: Playlist not Found", async () => { await request(app.getHttpServer()) - .post(`/playlists/entries`) + .post(`/playlists/${-1}/entries`) .send({ - playlistId: -1, songId: dummyRepository.songB1.id, }) .expect(404); @@ -254,7 +251,9 @@ describe("Playlist Controller", () => { describe("Reorder Entry", () => { it("Should Error: Negative Number", async () => { await request(app.getHttpServer()) - .put(`/playlists/${dummyRepository.playlist1.id}/reorder`) + .put( + `/playlists/${dummyRepository.playlist1.id}/entries/reorder`, + ) .send({ entryIds: [ dummyRepository.playlistEntry1.id, @@ -267,7 +266,9 @@ describe("Playlist Controller", () => { it("Should Error: Incomplete List", async () => { await request(app.getHttpServer()) - .put(`/playlists/${dummyRepository.playlist1.id}/reorder`) + .put( + `/playlists/${dummyRepository.playlist1.id}/entries/reorder`, + ) .send({ entryIds: [ dummyRepository.playlistEntry1.id, @@ -279,7 +280,9 @@ describe("Playlist Controller", () => { it("Should Error: Unknown Index", async () => { await request(app.getHttpServer()) - .put(`/playlists/${dummyRepository.playlist1.id}/reorder`) + .put( + `/playlists/${dummyRepository.playlist1.id}/entries/reorder`, + ) .send({ entryIds: [ dummyRepository.playlistEntry1.id, @@ -292,7 +295,9 @@ describe("Playlist Controller", () => { it("Should Error: Duplicate Index", async () => { await request(app.getHttpServer()) - .put(`/playlists/${dummyRepository.playlist1.id}/reorder`) + .put( + `/playlists/${dummyRepository.playlist1.id}/entries/reorder`, + ) .send({ entryIds: [ dummyRepository.playlistEntry1.id, @@ -304,7 +309,9 @@ describe("Playlist Controller", () => { }); it("Should Move Entries", async () => { await request(app.getHttpServer()) - .put(`/playlists/${dummyRepository.playlist1.id}/reorder`) + .put( + `/playlists/${dummyRepository.playlist1.id}/entries/reorder`, + ) .send({ entryIds: [ dummyRepository.playlistEntry1.id, diff --git a/server/src/playlist/playlist.controller.ts b/server/src/playlist/playlist.controller.ts index 44d7ba91..bd0e968e 100644 --- a/server/src/playlist/playlist.controller.ts +++ b/server/src/playlist/playlist.controller.ts @@ -163,21 +163,23 @@ export default class PlaylistController { @ApiOperation({ summary: "Add song to playlist", }) - @Post("entries") + @Post(":idOrSlug/entries") async addSongToPlaylist( + @IdentifierParam(PlaylistService) + where: PlaylistQueryParameters.WhereInput, @Body() playlistEntryDTO: CreatePlaylistEntryDTO, ) { await this.playlistService.addSong( { id: playlistEntryDTO.songId }, - { id: playlistEntryDTO.playlistId }, + where, ); } @ApiOperation({ summary: "Reorder entries in playlist", }) - @Put(":idOrSlug/reorder") + @Put(":idOrSlug/entries/reorder") async moveEntryInPlaylist( @Body() { entryIds }: ReorderPlaylistDTO, From 72116149c08ef742f824e696b05cfa7a8e2905b7 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 13:55:04 +0000 Subject: [PATCH 18/23] Server: Songs controller: PUT instead of POST to update songs --- front/src/api/api.ts | 4 ++-- matcher/matcher/api.py | 2 +- server/src/release/release.controller.ts | 10 ++++++++-- server/src/song/song.controller.spec.ts | 12 ++++++------ server/src/song/song.controller.ts | 10 +++++----- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index d3aa4b3f..40085186 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -570,7 +570,7 @@ export default class API { return API.fetch({ route: `/songs/${songSlugOrId}`, errorMessage: "Update Song Failed", - method: "POST", + method: "PUT", parameters: {}, emptyResponse: true, data: { type: newType }, @@ -1494,7 +1494,7 @@ export default class API { return API.fetch({ route: `/songs/${songSlugOrId}`, errorMessage: "Update Song Failed", - method: "POST", + method: "PUT", parameters: {}, emptyResponse: true, data: { masterTrackId: trackSlugOrId }, diff --git a/matcher/matcher/api.py b/matcher/matcher/api.py index 3bb14adc..6e4a7f35 100644 --- a/matcher/matcher/api.py +++ b/matcher/matcher/api.py @@ -123,7 +123,7 @@ def post_song_lyrics(self, song_id: int, lyrics: str): self._post(f"/songs/{song_id}/lyrics", json={"lyrics": lyrics}) def post_song_genres(self, song_id: int, genres: List[str]): - self._post(f"/songs/{song_id}", json={"genres": genres}) + self._put(f"/songs/{song_id}", json={"genres": genres}) @staticmethod def _to_page(obj: Any, t: type[T]) -> Page[T]: diff --git a/server/src/release/release.controller.ts b/server/src/release/release.controller.ts index aa0f85fc..fcef87f1 100644 --- a/server/src/release/release.controller.ts +++ b/server/src/release/release.controller.ts @@ -22,7 +22,6 @@ import { Get, Inject, ParseBoolPipe, - Put, Query, Res, forwardRef, @@ -34,7 +33,12 @@ import TrackService from "src/track/track.service"; import AlbumService from "src/album/album.service"; import AlbumQueryParameters from "src/album/models/album.query-parameters"; import type { Response as ExpressResponse } from "express"; -import { ApiOperation, ApiPropertyOptional, ApiTags } from "@nestjs/swagger"; +import { + ApiOkResponse, + ApiOperation, + ApiPropertyOptional, + ApiTags, +} from "@nestjs/swagger"; import { TrackResponseBuilder } from "src/track/models/track.response"; import RelationIncludeQuery from "src/relation-include/relation-include-query.decorator"; import IdentifierParam from "src/identifier/identifier.pipe"; @@ -126,6 +130,7 @@ export default class ReleaseController { @ApiOperation({ summary: "Get the ordered tracklist of a release", + description: "The returned entries are tracks", }) @Response({ handler: TrackResponseBuilder, type: ResponseType.Page }) @Get(":idOrSlug/tracklist") @@ -154,6 +159,7 @@ export default class ReleaseController { @ApiOperation({ summary: "Download an archive of the release", }) + @ApiOkResponse({ description: "A ZIP Binary" }) @Get(":idOrSlug/archive") async getReleaseArcive( @IdentifierParam(ReleaseService) diff --git a/server/src/song/song.controller.spec.ts b/server/src/song/song.controller.spec.ts index b73ad2d3..40888f2f 100644 --- a/server/src/song/song.controller.spec.ts +++ b/server/src/song/song.controller.spec.ts @@ -533,11 +533,11 @@ describe("Song Controller", () => { describe("Update Song", () => { it("Should update Song's Type", () => { return request(app.getHttpServer()) - .post(`/songs/${dummyRepository.songB1.id}`) + .put(`/songs/${dummyRepository.songB1.id}`) .send({ type: SongType.Remix, }) - .expect(201) + .expect(200) .expect((res) => { const song: Song = res.body; expect(song).toStrictEqual({ @@ -549,11 +549,11 @@ describe("Song Controller", () => { it("Should update Song's Type", () => { return request(app.getHttpServer()) - .post(`/songs/${dummyRepository.songB1.id}`) + .put(`/songs/${dummyRepository.songB1.id}`) .send({ genres: ["a", "B", "c"], }) - .expect(201) + .expect(200) .expect(async () => { const songGenres = await dummyRepository.genre .findMany({ @@ -574,7 +574,7 @@ describe("Song Controller", () => { it("should set track as master", () => { return request(app.getHttpServer()) - .post(`/songs/${dummyRepository.songA1.id}`) + .put(`/songs/${dummyRepository.songA1.id}`) .send({ masterTrackId: dummyRepository.trackA1_2Video.id, }) @@ -588,7 +588,7 @@ describe("Song Controller", () => { it("should fail as track does not belong to song", () => { return request(app.getHttpServer()) - .post(`/songs/${dummyRepository.songA1.id}`) + .put(`/songs/${dummyRepository.songA1.id}`) .send({ masterTrackId: dummyRepository.trackC1_1.id, }) diff --git a/server/src/song/song.controller.ts b/server/src/song/song.controller.ts index 2f522b9e..951b5cf0 100644 --- a/server/src/song/song.controller.ts +++ b/server/src/song/song.controller.ts @@ -91,14 +91,14 @@ export class Selector { @IsOptional() @ApiPropertyOptional({ - description: "Get related songs ", + description: "Get other versions of the song", }) @TransformIdentifier(SongService) versionsOf?: SongQueryParameters.WhereInput; @IsOptional() @ApiPropertyOptional({ - description: "Get related songs ", + description: "Filter songs by group", }) @TransformIdentifier({ formatIdentifierToWhereInput: (identifier) => @@ -126,7 +126,7 @@ export class Selector { @IsOptional() @ApiPropertyOptional({ description: - "Filter songs that are B-Sides of a release.\nThe release must be a studio recording, otherwise returns an emtpy list", + "Filter songs that are B-Sides of a release. The release must be a studio recording, otherwise returns an emtpy list", }) @TransformIdentifier(ReleaseService) bsides: ReleaseQueryParameters.WhereInput; @@ -140,7 +140,7 @@ export class Selector { @IsOptional() @ApiPropertyOptional({ - description: "The Seed to Sort the items", + description: "The seed to sort the items", }) @IsNumber() @IsPositive() @@ -231,7 +231,7 @@ export class SongController { summary: "Update a song", }) @Response({ handler: SongResponseBuilder }) - @Post(":idOrSlug") + @Put(":idOrSlug") @Role(Roles.Default, Roles.Microservice) async updateSong( @Body() updateDTO: UpdateSongDTO, From 54b6ddde37a3ba16ac4bad93d75aee2ba024186c Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 14:04:19 +0000 Subject: [PATCH 19/23] Server: Stream Controller: More doc --- server/src/stream/stream.controller.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/server/src/stream/stream.controller.ts b/server/src/stream/stream.controller.ts index 60178f1d..d40bcbb4 100644 --- a/server/src/stream/stream.controller.ts +++ b/server/src/stream/stream.controller.ts @@ -17,7 +17,12 @@ */ import { Controller, Get, Param, Req, Res, Response } from "@nestjs/common"; -import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from "@nestjs/swagger"; import FileService from "src/file/file.service"; import FileQueryParameters from "src/file/models/file.query-parameters"; import IdentifierParam from "src/identifier/identifier.pipe"; @@ -32,6 +37,7 @@ export class StreamController { summary: "Stream File", }) @Get(":idOrSlug/direct") + @ApiOkResponse({ description: "The raw binary content of the file" }) async streamFile( @IdentifierParam(FileService) where: FileQueryParameters.WhereInput, @@ -44,11 +50,18 @@ export class StreamController { @ApiOperation({ summary: "Transcode File", }) + @ApiParam({ + name: "path", + description: + "Endpoint of of the transcoder. See the possible endpoints [here](https://github.com/zoriya/Kyoo/blob/master/transcoder/main.go)", + example: "master.m3u8", + }) @Get(":idOrSlug/:path(*)") async transcodeFile( @IdentifierParam(FileService) where: FileQueryParameters.WhereInput, - @Param("path") path: string, + @Param("path") + path: string, @Res() res: Response, @Req() req: Express.Request, ) { From f8ef4a6cd21edf20978f375ed091d9078f786726 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 14:08:57 +0000 Subject: [PATCH 20/23] Server: User Controller: Rename Endpoint to create user --- front/src/api/api.ts | 2 +- server/src/user/user.controller.spec.ts | 10 +++++----- server/src/user/user.controller.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index 40085186..89926933 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -174,7 +174,7 @@ export default class API { */ static async register(credentials: AuthenticationInput): Promise { return API.fetch({ - route: "/users/new", + route: "/users", data: { name: credentials.username, password: credentials.password, diff --git a/server/src/user/user.controller.spec.ts b/server/src/user/user.controller.spec.ts index 17779388..71208555 100644 --- a/server/src/user/user.controller.spec.ts +++ b/server/src/user/user.controller.spec.ts @@ -42,7 +42,7 @@ describe("User Controller", () => { describe("Create a user account", () => { it("Should create the admin user", () => { return request(app.getHttpServer()) - .post(`/users/new`) + .post(`/users`) .send({ name: "admin", password: "admin1234", @@ -59,7 +59,7 @@ describe("User Controller", () => { it("Should create the user user", () => { return request(app.getHttpServer()) - .post(`/users/new`) + .post(`/users`) .send({ name: "user", password: "user1234", @@ -76,7 +76,7 @@ describe("User Controller", () => { it("Should return an error, as user already exists", () => { return request(app.getHttpServer()) - .post(`/users/new`) + .post(`/users`) .send({ name: "user", password: "user123456", @@ -86,7 +86,7 @@ describe("User Controller", () => { it("Should return an error, as username is not long enough", () => { return request(app.getHttpServer()) - .post(`/users/new`) + .post(`/users`) .send({ name: "use", password: "user123456", @@ -96,7 +96,7 @@ describe("User Controller", () => { it("Should return an error, as password is badly formed", () => { return request(app.getHttpServer()) - .post(`/users/new`) + .post(`/users`) .send({ name: "admi", password: "user", diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts index 1d02c4b7..7f71aa76 100644 --- a/server/src/user/user.controller.ts +++ b/server/src/user/user.controller.ts @@ -63,7 +63,7 @@ export default class UserController { }) @Public() @Response({ handler: UserResponseBuilder }) - @Post("new") + @Post() async createUserAccount(@Body() userDTO: UserCreateDTO) { return this.userService.create(userDTO); } From a5af9454d436626b6e18c4f38fb340e7f3819dab Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 14:14:05 +0000 Subject: [PATCH 21/23] Server: Video Controller: PUT instead of POST to update video --- front/src/api/api.ts | 4 ++-- server/src/video/video.controller.spec.ts | 4 ++-- server/src/video/video.controller.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index 89926933..cb82409f 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -584,7 +584,7 @@ export default class API { return API.fetch({ route: `/videos/${videoSlugOrId}`, errorMessage: "Update Video Failed", - method: "POST", + method: "PUT", parameters: {}, emptyResponse: true, data: { type: newType }, @@ -1513,7 +1513,7 @@ export default class API { return API.fetch({ route: `/videos/${videoSlugOrId}`, errorMessage: "Update Video Failed", - method: "POST", + method: "PUT", parameters: {}, emptyResponse: true, data: { masterTrackId: trackSlugOrId }, diff --git a/server/src/video/video.controller.spec.ts b/server/src/video/video.controller.spec.ts index c4533157..214d1c98 100644 --- a/server/src/video/video.controller.spec.ts +++ b/server/src/video/video.controller.spec.ts @@ -144,7 +144,7 @@ describe("Video Controller", () => { describe("Update Video", () => { it("should set track as master", () => { return request(app.getHttpServer()) - .post(`/videos/${dummyRepository.videoA1.id}`) + .put(`/videos/${dummyRepository.videoA1.id}`) .send({ masterTrackId: dummyRepository.trackA1_2Video.id, }) @@ -158,7 +158,7 @@ describe("Video Controller", () => { it("should fail as track does not belong to video", () => { return request(app.getHttpServer()) - .post(`/videos/${dummyRepository.videoA1.id}`) + .put(`/videos/${dummyRepository.videoA1.id}`) .send({ masterTrackId: dummyRepository.trackA1_1.id, }) diff --git a/server/src/video/video.controller.ts b/server/src/video/video.controller.ts index 626d7a86..a24d7996 100644 --- a/server/src/video/video.controller.ts +++ b/server/src/video/video.controller.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Body, Controller, Get, Post, Query } from "@nestjs/common"; +import { Body, Controller, Get, Put, Query } from "@nestjs/common"; import { ApiOperation, ApiPropertyOptional, ApiTags } from "@nestjs/swagger"; import Response, { ResponseType } from "src/response/response.decorator"; import { PaginationParameters } from "src/pagination/models/pagination-parameters"; @@ -48,7 +48,7 @@ export class Selector { @IsOptional() @ApiPropertyOptional({ enum: VideoType, - description: "Filter the videos by type", + description: "Filter videos by type", }) type?: VideoType; @@ -75,7 +75,7 @@ export class Selector { @IsOptional() @ApiPropertyOptional({ - description: "Get related songs", + description: "Filter videos by song", }) @TransformIdentifier(SongService) song?: SongQueryParameters.WhereInput; @@ -88,7 +88,7 @@ export class Selector { @IsOptional() @ApiPropertyOptional({ - description: "Get related songs ", + description: "Get videos of songs from group", }) @TransformIdentifier({ formatIdentifierToWhereInput: (identifier) => @@ -102,7 +102,7 @@ export class Selector { @IsOptional() @ApiPropertyOptional({ - description: "The Seed to Sort the items", + description: "The seed to sort the items", }) @IsNumber() @IsPositive() @@ -132,7 +132,7 @@ export class VideoController { summary: "Update a video", }) @Response({ handler: VideoResponseBuilder }) - @Post(":idOrSlug") + @Put(":idOrSlug") @Role(Roles.Default) async updateSong( @Body() updateDTO: UpdateVideoDTO, @@ -150,7 +150,7 @@ export class VideoController { } @ApiOperation({ - summary: "Get many Videos", + summary: "Get many videos", }) @Response({ handler: VideoResponseBuilder, From 16f9c987171d65fd36129ce77d6bfad52ff372f3 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 14:24:22 +0000 Subject: [PATCH 22/23] Front: Refactor API methods to set tracks as master --- front/src/api/api.ts | 46 ++----------------- .../src/components/actions/resource-type.tsx | 4 +- .../release-contextual-menu.tsx | 4 +- .../contextual-menu/track-contextual-menu.tsx | 4 +- 4 files changed, 12 insertions(+), 46 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index cb82409f..78649c78 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -565,7 +565,7 @@ export default class API { */ static async updateSong( songSlugOrId: number | string, - newType: SongType, + dto: Partial<{ type: SongType; masterTrackId: number }>, ): Promise { return API.fetch({ route: `/songs/${songSlugOrId}`, @@ -573,13 +573,13 @@ export default class API { method: "PUT", parameters: {}, emptyResponse: true, - data: { type: newType }, + data: dto, }); } static async updateVideo( videoSlugOrId: number | string, - newType: VideoType, + dto: Partial<{ type: VideoType; masterTrackId: number }>, ): Promise { return API.fetch({ route: `/videos/${videoSlugOrId}`, @@ -587,7 +587,7 @@ export default class API { method: "PUT", parameters: {}, emptyResponse: true, - data: { type: newType }, + data: dto, }); } @@ -1482,44 +1482,6 @@ export default class API { }); } - /** - * Mark a track as master - * @param trackSlugOrId - * @returns - */ - static async setTrackAsSongMaster( - trackSlugOrId: string | number, - songSlugOrId: string | number, - ): Promise { - return API.fetch({ - route: `/songs/${songSlugOrId}`, - errorMessage: "Update Song Failed", - method: "PUT", - parameters: {}, - emptyResponse: true, - data: { masterTrackId: trackSlugOrId }, - }); - } - - /** - * Mark a track as master - * @param trackSlugOrId - * @returns - */ - static async setTrackAsVideoMaster( - trackSlugOrId: string | number, - videoSlugOrId: string | number, - ): Promise { - return API.fetch({ - route: `/videos/${videoSlugOrId}`, - errorMessage: "Update Video Failed", - method: "PUT", - parameters: {}, - emptyResponse: true, - data: { masterTrackId: trackSlugOrId }, - }); - } - private static buildURL( route: string, parameters: QueryParameters, diff --git a/front/src/components/actions/resource-type.tsx b/front/src/components/actions/resource-type.tsx index 8bca39b4..2e961894 100644 --- a/front/src/components/actions/resource-type.tsx +++ b/front/src/components/actions/resource-type.tsx @@ -127,7 +127,7 @@ const ChangeSongType = ( "changeSongType", client, (newType: SongType) => - API.updateSong(s.id, newType).then((res) => { + API.updateSong(s.id, { type: newType }).then((res) => { client.client.invalidateQueries("songs"); client.client.invalidateQueries("release"); client.client.invalidateQueries("tracks"); @@ -167,7 +167,7 @@ const ChangeVideoType = ( "changeVideoType", client, (newType: VideoType) => - API.updateVideo(v.id, newType).then((res) => { + API.updateVideo(v.id, { type: newType }).then((res) => { client.client.invalidateQueries("videos"); return res; }), diff --git a/front/src/components/contextual-menu/release-contextual-menu.tsx b/front/src/components/contextual-menu/release-contextual-menu.tsx index 7a2f61c3..00cdc4b4 100644 --- a/front/src/components/contextual-menu/release-contextual-menu.tsx +++ b/front/src/components/contextual-menu/release-contextual-menu.tsx @@ -66,7 +66,9 @@ const ReleaseContextualMenu = (props: ReleaseContextualMenuProps) => { .reverse() .filter((track) => track.songId != null) .map((track) => - API.setTrackAsSongMaster(track.id, track.songId!), + API.updateSong(track.songId!, { + masterTrackId: track.id, + }), ), ) .then(() => { diff --git a/front/src/components/contextual-menu/track-contextual-menu.tsx b/front/src/components/contextual-menu/track-contextual-menu.tsx index b596a9a1..3b8bc6f1 100644 --- a/front/src/components/contextual-menu/track-contextual-menu.tsx +++ b/front/src/components/contextual-menu/track-contextual-menu.tsx @@ -70,7 +70,9 @@ const TrackContextualMenu = (props: TrackContextualMenuProps) => { const { t } = useTranslation(); const { playNext, playAfter } = usePlayerContext(); const masterMutation = useMutation(async () => { - return API.setTrackAsSongMaster(props.track.id, props.track.songId!) + return API.updateSong(props.track.songId!, { + masterTrackId: props.track.id, + }) .then(() => { toast.success(t("trackSetAsMaster")); queryClient.client.invalidateQueries(); From 1d8296c0d689431a3b50d62f5f36f7d30ce0f0c4 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 8 Jan 2025 14:33:37 +0000 Subject: [PATCH 23/23] Front: Refactor API for External Metadata --- front/src/api/api.ts | 60 +++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/front/src/api/api.ts b/front/src/api/api.ts index 78649c78..41cf9d1f 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -73,7 +73,6 @@ import { TaskResponse } from "../models/task"; import { AlbumExternalMetadata, ArtistExternalMetadata, - ReleaseExternalMetadata, SongExternalMetadata, } from "../models/external-metadata"; import { @@ -1236,58 +1235,45 @@ export default class API { static getArtistExternalMetadata( slugOrId: string | number, ): Query { - return { - key: ["artist", slugOrId, "external-metadata"], - exec: () => - API.fetch({ - route: `/external-metadata?artist=${slugOrId}`, - errorMessage: "Metadata could not be loaded", - parameters: {}, - validator: ArtistExternalMetadata, - }).catch(() => null), - }; + return API.getResourceExternalMetadata( + slugOrId, + "artist", + ArtistExternalMetadata, + ); } static getSongExternalMetadata( slugOrId: string | number, ): Query { - return { - key: ["song", slugOrId, "external-metadata"], - exec: () => - API.fetch({ - route: `/external-metadata?song=${slugOrId}`, - errorMessage: "Metadata could not be loaded", - parameters: {}, - validator: SongExternalMetadata, - }).catch(() => null), - }; + return API.getResourceExternalMetadata( + slugOrId, + "song", + SongExternalMetadata, + ); } static getAlbumExternalMetadata( slugOrId: string | number, ): Query { - return { - key: ["album", slugOrId, "external-metadata"], - exec: () => - API.fetch({ - route: `/external-metadata?album=${slugOrId}`, - errorMessage: "Metadata could not be loaded", - parameters: {}, - validator: AlbumExternalMetadata, - }).catch(() => null), - }; + return API.getResourceExternalMetadata( + slugOrId, + "album", + AlbumExternalMetadata, + ); } - static getReleaseExternalMetadata( - slugOrId: string | number, - ): Query { + static getResourceExternalMetadata>( + resourceSlugOrId: string | number, + resourceType: "artist" | "album" | "song", + validator: V, + ): Query { return { - key: ["release", slugOrId, "external-metadata"], + key: [resourceType, resourceSlugOrId, "external-metadata"], exec: () => API.fetch({ - route: `/external-metadata?release=${slugOrId}`, + route: `/external-metadata?${resourceType}=${resourceSlugOrId}`, errorMessage: "Metadata could not be loaded", parameters: {}, - validator: ReleaseExternalMetadata, + validator: validator, }).catch(() => null), }; }