Compare commits

..
16 Commits
Author SHA1 Message Date
Arthur Jamet 2be1a5b240 Front: Fix skeleton on release tile when release date is null 2024-07-27 17:15:36 +02:00
Arthur Jamet bb7d5490da Front: Add Links to Github + doc in settings 2024-07-26 19:01:51 +02:00
Arthur Jamet 9cd7d1e641 Server: Fix tests 2024-07-13 13:03:43 +02:00
Arthur Jamet 6e0c250666 Servier: Single push for song sorting parameters 2024-07-13 13:03:43 +02:00
Arthur Jamet c4c1a8072e Front: rare songs ordered by release date in artist page 2024-07-13 13:03:43 +02:00
Arthur Jamet 050cc507fb Song: Sort by release date 2024-07-13 13:03:43 +02:00
Arthur Jamet 6692fff5ce front: Add page for rare artist songs 2024-07-13 11:01:43 +02:00
Arthur Jamet 2a199b8b12 Update README.md 2024-07-09 18:32:25 +02:00
Arthur Jamet e27db49826 Get Rare Songs (#691)
* wip

* Song Controller: Get Rare songs by an artist

* Front: Artist Page: Get Rare songs

* Rare Songs: Only consider songs that have only one version AND are demos or acoustic versions

* B-Sides: apply some rules from rare songs

* Server: Prettier
2024-07-09 18:02:22 +02:00
Arthur Jamet 8368dacc5c Front: Better skeletons for release tracklist 2024-07-08 19:16:03 +02:00
Arthur Jamet 93bfd39ce6 Documentation fixes (#687)
* Documentation: Settings example: Remove genius provider, as it need an api key

* docker compose: set postgres host here, instead of .env

* Doc: env: remove postgres-host + better explaination of the CONFIG_DIR variable

* Doc: env: name section
2024-07-07 19:26:21 +02:00
Arthur Jamet 0ef38851bb Front: remove artist from Skeleton in release tracklist 2024-07-07 15:29:40 +02:00
Arthur Jamet 510d92fa4a Front: do not unmount video player on tab change 2024-07-05 20:24:55 +02:00
Arthur Jamet dd8ad74162 Front: Limit genre list to 10 items 2024-07-05 20:24:55 +02:00
Arthur Jamet 190e22358b Server: Fix path encoding for Transcoder 2024-07-04 18:12:14 +02:00
Arthur Jamet a2a527b4ba Front: Add padding for lyrics in player + fix scroll on mobile 2024-06-20 08:15:01 +02:00
27 changed files with 570 additions and 267 deletions

No files matched your search

+8 -9
View File
@@ -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
+1
View File
@@ -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)
+1
View File
@@ -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
+1
View File
@@ -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"]
+1
View File
@@ -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"]
+1
View File
@@ -666,6 +666,7 @@ export default class API {
genre?: Identifier;
artist?: Identifier;
versionsOf?: Identifier;
rare?: Identifier;
query?: string;
bsides?: Identifier;
random?: number;
@@ -158,9 +158,7 @@ const InfiniteSongView = <
]}
disableSorting={props.disableSorting}
onChange={setOptions}
sortingKeys={SongSortingKeys.filter(
(key) => key !== "userPlayCount",
)}
sortingKeys={SongSortingKeys}
defaultSortingOrder={props.initialSortingOrder}
defaultSortingKey={props.initialSortingField}
router={props.light == true ? undefined : router}
+173 -179
View File
@@ -44,7 +44,7 @@ import {
useTheme,
} from "@mui/material";
import Illustration from "../illustration";
import { LegacyRef, useState } from "react";
import { LegacyRef, useCallback, useState } from "react";
import PlayerSlider from "./controls/slider";
import API from "../../api/api";
import { useQuery } from "../../api/use-query";
@@ -241,7 +241,7 @@ const ExpandedPlayerControls = (
const [selectedTab, selectTab] = useState<"player" | "lyrics" | "playlist">(
"player",
);
const requestFullscreen = () => {
const requestFullscreen = useCallback(() => {
const el: any = document.getElementById("videoPlayer");
if (el.requestFullscreen) {
@@ -257,7 +257,7 @@ const ExpandedPlayerControls = (
} else if (el.enterFullscreen) {
el.enterFullscreen();
}
};
}, []);
return (
<Stack
@@ -323,163 +323,98 @@ const ExpandedPlayerControls = (
<Divider sx={{ margin: 1 }} variant="middle" />
)}
{selectedTab === "player" && (
<Grid
container
sx={
selectedTab !== "player"
? { width: 0, height: 0, overflow: "hidden" }
: {
height: "100%",
minHeight: "500px",
flexWrap: "nowrap",
justifyContent: "center",
alignSelf: "center",
maxWidth: theme.breakpoints.values.md,
}
}
direction="column"
>
<Grid
container
item
xs={7}
sx={{
height: "100%",
minHeight: "500px",
flexWrap: "nowrap",
padding: 3,
overflow: "hidden",
display: "flex",
justifyContent: "center",
alignSelf: "center",
maxWidth: theme.breakpoints.values.md,
}}
direction="column"
>
<Grid
item
xs={7}
sx={{
padding: 3,
overflow: "hidden",
display: "flex",
justifyContent: "center",
}}
>
{props.track?.type == "Video" ? (
<video
playsInline
id="videoPlayer"
ref={props.videoRef}
disablePictureInPicture={false}
style={{
borderRadius: theme.shape.borderRadius,
}}
onClick={requestFullscreen}
{props.track?.type == "Video" ? (
<video
playsInline
id="videoPlayer"
ref={props.videoRef}
disablePictureInPicture={false}
style={{
borderRadius: theme.shape.borderRadius,
}}
onClick={requestFullscreen}
/>
) : (
<Box
sx={{
height: "100%",
aspectRatio: "1",
objectFit: "contain",
overflow: "hidden",
}}
>
<Illustration
quality="original"
illustration={props.track?.illustration ?? null}
fallback={<TrackIcon />}
/>
) : (
<Box
</Box>
)}
</Grid>
<Grid item sx={{ width: "100%" }}>
<Stack spacing={3}>
<Grid
container
sx={{
...playerTextStyle,
width: "100%",
flexGrow: 1,
display: "flex",
justifyContent: "center",
}}
>
<Grid
item
xs={1}
sx={{
height: "100%",
aspectRatio: "1",
objectFit: "contain",
overflow: "hidden",
display: "flex",
justifyContent: "end",
}}
>
<Illustration
quality="original"
illustration={
props.track?.illustration ?? null
}
fallback={<TrackIcon />}
/>
</Box>
)}
</Grid>
<Grid item sx={{ width: "100%" }}>
<Stack spacing={3}>
{props.track?.type == "Video" && (
<IconButton onClick={requestFullscreen}>
<FullscreenIcon size={18} />
</IconButton>
)}
</Grid>
<Grid
container
item
xs={10}
sx={{
...playerTextStyle,
width: "100%",
flexGrow: 1,
display: "flex",
justifyContent: "center",
}}
>
<Grid
item
xs={1}
sx={{
display: "flex",
justifyContent: "end",
}}
>
{props.track?.type == "Video" && (
<IconButton onClick={requestFullscreen}>
<FullscreenIcon size={18} />
</IconButton>
)}
</Grid>
<Grid
item
xs={10}
sx={{
...playerTextStyle,
display: "flex",
justifyContent: "center",
}}
>
{props.artist && props.track ? (
<Link
href={`/releases/${props.track.releaseId}`}
style={{
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
<Button
onClick={() =>
props.onExpand(false)
}
sx={{
textTransform: "none",
color: "inherit",
width: "100%",
}}
>
<Typography
sx={{
fontWeight: "bold",
...playerTextStyle,
}}
>
{props.track?.name}
</Typography>
</Button>
</Link>
) : (
<Skeleton
animation={false}
width={"70%"}
/>
)}
</Grid>
<Grid item xs={1}>
{
props.track &&
parentSong.data &&
props.artist ? (
<ReleaseTrackContextualMenu
artist={props.artist}
track={{
...props.track,
song: parentSong.data,
}}
onSelect={() =>
props.onExpand(false)
}
/>
) : (
<IconButton>
<ContextualMenuIcon />
</IconButton>
)
// To avoid slight shift on loaded
}
</Grid>
</Grid>
<Box
sx={{
display: "flex",
justifyContent: "center",
}}
>
{props.track && props.artist ? (
{props.artist && props.track ? (
<Link
href={`/artists/${props.artist.slug}`}
href={`/releases/${props.track.releaseId}`}
style={{
overflow: "hidden",
textOverflow: "ellipsis",
@@ -496,57 +431,116 @@ const ExpandedPlayerControls = (
}}
>
<Typography
sx={{ ...playerTextStyle }}
sx={{
fontWeight: "bold",
...playerTextStyle,
}}
>
{formatArtists(
props.artist,
parentSong.data?.featuring,
)}
{props.track?.name}
</Typography>
</Button>
</Link>
) : (
<Skeleton
animation={false}
sx={{ margin: 1 }}
width={"50%"}
/>
<Skeleton animation={false} width={"70%"} />
)}
</Box>
<Stack
spacing={2}
sx={{
justifyContent: "center",
display: "flex",
}}
direction="row"
>
<PreviousButton onClick={props.onRewind} />
<PlayButton
onPause={props.onPause}
onPlay={props.onPlay}
isPlaying={props.playing}
</Grid>
<Grid item xs={1}>
{
props.track &&
parentSong.data &&
props.artist ? (
<ReleaseTrackContextualMenu
artist={props.artist}
track={{
...props.track,
song: parentSong.data,
}}
onSelect={() =>
props.onExpand(false)
}
/>
) : (
<IconButton>
<ContextualMenuIcon />
</IconButton>
)
// To avoid slight shift on loaded
}
</Grid>
</Grid>
<Box
sx={{
display: "flex",
justifyContent: "center",
}}
>
{props.track && props.artist ? (
<Link
href={`/artists/${props.artist.slug}`}
style={{
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
<Button
onClick={() => props.onExpand(false)}
sx={{
textTransform: "none",
color: "inherit",
width: "100%",
}}
>
<Typography sx={{ ...playerTextStyle }}>
{formatArtists(
props.artist,
parentSong.data?.featuring,
)}
</Typography>
</Button>
</Link>
) : (
<Skeleton
animation={false}
sx={{ margin: 1 }}
width={"50%"}
/>
<SkipButton onClick={props.onSkipTrack} />
</Stack>
<Container maxWidth={false}>
<PlayerSlider
onSlide={props.onSlide}
duration={props.duration}
progress={props.progress}
/>
</Container>
)}
</Box>
<Stack
spacing={2}
sx={{
justifyContent: "center",
display: "flex",
}}
direction="row"
>
<PreviousButton onClick={props.onRewind} />
<PlayButton
onPause={props.onPause}
onPlay={props.onPlay}
isPlaying={props.playing}
/>
<SkipButton onClick={props.onSkipTrack} />
</Stack>
</Grid>
<Container maxWidth={false}>
<PlayerSlider
onSlide={props.onSlide}
duration={props.duration}
progress={props.progress}
/>
</Container>
</Stack>
</Grid>
)}
</Grid>
{selectedTab == "lyrics" && (
<Box
sx={{
height: "100%",
width: "100%",
overflowY: "scroll",
margin: 1,
marginLeft: 2,
paddingRight: 3,
alignSelf: "center",
maxWidth: theme.breakpoints.values.md,
}}
+22 -13
View File
@@ -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",
+1 -1
View File
@@ -339,7 +339,7 @@ const Scaffold = (props: { children: any }) => {
}, [props.children]);
return (
<Box sx={{ display: "flex", width: "100%", height: "100vh" }}>
<Box sx={{ display: "flex", width: "100%", height: "100svh" }}>
<RootGradientBackground />
<Drawer
openBottomDrawer={tempDrawerIsOpen}
@@ -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>
);
};
+1 -1
View File
@@ -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
+8 -1
View File
@@ -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"
}
+8 -1
View File
@@ -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"
}
+3 -2
View File
@@ -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;
+13 -7
View File
@@ -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,
+30 -32
View File
@@ -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],
@@ -545,7 +537,6 @@ const ReleasePage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({ props }) => {
<Box>
<Grid
container
rowSpacing={1.5}
spacing={1}
sx={{ alignItems: "center" }}
>
@@ -563,27 +554,34 @@ const ReleasePage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({ props }) => {
{(
albumGenres.data?.pages.at(0)
?.items ?? generateArray(3)
).map((genre, index) => (
<Grid
item
key={index}
sx={{ display: "flex" }}
>
<GenreButton
genre={genre}
sx={{
borderColor:
accentColor?.light,
[theme.getColorSchemeSelector(
"dark",
)]: {
)
.sort(
(a, b) =>
a.slug.length -
b.slug.length,
)
.slice(0, 10)
.map((genre, index) => (
<Grid
item
key={index}
sx={{ display: "flex" }}
>
<GenreButton
genre={genre}
sx={{
borderColor:
accentColor?.dark,
},
}}
/>
</Grid>
)) ?? []}
accentColor?.light,
[theme.getColorSchemeSelector(
"dark",
)]: {
borderColor:
accentColor?.dark,
},
}}
/>
</Grid>
)) ?? []}
</Grid>
<Divider
sx={{
@@ -602,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.",
+7 -2
View File
@@ -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,
);
});
+3
View File
@@ -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) {}
+15 -1
View File
@@ -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")
+161 -8
View File
@@ -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,
},
},
},
],
},
],
},
+1 -1
View File
@@ -77,7 +77,7 @@ export class StreamService {
const proxy = createProxyMiddleware({
router: () =>
`${this.transcoderUrl}/${Buffer.from(fullFilePath).toString(
"base64",
"base64url",
)}/${route}`,
ignorePath: true,
});
-1
View File
@@ -8,7 +8,6 @@
"useExternalProviderGenres": true
},
"providers": {
"genius": {},
"musicbrainz": {}
},
"compilations": {