diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml deleted file mode 100644 index 796fe5a..0000000 --- a/.github/workflows/lint-format.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Lint and Format - -on: - push: - branches: - - '**' - -jobs: - lint_format: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - token: ${{ secrets.GH_TOKEN }} - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - run: npm ci - - run: npm run format - - name: Commit and push formatting changes - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git diff --staged --quiet || git commit -m "style: auto-format code" - git push - - run: npm run lint - continue-on-error: true diff --git a/README.md b/README.md index 4b1df6e..7896204 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@
- stable · beta + web · beta · iOS
> Need Help? Join our [Development Server](https://discord.gg/Wxrp73HVj4) @@ -167,10 +167,10 @@ You can view our privacy policy [here](https://maple.music/privacy)! ## Credits -### Lead & Only Developer +### Lead Developer - Cattn -### Logos & Branding +### Logos & Various Help -- Nailington +- Nailington ([@Nailington](https://github.com/Nailington)) diff --git a/api/maple.js b/api/maple.js index b904f82..e957274 100644 --- a/api/maple.js +++ b/api/maple.js @@ -54,9 +54,9 @@ var options = { } }; -const limiter = slowDown({ +const slower = slowDown({ windowMs: 2 * 60 * 1000, - delayAfter: 5, + delayAfter: 50, delayMs: (hits) => { if (hits <= 15) return hits * 100; return (hits - 15) * 1000 + 2000; @@ -64,10 +64,10 @@ const limiter = slowDown({ maxDelayMs: 15000, }); -// const limiter = rateLimit({ -// windowMs: 15 * 60 * 1000, -// limit: 20, -// }) +const limiter = rateLimit({ + windowMs: 2 * 60 * 1000, + limit: 100, +}) try { @@ -96,6 +96,7 @@ try { const friends = require('./user/friends.js'); app.use(limiter); + app.use(slower); app.use(cors(corsOptions)); diff --git a/api/package-lock.json b/api/package-lock.json index dfefad8..897f0b5 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -18,13 +18,13 @@ "express-slow-down": "^3.0.1", "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", - "multer": "^1.4.5-lts.1", + "multer": "^2.0.2", "mysql": "^2.18.1", "mysql2": "^3.12.0", "peer": "^1.0.2", "socket.io": "^4.8.1", "uuid": "^11.0.5", - "validator": "^13.12.0" + "validator": "^13.15.23" } }, "node_modules/@mapbox/node-pre-gyp": { @@ -449,50 +449,20 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "engines": [ - "node >= 0.8" + "node >= 6.0" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/concat-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -806,7 +776,6 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -896,12 +865,13 @@ } }, "node_modules/express-validator": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", - "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", + "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", + "license": "MIT", "dependencies": { "lodash": "^4.17.21", - "validator": "~13.12.0" + "validator": "~13.15.23" }, "engines": { "node": ">= 8.0.0" @@ -1516,21 +1486,21 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/multer": { - "version": "1.4.5-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" + "type-is": "^1.6.18", + "xtend": "^4.0.2" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 10.16.0" } }, "node_modules/multer/node_modules/mkdirp": { @@ -2334,9 +2304,10 @@ } }, "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", "engines": { "node": ">= 0.10" } diff --git a/api/package.json b/api/package.json index d16c514..8edac9d 100644 --- a/api/package.json +++ b/api/package.json @@ -29,12 +29,12 @@ "express-slow-down": "^3.0.1", "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", - "multer": "^1.4.5-lts.1", + "multer": "^2.0.2", "mysql": "^2.18.1", "mysql2": "^3.12.0", "peer": "^1.0.2", "socket.io": "^4.8.1", "uuid": "^11.0.5", - "validator": "^13.12.0" + "validator": "^13.15.23" } } diff --git a/src/lib/api/UserManager.ts b/src/lib/api/UserManager.ts index 9a01899..a97a96e 100644 --- a/src/lib/api/UserManager.ts +++ b/src/lib/api/UserManager.ts @@ -1,5 +1,6 @@ import { SERVER } from '$lib/api/server'; -import { SavedUser, UserInfo } from '$lib/store'; +import { refreshFriends, refreshRequests } from '$lib/refreshFriends'; +import { friendNowPlaying, SavedUser, UserInfo } from '$lib/store'; import type { User } from '$lib/types'; import { get } from 'svelte/store'; @@ -85,4 +86,248 @@ export class UserManager { return null; } }; + + public static updateProfilePicture = async (file: File) => { + try { + const userId = get(UserInfo)?.id; + if (!userId) return { error: 'Not logged in' }; + + const formData = new FormData(); + formData.append('pfp', file); + + const response = await fetch(`${SERVER}/user/manage/setProfile/${userId}`, { + credentials: 'include', + method: 'POST', + body: formData + }); + + if (response.ok) { + await this.getUser(); + return { success: true }; + } + const data = await response.json(); + return { error: data.error || 'Failed to update profile picture' }; + } catch (e) { + console.error('Error:', e); + return { error: 'Failed to update profile picture' }; + } + }; + + public static updateDisplayName = async (displayName: string) => { + try { + const userId = get(UserInfo)?.id; + if (!userId) return { error: 'Not logged in' }; + + const response = await fetch(`${SERVER}/user/manage/setDisplayName/${userId}`, { + credentials: 'include', + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ displayName }) + }); + + if (response.ok) { + await this.getUser(); + return { success: true }; + } + const data = await response.json(); + return { error: data.error || 'Failed to update display name' }; + } catch (e) { + console.error('Error:', e); + return { error: 'Failed to update display name' }; + } + }; + + public static getRequests = async () => { + try { + const userId = get(UserInfo)?.id; + if (!userId) return { error: 'Not logged in' }; + + const response = await fetch(`${SERVER}/user/friends/get/requests/${userId}`, { + credentials: 'include', + method: 'GET' + }); + const data = await response.json(); + if (response.ok) { + return data; + } + } catch (error) { + return console.error('Error:', error); + } + }; + + public static getFriends = async () => { + try { + const userId = get(UserInfo)?.id; + if (!userId) return { error: 'Not logged in' }; + + const response = await fetch(`${SERVER}/user/friends/get/friends/${userId}`, { + credentials: 'include', + method: 'GET' + }); + const data = await response.json(); + if (response.ok) { + return data; + } + } catch (error) { + return console.error('Error:', error); + } + }; + + public static removeFriend = async (id: string) => { + try { + const userId = get(UserInfo)?.id; + if (!userId) return { error: 'Not logged in' }; + + const response = await fetch(`${SERVER}/user/friends/remove/${userId}`, { + credentials: 'include', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ friendId: id }) + }); + const data = await response.json(); + if (response.ok) { + refreshFriends(); + return data; + } + } catch (error) { + return console.error('Error:', error); + } + }; + + public static getUserbyId = async (id: string) => { + try { + const response = await fetch(`${SERVER}/public/get/user/id/${id}`, { + credentials: 'include', + method: 'GET' + }); + const data = await response.json(); + if (response.ok) { + if (data.nowPlaying) { + friendNowPlaying.set(data.nowPlaying); + } + return data; + } else { + return; + } + } catch (error) { + return console.error('Error:', error); + } + }; + + public static getUserName = async (username: string) => { + try { + const response = await fetch(`${SERVER}/public/get/user/${username}`, { + credentials: 'include', + method: 'GET' + }); + const data = await response.json(); + if (response.ok) { + return data; + } else { + return; + } + } catch (error) { + return console.error('Error:', error); + } + }; + + public static acceptRequest = async (id: string) => { + try { + const userId = get(UserInfo)?.id; + if (!userId) return { error: 'Not logged in' }; + + const response = await fetch(`${SERVER}/user/friends/accept/${userId}`, { + credentials: 'include', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ friendId: id }) + }); + const data = await response.json(); + if (response.ok) { + return data; + } + } catch (error) { + return console.error('Error:', error); + } + }; + + public static rejectRequest = async (id: string) => { + try { + const userId = get(UserInfo)?.id; + if (!userId) return { error: 'Not logged in' }; + + const response = await fetch(`${SERVER}/user/friends/decline/${userId}`, { + credentials: 'include', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ friendId: id }) + }); + const data = await response.json(); + if (response.ok) { + refreshRequests(); + refreshFriends(); + return data; + } + } catch (error) { + return console.error('Error:', error); + } + }; + + public static addFriend = async (id: string) => { + try { + const userId = get(UserInfo)?.id; + if (!userId) return { error: 'Not logged in' }; + + const response = await fetch(`${SERVER}/user/friends/add/${userId}`, { + credentials: 'include', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ friendId: id }) + }); + const data = await response.json(); + if (response.ok) { + return data; + } + } catch (error) { + return console.error('Error:', error); + } + }; + + public static setAlbumArt = async (file: File) => { + try { + const userId = get(UserInfo)?.id; + if (!userId) return { error: 'Not logged in' }; + + const formData = new FormData(); + formData.append('albumArt', file); + + const response = await fetch(`${SERVER}/user/manage/setAlbumArt/${userId}`, { + credentials: 'include', + method: 'POST', + body: formData + }); + + if (response.ok) { + return { success: true }; + } + const data = await response.json(); + return { error: data.error || 'Failed to update album art' }; + } catch (e) { + console.error('Error:', e); + return { error: 'Failed to update album art' }; + } + }; + + public static isLoggedIn = async () => { + const userId = get(UserInfo)?.id; + return !!userId; + }; } diff --git a/src/lib/api/server.ts b/src/lib/api/server.ts index db4e984..aadd6cf 100644 --- a/src/lib/api/server.ts +++ b/src/lib/api/server.ts @@ -1 +1 @@ -export const SERVER = 'http://localhost:3000'; +export const SERVER = 'https://api.maple.music'; diff --git a/src/lib/components/BottomBar.svelte b/src/lib/components/BottomBar.svelte index c0a476c..2fb6f07 100644 --- a/src/lib/components/BottomBar.svelte +++ b/src/lib/components/BottomBar.svelte @@ -19,6 +19,8 @@ } from '$lib/player'; import { OPFS } from '$lib/opfs'; import { browser } from '$app/environment'; + import { page } from '$app/state'; + import Queue from './Queue.svelte'; let artwork = $state(null as string | null); let progress = $state(0); @@ -29,6 +31,12 @@ let fillPercent = $state(0); let trackEl: HTMLDivElement | null = null; let dragging = $state(false); + let queueOpen = $state(false); + + $effect(() => { + page.url.pathname; + queueOpen = false; + }); $effect(() => { if (!browser) return; @@ -111,6 +119,10 @@ } catch {} dragging = false; } + + function openQueue() { + queueOpen = !queueOpen; + }{$activeSong.title || 'Song Name'}
++ {$activeSong.artist || 'Artist Name'} +
++ {itemsCache.length} + {itemsCache.length === 1 ? 'track' : 'tracks'} +
+{track?.title || playlist?.name || album?.name || artist?.name}
-{track?.artist || '' || album?.artist || ''}
++ {track?.title || playlist?.name || album?.name || artist?.name} +
++ {track?.artist || '' || album?.artist || ''} +