diff --git a/src/album/album.pipe.ts b/src/album/album.pipe.ts index 106b9a2e1..546b8097d 100644 --- a/src/album/album.pipe.ts +++ b/src/album/album.pipe.ts @@ -1,8 +1,9 @@ import type { ArgumentMetadata, PipeTransform } from "@nestjs/common"; import { InvalidRequestException } from "src/exceptions/meelo-exception"; import { ParseIdPipe } from "src/identifier/id.pipe"; +import ParseMultipleSlugPipe from "src/identifier/identifier.parse-slugs"; import { SlugSeparator } from "src/identifier/identifier.slug-separator"; -import Slug from "src/slug/slug"; +import compilationAlbumArtistKeyword from "src/utils/compilation"; import type AlbumQueryParameters from "./models/album.query-parameters"; export default class ParseAlbumIdentifierPipe implements PipeTransform { @@ -10,15 +11,15 @@ export default class ParseAlbumIdentifierPipe implements PipeTransform { try { return { byId: { id: new ParseIdPipe().transform(value.idOrSlug, _metadata) }}; } catch { - const slugs = value.idOrSlug.split(SlugSeparator); + const slugs = new ParseMultipleSlugPipe().transform(value.idOrSlug, _metadata); if (slugs.length != 2) throw new InvalidRequestException(`Expected the following string format: 'artist-slug${SlugSeparator}album-slug'`); return { bySlug: { - slug: new Slug(slugs[1]), - artist: { - slug: new Slug(slugs[0]), - } + slug: slugs[1], + artist: slugs[0].toString() == compilationAlbumArtistKeyword + ? undefined + : { slug: slugs[0] } } } } diff --git a/src/artist/artist.pipe.ts b/src/artist/artist.pipe.ts index 931d6a044..fdc108e35 100644 --- a/src/artist/artist.pipe.ts +++ b/src/artist/artist.pipe.ts @@ -1,9 +1,9 @@ import type { ArgumentMetadata } from "@nestjs/common"; -import ParseResourceIdentifierPipe from "src/identifier/identifier.pipe"; +import ParseBaseIdentifierPipe from "src/identifier/identifier.base-pipe"; import compilationAlbumArtistKeyword from "src/utils/compilation"; import type ArtistQueryParameters from "./models/artist.query-parameters"; -class ParseArtistIdentifierPipe extends ParseResourceIdentifierPipe { +class ParseArtistIdentifierPipe extends ParseBaseIdentifierPipe { transform(value: T, _metadata: ArgumentMetadata): ArtistQueryParameters.WhereInput { const transformedIdentifier = super.transform(value, _metadata); if (transformedIdentifier.slug?.toString() == compilationAlbumArtistKeyword) { diff --git a/src/identifier/identifier.pipe.ts b/src/identifier/identifier.base-pipe.ts similarity index 77% rename from src/identifier/identifier.pipe.ts rename to src/identifier/identifier.base-pipe.ts index 1f7ccc105..a4145276f 100644 --- a/src/identifier/identifier.pipe.ts +++ b/src/identifier/identifier.base-pipe.ts @@ -2,7 +2,7 @@ import type { ArgumentMetadata, PipeTransform } from "@nestjs/common"; import { ParseIdPipe } from "src/identifier/id.pipe"; import Slug from "src/slug/slug"; -export default class ParseResourceIdentifierPipe> implements PipeTransform { +export default class ParseBaseIdentifierPipe> implements PipeTransform { transform(value: T, _metadata: ArgumentMetadata): W { try { const id = new ParseIdPipe().transform(value.idOrSlug, _metadata); diff --git a/src/identifier/identifier.parse-slugs.ts b/src/identifier/identifier.parse-slugs.ts new file mode 100644 index 000000000..1ae618563 --- /dev/null +++ b/src/identifier/identifier.parse-slugs.ts @@ -0,0 +1,10 @@ +import type { ArgumentMetadata, PipeTransform } from "@nestjs/common"; +import Slug from "src/slug/slug"; +import { SlugSeparator } from "./identifier.slug-separator"; + +export default class ParseMultipleSlugPipe implements PipeTransform { + transform(value: any, _metadata: ArgumentMetadata): Slug[] { + const slugs = value.split(SlugSeparator).map((slugString: string) => new Slug(slugString)); + return slugs; + } +}; \ No newline at end of file diff --git a/src/library/library.pipe.ts b/src/library/library.pipe.ts index 2ebde1c1e..267785b0a 100644 --- a/src/library/library.pipe.ts +++ b/src/library/library.pipe.ts @@ -1,5 +1,5 @@ -import ParseResourceIdentifierPipe from "src/identifier/identifier.pipe"; +import ParseBaseIdentifierPipe from "src/identifier/identifier.base-pipe"; import type LibraryQueryParameters from "./models/library.query-parameters"; -class ParseLibraryIdentifierPipe extends ParseResourceIdentifierPipe {}; +class ParseLibraryIdentifierPipe extends ParseBaseIdentifierPipe {}; export default ParseLibraryIdentifierPipe; \ No newline at end of file diff --git a/src/song/song.controller.spec.ts b/src/song/song.controller.spec.ts index f160097c8..4f24adf9a 100644 --- a/src/song/song.controller.spec.ts +++ b/src/song/song.controller.spec.ts @@ -216,6 +216,18 @@ describe('Song Controller', () => { }); }); }); + it("should return song (w/ slug)", () => { + return request(app.getHttpServer()) + .get(`/songs/${artist.slug}+${song1.slug}`) + .expect(200) + .expect((res) => { + let song: Song = res.body + expect(song).toStrictEqual({ + ...song1, + illustration: `http://meelo.com/songs/${song1.id}/illustration`, + }); + }); + }); it("should return song w/ artist", () => { return request(app.getHttpServer()) .get(`/songs/${song1.id}?with=artist`) @@ -359,6 +371,18 @@ describe('Song Controller', () => { }); }); }); + it("should return artist", () => { + return request(app.getHttpServer()) + .get(`/songs/${artist.slug}+${song2.slug}/artist`) + .expect(200) + .expect((res) => { + let fetchedArtist : Artist = res.body + expect(fetchedArtist).toStrictEqual({ + ...artist, + illustration: `http://meelo.com/artists/${artist.id}/illustration` + }); + }); + }); it("should return artist w/ songs & albums", () => { return request(app.getHttpServer()) .get(`/songs/${song2.id}/artist?with=songs,albums`) diff --git a/src/song/song.controller.ts b/src/song/song.controller.ts index e24e6d006..4ea4c1197 100644 --- a/src/song/song.controller.ts +++ b/src/song/song.controller.ts @@ -2,13 +2,13 @@ import { Controller, forwardRef, Get, Inject, Param, Query, Redirect } from '@ne import { UrlGeneratorService } from 'nestjs-url-generator'; import ArtistService from 'src/artist/artist.service'; import ArtistQueryParameters from 'src/artist/models/artist.query-parameters'; -import { ParseIdPipe } from 'src/identifier/id.pipe'; import type { PaginationParameters } from 'src/pagination/models/pagination-parameters'; import ParsePaginationParameterPipe from 'src/pagination/pagination.pipe'; import TrackQueryParameters from 'src/track/models/track.query-parameters'; import { TrackController } from 'src/track/track.controller'; import TrackService from 'src/track/track.service'; import SongQueryParameters from './models/song.query-params'; +import ParseSongIdentifierPipe from './song.pipe'; import SongService from './song.service'; @@ -36,70 +36,64 @@ export class SongController { return songs.map((song) => this.songService.buildSongResponse(song)); } - @Get(':id') + @Get(':idOrSlug') async getSong( @Query('with', SongQueryParameters.ParseRelationIncludePipe) include: SongQueryParameters.RelationInclude, - @Param('id', ParseIdPipe) - songId: number + @Param(ParseSongIdentifierPipe) + where: SongQueryParameters.WhereInput ) { - let song = await this.songService.getSong({ byId: { id: songId } }, include); + let song = await this.songService.getSong(where, include); return this.songService.buildSongResponse(song); } - @Get(':id/artist') + @Get(':idOrSlug/artist') async getSongArtist( @Query('with', ArtistQueryParameters.ParseRelationIncludePipe) include: ArtistQueryParameters.RelationInclude, - @Param('id', ParseIdPipe) - songId: number + @Param(ParseSongIdentifierPipe) + where: SongQueryParameters.WhereInput ) { - let song = await this.songService.getSong({ byId: { id: songId } }); + let song = await this.songService.getSong(where); let artist = await this.artistService.getArtist({ id: song.artistId }, include); return this.artistService.buildArtistResponse(artist); } - @Get(':id/master') + @Get(':idOrSlug/master') async getSongMaster( @Query('with', TrackQueryParameters.ParseRelationIncludePipe) include: TrackQueryParameters.RelationInclude, - @Param('id', ParseIdPipe) - songId: number + @Param(ParseSongIdentifierPipe) + where: SongQueryParameters.WhereInput ) { - let master = await this.trackService.getMasterTrack({ - byId: { id: songId } - }, include); + let master = await this.trackService.getMasterTrack(where, include); return this.trackService.buildTrackResponse(master); } - @Get(':id/tracks') + @Get(':idOrSlug/tracks') async getSongTracks( @Query(ParsePaginationParameterPipe) paginationParameters: PaginationParameters, @Query('with', TrackQueryParameters.ParseRelationIncludePipe) include: TrackQueryParameters.RelationInclude, - @Param('id', ParseIdPipe) - songId: number + @Param(ParseSongIdentifierPipe) + where: SongQueryParameters.WhereInput ) { - let tracks = await this.trackService.getSongTracks({ - byId: { id: songId } - }, paginationParameters, include); + let tracks = await this.trackService.getSongTracks(where, paginationParameters, include); if (tracks.length == 0) - await this.songService.getSong({ byId: { id: songId }}); + await this.songService.getSong(where); return tracks.map((track) => this.trackService.buildTrackResponse(track)); } - @Get(':id/illustration') + @Get(':idOrSlug/illustration') @Redirect() async getSongIllustration( - @Param('id', ParseIdPipe) - songId: number + @Param(ParseSongIdentifierPipe) + where: SongQueryParameters.WhereInput ) { - let master = await this.trackService.getMasterTrack({ - byId: { id: songId } - }); + let master = await this.trackService.getMasterTrack(where); const illustrationRedirectUrl = this.urlGeneratorService.generateUrlFromController({ controller: TrackController, controllerMethod: TrackController.prototype.getTrackIllustration, diff --git a/src/song/song.pipe.ts b/src/song/song.pipe.ts new file mode 100644 index 000000000..1951aec1d --- /dev/null +++ b/src/song/song.pipe.ts @@ -0,0 +1,26 @@ +import type { PipeTransform, ArgumentMetadata } from "@nestjs/common"; +import { InvalidRequestException } from "src/exceptions/meelo-exception"; +import { ParseIdPipe } from "src/identifier/id.pipe"; +import ParseMultipleSlugPipe from "src/identifier/identifier.parse-slugs"; +import { SlugSeparator } from "src/identifier/identifier.slug-separator"; +import type SongQueryParameters from "./models/song.query-params"; + +export default class ParseSongIdentifierPipe implements PipeTransform { + transform(value: T, _metadata: ArgumentMetadata): SongQueryParameters.WhereInput { + try { + return { byId: { id: new ParseIdPipe().transform(value.idOrSlug, _metadata) }}; + } catch { + const slugs = new ParseMultipleSlugPipe().transform(value.idOrSlug, _metadata); + if (slugs.length != 2) + throw new InvalidRequestException(`Expected the following string format: 'artist-slug${SlugSeparator}song-slug'`); + return { + bySlug: { + slug: slugs[1], + artist: { + slug: slugs[0], + } + } + } + } + } +}; \ No newline at end of file diff --git a/src/song/song.service.ts b/src/song/song.service.ts index 54a2fd2bb..592ee9450 100644 --- a/src/song/song.service.ts +++ b/src/song/song.service.ts @@ -176,7 +176,7 @@ export default class SongService { controller: SongController, controllerMethod: SongController.prototype.getSongIllustration, params: { - id: song.id.toString() + idOrSlug: song.id.toString() } }) };