Skip to content

Commit

Permalink
feat: Add syncLikedSongs
Browse files Browse the repository at this point in the history
  • Loading branch information
M2K3K5 committed Sep 1, 2024
1 parent 10c73de commit 55dc195
Show file tree
Hide file tree
Showing 16 changed files with 432 additions and 29 deletions.
9 changes: 9 additions & 0 deletions apps/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import CollaborativeArtists from "./scenes/Collaborative/Affinity/Artists";
import "./App.css";
import RegistrationsDisabled from "./scenes/Error/RegistrationsDisabled";
import Affinity from "./scenes/Collaborative/Affinity";
import LikedSongs from "./scenes/Collaborative/LikedSongs";
import { useTheme } from "./services/theme";
import { selectDarkMode } from "./services/redux/modules/user/selector";
import PlaylistDialog from "./components/PlaylistDialog";
Expand Down Expand Up @@ -119,6 +120,14 @@ function App() {
</PrivateRoute>
}
/>
<Route
path="/collaborative/liked-songs"
element={
<PrivateRoute>
<LikedSongs />
</PrivateRoute>
}
/>
<Route
path="/collaborative/top/songs/:mode"
element={
Expand Down
11 changes: 10 additions & 1 deletion apps/client/src/components/Layout/Sider/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
ShareOutlined,
Speed,
SpeedOutlined,
OfflineShare,
OfflineShareOutlined,
} from "@mui/icons-material";

export interface SiderLink {
Expand Down Expand Up @@ -80,7 +82,7 @@ export const links: SiderCategory[] = [
],
},
{
label: "With people",
label: "Social",
items: [
{
label: "Affinity",
Expand All @@ -89,6 +91,13 @@ export const links: SiderCategory[] = [
iconOn: <MusicNote />,
restrict: "guest",
},
{
label: "Share liked songs",
link: "/collaborative/liked-songs",
icon: <OfflineShareOutlined />,
iconOn: <OfflineShare />,
restrict: "guest",
},
],
},
{
Expand Down
134 changes: 134 additions & 0 deletions apps/client/src/scenes/Collaborative/LikedSongs/LikedSongs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useState, useEffect } from "react";
import Header from "../../../components/Header";
import Text from "../../../components/Text";
import s from "./index.module.css";
import { Button } from "@mui/material";
import { useAppDispatch } from "../../../services/redux/tools";
import { setSyncLikedSongs } from "../../../services/redux/modules/user/thunk";
import { alertMessage } from "../../../services/redux/modules/message/reducer";
import { selectUser } from "../../../services/redux/modules/user/selector";
import { useSelector } from "react-redux";
import { SyncLikedSongsResponse } from "../../../services/redux/modules/user/types";
import { useAPI } from "../../../services/hooks/hooks";
import { api } from "../../../services/apis/api";

export default function LikedSongs() {
const user = useSelector(selectUser);
const [likedSongsPlaylistId, setLikedSongsPlaylistId] = useState<string>("");
const [syncEnabled, setSyncEnabled] = useState<boolean>(false);
const [likedSongsLoading, setLoadingLikedSongs] = useState<boolean>(true);
const [playlistsLoading, setLoadingPlaylist] = useState<boolean>(true);
const [showPlaylistButton, setShowPlaylistButton] = useState<boolean>(false);
const dispatch = useAppDispatch();
const playlists = useAPI(api.getPlaylists)

if (!user) {
return null;
}

useEffect(() => {
const fetchInitialState = async () => {
setLoadingLikedSongs(true);
try {
setSyncEnabled(user.syncLikedSongs);
setShowPlaylistButton(user.syncLikedSongs);

if (playlists != null) {
let result = await playlists.find((playlist: any) => playlist.name === "Liked songs • " + user.username)?.id;
if (result != undefined) {
setLikedSongsPlaylistId(result);
}
}
setLoadingPlaylist(false);
} catch (error) {
console.error("Failed to fetch initial state:", error);
showRequestError();
} finally {
setLoadingLikedSongs(false);
}
};

fetchInitialState();
}, [user, playlists]);

const toggleSync = async () => {
if (likedSongsLoading) return;

setLoadingLikedSongs(true);
const newSyncEnabled = !syncEnabled;

try {
let result = (await dispatch(setSyncLikedSongs(newSyncEnabled))).payload as SyncLikedSongsResponse;
if (result.success) {
setSyncEnabled(newSyncEnabled);
setShowPlaylistButton(newSyncEnabled);

if (newSyncEnabled) {
setLikedSongsPlaylistId(result.playlistId);
} else {
setLikedSongsPlaylistId("");
}
}
} catch (error) {
console.error("Failed to update syncLikedSongs state:", error);
showRequestError();
} finally {
setLoadingLikedSongs(false);
}
};

const openPlaylist = async () => {
if (likedSongsPlaylistId === "") {
if (playlists != null) {
if (playlists.length == 0) {
showRequestError("No playlists found");
}
} else {
showRequestError();
}
return;
}

const url = `https://open.spotify.com/playlist/${likedSongsPlaylistId}`;
window.open(url, "_blank");
};

const showRequestError = (msg: string = "The web application could not communicate with the server") => {
dispatch(alertMessage({
level: "error",
message: msg,
}));
}

return (
<div className={s.root}>
<Header
hideInterval
title={<div className={s.title}>Share liked songs</div>}
subtitle="Automatically syncs your liked songs into a shareable playlist every night"
/>
<div className={s.content}>
<Text>Sync your liked songs:</Text>
<Button
className={s.syncButton}
variant="contained"
onClick={toggleSync}
disabled={likedSongsLoading} // Disable button while setLikedSongsLoading
>
{syncEnabled ? "On" : "Off"}
</Button>
<br></br>
{showPlaylistButton && (
<Button
className={s.playlistButton}
variant="contained"
onClick={openPlaylist}
disabled={playlistsLoading}
>
Open playlist
</Button>
)}
</div>
</div>
);
}
21 changes: 21 additions & 0 deletions apps/client/src/scenes/Collaborative/LikedSongs/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.root {
margin: auto;
height: 100vh;
display: flex;
flex-direction: column;
}

.content {
margin-left: 25px;
}

.title {
display: flex;
flex-direction: row;
align-items: center;
}

.syncButton {
width: fit-content;
margin-left: 20px !important;
}
1 change: 1 addition & 0 deletions apps/client/src/scenes/Collaborative/LikedSongs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./LikedSongs";
6 changes: 5 additions & 1 deletion apps/client/src/services/apis/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Axios from "axios";
import { AdminAccount } from "../redux/modules/admin/reducer";
import { ImporterState } from "../redux/modules/import/types";
import { Playlist, PlaylistContext } from "../redux/modules/playlist/types";
import { User } from "../redux/modules/user/types";
import { SyncLikedSongsResponse, User } from "../redux/modules/user/types";
import {
Album,
Artist,
Expand Down Expand Up @@ -549,6 +549,10 @@ export const api = {
};
}[]
>("/spotify/top/sessions", { start, end }),
setSyncLikedSongs: (status: boolean) =>
post<SyncLikedSongsResponse>("/spotify/sync-liked-songs", {
status,
}),
};

export const DEFAULT_ITEMS_TO_LOAD = 20;
Expand Down
22 changes: 21 additions & 1 deletion apps/client/src/services/redux/modules/user/thunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DateFormatter } from "../../../date";
import { myAsyncThunk } from "../../tools";
import { alertMessage } from "../message/reducer";
import { selectIsPublic } from "./selector";
import { DarkModeType, User } from "./types";
import { DarkModeType, SyncLikedSongsResponse, User } from "./types";

export const checkLogged = myAsyncThunk<User | null, void>(
"@user/checklogged",
Expand Down Expand Up @@ -181,3 +181,23 @@ export const unblacklistArtist = myAsyncThunk<void, string>(
}
},
);

export const setSyncLikedSongs = myAsyncThunk<SyncLikedSongsResponse, boolean>(
"@user/set-sync-liked-songs",
async (status, tapi) => {
try {
const resp: SyncLikedSongsResponse = (await api.setSyncLikedSongs(status)).data;
await tapi.dispatch(checkLogged());
return resp;
} catch (e) {
console.error(e);
tapi.dispatch(
alertMessage({
level: "error",
message: `Could not update sync liked songs to ${status}`,
}),
);
return { success: false, playlistId: "" } as SyncLikedSongsResponse;
}
},
);
6 changes: 6 additions & 0 deletions apps/client/src/services/redux/modules/user/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export interface User {
publicToken: string | null;
firstListenedAt: string;
isGuest: boolean;
syncLikedSongs: boolean;
}

export interface SyncLikedSongsResponse {
success: boolean;
playlistId: string;
}

export interface ReduxPresetIntervalDetail {
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/database/queries/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const createUser = (
metricUsed: "number",
dateFormat: "default",
},
syncLikedSongs: false,
});

export const storeInUser = <F extends keyof User>(
Expand Down
30 changes: 30 additions & 0 deletions apps/server/src/database/schemas/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,36 @@ export interface RecentlyPlayedTrack {
track: SpotifyTrack;
}

export interface PlaylistTrack {
added_at: string;
added_by: AddedBy;
is_local: boolean;
track: SpotifyTrack;
}

export interface AddedBy {
external_urls: ExternalUrls;
followers: Followers;
href: string;
id: string;
type: string;
uri: string;
}

export interface ExternalUrls {
spotify: string;
}

export interface Followers {
href: string;
total: number;
}

export interface SavedTrack {
added_at: string;
track: SpotifyTrack;
}

export const TrackSchema = new Schema<Track>(
{
album: { type: String, index: true }, // Id of the album
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/database/schemas/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface User {
lastImport: string | null;
publicToken: string | null;
firstListenedAt?: Date;
syncLikedSongs: boolean;
}

export const UserSchema = new Schema<User>(
Expand Down Expand Up @@ -63,6 +64,7 @@ export const UserSchema = new Schema<User>(
lastImport: { type: String, default: null },
publicToken: { type: String, default: null, index: true },
firstListenedAt: { type: Date },
syncLikedSongs: { type: Boolean, default: false },
},
{ toJSON: { virtuals: true }, toObject: { virtuals: true } },
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { UserModel } from "../database/Models";
import { startMigration } from "../tools/migrations";

export async function up() {
startMigration("add syncLikedSongs to user");

for await (const user of UserModel.find()) {
if (user.syncLikedSongs !== true) {
await UserModel.updateOne(
{ _id: user._id },
{ $set: { syncLikedSongs: false } }
);
}
}
}

export async function down() {}
Loading

0 comments on commit 55dc195

Please sign in to comment.