diff --git a/front/src/api/api.ts b/front/src/api/api.ts index 1ed207eb..9cedac5b 100644 --- a/front/src/api/api.ts +++ b/front/src/api/api.ts @@ -666,6 +666,7 @@ export default class API { genre?: Identifier; artist?: Identifier; versionsOf?: Identifier; + rare?: Identifier; query?: string; bsides?: Identifier; random?: number; diff --git a/front/src/components/infinite/infinite-resource-view/infinite-song-view.tsx b/front/src/components/infinite/infinite-resource-view/infinite-song-view.tsx index b7f6f035..651feba9 100644 --- a/front/src/components/infinite/infinite-resource-view/infinite-song-view.tsx +++ b/front/src/components/infinite/infinite-resource-view/infinite-song-view.tsx @@ -158,15 +158,9 @@ const InfiniteSongView = < ]} disableSorting={props.disableSorting} onChange={setOptions} - sortingKeys={SongSortingKeys.filter( - (key) => key !== "userPlayCount", - )} + sortingKeys={SongSortingKeys} defaultSortingOrder={props.initialSortingOrder} - defaultSortingKey={ - props.initialSortingField == "userPlayCount" - ? "totalPlayCount" - : props.initialSortingField - } + defaultSortingKey={props.initialSortingField} router={props.light == true ? undefined : router} disableLayoutToggle defaultLayout={"list"} diff --git a/front/src/i18n/translations/en.json b/front/src/i18n/translations/en.json index 631dc437..e22bf312 100644 --- a/front/src/i18n/translations/en.json +++ b/front/src/i18n/translations/en.json @@ -237,5 +237,6 @@ "fr": "French", "yes": "Yes", "no": "No", - "featuredAlbums": "Featured Albums" + "featuredAlbums": "Featured Albums", + "rareSongs": "Rare Songs" } diff --git a/front/src/i18n/translations/fr.json b/front/src/i18n/translations/fr.json index 478de7c3..c3b42bbb 100644 --- a/front/src/i18n/translations/fr.json +++ b/front/src/i18n/translations/fr.json @@ -226,5 +226,6 @@ "fr": "Français", "yes": "Oui", "no": "Non", - "featuredAlbums": "En Vedette" + "featuredAlbums": "En Vedette", + "rareSongs": "Pistes Inédites" } diff --git a/front/src/pages/artists/[slugOrId]/index.tsx b/front/src/pages/artists/[slugOrId]/index.tsx index 74436e9a..0219b711 100644 --- a/front/src/pages/artists/[slugOrId]/index.tsx +++ b/front/src/pages/artists/[slugOrId]/index.tsx @@ -71,6 +71,13 @@ const topSongsQuery = (artistSlugOrId: string | number) => ["artist", "featuring", "master", "illustration"], ); +const rareSongsQuery = (artistSlugOrId: string | number) => + API.getSongs( + { rare: artistSlugOrId }, + { sortBy: "totalPlayCount", order: "desc" }, + ["artist", "featuring", "master", "illustration"], + ); + const artistQuery = (artistSlugOrId: string | number) => API.getArtist(artistSlugOrId, ["externalIds", "illustration"]); @@ -88,6 +95,7 @@ const prepareSSR = (context: NextPageContext) => { additionalProps: { artistIdentifier }, queries: [artistQuery(artistIdentifier)], infiniteQueries: [ + rareSongsQuery(artistIdentifier), videosQuery(artistIdentifier), topSongsQuery(artistIdentifier), appearanceQuery(artistIdentifier), @@ -110,6 +118,7 @@ const ArtistPage: Page> = ({ props }) => { })); const videos = useInfiniteQuery(videosQuery, artistIdentifier); const topSongs = useInfiniteQuery(topSongsQuery, artistIdentifier); + const rareSongs = useInfiniteQuery(rareSongsQuery, artistIdentifier); const appearances = useInfiniteQuery(appearanceQuery, artistIdentifier); const externalIdWithDescription = artist.data?.externalIds .filter(({ provider }) => provider.name.toLowerCase() !== "discogs") @@ -262,6 +271,20 @@ const ArtistPage: Page> = ({ props }) => { ))} + {(rareSongs.data?.pages?.at(0)?.items.length ?? 0) != 0 && ( + <> + + + + + + )} {[ { label: "topVideos", items: musicVideos } as const, { label: "extras", items: extras } as const, @@ -269,6 +292,7 @@ const ArtistPage: Page> = ({ props }) => { ({ label, items }) => items.length != 0 && ( + { parserService.getSongType("My Song (Original Version)"), ).toBe(SongType.Original); }); + it("Original Version (Original Mix)", () => { + expect(parserService.getSongType("My Song (Original Mix)")).toBe( + SongType.Original, + ); + }); it("Original Version (Feat Group)", () => { expect(parserService.getSongType("My Song (feat. A)")).toBe( SongType.Original, @@ -852,12 +857,12 @@ describe("Parser Service", () => { it("Demo (Rough Mix)", () => { expect(parserService.getSongType("Fever (Rough Mix)")).toBe( - SongType.Original, + SongType.Demo, ); }); it("Demo (Rough Mix Edit)", () => { expect(parserService.getSongType("Fever (Rough Mix Edit)")).toBe( - SongType.Original, + SongType.Demo, ); }); diff --git a/server/src/scanner/parser.service.ts b/server/src/scanner/parser.service.ts index dc485a27..5195482d 100644 --- a/server/src/scanner/parser.service.ts +++ b/server/src/scanner/parser.service.ts @@ -409,6 +409,9 @@ export default class ParserService { return SongType.Clean; } if (jointExtensionWords.includes("rough mix")) { + return SongType.Demo; + } + if (jointExtensionWords == "original mix") { return SongType.Original; } if (containsWord("mix") && containsWord("edit")) { diff --git a/server/src/song/song.controller.ts b/server/src/song/song.controller.ts index a070acbd..272ae62c 100644 --- a/server/src/song/song.controller.ts +++ b/server/src/song/song.controller.ts @@ -130,6 +130,13 @@ export class Selector { @TransformIdentifier(ReleaseService) bsides: ReleaseQueryParameters.WhereInput; + @IsOptional() + @ApiPropertyOptional({ + description: "Filter songs that would be considered to be 'rare'", + }) + @TransformIdentifier(ReleaseService) + rare: ArtistQueryParameters.WhereInput; + @IsOptional() @ApiPropertyOptional({ description: "The Seed to Sort the items", @@ -188,6 +195,13 @@ export class SongController { include, sort, ); + } else if (selector.rare) { + return this.songService.getRareSongsByArtist( + selector.rare, + paginationParameters, + include, + sort, + ); } return this.songService.getMany( selector, @@ -212,7 +226,7 @@ export class SongController { } @ApiOperation({ - summary: "Upate a song", + summary: "Update a song", }) @Response({ handler: SongResponseBuilder }) @Post(":idOrSlug") diff --git a/server/src/song/song.service.ts b/server/src/song/song.service.ts index ffc059f6..9c6101ba 100644 --- a/server/src/song/song.service.ts +++ b/server/src/song/song.service.ts @@ -53,6 +53,7 @@ import { getRandomIds, sortItemsUsingOrderedIdList, } from "src/repository/repository.utils"; +import ArtistQueryParameters from "src/artist/models/artist.query-parameters"; @Injectable() export default class SongService extends SearchableRepositoryService { @@ -676,14 +677,138 @@ export default class SongService extends SearchableRepositoryService { // We only want songs that have at least one audtio tracks { tracks: { some: { type: TrackType.Audio } } }, { - type: { - in: [ - SongType.Original, - SongType.Acoustic, - SongType.Demo, - SongType.NonMusic, - ], - }, + OR: [ + // We take original songs or extras + { + type: { in: ["Original", "NonMusic"] }, + }, + // Or songs that are only available as demos/acoustic versions + { + group: { + versions: { + every: { + type: { in: ["Demo", "Acoustic"] }, + }, + }, + }, + }, + ], + }, + ], + }, + orderBy: sort ? this.formatSortingInput(sort) : undefined, + include: include ?? ({} as I), + ...formatPaginationParameters(pagination), + }); + } + + async getRareSongsByArtist( + where: ArtistQueryParameters.WhereInput, + pagination?: PaginationParameters, + include?: I, + sort?: SongQueryParameters.SortingParameter, + ) { + const artist = await this.artistService + .get(where, { albums: true }) + .catch(() => null); + + if (!artist || artist.albums.length == 0) { + // if the artist does not have albums, lets skip this + return []; + } + return this.prismaService.song.findMany({ + where: { + // Take the tracks that have at least one audio track + tracks: { + some: { type: "Audio" }, + }, + AND: [ + { + OR: [ + { + type: { in: ["Original"] }, + }, + { + group: { + versions: { + every: { + type: { in: ["Demo", "Acoustic"] }, + }, + }, + }, + }, + ], + }, + { + OR: [ + { artistId: artist.id }, + { featuring: { some: { id: artist.id } } }, + ], + }, + { + OR: [ + // Take songs that only appears on other artist's album + { + // In that case, we only want song with artist being the main one + artistId: artist.id, + tracks: { + every: { + release: { + album: { + artistId: { not: artist.id }, + }, + }, + }, + }, + }, + // Take all tracks that only appear on non-master albums + { + tracks: { + every: { + release: { + album: { + type: "StudioRecording", + }, + masterOf: null, + }, + }, + }, + }, + // Take all tracks that appear only on singles AND non master albums + { + tracks: { + every: { + OR: [ + { + release: { + album: { + type: "Single", + }, + }, + trackIndex: { + notIn: [0, 1], + }, + }, + { + release: { + album: { + type: "StudioRecording", + }, + masterOf: null, + }, + }, + ], + }, + }, + }, + { + tracks: { + some: { + isBonus: true, + }, + }, + }, + ], }, ], },