Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2be1a5b240 | ||
|
|
bb7d5490da | ||
|
|
9cd7d1e641 | ||
|
|
6e0c250666 | ||
|
|
c4c1a8072e | ||
|
|
050cc507fb | ||
|
|
6692fff5ce | ||
|
|
2a199b8b12 | ||
|
|
e27db49826 | ||
|
|
8368dacc5c | ||
|
|
93bfd39ce6 | ||
|
|
0ef38851bb |
No files matched your search
+8
-9
@@ -1,3 +1,4 @@
|
||||
#################### Database
|
||||
# The port on the host where meelo will be accessible
|
||||
PORT=
|
||||
# Username to access database
|
||||
@@ -6,24 +7,22 @@ POSTGRES_USER=
|
||||
POSTGRES_PASSWORD=
|
||||
# Name of Meelo's database
|
||||
POSTGRES_DB=
|
||||
# Hostname from which Meelo can access the database
|
||||
# If dockerized, this must be the container_name
|
||||
POSTGRES_HOST=
|
||||
####################
|
||||
# The path to the `settings.json` file, and the illustration/metadata folder (on the host machine)
|
||||
#################### Config
|
||||
# The directory that contains the `settings.json` file (and where the illustrations will be stored) (on the host machine)
|
||||
CONFIG_DIR=
|
||||
# The root path of your libraries (on the host machine)
|
||||
DATA_DIR=
|
||||
####################
|
||||
# Server URL that would be accessible to the front app's server or any client
|
||||
#################### Web app
|
||||
# URL of the server/api that would be accessible to the front app's server or any client
|
||||
# If PORT=5000, it should look like 'http://0.0.0.0:5000/api' (mind the '/api')
|
||||
PUBLIC_SERVER_URL=
|
||||
####################
|
||||
#################### Security
|
||||
# Random String used to sign JWT Tokens
|
||||
JWT_SIGNATURE=
|
||||
# Key used to authenticate the Meilisearch Instance
|
||||
# Should be a random string, must be at least 16 bytes
|
||||
MEILI_MASTER_KEY=
|
||||
####################
|
||||
#################### Internal
|
||||
# Do not change this
|
||||
INTERNAL_DATA_DIR=/data
|
||||
INTERNAL_CONFIG_DIR=/config
|
||||
@@ -25,6 +25,7 @@ Get started! Look over [here](https://arthi-chaud.github.io/Meelo/) for setup in
|
||||
- Access, stream, and download songs from your server
|
||||
- Music Videos
|
||||
- B-Sides
|
||||
- Identifies 'Rare' Tracks in your collection
|
||||
- Playlists
|
||||
- Metadata scan using:
|
||||
- Embedded tags (including bitrate)
|
||||
|
||||
@@ -24,6 +24,7 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- TRANSCODER_URL=http://transcoder:7666
|
||||
- POSTGRES_HOST=db
|
||||
- MEILI_HOST=http://meilisearch:7700
|
||||
- REDIS_HOST=redis
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?schema=public
|
||||
|
||||
@@ -19,6 +19,7 @@ services:
|
||||
- TRANSCODER_URL=http://transcoder:7666
|
||||
- MEILI_HOST=http://meilisearch:7700
|
||||
- REDIS_HOST=redis
|
||||
- POSTGRES_HOST=db
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?schema=public
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- localhost:4000"]
|
||||
|
||||
@@ -20,6 +20,7 @@ services:
|
||||
- TRANSCODER_URL=http://transcoder:7666
|
||||
- MEILI_HOST=http://meilisearch:7700
|
||||
- REDIS_HOST=redis
|
||||
- POSTGRES_HOST=db
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?schema=public
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- localhost:4000"]
|
||||
|
||||
@@ -666,6 +666,7 @@ export default class API {
|
||||
genre?: Identifier;
|
||||
artist?: Identifier;
|
||||
versionsOf?: Identifier;
|
||||
rare?: Identifier;
|
||||
query?: string;
|
||||
bsides?: Identifier;
|
||||
random?: number;
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Box,
|
||||
Divider,
|
||||
Icon,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
@@ -37,7 +38,7 @@ import Artist from "../models/artist";
|
||||
import formatDuration from "../utils/formatDuration";
|
||||
import ReleaseTrackContextualMenu from "./contextual-menu/release-track-contextual-menu";
|
||||
import { SongWithRelations } from "../models/song";
|
||||
import { VideoIcon } from "./icons";
|
||||
import { ContextualMenuIcon, VideoIcon } from "./icons";
|
||||
import formatArtists from "../utils/formatArtists";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Fragment, useMemo } from "react";
|
||||
@@ -125,19 +126,25 @@ const ReleaseTrackList = ({
|
||||
<Fragment key={index}>
|
||||
<ListItem
|
||||
dense={
|
||||
currentTrack
|
||||
? currentTrack.song.artistId !=
|
||||
mainArtist?.id
|
||||
: false
|
||||
mainArtist === undefined
|
||||
? false
|
||||
: currentTrack
|
||||
? currentTrack.song.artistId !=
|
||||
mainArtist?.id
|
||||
: false
|
||||
}
|
||||
disablePadding
|
||||
disableGutters
|
||||
secondaryAction={
|
||||
currentTrack && (
|
||||
currentTrack ? (
|
||||
<ReleaseTrackContextualMenu
|
||||
track={currentTrack}
|
||||
artist={currentTrack.song.artist}
|
||||
/>
|
||||
) : (
|
||||
<IconButton disabled>
|
||||
<ContextualMenuIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -180,7 +187,7 @@ const ReleaseTrackList = ({
|
||||
)
|
||||
) : (
|
||||
<Skeleton
|
||||
width="30px"
|
||||
width="20px"
|
||||
sx={{ color: "text.disabled" }}
|
||||
/>
|
||||
)}
|
||||
@@ -188,18 +195,20 @@ const ReleaseTrackList = ({
|
||||
<ListItemText
|
||||
primary={
|
||||
currentTrack?.name ?? (
|
||||
<Skeleton width="100%" />
|
||||
<Skeleton width="120px" />
|
||||
)
|
||||
}
|
||||
primaryTypographyProps={{
|
||||
fontSize: "medium",
|
||||
}}
|
||||
secondary={
|
||||
currentTrack
|
||||
? formatTracksubtitle(
|
||||
currentTrack.song,
|
||||
)
|
||||
: undefined
|
||||
mainArtist === undefined
|
||||
? null
|
||||
: currentTrack
|
||||
? formatTracksubtitle(
|
||||
currentTrack.song,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
secondaryTypographyProps={{
|
||||
fontSize: "small",
|
||||
|
||||
@@ -23,6 +23,8 @@ import { Language, Languages, persistLanguage } from "../../i18n/i18n";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import Link from "next/link";
|
||||
import { Book1, Star, Warning2 } from "iconsax-react";
|
||||
|
||||
const SettingGroupStyle = {
|
||||
paddingTop: 1,
|
||||
@@ -35,6 +37,9 @@ const InputContainerStyle = {
|
||||
display: "flex",
|
||||
} as const;
|
||||
|
||||
const RepositoryUrl = "https://github.com/Arthi-chaud/meelo";
|
||||
const LinkIconStyle = { marginBottom: -5, marginRight: 5 };
|
||||
|
||||
const UserSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const colorScheme = useColorScheme();
|
||||
@@ -153,6 +158,36 @@ const UserSettings = () => {
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<SectionHeader heading={t("project")} />
|
||||
<p>
|
||||
<Star style={LinkIconStyle} />
|
||||
{t("enjoyingTheProject")}{" "}
|
||||
<Link
|
||||
style={{ textDecoration: "underline" }}
|
||||
href={RepositoryUrl}
|
||||
>
|
||||
{t("starOnGithub")}
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Warning2 style={LinkIconStyle} />
|
||||
{t("encounteredABug")}{" "}
|
||||
<Link
|
||||
style={{ textDecoration: "underline" }}
|
||||
href={`${RepositoryUrl}/issues`}
|
||||
>
|
||||
{t("openAnIssue")}
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Book1 style={LinkIconStyle} />
|
||||
<Link
|
||||
style={{ textDecoration: "underline" }}
|
||||
href="https://arthi-chaud.github.io/Meelo"
|
||||
>
|
||||
{t("readTheDoc")}
|
||||
</Link>
|
||||
</p>
|
||||
</NoSsr>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ const ReleaseTile = (props: {
|
||||
formatSubtitle?: (release: ReleaseWithRelations<"album">) => string;
|
||||
}) => {
|
||||
const yearFormat = props.release
|
||||
? getYear(props.release.releaseDate)?.toString()
|
||||
? getYear(props.release.releaseDate)?.toString() ?? ""
|
||||
: "";
|
||||
return (
|
||||
<Tile
|
||||
|
||||
@@ -228,14 +228,21 @@
|
||||
"useSystemeTheme": "Use System Theme",
|
||||
"useDarkTheme": "Use Dark Theme",
|
||||
"language": "Language",
|
||||
"project": "Project",
|
||||
"useSystemeLanguage": "Use System Language",
|
||||
"notifications": "Notifications",
|
||||
"permissions": "Permissions",
|
||||
"notifyOnTrackChange": "Notify On Track Change",
|
||||
"enjoyingTheProject": "Enjoying the project?",
|
||||
"starOnGithub": "Star Meelo on GitHub!",
|
||||
"encounteredABug": "You encountered a bug?",
|
||||
"openAnIssue": "Open an issue on GitHub!",
|
||||
"readTheDoc": "Read the user documentation",
|
||||
|
||||
"en": "English",
|
||||
"fr": "French",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"featuredAlbums": "Featured Albums"
|
||||
"featuredAlbums": "Featured Albums",
|
||||
"rareSongs": "Rare Songs"
|
||||
}
|
||||
@@ -217,14 +217,21 @@
|
||||
"useSystemeTheme": "Utiliser le thème système",
|
||||
"useDarkTheme": "Utiliser le mode sombre",
|
||||
"language": "Langue",
|
||||
"project": "Projet",
|
||||
"useSystemeLanguage": "Utiliser la langue du système",
|
||||
"notifications": "Notifications",
|
||||
"permissions": "Permissions",
|
||||
"notifyOnTrackChange": "Notifier quand le morceau change",
|
||||
"enjoyingTheProject": "Vous aimez le projet?",
|
||||
"starOnGithub": "Donner une étoile à Meelo sur GitHub!",
|
||||
"encounteredABug": "Vous avez rencontré un problème?",
|
||||
"openAnIssue": "Ouvrir une issue sur GitHub!",
|
||||
"readTheDoc": "Accéder à la documentation utilisateur",
|
||||
|
||||
"en": "Anglais",
|
||||
"fr": "Français",
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
"featuredAlbums": "En Vedette"
|
||||
"featuredAlbums": "En Vedette",
|
||||
"rareSongs": "Pistes Inédites"
|
||||
}
|
||||
@@ -97,9 +97,10 @@ type SongWithRelations<Selection extends SongInclude | never = never> =
|
||||
export default Song;
|
||||
export const SongSortingKeys = [
|
||||
"name",
|
||||
"artistName",
|
||||
"releaseDate",
|
||||
"addDate",
|
||||
"userPlayCount",
|
||||
"totalPlayCount",
|
||||
"artistName",
|
||||
"addDate",
|
||||
] as const;
|
||||
export { type SongInclude, SongWithRelations, SongRelations };
|
||||
@@ -71,6 +71,13 @@ const topSongsQuery = (artistSlugOrId: string | number) =>
|
||||
["artist", "featuring", "master", "illustration"],
|
||||
);
|
||||
|
||||
const rareSongsQuery = (artistSlugOrId: string | number) =>
|
||||
API.getSongs(
|
||||
{ rare: artistSlugOrId },
|
||||
{ sortBy: "releaseDate", 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<GetPropsTypesFrom<typeof prepareSSR>> = ({ 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,48 @@ const ArtistPage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({ props }) => {
|
||||
</Grid>
|
||||
</Fragment>
|
||||
))}
|
||||
{(rareSongs.data?.pages?.at(0)?.items.length ?? 0) != 0 && (
|
||||
<>
|
||||
<SectionHeader
|
||||
heading={t("rareSongs")}
|
||||
trailing={
|
||||
<Fade
|
||||
in={
|
||||
(rareSongs.data?.pages.at(0)?.items
|
||||
.length ?? 0) > songListSize
|
||||
}
|
||||
>
|
||||
<Link
|
||||
href={`/artists/${artistIdentifier}/rare-songs`}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
endIcon={<MoreIcon />}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{t("seeAll")}
|
||||
</Button>
|
||||
</Link>
|
||||
</Fade>
|
||||
}
|
||||
/>
|
||||
<Grid item sx={{ overflowX: "clip", width: "100%" }}>
|
||||
<SongGrid
|
||||
parentArtistName={artist.data?.name}
|
||||
songs={
|
||||
rareSongs.data?.pages
|
||||
?.at(0)
|
||||
?.items.slice(0, songListSize) ??
|
||||
generateArray(songListSize)
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{[
|
||||
{ label: "topVideos", items: musicVideos } as const,
|
||||
{ label: "extras", items: extras } as const,
|
||||
@@ -269,6 +320,7 @@ const ArtistPage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({ props }) => {
|
||||
({ label, items }) =>
|
||||
items.length != 0 && (
|
||||
<Fragment key={`videos-${label}`}>
|
||||
<Box sx={{ paddingBottom: sectionPadding }} />
|
||||
<SectionHeader
|
||||
heading={t(label)}
|
||||
trailing={
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Meelo is a music server and application to enjoy your personal music files anywhere, anytime you want.
|
||||
* Copyright (C) 2023
|
||||
*
|
||||
* Meelo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Meelo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ArtistSongPage from "./songs";
|
||||
// routing checks are done there
|
||||
export default ArtistSongPage;
|
||||
@@ -38,6 +38,9 @@ import { useGradientBackground } from "../../../utils/gradient-background";
|
||||
const artistQuery = (identifier: string | number) =>
|
||||
API.getArtist(identifier, ["illustration"]);
|
||||
|
||||
const isRareSongsPage = ({ asPath }: { asPath?: string }) =>
|
||||
asPath?.includes("/rare-songs") ?? false;
|
||||
|
||||
const prepareSSR = async (
|
||||
context: NextPageContext,
|
||||
queryClient: QueryClient,
|
||||
@@ -47,12 +50,14 @@ const prepareSSR = async (
|
||||
const sortBy = getSortingFieldParams(context.query.sortBy, SongSortingKeys);
|
||||
const songs = await queryClient.fetchInfiniteQuery(
|
||||
prepareMeeloInfiniteQuery(() =>
|
||||
API.getSongs({ artist: artistIdentifier }, { sortBy, order }, [
|
||||
"artist",
|
||||
"featuring",
|
||||
"master",
|
||||
"illustration",
|
||||
]),
|
||||
API.getSongs(
|
||||
{
|
||||
[isRareSongsPage(context) ? "rare" : "artist"]:
|
||||
artistIdentifier,
|
||||
},
|
||||
{ sortBy, order },
|
||||
["artist", "featuring", "master", "illustration"],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -96,7 +101,8 @@ const ArtistSongPage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({
|
||||
query={({ library, sortBy, order, type, random }) =>
|
||||
API.getSongs(
|
||||
{
|
||||
artist: artistIdentifier,
|
||||
[isRareSongsPage(router) ? "rare" : "artist"]:
|
||||
artistIdentifier,
|
||||
type,
|
||||
random,
|
||||
library: library ?? undefined,
|
||||
|
||||
@@ -100,7 +100,7 @@ const releaseTracklistQuery = (releaseIdentifier: number | string) => {
|
||||
};
|
||||
};
|
||||
const albumQuery = (albumId: number) =>
|
||||
API.getAlbum(albumId, ["externalIds", "genres"]);
|
||||
API.getAlbum(albumId, ["externalIds", "genres", "artist"]);
|
||||
const artistsOnAlbumQuery = (albumId: number) => {
|
||||
const query = API.getArtists({ album: albumId }, undefined, [
|
||||
"illustration",
|
||||
@@ -216,15 +216,7 @@ const ReleasePage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({ props }) => {
|
||||
relatedPlaylistsQuery,
|
||||
release.data?.albumId,
|
||||
);
|
||||
const albumArtist = useMemo(
|
||||
() =>
|
||||
album.data?.artistId === null
|
||||
? null
|
||||
: artists.data
|
||||
? artists.data.find((artist) => artist.id === artistId)
|
||||
: undefined,
|
||||
[artistId, artists, album.data],
|
||||
);
|
||||
const albumArtist = useMemo(() => album.data?.artist, [album.data]);
|
||||
const featurings = useMemo(
|
||||
() => artists.data?.filter((artist) => artist.id !== artistId),
|
||||
[artistId, artists],
|
||||
@@ -608,7 +600,7 @@ const ReleasePage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({ props }) => {
|
||||
<ReleaseTrackList
|
||||
mainArtist={albumArtist}
|
||||
tracklist={
|
||||
trackList
|
||||
trackList && album.data // need to wait for main artist
|
||||
? Object.fromEntries(
|
||||
Array.from(
|
||||
Object.entries(trackList),
|
||||
|
||||
@@ -36,11 +36,7 @@ describe("Wikipedia Provider", () => {
|
||||
|
||||
expect(value).toBe("Siobhán Donaghy");
|
||||
expect(description).not.toBeNull();
|
||||
expect(
|
||||
description!.startsWith(
|
||||
"Siobhán Emma Donaghy (born 14 June 1984)",
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(description!.includes("Siobhán Emma Donaghy")).toBeTruthy();
|
||||
expect(
|
||||
description!.endsWith(
|
||||
"rights to the Sugababes name again in 2019.",
|
||||
|
||||
@@ -579,6 +579,11 @@ describe("Parser Service", () => {
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -128,6 +128,7 @@ namespace SongQueryParameters {
|
||||
"userPlayCount",
|
||||
"artistName",
|
||||
"addDate",
|
||||
"releaseDate",
|
||||
] as const;
|
||||
export type SortingKeys = typeof SortingKeys;
|
||||
export class SortingParameter extends ModelSortingParameter(SortingKeys) {}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
@@ -362,6 +363,34 @@ export default class SongService extends SearchableRepositoryService {
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "releaseDate":
|
||||
sort.push(
|
||||
{
|
||||
master: {
|
||||
release: {
|
||||
album: {
|
||||
releaseDate: sortingParameter.order,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
master: {
|
||||
release: {
|
||||
releaseDate: sortingParameter.order,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "asc",
|
||||
},
|
||||
{
|
||||
artist: {
|
||||
name: "asc",
|
||||
},
|
||||
},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
sort.push({
|
||||
[sortingParameter.sortBy ?? "id"]: sortingParameter.order,
|
||||
@@ -676,14 +705,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<I extends SongQueryParameters.RelationInclude>(
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"useExternalProviderGenres": true
|
||||
},
|
||||
"providers": {
|
||||
"genius": {},
|
||||
"musicbrainz": {}
|
||||
},
|
||||
"compilations": {
|
||||
|
||||
Reference in new issue
Block a user